Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • master
  • v1.0.0
  • v1.0.1
  • v1.1.0
  • v1.10.0
  • v1.10.1
  • v1.10.2
  • v1.11.0
  • v1.12.0
  • v1.12.1
  • v1.12.2
  • v1.12.3
  • v1.12.4
  • v1.12.5
  • v1.12.6
  • v1.12.7
  • v1.12.8
  • v1.13.0
  • v1.13.1
  • v1.13.2
  • v1.14.0
  • v1.15.0
  • v1.15.1
  • v1.15.10
  • v1.15.11
  • v1.15.12
  • v1.15.13
  • v1.15.14
  • v1.15.15
  • v1.15.16
  • v1.15.17
  • v1.15.2
  • v1.15.3
  • v1.15.4
  • v1.15.5
  • v1.15.6
  • v1.15.7
  • v1.15.8
  • v1.15.9
  • v1.16.0
  • v1.16.1
  • v1.17.0
  • v1.18.0
  • v1.18.1
  • v1.18.2
  • v1.19.0
  • v1.19.1
  • v1.19.2
  • v1.19.3
  • v1.19.4
  • v1.2.0
  • v1.20.0
  • v1.20.1
  • v1.20.2
  • v1.20.3
  • v1.21.0
  • v1.21.1
  • v1.22.0
  • v1.23.0
  • v1.23.1
  • v1.23.2
  • v1.3.0
  • v1.3.1
  • v1.3.2
  • v1.4.0
  • v1.5.0
  • v1.5.1
  • v1.6.0
  • v1.6.1
  • v1.7.0
  • v1.7.1
  • v1.7.2
  • v1.7.3
  • v1.8.0
  • v1.8.1
  • v1.9.0
76 results

Target

Select target project
No results found
Select Git revision
  • master
  • v1.0.0
  • v1.0.1
  • v1.1.0
  • v1.10.0
  • v1.10.1
  • v1.10.2
  • v1.11.0
  • v1.12.0
  • v1.12.1
  • v1.12.2
  • v1.12.3
  • v1.12.4
  • v1.12.5
  • v1.12.6
  • v1.12.7
  • v1.12.8
  • v1.13.0
  • v1.13.1
  • v1.13.2
  • v1.14.0
  • v1.15.0
  • v1.15.1
  • v1.15.10
  • v1.15.11
  • v1.15.12
  • v1.15.13
  • v1.15.14
  • v1.15.15
  • v1.15.16
  • v1.15.17
  • v1.15.2
  • v1.15.3
  • v1.15.4
  • v1.15.5
  • v1.15.6
  • v1.15.7
  • v1.15.8
  • v1.15.9
  • v1.16.0
  • v1.16.1
  • v1.17.0
  • v1.18.0
  • v1.18.1
  • v1.18.2
  • v1.19.0
  • v1.19.1
  • v1.19.2
  • v1.19.3
  • v1.19.4
  • v1.2.0
  • v1.20.0
  • v1.20.1
  • v1.20.2
  • v1.20.3
  • v1.21.0
  • v1.21.1
  • v1.22.0
  • v1.23.0
  • v1.23.1
  • v1.23.2
  • v1.3.0
  • v1.3.1
  • v1.3.2
  • v1.4.0
  • v1.5.0
  • v1.5.1
  • v1.6.0
  • v1.6.1
  • v1.7.0
  • v1.7.1
  • v1.7.2
  • v1.7.3
  • v1.8.0
  • v1.8.1
  • v1.9.0
76 results
Show changes
69 files
+ 2337
2204
Compare changes
  • Side-by-side
  • Inline

Files

