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

Target

Select target project
  • oss/libraries/go/services/job-queues
1 result
Select Git revision
Show changes
Commits on Source (2)
Showing
with 1476 additions and 1678 deletions
...@@ -4,9 +4,7 @@ ...@@ -4,9 +4,7 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="9979eb22-471e-4f2f-b624-fd3edb5e8c6e" name="Changes" comment=""> <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>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" /> <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
...@@ -40,61 +38,61 @@ ...@@ -40,61 +38,61 @@
<option name="sortByType" value="true" /> <option name="sortByType" value="true" />
<option name="sortKey" value="BY_TYPE" /> <option name="sortKey" value="BY_TYPE" />
</component> </component>
<component name="PropertiesComponent"><![CDATA[{ <component name="PropertiesComponent">{
"keyToString": { &quot;keyToString&quot;: {
"DefaultGoTemplateProperty": "Go File", &quot;DefaultGoTemplateProperty&quot;: &quot;Go File&quot;,
"Go Test.TestCreateJobAndSchedulerFromInput in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestCreateJobAndSchedulerFromInput in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestDeleteJob in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestDeleteJob in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestDummyRunnable in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestDummyRunnable in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestJobPersistenceUnmarshalJSON in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestJobPersistenceUnmarshalJSON in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestJobPersistenceUnmarshalJSON/Date_and_Time in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestJobPersistenceUnmarshalJSON/Date_and_Time in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestJobPersistenceUnmarshalJSON/RFC3339_Format in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestJobPersistenceUnmarshalJSON/RFC3339_Format in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestJobPersistence_MarshalUnmarshalJSON in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestJobPersistence_MarshalUnmarshalJSON in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestManagerEventHandling in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestManagerEventHandling in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestReadJsonFile in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestReadJsonFile in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestResetStats in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestResetStats in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestRoundTrip in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestRoundTrip in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestScheduleJob in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestScheduleJob in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestSchedulerPersistenceUnmarshalJSON in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestSchedulerPersistenceUnmarshalJSON in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestSchedulerPersistenceUnmarshalJSON/Date_and_Time_with_Space in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestSchedulerPersistenceUnmarshalJSON/Date_and_Time_with_Space in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestSchedulerPersistenceUnmarshalJSON/Date_and_Time_with_Space_and_Seconds in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &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;,
"Go Test.TestSetAndGetTimeout in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestSetAndGetTimeout in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestStructure in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestStructure in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestTimeFunctionSame in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestTimeFunctionSame in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestUnmarshalJSON in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestUnmarshalJSON in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestUnmarshalSchedulerPersistenceIntervalYAML in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestUnmarshalSchedulerPersistenceIntervalYAML in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestUnmarshalSchedulerPersistenceYAML in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestUnmarshalSchedulerPersistenceYAML in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestUnmarshalYAML in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestUnmarshalYAML in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestWorkerLifeCycle in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestWorkerLifeCycle in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestWriteToDB1 in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestWriteToDB1 in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestWriteToDB2 in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestWriteToDB2 in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.TestWriteToDB4 in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.TestWriteToDB4 in gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"Go Test.go test gitlab.schukai.com/oss/libraries/go/services/job-queues.executor": "Debug", &quot;Go Test.go test gitlab.schukai.com/oss/libraries/go/services/job-queues.executor&quot;: &quot;Debug&quot;,
"NIXITCH_NIXPKGS_CONFIG": "/etc/nix/nixpkgs-config.nix", &quot;NIXITCH_NIXPKGS_CONFIG&quot;: &quot;/etc/nix/nixpkgs-config.nix&quot;,
"NIXITCH_NIX_CONF_DIR": "", &quot;NIXITCH_NIX_CONF_DIR&quot;: &quot;&quot;,
"NIXITCH_NIX_OTHER_STORES": "", &quot;NIXITCH_NIX_OTHER_STORES&quot;: &quot;&quot;,
"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", &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;,
"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", &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;,
"NIXITCH_NIX_REMOTE": "", &quot;NIXITCH_NIX_REMOTE&quot;: &quot;&quot;,
"NIXITCH_NIX_USER_PROFILE_DIR": "/nix/var/nix/profiles/per-user/vs", &quot;NIXITCH_NIX_USER_PROFILE_DIR&quot;: &quot;/nix/var/nix/profiles/per-user/vs&quot;,
"RunOnceActivity.ShowReadmeOnStart": "true", &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
"RunOnceActivity.go.formatter.settings.were.checked": "true", &quot;RunOnceActivity.go.formatter.settings.were.checked&quot;: &quot;true&quot;,
"RunOnceActivity.go.migrated.go.modules.settings": "true", &quot;RunOnceActivity.go.migrated.go.modules.settings&quot;: &quot;true&quot;,
"RunOnceActivity.go.modules.automatic.dependencies.download": "true", &quot;RunOnceActivity.go.modules.automatic.dependencies.download&quot;: &quot;true&quot;,
"SHARE_PROJECT_CONFIGURATION_FILES": "true", &quot;SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
"git-widget-placeholder": "master", &quot;git-widget-placeholder&quot;: &quot;master&quot;,
"go.import.settings.migrated": "true", &quot;go.import.settings.migrated&quot;: &quot;true&quot;,
"go.sdk.automatically.set": "true", &quot;go.sdk.automatically.set&quot;: &quot;true&quot;,
"last_opened_file_path": "/home/vs/workspaces/oss/go-libs/job-queues", &quot;last_opened_file_path&quot;: &quot;/home/vs/workspaces/oss/go-libs/job-queues&quot;,
"node.js.detected.package.eslint": "true", &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
"node.js.detected.package.tslint": "true", &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
"node.js.selected.package.eslint": "(autodetect)", &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
"node.js.selected.package.tslint": "(autodetect)", &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
"nodejs_package_manager_path": "npm", &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
"settings.editor.selected.configurable": "go.sdk", &quot;settings.editor.selected.configurable&quot;: &quot;go.sdk&quot;,
"vue.rearranger.settings.migration": "true" &quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
} }
}]]></component> }</component>
<component name="RecentsManager"> <component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS"> <key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$" /> <recent name="$PROJECT_DIR$" />
......
...@@ -60,7 +60,9 @@ _ = m.ScheduleJob(job, &scheduler) ...@@ -60,7 +60,9 @@ _ = m.ScheduleJob(job, &scheduler)
## License ## 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 ## Contact
......
...@@ -7,14 +7,14 @@ require ( ...@@ -7,14 +7,14 @@ require (
github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/docker/docker v24.0.6+incompatible github.com/docker/docker v24.0.6+incompatible
github.com/docker/go-connections v0.4.0 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/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/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v3 v3.24.5 github.com/shirou/gopsutil/v3 v3.24.5
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
go.uber.org/zap v1.27.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 gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.7 gorm.io/driver/mysql v1.5.7
gorm.io/driver/sqlite v1.5.5 gorm.io/driver/sqlite v1.5.5
...@@ -46,14 +46,14 @@ require ( ...@@ -46,14 +46,14 @@ require (
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.14 // 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 github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/mod v0.17.0 // indirect golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.25.0 // indirect golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.8.0 // indirect golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.18.0 // indirect golang.org/x/text v0.19.0 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
) )
...@@ -21,6 +21,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 ...@@ -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/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 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 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.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 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 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= ...@@ -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/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 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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= 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 ...@@ -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/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 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= 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.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/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= 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 ...@@ -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-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.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.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 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= 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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.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.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 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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= 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 ...@@ -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-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.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.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 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 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-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-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-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.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 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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= 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 ...@@ -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-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.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.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 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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-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.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.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 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= 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.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.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.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.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 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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= 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 ...@@ -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-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.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.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 h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
......
freebsd_task: freebsd_task:
name: 'FreeBSD' name: 'FreeBSD'
freebsd_instance: freebsd_instance:
image_family: freebsd-13-2 image_family: freebsd-14-1
install_script: install_script:
- pkg update -f - pkg update -f
- pkg install -y go - pkg install -y go
...@@ -9,5 +9,6 @@ freebsd_task: ...@@ -9,5 +9,6 @@ freebsd_task:
# run tests as user "cirrus" instead of root # run tests as user "cirrus" instead of root
- pw useradd cirrus -m - pw useradd cirrus -m
- chown -R cirrus:cirrus . - chown -R cirrus:cirrus .
- FSNOTIFY_BUFFER=4096 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./... - 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 ./... - 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 ./...
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
go.sum linguist-generated
...@@ -5,3 +5,6 @@ ...@@ -5,3 +5,6 @@
# Output of go build ./cmd/fsnotify # Output of go build ./cmd/fsnotify
/fsnotify /fsnotify
/fsnotify.exe /fsnotify.exe
/test/kqueue
/test/a.out
# Changelog # Changelog
Unreleased 1.8.0 2023-10-31
---------- ----------------
Nothing yet.
### 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 1.7.0 - 2023-10-22
------------------ ------------------
......
Thank you for your interest in contributing to fsnotify! We try to review and 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: 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 can just send PRs, but they may end up being rejected for one reason or the
other. other.
...@@ -20,6 +20,124 @@ platforms. Testing different platforms locally can be done with something like ...@@ -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. 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 [goon]: https://github.com/arp242/goon
[Vagrant]: https://www.vagrantup.com/ [Vagrant]: https://www.vagrantup.com/
......
//go:build solaris //go:build solaris
// +build solaris
// Note: the documentation on the Watcher type and methods is generated from // FEN backend for illumos (supported) and Solaris (untested, but should work).
// mkdoc.zsh //
// See port_create(3c) etc. for docs. https://www.illumos.org/man/3C/port_create
package fsnotify package fsnotify
...@@ -12,150 +12,33 @@ import ( ...@@ -12,150 +12,33 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
"time"
"github.com/fsnotify/fsnotify/internal"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
// Watcher watches a set of paths, delivering events on a channel. type fen struct {
//
// 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.
Events chan Event 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 Errors chan error
mu sync.Mutex mu sync.Mutex
port *unix.EventPort port *unix.EventPort
done chan struct{} // Channel for sending a "quit message" to the reader goroutine done chan struct{} // Channel for sending a "quit message" to the reader goroutine
dirs map[string]struct{} // Explicitly watched directories dirs map[string]Op // Explicitly watched directories
watches map[string]struct{} // Explicitly watched non-directories watches map[string]Op // Explicitly watched non-directories
} }
// NewWatcher creates a new Watcher. func newBackend(ev chan Event, errs chan error) (backend, error) {
func NewWatcher() (*Watcher, error) { return newBufferedBackend(0, ev, errs)
return NewBufferedWatcher(0)
} }
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
// channel. w := &fen{
// Events: ev,
// The main use case for this is situations with a very large number of events Errors: errs,
// where the kernel buffer size can't be increased (e.g. due to lack of dirs: make(map[string]Op),
// permissions). An unbuffered Watcher will perform better for almost all use watches: make(map[string]Op),
// 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{}),
done: make(chan struct{}), done: make(chan struct{}),
} }
...@@ -171,27 +54,30 @@ func NewBufferedWatcher(sz uint) (*Watcher, error) { ...@@ -171,27 +54,30 @@ func NewBufferedWatcher(sz uint) (*Watcher, error) {
// sendEvent attempts to send an event to the user, returning true if the event // 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. // 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 { select {
case w.Events <- Event{Name: name, Op: op}:
return true
case <-w.done: case <-w.done:
return false 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 // 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. // was put in the channel successfully and false if the watcher has been closed.
func (w *Watcher) sendError(err error) (sent bool) { func (w *fen) sendError(err error) (sent bool) {
select { if err == nil {
case w.Errors <- err:
return true return true
}
select {
case <-w.done: case <-w.done:
return false return false
case w.Errors <- err:
return true
} }
} }
func (w *Watcher) isClosed() bool { func (w *fen) isClosed() bool {
select { select {
case <-w.done: case <-w.done:
return true return true
...@@ -200,8 +86,7 @@ func (w *Watcher) isClosed() bool { ...@@ -200,8 +86,7 @@ func (w *Watcher) isClosed() bool {
} }
} }
// Close removes all watches and closes the Events channel. func (w *fen) Close() error {
func (w *Watcher) Close() error {
// Take the lock used by associateFile to prevent lingering events from // Take the lock used by associateFile to prevent lingering events from
// being processed after the close // being processed after the close
w.mu.Lock() w.mu.Lock()
...@@ -213,60 +98,21 @@ func (w *Watcher) Close() error { ...@@ -213,60 +98,21 @@ func (w *Watcher) Close() error {
return w.port.Close() return w.port.Close()
} }
// Add starts monitoring the path for changes. func (w *fen) Add(name string) error { return w.AddWith(name) }
//
// 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) }
// AddWith is like [Watcher.Add], but allows adding options. When using Add() func (w *fen) AddWith(name string, opts ...addOpt) error {
// 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 {
if w.isClosed() { if w.isClosed() {
return ErrClosed return ErrClosed
} }
if w.port.PathIsWatched(name) { if debug {
return nil 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 // Currently we resolve symlinks that were explicitly requested to be
// watched. Otherwise we would use LStat here. // watched. Otherwise we would use LStat here.
...@@ -283,7 +129,7 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error { ...@@ -283,7 +129,7 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error {
} }
w.mu.Lock() w.mu.Lock()
w.dirs[name] = struct{}{} w.dirs[name] = with.op
w.mu.Unlock() w.mu.Unlock()
return nil return nil
} }
...@@ -294,26 +140,22 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error { ...@@ -294,26 +140,22 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error {
} }
w.mu.Lock() w.mu.Lock()
w.watches[name] = struct{}{} w.watches[name] = with.op
w.mu.Unlock() w.mu.Unlock()
return nil return nil
} }
// Remove stops monitoring the path for changes. func (w *fen) Remove(name string) error {
//
// 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 {
if w.isClosed() { if w.isClosed() {
return nil return nil
} }
if !w.port.PathIsWatched(name) { if !w.port.PathIsWatched(name) {
return fmt.Errorf("%w: %s", ErrNonExistentWatch, 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 // 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 // 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 { ...@@ -346,7 +188,7 @@ func (w *Watcher) Remove(name string) error {
} }
// readEvents contains the main loop that runs in a goroutine watching for events. // 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 // If this function returns, the watcher has been closed and we can close
// these channels // these channels
defer func() { defer func() {
...@@ -382,17 +224,19 @@ func (w *Watcher) readEvents() { ...@@ -382,17 +224,19 @@ func (w *Watcher) readEvents() {
continue continue
} }
if debug {
internal.Debug(pevent.Path, pevent.Events)
}
err = w.handleEvent(&pevent) err = w.handleEvent(&pevent)
if err != nil { if !w.sendError(err) {
if !w.sendError(err) { return
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) files, err := os.ReadDir(path)
if err != nil { if err != nil {
return err return err
...@@ -418,7 +262,7 @@ func (w *Watcher) handleDirectory(path string, stat os.FileInfo, follow bool, ha ...@@ -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 // 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 // had the attributes changed between when the association was created and the
// when event was returned) // when event was returned)
func (w *Watcher) handleEvent(event *unix.PortEvent) error { func (w *fen) handleEvent(event *unix.PortEvent) error {
var ( var (
events = event.Events events = event.Events
path = event.Path path = event.Path
...@@ -510,15 +354,9 @@ func (w *Watcher) handleEvent(event *unix.PortEvent) error { ...@@ -510,15 +354,9 @@ func (w *Watcher) handleEvent(event *unix.PortEvent) error {
} }
if events&unix.FILE_MODIFIED != 0 { if events&unix.FILE_MODIFIED != 0 {
if fmode.IsDir() { if fmode.IsDir() && watchedDir {
if watchedDir { if err := w.updateDirectory(path); err != nil {
if err := w.updateDirectory(path); err != nil { return err
return err
}
} else {
if !w.sendEvent(path, Write) {
return nil
}
} }
} else { } else {
if !w.sendEvent(path, Write) { if !w.sendEvent(path, Write) {
...@@ -543,7 +381,7 @@ func (w *Watcher) handleEvent(event *unix.PortEvent) error { ...@@ -543,7 +381,7 @@ func (w *Watcher) handleEvent(event *unix.PortEvent) error {
return nil 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 // The directory was modified, so we must find unwatched entities and watch
// them. If something was removed from the directory, nothing will happen, // them. If something was removed from the directory, nothing will happen,
// as everything else should still be watched. // as everything else should still be watched.
...@@ -563,10 +401,8 @@ func (w *Watcher) updateDirectory(path string) error { ...@@ -563,10 +401,8 @@ func (w *Watcher) updateDirectory(path string) error {
return err return err
} }
err = w.associateFile(path, finfo, false) err = w.associateFile(path, finfo, false)
if err != nil { if !w.sendError(err) {
if !w.sendError(err) { return nil
return nil
}
} }
if !w.sendEvent(path, Create) { if !w.sendEvent(path, Create) {
return nil return nil
...@@ -575,7 +411,7 @@ func (w *Watcher) updateDirectory(path string) error { ...@@ -575,7 +411,7 @@ func (w *Watcher) updateDirectory(path string) error {
return nil 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() { if w.isClosed() {
return ErrClosed return ErrClosed
} }
...@@ -593,34 +429,34 @@ func (w *Watcher) associateFile(path string, stat os.FileInfo, follow bool) erro ...@@ -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 // cleared up that discrepancy. The most likely cause is that the event
// has fired but we haven't processed it yet. // has fired but we haven't processed it yet.
err := w.port.DissociatePath(path) err := w.port.DissociatePath(path)
if err != nil && err != unix.ENOENT { if err != nil && !errors.Is(err, unix.ENOENT) {
return err return err
} }
} }
// FILE_NOFOLLOW means we watch symlinks themselves rather than their
// targets. var events int
events := unix.FILE_MODIFIED | unix.FILE_ATTRIB | unix.FILE_NOFOLLOW if !follow {
if follow { // Watch symlinks themselves rather than their targets unless this entry
// We *DO* follow symlinks for explicitly watched entries. // is explicitly watched.
events = unix.FILE_MODIFIED | unix.FILE_ATTRIB events |= unix.FILE_NOFOLLOW
}
if true { // TODO: implement withOps()
events |= unix.FILE_MODIFIED
} }
return w.port.AssociatePath(path, stat, if true {
events, events |= unix.FILE_ATTRIB
stat.Mode()) }
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) { if !w.port.PathIsWatched(path) {
return nil return nil
} }
return w.port.DissociatePath(path) return w.port.DissociatePath(path)
} }
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not func (w *fen) WatchList() []string {
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string {
if w.isClosed() { if w.isClosed() {
return nil return nil
} }
...@@ -638,3 +474,11 @@ func (w *Watcher) WatchList() []string { ...@@ -638,3 +474,11 @@ func (w *Watcher) WatchList() []string {
return entries 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
}
//go:build linux && !appengine //go:build linux && !appengine
// +build linux,!appengine
// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh
package fsnotify package fsnotify
...@@ -10,127 +6,20 @@ import ( ...@@ -10,127 +6,20 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
"time"
"unsafe" "unsafe"
"github.com/fsnotify/fsnotify/internal"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
// Watcher watches a set of paths, delivering events on a channel. type inotify struct {
//
// 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.
Events chan Event 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 Errors chan error
// Store fd here as os.File.Read() will no longer return on close after // Store fd here as os.File.Read() will no longer return on close after
...@@ -139,8 +28,26 @@ type Watcher struct { ...@@ -139,8 +28,26 @@ type Watcher struct {
inotifyFile *os.File inotifyFile *os.File
watches *watches watches *watches
done chan struct{} // Channel for sending a "quit message" to the reader goroutine done chan struct{} // Channel for sending a "quit message" to the reader goroutine
closeMu sync.Mutex doneMu sync.Mutex
doneResp chan struct{} // Channel to respond to Close doneResp chan struct{} // Channel to respond to Close
// Store rename cookies in an array, with the index wrapping to 0. Almost
// all of the time what we get is a MOVED_FROM to set the cookie and the
// next event inotify sends will be MOVED_TO to read it. However, this is
// not guaranteed – as described in inotify(7) – and we may get other events
// between the two MOVED_* events (including other MOVED_* ones).
//
// A second issue is that moving a file outside the watched directory will
// trigger a MOVED_FROM to set the cookie, but we never see the MOVED_TO to
// read and delete it. So just storing it in a map would slowly leak memory.
//
// Doing it like this gives us a simple fast LRU-cache that won't allocate.
// Ten items should be more than enough for our purpose, and a loop over
// such a short array is faster than a map access anyway (not that it hugely
// matters since we're talking about hundreds of ns at the most, but still).
cookies [10]koekje
cookieIndex uint8
cookiesMu sync.Mutex
} }
type ( type (
...@@ -150,9 +57,14 @@ type ( ...@@ -150,9 +57,14 @@ type (
path map[string]uint32 // pathname → wd path map[string]uint32 // pathname → wd
} }
watch struct { watch struct {
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall) wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags) flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
path string // Watch path. path string // Watch path.
recurse bool // Recursion with ./...?
}
koekje struct {
cookie uint32
path string
} }
) )
...@@ -179,23 +91,45 @@ func (w *watches) add(ww *watch) { ...@@ -179,23 +91,45 @@ func (w *watches) add(ww *watch) {
func (w *watches) remove(wd uint32) { func (w *watches) remove(wd uint32) {
w.mu.Lock() w.mu.Lock()
defer w.mu.Unlock() defer w.mu.Unlock()
delete(w.path, w.wd[wd].path) watch := w.wd[wd] // Could have had Remove() called. See #616.
if watch == nil {
return
}
delete(w.path, watch.path)
delete(w.wd, wd) delete(w.wd, wd)
} }
func (w *watches) removePath(path string) (uint32, bool) { func (w *watches) removePath(path string) ([]uint32, error) {
w.mu.Lock() w.mu.Lock()
defer w.mu.Unlock() defer w.mu.Unlock()
path, recurse := recursivePath(path)
wd, ok := w.path[path] wd, ok := w.path[path]
if !ok { if !ok {
return 0, false return nil, fmt.Errorf("%w: %s", ErrNonExistentWatch, path)
}
watch := w.wd[wd]
if recurse && !watch.recurse {
return nil, fmt.Errorf("can't use /... with non-recursive watch %q", path)
} }
delete(w.path, path) delete(w.path, path)
delete(w.wd, wd) delete(w.wd, wd)
if !watch.recurse {
return []uint32{wd}, nil
}
return wd, true wds := make([]uint32, 0, 8)
wds = append(wds, wd)
for p, rwd := range w.path {
if filepath.HasPrefix(p, path) {
delete(w.path, p)
delete(w.wd, rwd)
wds = append(wds, rwd)
}
}
return wds, nil
} }
func (w *watches) byPath(path string) *watch { func (w *watches) byPath(path string) *watch {
...@@ -236,20 +170,11 @@ func (w *watches) updatePath(path string, f func(*watch) (*watch, error)) error ...@@ -236,20 +170,11 @@ func (w *watches) updatePath(path string, f func(*watch) (*watch, error)) error
return nil return nil
} }
// NewWatcher creates a new Watcher. func newBackend(ev chan Event, errs chan error) (backend, error) {
func NewWatcher() (*Watcher, error) { return newBufferedBackend(0, ev, errs)
return NewBufferedWatcher(0)
} }
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
// 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) {
// Need to set nonblocking mode for SetDeadline to work, otherwise blocking // Need to set nonblocking mode for SetDeadline to work, otherwise blocking
// I/O operations won't terminate on close. // I/O operations won't terminate on close.
fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK) fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
...@@ -257,12 +182,12 @@ func NewBufferedWatcher(sz uint) (*Watcher, error) { ...@@ -257,12 +182,12 @@ func NewBufferedWatcher(sz uint) (*Watcher, error) {
return nil, errno return nil, errno
} }
w := &Watcher{ w := &inotify{
Events: ev,
Errors: errs,
fd: fd, fd: fd,
inotifyFile: os.NewFile(uintptr(fd), ""), inotifyFile: os.NewFile(uintptr(fd), ""),
watches: newWatches(), watches: newWatches(),
Events: make(chan Event, sz),
Errors: make(chan error),
done: make(chan struct{}), done: make(chan struct{}),
doneResp: make(chan struct{}), doneResp: make(chan struct{}),
} }
...@@ -272,26 +197,29 @@ func NewBufferedWatcher(sz uint) (*Watcher, error) { ...@@ -272,26 +197,29 @@ func NewBufferedWatcher(sz uint) (*Watcher, error) {
} }
// Returns true if the event was sent, or false if watcher is closed. // Returns true if the event was sent, or false if watcher is closed.
func (w *Watcher) sendEvent(e Event) bool { func (w *inotify) sendEvent(e Event) bool {
select { select {
case w.Events <- e:
return true
case <-w.done: case <-w.done:
return false return false
case w.Events <- e:
return true
} }
} }
// Returns true if the error was sent, or false if watcher is closed. // Returns true if the error was sent, or false if watcher is closed.
func (w *Watcher) sendError(err error) bool { func (w *inotify) sendError(err error) bool {
select { if err == nil {
case w.Errors <- err:
return true return true
}
select {
case <-w.done: case <-w.done:
return false return false
case w.Errors <- err:
return true
} }
} }
func (w *Watcher) isClosed() bool { func (w *inotify) isClosed() bool {
select { select {
case <-w.done: case <-w.done:
return true return true
...@@ -300,15 +228,14 @@ func (w *Watcher) isClosed() bool { ...@@ -300,15 +228,14 @@ func (w *Watcher) isClosed() bool {
} }
} }
// Close removes all watches and closes the Events channel. func (w *inotify) Close() error {
func (w *Watcher) Close() error { w.doneMu.Lock()
w.closeMu.Lock()
if w.isClosed() { if w.isClosed() {
w.closeMu.Unlock() w.doneMu.Unlock()
return nil return nil
} }
close(w.done) close(w.done)
w.closeMu.Unlock() w.doneMu.Unlock()
// Causes any blocking reads to return with an error, provided the file // Causes any blocking reads to return with an error, provided the file
// still supports deadline operations. // still supports deadline operations.
...@@ -323,78 +250,104 @@ func (w *Watcher) Close() error { ...@@ -323,78 +250,104 @@ func (w *Watcher) Close() error {
return nil return nil
} }
// Add starts monitoring the path for changes. func (w *inotify) Add(name string) error { return w.AddWith(name) }
//
// A path can only be watched once; watching it more than once is a no-op and will func (w *inotify) AddWith(path string, opts ...addOpt) error {
// 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) }
// 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 {
if w.isClosed() { if w.isClosed() {
return ErrClosed return ErrClosed
} }
if debug {
fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s AddWith(%q)\n",
time.Now().Format("15:04:05.000000000"), path)
}
with := getOptions(opts...)
if !w.xSupports(with.op) {
return fmt.Errorf("%w: %s", xErrUnsupported, with.op)
}
name = filepath.Clean(name) path, recurse := recursivePath(path)
_ = getOptions(opts...) if recurse {
return filepath.WalkDir(path, func(root string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
if root == path {
return fmt.Errorf("fsnotify: not a directory: %q", path)
}
return nil
}
var flags uint32 = unix.IN_MOVED_TO | unix.IN_MOVED_FROM | // Send a Create event when adding new directory from a recursive
unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY | // watch; this is for "mkdir -p one/two/three". Usually all those
unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF // directories will be created before we can set up watchers on the
// subdirectories, so only "one" would be sent as a Create event and
// not "one/two" and "one/two/three" (inotifywait -r has the same
// problem).
if with.sendCreate && root != path {
w.sendEvent(Event{Name: root, Op: Create})
}
return w.add(root, with, true)
})
}
return w.watches.updatePath(name, func(existing *watch) (*watch, error) { return w.add(path, with, false)
}
func (w *inotify) add(path string, with withOpts, recurse bool) error {
var flags uint32
if with.noFollow {
flags |= unix.IN_DONT_FOLLOW
}
if with.op.Has(Create) {
flags |= unix.IN_CREATE
}
if with.op.Has(Write) {
flags |= unix.IN_MODIFY
}
if with.op.Has(Remove) {
flags |= unix.IN_DELETE | unix.IN_DELETE_SELF
}
if with.op.Has(Rename) {
flags |= unix.IN_MOVED_TO | unix.IN_MOVED_FROM | unix.IN_MOVE_SELF
}
if with.op.Has(Chmod) {
flags |= unix.IN_ATTRIB
}
if with.op.Has(xUnportableOpen) {
flags |= unix.IN_OPEN
}
if with.op.Has(xUnportableRead) {
flags |= unix.IN_ACCESS
}
if with.op.Has(xUnportableCloseWrite) {
flags |= unix.IN_CLOSE_WRITE
}
if with.op.Has(xUnportableCloseRead) {
flags |= unix.IN_CLOSE_NOWRITE
}
return w.register(path, flags, recurse)
}
func (w *inotify) register(path string, flags uint32, recurse bool) error {
return w.watches.updatePath(path, func(existing *watch) (*watch, error) {
if existing != nil { if existing != nil {
flags |= existing.flags | unix.IN_MASK_ADD flags |= existing.flags | unix.IN_MASK_ADD
} }
wd, err := unix.InotifyAddWatch(w.fd, name, flags) wd, err := unix.InotifyAddWatch(w.fd, path, flags)
if wd == -1 { if wd == -1 {
return nil, err return nil, err
} }
if existing == nil { if existing == nil {
return &watch{ return &watch{
wd: uint32(wd), wd: uint32(wd),
path: name, path: path,
flags: flags, flags: flags,
recurse: recurse,
}, nil }, nil
} }
...@@ -404,49 +357,44 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error { ...@@ -404,49 +357,44 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error {
}) })
} }
// Remove stops monitoring the path for changes. func (w *inotify) Remove(name string) error {
//
// 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 {
if w.isClosed() { if w.isClosed() {
return nil return nil
} }
if debug {
fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s Remove(%q)\n",
time.Now().Format("15:04:05.000000000"), name)
}
return w.remove(filepath.Clean(name)) return w.remove(filepath.Clean(name))
} }
func (w *Watcher) remove(name string) error { func (w *inotify) remove(name string) error {
wd, ok := w.watches.removePath(name) wds, err := w.watches.removePath(name)
if !ok { if err != nil {
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name) return err
} }
success, errno := unix.InotifyRmWatch(w.fd, wd) for _, wd := range wds {
if success == -1 { _, err := unix.InotifyRmWatch(w.fd, wd)
// TODO: Perhaps it's not helpful to return an error here in every case; if err != nil {
// The only two possible errors are: // TODO: Perhaps it's not helpful to return an error here in every
// // case; the only two possible errors are:
// - EBADF, which happens when w.fd is not a valid file descriptor //
// of any kind. // EBADF, which happens when w.fd is not a valid file descriptor of
// - EINVAL, which is when fd is not an inotify descriptor or wd // any kind.
// is not a valid watch descriptor. Watch descriptors are //
// invalidated when they are removed explicitly or implicitly; // EINVAL, which is when fd is not an inotify descriptor or wd is
// explicitly by inotify_rm_watch, implicitly when the file they // not a valid watch descriptor. Watch descriptors are invalidated
// are watching is deleted. // when they are removed explicitly or implicitly; explicitly by
return errno // inotify_rm_watch, implicitly when the file they are watching is
// deleted.
return err
}
} }
return nil return nil
} }
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not func (w *inotify) WatchList() []string {
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string {
if w.isClosed() { if w.isClosed() {
return nil return nil
} }
...@@ -463,7 +411,7 @@ func (w *Watcher) WatchList() []string { ...@@ -463,7 +411,7 @@ func (w *Watcher) WatchList() []string {
// readEvents reads from the inotify file descriptor, converts the // readEvents reads from the inotify file descriptor, converts the
// received events into Event objects and sends them via the Events channel // received events into Event objects and sends them via the Events channel
func (w *Watcher) readEvents() { func (w *inotify) readEvents() {
defer func() { defer func() {
close(w.doneResp) close(w.doneResp)
close(w.Errors) close(w.Errors)
...@@ -506,15 +454,17 @@ func (w *Watcher) readEvents() { ...@@ -506,15 +454,17 @@ func (w *Watcher) readEvents() {
continue continue
} }
var offset uint32
// We don't know how many events we just read into the buffer // We don't know how many events we just read into the buffer
// While the offset points to at least one whole event... // While the offset points to at least one whole event...
var offset uint32
for offset <= uint32(n-unix.SizeofInotifyEvent) { for offset <= uint32(n-unix.SizeofInotifyEvent) {
var ( var (
// Point "raw" to the event in the buffer // Point "raw" to the event in the buffer
raw = (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset])) raw = (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset]))
mask = uint32(raw.Mask) mask = uint32(raw.Mask)
nameLen = uint32(raw.Len) nameLen = uint32(raw.Len)
// Move to the next event in the buffer
next = func() { offset += unix.SizeofInotifyEvent + nameLen }
) )
if mask&unix.IN_Q_OVERFLOW != 0 { if mask&unix.IN_Q_OVERFLOW != 0 {
...@@ -523,21 +473,53 @@ func (w *Watcher) readEvents() { ...@@ -523,21 +473,53 @@ func (w *Watcher) readEvents() {
} }
} }
// If the event happened to the watched directory or the watched file, the kernel /// If the event happened to the watched directory or the watched
// doesn't append the filename to the event, but we would like to always fill the /// file, the kernel doesn't append the filename to the event, but
// the "Name" field with a valid filename. We retrieve the path of the watch from /// we would like to always fill the the "Name" field with a valid
// the "paths" map. /// filename. We retrieve the path of the watch from the "paths"
/// map.
watch := w.watches.byWd(uint32(raw.Wd)) watch := w.watches.byWd(uint32(raw.Wd))
/// Can be nil if Remove() was called in another goroutine for this
/// path inbetween reading the events from the kernel and reading
/// the internal state. Not much we can do about it, so just skip.
/// See #616.
if watch == nil {
next()
continue
}
name := watch.path
if nameLen > 0 {
/// Point "bytes" at the first byte of the filename
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen]
/// The filename is padded with NULL bytes. TrimRight() gets rid of those.
name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000")
}
if debug {
internal.Debug(name, raw.Mask, raw.Cookie)
}
if mask&unix.IN_IGNORED != 0 { //&& event.Op != 0
next()
continue
}
// inotify will automatically remove the watch on deletes; just need // inotify will automatically remove the watch on deletes; just need
// to clean our state here. // to clean our state here.
if watch != nil && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF { if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
w.watches.remove(watch.wd) w.watches.remove(watch.wd)
} }
// We can't really update the state when a watched path is moved; // We can't really update the state when a watched path is moved;
// only IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove // only IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove
// the watch. // the watch.
if watch != nil && mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF { if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF {
if watch.recurse {
next() // Do nothing
continue
}
err := w.remove(watch.path) err := w.remove(watch.path)
if err != nil && !errors.Is(err, ErrNonExistentWatch) { if err != nil && !errors.Is(err, ErrNonExistentWatch) {
if !w.sendError(err) { if !w.sendError(err) {
...@@ -546,34 +528,69 @@ func (w *Watcher) readEvents() { ...@@ -546,34 +528,69 @@ func (w *Watcher) readEvents() {
} }
} }
var name string /// Skip if we're watching both this path and the parent; the parent
if watch != nil { /// will already send a delete so no need to do it twice.
name = watch.path if mask&unix.IN_DELETE_SELF != 0 {
} if _, ok := w.watches.path[filepath.Dir(watch.path)]; ok {
if nameLen > 0 { next()
// Point "bytes" at the first byte of the filename continue
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen] }
// The filename is padded with NULL bytes. TrimRight() gets rid of those.
name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000")
} }
event := w.newEvent(name, mask) ev := w.newEvent(name, mask, raw.Cookie)
// Need to update watch path for recurse.
if watch.recurse {
isDir := mask&unix.IN_ISDIR == unix.IN_ISDIR
/// New directory created: set up watch on it.
if isDir && ev.Has(Create) {
err := w.register(ev.Name, watch.flags, true)
if !w.sendError(err) {
return
}
// Send the events that are not ignored on the events channel // This was a directory rename, so we need to update all
if mask&unix.IN_IGNORED == 0 { // the children.
if !w.sendEvent(event) { //
return // TODO: this is of course pretty slow; we should use a
// better data structure for storing all of this, e.g. store
// children in the watch. I have some code for this in my
// kqueue refactor we can use in the future. For now I'm
// okay with this as it's not publicly available.
// Correctness first, performance second.
if ev.renamedFrom != "" {
w.watches.mu.Lock()
for k, ww := range w.watches.wd {
if k == watch.wd || ww.path == ev.Name {
continue
}
if strings.HasPrefix(ww.path, ev.renamedFrom) {
ww.path = strings.Replace(ww.path, ev.renamedFrom, ev.Name, 1)
w.watches.wd[k] = ww
}
}
w.watches.mu.Unlock()
}
} }
} }
// Move to the next event in the buffer /// Send the events that are not ignored on the events channel
offset += unix.SizeofInotifyEvent + nameLen if !w.sendEvent(ev) {
return
}
next()
} }
} }
} }
// newEvent returns an platform-independent Event based on an inotify mask. func (w *inotify) isRecursive(path string) bool {
func (w *Watcher) newEvent(name string, mask uint32) Event { ww := w.watches.byPath(path)
if ww == nil { // path could be a file, so also check the Dir.
ww = w.watches.byPath(filepath.Dir(path))
}
return ww != nil && ww.recurse
}
func (w *inotify) newEvent(name string, mask, cookie uint32) Event {
e := Event{Name: name} e := Event{Name: name}
if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO { if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO {
e.Op |= Create e.Op |= Create
...@@ -584,11 +601,58 @@ func (w *Watcher) newEvent(name string, mask uint32) Event { ...@@ -584,11 +601,58 @@ func (w *Watcher) newEvent(name string, mask uint32) Event {
if mask&unix.IN_MODIFY == unix.IN_MODIFY { if mask&unix.IN_MODIFY == unix.IN_MODIFY {
e.Op |= Write e.Op |= Write
} }
if mask&unix.IN_OPEN == unix.IN_OPEN {
e.Op |= xUnportableOpen
}
if mask&unix.IN_ACCESS == unix.IN_ACCESS {
e.Op |= xUnportableRead
}
if mask&unix.IN_CLOSE_WRITE == unix.IN_CLOSE_WRITE {
e.Op |= xUnportableCloseWrite
}
if mask&unix.IN_CLOSE_NOWRITE == unix.IN_CLOSE_NOWRITE {
e.Op |= xUnportableCloseRead
}
if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM { if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM {
e.Op |= Rename e.Op |= Rename
} }
if mask&unix.IN_ATTRIB == unix.IN_ATTRIB { if mask&unix.IN_ATTRIB == unix.IN_ATTRIB {
e.Op |= Chmod e.Op |= Chmod
} }
if cookie != 0 {
if mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM {
w.cookiesMu.Lock()
w.cookies[w.cookieIndex] = koekje{cookie: cookie, path: e.Name}
w.cookieIndex++
if w.cookieIndex > 9 {
w.cookieIndex = 0
}
w.cookiesMu.Unlock()
} else if mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO {
w.cookiesMu.Lock()
var prev string
for _, c := range w.cookies {
if c.cookie == cookie {
prev = c.path
break
}
}
w.cookiesMu.Unlock()
e.renamedFrom = prev
}
}
return e return e
} }
func (w *inotify) xSupports(op Op) bool {
return true // Supports everything.
}
func (w *inotify) state() {
w.watches.mu.Lock()
defer w.watches.mu.Unlock()
for wd, ww := range w.watches.wd {
fmt.Fprintf(os.Stderr, "%4d: recurse=%t %q\n", wd, ww.recurse, ww.path)
}
}
//go:build freebsd || openbsd || netbsd || dragonfly || darwin //go:build freebsd || openbsd || netbsd || dragonfly || darwin
// +build freebsd openbsd netbsd dragonfly darwin
// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh
package fsnotify package fsnotify
...@@ -11,174 +7,195 @@ import ( ...@@ -11,174 +7,195 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"sync" "sync"
"time"
"github.com/fsnotify/fsnotify/internal"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
// Watcher watches a set of paths, delivering events on a channel. type kqueue struct {
//
// 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.
Events chan Event 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 Errors chan error
done chan struct{} kq int // File descriptor (as returned by the kqueue() syscall).
kq int // File descriptor (as returned by the kqueue() syscall). closepipe [2]int // Pipe used for closing kq.
closepipe [2]int // Pipe used for closing. watches *watches
mu sync.Mutex // Protects access to watcher data done chan struct{}
watches map[string]int // Watched file descriptors (key: path). doneMu sync.Mutex
watchesByDir map[string]map[int]struct{} // Watched file descriptors indexed by the parent directory (key: dirname(path)).
userWatches map[string]struct{} // Watches added with Watcher.Add()
dirFlags map[string]uint32 // Watched directories to fflags used in kqueue.
paths map[int]pathInfo // File descriptors to path names for processing kqueue events.
fileExists map[string]struct{} // Keep track of if we know this file exists (to stop duplicate create events).
isClosed bool // Set to true when Close() is first called
} }
type pathInfo struct { type (
name string watches struct {
isDir bool mu sync.RWMutex
wd map[int]watch // wd → watch
path map[string]int // pathname → wd
byDir map[string]map[int]struct{} // dirname(path) → wd
seen map[string]struct{} // Keep track of if we know this file exists.
byUser map[string]struct{} // Watches added with Watcher.Add()
}
watch struct {
wd int
name string
linkName string // In case of links; name is the target, and this is the link.
isDir bool
dirFlags uint32
}
)
func newWatches() *watches {
return &watches{
wd: make(map[int]watch),
path: make(map[string]int),
byDir: make(map[string]map[int]struct{}),
seen: make(map[string]struct{}),
byUser: make(map[string]struct{}),
}
} }
// NewWatcher creates a new Watcher. func (w *watches) listPaths(userOnly bool) []string {
func NewWatcher() (*Watcher, error) { w.mu.RLock()
return NewBufferedWatcher(0) defer w.mu.RUnlock()
if userOnly {
l := make([]string, 0, len(w.byUser))
for p := range w.byUser {
l = append(l, p)
}
return l
}
l := make([]string, 0, len(w.path))
for p := range w.path {
l = append(l, p)
}
return l
} }
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events func (w *watches) watchesInDir(path string) []string {
// channel. w.mu.RLock()
// defer w.mu.RUnlock()
// 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 l := make([]string, 0, 4)
// permissions). An unbuffered Watcher will perform better for almost all use for fd := range w.byDir[path] {
// cases, and whenever possible you will be better off increasing the kernel info := w.wd[fd]
// buffers instead of adding a large userspace buffer. if _, ok := w.byUser[info.name]; !ok {
func NewBufferedWatcher(sz uint) (*Watcher, error) { l = append(l, info.name)
}
}
return l
}
// Mark path as added by the user.
func (w *watches) addUserWatch(path string) {
w.mu.Lock()
defer w.mu.Unlock()
w.byUser[path] = struct{}{}
}
func (w *watches) addLink(path string, fd int) {
w.mu.Lock()
defer w.mu.Unlock()
w.path[path] = fd
w.seen[path] = struct{}{}
}
func (w *watches) add(path, linkPath string, fd int, isDir bool) {
w.mu.Lock()
defer w.mu.Unlock()
w.path[path] = fd
w.wd[fd] = watch{wd: fd, name: path, linkName: linkPath, isDir: isDir}
parent := filepath.Dir(path)
byDir, ok := w.byDir[parent]
if !ok {
byDir = make(map[int]struct{}, 1)
w.byDir[parent] = byDir
}
byDir[fd] = struct{}{}
}
func (w *watches) byWd(fd int) (watch, bool) {
w.mu.RLock()
defer w.mu.RUnlock()
info, ok := w.wd[fd]
return info, ok
}
func (w *watches) byPath(path string) (watch, bool) {
w.mu.RLock()
defer w.mu.RUnlock()
info, ok := w.wd[w.path[path]]
return info, ok
}
func (w *watches) updateDirFlags(path string, flags uint32) {
w.mu.Lock()
defer w.mu.Unlock()
fd := w.path[path]
info := w.wd[fd]
info.dirFlags = flags
w.wd[fd] = info
}
func (w *watches) remove(fd int, path string) bool {
w.mu.Lock()
defer w.mu.Unlock()
isDir := w.wd[fd].isDir
delete(w.path, path)
delete(w.byUser, path)
parent := filepath.Dir(path)
delete(w.byDir[parent], fd)
if len(w.byDir[parent]) == 0 {
delete(w.byDir, parent)
}
delete(w.wd, fd)
delete(w.seen, path)
return isDir
}
func (w *watches) markSeen(path string, exists bool) {
w.mu.Lock()
defer w.mu.Unlock()
if exists {
w.seen[path] = struct{}{}
} else {
delete(w.seen, path)
}
}
func (w *watches) seenBefore(path string) bool {
w.mu.RLock()
defer w.mu.RUnlock()
_, ok := w.seen[path]
return ok
}
func newBackend(ev chan Event, errs chan error) (backend, error) {
return newBufferedBackend(0, ev, errs)
}
func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
kq, closepipe, err := newKqueue() kq, closepipe, err := newKqueue()
if err != nil { if err != nil {
return nil, err return nil, err
} }
w := &Watcher{ w := &kqueue{
kq: kq, Events: ev,
closepipe: closepipe, Errors: errs,
watches: make(map[string]int), kq: kq,
watchesByDir: make(map[string]map[int]struct{}), closepipe: closepipe,
dirFlags: make(map[string]uint32), done: make(chan struct{}),
paths: make(map[int]pathInfo), watches: newWatches(),
fileExists: make(map[string]struct{}),
userWatches: make(map[string]struct{}),
Events: make(chan Event, sz),
Errors: make(chan error),
done: make(chan struct{}),
} }
go w.readEvents() go w.readEvents()
...@@ -203,6 +220,8 @@ func newKqueue() (kq int, closepipe [2]int, err error) { ...@@ -203,6 +220,8 @@ func newKqueue() (kq int, closepipe [2]int, err error) {
unix.Close(kq) unix.Close(kq)
return kq, closepipe, err return kq, closepipe, err
} }
unix.CloseOnExec(closepipe[0])
unix.CloseOnExec(closepipe[1])
// Register changes to listen on the closepipe. // Register changes to listen on the closepipe.
changes := make([]unix.Kevent_t, 1) changes := make([]unix.Kevent_t, 1)
...@@ -221,166 +240,108 @@ func newKqueue() (kq int, closepipe [2]int, err error) { ...@@ -221,166 +240,108 @@ func newKqueue() (kq int, closepipe [2]int, err error) {
} }
// Returns true if the event was sent, or false if watcher is closed. // Returns true if the event was sent, or false if watcher is closed.
func (w *Watcher) sendEvent(e Event) bool { func (w *kqueue) sendEvent(e Event) bool {
select { select {
case w.Events <- e:
return true
case <-w.done: case <-w.done:
return false return false
case w.Events <- e:
return true
} }
} }
// Returns true if the error was sent, or false if watcher is closed. // Returns true if the error was sent, or false if watcher is closed.
func (w *Watcher) sendError(err error) bool { func (w *kqueue) sendError(err error) bool {
if err == nil {
return true
}
select { select {
case <-w.done:
return false
case w.Errors <- err: case w.Errors <- err:
return true return true
}
}
func (w *kqueue) isClosed() bool {
select {
case <-w.done: case <-w.done:
return true
default:
return false return false
} }
} }
// Close removes all watches and closes the Events channel. func (w *kqueue) Close() error {
func (w *Watcher) Close() error { w.doneMu.Lock()
w.mu.Lock() if w.isClosed() {
if w.isClosed { w.doneMu.Unlock()
w.mu.Unlock()
return nil return nil
} }
w.isClosed = true close(w.done)
w.doneMu.Unlock()
// copy paths to remove while locked pathsToRemove := w.watches.listPaths(false)
pathsToRemove := make([]string, 0, len(w.watches))
for name := range w.watches {
pathsToRemove = append(pathsToRemove, name)
}
w.mu.Unlock() // Unlock before calling Remove, which also locks
for _, name := range pathsToRemove { for _, name := range pathsToRemove {
w.Remove(name) w.Remove(name)
} }
// Send "quit" message to the reader goroutine. // Send "quit" message to the reader goroutine.
unix.Close(w.closepipe[1]) unix.Close(w.closepipe[1])
close(w.done)
return nil return nil
} }
// Add starts monitoring the path for changes. func (w *kqueue) Add(name string) error { return w.AddWith(name) }
//
// 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) }
// AddWith is like [Watcher.Add], but allows adding options. When using Add() func (w *kqueue) AddWith(name string, opts ...addOpt) error {
// the defaults described below are used. if debug {
// fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s AddWith(%q)\n",
// Possible options are: time.Now().Format("15:04:05.000000000"), name)
// }
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes). with := getOptions(opts...)
func (w *Watcher) AddWith(name string, opts ...addOpt) error { if !w.xSupports(with.op) {
_ = getOptions(opts...) return fmt.Errorf("%w: %s", xErrUnsupported, with.op)
}
w.mu.Lock()
w.userWatches[name] = struct{}{}
w.mu.Unlock()
_, err := w.addWatch(name, noteAllEvents) _, err := w.addWatch(name, noteAllEvents)
return err if err != nil {
return err
}
w.watches.addUserWatch(name)
return nil
} }
// Remove stops monitoring the path for changes. func (w *kqueue) Remove(name string) error {
// if debug {
// Directories are always removed non-recursively. For example, if you added fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s Remove(%q)\n",
// /tmp/dir and /tmp/dir/subdir then you will need to remove both. time.Now().Format("15:04:05.000000000"), name)
// }
// 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 w.remove(name, true) return w.remove(name, true)
} }
func (w *Watcher) remove(name string, unwatchFiles bool) error { func (w *kqueue) remove(name string, unwatchFiles bool) error {
name = filepath.Clean(name) if w.isClosed() {
w.mu.Lock()
if w.isClosed {
w.mu.Unlock()
return nil return nil
} }
watchfd, ok := w.watches[name]
w.mu.Unlock() name = filepath.Clean(name)
info, ok := w.watches.byPath(name)
if !ok { if !ok {
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name) return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
} }
err := w.register([]int{watchfd}, unix.EV_DELETE, 0) err := w.register([]int{info.wd}, unix.EV_DELETE, 0)
if err != nil { if err != nil {
return err return err
} }
unix.Close(watchfd) unix.Close(info.wd)
w.mu.Lock()
isDir := w.paths[watchfd].isDir
delete(w.watches, name)
delete(w.userWatches, name)
parentName := filepath.Dir(name)
delete(w.watchesByDir[parentName], watchfd)
if len(w.watchesByDir[parentName]) == 0 {
delete(w.watchesByDir, parentName)
}
delete(w.paths, watchfd) isDir := w.watches.remove(info.wd, name)
delete(w.dirFlags, name)
delete(w.fileExists, name)
w.mu.Unlock()
// Find all watched paths that are in this directory that are not external. // Find all watched paths that are in this directory that are not external.
if unwatchFiles && isDir { if unwatchFiles && isDir {
var pathsToRemove []string pathsToRemove := w.watches.watchesInDir(name)
w.mu.Lock()
for fd := range w.watchesByDir[name] {
path := w.paths[fd]
if _, ok := w.userWatches[path.name]; !ok {
pathsToRemove = append(pathsToRemove, path.name)
}
}
w.mu.Unlock()
for _, name := range pathsToRemove { for _, name := range pathsToRemove {
// Since these are internal, not much sense in propagating error to // Since these are internal, not much sense in propagating error to
// the user, as that will just confuse them with an error about a // the user, as that will just confuse them with an error about a
...@@ -391,23 +352,11 @@ func (w *Watcher) remove(name string, unwatchFiles bool) error { ...@@ -391,23 +352,11 @@ func (w *Watcher) remove(name string, unwatchFiles bool) error {
return nil return nil
} }
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not func (w *kqueue) WatchList() []string {
// yet removed). if w.isClosed() {
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string {
w.mu.Lock()
defer w.mu.Unlock()
if w.isClosed {
return nil return nil
} }
return w.watches.listPaths(true)
entries := make([]string, 0, len(w.userWatches))
for pathname := range w.userWatches {
entries = append(entries, pathname)
}
return entries
} }
// Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE) // Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE)
...@@ -417,34 +366,26 @@ const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | un ...@@ -417,34 +366,26 @@ const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | un
// described in kevent(2). // described in kevent(2).
// //
// Returns the real path to the file which was added, with symlinks resolved. // Returns the real path to the file which was added, with symlinks resolved.
func (w *Watcher) addWatch(name string, flags uint32) (string, error) { func (w *kqueue) addWatch(name string, flags uint32) (string, error) {
var isDir bool if w.isClosed() {
name = filepath.Clean(name)
w.mu.Lock()
if w.isClosed {
w.mu.Unlock()
return "", ErrClosed return "", ErrClosed
} }
watchfd, alreadyWatching := w.watches[name]
// We already have a watch, but we can still override flags.
if alreadyWatching {
isDir = w.paths[watchfd].isDir
}
w.mu.Unlock()
name = filepath.Clean(name)
info, alreadyWatching := w.watches.byPath(name)
if !alreadyWatching { if !alreadyWatching {
fi, err := os.Lstat(name) fi, err := os.Lstat(name)
if err != nil { if err != nil {
return "", err return "", err
} }
// Don't watch sockets or named pipes // Don't watch sockets or named pipes.
if (fi.Mode()&os.ModeSocket == os.ModeSocket) || (fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe) { if (fi.Mode()&os.ModeSocket == os.ModeSocket) || (fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe) {
return "", nil return "", nil
} }
// Follow Symlinks. // Follow symlinks.
if fi.Mode()&os.ModeSymlink == os.ModeSymlink { if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
link, err := os.Readlink(name) link, err := os.Readlink(name)
if err != nil { if err != nil {
...@@ -455,18 +396,15 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) { ...@@ -455,18 +396,15 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
return "", nil return "", nil
} }
w.mu.Lock() _, alreadyWatching = w.watches.byPath(link)
_, alreadyWatching = w.watches[link]
w.mu.Unlock()
if alreadyWatching { if alreadyWatching {
// Add to watches so we don't get spurious Create events later // Add to watches so we don't get spurious Create events later
// on when we diff the directories. // on when we diff the directories.
w.watches[name] = 0 w.watches.addLink(name, 0)
w.fileExists[name] = struct{}{}
return link, nil return link, nil
} }
info.linkName = name
name = link name = link
fi, err = os.Lstat(name) fi, err = os.Lstat(name)
if err != nil { if err != nil {
...@@ -477,7 +415,7 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) { ...@@ -477,7 +415,7 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
// Retry on EINTR; open() can return EINTR in practice on macOS. // Retry on EINTR; open() can return EINTR in practice on macOS.
// See #354, and Go issues 11180 and 39237. // See #354, and Go issues 11180 and 39237.
for { for {
watchfd, err = unix.Open(name, openMode, 0) info.wd, err = unix.Open(name, openMode, 0)
if err == nil { if err == nil {
break break
} }
...@@ -488,40 +426,25 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) { ...@@ -488,40 +426,25 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
return "", err return "", err
} }
isDir = fi.IsDir() info.isDir = fi.IsDir()
} }
err := w.register([]int{watchfd}, unix.EV_ADD|unix.EV_CLEAR|unix.EV_ENABLE, flags) err := w.register([]int{info.wd}, unix.EV_ADD|unix.EV_CLEAR|unix.EV_ENABLE, flags)
if err != nil { if err != nil {
unix.Close(watchfd) unix.Close(info.wd)
return "", err return "", err
} }
if !alreadyWatching { if !alreadyWatching {
w.mu.Lock() w.watches.add(name, info.linkName, info.wd, info.isDir)
parentName := filepath.Dir(name)
w.watches[name] = watchfd
watchesByDir, ok := w.watchesByDir[parentName]
if !ok {
watchesByDir = make(map[int]struct{}, 1)
w.watchesByDir[parentName] = watchesByDir
}
watchesByDir[watchfd] = struct{}{}
w.paths[watchfd] = pathInfo{name: name, isDir: isDir}
w.mu.Unlock()
} }
if isDir { // Watch the directory if it has not been watched before, or if it was
// Watch the directory if it has not been watched before, or if it was // watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles)
// watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles) if info.isDir {
w.mu.Lock()
watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE && watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE &&
(!alreadyWatching || (w.dirFlags[name]&unix.NOTE_WRITE) != unix.NOTE_WRITE) (!alreadyWatching || (info.dirFlags&unix.NOTE_WRITE) != unix.NOTE_WRITE)
// Store flags so this watch can be updated later w.watches.updateDirFlags(name, flags)
w.dirFlags[name] = flags
w.mu.Unlock()
if watchDir { if watchDir {
if err := w.watchDirectoryFiles(name); err != nil { if err := w.watchDirectoryFiles(name); err != nil {
...@@ -534,7 +457,7 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) { ...@@ -534,7 +457,7 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
// readEvents reads from kqueue and converts the received kevents into // readEvents reads from kqueue and converts the received kevents into
// Event values that it sends down the Events channel. // Event values that it sends down the Events channel.
func (w *Watcher) readEvents() { func (w *kqueue) readEvents() {
defer func() { defer func() {
close(w.Events) close(w.Events)
close(w.Errors) close(w.Errors)
...@@ -543,50 +466,65 @@ func (w *Watcher) readEvents() { ...@@ -543,50 +466,65 @@ func (w *Watcher) readEvents() {
}() }()
eventBuffer := make([]unix.Kevent_t, 10) eventBuffer := make([]unix.Kevent_t, 10)
for closed := false; !closed; { for {
kevents, err := w.read(eventBuffer) kevents, err := w.read(eventBuffer)
// EINTR is okay, the syscall was interrupted before timeout expired. // EINTR is okay, the syscall was interrupted before timeout expired.
if err != nil && err != unix.EINTR { if err != nil && err != unix.EINTR {
if !w.sendError(fmt.Errorf("fsnotify.readEvents: %w", err)) { if !w.sendError(fmt.Errorf("fsnotify.readEvents: %w", err)) {
closed = true return
} }
continue
} }
// Flush the events we received to the Events channel
for _, kevent := range kevents { for _, kevent := range kevents {
var ( var (
watchfd = int(kevent.Ident) wd = int(kevent.Ident)
mask = uint32(kevent.Fflags) mask = uint32(kevent.Fflags)
) )
// Shut down the loop when the pipe is closed, but only after all // Shut down the loop when the pipe is closed, but only after all
// other events have been processed. // other events have been processed.
if watchfd == w.closepipe[0] { if wd == w.closepipe[0] {
closed = true return
continue
} }
w.mu.Lock() path, ok := w.watches.byWd(wd)
path := w.paths[watchfd] if debug {
w.mu.Unlock() internal.Debug(path.name, &kevent)
}
event := w.newEvent(path.name, mask) // On macOS it seems that sometimes an event with Ident=0 is
// delivered, and no other flags/information beyond that, even
// though we never saw such a file descriptor. For example in
// TestWatchSymlink/277 (usually at the end, but sometimes sooner):
//
// fmt.Printf("READ: %2d %#v\n", kevent.Ident, kevent)
// unix.Kevent_t{Ident:0x2a, Filter:-4, Flags:0x25, Fflags:0x2, Data:0, Udata:(*uint8)(nil)}
// unix.Kevent_t{Ident:0x0, Filter:-4, Flags:0x25, Fflags:0x2, Data:0, Udata:(*uint8)(nil)}
//
// The first is a normal event, the second with Ident 0. No error
// flag, no data, no ... nothing.
//
// I read a bit through bsd/kern_event.c from the xnu source, but I
// don't really see an obvious location where this is triggered –
// this doesn't seem intentional, but idk...
//
// Technically fd 0 is a valid descriptor, so only skip it if
// there's no path, and if we're on macOS.
if !ok && kevent.Ident == 0 && runtime.GOOS == "darwin" {
continue
}
event := w.newEvent(path.name, path.linkName, mask)
if event.Has(Rename) || event.Has(Remove) { if event.Has(Rename) || event.Has(Remove) {
w.remove(event.Name, false) w.remove(event.Name, false)
w.mu.Lock() w.watches.markSeen(event.Name, false)
delete(w.fileExists, event.Name)
w.mu.Unlock()
} }
if path.isDir && event.Has(Write) && !event.Has(Remove) { if path.isDir && event.Has(Write) && !event.Has(Remove) {
w.sendDirectoryChangeEvents(event.Name) w.dirChange(event.Name)
} else { } else if !w.sendEvent(event) {
if !w.sendEvent(event) { return
closed = true
continue
}
} }
if event.Has(Remove) { if event.Has(Remove) {
...@@ -594,25 +532,34 @@ func (w *Watcher) readEvents() { ...@@ -594,25 +532,34 @@ func (w *Watcher) readEvents() {
// mv f1 f2 will delete f2, then create f2. // mv f1 f2 will delete f2, then create f2.
if path.isDir { if path.isDir {
fileDir := filepath.Clean(event.Name) fileDir := filepath.Clean(event.Name)
w.mu.Lock() _, found := w.watches.byPath(fileDir)
_, found := w.watches[fileDir]
w.mu.Unlock()
if found { if found {
err := w.sendDirectoryChangeEvents(fileDir) // TODO: this branch is never triggered in any test.
if err != nil { // Added in d6220df (2012).
if !w.sendError(err) { // isDir check added in 8611c35 (2016): https://github.com/fsnotify/fsnotify/pull/111
closed = true //
} // I don't really get how this can be triggered either.
// And it wasn't triggered in the patch that added it,
// either.
//
// Original also had a comment:
// make sure the directory exists before we watch for
// changes. When we do a recursive watch and perform
// rm -rf, the parent directory might have gone
// missing, ignore the missing directory and let the
// upcoming delete event remove the watch from the
// parent directory.
err := w.dirChange(fileDir)
if !w.sendError(err) {
return
} }
} }
} else { } else {
filePath := filepath.Clean(event.Name) path := filepath.Clean(event.Name)
if fi, err := os.Lstat(filePath); err == nil { if fi, err := os.Lstat(path); err == nil {
err := w.sendFileCreatedEventIfNew(filePath, fi) err := w.sendCreateIfNew(path, fi)
if err != nil { if !w.sendError(err) {
if !w.sendError(err) { return
closed = true
}
} }
} }
} }
...@@ -622,8 +569,14 @@ func (w *Watcher) readEvents() { ...@@ -622,8 +569,14 @@ func (w *Watcher) readEvents() {
} }
// newEvent returns an platform-independent Event based on kqueue Fflags. // newEvent returns an platform-independent Event based on kqueue Fflags.
func (w *Watcher) newEvent(name string, mask uint32) Event { func (w *kqueue) newEvent(name, linkName string, mask uint32) Event {
e := Event{Name: name} e := Event{Name: name}
if linkName != "" {
// If the user watched "/path/link" then emit events as "/path/link"
// rather than "/path/target".
e.Name = linkName
}
if mask&unix.NOTE_DELETE == unix.NOTE_DELETE { if mask&unix.NOTE_DELETE == unix.NOTE_DELETE {
e.Op |= Remove e.Op |= Remove
} }
...@@ -645,8 +598,7 @@ func (w *Watcher) newEvent(name string, mask uint32) Event { ...@@ -645,8 +598,7 @@ func (w *Watcher) newEvent(name string, mask uint32) Event {
} }
// watchDirectoryFiles to mimic inotify when adding a watch on a directory // watchDirectoryFiles to mimic inotify when adding a watch on a directory
func (w *Watcher) watchDirectoryFiles(dirPath string) error { func (w *kqueue) watchDirectoryFiles(dirPath string) error {
// Get all files
files, err := os.ReadDir(dirPath) files, err := os.ReadDir(dirPath)
if err != nil { if err != nil {
return err return err
...@@ -674,9 +626,7 @@ func (w *Watcher) watchDirectoryFiles(dirPath string) error { ...@@ -674,9 +626,7 @@ func (w *Watcher) watchDirectoryFiles(dirPath string) error {
} }
} }
w.mu.Lock() w.watches.markSeen(cleanPath, true)
w.fileExists[cleanPath] = struct{}{}
w.mu.Unlock()
} }
return nil return nil
...@@ -686,7 +636,7 @@ func (w *Watcher) watchDirectoryFiles(dirPath string) error { ...@@ -686,7 +636,7 @@ func (w *Watcher) watchDirectoryFiles(dirPath string) error {
// //
// This functionality is to have the BSD watcher match the inotify, which sends // This functionality is to have the BSD watcher match the inotify, which sends
// a create event for files created in a watched directory. // a create event for files created in a watched directory.
func (w *Watcher) sendDirectoryChangeEvents(dir string) error { func (w *kqueue) dirChange(dir string) error {
files, err := os.ReadDir(dir) files, err := os.ReadDir(dir)
if err != nil { if err != nil {
// Directory no longer exists: we can ignore this safely. kqueue will // Directory no longer exists: we can ignore this safely. kqueue will
...@@ -694,61 +644,51 @@ func (w *Watcher) sendDirectoryChangeEvents(dir string) error { ...@@ -694,61 +644,51 @@ func (w *Watcher) sendDirectoryChangeEvents(dir string) error {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
return nil return nil
} }
return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err) return fmt.Errorf("fsnotify.dirChange: %w", err)
} }
for _, f := range files { for _, f := range files {
fi, err := f.Info() fi, err := f.Info()
if err != nil { if err != nil {
return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err) return fmt.Errorf("fsnotify.dirChange: %w", err)
} }
err = w.sendFileCreatedEventIfNew(filepath.Join(dir, fi.Name()), fi) err = w.sendCreateIfNew(filepath.Join(dir, fi.Name()), fi)
if err != nil { if err != nil {
// Don't need to send an error if this file isn't readable. // Don't need to send an error if this file isn't readable.
if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) { if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) {
return nil return nil
} }
return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err) return fmt.Errorf("fsnotify.dirChange: %w", err)
} }
} }
return nil return nil
} }
// sendFileCreatedEvent sends a create event if the file isn't already being tracked. // Send a create event if the file isn't already being tracked, and start
func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fi os.FileInfo) (err error) { // watching this file.
w.mu.Lock() func (w *kqueue) sendCreateIfNew(path string, fi os.FileInfo) error {
_, doesExist := w.fileExists[filePath] if !w.watches.seenBefore(path) {
w.mu.Unlock() if !w.sendEvent(Event{Name: path, Op: Create}) {
if !doesExist { return nil
if !w.sendEvent(Event{Name: filePath, Op: Create}) {
return
} }
} }
// like watchDirectoryFiles (but without doing another ReadDir) // Like watchDirectoryFiles, but without doing another ReadDir.
filePath, err = w.internalWatch(filePath, fi) path, err := w.internalWatch(path, fi)
if err != nil { if err != nil {
return err return err
} }
w.watches.markSeen(path, true)
w.mu.Lock()
w.fileExists[filePath] = struct{}{}
w.mu.Unlock()
return nil return nil
} }
func (w *Watcher) internalWatch(name string, fi os.FileInfo) (string, error) { func (w *kqueue) internalWatch(name string, fi os.FileInfo) (string, error) {
if fi.IsDir() { if fi.IsDir() {
// mimic Linux providing delete events for subdirectories, but preserve // mimic Linux providing delete events for subdirectories, but preserve
// the flags used if currently watching subdirectory // the flags used if currently watching subdirectory
w.mu.Lock() info, _ := w.watches.byPath(name)
flags := w.dirFlags[name] return w.addWatch(name, info.dirFlags|unix.NOTE_DELETE|unix.NOTE_RENAME)
w.mu.Unlock()
flags |= unix.NOTE_DELETE | unix.NOTE_RENAME
return w.addWatch(name, flags)
} }
// watch file to mimic Linux inotify // watch file to mimic Linux inotify
...@@ -756,7 +696,7 @@ func (w *Watcher) internalWatch(name string, fi os.FileInfo) (string, error) { ...@@ -756,7 +696,7 @@ func (w *Watcher) internalWatch(name string, fi os.FileInfo) (string, error) {
} }
// Register events with the queue. // Register events with the queue.
func (w *Watcher) register(fds []int, flags int, fflags uint32) error { func (w *kqueue) register(fds []int, flags int, fflags uint32) error {
changes := make([]unix.Kevent_t, len(fds)) changes := make([]unix.Kevent_t, len(fds))
for i, fd := range fds { for i, fd := range fds {
// SetKevent converts int to the platform-specific types. // SetKevent converts int to the platform-specific types.
...@@ -773,10 +713,21 @@ func (w *Watcher) register(fds []int, flags int, fflags uint32) error { ...@@ -773,10 +713,21 @@ func (w *Watcher) register(fds []int, flags int, fflags uint32) error {
} }
// read retrieves pending events, or waits until an event occurs. // read retrieves pending events, or waits until an event occurs.
func (w *Watcher) read(events []unix.Kevent_t) ([]unix.Kevent_t, error) { func (w *kqueue) read(events []unix.Kevent_t) ([]unix.Kevent_t, error) {
n, err := unix.Kevent(w.kq, nil, events, nil) n, err := unix.Kevent(w.kq, nil, events, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return events[0:n], nil return events[0:n], nil
} }
func (w *kqueue) xSupports(op Op) bool {
if runtime.GOOS == "freebsd" {
//return true // Supports everything.
}
if op.Has(xUnportableOpen) || op.Has(xUnportableRead) ||
op.Has(xUnportableCloseWrite) || op.Has(xUnportableCloseRead) {
return false
}
return true
}
//go:build appengine || (!darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows) //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 package fsnotify
import "errors" import "errors"
// Watcher watches a set of paths, delivering events on a channel. type other struct {
//
// 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.
Events chan Event 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 Errors chan error
} }
// NewWatcher creates a new Watcher. func newBackend(ev chan Event, errs chan error) (backend, error) {
func NewWatcher() (*Watcher, error) {
return nil, errors.New("fsnotify not supported on the current platform") return nil, errors.New("fsnotify not supported on the current platform")
} }
func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events return newBackend(ev, errs)
// channel. }
// func (w *other) Close() error { return nil }
// The main use case for this is situations with a very large number of events func (w *other) WatchList() []string { return nil }
// where the kernel buffer size can't be increased (e.g. due to lack of func (w *other) Add(name string) error { return nil }
// permissions). An unbuffered Watcher will perform better for almost all use func (w *other) AddWith(name string, opts ...addOpt) error { return nil }
// cases, and whenever possible you will be better off increasing the kernel func (w *other) Remove(name string) error { return nil }
// buffers instead of adding a large userspace buffer. func (w *other) xSupports(op Op) bool { return false }
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 }
//go:build windows //go:build windows
// +build windows
// Windows backend based on ReadDirectoryChangesW() // Windows backend based on ReadDirectoryChangesW()
// //
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-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 package fsnotify
...@@ -19,123 +15,15 @@ import ( ...@@ -19,123 +15,15 @@ import (
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"time"
"unsafe" "unsafe"
"github.com/fsnotify/fsnotify/internal"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
// Watcher watches a set of paths, delivering events on a channel. type readDirChangesW struct {
//
// 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.
Events chan Event 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 Errors chan error
port windows.Handle // Handle to completion port port windows.Handle // Handle to completion port
...@@ -147,48 +35,40 @@ type Watcher struct { ...@@ -147,48 +35,40 @@ type Watcher struct {
closed bool // Set to true when Close() is first called closed bool // Set to true when Close() is first called
} }
// NewWatcher creates a new Watcher. func newBackend(ev chan Event, errs chan error) (backend, error) {
func NewWatcher() (*Watcher, error) { return newBufferedBackend(50, ev, errs)
return NewBufferedWatcher(50)
} }
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
// 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) {
port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0) port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0)
if err != nil { if err != nil {
return nil, os.NewSyscallError("CreateIoCompletionPort", err) return nil, os.NewSyscallError("CreateIoCompletionPort", err)
} }
w := &Watcher{ w := &readDirChangesW{
Events: ev,
Errors: errs,
port: port, port: port,
watches: make(watchMap), watches: make(watchMap),
input: make(chan *input, 1), input: make(chan *input, 1),
Events: make(chan Event, sz),
Errors: make(chan error),
quit: make(chan chan<- error, 1), quit: make(chan chan<- error, 1),
} }
go w.readEvents() go w.readEvents()
return w, nil return w, nil
} }
func (w *Watcher) isClosed() bool { func (w *readDirChangesW) isClosed() bool {
w.mu.Lock() w.mu.Lock()
defer w.mu.Unlock() defer w.mu.Unlock()
return w.closed 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 { if mask == 0 {
return false return false
} }
event := w.newEvent(name, uint32(mask)) event := w.newEvent(name, uint32(mask))
event.renamedFrom = renamedFrom
select { select {
case ch := <-w.quit: case ch := <-w.quit:
w.quit <- ch w.quit <- ch
...@@ -198,17 +78,19 @@ func (w *Watcher) sendEvent(name string, mask uint64) bool { ...@@ -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. // 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 { select {
case w.Errors <- err: case w.Errors <- err:
return true return true
case <-w.quit: case <-w.quit:
return false
} }
return false
} }
// Close removes all watches and closes the Events channel. func (w *readDirChangesW) Close() error {
func (w *Watcher) Close() error {
if w.isClosed() { if w.isClosed() {
return nil return nil
} }
...@@ -226,57 +108,21 @@ func (w *Watcher) Close() error { ...@@ -226,57 +108,21 @@ func (w *Watcher) Close() error {
return <-ch return <-ch
} }
// Add starts monitoring the path for changes. func (w *readDirChangesW) Add(name string) error { return w.AddWith(name) }
//
// 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) }
// AddWith is like [Watcher.Add], but allows adding options. When using Add() func (w *readDirChangesW) AddWith(name string, opts ...addOpt) error {
// 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 {
if w.isClosed() { if w.isClosed() {
return ErrClosed 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...) with := getOptions(opts...)
if !w.xSupports(with.op) {
return fmt.Errorf("%w: %s", xErrUnsupported, with.op)
}
if with.bufsize < 4096 { if with.bufsize < 4096 {
return fmt.Errorf("fsnotify.WithBufferSize: buffer size cannot be smaller than 4096 bytes") 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 { ...@@ -295,18 +141,14 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error {
return <-in.reply return <-in.reply
} }
// Remove stops monitoring the path for changes. func (w *readDirChangesW) Remove(name string) error {
//
// 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 {
if w.isClosed() { if w.isClosed() {
return nil 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{ in := &input{
op: opRemoveWatch, op: opRemoveWatch,
...@@ -320,11 +162,7 @@ func (w *Watcher) Remove(name string) error { ...@@ -320,11 +162,7 @@ func (w *Watcher) Remove(name string) error {
return <-in.reply return <-in.reply
} }
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not func (w *readDirChangesW) WatchList() []string {
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string {
if w.isClosed() { if w.isClosed() {
return nil return nil
} }
...@@ -335,7 +173,13 @@ func (w *Watcher) WatchList() []string { ...@@ -335,7 +173,13 @@ func (w *Watcher) WatchList() []string {
entries := make([]string, 0, len(w.watches)) entries := make([]string, 0, len(w.watches))
for _, entry := range w.watches { for _, entry := range w.watches {
for _, watchEntry := range entry { for _, watchEntry := range entry {
entries = append(entries, watchEntry.path) 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)
}
} }
} }
...@@ -361,7 +205,7 @@ const ( ...@@ -361,7 +205,7 @@ const (
sysFSIGNORED = 0x8000 sysFSIGNORED = 0x8000
) )
func (w *Watcher) newEvent(name string, mask uint32) Event { func (w *readDirChangesW) newEvent(name string, mask uint32) Event {
e := Event{Name: name} e := Event{Name: name}
if mask&sysFSCREATE == sysFSCREATE || mask&sysFSMOVEDTO == sysFSMOVEDTO { if mask&sysFSCREATE == sysFSCREATE || mask&sysFSMOVEDTO == sysFSMOVEDTO {
e.Op |= Create e.Op |= Create
...@@ -417,7 +261,7 @@ type ( ...@@ -417,7 +261,7 @@ type (
watchMap map[uint32]indexMap watchMap map[uint32]indexMap
) )
func (w *Watcher) wakeupReader() error { func (w *readDirChangesW) wakeupReader() error {
err := windows.PostQueuedCompletionStatus(w.port, 0, 0, nil) err := windows.PostQueuedCompletionStatus(w.port, 0, 0, nil)
if err != nil { if err != nil {
return os.NewSyscallError("PostQueuedCompletionStatus", err) return os.NewSyscallError("PostQueuedCompletionStatus", err)
...@@ -425,7 +269,7 @@ func (w *Watcher) wakeupReader() error { ...@@ -425,7 +269,7 @@ func (w *Watcher) wakeupReader() error {
return nil 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)) attr, err := windows.GetFileAttributes(windows.StringToUTF16Ptr(pathname))
if err != nil { if err != nil {
return "", os.NewSyscallError("GetFileAttributes", err) return "", os.NewSyscallError("GetFileAttributes", err)
...@@ -439,7 +283,7 @@ func (w *Watcher) getDir(pathname string) (dir string, err error) { ...@@ -439,7 +283,7 @@ func (w *Watcher) getDir(pathname string) (dir string, err error) {
return 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), h, err := windows.CreateFile(windows.StringToUTF16Ptr(path),
windows.FILE_LIST_DIRECTORY, windows.FILE_LIST_DIRECTORY,
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
...@@ -482,9 +326,8 @@ func (m watchMap) set(ino *inode, watch *watch) { ...@@ -482,9 +326,8 @@ func (m watchMap) set(ino *inode, watch *watch) {
} }
// Must run within the I/O thread. // Must run within the I/O thread.
func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error { func (w *readDirChangesW) addWatch(pathname string, flags uint64, bufsize int) error {
//pathname, recurse := recursivePath(pathname) pathname, recurse := recursivePath(pathname)
recurse := false
dir, err := w.getDir(pathname) dir, err := w.getDir(pathname)
if err != nil { if err != nil {
...@@ -538,7 +381,7 @@ func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error { ...@@ -538,7 +381,7 @@ func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error {
} }
// Must run within the I/O thread. // 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) pathname, recurse := recursivePath(pathname)
dir, err := w.getDir(pathname) dir, err := w.getDir(pathname)
...@@ -566,11 +409,11 @@ func (w *Watcher) remWatch(pathname string) error { ...@@ -566,11 +409,11 @@ func (w *Watcher) remWatch(pathname string) error {
return fmt.Errorf("%w: %s", ErrNonExistentWatch, pathname) return fmt.Errorf("%w: %s", ErrNonExistentWatch, pathname)
} }
if pathname == dir { if pathname == dir {
w.sendEvent(watch.path, watch.mask&sysFSIGNORED) w.sendEvent(watch.path, "", watch.mask&sysFSIGNORED)
watch.mask = 0 watch.mask = 0
} else { } else {
name := filepath.Base(pathname) 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) delete(watch.names, name)
} }
...@@ -578,23 +421,23 @@ func (w *Watcher) remWatch(pathname string) error { ...@@ -578,23 +421,23 @@ func (w *Watcher) remWatch(pathname string) error {
} }
// Must run within the I/O thread. // 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 { for name, mask := range watch.names {
if mask&provisional == 0 { 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) delete(watch.names, name)
} }
if watch.mask != 0 { if watch.mask != 0 {
if watch.mask&provisional == 0 { if watch.mask&provisional == 0 {
w.sendEvent(watch.path, watch.mask&sysFSIGNORED) w.sendEvent(watch.path, "", watch.mask&sysFSIGNORED)
} }
watch.mask = 0 watch.mask = 0
} }
} }
// Must run within the I/O thread. // 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) err := windows.CancelIo(watch.ino.handle)
if err != nil { if err != nil {
w.sendError(os.NewSyscallError("CancelIo", err)) w.sendError(os.NewSyscallError("CancelIo", err))
...@@ -624,7 +467,7 @@ func (w *Watcher) startRead(watch *watch) error { ...@@ -624,7 +467,7 @@ func (w *Watcher) startRead(watch *watch) error {
err := os.NewSyscallError("ReadDirectoryChanges", rdErr) err := os.NewSyscallError("ReadDirectoryChanges", rdErr)
if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 { if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 {
// Watched directory was probably removed // Watched directory was probably removed
w.sendEvent(watch.path, watch.mask&sysFSDELETESELF) w.sendEvent(watch.path, "", watch.mask&sysFSDELETESELF)
err = nil err = nil
} }
w.deleteWatch(watch) w.deleteWatch(watch)
...@@ -637,7 +480,7 @@ func (w *Watcher) startRead(watch *watch) error { ...@@ -637,7 +480,7 @@ func (w *Watcher) startRead(watch *watch) error {
// readEvents reads from the I/O completion port, converts the // readEvents reads from the I/O completion port, converts the
// received events into Event objects and sends them via the Events channel. // received events into Event objects and sends them via the Events channel.
// Entry point to the I/O thread. // Entry point to the I/O thread.
func (w *Watcher) readEvents() { func (w *readDirChangesW) readEvents() {
var ( var (
n uint32 n uint32
key uintptr key uintptr
...@@ -700,7 +543,7 @@ func (w *Watcher) readEvents() { ...@@ -700,7 +543,7 @@ func (w *Watcher) readEvents() {
} }
case windows.ERROR_ACCESS_DENIED: case windows.ERROR_ACCESS_DENIED:
// Watched directory was probably removed // Watched directory was probably removed
w.sendEvent(watch.path, watch.mask&sysFSDELETESELF) w.sendEvent(watch.path, "", watch.mask&sysFSDELETESELF)
w.deleteWatch(watch) w.deleteWatch(watch)
w.startRead(watch) w.startRead(watch)
continue continue
...@@ -733,6 +576,10 @@ func (w *Watcher) readEvents() { ...@@ -733,6 +576,10 @@ func (w *Watcher) readEvents() {
name := windows.UTF16ToString(buf) name := windows.UTF16ToString(buf)
fullname := filepath.Join(watch.path, name) fullname := filepath.Join(watch.path, name)
if debug {
internal.Debug(fullname, raw.Action)
}
var mask uint64 var mask uint64
switch raw.Action { switch raw.Action {
case windows.FILE_ACTION_REMOVED: case windows.FILE_ACTION_REMOVED:
...@@ -761,21 +608,22 @@ func (w *Watcher) readEvents() { ...@@ -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 { if raw.Action != windows.FILE_ACTION_RENAMED_NEW_NAME {
sendNameEvent() w.sendEvent(fullname, "", watch.names[name]&mask)
} }
if raw.Action == windows.FILE_ACTION_REMOVED { 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) 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 { if raw.Action == windows.FILE_ACTION_RENAMED_NEW_NAME {
fullname = filepath.Join(watch.path, watch.rename) w.sendEvent(filepath.Join(watch.path, watch.rename), "", watch.names[name]&mask)
sendNameEvent()
} }
// Move to the next event in the buffer // Move to the next event in the buffer
...@@ -787,8 +635,7 @@ func (w *Watcher) readEvents() { ...@@ -787,8 +635,7 @@ func (w *Watcher) readEvents() {
// Error! // Error!
if offset >= n { if offset >= n {
//lint:ignore ST1005 Windows should be capitalized //lint:ignore ST1005 Windows should be capitalized
w.sendError(errors.New( w.sendError(errors.New("Windows system assumed buffer larger than it is, events have likely been missed"))
"Windows system assumed buffer larger than it is, events have likely been missed"))
break break
} }
} }
...@@ -799,7 +646,7 @@ func (w *Watcher) readEvents() { ...@@ -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 var m uint32
if mask&sysFSMODIFY != 0 { if mask&sysFSMODIFY != 0 {
m |= windows.FILE_NOTIFY_CHANGE_LAST_WRITE m |= windows.FILE_NOTIFY_CHANGE_LAST_WRITE
...@@ -810,7 +657,7 @@ func (w *Watcher) toWindowsFlags(mask uint64) uint32 { ...@@ -810,7 +657,7 @@ func (w *Watcher) toWindowsFlags(mask uint64) uint32 {
return m return m
} }
func (w *Watcher) toFSnotifyFlags(action uint32) uint64 { func (w *readDirChangesW) toFSnotifyFlags(action uint32) uint64 {
switch action { switch action {
case windows.FILE_ACTION_ADDED: case windows.FILE_ACTION_ADDED:
return sysFSCREATE return sysFSCREATE
...@@ -825,3 +672,11 @@ func (w *Watcher) toFSnotifyFlags(action uint32) uint64 { ...@@ -825,3 +672,11 @@ func (w *Watcher) toFSnotifyFlags(action uint32) uint64 {
} }
return 0 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
}
...@@ -3,19 +3,146 @@ ...@@ -3,19 +3,146 @@
// //
// Currently supported systems: // Currently supported systems:
// //
// Linux 2.6.32+ via inotify // - Linux via inotify
// BSD, macOS via kqueue // - BSD, macOS via kqueue
// Windows via ReadDirectoryChangesW // - Windows via ReadDirectoryChangesW
// illumos via FEN // - 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 package fsnotify
import ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"strings" "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. // Event represents a file system notification.
type Event struct { type Event struct {
// Path to the file or directory. // Path to the file or directory.
...@@ -30,6 +157,16 @@ type Event struct { ...@@ -30,6 +157,16 @@ type Event struct {
// This is a bitmask and some systems may send multiple operations at once. // This is a bitmask and some systems may send multiple operations at once.
// Use the Event.Has() method instead of comparing with ==. // Use the Event.Has() method instead of comparing with ==.
Op Op 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. // Op describes a set of file operations.
...@@ -50,7 +187,7 @@ const ( ...@@ -50,7 +187,7 @@ const (
// example "remove to trash" is often a rename). // example "remove to trash" is often a rename).
Remove 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. // removed.
Rename Rename
...@@ -60,15 +197,155 @@ const ( ...@@ -60,15 +197,155 @@ const (
// get triggered very frequently by some software. For example, Spotlight // get triggered very frequently by some software. For example, Spotlight
// indexing on macOS, anti-virus software, backup software, etc. // indexing on macOS, anti-virus software, backup software, etc.
Chmod 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 ( 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") ErrNonExistentWatch = errors.New("fsnotify: can't remove non-existent watch")
ErrEventOverflow = errors.New("fsnotify: queue or buffer overflow")
ErrClosed = errors.New("fsnotify: watcher already closed") // 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 { func (o Op) String() string {
var b strings.Builder var b strings.Builder
if o.Has(Create) { if o.Has(Create) {
...@@ -80,6 +357,18 @@ func (o Op) String() string { ...@@ -80,6 +357,18 @@ func (o Op) String() string {
if o.Has(Write) { if o.Has(Write) {
b.WriteString("|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) { if o.Has(Rename) {
b.WriteString("|RENAME") b.WriteString("|RENAME")
} }
...@@ -100,24 +389,48 @@ func (e Event) Has(op Op) bool { return e.Op.Has(op) } ...@@ -100,24 +389,48 @@ func (e Event) Has(op Op) bool { return e.Op.Has(op) }
// String returns a string representation of the event with their path. // String returns a string representation of the event with their path.
func (e Event) String() string { 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) return fmt.Sprintf("%-13s %q", e.Op.String(), e.Name)
} }
type ( type (
backend interface {
Add(string) error
AddWith(string, ...addOpt) error
Remove(string) error
WatchList() []string
Close() error
xSupports(Op) bool
}
addOpt func(opt *withOpts) addOpt func(opt *withOpts)
withOpts struct { withOpts struct {
bufsize int 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{ var defaultOpts = withOpts{
bufsize: 65536, // 64K bufsize: 65536, // 64K
op: Create | Write | Remove | Rename | Chmod,
} }
func getOptions(opts ...addOpt) withOpts { func getOptions(opts ...addOpt) withOpts {
with := defaultOpts with := defaultOpts
for _, o := range opts { for _, o := range opts {
o(&with) if o != nil {
o(&with)
}
} }
return with return with
} }
...@@ -136,9 +449,44 @@ func WithBufferSize(bytes int) addOpt { ...@@ -136,9 +449,44 @@ func WithBufferSize(bytes int) addOpt {
return func(opt *withOpts) { opt.bufsize = bytes } 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 // Check if this path is recursive (ends with "/..." or "\..."), and return the
// path with the /... stripped. // path with the /... stripped.
func recursivePath(path string) (string, bool) { 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) == "..." { if filepath.Base(path) == "..." {
return filepath.Dir(path), true return filepath.Dir(path), true
} }
......
#!/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
//go:build freebsd || openbsd || netbsd || dragonfly //go:build freebsd || openbsd || netbsd || dragonfly
// +build freebsd openbsd netbsd dragonfly
package fsnotify package fsnotify
......
//go:build darwin //go:build darwin
// +build darwin
package fsnotify package fsnotify
......
...@@ -32,10 +32,10 @@ func (fi *fileInfo) Name() string { return fi.name } ...@@ -32,10 +32,10 @@ func (fi *fileInfo) Name() string { return fi.name }
func (fi *fileInfo) Size() int64 { return int64(fi.stat.Size) } func (fi *fileInfo) Size() int64 { return int64(fi.stat.Size) }
// Mode returns file mode bits. // 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. // 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. // IsDir returns true if the file is a directory.
func (fi *fileInfo) IsDir() bool { return fi.Mode().IsDir() } func (fi *fileInfo) IsDir() bool { return fi.Mode().IsDir() }
...@@ -56,6 +56,21 @@ type FileStat struct { ...@@ -56,6 +56,21 @@ type FileStat struct {
Extended []StatExtended 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. // StatExtended contains additional, extended information for a FileStat.
type StatExtended struct { type StatExtended struct {
ExtType string ExtType string
......