Original line number Diff line number Diff line
@@ -4,9 +4,7 @@
    <option name="autoReloadType" value="SELECTIVE" />
  </component>
  <component name="ChangeListManager">
    <list default="true" id="9979eb22-471e-4f2f-b624-fd3edb5e8c6e" name="Changes" comment="">
      <change beforePath="$PROJECT_DIR$/.gitlab-ci.yml" beforeDir="false" afterPath="$PROJECT_DIR$/.gitlab-ci.yml" afterDir="false" />
    </list>
    <list default="true" id="9979eb22-471e-4f2f-b624-fd3edb5e8c6e" name="Changes" comment="" />
    <option name="SHOW_DIALOG" value="false" />
    <option name="HIGHLIGHT_CONFLICTS" value="true" />
    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@@ -40,61 +38,61 @@
    <option name="sortByType" value="true" />
    <option name="sortKey" value="BY_TYPE" />
  </component>
  <component name="PropertiesComponent"><![CDATA[{
  "keyToString": {
    "DefaultGoTemplateProperty": "Go File",
    "Go Test.TestCreateJobAndSchedulerFromInput in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestDeleteJob in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestDummyRunnable in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestJobPersistenceUnmarshalJSON in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestJobPersistenceUnmarshalJSON/Date_and_Time in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestJobPersistenceUnmarshalJSON/RFC3339_Format in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestJobPersistence_MarshalUnmarshalJSON in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestManagerEventHandling in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestReadJsonFile in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestResetStats in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestRoundTrip in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestScheduleJob in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestSchedulerPersistenceUnmarshalJSON in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestSchedulerPersistenceUnmarshalJSON/Date_and_Time_with_Space in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestSchedulerPersistenceUnmarshalJSON/Date_and_Time_with_Space_and_Seconds in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestSetAndGetTimeout in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestStructure in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestTimeFunctionSame in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestUnmarshalJSON in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestUnmarshalSchedulerPersistenceIntervalYAML in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestUnmarshalSchedulerPersistenceYAML in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestUnmarshalYAML in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestWorkerLifeCycle in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestWriteToDB1 in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestWriteToDB2 in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.TestWriteToDB4 in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "Go Test.go test gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug",
    "NIXITCH_NIXPKGS_CONFIG": "/etc/nix/nixpkgs-config.nix",
    "NIXITCH_NIX_CONF_DIR": "",
    "NIXITCH_NIX_OTHER_STORES": "",
    "NIXITCH_NIX_PATH": "/home/vs/.nix-defexpr/channels:nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos:nixos-config=/etc/nixos/configuration.nix:/nix/var/nix/profiles/per-user/root/channels",
    "NIXITCH_NIX_PROFILES": "/run/current-system/sw /nix/var/nix/profiles/default /etc/profiles/per-user/vs /home/vs/.local/state/nix/profile /nix/profile /home/vs/.nix-profile",
    "NIXITCH_NIX_REMOTE": "",
    "NIXITCH_NIX_USER_PROFILE_DIR": "/nix/var/nix/profiles/per-user/vs",
    "RunOnceActivity.ShowReadmeOnStart": "true",
    "RunOnceActivity.go.formatter.settings.were.checked": "true",
    "RunOnceActivity.go.migrated.go.modules.settings": "true",
    "RunOnceActivity.go.modules.automatic.dependencies.download": "true",
    "SHARE_PROJECT_CONFIGURATION_FILES": "true",
    "git-widget-placeholder": "master",
    "go.import.settings.migrated": "true",
    "go.sdk.automatically.set": "true",
    "last_opened_file_path": "/home/vs/workspaces/oss/go-libs/job-queues",
    "node.js.detected.package.eslint": "true",
    "node.js.detected.package.tslint": "true",
    "node.js.selected.package.eslint": "(autodetect)",
    "node.js.selected.package.tslint": "(autodetect)",
    "nodejs_package_manager_path": "npm",
    "settings.editor.selected.configurable": "go.sdk",
    "vue.rearranger.settings.migration": "true"
  <component name="PropertiesComponent">{
  &quot;keyToString&quot;: {
    &quot;DefaultGoTemplateProperty&quot;: &quot;Go File&quot;,
    &quot;Go Test.TestCreateJobAndSchedulerFromInput in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestDeleteJob in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestDummyRunnable in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestJobPersistenceUnmarshalJSON in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestJobPersistenceUnmarshalJSON/Date_and_Time in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestJobPersistenceUnmarshalJSON/RFC3339_Format in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestJobPersistence_MarshalUnmarshalJSON in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestManagerEventHandling in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestReadJsonFile in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestResetStats in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestRoundTrip in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestScheduleJob in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestSchedulerPersistenceUnmarshalJSON in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestSchedulerPersistenceUnmarshalJSON/Date_and_Time_with_Space in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestSchedulerPersistenceUnmarshalJSON/Date_and_Time_with_Space_and_Seconds in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestSetAndGetTimeout in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestStructure in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestTimeFunctionSame in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestUnmarshalJSON in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestUnmarshalSchedulerPersistenceIntervalYAML in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestUnmarshalSchedulerPersistenceYAML in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestUnmarshalYAML in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestWorkerLifeCycle in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestWriteToDB1 in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestWriteToDB2 in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.TestWriteToDB4 in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;Go Test.go test gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
    &quot;NIXITCH_NIXPKGS_CONFIG&quot;: &quot;/etc/nix/nixpkgs-config.nix&quot;,
    &quot;NIXITCH_NIX_CONF_DIR&quot;: &quot;&quot;,
    &quot;NIXITCH_NIX_OTHER_STORES&quot;: &quot;&quot;,
    &quot;NIXITCH_NIX_PATH&quot;: &quot;/home/vs/.nix-defexpr/channels:nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos:nixos-config=/etc/nixos/configuration.nix:/nix/var/nix/profiles/per-user/root/channels&quot;,
    &quot;NIXITCH_NIX_PROFILES&quot;: &quot;/run/current-system/sw /nix/var/nix/profiles/default /etc/profiles/per-user/vs /home/vs/.local/state/nix/profile /nix/profile /home/vs/.nix-profile&quot;,
    &quot;NIXITCH_NIX_REMOTE&quot;: &quot;&quot;,
    &quot;NIXITCH_NIX_USER_PROFILE_DIR&quot;: &quot;/nix/var/nix/profiles/per-user/vs&quot;,
    &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
    &quot;RunOnceActivity.go.formatter.settings.were.checked&quot;: &quot;true&quot;,
    &quot;RunOnceActivity.go.migrated.go.modules.settings&quot;: &quot;true&quot;,
    &quot;RunOnceActivity.go.modules.automatic.dependencies.download&quot;: &quot;true&quot;,
    &quot;SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
    &quot;git-widget-placeholder&quot;: &quot;master&quot;,
    &quot;go.import.settings.migrated&quot;: &quot;true&quot;,
    &quot;go.sdk.automatically.set&quot;: &quot;true&quot;,
    &quot;last_opened_file_path&quot;: &quot;/home/vs/workspaces/oss/go-libs/job-queues&quot;,
    &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
    &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
    &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
    &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
    &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
    &quot;settings.editor.selected.configurable&quot;: &quot;go.sdk&quot;,
    &quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
  }
}]]></component>
}</component>
  <component name="RecentsManager">
    <key name="CopyFile.RECENT_KEYS">
      <recent name="$PROJECT_DIR$" />
+3 −1
Original line number Diff line number Diff line
@@ -60,7 +60,9 @@ _ = m.ScheduleJob(job, &scheduler)

## License

This project is licensed under the AGPL-3.0 License - see the [LICENSE.md](LICENSE.md) file for details.
This library is available under the [AGPL-3.0](https://choosealicense.com/licenses/agpl-3.0/) 
license for open-source projects. For commercial use, please reach out to us 
at [sales@schukai.com](mailto:sales@schukai.com).

## Contact

+6 −6
Original line number Diff line number Diff line
@@ -7,14 +7,14 @@ require (
	github.com/DATA-DOG/go-sqlmock v1.5.0
	github.com/docker/docker v24.0.6+incompatible
	github.com/docker/go-connections v0.4.0
	github.com/fsnotify/fsnotify v1.7.0
	github.com/fsnotify/fsnotify v1.8.0
	github.com/google/uuid v1.6.0
	github.com/pkg/sftp v1.13.6
	github.com/pkg/sftp v1.13.7
	github.com/robfig/cron/v3 v3.0.1
	github.com/shirou/gopsutil/v3 v3.24.5
	github.com/stretchr/testify v1.9.0
	go.uber.org/zap v1.27.0
	golang.org/x/crypto v0.27.0
	golang.org/x/crypto v0.28.0
	gopkg.in/yaml.v3 v3.0.1
	gorm.io/driver/mysql v1.5.7
	gorm.io/driver/sqlite v1.5.5
@@ -46,14 +46,14 @@ require (
	github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
	github.com/shoenig/go-m1cpu v0.1.6 // indirect
	github.com/tklauser/go-sysconf v0.3.14 // indirect
	github.com/tklauser/numcpus v0.8.0 // indirect
	github.com/tklauser/numcpus v0.9.0 // indirect
	github.com/yusufpapurcu/wmi v1.2.4 // indirect
	go.uber.org/multierr v1.11.0 // indirect
	golang.org/x/mod v0.17.0 // indirect
	golang.org/x/net v0.25.0 // indirect
	golang.org/x/sync v0.8.0 // indirect
	golang.org/x/sys v0.25.0 // indirect
	golang.org/x/text v0.18.0 // indirect
	golang.org/x/sys v0.26.0 // indirect
	golang.org/x/text v0.19.0 // indirect
	golang.org/x/time v0.3.0 // indirect
	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
)
+27 −0
Original line number Diff line number Diff line
@@ -21,6 +21,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@@ -57,6 +59,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/pkg/sftp v1.13.7 h1:uv+I3nNJvlKZIQGSr8JVQLNHFU9YhhNpvC14Y6KgmSM=
github.com/pkg/sftp v1.13.7/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
@@ -79,6 +83,8 @@ github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZ
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -95,11 +101,15 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -109,12 +119,15 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -127,19 +140,32 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -147,6 +173,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Original line number Diff line number Diff line
freebsd_task:
  name: 'FreeBSD'
  freebsd_instance:
    image_family: freebsd-13-2
    image_family: freebsd-14-1
  install_script:
    - pkg update -f
    - pkg install -y go
@@ -11,3 +11,4 @@ freebsd_task:
    - chown -R cirrus:cirrus .
    - FSNOTIFY_BUFFER=4096 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race    ./...
    -                      sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race    ./...
    - FSNOTIFY_DEBUG=1     sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race -v ./...
+0 −12
Original line number Diff line number Diff line
root = true

[*.go]
indent_style = tab
indent_size = 4
insert_final_newline = true

[*.{yml,yaml}]
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
Original line number Diff line number Diff line
# Changelog

Unreleased
----------
Nothing yet.
1.8.0 2023-10-31
----------------

### Additions

- all: add `FSNOTIFY_DEBUG` to print debug logs to stderr ([#619])

### Changes and fixes

- windows: fix behaviour of `WatchList()` to be consistent with other platforms ([#610])

- kqueue: ignore events with Ident=0 ([#590])

- kqueue: set O_CLOEXEC to prevent passing file descriptors to children ([#617])

- kqueue: emit events as "/path/dir/file" instead of "path/link/file" when watching a symlink ([#625])

- inotify: don't send event for IN_DELETE_SELF when also watching the parent ([#620])

- inotify: fix panic when calling Remove() in a goroutine ([#650])

- fen: allow watching subdirectories of watched directories ([#621])

[#590]: https://github.com/fsnotify/fsnotify/pull/590
[#610]: https://github.com/fsnotify/fsnotify/pull/610
[#617]: https://github.com/fsnotify/fsnotify/pull/617
[#619]: https://github.com/fsnotify/fsnotify/pull/619
[#620]: https://github.com/fsnotify/fsnotify/pull/620
[#621]: https://github.com/fsnotify/fsnotify/pull/621
[#625]: https://github.com/fsnotify/fsnotify/pull/625
[#650]: https://github.com/fsnotify/fsnotify/pull/650

1.7.0 - 2023-10-22
------------------
Original line number Diff line number Diff line
Thank you for your interest in contributing to fsnotify! We try to review and
merge PRs in a reasonable timeframe, but please be aware that:

- To avoid "wasted" work, please discus changes on the issue tracker first. You
- To avoid "wasted" work, please discuss changes on the issue tracker first. You
  can just send PRs, but they may end up being rejected for one reason or the
  other.

@@ -20,6 +20,124 @@ platforms. Testing different platforms locally can be done with something like

Use the `-short` flag to make the "stress test" run faster.

Writing new tests
-----------------
Scripts in the testdata directory allow creating test cases in a "shell-like"
syntax. The basic format is:

    script

    Output:
    desired output

For example:

    # Create a new empty file with some data.
    watch /
    echo data >/file

    Output:
        create  /file
        write   /file

Just create a new file to add a new test; select which tests to run with
`-run TestScript/[path]`.

script
------
The script is a "shell-like" script:

    cmd arg arg

Comments are supported with `#`:

    # Comment
    cmd arg arg  # Comment

All operations are done in a temp directory; a path like "/foo" is rewritten to
"/tmp/TestFoo/foo".

Arguments can be quoted with `"` or `'`; there are no escapes and they're
functionally identical right now, but this may change in the future, so best to
assume shell-like rules.

    touch "/file with spaces"

End-of-line escapes with `\` are not supported.

### Supported commands

    watch path [ops]    # Watch the path, reporting events for it. Nothing is
                        # watched by default. Optionally a list of ops can be
                        # given, as with AddWith(path, WithOps(...)).
    unwatch path        # Stop watching the path.
    watchlist n         # Assert watchlist length.

    stop                # Stop running the script; for debugging.
    debug [yes/no]      # Enable/disable FSNOTIFY_DEBUG (tests are run in
                          parallel by default, so -parallel=1 is probably a good
                          idea).

    touch path
    mkdir [-p] dir
    ln -s target link   # Only ln -s supported.
    mkfifo path
    mknod dev path
    mv src dst
    rm [-r] path
    chmod mode path     # Octal only
    sleep time-in-ms

    cat path            # Read path (does nothing with the data; just reads it).
    echo str >>path     # Append "str" to "path".
    echo str >path      # Truncate "path" and write "str".

    require reason      # Skip the test if "reason" is true; "skip" and
    skip reason         # "require" behave identical; it supports both for
                        # readability. Possible reasons are:
                        #
                        #   always    Always skip this test.
                        #   symlink   Symlinks are supported (requires admin
                        #             permissions on Windows).
                        #   mkfifo    Platform doesn't support FIFO named sockets.
                        #   mknod     Platform doesn't support device nodes.


output
------
After `Output:` the desired output is given; this is indented by convention, but
that's not required.

The format of that is:

    # Comment
    event  path  # Comment

    system:
        event  path
    system2:
        event  path

Every event is one line, and any whitespace between the event and path are
ignored. The path can optionally be surrounded in ". Anything after a "#" is
ignored.

Platform-specific tests can be added after GOOS; for example:

    watch /
    touch /file

    Output:
        # Tested if nothing else matches
        create    /file

        # Windows-specific test.
        windows:
            write  /file

You can specify multiple platforms with a comma (e.g. "windows, linux:").
"kqueue" is a shortcut for all kqueue systems (BSD, macOS).


[goon]: https://github.com/arp242/goon
[Vagrant]: https://www.vagrantup.com/
Original line number Diff line number Diff line
//go:build solaris
// +build solaris

// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh
// FEN backend for illumos (supported) and Solaris (untested, but should work).
//
// See port_create(3c) etc. for docs. https://www.illumos.org/man/3C/port_create

package fsnotify

@@ -12,150 +12,33 @@ import (
	"os"
	"path/filepath"
	"sync"
	"time"

	"github.com/fsnotify/fsnotify/internal"
	"golang.org/x/sys/unix"
)

// Watcher watches a set of paths, delivering events on a channel.
//
// A watcher should not be copied (e.g. pass it by pointer, rather than by
// value).
//
// # Linux notes
//
// When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example:
//
//	fp := os.Open("file")
//	os.Remove("file")        // Triggers Chmod
//	fp.Close()               // Triggers Remove
//
// This is the event that inotify sends, so not much can be changed about this.
//
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
// create is an "instance", and every path you add is a "watch".
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
//
// To increase them you can use sysctl or write the value to the /proc file:
//
//	# Default values on Linux 5.18
//	sysctl fs.inotify.max_user_watches=124983
//	sysctl fs.inotify.max_user_instances=128
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
//	fs.inotify.max_user_watches=124983
//	fs.inotify.max_user_instances=128
//
// Reaching the limit will result in a "no space left on device" or "too many open
// files" error.
//
// # kqueue notes (macOS, BSD)
//
// kqueue requires opening a file descriptor for every file that's being watched;
// so if you're watching a directory with five files then that's six file
// descriptors. You will run in to your system's "max open files" limit faster on
// these platforms.
//
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
// control the maximum number of open files, as well as /etc/login.conf on BSD
// systems.
//
// # Windows notes
//
// Paths can be added as "C:\path\to\dir", but forward slashes
// ("C:/path/to/dir") will also work.
//
// When a watched directory is removed it will always send an event for the
// directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct {
	// Events sends the filesystem change events.
	//
	// fsnotify can send the following events; a "path" here can refer to a
	// file, directory, symbolic link, or special file like a FIFO.
	//
	//   fsnotify.Create    A new path was created; this may be followed by one
	//                      or more Write events if data also gets written to a
	//                      file.
	//
	//   fsnotify.Remove    A path was removed.
	//
	//   fsnotify.Rename    A path was renamed. A rename is always sent with the
	//                      old path as Event.Name, and a Create event will be
	//                      sent with the new name. Renames are only sent for
	//                      paths that are currently watched; e.g. moving an
	//                      unmonitored file into a monitored directory will
	//                      show up as just a Create. Similarly, renaming a file
	//                      to outside a monitored directory will show up as
	//                      only a Rename.
	//
	//   fsnotify.Write     A file or named pipe was written to. A Truncate will
	//                      also trigger a Write. A single "write action"
	//                      initiated by the user may show up as one or multiple
	//                      writes, depending on when the system syncs things to
	//                      disk. For example when compiling a large Go program
	//                      you may get hundreds of Write events, and you may
	//                      want to wait until you've stopped receiving them
	//                      (see the dedup example in cmd/fsnotify).
	//
	//                      Some systems may send Write event for directories
	//                      when the directory content changes.
	//
	//   fsnotify.Chmod     Attributes were changed. On Linux this is also sent
	//                      when a file is removed (or more accurately, when a
	//                      link to an inode is removed). On kqueue it's sent
	//                      when a file is truncated. On Windows it's never
	//                      sent.
type fen struct {
	Events chan Event

	// Errors sends any errors.
	//
	// ErrEventOverflow is used to indicate there are too many events:
	//
	//  - inotify:      There are too many queued events (fs.inotify.max_queued_events sysctl)
	//  - windows:      The buffer size is too small; WithBufferSize() can be used to increase it.
	//  - kqueue, fen:  Not used.
	Errors chan error

	mu      sync.Mutex
	port    *unix.EventPort
	done    chan struct{} // Channel for sending a "quit message" to the reader goroutine
	dirs    map[string]struct{} // Explicitly watched directories
	watches map[string]struct{} // Explicitly watched non-directories
	dirs    map[string]Op // Explicitly watched directories
	watches map[string]Op // Explicitly watched non-directories
}

// NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) {
	return NewBufferedWatcher(0)
func newBackend(ev chan Event, errs chan error) (backend, error) {
	return newBufferedBackend(0, ev, errs)
}

// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) {
	w := &Watcher{
		Events:  make(chan Event, sz),
		Errors:  make(chan error),
		dirs:    make(map[string]struct{}),
		watches: make(map[string]struct{}),
func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
	w := &fen{
		Events:  ev,
		Errors:  errs,
		dirs:    make(map[string]Op),
		watches: make(map[string]Op),
		done:    make(chan struct{}),
	}

@@ -171,27 +54,30 @@ func NewBufferedWatcher(sz uint) (*Watcher, error) {

// sendEvent attempts to send an event to the user, returning true if the event
// was put in the channel successfully and false if the watcher has been closed.
func (w *Watcher) sendEvent(name string, op Op) (sent bool) {
func (w *fen) sendEvent(name string, op Op) (sent bool) {
	select {
	case w.Events <- Event{Name: name, Op: op}:
		return true
	case <-w.done:
		return false
	case w.Events <- Event{Name: name, Op: op}:
		return true
	}
}

// sendError attempts to send an error to the user, returning true if the error
// was put in the channel successfully and false if the watcher has been closed.
func (w *Watcher) sendError(err error) (sent bool) {
	select {
	case w.Errors <- err:
func (w *fen) sendError(err error) (sent bool) {
	if err == nil {
		return true
	}
	select {
	case <-w.done:
		return false
	case w.Errors <- err:
		return true
	}
}

func (w *Watcher) isClosed() bool {
func (w *fen) isClosed() bool {
	select {
	case <-w.done:
		return true
@@ -200,8 +86,7 @@ func (w *Watcher) isClosed() bool {
	}
}

// Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error {
func (w *fen) Close() error {
	// Take the lock used by associateFile to prevent lingering events from
	// being processed after the close
	w.mu.Lock()
@@ -213,60 +98,21 @@ func (w *Watcher) Close() error {
	return w.port.Close()
}

// Add starts monitoring the path for changes.
//
// A path can only be watched once; watching it more than once is a no-op and will
// not return an error. Paths that do not yet exist on the filesystem cannot be
// watched.
//
// A watch will be automatically removed if the watched path is deleted or
// renamed. The exception is the Windows backend, which doesn't remove the
// watcher on renames.
//
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work.
//
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
//
// # Watching files
//
// Watching individual files (rather than directories) is generally not
// recommended as many programs (especially editors) update files atomically: it
// will write to a temporary file which is then moved to to destination,
// overwriting the original (or some variant thereof). The watcher on the
// original file is now lost, as that no longer exists.
//
// The upshot of this is that a power failure or crash won't leave a
// half-written file.
//
// Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
func (w *fen) Add(name string) error { return w.AddWith(name) }

// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
//   - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
//     other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
func (w *fen) AddWith(name string, opts ...addOpt) error {
	if w.isClosed() {
		return ErrClosed
	}
	if w.port.PathIsWatched(name) {
		return nil
	if debug {
		fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s  AddWith(%q)\n",
			time.Now().Format("15:04:05.000000000"), name)
	}

	_ = getOptions(opts...)
	with := getOptions(opts...)
	if !w.xSupports(with.op) {
		return fmt.Errorf("%w: %s", xErrUnsupported, with.op)
	}

	// Currently we resolve symlinks that were explicitly requested to be
	// watched. Otherwise we would use LStat here.
@@ -283,7 +129,7 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error {
		}

		w.mu.Lock()
		w.dirs[name] = struct{}{}
		w.dirs[name] = with.op
		w.mu.Unlock()
		return nil
	}
@@ -294,26 +140,22 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error {
	}

	w.mu.Lock()
	w.watches[name] = struct{}{}
	w.watches[name] = with.op
	w.mu.Unlock()
	return nil
}

// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) Remove(name string) error {
func (w *fen) Remove(name string) error {
	if w.isClosed() {
		return nil
	}
	if !w.port.PathIsWatched(name) {
		return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
	}
	if debug {
		fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s  Remove(%q)\n",
			time.Now().Format("15:04:05.000000000"), name)
	}

	// The user has expressed an intent. Immediately remove this name from
	// whichever watch list it might be in. If it's not in there the delete
@@ -346,7 +188,7 @@ func (w *Watcher) Remove(name string) error {
}

// readEvents contains the main loop that runs in a goroutine watching for events.
func (w *Watcher) readEvents() {
func (w *fen) readEvents() {
	// If this function returns, the watcher has been closed and we can close
	// these channels
	defer func() {
@@ -382,17 +224,19 @@ func (w *Watcher) readEvents() {
				continue
			}

			if debug {
				internal.Debug(pevent.Path, pevent.Events)
			}

			err = w.handleEvent(&pevent)
			if err != nil {
			if !w.sendError(err) {
				return
			}
		}
	}
}
}

func (w *Watcher) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error {
func (w *fen) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error {
	files, err := os.ReadDir(path)
	if err != nil {
		return err
@@ -418,7 +262,7 @@ func (w *Watcher) handleDirectory(path string, stat os.FileInfo, follow bool, ha
// bitmap matches more than one event type (e.g. the file was both modified and
// had the attributes changed between when the association was created and the
// when event was returned)
func (w *Watcher) handleEvent(event *unix.PortEvent) error {
func (w *fen) handleEvent(event *unix.PortEvent) error {
	var (
		events     = event.Events
		path       = event.Path
@@ -510,8 +354,7 @@ func (w *Watcher) handleEvent(event *unix.PortEvent) error {
	}

	if events&unix.FILE_MODIFIED != 0 {
		if fmode.IsDir() {
			if watchedDir {
		if fmode.IsDir() && watchedDir {
			if err := w.updateDirectory(path); err != nil {
				return err
			}
@@ -520,11 +363,6 @@ func (w *Watcher) handleEvent(event *unix.PortEvent) error {
				return nil
			}
		}
		} else {
			if !w.sendEvent(path, Write) {
				return nil
			}
		}
	}
	if events&unix.FILE_ATTRIB != 0 && stat != nil {
		// Only send Chmod if perms changed
@@ -543,7 +381,7 @@ func (w *Watcher) handleEvent(event *unix.PortEvent) error {
	return nil
}

func (w *Watcher) updateDirectory(path string) error {
func (w *fen) updateDirectory(path string) error {
	// The directory was modified, so we must find unwatched entities and watch
	// them. If something was removed from the directory, nothing will happen,
	// as everything else should still be watched.
@@ -563,11 +401,9 @@ func (w *Watcher) updateDirectory(path string) error {
			return err
		}
		err = w.associateFile(path, finfo, false)
		if err != nil {
		if !w.sendError(err) {
			return nil
		}
		}
		if !w.sendEvent(path, Create) {
			return nil
		}
@@ -575,7 +411,7 @@ func (w *Watcher) updateDirectory(path string) error {
	return nil
}

func (w *Watcher) associateFile(path string, stat os.FileInfo, follow bool) error {
func (w *fen) associateFile(path string, stat os.FileInfo, follow bool) error {
	if w.isClosed() {
		return ErrClosed
	}
@@ -593,34 +429,34 @@ func (w *Watcher) associateFile(path string, stat os.FileInfo, follow bool) erro
		// cleared up that discrepancy. The most likely cause is that the event
		// has fired but we haven't processed it yet.
		err := w.port.DissociatePath(path)
		if err != nil && err != unix.ENOENT {
		if err != nil && !errors.Is(err, unix.ENOENT) {
			return err
		}
	}
	// FILE_NOFOLLOW means we watch symlinks themselves rather than their
	// targets.
	events := unix.FILE_MODIFIED | unix.FILE_ATTRIB | unix.FILE_NOFOLLOW
	if follow {
		// We *DO* follow symlinks for explicitly watched entries.
		events = unix.FILE_MODIFIED | unix.FILE_ATTRIB

	var events int
	if !follow {
		// Watch symlinks themselves rather than their targets unless this entry
		// is explicitly watched.
		events |= unix.FILE_NOFOLLOW
	}
	if true { // TODO: implement withOps()
		events |= unix.FILE_MODIFIED
	}
	return w.port.AssociatePath(path, stat,
		events,
		stat.Mode())
	if true {
		events |= unix.FILE_ATTRIB
	}
	return w.port.AssociatePath(path, stat, events, stat.Mode())
}

func (w *Watcher) dissociateFile(path string, stat os.FileInfo, unused bool) error {
func (w *fen) dissociateFile(path string, stat os.FileInfo, unused bool) error {
	if !w.port.PathIsWatched(path) {
		return nil
	}
	return w.port.DissociatePath(path)
}

// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string {
func (w *fen) WatchList() []string {
	if w.isClosed() {
		return nil
	}
@@ -638,3 +474,11 @@ func (w *Watcher) WatchList() []string {

	return entries
}

func (w *fen) xSupports(op Op) bool {
	if op.Has(xUnportableOpen) || op.Has(xUnportableRead) ||
		op.Has(xUnportableCloseWrite) || op.Has(xUnportableCloseRead) {
		return false
	}
	return true
}
Original line number Diff line number Diff line
//go:build appengine || (!darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows)
// +build appengine !darwin,!dragonfly,!freebsd,!openbsd,!linux,!netbsd,!solaris,!windows

// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh

package fsnotify

import "errors"

// Watcher watches a set of paths, delivering events on a channel.
//
// A watcher should not be copied (e.g. pass it by pointer, rather than by
// value).
//
// # Linux notes
//
// When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example:
//
//	fp := os.Open("file")
//	os.Remove("file")        // Triggers Chmod
//	fp.Close()               // Triggers Remove
//
// This is the event that inotify sends, so not much can be changed about this.
//
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
// create is an "instance", and every path you add is a "watch".
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
//
// To increase them you can use sysctl or write the value to the /proc file:
//
//	# Default values on Linux 5.18
//	sysctl fs.inotify.max_user_watches=124983
//	sysctl fs.inotify.max_user_instances=128
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
//	fs.inotify.max_user_watches=124983
//	fs.inotify.max_user_instances=128
//
// Reaching the limit will result in a "no space left on device" or "too many open
// files" error.
//
// # kqueue notes (macOS, BSD)
//
// kqueue requires opening a file descriptor for every file that's being watched;
// so if you're watching a directory with five files then that's six file
// descriptors. You will run in to your system's "max open files" limit faster on
// these platforms.
//
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
// control the maximum number of open files, as well as /etc/login.conf on BSD
// systems.
//
// # Windows notes
//
// Paths can be added as "C:\path\to\dir", but forward slashes
// ("C:/path/to/dir") will also work.
//
// When a watched directory is removed it will always send an event for the
// directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct {
	// Events sends the filesystem change events.
	//
	// fsnotify can send the following events; a "path" here can refer to a
	// file, directory, symbolic link, or special file like a FIFO.
	//
	//   fsnotify.Create    A new path was created; this may be followed by one
	//                      or more Write events if data also gets written to a
	//                      file.
	//
	//   fsnotify.Remove    A path was removed.
	//
	//   fsnotify.Rename    A path was renamed. A rename is always sent with the
	//                      old path as Event.Name, and a Create event will be
	//                      sent with the new name. Renames are only sent for
	//                      paths that are currently watched; e.g. moving an
	//                      unmonitored file into a monitored directory will
	//                      show up as just a Create. Similarly, renaming a file
	//                      to outside a monitored directory will show up as
	//                      only a Rename.
	//
	//   fsnotify.Write     A file or named pipe was written to. A Truncate will
	//                      also trigger a Write. A single "write action"
	//                      initiated by the user may show up as one or multiple
	//                      writes, depending on when the system syncs things to
	//                      disk. For example when compiling a large Go program
	//                      you may get hundreds of Write events, and you may
	//                      want to wait until you've stopped receiving them
	//                      (see the dedup example in cmd/fsnotify).
	//
	//                      Some systems may send Write event for directories
	//                      when the directory content changes.
	//
	//   fsnotify.Chmod     Attributes were changed. On Linux this is also sent
	//                      when a file is removed (or more accurately, when a
	//                      link to an inode is removed). On kqueue it's sent
	//                      when a file is truncated. On Windows it's never
	//                      sent.
type other struct {
	Events chan Event

	// Errors sends any errors.
	//
	// ErrEventOverflow is used to indicate there are too many events:
	//
	//  - inotify:      There are too many queued events (fs.inotify.max_queued_events sysctl)
	//  - windows:      The buffer size is too small; WithBufferSize() can be used to increase it.
	//  - kqueue, fen:  Not used.
	Errors chan error
}

// NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) {
func newBackend(ev chan Event, errs chan error) (backend, error) {
	return nil, errors.New("fsnotify not supported on the current platform")
}

// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) { return NewWatcher() }

// Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error { return nil }

// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string { return nil }

// Add starts monitoring the path for changes.
//
// A path can only be watched once; watching it more than once is a no-op and will
// not return an error. Paths that do not yet exist on the filesystem cannot be
// watched.
//
// A watch will be automatically removed if the watched path is deleted or
// renamed. The exception is the Windows backend, which doesn't remove the
// watcher on renames.
//
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work.
//
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
//
// # Watching files
//
// Watching individual files (rather than directories) is generally not
// recommended as many programs (especially editors) update files atomically: it
// will write to a temporary file which is then moved to to destination,
// overwriting the original (or some variant thereof). The watcher on the
// original file is now lost, as that no longer exists.
//
// The upshot of this is that a power failure or crash won't leave a
// half-written file.
//
// Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
func (w *Watcher) Add(name string) error { return nil }

// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
//   - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
//     other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error { return nil }

// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) Remove(name string) error { return nil }
func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
	return newBackend(ev, errs)
}
func (w *other) Close() error                              { return nil }
func (w *other) WatchList() []string                       { return nil }
func (w *other) Add(name string) error                     { return nil }
func (w *other) AddWith(name string, opts ...addOpt) error { return nil }
func (w *other) Remove(name string) error                  { return nil }
func (w *other) xSupports(op Op) bool                      { return false }
Original line number Diff line number Diff line
//go:build windows
// +build windows

// Windows backend based on ReadDirectoryChangesW()
//
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw
//
// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh

package fsnotify

@@ -19,123 +15,15 @@ import (
	"runtime"
	"strings"
	"sync"
	"time"
	"unsafe"

	"github.com/fsnotify/fsnotify/internal"
	"golang.org/x/sys/windows"
)

// Watcher watches a set of paths, delivering events on a channel.
//
// A watcher should not be copied (e.g. pass it by pointer, rather than by
// value).
//
// # Linux notes
//
// When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example:
//
//	fp := os.Open("file")
//	os.Remove("file")        // Triggers Chmod
//	fp.Close()               // Triggers Remove
//
// This is the event that inotify sends, so not much can be changed about this.
//
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
// create is an "instance", and every path you add is a "watch".
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
//
// To increase them you can use sysctl or write the value to the /proc file:
//
//	# Default values on Linux 5.18
//	sysctl fs.inotify.max_user_watches=124983
//	sysctl fs.inotify.max_user_instances=128
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
//	fs.inotify.max_user_watches=124983
//	fs.inotify.max_user_instances=128
//
// Reaching the limit will result in a "no space left on device" or "too many open
// files" error.
//
// # kqueue notes (macOS, BSD)
//
// kqueue requires opening a file descriptor for every file that's being watched;
// so if you're watching a directory with five files then that's six file
// descriptors. You will run in to your system's "max open files" limit faster on
// these platforms.
//
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
// control the maximum number of open files, as well as /etc/login.conf on BSD
// systems.
//
// # Windows notes
//
// Paths can be added as "C:\path\to\dir", but forward slashes
// ("C:/path/to/dir") will also work.
//
// When a watched directory is removed it will always send an event for the
// directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct {
	// Events sends the filesystem change events.
	//
	// fsnotify can send the following events; a "path" here can refer to a
	// file, directory, symbolic link, or special file like a FIFO.
	//
	//   fsnotify.Create    A new path was created; this may be followed by one
	//                      or more Write events if data also gets written to a
	//                      file.
	//
	//   fsnotify.Remove    A path was removed.
	//
	//   fsnotify.Rename    A path was renamed. A rename is always sent with the
	//                      old path as Event.Name, and a Create event will be
	//                      sent with the new name. Renames are only sent for
	//                      paths that are currently watched; e.g. moving an
	//                      unmonitored file into a monitored directory will
	//                      show up as just a Create. Similarly, renaming a file
	//                      to outside a monitored directory will show up as
	//                      only a Rename.
	//
	//   fsnotify.Write     A file or named pipe was written to. A Truncate will
	//                      also trigger a Write. A single "write action"
	//                      initiated by the user may show up as one or multiple
	//                      writes, depending on when the system syncs things to
	//                      disk. For example when compiling a large Go program
	//                      you may get hundreds of Write events, and you may
	//                      want to wait until you've stopped receiving them
	//                      (see the dedup example in cmd/fsnotify).
	//
	//                      Some systems may send Write event for directories
	//                      when the directory content changes.
	//
	//   fsnotify.Chmod     Attributes were changed. On Linux this is also sent
	//                      when a file is removed (or more accurately, when a
	//                      link to an inode is removed). On kqueue it's sent
	//                      when a file is truncated. On Windows it's never
	//                      sent.
type readDirChangesW struct {
	Events chan Event

	// Errors sends any errors.
	//
	// ErrEventOverflow is used to indicate there are too many events:
	//
	//  - inotify:      There are too many queued events (fs.inotify.max_queued_events sysctl)
	//  - windows:      The buffer size is too small; WithBufferSize() can be used to increase it.
	//  - kqueue, fen:  Not used.
	Errors chan error

	port  windows.Handle // Handle to completion port
@@ -147,48 +35,40 @@ type Watcher struct {
	closed  bool       // Set to true when Close() is first called
}

// NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) {
	return NewBufferedWatcher(50)
func newBackend(ev chan Event, errs chan error) (backend, error) {
	return newBufferedBackend(50, ev, errs)
}

// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) {
func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
	port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0)
	if err != nil {
		return nil, os.NewSyscallError("CreateIoCompletionPort", err)
	}
	w := &Watcher{
	w := &readDirChangesW{
		Events:  ev,
		Errors:  errs,
		port:    port,
		watches: make(watchMap),
		input:   make(chan *input, 1),
		Events:  make(chan Event, sz),
		Errors:  make(chan error),
		quit:    make(chan chan<- error, 1),
	}
	go w.readEvents()
	return w, nil
}

func (w *Watcher) isClosed() bool {
func (w *readDirChangesW) isClosed() bool {
	w.mu.Lock()
	defer w.mu.Unlock()
	return w.closed
}

func (w *Watcher) sendEvent(name string, mask uint64) bool {
func (w *readDirChangesW) sendEvent(name, renamedFrom string, mask uint64) bool {
	if mask == 0 {
		return false
	}

	event := w.newEvent(name, uint32(mask))
	event.renamedFrom = renamedFrom
	select {
	case ch := <-w.quit:
		w.quit <- ch
@@ -198,17 +78,19 @@ func (w *Watcher) sendEvent(name string, mask uint64) bool {
}

// Returns true if the error was sent, or false if watcher is closed.
func (w *Watcher) sendError(err error) bool {
func (w *readDirChangesW) sendError(err error) bool {
	if err == nil {
		return true
	}
	select {
	case w.Errors <- err:
		return true
	case <-w.quit:
	}
		return false
	}
}

// Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error {
func (w *readDirChangesW) Close() error {
	if w.isClosed() {
		return nil
	}
@@ -226,57 +108,21 @@ func (w *Watcher) Close() error {
	return <-ch
}

// Add starts monitoring the path for changes.
//
// A path can only be watched once; watching it more than once is a no-op and will
// not return an error. Paths that do not yet exist on the filesystem cannot be
// watched.
//
// A watch will be automatically removed if the watched path is deleted or
// renamed. The exception is the Windows backend, which doesn't remove the
// watcher on renames.
//
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work.
//
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
//
// # Watching files
//
// Watching individual files (rather than directories) is generally not
// recommended as many programs (especially editors) update files atomically: it
// will write to a temporary file which is then moved to to destination,
// overwriting the original (or some variant thereof). The watcher on the
// original file is now lost, as that no longer exists.
//
// The upshot of this is that a power failure or crash won't leave a
// half-written file.
//
// Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
func (w *readDirChangesW) Add(name string) error { return w.AddWith(name) }

// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
//   - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
//     other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
func (w *readDirChangesW) AddWith(name string, opts ...addOpt) error {
	if w.isClosed() {
		return ErrClosed
	}
	if debug {
		fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s  AddWith(%q)\n",
			time.Now().Format("15:04:05.000000000"), filepath.ToSlash(name))
	}

	with := getOptions(opts...)
	if !w.xSupports(with.op) {
		return fmt.Errorf("%w: %s", xErrUnsupported, with.op)
	}
	if with.bufsize < 4096 {
		return fmt.Errorf("fsnotify.WithBufferSize: buffer size cannot be smaller than 4096 bytes")
	}
@@ -295,18 +141,14 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error {
	return <-in.reply
}

// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) Remove(name string) error {
func (w *readDirChangesW) Remove(name string) error {
	if w.isClosed() {
		return nil
	}
	if debug {
		fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s  Remove(%q)\n",
			time.Now().Format("15:04:05.000000000"), filepath.ToSlash(name))
	}

	in := &input{
		op:    opRemoveWatch,
@@ -320,11 +162,7 @@ func (w *Watcher) Remove(name string) error {
	return <-in.reply
}

// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string {
func (w *readDirChangesW) WatchList() []string {
	if w.isClosed() {
		return nil
	}
@@ -335,9 +173,15 @@ func (w *Watcher) WatchList() []string {
	entries := make([]string, 0, len(w.watches))
	for _, entry := range w.watches {
		for _, watchEntry := range entry {
			for name := range watchEntry.names {
				entries = append(entries, filepath.Join(watchEntry.path, name))
			}
			// the directory itself is being watched
			if watchEntry.mask != 0 {
				entries = append(entries, watchEntry.path)
			}
		}
	}

	return entries
}
@@ -361,7 +205,7 @@ const (
	sysFSIGNORED    = 0x8000
)

func (w *Watcher) newEvent(name string, mask uint32) Event {
func (w *readDirChangesW) newEvent(name string, mask uint32) Event {
	e := Event{Name: name}
	if mask&sysFSCREATE == sysFSCREATE || mask&sysFSMOVEDTO == sysFSMOVEDTO {
		e.Op |= Create
@@ -417,7 +261,7 @@ type (
	watchMap map[uint32]indexMap
)

func (w *Watcher) wakeupReader() error {
func (w *readDirChangesW) wakeupReader() error {
	err := windows.PostQueuedCompletionStatus(w.port, 0, 0, nil)
	if err != nil {
		return os.NewSyscallError("PostQueuedCompletionStatus", err)
@@ -425,7 +269,7 @@ func (w *Watcher) wakeupReader() error {
	return nil
}

func (w *Watcher) getDir(pathname string) (dir string, err error) {
func (w *readDirChangesW) getDir(pathname string) (dir string, err error) {
	attr, err := windows.GetFileAttributes(windows.StringToUTF16Ptr(pathname))
	if err != nil {
		return "", os.NewSyscallError("GetFileAttributes", err)
@@ -439,7 +283,7 @@ func (w *Watcher) getDir(pathname string) (dir string, err error) {
	return
}

func (w *Watcher) getIno(path string) (ino *inode, err error) {
func (w *readDirChangesW) getIno(path string) (ino *inode, err error) {
	h, err := windows.CreateFile(windows.StringToUTF16Ptr(path),
		windows.FILE_LIST_DIRECTORY,
		windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
@@ -482,9 +326,8 @@ func (m watchMap) set(ino *inode, watch *watch) {
}

// Must run within the I/O thread.
func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error {
	//pathname, recurse := recursivePath(pathname)
	recurse := false
func (w *readDirChangesW) addWatch(pathname string, flags uint64, bufsize int) error {
	pathname, recurse := recursivePath(pathname)

	dir, err := w.getDir(pathname)
	if err != nil {
@@ -538,7 +381,7 @@ func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error {
}

// Must run within the I/O thread.
func (w *Watcher) remWatch(pathname string) error {
func (w *readDirChangesW) remWatch(pathname string) error {
	pathname, recurse := recursivePath(pathname)

	dir, err := w.getDir(pathname)
@@ -566,11 +409,11 @@ func (w *Watcher) remWatch(pathname string) error {
		return fmt.Errorf("%w: %s", ErrNonExistentWatch, pathname)
	}
	if pathname == dir {
		w.sendEvent(watch.path, watch.mask&sysFSIGNORED)
		w.sendEvent(watch.path, "", watch.mask&sysFSIGNORED)
		watch.mask = 0
	} else {
		name := filepath.Base(pathname)
		w.sendEvent(filepath.Join(watch.path, name), watch.names[name]&sysFSIGNORED)
		w.sendEvent(filepath.Join(watch.path, name), "", watch.names[name]&sysFSIGNORED)
		delete(watch.names, name)
	}

@@ -578,23 +421,23 @@ func (w *Watcher) remWatch(pathname string) error {
}

// Must run within the I/O thread.
func (w *Watcher) deleteWatch(watch *watch) {
func (w *readDirChangesW) deleteWatch(watch *watch) {
	for name, mask := range watch.names {
		if mask&provisional == 0 {
			w.sendEvent(filepath.Join(watch.path, name), mask&sysFSIGNORED)
			w.sendEvent(filepath.Join(watch.path, name), "", mask&sysFSIGNORED)
		}
		delete(watch.names, name)
	}
	if watch.mask != 0 {
		if watch.mask&provisional == 0 {
			w.sendEvent(watch.path, watch.mask&sysFSIGNORED)
			w.sendEvent(watch.path, "", watch.mask&sysFSIGNORED)
		}
		watch.mask = 0
	}
}

// Must run within the I/O thread.
func (w *Watcher) startRead(watch *watch) error {
func (w *readDirChangesW) startRead(watch *watch) error {
	err := windows.CancelIo(watch.ino.handle)
	if err != nil {
		w.sendError(os.NewSyscallError("CancelIo", err))
@@ -624,7 +467,7 @@ func (w *Watcher) startRead(watch *watch) error {
		err := os.NewSyscallError("ReadDirectoryChanges", rdErr)
		if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 {
			// Watched directory was probably removed
			w.sendEvent(watch.path, watch.mask&sysFSDELETESELF)
			w.sendEvent(watch.path, "", watch.mask&sysFSDELETESELF)
			err = nil
		}
		w.deleteWatch(watch)
@@ -637,7 +480,7 @@ func (w *Watcher) startRead(watch *watch) error {
// readEvents reads from the I/O completion port, converts the
// received events into Event objects and sends them via the Events channel.
// Entry point to the I/O thread.
func (w *Watcher) readEvents() {
func (w *readDirChangesW) readEvents() {
	var (
		n   uint32
		key uintptr
@@ -700,7 +543,7 @@ func (w *Watcher) readEvents() {
			}
		case windows.ERROR_ACCESS_DENIED:
			// Watched directory was probably removed
			w.sendEvent(watch.path, watch.mask&sysFSDELETESELF)
			w.sendEvent(watch.path, "", watch.mask&sysFSDELETESELF)
			w.deleteWatch(watch)
			w.startRead(watch)
			continue
@@ -733,6 +576,10 @@ func (w *Watcher) readEvents() {
			name := windows.UTF16ToString(buf)
			fullname := filepath.Join(watch.path, name)

			if debug {
				internal.Debug(fullname, raw.Action)
			}

			var mask uint64
			switch raw.Action {
			case windows.FILE_ACTION_REMOVED:
@@ -761,21 +608,22 @@ func (w *Watcher) readEvents() {
				}
			}

			sendNameEvent := func() {
				w.sendEvent(fullname, watch.names[name]&mask)
			}
			if raw.Action != windows.FILE_ACTION_RENAMED_NEW_NAME {
				sendNameEvent()
				w.sendEvent(fullname, "", watch.names[name]&mask)
			}
			if raw.Action == windows.FILE_ACTION_REMOVED {
				w.sendEvent(fullname, watch.names[name]&sysFSIGNORED)
				w.sendEvent(fullname, "", watch.names[name]&sysFSIGNORED)
				delete(watch.names, name)
			}

			w.sendEvent(fullname, watch.mask&w.toFSnotifyFlags(raw.Action))
			if watch.rename != "" && raw.Action == windows.FILE_ACTION_RENAMED_NEW_NAME {
				w.sendEvent(fullname, filepath.Join(watch.path, watch.rename), watch.mask&w.toFSnotifyFlags(raw.Action))
			} else {
				w.sendEvent(fullname, "", watch.mask&w.toFSnotifyFlags(raw.Action))
			}

			if raw.Action == windows.FILE_ACTION_RENAMED_NEW_NAME {
				fullname = filepath.Join(watch.path, watch.rename)
				sendNameEvent()
				w.sendEvent(filepath.Join(watch.path, watch.rename), "", watch.names[name]&mask)
			}

			// Move to the next event in the buffer
@@ -787,8 +635,7 @@ func (w *Watcher) readEvents() {
			// Error!
			if offset >= n {
				//lint:ignore ST1005 Windows should be capitalized
				w.sendError(errors.New(
					"Windows system assumed buffer larger than it is, events have likely been missed"))
				w.sendError(errors.New("Windows system assumed buffer larger than it is, events have likely been missed"))
				break
			}
		}
@@ -799,7 +646,7 @@ func (w *Watcher) readEvents() {
	}
}

func (w *Watcher) toWindowsFlags(mask uint64) uint32 {
func (w *readDirChangesW) toWindowsFlags(mask uint64) uint32 {
	var m uint32
	if mask&sysFSMODIFY != 0 {
		m |= windows.FILE_NOTIFY_CHANGE_LAST_WRITE
@@ -810,7 +657,7 @@ func (w *Watcher) toWindowsFlags(mask uint64) uint32 {
	return m
}

func (w *Watcher) toFSnotifyFlags(action uint32) uint64 {
func (w *readDirChangesW) toFSnotifyFlags(action uint32) uint64 {
	switch action {
	case windows.FILE_ACTION_ADDED:
		return sysFSCREATE
@@ -825,3 +672,11 @@ func (w *Watcher) toFSnotifyFlags(action uint32) uint64 {
	}
	return 0
}

func (w *readDirChangesW) xSupports(op Op) bool {
	if op.Has(xUnportableOpen) || op.Has(xUnportableRead) ||
		op.Has(xUnportableCloseWrite) || op.Has(xUnportableCloseRead) {
		return false
	}
	return true
}
Original line number Diff line number Diff line
@@ -3,19 +3,146 @@
//
// Currently supported systems:
//
//	Linux 2.6.32+    via inotify
//	BSD, macOS       via kqueue
//	Windows          via ReadDirectoryChangesW
//	illumos          via FEN
//   - Linux      via inotify
//   - BSD, macOS via kqueue
//   - Windows    via ReadDirectoryChangesW
//   - illumos    via FEN
//
// # FSNOTIFY_DEBUG
//
// Set the FSNOTIFY_DEBUG environment variable to "1" to print debug messages to
// stderr. This can be useful to track down some problems, especially in cases
// where fsnotify is used as an indirect dependency.
//
// Every event will be printed as soon as there's something useful to print,
// with as little processing from fsnotify.
//
// Example output:
//
//	FSNOTIFY_DEBUG: 11:34:23.633087586   256:IN_CREATE            → "/tmp/file-1"
//	FSNOTIFY_DEBUG: 11:34:23.633202319     4:IN_ATTRIB            → "/tmp/file-1"
//	FSNOTIFY_DEBUG: 11:34:28.989728764   512:IN_DELETE            → "/tmp/file-1"
package fsnotify

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

// Watcher watches a set of paths, delivering events on a channel.
//
// A watcher should not be copied (e.g. pass it by pointer, rather than by
// value).
//
// # Linux notes
//
// When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example:
//
//	fp := os.Open("file")
//	os.Remove("file")        // Triggers Chmod
//	fp.Close()               // Triggers Remove
//
// This is the event that inotify sends, so not much can be changed about this.
//
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
// create is an "instance", and every path you add is a "watch".
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
//
// To increase them you can use sysctl or write the value to the /proc file:
//
//	# Default values on Linux 5.18
//	sysctl fs.inotify.max_user_watches=124983
//	sysctl fs.inotify.max_user_instances=128
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
//	fs.inotify.max_user_watches=124983
//	fs.inotify.max_user_instances=128
//
// Reaching the limit will result in a "no space left on device" or "too many open
// files" error.
//
// # kqueue notes (macOS, BSD)
//
// kqueue requires opening a file descriptor for every file that's being watched;
// so if you're watching a directory with five files then that's six file
// descriptors. You will run in to your system's "max open files" limit faster on
// these platforms.
//
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
// control the maximum number of open files, as well as /etc/login.conf on BSD
// systems.
//
// # Windows notes
//
// Paths can be added as "C:\\path\\to\\dir", but forward slashes
// ("C:/path/to/dir") will also work.
//
// When a watched directory is removed it will always send an event for the
// directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all files, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct {
	b backend

	// Events sends the filesystem change events.
	//
	// fsnotify can send the following events; a "path" here can refer to a
	// file, directory, symbolic link, or special file like a FIFO.
	//
	//   fsnotify.Create    A new path was created; this may be followed by one
	//                      or more Write events if data also gets written to a
	//                      file.
	//
	//   fsnotify.Remove    A path was removed.
	//
	//   fsnotify.Rename    A path was renamed. A rename is always sent with the
	//                      old path as Event.Name, and a Create event will be
	//                      sent with the new name. Renames are only sent for
	//                      paths that are currently watched; e.g. moving an
	//                      unmonitored file into a monitored directory will
	//                      show up as just a Create. Similarly, renaming a file
	//                      to outside a monitored directory will show up as
	//                      only a Rename.
	//
	//   fsnotify.Write     A file or named pipe was written to. A Truncate will
	//                      also trigger a Write. A single "write action"
	//                      initiated by the user may show up as one or multiple
	//                      writes, depending on when the system syncs things to
	//                      disk. For example when compiling a large Go program
	//                      you may get hundreds of Write events, and you may
	//                      want to wait until you've stopped receiving them
	//                      (see the dedup example in cmd/fsnotify).
	//
	//                      Some systems may send Write event for directories
	//                      when the directory content changes.
	//
	//   fsnotify.Chmod     Attributes were changed. On Linux this is also sent
	//                      when a file is removed (or more accurately, when a
	//                      link to an inode is removed). On kqueue it's sent
	//                      when a file is truncated. On Windows it's never
	//                      sent.
	Events chan Event

	// Errors sends any errors.
	Errors chan error
}

// Event represents a file system notification.
type Event struct {
	// Path to the file or directory.
@@ -30,6 +157,16 @@ type Event struct {
	// This is a bitmask and some systems may send multiple operations at once.
	// Use the Event.Has() method instead of comparing with ==.
	Op Op

	// Create events will have this set to the old path if it's a rename. This
	// only works when both the source and destination are watched. It's not
	// reliable when watching individual files, only directories.
	//
	// For example "mv /tmp/file /tmp/rename" will emit:
	//
	//   Event{Op: Rename, Name: "/tmp/file"}
	//   Event{Op: Create, Name: "/tmp/rename", RenamedFrom: "/tmp/file"}
	renamedFrom string
}

// Op describes a set of file operations.
@@ -50,7 +187,7 @@ const (
	// example "remove to trash" is often a rename).
	Remove

	// The path was renamed to something else; any watched on it will be
	// The path was renamed to something else; any watches on it will be
	// removed.
	Rename

@@ -60,15 +197,155 @@ const (
	// get triggered very frequently by some software. For example, Spotlight
	// indexing on macOS, anti-virus software, backup software, etc.
	Chmod

	// File descriptor was opened.
	//
	// Only works on Linux and FreeBSD.
	xUnportableOpen

	// File was read from.
	//
	// Only works on Linux and FreeBSD.
	xUnportableRead

	// File opened for writing was closed.
	//
	// Only works on Linux and FreeBSD.
	//
	// The advantage of using this over Write is that it's more reliable than
	// waiting for Write events to stop. It's also faster (if you're not
	// listening to Write events): copying a file of a few GB can easily
	// generate tens of thousands of Write events in a short span of time.
	xUnportableCloseWrite

	// File opened for reading was closed.
	//
	// Only works on Linux and FreeBSD.
	xUnportableCloseRead
)

// Common errors that can be reported.
var (
	// ErrNonExistentWatch is used when Remove() is called on a path that's not
	// added.
	ErrNonExistentWatch = errors.New("fsnotify: can't remove non-existent watch")
	ErrEventOverflow    = errors.New("fsnotify: queue or buffer overflow")

	// ErrClosed is used when trying to operate on a closed Watcher.
	ErrClosed = errors.New("fsnotify: watcher already closed")

	// ErrEventOverflow is reported from the Errors channel when there are too
	// many events:
	//
	//  - inotify:      inotify returns IN_Q_OVERFLOW – because there are too
	//                  many queued events (the fs.inotify.max_queued_events
	//                  sysctl can be used to increase this).
	//  - windows:      The buffer size is too small; WithBufferSize() can be used to increase it.
	//  - kqueue, fen:  Not used.
	ErrEventOverflow = errors.New("fsnotify: queue or buffer overflow")

	// ErrUnsupported is returned by AddWith() when WithOps() specified an
	// Unportable event that's not supported on this platform.
	xErrUnsupported = errors.New("fsnotify: not supported with this backend")
)

// NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) {
	ev, errs := make(chan Event), make(chan error)
	b, err := newBackend(ev, errs)
	if err != nil {
		return nil, err
	}
	return &Watcher{b: b, Events: ev, Errors: errs}, nil
}

// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) {
	ev, errs := make(chan Event), make(chan error)
	b, err := newBufferedBackend(sz, ev, errs)
	if err != nil {
		return nil, err
	}
	return &Watcher{b: b, Events: ev, Errors: errs}, nil
}

// Add starts monitoring the path for changes.
//
// A path can only be watched once; watching it more than once is a no-op and will
// not return an error. Paths that do not yet exist on the filesystem cannot be
// watched.
//
// A watch will be automatically removed if the watched path is deleted or
// renamed. The exception is the Windows backend, which doesn't remove the
// watcher on renames.
//
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work.
//
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
//
// # Watching files
//
// Watching individual files (rather than directories) is generally not
// recommended as many programs (especially editors) update files atomically: it
// will write to a temporary file which is then moved to destination,
// overwriting the original (or some variant thereof). The watcher on the
// original file is now lost, as that no longer exists.
//
// The upshot of this is that a power failure or crash won't leave a
// half-written file.
//
// Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
func (w *Watcher) Add(path string) error { return w.b.Add(path) }

// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
//   - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
//     other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(path string, opts ...addOpt) error { return w.b.AddWith(path, opts...) }

// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) Remove(path string) error { return w.b.Remove(path) }

// Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error { return w.b.Close() }

// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string { return w.b.WatchList() }

// Supports reports if all the listed operations are supported by this platform.
//
// Create, Write, Remove, Rename, and Chmod are always supported. It can only
// return false for an Op starting with Unportable.
func (w *Watcher) xSupports(op Op) bool { return w.b.xSupports(op) }

func (o Op) String() string {
	var b strings.Builder
	if o.Has(Create) {
@@ -80,6 +357,18 @@ func (o Op) String() string {
	if o.Has(Write) {
		b.WriteString("|WRITE")
	}
	if o.Has(xUnportableOpen) {
		b.WriteString("|OPEN")
	}
	if o.Has(xUnportableRead) {
		b.WriteString("|READ")
	}
	if o.Has(xUnportableCloseWrite) {
		b.WriteString("|CLOSE_WRITE")
	}
	if o.Has(xUnportableCloseRead) {
		b.WriteString("|CLOSE_READ")
	}
	if o.Has(Rename) {
		b.WriteString("|RENAME")
	}
@@ -100,25 +389,49 @@ func (e Event) Has(op Op) bool { return e.Op.Has(op) }

// String returns a string representation of the event with their path.
func (e Event) String() string {
	if e.renamedFrom != "" {
		return fmt.Sprintf("%-13s %q ← %q", e.Op.String(), e.Name, e.renamedFrom)
	}
	return fmt.Sprintf("%-13s %q", e.Op.String(), e.Name)
}

type (
	backend interface {
		Add(string) error
		AddWith(string, ...addOpt) error
		Remove(string) error
		WatchList() []string
		Close() error
		xSupports(Op) bool
	}
	addOpt   func(opt *withOpts)
	withOpts struct {
		bufsize    int
		op         Op
		noFollow   bool
		sendCreate bool
	}
)

var debug = func() bool {
	// Check for exactly "1" (rather than mere existence) so we can add
	// options/flags in the future. I don't know if we ever want that, but it's
	// nice to leave the option open.
	return os.Getenv("FSNOTIFY_DEBUG") == "1"
}()

var defaultOpts = withOpts{
	bufsize: 65536, // 64K
	op:      Create | Write | Remove | Rename | Chmod,
}

func getOptions(opts ...addOpt) withOpts {
	with := defaultOpts
	for _, o := range opts {
		if o != nil {
			o(&with)
		}
	}
	return with
}

@@ -136,9 +449,44 @@ func WithBufferSize(bytes int) addOpt {
	return func(opt *withOpts) { opt.bufsize = bytes }
}

// WithOps sets which operations to listen for. The default is [Create],
// [Write], [Remove], [Rename], and [Chmod].
//
// Excluding operations you're not interested in can save quite a bit of CPU
// time; in some use cases there may be hundreds of thousands of useless Write
// or Chmod operations per second.
//
// This can also be used to add unportable operations not supported by all
// platforms; unportable operations all start with "Unportable":
// [UnportableOpen], [UnportableRead], [UnportableCloseWrite], and
// [UnportableCloseRead].
//
// AddWith returns an error when using an unportable operation that's not
// supported. Use [Watcher.Support] to check for support.
func withOps(op Op) addOpt {
	return func(opt *withOpts) { opt.op = op }
}

// WithNoFollow disables following symlinks, so the symlinks themselves are
// watched.
func withNoFollow() addOpt {
	return func(opt *withOpts) { opt.noFollow = true }
}

// "Internal" option for recursive watches on inotify.
func withCreate() addOpt {
	return func(opt *withOpts) { opt.sendCreate = true }
}

var enableRecurse = false

// Check if this path is recursive (ends with "/..." or "\..."), and return the
// path with the /... stripped.
func recursivePath(path string) (string, bool) {
	path = filepath.Clean(path)
	if !enableRecurse { // Only enabled in tests for now.
		return path, false
	}
	if filepath.Base(path) == "..." {
		return filepath.Dir(path), true
	}
+0 −259
Original line number Diff line number Diff line
#!/usr/bin/env zsh
[ "${ZSH_VERSION:-}" = "" ] && echo >&2 "Only works with zsh" && exit 1
setopt err_exit no_unset pipefail extended_glob

# Simple script to update the godoc comments on all watchers so you don't need
# to update the same comment 5 times.

watcher=$(<<EOF
// Watcher watches a set of paths, delivering events on a channel.
//
// A watcher should not be copied (e.g. pass it by pointer, rather than by
// value).
//
// # Linux notes
//
// When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example:
//
//	fp := os.Open("file")
//	os.Remove("file")        // Triggers Chmod
//	fp.Close()               // Triggers Remove
//
// This is the event that inotify sends, so not much can be changed about this.
//
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
// create is an "instance", and every path you add is a "watch".
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
//
// To increase them you can use sysctl or write the value to the /proc file:
//
//	# Default values on Linux 5.18
//	sysctl fs.inotify.max_user_watches=124983
//	sysctl fs.inotify.max_user_instances=128
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
//	fs.inotify.max_user_watches=124983
//	fs.inotify.max_user_instances=128
//
// Reaching the limit will result in a "no space left on device" or "too many open
// files" error.
//
// # kqueue notes (macOS, BSD)
//
// kqueue requires opening a file descriptor for every file that's being watched;
// so if you're watching a directory with five files then that's six file
// descriptors. You will run in to your system's "max open files" limit faster on
// these platforms.
//
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
// control the maximum number of open files, as well as /etc/login.conf on BSD
// systems.
//
// # Windows notes
//
// Paths can be added as "C:\\path\\to\\dir", but forward slashes
// ("C:/path/to/dir") will also work.
//
// When a watched directory is removed it will always send an event for the
// directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
EOF
)

new=$(<<EOF
// NewWatcher creates a new Watcher.
EOF
)

newbuffered=$(<<EOF
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
EOF
)

add=$(<<EOF
// Add starts monitoring the path for changes.
//
// A path can only be watched once; watching it more than once is a no-op and will
// not return an error. Paths that do not yet exist on the filesystem cannot be
// watched.
//
// A watch will be automatically removed if the watched path is deleted or
// renamed. The exception is the Windows backend, which doesn't remove the
// watcher on renames.
//
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work.
//
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
//
// # Watching files
//
// Watching individual files (rather than directories) is generally not
// recommended as many programs (especially editors) update files atomically: it
// will write to a temporary file which is then moved to to destination,
// overwriting the original (or some variant thereof). The watcher on the
// original file is now lost, as that no longer exists.
//
// The upshot of this is that a power failure or crash won't leave a
// half-written file.
//
// Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
EOF
)

addwith=$(<<EOF
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
//   - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
//     other platforms. The default is 64K (65536 bytes).
EOF
)

remove=$(<<EOF
// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
EOF
)

close=$(<<EOF
// Close removes all watches and closes the Events channel.
EOF
)

watchlist=$(<<EOF
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
EOF
)

events=$(<<EOF
	// Events sends the filesystem change events.
	//
	// fsnotify can send the following events; a "path" here can refer to a
	// file, directory, symbolic link, or special file like a FIFO.
	//
	//   fsnotify.Create    A new path was created; this may be followed by one
	//                      or more Write events if data also gets written to a
	//                      file.
	//
	//   fsnotify.Remove    A path was removed.
	//
	//   fsnotify.Rename    A path was renamed. A rename is always sent with the
	//                      old path as Event.Name, and a Create event will be
	//                      sent with the new name. Renames are only sent for
	//                      paths that are currently watched; e.g. moving an
	//                      unmonitored file into a monitored directory will
	//                      show up as just a Create. Similarly, renaming a file
	//                      to outside a monitored directory will show up as
	//                      only a Rename.
	//
	//   fsnotify.Write     A file or named pipe was written to. A Truncate will
	//                      also trigger a Write. A single "write action"
	//                      initiated by the user may show up as one or multiple
	//                      writes, depending on when the system syncs things to
	//                      disk. For example when compiling a large Go program
	//                      you may get hundreds of Write events, and you may
	//                      want to wait until you've stopped receiving them
	//                      (see the dedup example in cmd/fsnotify).
	//
	//                      Some systems may send Write event for directories
	//                      when the directory content changes.
	//
	//   fsnotify.Chmod     Attributes were changed. On Linux this is also sent
	//                      when a file is removed (or more accurately, when a
	//                      link to an inode is removed). On kqueue it's sent
	//                      when a file is truncated. On Windows it's never
	//                      sent.
EOF
)

errors=$(<<EOF
	// Errors sends any errors.
	//
	// ErrEventOverflow is used to indicate there are too many events:
	//
	//  - inotify:      There are too many queued events (fs.inotify.max_queued_events sysctl)
	//  - windows:      The buffer size is too small; WithBufferSize() can be used to increase it.
	//  - kqueue, fen:  Not used.
EOF
)

set-cmt() {
	local pat=$1
	local cmt=$2

	IFS=$'\n' local files=($(grep -n $pat backend_*~*_test.go))
	for f in $files; do
		IFS=':' local fields=($=f)
		local file=$fields[1]
		local end=$(( $fields[2] - 1 ))

		# Find start of comment.
		local start=0
		IFS=$'\n' local lines=($(head -n$end $file))
		for (( i = 1; i <= $#lines; i++ )); do
			local line=$lines[-$i]
			if ! grep -q '^[[:space:]]*//' <<<$line; then
				start=$(( end - (i - 2) ))
				break
			fi
		done

		head -n $(( start - 1 )) $file  >/tmp/x
		print -r -- $cmt                >>/tmp/x
		tail -n+$(( end + 1 ))   $file  >>/tmp/x
		mv /tmp/x $file
	done
}

set-cmt '^type Watcher struct '             $watcher
set-cmt '^func NewWatcher('                 $new
set-cmt '^func NewBufferedWatcher('         $newbuffered
set-cmt '^func (w \*Watcher) Add('          $add
set-cmt '^func (w \*Watcher) AddWith('      $addwith
set-cmt '^func (w \*Watcher) Remove('       $remove
set-cmt '^func (w \*Watcher) Close('        $close
set-cmt '^func (w \*Watcher) WatchList('    $watchlist
set-cmt '^[[:space:]]*Events *chan Event$'  $events
set-cmt '^[[:space:]]*Errors *chan error$'  $errors
Original line number Diff line number Diff line
@@ -32,10 +32,10 @@ func (fi *fileInfo) Name() string { return fi.name }
func (fi *fileInfo) Size() int64 { return int64(fi.stat.Size) }

// Mode returns file mode bits.
func (fi *fileInfo) Mode() os.FileMode { return toFileMode(fi.stat.Mode) }
func (fi *fileInfo) Mode() os.FileMode { return fi.stat.FileMode() }

// ModTime returns the last modification time of the file.
func (fi *fileInfo) ModTime() time.Time { return time.Unix(int64(fi.stat.Mtime), 0) }
func (fi *fileInfo) ModTime() time.Time { return fi.stat.ModTime() }

// IsDir returns true if the file is a directory.
func (fi *fileInfo) IsDir() bool { return fi.Mode().IsDir() }
@@ -56,6 +56,21 @@ type FileStat struct {
	Extended []StatExtended
}

// ModTime returns the Mtime SFTP file attribute converted to a time.Time
func (fs *FileStat) ModTime() time.Time {
	return time.Unix(int64(fs.Mtime), 0)
}

// AccessTime returns the Atime SFTP file attribute converted to a time.Time
func (fs *FileStat) AccessTime() time.Time {
	return time.Unix(int64(fs.Atime), 0)
}

// FileMode returns the Mode SFTP file attribute converted to an os.FileMode
func (fs *FileStat) FileMode() os.FileMode {
	return toFileMode(fs.Mode)
}

// StatExtended contains additional, extended information for a FileStat.
type StatExtended struct {
	ExtType string
Original line number Diff line number Diff line
//go:build darwin || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris || aix || js
// +build darwin dragonfly freebsd !android,linux netbsd openbsd solaris aix js
//go:build darwin || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris || aix || js || zos
// +build darwin dragonfly freebsd !android,linux netbsd openbsd solaris aix js zos

package sftp

Original line number Diff line number Diff line
package sftp

import (
	"context"
	"encoding"
	"fmt"
	"io"
@@ -128,15 +129,20 @@ type idmarshaler interface {
	encoding.BinaryMarshaler
}

func (c *clientConn) sendPacket(ch chan result, p idmarshaler) (byte, []byte, error) {
func (c *clientConn) sendPacket(ctx context.Context, ch chan result, p idmarshaler) (byte, []byte, error) {
	if cap(ch) < 1 {
		ch = make(chan result, 1)
	}

	c.dispatchRequest(ch, p)
	s := <-ch

	select {
	case <-ctx.Done():
		return 0, nil, ctx.Err()
	case s := <-ch:
		return s.typ, s.data, s.err
	}
}

// dispatchRequest should ideally only be called by race-detection tests outside of this file,
// where you have to ensure two packets are in flight sequentially after each other.
Original line number Diff line number Diff line
//go:build aix || darwin || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris || js
// +build aix darwin dragonfly freebsd !android,linux netbsd openbsd solaris js
//go:build aix || darwin || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris || js || zos
// +build aix darwin dragonfly freebsd !android,linux netbsd openbsd solaris js zos

package sftp

Original line number Diff line number Diff line
@@ -3,7 +3,6 @@ package sftp
// Methods on the Request object to make working with the Flags bitmasks and
// Attr(ibutes) byte blob easier. Use Pflags() when working with an Open/Write
// request and AttrFlags() and Attributes() when working with SetStat requests.
import "os"

// FileOpenFlags defines Open and Write Flags. Correlate directly with with os.OpenFile flags
// (https://golang.org/pkg/os/#pkg-constants).
@@ -50,14 +49,9 @@ func (r *Request) AttrFlags() FileAttrFlags {
	return newFileAttrFlags(r.Flags)
}

// FileMode returns the Mode SFTP file attributes wrapped as os.FileMode
func (a FileStat) FileMode() os.FileMode {
	return os.FileMode(a.Mode)
}

// Attributes parses file attributes byte blob and return them in a
// FileStat object.
func (r *Request) Attributes() *FileStat {
	fs, _ := unmarshalFileStat(r.Flags, r.Attrs)
	fs, _, _ := unmarshalFileStat(r.Flags, r.Attrs)
	return fs
}
Original line number Diff line number Diff line
@@ -30,7 +30,7 @@ type FileReader interface {
// FileWriter should return an io.WriterAt for the filepath.
//
// The request server code will call Close() on the returned io.WriterAt
// ojbect if an io.Closer type assertion succeeds.
// object if an io.Closer type assertion succeeds.
// Note in cases of an error, the error text will be sent to the client.
// Note when receiving an Append flag it is important to not open files using
// O_APPEND if you plan to use WriteAt, as they conflict.
@@ -144,6 +144,8 @@ type NameLookupFileLister interface {
//
// If a populated entry implements [FileInfoExtendedData], extended attributes will also be returned to the client.
//
// The request server code will call Close() on ListerAt if an io.Closer type assertion succeeds.
//
// Note in cases of an error, the error text will be sent to the client.
type ListerAt interface {
	ListAt([]os.FileInfo, int64) (int, error)
+0 −10
Original line number Diff line number Diff line
//go:build plan9 || windows || (js && wasm)
// +build plan9 windows js,wasm

// Go defines S_IFMT on windows, plan9 and js/wasm as 0x1f000 instead of
// 0xf000. None of the the other S_IFxyz values include the "1" (in 0x1f000)
// which prevents them from matching the bitmask.

package sftp

const S_IFMT = 0xf000
+0 −10
Original line number Diff line number Diff line
//go:build !plan9 && !windows && (!js || !wasm)
// +build !plan9
// +build !windows
// +build !js !wasm

package sftp

import "syscall"

const S_IFMT = syscall.S_IFMT
Original line number Diff line number Diff line
env:
  CIRRUS_CLONE_DEPTH: 1
  GO_VERSION: go1.22.2
  GO_VERSION: go1.23.0

freebsd_13_task:
  freebsd_instance:
    image_family: freebsd-13-2
    image_family: freebsd-13-3
  install_script: |
    pkg install -y go
    GOBIN=$PWD/bin go install golang.org/dl/${GO_VERSION}@latest
Original line number Diff line number Diff line
@@ -510,8 +510,8 @@ userAuthLoop:
			if err := s.transport.writePacket(Marshal(discMsg)); err != nil {
				return nil, err
			}

			return nil, discMsg
			authErrs = append(authErrs, discMsg)
			return nil, &ServerAuthError{Errors: authErrs}
		}

		var userAuthReq userAuthRequestMsg
Original line number Diff line number Diff line
@@ -156,7 +156,7 @@ from the generated architecture-specific files listed below, and merge these
into a common file for each OS.

The merge is performed in the following steps:
1. Construct the set of common code that is idential in all architecture-specific files.
1. Construct the set of common code that is identical in all architecture-specific files.
2. Write this common code to the merged file.
3. Remove the common code from all architecture-specific files.

File changed.

Preview size limit exceeded, changes collapsed.