├── .circleci └── config.yml ├── .cirrus.yml ├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── other.md └── workflows │ ├── Vagrantfile.debian6 │ ├── build.yml │ ├── staticcheck.yml │ └── test.yml ├── .gitignore ├── .mailmap ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── backend_fen.go ├── backend_fen_test.go ├── backend_inotify.go ├── backend_inotify_test.go ├── backend_kqueue.go ├── backend_kqueue_test.go ├── backend_other.go ├── backend_windows.go ├── backend_windows_test.go ├── cmd └── fsnotify │ ├── dedup.go │ ├── file.go │ ├── main.go │ └── watch.go ├── fsnotify.go ├── fsnotify_test.go ├── go.mod ├── go.sum ├── helpers_test.go ├── internal ├── darwin.go ├── debug_darwin.go ├── debug_dragonfly.go ├── debug_freebsd.go ├── debug_kqueue.go ├── debug_linux.go ├── debug_netbsd.go ├── debug_openbsd.go ├── debug_solaris.go ├── debug_windows.go ├── freebsd.go ├── internal.go ├── unix.go ├── unix2.go └── windows.go ├── mkdoc.zsh ├── system_bsd.go ├── system_darwin.go └── test └── kqueue.c /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | workflows: 3 | main: 4 | jobs: ['linux-arm64', 'ios'] 5 | 6 | jobs: 7 | # linux/arm64 8 | linux-arm64: 9 | machine: 10 | image: ubuntu-2204:2022.07.1 11 | resource_class: arm.medium 12 | working_directory: ~/repo 13 | steps: 14 | - checkout 15 | 16 | - run: 17 | name: install-go 18 | command: | 19 | sudo apt -y install golang 20 | 21 | - run: 22 | name: test 23 | command: | 24 | uname -a 25 | go version 26 | FSNOTIFY_BUFFER=4096 go test -parallel 1 -race ./... 27 | go test -parallel 1 -race ./... 28 | 29 | # iOS 30 | ios: 31 | macos: 32 | xcode: 13.4.1 33 | working_directory: ~/repo 34 | steps: 35 | - checkout 36 | 37 | - run: 38 | name: install-go 39 | command: | 40 | export HOMEBREW_NO_AUTO_UPDATE=1 41 | brew install go 42 | 43 | - run: 44 | name: test 45 | environment: 46 | SCAN_DEVICE: iPhone 6 47 | SCAN_SCHEME: WebTests 48 | command: | 49 | export PATH=$PATH:/usr/local/Cellar/go/*/bin 50 | uname -a 51 | go version 52 | FSNOTIFY_BUFFER=4096 go test -parallel 1 -race ./... 53 | go test -parallel 1 -race ./... 54 | 55 | # This is just Linux x86_64; also need to get a Go with GOOS=android, but 56 | # there aren't any pre-built versions of that on the Go site. Idk, disable for 57 | # now; number of people using Go on Android is probably very tiny, and the 58 | # number of people using Go with this lib smaller still. 59 | # android: 60 | # machine: 61 | # image: android:2022.01.1 62 | # working_directory: ~/repo 63 | # steps: 64 | # - checkout 65 | 66 | # - run: 67 | # name: install-go 68 | # command: | 69 | # v=1.19.2 70 | # curl --silent --show-error --location --fail --retry 3 --output /tmp/go${v}.tgz \ 71 | # "https://go.dev/dl/go$v.linux-arm64.tar.gz" 72 | # sudo tar -C /usr/local -xzf /tmp/go${v}.tgz 73 | # rm /tmp/go${v}.tgz 74 | 75 | # - run: 76 | # name: test 77 | # command: | 78 | # uname -a 79 | # export PATH=/usr/local/go/bin:$PATH 80 | # go version 81 | # FSNOTIFY_BUFFER=4096 go test -parallel 1 -race ./... 82 | # go test -parallel 1 -race ./... 83 | # 84 | -------------------------------------------------------------------------------- /.cirrus.yml: -------------------------------------------------------------------------------- 1 | freebsd_task: 2 | name: 'FreeBSD' 3 | freebsd_instance: 4 | image_family: freebsd-13-2 5 | install_script: 6 | - pkg update -f 7 | - pkg install -y go 8 | test_script: 9 | # run tests as user "cirrus" instead of root 10 | - pw useradd cirrus -m 11 | - chown -R cirrus:cirrus . 12 | - FSNOTIFY_BUFFER=4096 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./... 13 | - sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./... 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.go] 4 | indent_style = tab 5 | indent_size = 4 6 | insert_final_newline = true 7 | 8 | [*.{yml,yaml}] 9 | indent_style = space 10 | indent_size = 2 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | go.sum linguist-generated 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: arp242 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Bug report' 3 | description: 'Create a bug report' 4 | labels: ['bug'] 5 | 6 | body: 7 | - type: 'textarea' 8 | validations: {"required": true} 9 | attributes: 10 | label: 'Describe the bug' 11 | placeholder: 'A clear and concise description of what the bug is: expected behaviour, observed behaviour, etc.' 12 | 13 | - type: 'textarea' 14 | validations: {"required": true} 15 | attributes: 16 | label: 'To Reproduce' 17 | placeholder: 'Please provide the FULL code to reproduce the problem (something that can be copy/pasted and run without having to write additional code) and any additional steps that are needed.' 18 | 19 | - type: 'textarea' 20 | validations: {"required": true} 21 | attributes: 22 | label: 'Which operating system and version are you using?' 23 | description: | 24 | ``` 25 | Linux: lsb_release -a 26 | macOS: sw_vers 27 | Windows: systeminfo | findstr /B /C:OS 28 | BSD: uname -a 29 | ``` 30 | 31 | - type: 'input' 32 | validations: {"required": true} 33 | attributes: 34 | label: 'Which fsnotify version are you using?' 35 | 36 | - type: 'dropdown' 37 | validations: {"required": true} 38 | attributes: 39 | label: 'Did you try the latest main branch?' 40 | description: 'Please try the latest main branch as well, with e.g.:
`go get github.com/fsnotify/fsnotify@main`' 41 | options: ['No', 'Yes'] 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Other' 3 | about: "Anything that's not a bug such as feature requests, questions, etc." 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | -------------------------------------------------------------------------------- /.github/workflows/Vagrantfile.debian6: -------------------------------------------------------------------------------- 1 | Vagrant.configure("2") do |config| 2 | config.vm.box = "threatstack/debian6" 3 | config.vm.box_version = "1.0.0" 4 | 5 | config.vm.define 'debian6' 6 | end 7 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 'build' 2 | on: 3 | pull_request: 4 | paths: ['**.go', 'go.mod', '.github/workflows/*'] 5 | push: 6 | branches: ['main', 'aix'] 7 | 8 | jobs: 9 | cross-compile: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | go: ['1.17', '1.21'] 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: checkout 17 | uses: actions/checkout@v3 18 | 19 | - name: setup Go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: ${{ matrix.go }} 23 | 24 | - name: build 25 | run: | 26 | for a in $(go tool dist list); do 27 | export GOOS=${a%%/*} 28 | export GOARCH=${a#*/} 29 | 30 | case "$GOOS" in 31 | (android|ios) exit 0 ;; # Requires cgo to link. 32 | (js) exit 0 ;; # No build tags in internal/ TODO: should maybe fix? 33 | (openbsd) 34 | case "$GOARCH" in 35 | # Fails with: 36 | # golang.org/x/sys/unix.syscall_syscall9: relocation target syscall.syscall10 not defined 37 | # golang.org/x/sys/unix.kevent: relocation target syscall.syscall6 not defined 38 | # golang.org/x/sys/unix.pipe2: relocation target syscall.rawSyscall not defined 39 | # ... 40 | # Works for me locally though, hmm... 41 | (386) exit 0 ;; 42 | esac 43 | esac 44 | 45 | go test -c 46 | go build ./cmd/fsnotify 47 | done 48 | -------------------------------------------------------------------------------- /.github/workflows/staticcheck.yml: -------------------------------------------------------------------------------- 1 | name: 'staticcheck' 2 | on: 3 | pull_request: 4 | paths: ['**.go', 'go.mod', '.github/workflows/*'] 5 | push: 6 | branches: ['main', 'aix'] 7 | 8 | jobs: 9 | staticcheck: 10 | name: 'staticcheck' 11 | runs-on: ubuntu-latest 12 | steps: 13 | - id: install_go 14 | uses: WillAbides/setup-go-faster@v1.7.0 15 | with: 16 | go-version: "1.19.x" 17 | 18 | - uses: actions/cache@v3 19 | with: 20 | key: ${{ runner.os }}-staticcheck 21 | path: | 22 | ${{ runner.temp }}/staticcheck 23 | ${{ steps.install_go.outputs.GOCACHE || '' }} 24 | 25 | - run: | 26 | export STATICCHECK_CACHE="${{ runner.temp }}/staticcheck" 27 | go install honnef.co/go/tools/cmd/staticcheck@latest 28 | 29 | $(go env GOPATH)/bin/staticcheck -matrix < 2 | Nathan Youngman <4566+nathany@users.noreply.github.com> 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Unreleased 4 | ---------- 5 | Nothing yet. 6 | 7 | 1.7.0 - 2023-10-22 8 | ------------------ 9 | This version of fsnotify needs Go 1.17. 10 | 11 | ### Additions 12 | 13 | - illumos: add FEN backend to support illumos and Solaris. ([#371]) 14 | 15 | - all: add `NewBufferedWatcher()` to use a buffered channel, which can be useful 16 | in cases where you can't control the kernel buffer and receive a large number 17 | of events in bursts. ([#550], [#572]) 18 | 19 | - all: add `AddWith()`, which is identical to `Add()` but allows passing 20 | options. ([#521]) 21 | 22 | - windows: allow setting the ReadDirectoryChangesW() buffer size with 23 | `fsnotify.WithBufferSize()`; the default of 64K is the highest value that 24 | works on all platforms and is enough for most purposes, but in some cases a 25 | highest buffer is needed. ([#521]) 26 | 27 | ### Changes and fixes 28 | 29 | - inotify: remove watcher if a watched path is renamed ([#518]) 30 | 31 | After a rename the reported name wasn't updated, or even an empty string. 32 | Inotify doesn't provide any good facilities to update it, so just remove the 33 | watcher. This is already how it worked on kqueue and FEN. 34 | 35 | On Windows this does work, and remains working. 36 | 37 | - windows: don't listen for file attribute changes ([#520]) 38 | 39 | File attribute changes are sent as `FILE_ACTION_MODIFIED` by the Windows API, 40 | with no way to see if they're a file write or attribute change, so would show 41 | up as a fsnotify.Write event. This is never useful, and could result in many 42 | spurious Write events. 43 | 44 | - windows: return `ErrEventOverflow` if the buffer is full ([#525]) 45 | 46 | Before it would merely return "short read", making it hard to detect this 47 | error. 48 | 49 | - kqueue: make sure events for all files are delivered properly when removing a 50 | watched directory ([#526]) 51 | 52 | Previously they would get sent with `""` (empty string) or `"."` as the path 53 | name. 54 | 55 | - kqueue: don't emit spurious Create events for symbolic links ([#524]) 56 | 57 | The link would get resolved but kqueue would "forget" it already saw the link 58 | itself, resulting on a Create for every Write event for the directory. 59 | 60 | - all: return `ErrClosed` on `Add()` when the watcher is closed ([#516]) 61 | 62 | - other: add `Watcher.Errors` and `Watcher.Events` to the no-op `Watcher` in 63 | `backend_other.go`, making it easier to use on unsupported platforms such as 64 | WASM, AIX, etc. ([#528]) 65 | 66 | - other: use the `backend_other.go` no-op if the `appengine` build tag is set; 67 | Google AppEngine forbids usage of the unsafe package so the inotify backend 68 | won't compile there. 69 | 70 | [#371]: https://github.com/fsnotify/fsnotify/pull/371 71 | [#516]: https://github.com/fsnotify/fsnotify/pull/516 72 | [#518]: https://github.com/fsnotify/fsnotify/pull/518 73 | [#520]: https://github.com/fsnotify/fsnotify/pull/520 74 | [#521]: https://github.com/fsnotify/fsnotify/pull/521 75 | [#524]: https://github.com/fsnotify/fsnotify/pull/524 76 | [#525]: https://github.com/fsnotify/fsnotify/pull/525 77 | [#526]: https://github.com/fsnotify/fsnotify/pull/526 78 | [#528]: https://github.com/fsnotify/fsnotify/pull/528 79 | [#537]: https://github.com/fsnotify/fsnotify/pull/537 80 | [#550]: https://github.com/fsnotify/fsnotify/pull/550 81 | [#572]: https://github.com/fsnotify/fsnotify/pull/572 82 | 83 | 1.6.0 - 2022-10-13 84 | ------------------ 85 | This version of fsnotify needs Go 1.16 (this was already the case since 1.5.1, 86 | but not documented). It also increases the minimum Linux version to 2.6.32. 87 | 88 | ### Additions 89 | 90 | - all: add `Event.Has()` and `Op.Has()` ([#477]) 91 | 92 | This makes checking events a lot easier; for example: 93 | 94 | if event.Op&Write == Write && !(event.Op&Remove == Remove) { 95 | } 96 | 97 | Becomes: 98 | 99 | if event.Has(Write) && !event.Has(Remove) { 100 | } 101 | 102 | - all: add cmd/fsnotify ([#463]) 103 | 104 | A command-line utility for testing and some examples. 105 | 106 | ### Changes and fixes 107 | 108 | - inotify: don't ignore events for files that don't exist ([#260], [#470]) 109 | 110 | Previously the inotify watcher would call `os.Lstat()` to check if a file 111 | still exists before emitting events. 112 | 113 | This was inconsistent with other platforms and resulted in inconsistent event 114 | reporting (e.g. when a file is quickly removed and re-created), and generally 115 | a source of confusion. It was added in 2013 to fix a memory leak that no 116 | longer exists. 117 | 118 | - all: return `ErrNonExistentWatch` when `Remove()` is called on a path that's 119 | not watched ([#460]) 120 | 121 | - inotify: replace epoll() with non-blocking inotify ([#434]) 122 | 123 | Non-blocking inotify was not generally available at the time this library was 124 | written in 2014, but now it is. As a result, the minimum Linux version is 125 | bumped from 2.6.27 to 2.6.32. This hugely simplifies the code and is faster. 126 | 127 | - kqueue: don't check for events every 100ms ([#480]) 128 | 129 | The watcher would wake up every 100ms, even when there was nothing to do. Now 130 | it waits until there is something to do. 131 | 132 | - macos: retry opening files on EINTR ([#475]) 133 | 134 | - kqueue: skip unreadable files ([#479]) 135 | 136 | kqueue requires a file descriptor for every file in a directory; this would 137 | fail if a file was unreadable by the current user. Now these files are simply 138 | skipped. 139 | 140 | - windows: fix renaming a watched directory if the parent is also watched ([#370]) 141 | 142 | - windows: increase buffer size from 4K to 64K ([#485]) 143 | 144 | - windows: close file handle on Remove() ([#288]) 145 | 146 | - kqueue: put pathname in the error if watching a file fails ([#471]) 147 | 148 | - inotify, windows: calling Close() more than once could race ([#465]) 149 | 150 | - kqueue: improve Close() performance ([#233]) 151 | 152 | - all: various documentation additions and clarifications. 153 | 154 | [#233]: https://github.com/fsnotify/fsnotify/pull/233 155 | [#260]: https://github.com/fsnotify/fsnotify/pull/260 156 | [#288]: https://github.com/fsnotify/fsnotify/pull/288 157 | [#370]: https://github.com/fsnotify/fsnotify/pull/370 158 | [#434]: https://github.com/fsnotify/fsnotify/pull/434 159 | [#460]: https://github.com/fsnotify/fsnotify/pull/460 160 | [#463]: https://github.com/fsnotify/fsnotify/pull/463 161 | [#465]: https://github.com/fsnotify/fsnotify/pull/465 162 | [#470]: https://github.com/fsnotify/fsnotify/pull/470 163 | [#471]: https://github.com/fsnotify/fsnotify/pull/471 164 | [#475]: https://github.com/fsnotify/fsnotify/pull/475 165 | [#477]: https://github.com/fsnotify/fsnotify/pull/477 166 | [#479]: https://github.com/fsnotify/fsnotify/pull/479 167 | [#480]: https://github.com/fsnotify/fsnotify/pull/480 168 | [#485]: https://github.com/fsnotify/fsnotify/pull/485 169 | 170 | ## [1.5.4] - 2022-04-25 171 | 172 | * Windows: add missing defer to `Watcher.WatchList` [#447](https://github.com/fsnotify/fsnotify/pull/447) 173 | * go.mod: use latest x/sys [#444](https://github.com/fsnotify/fsnotify/pull/444) 174 | * Fix compilation for OpenBSD [#443](https://github.com/fsnotify/fsnotify/pull/443) 175 | 176 | ## [1.5.3] - 2022-04-22 177 | 178 | * This version is retracted. An incorrect branch is published accidentally [#445](https://github.com/fsnotify/fsnotify/issues/445) 179 | 180 | ## [1.5.2] - 2022-04-21 181 | 182 | * Add a feature to return the directories and files that are being monitored [#374](https://github.com/fsnotify/fsnotify/pull/374) 183 | * Fix potential crash on windows if `raw.FileNameLength` exceeds `syscall.MAX_PATH` [#361](https://github.com/fsnotify/fsnotify/pull/361) 184 | * Allow build on unsupported GOOS [#424](https://github.com/fsnotify/fsnotify/pull/424) 185 | * Don't set `poller.fd` twice in `newFdPoller` [#406](https://github.com/fsnotify/fsnotify/pull/406) 186 | * fix go vet warnings: call to `(*T).Fatalf` from a non-test goroutine [#416](https://github.com/fsnotify/fsnotify/pull/416) 187 | 188 | ## [1.5.1] - 2021-08-24 189 | 190 | * Revert Add AddRaw to not follow symlinks [#394](https://github.com/fsnotify/fsnotify/pull/394) 191 | 192 | ## [1.5.0] - 2021-08-20 193 | 194 | * Go: Increase minimum required version to Go 1.12 [#381](https://github.com/fsnotify/fsnotify/pull/381) 195 | * Feature: Add AddRaw method which does not follow symlinks when adding a watch [#289](https://github.com/fsnotify/fsnotify/pull/298) 196 | * Windows: Follow symlinks by default like on all other systems [#289](https://github.com/fsnotify/fsnotify/pull/289) 197 | * CI: Use GitHub Actions for CI and cover go 1.12-1.17 198 | [#378](https://github.com/fsnotify/fsnotify/pull/378) 199 | [#381](https://github.com/fsnotify/fsnotify/pull/381) 200 | [#385](https://github.com/fsnotify/fsnotify/pull/385) 201 | * Go 1.14+: Fix unsafe pointer conversion [#325](https://github.com/fsnotify/fsnotify/pull/325) 202 | 203 | ## [1.4.9] - 2020-03-11 204 | 205 | * Move example usage to the readme #329. This may resolve #328. 206 | 207 | ## [1.4.8] - 2020-03-10 208 | 209 | * CI: test more go versions (@nathany 1d13583d846ea9d66dcabbfefbfb9d8e6fb05216) 210 | * Tests: Queued inotify events could have been read by the test before max_queued_events was hit (@matthias-stone #265) 211 | * Tests: t.Fatalf -> t.Errorf in go routines (@gdey #266) 212 | * CI: Less verbosity (@nathany #267) 213 | * Tests: Darwin: Exchangedata is deprecated on 10.13 (@nathany #267) 214 | * Tests: Check if channels are closed in the example (@alexeykazakov #244) 215 | * CI: Only run golint on latest version of go and fix issues (@cpuguy83 #284) 216 | * CI: Add windows to travis matrix (@cpuguy83 #284) 217 | * Docs: Remover appveyor badge (@nathany 11844c0959f6fff69ba325d097fce35bd85a8e93) 218 | * Linux: create epoll and pipe fds with close-on-exec (@JohannesEbke #219) 219 | * Linux: open files with close-on-exec (@linxiulei #273) 220 | * Docs: Plan to support fanotify (@nathany ab058b44498e8b7566a799372a39d150d9ea0119 ) 221 | * Project: Add go.mod (@nathany #309) 222 | * Project: Revise editor config (@nathany #309) 223 | * Project: Update copyright for 2019 (@nathany #309) 224 | * CI: Drop go1.8 from CI matrix (@nathany #309) 225 | * Docs: Updating the FAQ section for supportability with NFS & FUSE filesystems (@Pratik32 4bf2d1fec78374803a39307bfb8d340688f4f28e ) 226 | 227 | ## [1.4.7] - 2018-01-09 228 | 229 | * BSD/macOS: Fix possible deadlock on closing the watcher on kqueue (thanks @nhooyr and @glycerine) 230 | * Tests: Fix missing verb on format string (thanks @rchiossi) 231 | * Linux: Fix deadlock in Remove (thanks @aarondl) 232 | * Linux: Watch.Add improvements (avoid race, fix consistency, reduce garbage) (thanks @twpayne) 233 | * Docs: Moved FAQ into the README (thanks @vahe) 234 | * Linux: Properly handle inotify's IN_Q_OVERFLOW event (thanks @zeldovich) 235 | * Docs: replace references to OS X with macOS 236 | 237 | ## [1.4.2] - 2016-10-10 238 | 239 | * Linux: use InotifyInit1 with IN_CLOEXEC to stop leaking a file descriptor to a child process when using fork/exec [#178](https://github.com/fsnotify/fsnotify/pull/178) (thanks @pattyshack) 240 | 241 | ## [1.4.1] - 2016-10-04 242 | 243 | * Fix flaky inotify stress test on Linux [#177](https://github.com/fsnotify/fsnotify/pull/177) (thanks @pattyshack) 244 | 245 | ## [1.4.0] - 2016-10-01 246 | 247 | * add a String() method to Event.Op [#165](https://github.com/fsnotify/fsnotify/pull/165) (thanks @oozie) 248 | 249 | ## [1.3.1] - 2016-06-28 250 | 251 | * Windows: fix for double backslash when watching the root of a drive [#151](https://github.com/fsnotify/fsnotify/issues/151) (thanks @brunoqc) 252 | 253 | ## [1.3.0] - 2016-04-19 254 | 255 | * Support linux/arm64 by [patching](https://go-review.googlesource.com/#/c/21971/) x/sys/unix and switching to to it from syscall (thanks @suihkulokki) [#135](https://github.com/fsnotify/fsnotify/pull/135) 256 | 257 | ## [1.2.10] - 2016-03-02 258 | 259 | * Fix golint errors in windows.go [#121](https://github.com/fsnotify/fsnotify/pull/121) (thanks @tiffanyfj) 260 | 261 | ## [1.2.9] - 2016-01-13 262 | 263 | kqueue: Fix logic for CREATE after REMOVE [#111](https://github.com/fsnotify/fsnotify/pull/111) (thanks @bep) 264 | 265 | ## [1.2.8] - 2015-12-17 266 | 267 | * kqueue: fix race condition in Close [#105](https://github.com/fsnotify/fsnotify/pull/105) (thanks @djui for reporting the issue and @ppknap for writing a failing test) 268 | * inotify: fix race in test 269 | * enable race detection for continuous integration (Linux, Mac, Windows) 270 | 271 | ## [1.2.5] - 2015-10-17 272 | 273 | * inotify: use epoll_create1 for arm64 support (requires Linux 2.6.27 or later) [#100](https://github.com/fsnotify/fsnotify/pull/100) (thanks @suihkulokki) 274 | * inotify: fix path leaks [#73](https://github.com/fsnotify/fsnotify/pull/73) (thanks @chamaken) 275 | * kqueue: watch for rename events on subdirectories [#83](https://github.com/fsnotify/fsnotify/pull/83) (thanks @guotie) 276 | * kqueue: avoid infinite loops from symlinks cycles [#101](https://github.com/fsnotify/fsnotify/pull/101) (thanks @illicitonion) 277 | 278 | ## [1.2.1] - 2015-10-14 279 | 280 | * kqueue: don't watch named pipes [#98](https://github.com/fsnotify/fsnotify/pull/98) (thanks @evanphx) 281 | 282 | ## [1.2.0] - 2015-02-08 283 | 284 | * inotify: use epoll to wake up readEvents [#66](https://github.com/fsnotify/fsnotify/pull/66) (thanks @PieterD) 285 | * inotify: closing watcher should now always shut down goroutine [#63](https://github.com/fsnotify/fsnotify/pull/63) (thanks @PieterD) 286 | * kqueue: close kqueue after removing watches, fixes [#59](https://github.com/fsnotify/fsnotify/issues/59) 287 | 288 | ## [1.1.1] - 2015-02-05 289 | 290 | * inotify: Retry read on EINTR [#61](https://github.com/fsnotify/fsnotify/issues/61) (thanks @PieterD) 291 | 292 | ## [1.1.0] - 2014-12-12 293 | 294 | * kqueue: rework internals [#43](https://github.com/fsnotify/fsnotify/pull/43) 295 | * add low-level functions 296 | * only need to store flags on directories 297 | * less mutexes [#13](https://github.com/fsnotify/fsnotify/issues/13) 298 | * done can be an unbuffered channel 299 | * remove calls to os.NewSyscallError 300 | * More efficient string concatenation for Event.String() [#52](https://github.com/fsnotify/fsnotify/pull/52) (thanks @mdlayher) 301 | * kqueue: fix regression in rework causing subdirectories to be watched [#48](https://github.com/fsnotify/fsnotify/issues/48) 302 | * kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51) 303 | 304 | ## [1.0.4] - 2014-09-07 305 | 306 | * kqueue: add dragonfly to the build tags. 307 | * Rename source code files, rearrange code so exported APIs are at the top. 308 | * Add done channel to example code. [#37](https://github.com/fsnotify/fsnotify/pull/37) (thanks @chenyukang) 309 | 310 | ## [1.0.3] - 2014-08-19 311 | 312 | * [Fix] Windows MOVED_TO now translates to Create like on BSD and Linux. [#36](https://github.com/fsnotify/fsnotify/issues/36) 313 | 314 | ## [1.0.2] - 2014-08-17 315 | 316 | * [Fix] Missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso) 317 | * [Fix] Make ./path and path equivalent. (thanks @zhsso) 318 | 319 | ## [1.0.0] - 2014-08-15 320 | 321 | * [API] Remove AddWatch on Windows, use Add. 322 | * Improve documentation for exported identifiers. [#30](https://github.com/fsnotify/fsnotify/issues/30) 323 | * Minor updates based on feedback from golint. 324 | 325 | ## dev / 2014-07-09 326 | 327 | * Moved to [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify). 328 | * Use os.NewSyscallError instead of returning errno (thanks @hariharan-uno) 329 | 330 | ## dev / 2014-07-04 331 | 332 | * kqueue: fix incorrect mutex used in Close() 333 | * Update example to demonstrate usage of Op. 334 | 335 | ## dev / 2014-06-28 336 | 337 | * [API] Don't set the Write Op for attribute notifications [#4](https://github.com/fsnotify/fsnotify/issues/4) 338 | * Fix for String() method on Event (thanks Alex Brainman) 339 | * Don't build on Plan 9 or Solaris (thanks @4ad) 340 | 341 | ## dev / 2014-06-21 342 | 343 | * Events channel of type Event rather than *Event. 344 | * [internal] use syscall constants directly for inotify and kqueue. 345 | * [internal] kqueue: rename events to kevents and fileEvent to event. 346 | 347 | ## dev / 2014-06-19 348 | 349 | * Go 1.3+ required on Windows (uses syscall.ERROR_MORE_DATA internally). 350 | * [internal] remove cookie from Event struct (unused). 351 | * [internal] Event struct has the same definition across every OS. 352 | * [internal] remove internal watch and removeWatch methods. 353 | 354 | ## dev / 2014-06-12 355 | 356 | * [API] Renamed Watch() to Add() and RemoveWatch() to Remove(). 357 | * [API] Pluralized channel names: Events and Errors. 358 | * [API] Renamed FileEvent struct to Event. 359 | * [API] Op constants replace methods like IsCreate(). 360 | 361 | ## dev / 2014-06-12 362 | 363 | * Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98) 364 | 365 | ## dev / 2014-05-23 366 | 367 | * [API] Remove current implementation of WatchFlags. 368 | * current implementation doesn't take advantage of OS for efficiency 369 | * provides little benefit over filtering events as they are received, but has extra bookkeeping and mutexes 370 | * no tests for the current implementation 371 | * not fully implemented on Windows [#93](https://github.com/howeyc/fsnotify/issues/93#issuecomment-39285195) 372 | 373 | ## [0.9.3] - 2014-12-31 374 | 375 | * kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51) 376 | 377 | ## [0.9.2] - 2014-08-17 378 | 379 | * [Backport] Fix missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso) 380 | 381 | ## [0.9.1] - 2014-06-12 382 | 383 | * Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98) 384 | 385 | ## [0.9.0] - 2014-01-17 386 | 387 | * IsAttrib() for events that only concern a file's metadata [#79][] (thanks @abustany) 388 | * [Fix] kqueue: fix deadlock [#77][] (thanks @cespare) 389 | * [NOTICE] Development has moved to `code.google.com/p/go.exp/fsnotify` in preparation for inclusion in the Go standard library. 390 | 391 | ## [0.8.12] - 2013-11-13 392 | 393 | * [API] Remove FD_SET and friends from Linux adapter 394 | 395 | ## [0.8.11] - 2013-11-02 396 | 397 | * [Doc] Add Changelog [#72][] (thanks @nathany) 398 | * [Doc] Spotlight and double modify events on macOS [#62][] (reported by @paulhammond) 399 | 400 | ## [0.8.10] - 2013-10-19 401 | 402 | * [Fix] kqueue: remove file watches when parent directory is removed [#71][] (reported by @mdwhatcott) 403 | * [Fix] kqueue: race between Close and readEvents [#70][] (reported by @bernerdschaefer) 404 | * [Doc] specify OS-specific limits in README (thanks @debrando) 405 | 406 | ## [0.8.9] - 2013-09-08 407 | 408 | * [Doc] Contributing (thanks @nathany) 409 | * [Doc] update package path in example code [#63][] (thanks @paulhammond) 410 | * [Doc] GoCI badge in README (Linux only) [#60][] 411 | * [Doc] Cross-platform testing with Vagrant [#59][] (thanks @nathany) 412 | 413 | ## [0.8.8] - 2013-06-17 414 | 415 | * [Fix] Windows: handle `ERROR_MORE_DATA` on Windows [#49][] (thanks @jbowtie) 416 | 417 | ## [0.8.7] - 2013-06-03 418 | 419 | * [API] Make syscall flags internal 420 | * [Fix] inotify: ignore event changes 421 | * [Fix] race in symlink test [#45][] (reported by @srid) 422 | * [Fix] tests on Windows 423 | * lower case error messages 424 | 425 | ## [0.8.6] - 2013-05-23 426 | 427 | * kqueue: Use EVT_ONLY flag on Darwin 428 | * [Doc] Update README with full example 429 | 430 | ## [0.8.5] - 2013-05-09 431 | 432 | * [Fix] inotify: allow monitoring of "broken" symlinks (thanks @tsg) 433 | 434 | ## [0.8.4] - 2013-04-07 435 | 436 | * [Fix] kqueue: watch all file events [#40][] (thanks @ChrisBuchholz) 437 | 438 | ## [0.8.3] - 2013-03-13 439 | 440 | * [Fix] inoitfy/kqueue memory leak [#36][] (reported by @nbkolchin) 441 | * [Fix] kqueue: use fsnFlags for watching a directory [#33][] (reported by @nbkolchin) 442 | 443 | ## [0.8.2] - 2013-02-07 444 | 445 | * [Doc] add Authors 446 | * [Fix] fix data races for map access [#29][] (thanks @fsouza) 447 | 448 | ## [0.8.1] - 2013-01-09 449 | 450 | * [Fix] Windows path separators 451 | * [Doc] BSD License 452 | 453 | ## [0.8.0] - 2012-11-09 454 | 455 | * kqueue: directory watching improvements (thanks @vmirage) 456 | * inotify: add `IN_MOVED_TO` [#25][] (requested by @cpisto) 457 | * [Fix] kqueue: deleting watched directory [#24][] (reported by @jakerr) 458 | 459 | ## [0.7.4] - 2012-10-09 460 | 461 | * [Fix] inotify: fixes from https://codereview.appspot.com/5418045/ (ugorji) 462 | * [Fix] kqueue: preserve watch flags when watching for delete [#21][] (reported by @robfig) 463 | * [Fix] kqueue: watch the directory even if it isn't a new watch (thanks @robfig) 464 | * [Fix] kqueue: modify after recreation of file 465 | 466 | ## [0.7.3] - 2012-09-27 467 | 468 | * [Fix] kqueue: watch with an existing folder inside the watched folder (thanks @vmirage) 469 | * [Fix] kqueue: no longer get duplicate CREATE events 470 | 471 | ## [0.7.2] - 2012-09-01 472 | 473 | * kqueue: events for created directories 474 | 475 | ## [0.7.1] - 2012-07-14 476 | 477 | * [Fix] for renaming files 478 | 479 | ## [0.7.0] - 2012-07-02 480 | 481 | * [Feature] FSNotify flags 482 | * [Fix] inotify: Added file name back to event path 483 | 484 | ## [0.6.0] - 2012-06-06 485 | 486 | * kqueue: watch files after directory created (thanks @tmc) 487 | 488 | ## [0.5.1] - 2012-05-22 489 | 490 | * [Fix] inotify: remove all watches before Close() 491 | 492 | ## [0.5.0] - 2012-05-03 493 | 494 | * [API] kqueue: return errors during watch instead of sending over channel 495 | * kqueue: match symlink behavior on Linux 496 | * inotify: add `DELETE_SELF` (requested by @taralx) 497 | * [Fix] kqueue: handle EINTR (reported by @robfig) 498 | * [Doc] Godoc example [#1][] (thanks @davecheney) 499 | 500 | ## [0.4.0] - 2012-03-30 501 | 502 | * Go 1 released: build with go tool 503 | * [Feature] Windows support using winfsnotify 504 | * Windows does not have attribute change notifications 505 | * Roll attribute notifications into IsModify 506 | 507 | ## [0.3.0] - 2012-02-19 508 | 509 | * kqueue: add files when watch directory 510 | 511 | ## [0.2.0] - 2011-12-30 512 | 513 | * update to latest Go weekly code 514 | 515 | ## [0.1.0] - 2011-10-19 516 | 517 | * kqueue: add watch on file creation to match inotify 518 | * kqueue: create file event 519 | * inotify: ignore `IN_IGNORED` events 520 | * event String() 521 | * linux: common FileEvent functions 522 | * initial commit 523 | 524 | [#79]: https://github.com/howeyc/fsnotify/pull/79 525 | [#77]: https://github.com/howeyc/fsnotify/pull/77 526 | [#72]: https://github.com/howeyc/fsnotify/issues/72 527 | [#71]: https://github.com/howeyc/fsnotify/issues/71 528 | [#70]: https://github.com/howeyc/fsnotify/issues/70 529 | [#63]: https://github.com/howeyc/fsnotify/issues/63 530 | [#62]: https://github.com/howeyc/fsnotify/issues/62 531 | [#60]: https://github.com/howeyc/fsnotify/issues/60 532 | [#59]: https://github.com/howeyc/fsnotify/issues/59 533 | [#49]: https://github.com/howeyc/fsnotify/issues/49 534 | [#45]: https://github.com/howeyc/fsnotify/issues/45 535 | [#40]: https://github.com/howeyc/fsnotify/issues/40 536 | [#36]: https://github.com/howeyc/fsnotify/issues/36 537 | [#33]: https://github.com/howeyc/fsnotify/issues/33 538 | [#29]: https://github.com/howeyc/fsnotify/issues/29 539 | [#25]: https://github.com/howeyc/fsnotify/issues/25 540 | [#24]: https://github.com/howeyc/fsnotify/issues/24 541 | [#21]: https://github.com/howeyc/fsnotify/issues/21 542 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thank you for your interest in contributing to fsnotify! We try to review and 2 | merge PRs in a reasonable timeframe, but please be aware that: 3 | 4 | - To avoid "wasted" work, please discus changes on the issue tracker first. You 5 | can just send PRs, but they may end up being rejected for one reason or the 6 | other. 7 | 8 | - fsnotify is a cross-platform library, and changes must work reasonably well on 9 | all supported platforms. 10 | 11 | - Changes will need to be compatible; old code should still compile, and the 12 | runtime behaviour can't change in ways that are likely to lead to problems for 13 | users. 14 | 15 | Testing 16 | ------- 17 | Just `go test ./...` runs all the tests; the CI runs this on all supported 18 | platforms. Testing different platforms locally can be done with something like 19 | [goon] or [Vagrant], but this isn't super-easy to set up at the moment. 20 | 21 | Use the `-short` flag to make the "stress test" run faster. 22 | 23 | 24 | [goon]: https://github.com/arp242/goon 25 | [Vagrant]: https://www.vagrantup.com/ 26 | [integration_test.go]: /integration_test.go 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2012 The Go Authors. All rights reserved. 2 | Copyright © fsnotify Authors. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | * Neither the name of Google Inc. nor the names of its contributors may be used 13 | to endorse or promote products derived from this software without specific 14 | prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fsnotify is a Go library to provide cross-platform filesystem notifications on 2 | Windows, Linux, macOS, BSD, and illumos. 3 | 4 | Go 1.17 or newer is required; the full documentation is at 5 | https://pkg.go.dev/github.com/fsnotify/fsnotify 6 | 7 | --- 8 | 9 | Platform support: 10 | 11 | | Backend | OS | Status | 12 | | :-------------------- | :--------- | :------------------------------------------------------------------------ | 13 | | inotify | Linux | Supported | 14 | | kqueue | BSD, macOS | Supported | 15 | | ReadDirectoryChangesW | Windows | Supported | 16 | | FEN | illumos | Supported | 17 | | fanotify | Linux 5.9+ | [Not yet](https://github.com/fsnotify/fsnotify/issues/114) | 18 | | AHAFS | AIX | [aix branch]; experimental due to lack of maintainer and test environment | 19 | | FSEvents | macOS | [Needs support in x/sys/unix][fsevents] | 20 | | USN Journals | Windows | [Needs support in x/sys/windows][usn] | 21 | | Polling | *All* | [Not yet](https://github.com/fsnotify/fsnotify/issues/9) | 22 | 23 | Linux and illumos should include Android and Solaris, but these are currently 24 | untested. 25 | 26 | [fsevents]: https://github.com/fsnotify/fsnotify/issues/11#issuecomment-1279133120 27 | [usn]: https://github.com/fsnotify/fsnotify/issues/53#issuecomment-1279829847 28 | [aix branch]: https://github.com/fsnotify/fsnotify/issues/353#issuecomment-1284590129 29 | 30 | Usage 31 | ----- 32 | A basic example: 33 | 34 | ```go 35 | package main 36 | 37 | import ( 38 | "log" 39 | 40 | "github.com/fsnotify/fsnotify" 41 | ) 42 | 43 | func main() { 44 | // Create new watcher. 45 | watcher, err := fsnotify.NewWatcher() 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | defer watcher.Close() 50 | 51 | // Start listening for events. 52 | go func() { 53 | for { 54 | select { 55 | case event, ok := <-watcher.Events: 56 | if !ok { 57 | return 58 | } 59 | log.Println("event:", event) 60 | if event.Has(fsnotify.Write) { 61 | log.Println("modified file:", event.Name) 62 | } 63 | case err, ok := <-watcher.Errors: 64 | if !ok { 65 | return 66 | } 67 | log.Println("error:", err) 68 | } 69 | } 70 | }() 71 | 72 | // Add a path. 73 | err = watcher.Add("/tmp") 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | 78 | // Block main goroutine forever. 79 | <-make(chan struct{}) 80 | } 81 | ``` 82 | 83 | Some more examples can be found in [cmd/fsnotify](cmd/fsnotify), which can be 84 | run with: 85 | 86 | % go run ./cmd/fsnotify 87 | 88 | Further detailed documentation can be found in godoc: 89 | https://pkg.go.dev/github.com/fsnotify/fsnotify 90 | 91 | FAQ 92 | --- 93 | ### Will a file still be watched when it's moved to another directory? 94 | No, not unless you are watching the location it was moved to. 95 | 96 | ### Are subdirectories watched? 97 | No, you must add watches for any directory you want to watch (a recursive 98 | watcher is on the roadmap: [#18]). 99 | 100 | [#18]: https://github.com/fsnotify/fsnotify/issues/18 101 | 102 | ### Do I have to watch the Error and Event channels in a goroutine? 103 | Yes. You can read both channels in the same goroutine using `select` (you don't 104 | need a separate goroutine for both channels; see the example). 105 | 106 | ### Why don't notifications work with NFS, SMB, FUSE, /proc, or /sys? 107 | fsnotify requires support from underlying OS to work. The current NFS and SMB 108 | protocols does not provide network level support for file notifications, and 109 | neither do the /proc and /sys virtual filesystems. 110 | 111 | This could be fixed with a polling watcher ([#9]), but it's not yet implemented. 112 | 113 | [#9]: https://github.com/fsnotify/fsnotify/issues/9 114 | 115 | ### Why do I get many Chmod events? 116 | Some programs may generate a lot of attribute changes; for example Spotlight on 117 | macOS, anti-virus programs, backup applications, and some others are known to do 118 | this. As a rule, it's typically best to ignore Chmod events. They're often not 119 | useful, and tend to cause problems. 120 | 121 | Spotlight indexing on macOS can result in multiple events (see [#15]). A 122 | temporary workaround is to add your folder(s) to the *Spotlight Privacy 123 | settings* until we have a native FSEvents implementation (see [#11]). 124 | 125 | [#11]: https://github.com/fsnotify/fsnotify/issues/11 126 | [#15]: https://github.com/fsnotify/fsnotify/issues/15 127 | 128 | ### Watching a file doesn't work well 129 | Watching individual files (rather than directories) is generally not recommended 130 | as many programs (especially editors) update files atomically: it will write to 131 | a temporary file which is then moved to to destination, overwriting the original 132 | (or some variant thereof). The watcher on the original file is now lost, as that 133 | no longer exists. 134 | 135 | The upshot of this is that a power failure or crash won't leave a half-written 136 | file. 137 | 138 | Watch the parent directory and use `Event.Name` to filter out files you're not 139 | interested in. There is an example of this in `cmd/fsnotify/file.go`. 140 | 141 | Platform-specific notes 142 | ----------------------- 143 | ### Linux 144 | When a file is removed a REMOVE event won't be emitted until all file 145 | descriptors are closed; it will emit a CHMOD instead: 146 | 147 | fp := os.Open("file") 148 | os.Remove("file") // CHMOD 149 | fp.Close() // REMOVE 150 | 151 | This is the event that inotify sends, so not much can be changed about this. 152 | 153 | The `fs.inotify.max_user_watches` sysctl variable specifies the upper limit for 154 | the number of watches per user, and `fs.inotify.max_user_instances` specifies 155 | the maximum number of inotify instances per user. Every Watcher you create is an 156 | "instance", and every path you add is a "watch". 157 | 158 | These are also exposed in `/proc` as `/proc/sys/fs/inotify/max_user_watches` and 159 | `/proc/sys/fs/inotify/max_user_instances` 160 | 161 | To increase them you can use `sysctl` or write the value to proc file: 162 | 163 | # The default values on Linux 5.18 164 | sysctl fs.inotify.max_user_watches=124983 165 | sysctl fs.inotify.max_user_instances=128 166 | 167 | To make the changes persist on reboot edit `/etc/sysctl.conf` or 168 | `/usr/lib/sysctl.d/50-default.conf` (details differ per Linux distro; check your 169 | distro's documentation): 170 | 171 | fs.inotify.max_user_watches=124983 172 | fs.inotify.max_user_instances=128 173 | 174 | Reaching the limit will result in a "no space left on device" or "too many open 175 | files" error. 176 | 177 | ### kqueue (macOS, all BSD systems) 178 | kqueue requires opening a file descriptor for every file that's being watched; 179 | so if you're watching a directory with five files then that's six file 180 | descriptors. You will run in to your system's "max open files" limit faster on 181 | these platforms. 182 | 183 | The sysctl variables `kern.maxfiles` and `kern.maxfilesperproc` can be used to 184 | control the maximum number of open files. 185 | -------------------------------------------------------------------------------- /backend_fen.go: -------------------------------------------------------------------------------- 1 | //go:build solaris 2 | // +build solaris 3 | 4 | // Note: the documentation on the Watcher type and methods is generated from 5 | // mkdoc.zsh 6 | 7 | package fsnotify 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | "os" 13 | "path/filepath" 14 | "sync" 15 | 16 | "golang.org/x/sys/unix" 17 | ) 18 | 19 | // Watcher watches a set of paths, delivering events on a channel. 20 | // 21 | // A watcher should not be copied (e.g. pass it by pointer, rather than by 22 | // value). 23 | // 24 | // # Linux notes 25 | // 26 | // When a file is removed a Remove event won't be emitted until all file 27 | // descriptors are closed, and deletes will always emit a Chmod. For example: 28 | // 29 | // fp := os.Open("file") 30 | // os.Remove("file") // Triggers Chmod 31 | // fp.Close() // Triggers Remove 32 | // 33 | // This is the event that inotify sends, so not much can be changed about this. 34 | // 35 | // The fs.inotify.max_user_watches sysctl variable specifies the upper limit 36 | // for the number of watches per user, and fs.inotify.max_user_instances 37 | // specifies the maximum number of inotify instances per user. Every Watcher you 38 | // create is an "instance", and every path you add is a "watch". 39 | // 40 | // These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and 41 | // /proc/sys/fs/inotify/max_user_instances 42 | // 43 | // To increase them you can use sysctl or write the value to the /proc file: 44 | // 45 | // # Default values on Linux 5.18 46 | // sysctl fs.inotify.max_user_watches=124983 47 | // sysctl fs.inotify.max_user_instances=128 48 | // 49 | // To make the changes persist on reboot edit /etc/sysctl.conf or 50 | // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check 51 | // your distro's documentation): 52 | // 53 | // fs.inotify.max_user_watches=124983 54 | // fs.inotify.max_user_instances=128 55 | // 56 | // Reaching the limit will result in a "no space left on device" or "too many open 57 | // files" error. 58 | // 59 | // # kqueue notes (macOS, BSD) 60 | // 61 | // kqueue requires opening a file descriptor for every file that's being watched; 62 | // so if you're watching a directory with five files then that's six file 63 | // descriptors. You will run in to your system's "max open files" limit faster on 64 | // these platforms. 65 | // 66 | // The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to 67 | // control the maximum number of open files, as well as /etc/login.conf on BSD 68 | // systems. 69 | // 70 | // # Windows notes 71 | // 72 | // Paths can be added as "C:\path\to\dir", but forward slashes 73 | // ("C:/path/to/dir") will also work. 74 | // 75 | // When a watched directory is removed it will always send an event for the 76 | // directory itself, but may not send events for all files in that directory. 77 | // Sometimes it will send events for all times, sometimes it will send no 78 | // events, and often only for some files. 79 | // 80 | // The default ReadDirectoryChangesW() buffer size is 64K, which is the largest 81 | // value that is guaranteed to work with SMB filesystems. If you have many 82 | // events in quick succession this may not be enough, and you will have to use 83 | // [WithBufferSize] to increase the value. 84 | type Watcher struct { 85 | // Events sends the filesystem change events. 86 | // 87 | // fsnotify can send the following events; a "path" here can refer to a 88 | // file, directory, symbolic link, or special file like a FIFO. 89 | // 90 | // fsnotify.Create A new path was created; this may be followed by one 91 | // or more Write events if data also gets written to a 92 | // file. 93 | // 94 | // fsnotify.Remove A path was removed. 95 | // 96 | // fsnotify.Rename A path was renamed. A rename is always sent with the 97 | // old path as Event.Name, and a Create event will be 98 | // sent with the new name. Renames are only sent for 99 | // paths that are currently watched; e.g. moving an 100 | // unmonitored file into a monitored directory will 101 | // show up as just a Create. Similarly, renaming a file 102 | // to outside a monitored directory will show up as 103 | // only a Rename. 104 | // 105 | // fsnotify.Write A file or named pipe was written to. A Truncate will 106 | // also trigger a Write. A single "write action" 107 | // initiated by the user may show up as one or multiple 108 | // writes, depending on when the system syncs things to 109 | // disk. For example when compiling a large Go program 110 | // you may get hundreds of Write events, and you may 111 | // want to wait until you've stopped receiving them 112 | // (see the dedup example in cmd/fsnotify). 113 | // 114 | // Some systems may send Write event for directories 115 | // when the directory content changes. 116 | // 117 | // fsnotify.Chmod Attributes were changed. On Linux this is also sent 118 | // when a file is removed (or more accurately, when a 119 | // link to an inode is removed). On kqueue it's sent 120 | // when a file is truncated. On Windows it's never 121 | // sent. 122 | Events chan Event 123 | 124 | // Errors sends any errors. 125 | Errors chan error 126 | 127 | mu sync.Mutex 128 | port *unix.EventPort 129 | done chan struct{} // Channel for sending a "quit message" to the reader goroutine 130 | dirs map[string]struct{} // Explicitly watched directories 131 | watches map[string]struct{} // Explicitly watched non-directories 132 | } 133 | 134 | // NewWatcher creates a new Watcher. 135 | func NewWatcher() (*Watcher, error) { 136 | return NewBufferedWatcher(0) 137 | } 138 | 139 | // NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events 140 | // channel. 141 | // 142 | // The main use case for this is situations with a very large number of events 143 | // where the kernel buffer size can't be increased (e.g. due to lack of 144 | // permissions). An unbuffered Watcher will perform better for almost all use 145 | // cases, and whenever possible you will be better off increasing the kernel 146 | // buffers instead of adding a large userspace buffer. 147 | func NewBufferedWatcher(sz uint) (*Watcher, error) { 148 | w := &Watcher{ 149 | Events: make(chan Event, sz), 150 | Errors: make(chan error), 151 | dirs: make(map[string]struct{}), 152 | watches: make(map[string]struct{}), 153 | done: make(chan struct{}), 154 | } 155 | 156 | var err error 157 | w.port, err = unix.NewEventPort() 158 | if err != nil { 159 | return nil, fmt.Errorf("fsnotify.NewWatcher: %w", err) 160 | } 161 | 162 | go w.readEvents() 163 | return w, nil 164 | } 165 | 166 | // sendEvent attempts to send an event to the user, returning true if the event 167 | // was put in the channel successfully and false if the watcher has been closed. 168 | func (w *Watcher) sendEvent(name string, op Op) (sent bool) { 169 | select { 170 | case w.Events <- Event{Name: name, Op: op}: 171 | return true 172 | case <-w.done: 173 | return false 174 | } 175 | } 176 | 177 | // sendError attempts to send an error to the user, returning true if the error 178 | // was put in the channel successfully and false if the watcher has been closed. 179 | func (w *Watcher) sendError(err error) (sent bool) { 180 | select { 181 | case w.Errors <- err: 182 | return true 183 | case <-w.done: 184 | return false 185 | } 186 | } 187 | 188 | func (w *Watcher) isClosed() bool { 189 | select { 190 | case <-w.done: 191 | return true 192 | default: 193 | return false 194 | } 195 | } 196 | 197 | // Close removes all watches and closes the Events channel. 198 | func (w *Watcher) Close() error { 199 | // Take the lock used by associateFile to prevent lingering events from 200 | // being processed after the close 201 | w.mu.Lock() 202 | defer w.mu.Unlock() 203 | if w.isClosed() { 204 | return nil 205 | } 206 | close(w.done) 207 | return w.port.Close() 208 | } 209 | 210 | // Add starts monitoring the path for changes. 211 | // 212 | // A path can only be watched once; watching it more than once is a no-op and will 213 | // not return an error. Paths that do not yet exist on the filesystem cannot be 214 | // watched. 215 | // 216 | // A watch will be automatically removed if the watched path is deleted or 217 | // renamed. The exception is the Windows backend, which doesn't remove the 218 | // watcher on renames. 219 | // 220 | // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special 221 | // filesystems (/proc, /sys, etc.) generally don't work. 222 | // 223 | // Returns [ErrClosed] if [Watcher.Close] was called. 224 | // 225 | // See [Watcher.AddWith] for a version that allows adding options. 226 | // 227 | // # Watching directories 228 | // 229 | // All files in a directory are monitored, including new files that are created 230 | // after the watcher is started. Subdirectories are not watched (i.e. it's 231 | // non-recursive). 232 | // 233 | // # Watching files 234 | // 235 | // Watching individual files (rather than directories) is generally not 236 | // recommended as many programs (especially editors) update files atomically: it 237 | // will write to a temporary file which is then moved to to destination, 238 | // overwriting the original (or some variant thereof). The watcher on the 239 | // original file is now lost, as that no longer exists. 240 | // 241 | // The upshot of this is that a power failure or crash won't leave a 242 | // half-written file. 243 | // 244 | // Watch the parent directory and use Event.Name to filter out files you're not 245 | // interested in. There is an example of this in cmd/fsnotify/file.go. 246 | func (w *Watcher) Add(name string) error { return w.AddWith(name) } 247 | 248 | // AddWith is like [Watcher.Add], but allows adding options. When using Add() 249 | // the defaults described below are used. 250 | // 251 | // Possible options are: 252 | // 253 | // - [WithBufferSize] sets the buffer size for the Windows backend; no-op on 254 | // other platforms. The default is 64K (65536 bytes). 255 | func (w *Watcher) AddWith(name string, opts ...addOpt) error { 256 | if w.isClosed() { 257 | return ErrClosed 258 | } 259 | if w.port.PathIsWatched(name) { 260 | return nil 261 | } 262 | 263 | _ = getOptions(opts...) 264 | 265 | // Currently we resolve symlinks that were explicitly requested to be 266 | // watched. Otherwise we would use LStat here. 267 | stat, err := os.Stat(name) 268 | if err != nil { 269 | return err 270 | } 271 | 272 | // Associate all files in the directory. 273 | if stat.IsDir() { 274 | err := w.handleDirectory(name, stat, true, w.associateFile) 275 | if err != nil { 276 | return err 277 | } 278 | 279 | w.mu.Lock() 280 | w.dirs[name] = struct{}{} 281 | w.mu.Unlock() 282 | return nil 283 | } 284 | 285 | err = w.associateFile(name, stat, true) 286 | if err != nil { 287 | return err 288 | } 289 | 290 | w.mu.Lock() 291 | w.watches[name] = struct{}{} 292 | w.mu.Unlock() 293 | return nil 294 | } 295 | 296 | // Remove stops monitoring the path for changes. 297 | // 298 | // Directories are always removed non-recursively. For example, if you added 299 | // /tmp/dir and /tmp/dir/subdir then you will need to remove both. 300 | // 301 | // Removing a path that has not yet been added returns [ErrNonExistentWatch]. 302 | // 303 | // Returns nil if [Watcher.Close] was called. 304 | func (w *Watcher) Remove(name string) error { 305 | if w.isClosed() { 306 | return nil 307 | } 308 | if !w.port.PathIsWatched(name) { 309 | return fmt.Errorf("%w: %s", ErrNonExistentWatch, name) 310 | } 311 | 312 | // The user has expressed an intent. Immediately remove this name from 313 | // whichever watch list it might be in. If it's not in there the delete 314 | // doesn't cause harm. 315 | w.mu.Lock() 316 | delete(w.watches, name) 317 | delete(w.dirs, name) 318 | w.mu.Unlock() 319 | 320 | stat, err := os.Stat(name) 321 | if err != nil { 322 | return err 323 | } 324 | 325 | // Remove associations for every file in the directory. 326 | if stat.IsDir() { 327 | err := w.handleDirectory(name, stat, false, w.dissociateFile) 328 | if err != nil { 329 | return err 330 | } 331 | return nil 332 | } 333 | 334 | err = w.port.DissociatePath(name) 335 | if err != nil { 336 | return err 337 | } 338 | 339 | return nil 340 | } 341 | 342 | // readEvents contains the main loop that runs in a goroutine watching for events. 343 | func (w *Watcher) readEvents() { 344 | // If this function returns, the watcher has been closed and we can close 345 | // these channels 346 | defer func() { 347 | close(w.Errors) 348 | close(w.Events) 349 | }() 350 | 351 | pevents := make([]unix.PortEvent, 8) 352 | for { 353 | count, err := w.port.Get(pevents, 1, nil) 354 | if err != nil && err != unix.ETIME { 355 | // Interrupted system call (count should be 0) ignore and continue 356 | if errors.Is(err, unix.EINTR) && count == 0 { 357 | continue 358 | } 359 | // Get failed because we called w.Close() 360 | if errors.Is(err, unix.EBADF) && w.isClosed() { 361 | return 362 | } 363 | // There was an error not caused by calling w.Close() 364 | if !w.sendError(err) { 365 | return 366 | } 367 | } 368 | 369 | p := pevents[:count] 370 | for _, pevent := range p { 371 | if pevent.Source != unix.PORT_SOURCE_FILE { 372 | // Event from unexpected source received; should never happen. 373 | if !w.sendError(errors.New("Event from unexpected source received")) { 374 | return 375 | } 376 | continue 377 | } 378 | 379 | err = w.handleEvent(&pevent) 380 | if err != nil { 381 | if !w.sendError(err) { 382 | return 383 | } 384 | } 385 | } 386 | } 387 | } 388 | 389 | func (w *Watcher) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error { 390 | files, err := os.ReadDir(path) 391 | if err != nil { 392 | return err 393 | } 394 | 395 | // Handle all children of the directory. 396 | for _, entry := range files { 397 | finfo, err := entry.Info() 398 | if err != nil { 399 | return err 400 | } 401 | err = handler(filepath.Join(path, finfo.Name()), finfo, false) 402 | if err != nil { 403 | return err 404 | } 405 | } 406 | 407 | // And finally handle the directory itself. 408 | return handler(path, stat, follow) 409 | } 410 | 411 | // handleEvent might need to emit more than one fsnotify event if the events 412 | // bitmap matches more than one event type (e.g. the file was both modified and 413 | // had the attributes changed between when the association was created and the 414 | // when event was returned) 415 | func (w *Watcher) handleEvent(event *unix.PortEvent) error { 416 | var ( 417 | events = event.Events 418 | path = event.Path 419 | fmode = event.Cookie.(os.FileMode) 420 | reRegister = true 421 | ) 422 | 423 | w.mu.Lock() 424 | _, watchedDir := w.dirs[path] 425 | _, watchedPath := w.watches[path] 426 | w.mu.Unlock() 427 | isWatched := watchedDir || watchedPath 428 | 429 | if events&unix.FILE_DELETE != 0 { 430 | if !w.sendEvent(path, Remove) { 431 | return nil 432 | } 433 | reRegister = false 434 | } 435 | if events&unix.FILE_RENAME_FROM != 0 { 436 | if !w.sendEvent(path, Rename) { 437 | return nil 438 | } 439 | // Don't keep watching the new file name 440 | reRegister = false 441 | } 442 | if events&unix.FILE_RENAME_TO != 0 { 443 | // We don't report a Rename event for this case, because Rename events 444 | // are interpreted as referring to the _old_ name of the file, and in 445 | // this case the event would refer to the new name of the file. This 446 | // type of rename event is not supported by fsnotify. 447 | 448 | // inotify reports a Remove event in this case, so we simulate this 449 | // here. 450 | if !w.sendEvent(path, Remove) { 451 | return nil 452 | } 453 | // Don't keep watching the file that was removed 454 | reRegister = false 455 | } 456 | 457 | // The file is gone, nothing left to do. 458 | if !reRegister { 459 | if watchedDir { 460 | w.mu.Lock() 461 | delete(w.dirs, path) 462 | w.mu.Unlock() 463 | } 464 | if watchedPath { 465 | w.mu.Lock() 466 | delete(w.watches, path) 467 | w.mu.Unlock() 468 | } 469 | return nil 470 | } 471 | 472 | // If we didn't get a deletion the file still exists and we're going to have 473 | // to watch it again. Let's Stat it now so that we can compare permissions 474 | // and have what we need to continue watching the file 475 | 476 | stat, err := os.Lstat(path) 477 | if err != nil { 478 | // This is unexpected, but we should still emit an event. This happens 479 | // most often on "rm -r" of a subdirectory inside a watched directory We 480 | // get a modify event of something happening inside, but by the time we 481 | // get here, the sudirectory is already gone. Clearly we were watching 482 | // this path but now it is gone. Let's tell the user that it was 483 | // removed. 484 | if !w.sendEvent(path, Remove) { 485 | return nil 486 | } 487 | // Suppress extra write events on removed directories; they are not 488 | // informative and can be confusing. 489 | return nil 490 | } 491 | 492 | // resolve symlinks that were explicitly watched as we would have at Add() 493 | // time. this helps suppress spurious Chmod events on watched symlinks 494 | if isWatched { 495 | stat, err = os.Stat(path) 496 | if err != nil { 497 | // The symlink still exists, but the target is gone. Report the 498 | // Remove similar to above. 499 | if !w.sendEvent(path, Remove) { 500 | return nil 501 | } 502 | // Don't return the error 503 | } 504 | } 505 | 506 | if events&unix.FILE_MODIFIED != 0 { 507 | if fmode.IsDir() { 508 | if watchedDir { 509 | if err := w.updateDirectory(path); err != nil { 510 | return err 511 | } 512 | } else { 513 | if !w.sendEvent(path, Write) { 514 | return nil 515 | } 516 | } 517 | } else { 518 | if !w.sendEvent(path, Write) { 519 | return nil 520 | } 521 | } 522 | } 523 | if events&unix.FILE_ATTRIB != 0 && stat != nil { 524 | // Only send Chmod if perms changed 525 | if stat.Mode().Perm() != fmode.Perm() { 526 | if !w.sendEvent(path, Chmod) { 527 | return nil 528 | } 529 | } 530 | } 531 | 532 | if stat != nil { 533 | // If we get here, it means we've hit an event above that requires us to 534 | // continue watching the file or directory 535 | return w.associateFile(path, stat, isWatched) 536 | } 537 | return nil 538 | } 539 | 540 | func (w *Watcher) updateDirectory(path string) error { 541 | // The directory was modified, so we must find unwatched entities and watch 542 | // them. If something was removed from the directory, nothing will happen, 543 | // as everything else should still be watched. 544 | files, err := os.ReadDir(path) 545 | if err != nil { 546 | return err 547 | } 548 | 549 | for _, entry := range files { 550 | path := filepath.Join(path, entry.Name()) 551 | if w.port.PathIsWatched(path) { 552 | continue 553 | } 554 | 555 | finfo, err := entry.Info() 556 | if err != nil { 557 | return err 558 | } 559 | err = w.associateFile(path, finfo, false) 560 | if err != nil { 561 | if !w.sendError(err) { 562 | return nil 563 | } 564 | } 565 | if !w.sendEvent(path, Create) { 566 | return nil 567 | } 568 | } 569 | return nil 570 | } 571 | 572 | func (w *Watcher) associateFile(path string, stat os.FileInfo, follow bool) error { 573 | if w.isClosed() { 574 | return ErrClosed 575 | } 576 | // This is primarily protecting the call to AssociatePath but it is 577 | // important and intentional that the call to PathIsWatched is also 578 | // protected by this mutex. Without this mutex, AssociatePath has been seen 579 | // to error out that the path is already associated. 580 | w.mu.Lock() 581 | defer w.mu.Unlock() 582 | 583 | if w.port.PathIsWatched(path) { 584 | // Remove the old association in favor of this one If we get ENOENT, 585 | // then while the x/sys/unix wrapper still thought that this path was 586 | // associated, the underlying event port did not. This call will have 587 | // cleared up that discrepancy. The most likely cause is that the event 588 | // has fired but we haven't processed it yet. 589 | err := w.port.DissociatePath(path) 590 | if err != nil && err != unix.ENOENT { 591 | return err 592 | } 593 | } 594 | // FILE_NOFOLLOW means we watch symlinks themselves rather than their 595 | // targets. 596 | events := unix.FILE_MODIFIED | unix.FILE_ATTRIB | unix.FILE_NOFOLLOW 597 | if follow { 598 | // We *DO* follow symlinks for explicitly watched entries. 599 | events = unix.FILE_MODIFIED | unix.FILE_ATTRIB 600 | } 601 | return w.port.AssociatePath(path, stat, 602 | events, 603 | stat.Mode()) 604 | } 605 | 606 | func (w *Watcher) dissociateFile(path string, stat os.FileInfo, unused bool) error { 607 | if !w.port.PathIsWatched(path) { 608 | return nil 609 | } 610 | return w.port.DissociatePath(path) 611 | } 612 | 613 | // WatchList returns all paths explicitly added with [Watcher.Add] (and are not 614 | // yet removed). 615 | // 616 | // Returns nil if [Watcher.Close] was called. 617 | func (w *Watcher) WatchList() []string { 618 | if w.isClosed() { 619 | return nil 620 | } 621 | 622 | w.mu.Lock() 623 | defer w.mu.Unlock() 624 | 625 | entries := make([]string, 0, len(w.watches)+len(w.dirs)) 626 | for pathname := range w.dirs { 627 | entries = append(entries, pathname) 628 | } 629 | for pathname := range w.watches { 630 | entries = append(entries, pathname) 631 | } 632 | 633 | return entries 634 | } 635 | -------------------------------------------------------------------------------- /backend_fen_test.go: -------------------------------------------------------------------------------- 1 | //go:build solaris 2 | // +build solaris 3 | 4 | package fsnotify 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestRemoveState(t *testing.T) { 13 | var ( 14 | tmp = t.TempDir() 15 | dir = join(tmp, "dir") 16 | file = join(dir, "file") 17 | ) 18 | mkdir(t, dir) 19 | touch(t, file) 20 | 21 | w := newWatcher(t, tmp) 22 | addWatch(t, w, tmp) 23 | addWatch(t, w, file) 24 | 25 | check := func(wantDirs, wantFiles int) { 26 | t.Helper() 27 | if len(w.watches) != wantFiles { 28 | var d []string 29 | for k, v := range w.watches { 30 | d = append(d, fmt.Sprintf("%#v = %#v", k, v)) 31 | } 32 | t.Errorf("unexpected number of entries in w.watches (have %d, want %d):\n%v", 33 | len(w.watches), wantFiles, strings.Join(d, "\n")) 34 | } 35 | if len(w.dirs) != wantDirs { 36 | var d []string 37 | for k, v := range w.dirs { 38 | d = append(d, fmt.Sprintf("%#v = %#v", k, v)) 39 | } 40 | t.Errorf("unexpected number of entries in w.dirs (have %d, want %d):\n%v", 41 | len(w.dirs), wantDirs, strings.Join(d, "\n")) 42 | } 43 | } 44 | 45 | check(1, 1) 46 | 47 | if err := w.Remove(file); err != nil { 48 | t.Fatal(err) 49 | } 50 | check(1, 0) 51 | 52 | if err := w.Remove(tmp); err != nil { 53 | t.Fatal(err) 54 | } 55 | check(0, 0) 56 | } 57 | -------------------------------------------------------------------------------- /backend_inotify.go: -------------------------------------------------------------------------------- 1 | //go:build linux && !appengine 2 | // +build linux,!appengine 3 | 4 | // Note: the documentation on the Watcher type and methods is generated from 5 | // mkdoc.zsh 6 | 7 | package fsnotify 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | "io" 13 | "os" 14 | "path/filepath" 15 | "strings" 16 | "sync" 17 | "unsafe" 18 | 19 | "golang.org/x/sys/unix" 20 | ) 21 | 22 | // Watcher watches a set of paths, delivering events on a channel. 23 | // 24 | // A watcher should not be copied (e.g. pass it by pointer, rather than by 25 | // value). 26 | // 27 | // # Linux notes 28 | // 29 | // When a file is removed a Remove event won't be emitted until all file 30 | // descriptors are closed, and deletes will always emit a Chmod. For example: 31 | // 32 | // fp := os.Open("file") 33 | // os.Remove("file") // Triggers Chmod 34 | // fp.Close() // Triggers Remove 35 | // 36 | // This is the event that inotify sends, so not much can be changed about this. 37 | // 38 | // The fs.inotify.max_user_watches sysctl variable specifies the upper limit 39 | // for the number of watches per user, and fs.inotify.max_user_instances 40 | // specifies the maximum number of inotify instances per user. Every Watcher you 41 | // create is an "instance", and every path you add is a "watch". 42 | // 43 | // These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and 44 | // /proc/sys/fs/inotify/max_user_instances 45 | // 46 | // To increase them you can use sysctl or write the value to the /proc file: 47 | // 48 | // # Default values on Linux 5.18 49 | // sysctl fs.inotify.max_user_watches=124983 50 | // sysctl fs.inotify.max_user_instances=128 51 | // 52 | // To make the changes persist on reboot edit /etc/sysctl.conf or 53 | // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check 54 | // your distro's documentation): 55 | // 56 | // fs.inotify.max_user_watches=124983 57 | // fs.inotify.max_user_instances=128 58 | // 59 | // Reaching the limit will result in a "no space left on device" or "too many open 60 | // files" error. 61 | // 62 | // # kqueue notes (macOS, BSD) 63 | // 64 | // kqueue requires opening a file descriptor for every file that's being watched; 65 | // so if you're watching a directory with five files then that's six file 66 | // descriptors. You will run in to your system's "max open files" limit faster on 67 | // these platforms. 68 | // 69 | // The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to 70 | // control the maximum number of open files, as well as /etc/login.conf on BSD 71 | // systems. 72 | // 73 | // # Windows notes 74 | // 75 | // Paths can be added as "C:\path\to\dir", but forward slashes 76 | // ("C:/path/to/dir") will also work. 77 | // 78 | // When a watched directory is removed it will always send an event for the 79 | // directory itself, but may not send events for all files in that directory. 80 | // Sometimes it will send events for all times, sometimes it will send no 81 | // events, and often only for some files. 82 | // 83 | // The default ReadDirectoryChangesW() buffer size is 64K, which is the largest 84 | // value that is guaranteed to work with SMB filesystems. If you have many 85 | // events in quick succession this may not be enough, and you will have to use 86 | // [WithBufferSize] to increase the value. 87 | type Watcher struct { 88 | // Events sends the filesystem change events. 89 | // 90 | // fsnotify can send the following events; a "path" here can refer to a 91 | // file, directory, symbolic link, or special file like a FIFO. 92 | // 93 | // fsnotify.Create A new path was created; this may be followed by one 94 | // or more Write events if data also gets written to a 95 | // file. 96 | // 97 | // fsnotify.Remove A path was removed. 98 | // 99 | // fsnotify.Rename A path was renamed. A rename is always sent with the 100 | // old path as Event.Name, and a Create event will be 101 | // sent with the new name. Renames are only sent for 102 | // paths that are currently watched; e.g. moving an 103 | // unmonitored file into a monitored directory will 104 | // show up as just a Create. Similarly, renaming a file 105 | // to outside a monitored directory will show up as 106 | // only a Rename. 107 | // 108 | // fsnotify.Write A file or named pipe was written to. A Truncate will 109 | // also trigger a Write. A single "write action" 110 | // initiated by the user may show up as one or multiple 111 | // writes, depending on when the system syncs things to 112 | // disk. For example when compiling a large Go program 113 | // you may get hundreds of Write events, and you may 114 | // want to wait until you've stopped receiving them 115 | // (see the dedup example in cmd/fsnotify). 116 | // 117 | // Some systems may send Write event for directories 118 | // when the directory content changes. 119 | // 120 | // fsnotify.Chmod Attributes were changed. On Linux this is also sent 121 | // when a file is removed (or more accurately, when a 122 | // link to an inode is removed). On kqueue it's sent 123 | // when a file is truncated. On Windows it's never 124 | // sent. 125 | Events chan Event 126 | 127 | // Errors sends any errors. 128 | Errors chan error 129 | 130 | // Store fd here as os.File.Read() will no longer return on close after 131 | // calling Fd(). See: https://github.com/golang/go/issues/26439 132 | fd int 133 | inotifyFile *os.File 134 | watches *watches 135 | done chan struct{} // Channel for sending a "quit message" to the reader goroutine 136 | closeMu sync.Mutex 137 | doneResp chan struct{} // Channel to respond to Close 138 | } 139 | 140 | type ( 141 | watches struct { 142 | mu sync.RWMutex 143 | wd map[uint32]*watch // wd → watch 144 | path map[string]uint32 // pathname → wd 145 | } 146 | watch struct { 147 | wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall) 148 | flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags) 149 | path string // Watch path. 150 | } 151 | ) 152 | 153 | func newWatches() *watches { 154 | return &watches{ 155 | wd: make(map[uint32]*watch), 156 | path: make(map[string]uint32), 157 | } 158 | } 159 | 160 | func (w *watches) len() int { 161 | w.mu.RLock() 162 | defer w.mu.RUnlock() 163 | return len(w.wd) 164 | } 165 | 166 | func (w *watches) add(ww *watch) { 167 | w.mu.Lock() 168 | defer w.mu.Unlock() 169 | w.wd[ww.wd] = ww 170 | w.path[ww.path] = ww.wd 171 | } 172 | 173 | func (w *watches) remove(wd uint32) { 174 | w.mu.Lock() 175 | defer w.mu.Unlock() 176 | delete(w.path, w.wd[wd].path) 177 | delete(w.wd, wd) 178 | } 179 | 180 | func (w *watches) removePath(path string) (uint32, bool) { 181 | w.mu.Lock() 182 | defer w.mu.Unlock() 183 | 184 | wd, ok := w.path[path] 185 | if !ok { 186 | return 0, false 187 | } 188 | 189 | delete(w.path, path) 190 | delete(w.wd, wd) 191 | 192 | return wd, true 193 | } 194 | 195 | func (w *watches) byPath(path string) *watch { 196 | w.mu.RLock() 197 | defer w.mu.RUnlock() 198 | return w.wd[w.path[path]] 199 | } 200 | 201 | func (w *watches) byWd(wd uint32) *watch { 202 | w.mu.RLock() 203 | defer w.mu.RUnlock() 204 | return w.wd[wd] 205 | } 206 | 207 | func (w *watches) updatePath(path string, f func(*watch) (*watch, error)) error { 208 | w.mu.Lock() 209 | defer w.mu.Unlock() 210 | 211 | var existing *watch 212 | wd, ok := w.path[path] 213 | if ok { 214 | existing = w.wd[wd] 215 | } 216 | 217 | upd, err := f(existing) 218 | if err != nil { 219 | return err 220 | } 221 | if upd != nil { 222 | w.wd[upd.wd] = upd 223 | w.path[upd.path] = upd.wd 224 | 225 | if upd.wd != wd { 226 | delete(w.wd, wd) 227 | } 228 | } 229 | 230 | return nil 231 | } 232 | 233 | // NewWatcher creates a new Watcher. 234 | func NewWatcher() (*Watcher, error) { 235 | return NewBufferedWatcher(0) 236 | } 237 | 238 | // NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events 239 | // channel. 240 | // 241 | // The main use case for this is situations with a very large number of events 242 | // where the kernel buffer size can't be increased (e.g. due to lack of 243 | // permissions). An unbuffered Watcher will perform better for almost all use 244 | // cases, and whenever possible you will be better off increasing the kernel 245 | // buffers instead of adding a large userspace buffer. 246 | func NewBufferedWatcher(sz uint) (*Watcher, error) { 247 | // Need to set nonblocking mode for SetDeadline to work, otherwise blocking 248 | // I/O operations won't terminate on close. 249 | fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK) 250 | if fd == -1 { 251 | return nil, errno 252 | } 253 | 254 | w := &Watcher{ 255 | fd: fd, 256 | inotifyFile: os.NewFile(uintptr(fd), ""), 257 | watches: newWatches(), 258 | Events: make(chan Event, sz), 259 | Errors: make(chan error), 260 | done: make(chan struct{}), 261 | doneResp: make(chan struct{}), 262 | } 263 | 264 | go w.readEvents() 265 | return w, nil 266 | } 267 | 268 | // Returns true if the event was sent, or false if watcher is closed. 269 | func (w *Watcher) sendEvent(e Event) bool { 270 | select { 271 | case w.Events <- e: 272 | return true 273 | case <-w.done: 274 | return false 275 | } 276 | } 277 | 278 | // Returns true if the error was sent, or false if watcher is closed. 279 | func (w *Watcher) sendError(err error) bool { 280 | select { 281 | case w.Errors <- err: 282 | return true 283 | case <-w.done: 284 | return false 285 | } 286 | } 287 | 288 | func (w *Watcher) isClosed() bool { 289 | select { 290 | case <-w.done: 291 | return true 292 | default: 293 | return false 294 | } 295 | } 296 | 297 | // Close removes all watches and closes the Events channel. 298 | func (w *Watcher) Close() error { 299 | w.closeMu.Lock() 300 | if w.isClosed() { 301 | w.closeMu.Unlock() 302 | return nil 303 | } 304 | close(w.done) 305 | w.closeMu.Unlock() 306 | 307 | // Causes any blocking reads to return with an error, provided the file 308 | // still supports deadline operations. 309 | err := w.inotifyFile.Close() 310 | if err != nil { 311 | return err 312 | } 313 | 314 | // Wait for goroutine to close 315 | <-w.doneResp 316 | 317 | return nil 318 | } 319 | 320 | // Add starts monitoring the path for changes. 321 | // 322 | // A path can only be watched once; watching it more than once is a no-op and will 323 | // not return an error. Paths that do not yet exist on the filesystem cannot be 324 | // watched. 325 | // 326 | // A watch will be automatically removed if the watched path is deleted or 327 | // renamed. The exception is the Windows backend, which doesn't remove the 328 | // watcher on renames. 329 | // 330 | // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special 331 | // filesystems (/proc, /sys, etc.) generally don't work. 332 | // 333 | // Returns [ErrClosed] if [Watcher.Close] was called. 334 | // 335 | // See [Watcher.AddWith] for a version that allows adding options. 336 | // 337 | // # Watching directories 338 | // 339 | // All files in a directory are monitored, including new files that are created 340 | // after the watcher is started. Subdirectories are not watched (i.e. it's 341 | // non-recursive). 342 | // 343 | // # Watching files 344 | // 345 | // Watching individual files (rather than directories) is generally not 346 | // recommended as many programs (especially editors) update files atomically: it 347 | // will write to a temporary file which is then moved to to destination, 348 | // overwriting the original (or some variant thereof). The watcher on the 349 | // original file is now lost, as that no longer exists. 350 | // 351 | // The upshot of this is that a power failure or crash won't leave a 352 | // half-written file. 353 | // 354 | // Watch the parent directory and use Event.Name to filter out files you're not 355 | // interested in. There is an example of this in cmd/fsnotify/file.go. 356 | func (w *Watcher) Add(name string) error { return w.AddWith(name) } 357 | 358 | // AddWith is like [Watcher.Add], but allows adding options. When using Add() 359 | // the defaults described below are used. 360 | // 361 | // Possible options are: 362 | // 363 | // - [WithBufferSize] sets the buffer size for the Windows backend; no-op on 364 | // other platforms. The default is 64K (65536 bytes). 365 | func (w *Watcher) AddWith(name string, opts ...addOpt) error { 366 | if w.isClosed() { 367 | return ErrClosed 368 | } 369 | 370 | name = filepath.Clean(name) 371 | _ = getOptions(opts...) 372 | 373 | var flags uint32 = unix.IN_MOVED_TO | unix.IN_MOVED_FROM | 374 | unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY | 375 | unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF 376 | 377 | return w.watches.updatePath(name, func(existing *watch) (*watch, error) { 378 | if existing != nil { 379 | flags |= existing.flags | unix.IN_MASK_ADD 380 | } 381 | 382 | wd, err := unix.InotifyAddWatch(w.fd, name, flags) 383 | if wd == -1 { 384 | return nil, err 385 | } 386 | 387 | if existing == nil { 388 | return &watch{ 389 | wd: uint32(wd), 390 | path: name, 391 | flags: flags, 392 | }, nil 393 | } 394 | 395 | existing.wd = uint32(wd) 396 | existing.flags = flags 397 | return existing, nil 398 | }) 399 | } 400 | 401 | // Remove stops monitoring the path for changes. 402 | // 403 | // Directories are always removed non-recursively. For example, if you added 404 | // /tmp/dir and /tmp/dir/subdir then you will need to remove both. 405 | // 406 | // Removing a path that has not yet been added returns [ErrNonExistentWatch]. 407 | // 408 | // Returns nil if [Watcher.Close] was called. 409 | func (w *Watcher) Remove(name string) error { 410 | if w.isClosed() { 411 | return nil 412 | } 413 | return w.remove(filepath.Clean(name)) 414 | } 415 | 416 | func (w *Watcher) remove(name string) error { 417 | wd, ok := w.watches.removePath(name) 418 | if !ok { 419 | return fmt.Errorf("%w: %s", ErrNonExistentWatch, name) 420 | } 421 | 422 | success, errno := unix.InotifyRmWatch(w.fd, wd) 423 | if success == -1 { 424 | // TODO: Perhaps it's not helpful to return an error here in every case; 425 | // The only two possible errors are: 426 | // 427 | // - EBADF, which happens when w.fd is not a valid file descriptor 428 | // of any kind. 429 | // - EINVAL, which is when fd is not an inotify descriptor or wd 430 | // is not a valid watch descriptor. Watch descriptors are 431 | // invalidated when they are removed explicitly or implicitly; 432 | // explicitly by inotify_rm_watch, implicitly when the file they 433 | // are watching is deleted. 434 | return errno 435 | } 436 | return nil 437 | } 438 | 439 | // WatchList returns all paths explicitly added with [Watcher.Add] (and are not 440 | // yet removed). 441 | // 442 | // Returns nil if [Watcher.Close] was called. 443 | func (w *Watcher) WatchList() []string { 444 | if w.isClosed() { 445 | return nil 446 | } 447 | 448 | entries := make([]string, 0, w.watches.len()) 449 | w.watches.mu.RLock() 450 | for pathname := range w.watches.path { 451 | entries = append(entries, pathname) 452 | } 453 | w.watches.mu.RUnlock() 454 | 455 | return entries 456 | } 457 | 458 | // readEvents reads from the inotify file descriptor, converts the 459 | // received events into Event objects and sends them via the Events channel 460 | func (w *Watcher) readEvents() { 461 | defer func() { 462 | close(w.doneResp) 463 | close(w.Errors) 464 | close(w.Events) 465 | }() 466 | 467 | var ( 468 | buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events 469 | errno error // Syscall errno 470 | ) 471 | for { 472 | // See if we have been closed. 473 | if w.isClosed() { 474 | return 475 | } 476 | 477 | n, err := w.inotifyFile.Read(buf[:]) 478 | switch { 479 | case errors.Unwrap(err) == os.ErrClosed: 480 | return 481 | case err != nil: 482 | if !w.sendError(err) { 483 | return 484 | } 485 | continue 486 | } 487 | 488 | if n < unix.SizeofInotifyEvent { 489 | var err error 490 | if n == 0 { 491 | err = io.EOF // If EOF is received. This should really never happen. 492 | } else if n < 0 { 493 | err = errno // If an error occurred while reading. 494 | } else { 495 | err = errors.New("notify: short read in readEvents()") // Read was too short. 496 | } 497 | if !w.sendError(err) { 498 | return 499 | } 500 | continue 501 | } 502 | 503 | var offset uint32 504 | // We don't know how many events we just read into the buffer 505 | // While the offset points to at least one whole event... 506 | for offset <= uint32(n-unix.SizeofInotifyEvent) { 507 | var ( 508 | // Point "raw" to the event in the buffer 509 | raw = (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset])) 510 | mask = uint32(raw.Mask) 511 | nameLen = uint32(raw.Len) 512 | ) 513 | 514 | if mask&unix.IN_Q_OVERFLOW != 0 { 515 | if !w.sendError(ErrEventOverflow) { 516 | return 517 | } 518 | } 519 | 520 | // If the event happened to the watched directory or the watched file, the kernel 521 | // doesn't append the filename to the event, but we would like to always fill the 522 | // the "Name" field with a valid filename. We retrieve the path of the watch from 523 | // the "paths" map. 524 | watch := w.watches.byWd(uint32(raw.Wd)) 525 | 526 | // inotify will automatically remove the watch on deletes; just need 527 | // to clean our state here. 528 | if watch != nil && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF { 529 | w.watches.remove(watch.wd) 530 | } 531 | // We can't really update the state when a watched path is moved; 532 | // only IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove 533 | // the watch. 534 | if watch != nil && mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF { 535 | err := w.remove(watch.path) 536 | if err != nil && !errors.Is(err, ErrNonExistentWatch) { 537 | if !w.sendError(err) { 538 | return 539 | } 540 | } 541 | } 542 | 543 | var name string 544 | if watch != nil { 545 | name = watch.path 546 | } 547 | if nameLen > 0 { 548 | // Point "bytes" at the first byte of the filename 549 | bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen] 550 | // The filename is padded with NULL bytes. TrimRight() gets rid of those. 551 | name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000") 552 | } 553 | 554 | event := w.newEvent(name, mask) 555 | 556 | // Send the events that are not ignored on the events channel 557 | if mask&unix.IN_IGNORED == 0 { 558 | if !w.sendEvent(event) { 559 | return 560 | } 561 | } 562 | 563 | // Move to the next event in the buffer 564 | offset += unix.SizeofInotifyEvent + nameLen 565 | } 566 | } 567 | } 568 | 569 | // newEvent returns an platform-independent Event based on an inotify mask. 570 | func (w *Watcher) newEvent(name string, mask uint32) Event { 571 | e := Event{Name: name} 572 | if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO { 573 | e.Op |= Create 574 | } 575 | if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF || mask&unix.IN_DELETE == unix.IN_DELETE { 576 | e.Op |= Remove 577 | } 578 | if mask&unix.IN_MODIFY == unix.IN_MODIFY { 579 | e.Op |= Write 580 | } 581 | if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM { 582 | e.Op |= Rename 583 | } 584 | if mask&unix.IN_ATTRIB == unix.IN_ATTRIB { 585 | e.Op |= Chmod 586 | } 587 | return e 588 | } 589 | -------------------------------------------------------------------------------- /backend_inotify_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package fsnotify 5 | 6 | import ( 7 | "errors" 8 | "os" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | // Ensure that the correct error is returned on overflows. 17 | func TestInotifyOverflow(t *testing.T) { 18 | t.Parallel() 19 | 20 | tmp := t.TempDir() 21 | w := newWatcher(t) 22 | defer w.Close() 23 | 24 | // We need to generate many more events than the 25 | // fs.inotify.max_queued_events sysctl setting. 26 | numDirs, numFiles := 128, 1024 27 | 28 | // All events need to be in the inotify queue before pulling events off it 29 | // to trigger this error. 30 | var wg sync.WaitGroup 31 | for i := 0; i < numDirs; i++ { 32 | wg.Add(1) 33 | go func(i int) { 34 | defer wg.Done() 35 | 36 | dir := join(tmp, strconv.Itoa(i)) 37 | mkdir(t, dir, noWait) 38 | addWatch(t, w, dir) 39 | 40 | createFiles(t, dir, "", numFiles, 10*time.Second) 41 | }(i) 42 | } 43 | wg.Wait() 44 | 45 | var ( 46 | creates = 0 47 | overflows = 0 48 | ) 49 | for overflows == 0 && creates < numDirs*numFiles { 50 | select { 51 | case <-time.After(10 * time.Second): 52 | t.Fatalf("Not done") 53 | case err := <-w.Errors: 54 | if !errors.Is(err, ErrEventOverflow) { 55 | t.Fatalf("unexpected error from watcher: %v", err) 56 | } 57 | overflows++ 58 | case e := <-w.Events: 59 | if !strings.HasPrefix(e.Name, tmp) { 60 | t.Fatalf("Event for unknown file: %s", e.Name) 61 | } 62 | if e.Op == Create { 63 | creates++ 64 | } 65 | } 66 | } 67 | 68 | if creates == numDirs*numFiles { 69 | t.Fatalf("could not trigger overflow") 70 | } 71 | if overflows == 0 { 72 | t.Fatalf("no overflow and not enough CREATE events (expected %d, got %d)", 73 | numDirs*numFiles, creates) 74 | } 75 | } 76 | 77 | // Test inotify's "we don't send REMOVE until all file descriptors are removed" 78 | // behaviour. 79 | func TestInotifyDeleteOpenFile(t *testing.T) { 80 | t.Parallel() 81 | 82 | tmp := t.TempDir() 83 | file := join(tmp, "file") 84 | 85 | touch(t, file) 86 | fp, err := os.Open(file) 87 | if err != nil { 88 | t.Fatalf("Create failed: %v", err) 89 | } 90 | defer fp.Close() 91 | 92 | w := newCollector(t, file) 93 | w.collect(t) 94 | 95 | rm(t, file) 96 | waitForEvents() 97 | e := w.events(t) 98 | cmpEvents(t, tmp, e, newEvents(t, `chmod /file`)) 99 | 100 | fp.Close() 101 | e = w.stop(t) 102 | cmpEvents(t, tmp, e, newEvents(t, `remove /file`)) 103 | } 104 | 105 | func TestRemoveState(t *testing.T) { 106 | var ( 107 | tmp = t.TempDir() 108 | dir = join(tmp, "dir") 109 | file = join(dir, "file") 110 | ) 111 | mkdir(t, dir) 112 | touch(t, file) 113 | 114 | w := newWatcher(t, tmp) 115 | addWatch(t, w, tmp) 116 | addWatch(t, w, file) 117 | 118 | check := func(want int) { 119 | t.Helper() 120 | if w.watches.len() != want { 121 | t.Error(w.watches) 122 | } 123 | } 124 | 125 | check(2) 126 | 127 | if err := w.Remove(file); err != nil { 128 | t.Fatal(err) 129 | } 130 | check(1) 131 | 132 | if err := w.Remove(tmp); err != nil { 133 | t.Fatal(err) 134 | } 135 | check(0) 136 | } 137 | -------------------------------------------------------------------------------- /backend_kqueue.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd || openbsd || netbsd || dragonfly || darwin 2 | // +build freebsd openbsd netbsd dragonfly darwin 3 | 4 | // Note: the documentation on the Watcher type and methods is generated from 5 | // mkdoc.zsh 6 | 7 | package fsnotify 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | "os" 13 | "path/filepath" 14 | "sync" 15 | 16 | "golang.org/x/sys/unix" 17 | ) 18 | 19 | // Watcher watches a set of paths, delivering events on a channel. 20 | // 21 | // A watcher should not be copied (e.g. pass it by pointer, rather than by 22 | // value). 23 | // 24 | // # Linux notes 25 | // 26 | // When a file is removed a Remove event won't be emitted until all file 27 | // descriptors are closed, and deletes will always emit a Chmod. For example: 28 | // 29 | // fp := os.Open("file") 30 | // os.Remove("file") // Triggers Chmod 31 | // fp.Close() // Triggers Remove 32 | // 33 | // This is the event that inotify sends, so not much can be changed about this. 34 | // 35 | // The fs.inotify.max_user_watches sysctl variable specifies the upper limit 36 | // for the number of watches per user, and fs.inotify.max_user_instances 37 | // specifies the maximum number of inotify instances per user. Every Watcher you 38 | // create is an "instance", and every path you add is a "watch". 39 | // 40 | // These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and 41 | // /proc/sys/fs/inotify/max_user_instances 42 | // 43 | // To increase them you can use sysctl or write the value to the /proc file: 44 | // 45 | // # Default values on Linux 5.18 46 | // sysctl fs.inotify.max_user_watches=124983 47 | // sysctl fs.inotify.max_user_instances=128 48 | // 49 | // To make the changes persist on reboot edit /etc/sysctl.conf or 50 | // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check 51 | // your distro's documentation): 52 | // 53 | // fs.inotify.max_user_watches=124983 54 | // fs.inotify.max_user_instances=128 55 | // 56 | // Reaching the limit will result in a "no space left on device" or "too many open 57 | // files" error. 58 | // 59 | // # kqueue notes (macOS, BSD) 60 | // 61 | // kqueue requires opening a file descriptor for every file that's being watched; 62 | // so if you're watching a directory with five files then that's six file 63 | // descriptors. You will run in to your system's "max open files" limit faster on 64 | // these platforms. 65 | // 66 | // The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to 67 | // control the maximum number of open files, as well as /etc/login.conf on BSD 68 | // systems. 69 | // 70 | // # Windows notes 71 | // 72 | // Paths can be added as "C:\path\to\dir", but forward slashes 73 | // ("C:/path/to/dir") will also work. 74 | // 75 | // When a watched directory is removed it will always send an event for the 76 | // directory itself, but may not send events for all files in that directory. 77 | // Sometimes it will send events for all times, sometimes it will send no 78 | // events, and often only for some files. 79 | // 80 | // The default ReadDirectoryChangesW() buffer size is 64K, which is the largest 81 | // value that is guaranteed to work with SMB filesystems. If you have many 82 | // events in quick succession this may not be enough, and you will have to use 83 | // [WithBufferSize] to increase the value. 84 | type Watcher struct { 85 | // Events sends the filesystem change events. 86 | // 87 | // fsnotify can send the following events; a "path" here can refer to a 88 | // file, directory, symbolic link, or special file like a FIFO. 89 | // 90 | // fsnotify.Create A new path was created; this may be followed by one 91 | // or more Write events if data also gets written to a 92 | // file. 93 | // 94 | // fsnotify.Remove A path was removed. 95 | // 96 | // fsnotify.Rename A path was renamed. A rename is always sent with the 97 | // old path as Event.Name, and a Create event will be 98 | // sent with the new name. Renames are only sent for 99 | // paths that are currently watched; e.g. moving an 100 | // unmonitored file into a monitored directory will 101 | // show up as just a Create. Similarly, renaming a file 102 | // to outside a monitored directory will show up as 103 | // only a Rename. 104 | // 105 | // fsnotify.Write A file or named pipe was written to. A Truncate will 106 | // also trigger a Write. A single "write action" 107 | // initiated by the user may show up as one or multiple 108 | // writes, depending on when the system syncs things to 109 | // disk. For example when compiling a large Go program 110 | // you may get hundreds of Write events, and you may 111 | // want to wait until you've stopped receiving them 112 | // (see the dedup example in cmd/fsnotify). 113 | // 114 | // Some systems may send Write event for directories 115 | // when the directory content changes. 116 | // 117 | // fsnotify.Chmod Attributes were changed. On Linux this is also sent 118 | // when a file is removed (or more accurately, when a 119 | // link to an inode is removed). On kqueue it's sent 120 | // when a file is truncated. On Windows it's never 121 | // sent. 122 | Events chan Event 123 | 124 | // Errors sends any errors. 125 | Errors chan error 126 | 127 | done chan struct{} 128 | kq int // File descriptor (as returned by the kqueue() syscall). 129 | closepipe [2]int // Pipe used for closing. 130 | mu sync.Mutex // Protects access to watcher data 131 | watches map[string]int // Watched file descriptors (key: path). 132 | watchesByDir map[string]map[int]struct{} // Watched file descriptors indexed by the parent directory (key: dirname(path)). 133 | userWatches map[string]struct{} // Watches added with Watcher.Add() 134 | dirFlags map[string]uint32 // Watched directories to fflags used in kqueue. 135 | paths map[int]pathInfo // File descriptors to path names for processing kqueue events. 136 | fileExists map[string]struct{} // Keep track of if we know this file exists (to stop duplicate create events). 137 | isClosed bool // Set to true when Close() is first called 138 | } 139 | 140 | type pathInfo struct { 141 | name string 142 | isDir bool 143 | } 144 | 145 | // NewWatcher creates a new Watcher. 146 | func NewWatcher() (*Watcher, error) { 147 | return NewBufferedWatcher(0) 148 | } 149 | 150 | // NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events 151 | // channel. 152 | // 153 | // The main use case for this is situations with a very large number of events 154 | // where the kernel buffer size can't be increased (e.g. due to lack of 155 | // permissions). An unbuffered Watcher will perform better for almost all use 156 | // cases, and whenever possible you will be better off increasing the kernel 157 | // buffers instead of adding a large userspace buffer. 158 | func NewBufferedWatcher(sz uint) (*Watcher, error) { 159 | kq, closepipe, err := newKqueue() 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | w := &Watcher{ 165 | kq: kq, 166 | closepipe: closepipe, 167 | watches: make(map[string]int), 168 | watchesByDir: make(map[string]map[int]struct{}), 169 | dirFlags: make(map[string]uint32), 170 | paths: make(map[int]pathInfo), 171 | fileExists: make(map[string]struct{}), 172 | userWatches: make(map[string]struct{}), 173 | Events: make(chan Event, sz), 174 | Errors: make(chan error), 175 | done: make(chan struct{}), 176 | } 177 | 178 | go w.readEvents() 179 | return w, nil 180 | } 181 | 182 | // newKqueue creates a new kernel event queue and returns a descriptor. 183 | // 184 | // This registers a new event on closepipe, which will trigger an event when 185 | // it's closed. This way we can use kevent() without timeout/polling; without 186 | // the closepipe, it would block forever and we wouldn't be able to stop it at 187 | // all. 188 | func newKqueue() (kq int, closepipe [2]int, err error) { 189 | kq, err = unix.Kqueue() 190 | if kq == -1 { 191 | return kq, closepipe, err 192 | } 193 | 194 | // Register the close pipe. 195 | err = unix.Pipe(closepipe[:]) 196 | if err != nil { 197 | unix.Close(kq) 198 | return kq, closepipe, err 199 | } 200 | unix.CloseOnExec(closepipe[0]) 201 | unix.CloseOnExec(closepipe[1]) 202 | 203 | // Register changes to listen on the closepipe. 204 | changes := make([]unix.Kevent_t, 1) 205 | // SetKevent converts int to the platform-specific types. 206 | unix.SetKevent(&changes[0], closepipe[0], unix.EVFILT_READ, 207 | unix.EV_ADD|unix.EV_ENABLE|unix.EV_ONESHOT) 208 | 209 | ok, err := unix.Kevent(kq, changes, nil, nil) 210 | if ok == -1 { 211 | unix.Close(kq) 212 | unix.Close(closepipe[0]) 213 | unix.Close(closepipe[1]) 214 | return kq, closepipe, err 215 | } 216 | return kq, closepipe, nil 217 | } 218 | 219 | // Returns true if the event was sent, or false if watcher is closed. 220 | func (w *Watcher) sendEvent(e Event) bool { 221 | select { 222 | case w.Events <- e: 223 | return true 224 | case <-w.done: 225 | return false 226 | } 227 | } 228 | 229 | // Returns true if the error was sent, or false if watcher is closed. 230 | func (w *Watcher) sendError(err error) bool { 231 | select { 232 | case w.Errors <- err: 233 | return true 234 | case <-w.done: 235 | return false 236 | } 237 | } 238 | 239 | // Close removes all watches and closes the Events channel. 240 | func (w *Watcher) Close() error { 241 | w.mu.Lock() 242 | if w.isClosed { 243 | w.mu.Unlock() 244 | return nil 245 | } 246 | w.isClosed = true 247 | 248 | // copy paths to remove while locked 249 | pathsToRemove := make([]string, 0, len(w.watches)) 250 | for name := range w.watches { 251 | pathsToRemove = append(pathsToRemove, name) 252 | } 253 | w.mu.Unlock() // Unlock before calling Remove, which also locks 254 | for _, name := range pathsToRemove { 255 | w.Remove(name) 256 | } 257 | 258 | // Send "quit" message to the reader goroutine. 259 | unix.Close(w.closepipe[1]) 260 | close(w.done) 261 | 262 | return nil 263 | } 264 | 265 | // Add starts monitoring the path for changes. 266 | // 267 | // A path can only be watched once; watching it more than once is a no-op and will 268 | // not return an error. Paths that do not yet exist on the filesystem cannot be 269 | // watched. 270 | // 271 | // A watch will be automatically removed if the watched path is deleted or 272 | // renamed. The exception is the Windows backend, which doesn't remove the 273 | // watcher on renames. 274 | // 275 | // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special 276 | // filesystems (/proc, /sys, etc.) generally don't work. 277 | // 278 | // Returns [ErrClosed] if [Watcher.Close] was called. 279 | // 280 | // See [Watcher.AddWith] for a version that allows adding options. 281 | // 282 | // # Watching directories 283 | // 284 | // All files in a directory are monitored, including new files that are created 285 | // after the watcher is started. Subdirectories are not watched (i.e. it's 286 | // non-recursive). 287 | // 288 | // # Watching files 289 | // 290 | // Watching individual files (rather than directories) is generally not 291 | // recommended as many programs (especially editors) update files atomically: it 292 | // will write to a temporary file which is then moved to to destination, 293 | // overwriting the original (or some variant thereof). The watcher on the 294 | // original file is now lost, as that no longer exists. 295 | // 296 | // The upshot of this is that a power failure or crash won't leave a 297 | // half-written file. 298 | // 299 | // Watch the parent directory and use Event.Name to filter out files you're not 300 | // interested in. There is an example of this in cmd/fsnotify/file.go. 301 | func (w *Watcher) Add(name string) error { return w.AddWith(name) } 302 | 303 | // AddWith is like [Watcher.Add], but allows adding options. When using Add() 304 | // the defaults described below are used. 305 | // 306 | // Possible options are: 307 | // 308 | // - [WithBufferSize] sets the buffer size for the Windows backend; no-op on 309 | // other platforms. The default is 64K (65536 bytes). 310 | func (w *Watcher) AddWith(name string, opts ...addOpt) error { 311 | _ = getOptions(opts...) 312 | 313 | w.mu.Lock() 314 | w.userWatches[name] = struct{}{} 315 | w.mu.Unlock() 316 | _, err := w.addWatch(name, noteAllEvents) 317 | return err 318 | } 319 | 320 | func (w *Watcher) AddWithEvents(name string, flags uint32) error { 321 | w.mu.Lock() 322 | w.userWatches[name] = struct{}{} 323 | w.mu.Unlock() 324 | _, err := w.addWatch(name, flags) 325 | return err 326 | } 327 | 328 | // Remove stops monitoring the path for changes. 329 | // 330 | // Directories are always removed non-recursively. For example, if you added 331 | // /tmp/dir and /tmp/dir/subdir then you will need to remove both. 332 | // 333 | // Removing a path that has not yet been added returns [ErrNonExistentWatch]. 334 | // 335 | // Returns nil if [Watcher.Close] was called. 336 | func (w *Watcher) Remove(name string) error { 337 | return w.remove(name, true) 338 | } 339 | 340 | func (w *Watcher) remove(name string, unwatchFiles bool) error { 341 | name = filepath.Clean(name) 342 | w.mu.Lock() 343 | if w.isClosed { 344 | w.mu.Unlock() 345 | return nil 346 | } 347 | watchfd, ok := w.watches[name] 348 | w.mu.Unlock() 349 | if !ok { 350 | return fmt.Errorf("%w: %s", ErrNonExistentWatch, name) 351 | } 352 | 353 | err := w.register([]int{watchfd}, unix.EV_DELETE, 0) 354 | if err != nil { 355 | return err 356 | } 357 | 358 | unix.Close(watchfd) 359 | 360 | w.mu.Lock() 361 | isDir := w.paths[watchfd].isDir 362 | delete(w.watches, name) 363 | delete(w.userWatches, name) 364 | 365 | parentName := filepath.Dir(name) 366 | delete(w.watchesByDir[parentName], watchfd) 367 | 368 | if len(w.watchesByDir[parentName]) == 0 { 369 | delete(w.watchesByDir, parentName) 370 | } 371 | 372 | delete(w.paths, watchfd) 373 | delete(w.dirFlags, name) 374 | delete(w.fileExists, name) 375 | w.mu.Unlock() 376 | 377 | // Find all watched paths that are in this directory that are not external. 378 | if unwatchFiles && isDir { 379 | var pathsToRemove []string 380 | w.mu.Lock() 381 | for fd := range w.watchesByDir[name] { 382 | path := w.paths[fd] 383 | if _, ok := w.userWatches[path.name]; !ok { 384 | pathsToRemove = append(pathsToRemove, path.name) 385 | } 386 | } 387 | w.mu.Unlock() 388 | for _, name := range pathsToRemove { 389 | // Since these are internal, not much sense in propagating error to 390 | // the user, as that will just confuse them with an error about a 391 | // path they did not explicitly watch themselves. 392 | w.Remove(name) 393 | } 394 | } 395 | return nil 396 | } 397 | 398 | // WatchList returns all paths explicitly added with [Watcher.Add] (and are not 399 | // yet removed). 400 | // 401 | // Returns nil if [Watcher.Close] was called. 402 | func (w *Watcher) WatchList() []string { 403 | w.mu.Lock() 404 | defer w.mu.Unlock() 405 | if w.isClosed { 406 | return nil 407 | } 408 | 409 | entries := make([]string, 0, len(w.userWatches)) 410 | for pathname := range w.userWatches { 411 | entries = append(entries, pathname) 412 | } 413 | 414 | return entries 415 | } 416 | 417 | // Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE) 418 | const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME 419 | 420 | // addWatch adds name to the watched file set; the flags are interpreted as 421 | // described in kevent(2). 422 | // 423 | // Returns the real path to the file which was added, with symlinks resolved. 424 | func (w *Watcher) addWatch(name string, flags uint32) (string, error) { 425 | var isDir bool 426 | name = filepath.Clean(name) 427 | 428 | w.mu.Lock() 429 | if w.isClosed { 430 | w.mu.Unlock() 431 | return "", ErrClosed 432 | } 433 | watchfd, alreadyWatching := w.watches[name] 434 | // We already have a watch, but we can still override flags. 435 | if alreadyWatching { 436 | isDir = w.paths[watchfd].isDir 437 | } 438 | w.mu.Unlock() 439 | 440 | if !alreadyWatching { 441 | fi, err := os.Lstat(name) 442 | if err != nil { 443 | return "", err 444 | } 445 | 446 | // Don't watch sockets or named pipes 447 | if (fi.Mode()&os.ModeSocket == os.ModeSocket) || (fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe) { 448 | return "", nil 449 | } 450 | 451 | // Follow Symlinks. 452 | if fi.Mode()&os.ModeSymlink == os.ModeSymlink { 453 | link, err := os.Readlink(name) 454 | if err != nil { 455 | // Return nil because Linux can add unresolvable symlinks to the 456 | // watch list without problems, so maintain consistency with 457 | // that. There will be no file events for broken symlinks. 458 | // TODO: more specific check; returns os.PathError; ENOENT? 459 | return "", nil 460 | } 461 | 462 | w.mu.Lock() 463 | _, alreadyWatching = w.watches[link] 464 | w.mu.Unlock() 465 | 466 | if alreadyWatching { 467 | // Add to watches so we don't get spurious Create events later 468 | // on when we diff the directories. 469 | w.watches[name] = 0 470 | w.fileExists[name] = struct{}{} 471 | return link, nil 472 | } 473 | 474 | name = link 475 | fi, err = os.Lstat(name) 476 | if err != nil { 477 | return "", nil 478 | } 479 | } 480 | 481 | // Retry on EINTR; open() can return EINTR in practice on macOS. 482 | // See #354, and Go issues 11180 and 39237. 483 | for { 484 | watchfd, err = unix.Open(name, openMode, 0) 485 | if err == nil { 486 | break 487 | } 488 | if errors.Is(err, unix.EINTR) { 489 | continue 490 | } 491 | 492 | return "", err 493 | } 494 | 495 | isDir = fi.IsDir() 496 | } 497 | 498 | err := w.register([]int{watchfd}, unix.EV_ADD|unix.EV_CLEAR|unix.EV_ENABLE, flags) 499 | if err != nil { 500 | unix.Close(watchfd) 501 | return "", err 502 | } 503 | 504 | if !alreadyWatching { 505 | w.mu.Lock() 506 | parentName := filepath.Dir(name) 507 | w.watches[name] = watchfd 508 | 509 | watchesByDir, ok := w.watchesByDir[parentName] 510 | if !ok { 511 | watchesByDir = make(map[int]struct{}, 1) 512 | w.watchesByDir[parentName] = watchesByDir 513 | } 514 | watchesByDir[watchfd] = struct{}{} 515 | w.paths[watchfd] = pathInfo{name: name, isDir: isDir} 516 | w.mu.Unlock() 517 | } 518 | 519 | if isDir { 520 | // Watch the directory if it has not been watched before, or if it was 521 | // watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles) 522 | w.mu.Lock() 523 | 524 | watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE && 525 | (!alreadyWatching || (w.dirFlags[name]&unix.NOTE_WRITE) != unix.NOTE_WRITE) 526 | // Store flags so this watch can be updated later 527 | w.dirFlags[name] = flags 528 | w.mu.Unlock() 529 | 530 | if watchDir { 531 | if err := w.watchDirectoryFiles(name); err != nil { 532 | return "", err 533 | } 534 | } 535 | } 536 | return name, nil 537 | } 538 | 539 | // readEvents reads from kqueue and converts the received kevents into 540 | // Event values that it sends down the Events channel. 541 | func (w *Watcher) readEvents() { 542 | defer func() { 543 | close(w.Events) 544 | close(w.Errors) 545 | _ = unix.Close(w.kq) 546 | unix.Close(w.closepipe[0]) 547 | }() 548 | 549 | eventBuffer := make([]unix.Kevent_t, 10) 550 | for closed := false; !closed; { 551 | kevents, err := w.read(eventBuffer) 552 | // EINTR is okay, the syscall was interrupted before timeout expired. 553 | if err != nil && err != unix.EINTR { 554 | if !w.sendError(fmt.Errorf("fsnotify.readEvents: %w", err)) { 555 | closed = true 556 | } 557 | continue 558 | } 559 | 560 | // Flush the events we received to the Events channel 561 | for _, kevent := range kevents { 562 | var ( 563 | watchfd = int(kevent.Ident) 564 | mask = uint32(kevent.Fflags) 565 | ) 566 | 567 | // Shut down the loop when the pipe is closed, but only after all 568 | // other events have been processed. 569 | if watchfd == w.closepipe[0] { 570 | closed = true 571 | continue 572 | } 573 | 574 | w.mu.Lock() 575 | path := w.paths[watchfd] 576 | w.mu.Unlock() 577 | 578 | event := w.newEvent(path.name, mask) 579 | 580 | if event.Has(Rename) || event.Has(Remove) { 581 | w.remove(event.Name, false) 582 | w.mu.Lock() 583 | delete(w.fileExists, event.Name) 584 | w.mu.Unlock() 585 | } 586 | 587 | if path.isDir && event.Has(Write) && !event.Has(Remove) { 588 | w.sendDirectoryChangeEvents(event.Name) 589 | } else { 590 | if !w.sendEvent(event) { 591 | closed = true 592 | continue 593 | } 594 | } 595 | 596 | if event.Has(Remove) { 597 | // Look for a file that may have overwritten this; for example, 598 | // mv f1 f2 will delete f2, then create f2. 599 | if path.isDir { 600 | fileDir := filepath.Clean(event.Name) 601 | w.mu.Lock() 602 | _, found := w.watches[fileDir] 603 | w.mu.Unlock() 604 | if found { 605 | err := w.sendDirectoryChangeEvents(fileDir) 606 | if err != nil { 607 | if !w.sendError(err) { 608 | closed = true 609 | } 610 | } 611 | } 612 | } else { 613 | filePath := filepath.Clean(event.Name) 614 | if fi, err := os.Lstat(filePath); err == nil { 615 | err := w.sendFileCreatedEventIfNew(filePath, fi) 616 | if err != nil { 617 | if !w.sendError(err) { 618 | closed = true 619 | } 620 | } 621 | } 622 | } 623 | } 624 | } 625 | } 626 | } 627 | 628 | // newEvent returns an platform-independent Event based on kqueue Fflags. 629 | func (w *Watcher) newEvent(name string, mask uint32) Event { 630 | e := Event{Name: name} 631 | if mask&unix.NOTE_DELETE == unix.NOTE_DELETE { 632 | e.Op |= Remove 633 | } 634 | if mask&unix.NOTE_WRITE == unix.NOTE_WRITE { 635 | e.Op |= Write 636 | } 637 | if mask&unix.NOTE_RENAME == unix.NOTE_RENAME { 638 | e.Op |= Rename 639 | } 640 | if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB { 641 | e.Op |= Chmod 642 | } 643 | // No point sending a write and delete event at the same time: if it's gone, 644 | // then it's gone. 645 | if e.Op.Has(Write) && e.Op.Has(Remove) { 646 | e.Op &^= Write 647 | } 648 | return e 649 | } 650 | 651 | // watchDirectoryFiles to mimic inotify when adding a watch on a directory 652 | func (w *Watcher) watchDirectoryFiles(dirPath string) error { 653 | // Get all files 654 | files, err := os.ReadDir(dirPath) 655 | if err != nil { 656 | return err 657 | } 658 | 659 | for _, f := range files { 660 | path := filepath.Join(dirPath, f.Name()) 661 | 662 | fi, err := f.Info() 663 | if err != nil { 664 | return fmt.Errorf("%q: %w", path, err) 665 | } 666 | 667 | cleanPath, err := w.internalWatch(path, fi) 668 | if err != nil { 669 | // No permission to read the file; that's not a problem: just skip. 670 | // But do add it to w.fileExists to prevent it from being picked up 671 | // as a "new" file later (it still shows up in the directory 672 | // listing). 673 | switch { 674 | case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM): 675 | cleanPath = filepath.Clean(path) 676 | default: 677 | return fmt.Errorf("%q: %w", path, err) 678 | } 679 | } 680 | 681 | w.mu.Lock() 682 | w.fileExists[cleanPath] = struct{}{} 683 | w.mu.Unlock() 684 | } 685 | 686 | return nil 687 | } 688 | 689 | // Search the directory for new files and send an event for them. 690 | // 691 | // This functionality is to have the BSD watcher match the inotify, which sends 692 | // a create event for files created in a watched directory. 693 | func (w *Watcher) sendDirectoryChangeEvents(dir string) error { 694 | files, err := os.ReadDir(dir) 695 | if err != nil { 696 | // Directory no longer exists: we can ignore this safely. kqueue will 697 | // still give us the correct events. 698 | if errors.Is(err, os.ErrNotExist) { 699 | return nil 700 | } 701 | return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err) 702 | } 703 | 704 | for _, f := range files { 705 | fi, err := f.Info() 706 | if err != nil { 707 | return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err) 708 | } 709 | 710 | err = w.sendFileCreatedEventIfNew(filepath.Join(dir, fi.Name()), fi) 711 | if err != nil { 712 | // Don't need to send an error if this file isn't readable. 713 | if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) { 714 | return nil 715 | } 716 | return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err) 717 | } 718 | } 719 | return nil 720 | } 721 | 722 | // sendFileCreatedEvent sends a create event if the file isn't already being tracked. 723 | func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fi os.FileInfo) (err error) { 724 | w.mu.Lock() 725 | _, doesExist := w.fileExists[filePath] 726 | w.mu.Unlock() 727 | if !doesExist { 728 | if !w.sendEvent(Event{Name: filePath, Op: Create}) { 729 | return 730 | } 731 | } 732 | 733 | // like watchDirectoryFiles (but without doing another ReadDir) 734 | filePath, err = w.internalWatch(filePath, fi) 735 | if err != nil { 736 | return err 737 | } 738 | 739 | w.mu.Lock() 740 | w.fileExists[filePath] = struct{}{} 741 | w.mu.Unlock() 742 | 743 | return nil 744 | } 745 | 746 | func (w *Watcher) internalWatch(name string, fi os.FileInfo) (string, error) { 747 | if fi.IsDir() { 748 | // mimic Linux providing delete events for subdirectories, but preserve 749 | // the flags used if currently watching subdirectory 750 | w.mu.Lock() 751 | flags := w.dirFlags[name] 752 | w.mu.Unlock() 753 | 754 | flags |= unix.NOTE_DELETE | unix.NOTE_RENAME 755 | return w.addWatch(name, flags) 756 | } 757 | 758 | // watch file to mimic Linux inotify 759 | return w.addWatch(name, noteAllEvents) 760 | } 761 | 762 | // Register events with the queue. 763 | func (w *Watcher) register(fds []int, flags int, fflags uint32) error { 764 | changes := make([]unix.Kevent_t, len(fds)) 765 | for i, fd := range fds { 766 | // SetKevent converts int to the platform-specific types. 767 | unix.SetKevent(&changes[i], fd, unix.EVFILT_VNODE, flags) 768 | changes[i].Fflags = fflags 769 | } 770 | 771 | // Register the events. 772 | success, err := unix.Kevent(w.kq, changes, nil, nil) 773 | if success == -1 { 774 | return err 775 | } 776 | return nil 777 | } 778 | 779 | // read retrieves pending events, or waits until an event occurs. 780 | func (w *Watcher) read(events []unix.Kevent_t) ([]unix.Kevent_t, error) { 781 | n, err := unix.Kevent(w.kq, nil, events, nil) 782 | if err != nil { 783 | return nil, err 784 | } 785 | return events[0:n], nil 786 | } 787 | -------------------------------------------------------------------------------- /backend_kqueue_test.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd || openbsd || netbsd || dragonfly || darwin 2 | // +build freebsd openbsd netbsd dragonfly darwin 3 | 4 | package fsnotify 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestRemoveState(t *testing.T) { 13 | var ( 14 | tmp = t.TempDir() 15 | dir = join(tmp, "dir") 16 | file = join(dir, "file") 17 | ) 18 | mkdir(t, dir) 19 | touch(t, file) 20 | 21 | w := newWatcher(t, tmp) 22 | addWatch(t, w, tmp) 23 | addWatch(t, w, file) 24 | 25 | check := func(wantUser, wantTotal int) { 26 | t.Helper() 27 | 28 | if len(w.watches) != wantTotal { 29 | var d []string 30 | for k, v := range w.watches { 31 | d = append(d, fmt.Sprintf("%#v = %#v", k, v)) 32 | } 33 | t.Errorf("unexpected number of entries in w.watches (have %d, want %d):\n%v", 34 | len(w.watches), wantTotal, strings.Join(d, "\n")) 35 | } 36 | if len(w.paths) != wantTotal { 37 | var d []string 38 | for k, v := range w.paths { 39 | d = append(d, fmt.Sprintf("%#v = %#v", k, v)) 40 | } 41 | t.Errorf("unexpected number of entries in w.paths (have %d, want %d):\n%v", 42 | len(w.paths), wantTotal, strings.Join(d, "\n")) 43 | } 44 | if len(w.userWatches) != wantUser { 45 | var d []string 46 | for k, v := range w.userWatches { 47 | d = append(d, fmt.Sprintf("%#v = %#v", k, v)) 48 | } 49 | t.Errorf("unexpected number of entries in w.userWatches (have %d, want %d):\n%v", 50 | len(w.userWatches), wantUser, strings.Join(d, "\n")) 51 | } 52 | } 53 | 54 | check(2, 3) 55 | 56 | if err := w.Remove(file); err != nil { 57 | t.Fatal(err) 58 | } 59 | check(1, 2) 60 | 61 | if err := w.Remove(tmp); err != nil { 62 | t.Fatal(err) 63 | } 64 | check(0, 0) 65 | 66 | // Don't check these after ever remove since they don't map easily to number 67 | // of files watches. Just make sure they're 0 after everything is removed. 68 | { 69 | want := 0 70 | if len(w.watchesByDir) != want { 71 | var d []string 72 | for k, v := range w.watchesByDir { 73 | d = append(d, fmt.Sprintf("%#v = %#v", k, v)) 74 | } 75 | t.Errorf("unexpected number of entries in w.watchesByDir (have %d, want %d):\n%v", 76 | len(w.watchesByDir), want, strings.Join(d, "\n")) 77 | } 78 | if len(w.dirFlags) != want { 79 | var d []string 80 | for k, v := range w.dirFlags { 81 | d = append(d, fmt.Sprintf("%#v = %#v", k, v)) 82 | } 83 | t.Errorf("unexpected number of entries in w.dirFlags (have %d, want %d):\n%v", 84 | len(w.dirFlags), want, strings.Join(d, "\n")) 85 | } 86 | 87 | if len(w.fileExists) != want { 88 | var d []string 89 | for k, v := range w.fileExists { 90 | d = append(d, fmt.Sprintf("%#v = %#v", k, v)) 91 | } 92 | t.Errorf("unexpected number of entries in w.fileExists (have %d, want %d):\n%v", 93 | len(w.fileExists), want, strings.Join(d, "\n")) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /backend_other.go: -------------------------------------------------------------------------------- 1 | //go:build appengine || (!darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows) 2 | // +build appengine !darwin,!dragonfly,!freebsd,!openbsd,!linux,!netbsd,!solaris,!windows 3 | 4 | // Note: the documentation on the Watcher type and methods is generated from 5 | // mkdoc.zsh 6 | 7 | package fsnotify 8 | 9 | import "errors" 10 | 11 | // Watcher watches a set of paths, delivering events on a channel. 12 | // 13 | // A watcher should not be copied (e.g. pass it by pointer, rather than by 14 | // value). 15 | // 16 | // # Linux notes 17 | // 18 | // When a file is removed a Remove event won't be emitted until all file 19 | // descriptors are closed, and deletes will always emit a Chmod. For example: 20 | // 21 | // fp := os.Open("file") 22 | // os.Remove("file") // Triggers Chmod 23 | // fp.Close() // Triggers Remove 24 | // 25 | // This is the event that inotify sends, so not much can be changed about this. 26 | // 27 | // The fs.inotify.max_user_watches sysctl variable specifies the upper limit 28 | // for the number of watches per user, and fs.inotify.max_user_instances 29 | // specifies the maximum number of inotify instances per user. Every Watcher you 30 | // create is an "instance", and every path you add is a "watch". 31 | // 32 | // These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and 33 | // /proc/sys/fs/inotify/max_user_instances 34 | // 35 | // To increase them you can use sysctl or write the value to the /proc file: 36 | // 37 | // # Default values on Linux 5.18 38 | // sysctl fs.inotify.max_user_watches=124983 39 | // sysctl fs.inotify.max_user_instances=128 40 | // 41 | // To make the changes persist on reboot edit /etc/sysctl.conf or 42 | // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check 43 | // your distro's documentation): 44 | // 45 | // fs.inotify.max_user_watches=124983 46 | // fs.inotify.max_user_instances=128 47 | // 48 | // Reaching the limit will result in a "no space left on device" or "too many open 49 | // files" error. 50 | // 51 | // # kqueue notes (macOS, BSD) 52 | // 53 | // kqueue requires opening a file descriptor for every file that's being watched; 54 | // so if you're watching a directory with five files then that's six file 55 | // descriptors. You will run in to your system's "max open files" limit faster on 56 | // these platforms. 57 | // 58 | // The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to 59 | // control the maximum number of open files, as well as /etc/login.conf on BSD 60 | // systems. 61 | // 62 | // # Windows notes 63 | // 64 | // Paths can be added as "C:\path\to\dir", but forward slashes 65 | // ("C:/path/to/dir") will also work. 66 | // 67 | // When a watched directory is removed it will always send an event for the 68 | // directory itself, but may not send events for all files in that directory. 69 | // Sometimes it will send events for all times, sometimes it will send no 70 | // events, and often only for some files. 71 | // 72 | // The default ReadDirectoryChangesW() buffer size is 64K, which is the largest 73 | // value that is guaranteed to work with SMB filesystems. If you have many 74 | // events in quick succession this may not be enough, and you will have to use 75 | // [WithBufferSize] to increase the value. 76 | type Watcher struct { 77 | // Events sends the filesystem change events. 78 | // 79 | // fsnotify can send the following events; a "path" here can refer to a 80 | // file, directory, symbolic link, or special file like a FIFO. 81 | // 82 | // fsnotify.Create A new path was created; this may be followed by one 83 | // or more Write events if data also gets written to a 84 | // file. 85 | // 86 | // fsnotify.Remove A path was removed. 87 | // 88 | // fsnotify.Rename A path was renamed. A rename is always sent with the 89 | // old path as Event.Name, and a Create event will be 90 | // sent with the new name. Renames are only sent for 91 | // paths that are currently watched; e.g. moving an 92 | // unmonitored file into a monitored directory will 93 | // show up as just a Create. Similarly, renaming a file 94 | // to outside a monitored directory will show up as 95 | // only a Rename. 96 | // 97 | // fsnotify.Write A file or named pipe was written to. A Truncate will 98 | // also trigger a Write. A single "write action" 99 | // initiated by the user may show up as one or multiple 100 | // writes, depending on when the system syncs things to 101 | // disk. For example when compiling a large Go program 102 | // you may get hundreds of Write events, and you may 103 | // want to wait until you've stopped receiving them 104 | // (see the dedup example in cmd/fsnotify). 105 | // 106 | // Some systems may send Write event for directories 107 | // when the directory content changes. 108 | // 109 | // fsnotify.Chmod Attributes were changed. On Linux this is also sent 110 | // when a file is removed (or more accurately, when a 111 | // link to an inode is removed). On kqueue it's sent 112 | // when a file is truncated. On Windows it's never 113 | // sent. 114 | Events chan Event 115 | 116 | // Errors sends any errors. 117 | Errors chan error 118 | } 119 | 120 | // NewWatcher creates a new Watcher. 121 | func NewWatcher() (*Watcher, error) { 122 | return nil, errors.New("fsnotify not supported on the current platform") 123 | } 124 | 125 | // NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events 126 | // channel. 127 | // 128 | // The main use case for this is situations with a very large number of events 129 | // where the kernel buffer size can't be increased (e.g. due to lack of 130 | // permissions). An unbuffered Watcher will perform better for almost all use 131 | // cases, and whenever possible you will be better off increasing the kernel 132 | // buffers instead of adding a large userspace buffer. 133 | func NewBufferedWatcher(sz uint) (*Watcher, error) { return NewWatcher() } 134 | 135 | // Close removes all watches and closes the Events channel. 136 | func (w *Watcher) Close() error { return nil } 137 | 138 | // WatchList returns all paths explicitly added with [Watcher.Add] (and are not 139 | // yet removed). 140 | // 141 | // Returns nil if [Watcher.Close] was called. 142 | func (w *Watcher) WatchList() []string { return nil } 143 | 144 | // Add starts monitoring the path for changes. 145 | // 146 | // A path can only be watched once; watching it more than once is a no-op and will 147 | // not return an error. Paths that do not yet exist on the filesystem cannot be 148 | // watched. 149 | // 150 | // A watch will be automatically removed if the watched path is deleted or 151 | // renamed. The exception is the Windows backend, which doesn't remove the 152 | // watcher on renames. 153 | // 154 | // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special 155 | // filesystems (/proc, /sys, etc.) generally don't work. 156 | // 157 | // Returns [ErrClosed] if [Watcher.Close] was called. 158 | // 159 | // See [Watcher.AddWith] for a version that allows adding options. 160 | // 161 | // # Watching directories 162 | // 163 | // All files in a directory are monitored, including new files that are created 164 | // after the watcher is started. Subdirectories are not watched (i.e. it's 165 | // non-recursive). 166 | // 167 | // # Watching files 168 | // 169 | // Watching individual files (rather than directories) is generally not 170 | // recommended as many programs (especially editors) update files atomically: it 171 | // will write to a temporary file which is then moved to to destination, 172 | // overwriting the original (or some variant thereof). The watcher on the 173 | // original file is now lost, as that no longer exists. 174 | // 175 | // The upshot of this is that a power failure or crash won't leave a 176 | // half-written file. 177 | // 178 | // Watch the parent directory and use Event.Name to filter out files you're not 179 | // interested in. There is an example of this in cmd/fsnotify/file.go. 180 | func (w *Watcher) Add(name string) error { return nil } 181 | 182 | // AddWith is like [Watcher.Add], but allows adding options. When using Add() 183 | // the defaults described below are used. 184 | // 185 | // Possible options are: 186 | // 187 | // - [WithBufferSize] sets the buffer size for the Windows backend; no-op on 188 | // other platforms. The default is 64K (65536 bytes). 189 | func (w *Watcher) AddWith(name string, opts ...addOpt) error { return nil } 190 | 191 | // Remove stops monitoring the path for changes. 192 | // 193 | // Directories are always removed non-recursively. For example, if you added 194 | // /tmp/dir and /tmp/dir/subdir then you will need to remove both. 195 | // 196 | // Removing a path that has not yet been added returns [ErrNonExistentWatch]. 197 | // 198 | // Returns nil if [Watcher.Close] was called. 199 | func (w *Watcher) Remove(name string) error { return nil } 200 | -------------------------------------------------------------------------------- /backend_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | // Windows backend based on ReadDirectoryChangesW() 5 | // 6 | // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw 7 | // 8 | // Note: the documentation on the Watcher type and methods is generated from 9 | // mkdoc.zsh 10 | 11 | package fsnotify 12 | 13 | import ( 14 | "errors" 15 | "fmt" 16 | "os" 17 | "path/filepath" 18 | "reflect" 19 | "runtime" 20 | "strings" 21 | "sync" 22 | "unsafe" 23 | 24 | "golang.org/x/sys/windows" 25 | ) 26 | 27 | // Watcher watches a set of paths, delivering events on a channel. 28 | // 29 | // A watcher should not be copied (e.g. pass it by pointer, rather than by 30 | // value). 31 | // 32 | // # Linux notes 33 | // 34 | // When a file is removed a Remove event won't be emitted until all file 35 | // descriptors are closed, and deletes will always emit a Chmod. For example: 36 | // 37 | // fp := os.Open("file") 38 | // os.Remove("file") // Triggers Chmod 39 | // fp.Close() // Triggers Remove 40 | // 41 | // This is the event that inotify sends, so not much can be changed about this. 42 | // 43 | // The fs.inotify.max_user_watches sysctl variable specifies the upper limit 44 | // for the number of watches per user, and fs.inotify.max_user_instances 45 | // specifies the maximum number of inotify instances per user. Every Watcher you 46 | // create is an "instance", and every path you add is a "watch". 47 | // 48 | // These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and 49 | // /proc/sys/fs/inotify/max_user_instances 50 | // 51 | // To increase them you can use sysctl or write the value to the /proc file: 52 | // 53 | // # Default values on Linux 5.18 54 | // sysctl fs.inotify.max_user_watches=124983 55 | // sysctl fs.inotify.max_user_instances=128 56 | // 57 | // To make the changes persist on reboot edit /etc/sysctl.conf or 58 | // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check 59 | // your distro's documentation): 60 | // 61 | // fs.inotify.max_user_watches=124983 62 | // fs.inotify.max_user_instances=128 63 | // 64 | // Reaching the limit will result in a "no space left on device" or "too many open 65 | // files" error. 66 | // 67 | // # kqueue notes (macOS, BSD) 68 | // 69 | // kqueue requires opening a file descriptor for every file that's being watched; 70 | // so if you're watching a directory with five files then that's six file 71 | // descriptors. You will run in to your system's "max open files" limit faster on 72 | // these platforms. 73 | // 74 | // The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to 75 | // control the maximum number of open files, as well as /etc/login.conf on BSD 76 | // systems. 77 | // 78 | // # Windows notes 79 | // 80 | // Paths can be added as "C:\path\to\dir", but forward slashes 81 | // ("C:/path/to/dir") will also work. 82 | // 83 | // When a watched directory is removed it will always send an event for the 84 | // directory itself, but may not send events for all files in that directory. 85 | // Sometimes it will send events for all times, sometimes it will send no 86 | // events, and often only for some files. 87 | // 88 | // The default ReadDirectoryChangesW() buffer size is 64K, which is the largest 89 | // value that is guaranteed to work with SMB filesystems. If you have many 90 | // events in quick succession this may not be enough, and you will have to use 91 | // [WithBufferSize] to increase the value. 92 | type Watcher struct { 93 | // Events sends the filesystem change events. 94 | // 95 | // fsnotify can send the following events; a "path" here can refer to a 96 | // file, directory, symbolic link, or special file like a FIFO. 97 | // 98 | // fsnotify.Create A new path was created; this may be followed by one 99 | // or more Write events if data also gets written to a 100 | // file. 101 | // 102 | // fsnotify.Remove A path was removed. 103 | // 104 | // fsnotify.Rename A path was renamed. A rename is always sent with the 105 | // old path as Event.Name, and a Create event will be 106 | // sent with the new name. Renames are only sent for 107 | // paths that are currently watched; e.g. moving an 108 | // unmonitored file into a monitored directory will 109 | // show up as just a Create. Similarly, renaming a file 110 | // to outside a monitored directory will show up as 111 | // only a Rename. 112 | // 113 | // fsnotify.Write A file or named pipe was written to. A Truncate will 114 | // also trigger a Write. A single "write action" 115 | // initiated by the user may show up as one or multiple 116 | // writes, depending on when the system syncs things to 117 | // disk. For example when compiling a large Go program 118 | // you may get hundreds of Write events, and you may 119 | // want to wait until you've stopped receiving them 120 | // (see the dedup example in cmd/fsnotify). 121 | // 122 | // Some systems may send Write event for directories 123 | // when the directory content changes. 124 | // 125 | // fsnotify.Chmod Attributes were changed. On Linux this is also sent 126 | // when a file is removed (or more accurately, when a 127 | // link to an inode is removed). On kqueue it's sent 128 | // when a file is truncated. On Windows it's never 129 | // sent. 130 | Events chan Event 131 | 132 | // Errors sends any errors. 133 | Errors chan error 134 | 135 | port windows.Handle // Handle to completion port 136 | input chan *input // Inputs to the reader are sent on this channel 137 | quit chan chan<- error 138 | 139 | mu sync.Mutex // Protects access to watches, closed 140 | watches watchMap // Map of watches (key: i-number) 141 | closed bool // Set to true when Close() is first called 142 | } 143 | 144 | // NewWatcher creates a new Watcher. 145 | func NewWatcher() (*Watcher, error) { 146 | return NewBufferedWatcher(50) 147 | } 148 | 149 | // NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events 150 | // channel. 151 | // 152 | // The main use case for this is situations with a very large number of events 153 | // where the kernel buffer size can't be increased (e.g. due to lack of 154 | // permissions). An unbuffered Watcher will perform better for almost all use 155 | // cases, and whenever possible you will be better off increasing the kernel 156 | // buffers instead of adding a large userspace buffer. 157 | func NewBufferedWatcher(sz uint) (*Watcher, error) { 158 | port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0) 159 | if err != nil { 160 | return nil, os.NewSyscallError("CreateIoCompletionPort", err) 161 | } 162 | w := &Watcher{ 163 | port: port, 164 | watches: make(watchMap), 165 | input: make(chan *input, 1), 166 | Events: make(chan Event, sz), 167 | Errors: make(chan error), 168 | quit: make(chan chan<- error, 1), 169 | } 170 | go w.readEvents() 171 | return w, nil 172 | } 173 | 174 | func (w *Watcher) isClosed() bool { 175 | w.mu.Lock() 176 | defer w.mu.Unlock() 177 | return w.closed 178 | } 179 | 180 | func (w *Watcher) sendEvent(name string, mask uint64) bool { 181 | if mask == 0 { 182 | return false 183 | } 184 | 185 | event := w.newEvent(name, uint32(mask)) 186 | select { 187 | case ch := <-w.quit: 188 | w.quit <- ch 189 | case w.Events <- event: 190 | } 191 | return true 192 | } 193 | 194 | // Returns true if the error was sent, or false if watcher is closed. 195 | func (w *Watcher) sendError(err error) bool { 196 | select { 197 | case w.Errors <- err: 198 | return true 199 | case <-w.quit: 200 | } 201 | return false 202 | } 203 | 204 | // Close removes all watches and closes the Events channel. 205 | func (w *Watcher) Close() error { 206 | if w.isClosed() { 207 | return nil 208 | } 209 | 210 | w.mu.Lock() 211 | w.closed = true 212 | w.mu.Unlock() 213 | 214 | // Send "quit" message to the reader goroutine 215 | ch := make(chan error) 216 | w.quit <- ch 217 | if err := w.wakeupReader(); err != nil { 218 | return err 219 | } 220 | return <-ch 221 | } 222 | 223 | // Add starts monitoring the path for changes. 224 | // 225 | // A path can only be watched once; watching it more than once is a no-op and will 226 | // not return an error. Paths that do not yet exist on the filesystem cannot be 227 | // watched. 228 | // 229 | // A watch will be automatically removed if the watched path is deleted or 230 | // renamed. The exception is the Windows backend, which doesn't remove the 231 | // watcher on renames. 232 | // 233 | // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special 234 | // filesystems (/proc, /sys, etc.) generally don't work. 235 | // 236 | // Returns [ErrClosed] if [Watcher.Close] was called. 237 | // 238 | // See [Watcher.AddWith] for a version that allows adding options. 239 | // 240 | // # Watching directories 241 | // 242 | // All files in a directory are monitored, including new files that are created 243 | // after the watcher is started. Subdirectories are not watched (i.e. it's 244 | // non-recursive). 245 | // 246 | // # Watching files 247 | // 248 | // Watching individual files (rather than directories) is generally not 249 | // recommended as many programs (especially editors) update files atomically: it 250 | // will write to a temporary file which is then moved to to destination, 251 | // overwriting the original (or some variant thereof). The watcher on the 252 | // original file is now lost, as that no longer exists. 253 | // 254 | // The upshot of this is that a power failure or crash won't leave a 255 | // half-written file. 256 | // 257 | // Watch the parent directory and use Event.Name to filter out files you're not 258 | // interested in. There is an example of this in cmd/fsnotify/file.go. 259 | func (w *Watcher) Add(name string) error { return w.AddWith(name) } 260 | 261 | // AddWith is like [Watcher.Add], but allows adding options. When using Add() 262 | // the defaults described below are used. 263 | // 264 | // Possible options are: 265 | // 266 | // - [WithBufferSize] sets the buffer size for the Windows backend; no-op on 267 | // other platforms. The default is 64K (65536 bytes). 268 | func (w *Watcher) AddWith(name string, opts ...addOpt) error { 269 | if w.isClosed() { 270 | return ErrClosed 271 | } 272 | 273 | with := getOptions(opts...) 274 | if with.bufsize < 4096 { 275 | return fmt.Errorf("fsnotify.WithBufferSize: buffer size cannot be smaller than 4096 bytes") 276 | } 277 | 278 | in := &input{ 279 | op: opAddWatch, 280 | path: filepath.Clean(name), 281 | flags: sysFSALLEVENTS, 282 | reply: make(chan error), 283 | bufsize: with.bufsize, 284 | } 285 | w.input <- in 286 | if err := w.wakeupReader(); err != nil { 287 | return err 288 | } 289 | return <-in.reply 290 | } 291 | 292 | // Remove stops monitoring the path for changes. 293 | // 294 | // Directories are always removed non-recursively. For example, if you added 295 | // /tmp/dir and /tmp/dir/subdir then you will need to remove both. 296 | // 297 | // Removing a path that has not yet been added returns [ErrNonExistentWatch]. 298 | // 299 | // Returns nil if [Watcher.Close] was called. 300 | func (w *Watcher) Remove(name string) error { 301 | if w.isClosed() { 302 | return nil 303 | } 304 | 305 | in := &input{ 306 | op: opRemoveWatch, 307 | path: filepath.Clean(name), 308 | reply: make(chan error), 309 | } 310 | w.input <- in 311 | if err := w.wakeupReader(); err != nil { 312 | return err 313 | } 314 | return <-in.reply 315 | } 316 | 317 | // WatchList returns all paths explicitly added with [Watcher.Add] (and are not 318 | // yet removed). 319 | // 320 | // Returns nil if [Watcher.Close] was called. 321 | func (w *Watcher) WatchList() []string { 322 | if w.isClosed() { 323 | return nil 324 | } 325 | 326 | w.mu.Lock() 327 | defer w.mu.Unlock() 328 | 329 | entries := make([]string, 0, len(w.watches)) 330 | for _, entry := range w.watches { 331 | for _, watchEntry := range entry { 332 | for name := range watchEntry.names { 333 | entries = append(entries, filepath.Join(watchEntry.path, name)) 334 | } 335 | // the directory itself is being watched 336 | if watchEntry.mask != 0 { 337 | entries = append(entries, watchEntry.path) 338 | } 339 | } 340 | } 341 | 342 | return entries 343 | } 344 | 345 | // These options are from the old golang.org/x/exp/winfsnotify, where you could 346 | // add various options to the watch. This has long since been removed. 347 | // 348 | // The "sys" in the name is misleading as they're not part of any "system". 349 | // 350 | // This should all be removed at some point, and just use windows.FILE_NOTIFY_* 351 | const ( 352 | sysFSALLEVENTS = 0xfff 353 | sysFSCREATE = 0x100 354 | sysFSDELETE = 0x200 355 | sysFSDELETESELF = 0x400 356 | sysFSMODIFY = 0x2 357 | sysFSMOVE = 0xc0 358 | sysFSMOVEDFROM = 0x40 359 | sysFSMOVEDTO = 0x80 360 | sysFSMOVESELF = 0x800 361 | sysFSIGNORED = 0x8000 362 | ) 363 | 364 | func (w *Watcher) newEvent(name string, mask uint32) Event { 365 | e := Event{Name: name} 366 | if mask&sysFSCREATE == sysFSCREATE || mask&sysFSMOVEDTO == sysFSMOVEDTO { 367 | e.Op |= Create 368 | } 369 | if mask&sysFSDELETE == sysFSDELETE || mask&sysFSDELETESELF == sysFSDELETESELF { 370 | e.Op |= Remove 371 | } 372 | if mask&sysFSMODIFY == sysFSMODIFY { 373 | e.Op |= Write 374 | } 375 | if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM { 376 | e.Op |= Rename 377 | } 378 | return e 379 | } 380 | 381 | const ( 382 | opAddWatch = iota 383 | opRemoveWatch 384 | ) 385 | 386 | const ( 387 | provisional uint64 = 1 << (32 + iota) 388 | ) 389 | 390 | type input struct { 391 | op int 392 | path string 393 | flags uint32 394 | bufsize int 395 | reply chan error 396 | } 397 | 398 | type inode struct { 399 | handle windows.Handle 400 | volume uint32 401 | index uint64 402 | } 403 | 404 | type watch struct { 405 | ov windows.Overlapped 406 | ino *inode // i-number 407 | recurse bool // Recursive watch? 408 | path string // Directory path 409 | mask uint64 // Directory itself is being watched with these notify flags 410 | names map[string]uint64 // Map of names being watched and their notify flags 411 | rename string // Remembers the old name while renaming a file 412 | buf []byte // buffer, allocated later 413 | } 414 | 415 | type ( 416 | indexMap map[uint64]*watch 417 | watchMap map[uint32]indexMap 418 | ) 419 | 420 | func (w *Watcher) wakeupReader() error { 421 | err := windows.PostQueuedCompletionStatus(w.port, 0, 0, nil) 422 | if err != nil { 423 | return os.NewSyscallError("PostQueuedCompletionStatus", err) 424 | } 425 | return nil 426 | } 427 | 428 | func (w *Watcher) getDir(pathname string) (dir string, err error) { 429 | attr, err := windows.GetFileAttributes(windows.StringToUTF16Ptr(pathname)) 430 | if err != nil { 431 | return "", os.NewSyscallError("GetFileAttributes", err) 432 | } 433 | if attr&windows.FILE_ATTRIBUTE_DIRECTORY != 0 { 434 | dir = pathname 435 | } else { 436 | dir, _ = filepath.Split(pathname) 437 | dir = filepath.Clean(dir) 438 | } 439 | return 440 | } 441 | 442 | func (w *Watcher) getIno(path string) (ino *inode, err error) { 443 | h, err := windows.CreateFile(windows.StringToUTF16Ptr(path), 444 | windows.FILE_LIST_DIRECTORY, 445 | windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, 446 | nil, windows.OPEN_EXISTING, 447 | windows.FILE_FLAG_BACKUP_SEMANTICS|windows.FILE_FLAG_OVERLAPPED, 0) 448 | if err != nil { 449 | return nil, os.NewSyscallError("CreateFile", err) 450 | } 451 | 452 | var fi windows.ByHandleFileInformation 453 | err = windows.GetFileInformationByHandle(h, &fi) 454 | if err != nil { 455 | windows.CloseHandle(h) 456 | return nil, os.NewSyscallError("GetFileInformationByHandle", err) 457 | } 458 | ino = &inode{ 459 | handle: h, 460 | volume: fi.VolumeSerialNumber, 461 | index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow), 462 | } 463 | return ino, nil 464 | } 465 | 466 | // Must run within the I/O thread. 467 | func (m watchMap) get(ino *inode) *watch { 468 | if i := m[ino.volume]; i != nil { 469 | return i[ino.index] 470 | } 471 | return nil 472 | } 473 | 474 | // Must run within the I/O thread. 475 | func (m watchMap) set(ino *inode, watch *watch) { 476 | i := m[ino.volume] 477 | if i == nil { 478 | i = make(indexMap) 479 | m[ino.volume] = i 480 | } 481 | i[ino.index] = watch 482 | } 483 | 484 | // Must run within the I/O thread. 485 | func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error { 486 | //pathname, recurse := recursivePath(pathname) 487 | recurse := false 488 | 489 | dir, err := w.getDir(pathname) 490 | if err != nil { 491 | return err 492 | } 493 | 494 | ino, err := w.getIno(dir) 495 | if err != nil { 496 | return err 497 | } 498 | w.mu.Lock() 499 | watchEntry := w.watches.get(ino) 500 | w.mu.Unlock() 501 | if watchEntry == nil { 502 | _, err := windows.CreateIoCompletionPort(ino.handle, w.port, 0, 0) 503 | if err != nil { 504 | windows.CloseHandle(ino.handle) 505 | return os.NewSyscallError("CreateIoCompletionPort", err) 506 | } 507 | watchEntry = &watch{ 508 | ino: ino, 509 | path: dir, 510 | names: make(map[string]uint64), 511 | recurse: recurse, 512 | buf: make([]byte, bufsize), 513 | } 514 | w.mu.Lock() 515 | w.watches.set(ino, watchEntry) 516 | w.mu.Unlock() 517 | flags |= provisional 518 | } else { 519 | windows.CloseHandle(ino.handle) 520 | } 521 | if pathname == dir { 522 | watchEntry.mask |= flags 523 | } else { 524 | watchEntry.names[filepath.Base(pathname)] |= flags 525 | } 526 | 527 | err = w.startRead(watchEntry) 528 | if err != nil { 529 | return err 530 | } 531 | 532 | if pathname == dir { 533 | watchEntry.mask &= ^provisional 534 | } else { 535 | watchEntry.names[filepath.Base(pathname)] &= ^provisional 536 | } 537 | return nil 538 | } 539 | 540 | // Must run within the I/O thread. 541 | func (w *Watcher) remWatch(pathname string) error { 542 | pathname, recurse := recursivePath(pathname) 543 | 544 | dir, err := w.getDir(pathname) 545 | if err != nil { 546 | return err 547 | } 548 | ino, err := w.getIno(dir) 549 | if err != nil { 550 | return err 551 | } 552 | 553 | w.mu.Lock() 554 | watch := w.watches.get(ino) 555 | w.mu.Unlock() 556 | 557 | if recurse && !watch.recurse { 558 | return fmt.Errorf("can't use \\... with non-recursive watch %q", pathname) 559 | } 560 | 561 | err = windows.CloseHandle(ino.handle) 562 | if err != nil { 563 | w.sendError(os.NewSyscallError("CloseHandle", err)) 564 | } 565 | if watch == nil { 566 | return fmt.Errorf("%w: %s", ErrNonExistentWatch, pathname) 567 | } 568 | if pathname == dir { 569 | w.sendEvent(watch.path, watch.mask&sysFSIGNORED) 570 | watch.mask = 0 571 | } else { 572 | name := filepath.Base(pathname) 573 | w.sendEvent(filepath.Join(watch.path, name), watch.names[name]&sysFSIGNORED) 574 | delete(watch.names, name) 575 | } 576 | 577 | return w.startRead(watch) 578 | } 579 | 580 | // Must run within the I/O thread. 581 | func (w *Watcher) deleteWatch(watch *watch) { 582 | for name, mask := range watch.names { 583 | if mask&provisional == 0 { 584 | w.sendEvent(filepath.Join(watch.path, name), mask&sysFSIGNORED) 585 | } 586 | delete(watch.names, name) 587 | } 588 | if watch.mask != 0 { 589 | if watch.mask&provisional == 0 { 590 | w.sendEvent(watch.path, watch.mask&sysFSIGNORED) 591 | } 592 | watch.mask = 0 593 | } 594 | } 595 | 596 | // Must run within the I/O thread. 597 | func (w *Watcher) startRead(watch *watch) error { 598 | err := windows.CancelIo(watch.ino.handle) 599 | if err != nil { 600 | w.sendError(os.NewSyscallError("CancelIo", err)) 601 | w.deleteWatch(watch) 602 | } 603 | mask := w.toWindowsFlags(watch.mask) 604 | for _, m := range watch.names { 605 | mask |= w.toWindowsFlags(m) 606 | } 607 | if mask == 0 { 608 | err := windows.CloseHandle(watch.ino.handle) 609 | if err != nil { 610 | w.sendError(os.NewSyscallError("CloseHandle", err)) 611 | } 612 | w.mu.Lock() 613 | delete(w.watches[watch.ino.volume], watch.ino.index) 614 | w.mu.Unlock() 615 | return nil 616 | } 617 | 618 | // We need to pass the array, rather than the slice. 619 | hdr := (*reflect.SliceHeader)(unsafe.Pointer(&watch.buf)) 620 | rdErr := windows.ReadDirectoryChanges(watch.ino.handle, 621 | (*byte)(unsafe.Pointer(hdr.Data)), uint32(hdr.Len), 622 | watch.recurse, mask, nil, &watch.ov, 0) 623 | if rdErr != nil { 624 | err := os.NewSyscallError("ReadDirectoryChanges", rdErr) 625 | if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 { 626 | // Watched directory was probably removed 627 | w.sendEvent(watch.path, watch.mask&sysFSDELETESELF) 628 | err = nil 629 | } 630 | w.deleteWatch(watch) 631 | w.startRead(watch) 632 | return err 633 | } 634 | return nil 635 | } 636 | 637 | // readEvents reads from the I/O completion port, converts the 638 | // received events into Event objects and sends them via the Events channel. 639 | // Entry point to the I/O thread. 640 | func (w *Watcher) readEvents() { 641 | var ( 642 | n uint32 643 | key uintptr 644 | ov *windows.Overlapped 645 | ) 646 | runtime.LockOSThread() 647 | 648 | for { 649 | // This error is handled after the watch == nil check below. 650 | qErr := windows.GetQueuedCompletionStatus(w.port, &n, &key, &ov, windows.INFINITE) 651 | 652 | watch := (*watch)(unsafe.Pointer(ov)) 653 | if watch == nil { 654 | select { 655 | case ch := <-w.quit: 656 | w.mu.Lock() 657 | var indexes []indexMap 658 | for _, index := range w.watches { 659 | indexes = append(indexes, index) 660 | } 661 | w.mu.Unlock() 662 | for _, index := range indexes { 663 | for _, watch := range index { 664 | w.deleteWatch(watch) 665 | w.startRead(watch) 666 | } 667 | } 668 | 669 | err := windows.CloseHandle(w.port) 670 | if err != nil { 671 | err = os.NewSyscallError("CloseHandle", err) 672 | } 673 | close(w.Events) 674 | close(w.Errors) 675 | ch <- err 676 | return 677 | case in := <-w.input: 678 | switch in.op { 679 | case opAddWatch: 680 | in.reply <- w.addWatch(in.path, uint64(in.flags), in.bufsize) 681 | case opRemoveWatch: 682 | in.reply <- w.remWatch(in.path) 683 | } 684 | default: 685 | } 686 | continue 687 | } 688 | 689 | switch qErr { 690 | case nil: 691 | // No error 692 | case windows.ERROR_MORE_DATA: 693 | if watch == nil { 694 | w.sendError(errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer")) 695 | } else { 696 | // The i/o succeeded but the buffer is full. 697 | // In theory we should be building up a full packet. 698 | // In practice we can get away with just carrying on. 699 | n = uint32(unsafe.Sizeof(watch.buf)) 700 | } 701 | case windows.ERROR_ACCESS_DENIED: 702 | // Watched directory was probably removed 703 | w.sendEvent(watch.path, watch.mask&sysFSDELETESELF) 704 | w.deleteWatch(watch) 705 | w.startRead(watch) 706 | continue 707 | case windows.ERROR_OPERATION_ABORTED: 708 | // CancelIo was called on this handle 709 | continue 710 | default: 711 | w.sendError(os.NewSyscallError("GetQueuedCompletionPort", qErr)) 712 | continue 713 | } 714 | 715 | var offset uint32 716 | for { 717 | if n == 0 { 718 | w.sendError(ErrEventOverflow) 719 | break 720 | } 721 | 722 | // Point "raw" to the event in the buffer 723 | raw := (*windows.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset])) 724 | 725 | // Create a buf that is the size of the path name 726 | size := int(raw.FileNameLength / 2) 727 | var buf []uint16 728 | // TODO: Use unsafe.Slice in Go 1.17; https://stackoverflow.com/questions/51187973 729 | sh := (*reflect.SliceHeader)(unsafe.Pointer(&buf)) 730 | sh.Data = uintptr(unsafe.Pointer(&raw.FileName)) 731 | sh.Len = size 732 | sh.Cap = size 733 | name := windows.UTF16ToString(buf) 734 | fullname := filepath.Join(watch.path, name) 735 | 736 | var mask uint64 737 | switch raw.Action { 738 | case windows.FILE_ACTION_REMOVED: 739 | mask = sysFSDELETESELF 740 | case windows.FILE_ACTION_MODIFIED: 741 | mask = sysFSMODIFY 742 | case windows.FILE_ACTION_RENAMED_OLD_NAME: 743 | watch.rename = name 744 | case windows.FILE_ACTION_RENAMED_NEW_NAME: 745 | // Update saved path of all sub-watches. 746 | old := filepath.Join(watch.path, watch.rename) 747 | w.mu.Lock() 748 | for _, watchMap := range w.watches { 749 | for _, ww := range watchMap { 750 | if strings.HasPrefix(ww.path, old) { 751 | ww.path = filepath.Join(fullname, strings.TrimPrefix(ww.path, old)) 752 | } 753 | } 754 | } 755 | w.mu.Unlock() 756 | 757 | if watch.names[watch.rename] != 0 { 758 | watch.names[name] |= watch.names[watch.rename] 759 | delete(watch.names, watch.rename) 760 | mask = sysFSMOVESELF 761 | } 762 | } 763 | 764 | sendNameEvent := func() { 765 | w.sendEvent(fullname, watch.names[name]&mask) 766 | } 767 | if raw.Action != windows.FILE_ACTION_RENAMED_NEW_NAME { 768 | sendNameEvent() 769 | } 770 | if raw.Action == windows.FILE_ACTION_REMOVED { 771 | w.sendEvent(fullname, watch.names[name]&sysFSIGNORED) 772 | delete(watch.names, name) 773 | } 774 | 775 | w.sendEvent(fullname, watch.mask&w.toFSnotifyFlags(raw.Action)) 776 | if raw.Action == windows.FILE_ACTION_RENAMED_NEW_NAME { 777 | fullname = filepath.Join(watch.path, watch.rename) 778 | sendNameEvent() 779 | } 780 | 781 | // Move to the next event in the buffer 782 | if raw.NextEntryOffset == 0 { 783 | break 784 | } 785 | offset += raw.NextEntryOffset 786 | 787 | // Error! 788 | if offset >= n { 789 | //lint:ignore ST1005 Windows should be capitalized 790 | w.sendError(errors.New( 791 | "Windows system assumed buffer larger than it is, events have likely been missed")) 792 | break 793 | } 794 | } 795 | 796 | if err := w.startRead(watch); err != nil { 797 | w.sendError(err) 798 | } 799 | } 800 | } 801 | 802 | func (w *Watcher) toWindowsFlags(mask uint64) uint32 { 803 | var m uint32 804 | if mask&sysFSMODIFY != 0 { 805 | m |= windows.FILE_NOTIFY_CHANGE_LAST_WRITE 806 | } 807 | if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 { 808 | m |= windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME 809 | } 810 | return m 811 | } 812 | 813 | func (w *Watcher) toFSnotifyFlags(action uint32) uint64 { 814 | switch action { 815 | case windows.FILE_ACTION_ADDED: 816 | return sysFSCREATE 817 | case windows.FILE_ACTION_REMOVED: 818 | return sysFSDELETE 819 | case windows.FILE_ACTION_MODIFIED: 820 | return sysFSMODIFY 821 | case windows.FILE_ACTION_RENAMED_OLD_NAME: 822 | return sysFSMOVEDFROM 823 | case windows.FILE_ACTION_RENAMED_NEW_NAME: 824 | return sysFSMOVEDTO 825 | } 826 | return 0 827 | } 828 | -------------------------------------------------------------------------------- /backend_windows_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package fsnotify 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestRemoveState(t *testing.T) { 13 | // TODO: the Windows backend is too confusing; needs some serious attention. 14 | return 15 | 16 | var ( 17 | tmp = t.TempDir() 18 | dir = join(tmp, "dir") 19 | file = join(dir, "file") 20 | ) 21 | mkdir(t, dir) 22 | touch(t, file) 23 | 24 | w := newWatcher(t, tmp) 25 | addWatch(t, w, tmp) 26 | addWatch(t, w, file) 27 | 28 | check := func(want int) { 29 | t.Helper() 30 | if len(w.watches) != want { 31 | var d []string 32 | for k, v := range w.watches { 33 | d = append(d, fmt.Sprintf("%#v = %#v", k, v)) 34 | } 35 | t.Errorf("unexpected number of entries in w.watches (have %d, want %d):\n%v", 36 | len(w.watches), want, strings.Join(d, "\n")) 37 | } 38 | } 39 | 40 | check(2) 41 | 42 | if err := w.Remove(file); err != nil { 43 | t.Fatal(err) 44 | } 45 | check(1) 46 | 47 | if err := w.Remove(tmp); err != nil { 48 | t.Fatal(err) 49 | } 50 | check(0) 51 | } 52 | 53 | func TestWindowsRemWatch(t *testing.T) { 54 | tmp := t.TempDir() 55 | 56 | touch(t, tmp, "file") 57 | 58 | w := newWatcher(t) 59 | defer w.Close() 60 | 61 | addWatch(t, w, tmp) 62 | if err := w.Remove(tmp); err != nil { 63 | t.Fatalf("Could not remove the watch: %v\n", err) 64 | } 65 | if err := w.remWatch(tmp); err == nil { 66 | t.Fatal("Should be fail with closed handle\n") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /cmd/fsnotify/dedup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | "sync" 6 | "time" 7 | 8 | "github.com/fsnotify/fsnotify" 9 | ) 10 | 11 | // Depending on the system, a single "write" can generate many Write events; for 12 | // example compiling a large Go program can generate hundreds of Write events on 13 | // the binary. 14 | // 15 | // The general strategy to deal with this is to wait a short time for more write 16 | // events, resetting the wait period for every new event. 17 | func dedup(paths ...string) { 18 | if len(paths) < 1 { 19 | exit("must specify at least one path to watch") 20 | } 21 | 22 | // Create a new watcher. 23 | w, err := fsnotify.NewWatcher() 24 | if err != nil { 25 | exit("creating a new watcher: %s", err) 26 | } 27 | defer w.Close() 28 | 29 | // Start listening for events. 30 | go dedupLoop(w) 31 | 32 | // Add all paths from the commandline. 33 | for _, p := range paths { 34 | err = w.Add(p) 35 | if err != nil { 36 | exit("%q: %s", p, err) 37 | } 38 | } 39 | 40 | printTime("ready; press ^C to exit") 41 | <-make(chan struct{}) // Block forever 42 | } 43 | 44 | func dedupLoop(w *fsnotify.Watcher) { 45 | var ( 46 | // Wait 100ms for new events; each new event resets the timer. 47 | waitFor = 100 * time.Millisecond 48 | 49 | // Keep track of the timers, as path → timer. 50 | mu sync.Mutex 51 | timers = make(map[string]*time.Timer) 52 | 53 | // Callback we run. 54 | printEvent = func(e fsnotify.Event) { 55 | printTime(e.String()) 56 | 57 | // Don't need to remove the timer if you don't have a lot of files. 58 | mu.Lock() 59 | delete(timers, e.Name) 60 | mu.Unlock() 61 | } 62 | ) 63 | 64 | for { 65 | select { 66 | // Read from Errors. 67 | case err, ok := <-w.Errors: 68 | if !ok { // Channel was closed (i.e. Watcher.Close() was called). 69 | return 70 | } 71 | printTime("ERROR: %s", err) 72 | // Read from Events. 73 | case e, ok := <-w.Events: 74 | if !ok { // Channel was closed (i.e. Watcher.Close() was called). 75 | return 76 | } 77 | 78 | // We just want to watch for file creation, so ignore everything 79 | // outside of Create and Write. 80 | if !e.Has(fsnotify.Create) && !e.Has(fsnotify.Write) { 81 | continue 82 | } 83 | 84 | // Get timer. 85 | mu.Lock() 86 | t, ok := timers[e.Name] 87 | mu.Unlock() 88 | 89 | // No timer yet, so create one. 90 | if !ok { 91 | t = time.AfterFunc(math.MaxInt64, func() { printEvent(e) }) 92 | t.Stop() 93 | 94 | mu.Lock() 95 | timers[e.Name] = t 96 | mu.Unlock() 97 | } 98 | 99 | // Reset the timer for this path, so it will start from 100ms again. 100 | t.Reset(waitFor) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /cmd/fsnotify/file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/fsnotify/fsnotify" 8 | ) 9 | 10 | // Watch one or more files, but instead of watching the file directly it watches 11 | // the parent directory. This solves various issues where files are frequently 12 | // renamed, such as editors saving them. 13 | func file(files ...string) { 14 | if len(files) < 1 { 15 | exit("must specify at least one file to watch") 16 | } 17 | 18 | // Create a new watcher. 19 | w, err := fsnotify.NewWatcher() 20 | if err != nil { 21 | exit("creating a new watcher: %s", err) 22 | } 23 | defer w.Close() 24 | 25 | // Start listening for events. 26 | go fileLoop(w, files) 27 | 28 | // Add all files from the commandline. 29 | for _, p := range files { 30 | st, err := os.Lstat(p) 31 | if err != nil { 32 | exit("%s", err) 33 | } 34 | 35 | if st.IsDir() { 36 | exit("%q is a directory, not a file", p) 37 | } 38 | 39 | // Watch the directory, not the file itself. 40 | err = w.Add(filepath.Dir(p)) 41 | if err != nil { 42 | exit("%q: %s", p, err) 43 | } 44 | } 45 | 46 | printTime("ready; press ^C to exit") 47 | <-make(chan struct{}) // Block forever 48 | } 49 | 50 | func fileLoop(w *fsnotify.Watcher, files []string) { 51 | i := 0 52 | for { 53 | select { 54 | // Read from Errors. 55 | case err, ok := <-w.Errors: 56 | if !ok { // Channel was closed (i.e. Watcher.Close() was called). 57 | return 58 | } 59 | printTime("ERROR: %s", err) 60 | // Read from Events. 61 | case e, ok := <-w.Events: 62 | if !ok { // Channel was closed (i.e. Watcher.Close() was called). 63 | return 64 | } 65 | 66 | // Ignore files we're not interested in. Can use a 67 | // map[string]struct{} if you have a lot of files, but for just a 68 | // few files simply looping over a slice is faster. 69 | var found bool 70 | for _, f := range files { 71 | if f == e.Name { 72 | found = true 73 | } 74 | } 75 | if !found { 76 | continue 77 | } 78 | 79 | // Just print the event nicely aligned, and keep track how many 80 | // events we've seen. 81 | i++ 82 | printTime("%3d %s", i, e) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /cmd/fsnotify/main.go: -------------------------------------------------------------------------------- 1 | // Command fsnotify provides example usage of the fsnotify library. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | ) 10 | 11 | var usage = ` 12 | fsnotify is a Go library to provide cross-platform file system notifications. 13 | This command serves as an example and debugging tool. 14 | 15 | https://github.com/fsnotify/fsnotify 16 | 17 | Commands: 18 | 19 | watch [paths] Watch the paths for changes and print the events. 20 | file [file] Watch a single file for changes. 21 | dedup [paths] Watch the paths for changes, suppressing duplicate events. 22 | `[1:] 23 | 24 | func exit(format string, a ...interface{}) { 25 | fmt.Fprintf(os.Stderr, filepath.Base(os.Args[0])+": "+format+"\n", a...) 26 | fmt.Print("\n" + usage) 27 | os.Exit(1) 28 | } 29 | 30 | func help() { 31 | fmt.Printf("%s [command] [arguments]\n\n", filepath.Base(os.Args[0])) 32 | fmt.Print(usage) 33 | os.Exit(0) 34 | } 35 | 36 | // Print line prefixed with the time (a bit shorter than log.Print; we don't 37 | // really need the date and ms is useful here). 38 | func printTime(s string, args ...interface{}) { 39 | fmt.Printf(time.Now().Format("15:04:05.0000")+" "+s+"\n", args...) 40 | } 41 | 42 | func main() { 43 | if len(os.Args) == 1 { 44 | help() 45 | } 46 | // Always show help if -h[elp] appears anywhere before we do anything else. 47 | for _, f := range os.Args[1:] { 48 | switch f { 49 | case "help", "-h", "-help", "--help": 50 | help() 51 | } 52 | } 53 | 54 | cmd, args := os.Args[1], os.Args[2:] 55 | switch cmd { 56 | default: 57 | exit("unknown command: %q", cmd) 58 | case "watch": 59 | watch(args...) 60 | case "file": 61 | file(args...) 62 | case "dedup": 63 | dedup(args...) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /cmd/fsnotify/watch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/fsnotify/fsnotify" 4 | 5 | // This is the most basic example: it prints events to the terminal as we 6 | // receive them. 7 | func watch(paths ...string) { 8 | if len(paths) < 1 { 9 | exit("must specify at least one path to watch") 10 | } 11 | 12 | // Create a new watcher. 13 | w, err := fsnotify.NewWatcher() 14 | if err != nil { 15 | exit("creating a new watcher: %s", err) 16 | } 17 | defer w.Close() 18 | 19 | // Start listening for events. 20 | go watchLoop(w) 21 | 22 | // Add all paths from the commandline. 23 | for _, p := range paths { 24 | err = w.Add(p) 25 | if err != nil { 26 | exit("%q: %s", p, err) 27 | } 28 | } 29 | 30 | printTime("ready; press ^C to exit") 31 | <-make(chan struct{}) // Block forever 32 | } 33 | 34 | func watchLoop(w *fsnotify.Watcher) { 35 | i := 0 36 | for { 37 | select { 38 | // Read from Errors. 39 | case err, ok := <-w.Errors: 40 | if !ok { // Channel was closed (i.e. Watcher.Close() was called). 41 | return 42 | } 43 | printTime("ERROR: %s", err) 44 | // Read from Events. 45 | case e, ok := <-w.Events: 46 | if !ok { // Channel was closed (i.e. Watcher.Close() was called). 47 | return 48 | } 49 | 50 | // Just print the event nicely aligned, and keep track how many 51 | // events we've seen. 52 | i++ 53 | printTime("%3d %s", i, e) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /fsnotify.go: -------------------------------------------------------------------------------- 1 | // Package fsnotify provides a cross-platform interface for file system 2 | // notifications. 3 | // 4 | // Currently supported systems: 5 | // 6 | // Linux 2.6.32+ via inotify 7 | // BSD, macOS via kqueue 8 | // Windows via ReadDirectoryChangesW 9 | // illumos via FEN 10 | package fsnotify 11 | 12 | import ( 13 | "errors" 14 | "fmt" 15 | "path/filepath" 16 | "strings" 17 | ) 18 | 19 | // Event represents a file system notification. 20 | type Event struct { 21 | // Path to the file or directory. 22 | // 23 | // Paths are relative to the input; for example with Add("dir") the Name 24 | // will be set to "dir/file" if you create that file, but if you use 25 | // Add("/path/to/dir") it will be "/path/to/dir/file". 26 | Name string 27 | 28 | // File operation that triggered the event. 29 | // 30 | // This is a bitmask and some systems may send multiple operations at once. 31 | // Use the Event.Has() method instead of comparing with ==. 32 | Op Op 33 | } 34 | 35 | // Op describes a set of file operations. 36 | type Op uint32 37 | 38 | // The operations fsnotify can trigger; see the documentation on [Watcher] for a 39 | // full description, and check them with [Event.Has]. 40 | const ( 41 | // A new pathname was created. 42 | Create Op = 1 << iota 43 | 44 | // The pathname was written to; this does *not* mean the write has finished, 45 | // and a write can be followed by more writes. 46 | Write 47 | 48 | // The path was removed; any watches on it will be removed. Some "remove" 49 | // operations may trigger a Rename if the file is actually moved (for 50 | // example "remove to trash" is often a rename). 51 | Remove 52 | 53 | // The path was renamed to something else; any watched on it will be 54 | // removed. 55 | Rename 56 | 57 | // File attributes were changed. 58 | // 59 | // It's generally not recommended to take action on this event, as it may 60 | // get triggered very frequently by some software. For example, Spotlight 61 | // indexing on macOS, anti-virus software, backup software, etc. 62 | Chmod 63 | ) 64 | 65 | var ( 66 | // ErrNonExistentWatch is used when Remove() is called on a path that's not 67 | // added. 68 | ErrNonExistentWatch = errors.New("fsnotify: can't remove non-existent watch") 69 | 70 | // ErrClosed is used when trying to operate on a closed Watcher. 71 | ErrClosed = errors.New("fsnotify: watcher already closed") 72 | 73 | // ErrEventOverflow is reported from the Errors channel when there are too 74 | // many events: 75 | // 76 | // - inotify: inotify returns IN_Q_OVERFLOW – because there are too 77 | // many queued events (the fs.inotify.max_queued_events 78 | // sysctl can be used to increase this). 79 | // - windows: The buffer size is too small; WithBufferSize() can be used to increase it. 80 | // - kqueue, fen: Not used. 81 | ErrEventOverflow = errors.New("fsnotify: queue or buffer overflow") 82 | ) 83 | 84 | func (o Op) String() string { 85 | var b strings.Builder 86 | if o.Has(Create) { 87 | b.WriteString("|CREATE") 88 | } 89 | if o.Has(Remove) { 90 | b.WriteString("|REMOVE") 91 | } 92 | if o.Has(Write) { 93 | b.WriteString("|WRITE") 94 | } 95 | if o.Has(Rename) { 96 | b.WriteString("|RENAME") 97 | } 98 | if o.Has(Chmod) { 99 | b.WriteString("|CHMOD") 100 | } 101 | if b.Len() == 0 { 102 | return "[no events]" 103 | } 104 | return b.String()[1:] 105 | } 106 | 107 | // Has reports if this operation has the given operation. 108 | func (o Op) Has(h Op) bool { return o&h != 0 } 109 | 110 | // Has reports if this event has the given operation. 111 | func (e Event) Has(op Op) bool { return e.Op.Has(op) } 112 | 113 | // String returns a string representation of the event with their path. 114 | func (e Event) String() string { 115 | return fmt.Sprintf("%-13s %q", e.Op.String(), e.Name) 116 | } 117 | 118 | type ( 119 | addOpt func(opt *withOpts) 120 | withOpts struct { 121 | bufsize int 122 | } 123 | ) 124 | 125 | var defaultOpts = withOpts{ 126 | bufsize: 65536, // 64K 127 | } 128 | 129 | func getOptions(opts ...addOpt) withOpts { 130 | with := defaultOpts 131 | for _, o := range opts { 132 | o(&with) 133 | } 134 | return with 135 | } 136 | 137 | // WithBufferSize sets the [ReadDirectoryChangesW] buffer size. 138 | // 139 | // This only has effect on Windows systems, and is a no-op for other backends. 140 | // 141 | // The default value is 64K (65536 bytes) which is the highest value that works 142 | // on all filesystems and should be enough for most applications, but if you 143 | // have a large burst of events it may not be enough. You can increase it if 144 | // you're hitting "queue or buffer overflow" errors ([ErrEventOverflow]). 145 | // 146 | // [ReadDirectoryChangesW]: https://learn.microsoft.com/en-gb/windows/win32/api/winbase/nf-winbase-readdirectorychangesw 147 | func WithBufferSize(bytes int) addOpt { 148 | return func(opt *withOpts) { opt.bufsize = bytes } 149 | } 150 | 151 | // Check if this path is recursive (ends with "/..." or "\..."), and return the 152 | // path with the /... stripped. 153 | func recursivePath(path string) (string, bool) { 154 | if filepath.Base(path) == "..." { 155 | return filepath.Dir(path), true 156 | } 157 | return path, false 158 | } 159 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fsnotify/fsnotify 2 | 3 | go 1.17 4 | 5 | require golang.org/x/sys v0.4.0 6 | 7 | retract ( 8 | v1.5.3 // Published an incorrect branch accidentally https://github.com/fsnotify/fsnotify/issues/445 9 | v1.5.0 // Contains symlink regression https://github.com/fsnotify/fsnotify/pull/394 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 2 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 3 | -------------------------------------------------------------------------------- /helpers_test.go: -------------------------------------------------------------------------------- 1 | package fsnotify 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | "sort" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "testing" 14 | "time" 15 | 16 | "github.com/fsnotify/fsnotify/internal" 17 | ) 18 | 19 | type testCase struct { 20 | name string 21 | ops func(*testing.T, *Watcher, string) 22 | want string 23 | } 24 | 25 | func (tt testCase) run(t *testing.T) { 26 | t.Helper() 27 | t.Run(tt.name, func(t *testing.T) { 28 | t.Helper() 29 | t.Parallel() 30 | tmp := t.TempDir() 31 | 32 | w := newCollector(t) 33 | w.collect(t) 34 | 35 | tt.ops(t, w.w, tmp) 36 | 37 | cmpEvents(t, tmp, w.stop(t), newEvents(t, tt.want)) 38 | }) 39 | } 40 | 41 | // We wait a little bit after most commands; gives the system some time to sync 42 | // things and makes things more consistent across platforms. 43 | func eventSeparator() { time.Sleep(50 * time.Millisecond) } 44 | func waitForEvents() { time.Sleep(500 * time.Millisecond) } 45 | 46 | // To test the buffered watcher we run the tests twice in the CI: once as "go 47 | // test" and once with FSNOTIFY_BUFFER set. This is a bit hacky, but saves 48 | // having to refactor a lot of this code. Besides, running the tests in the CI 49 | // more than once isn't a bad thing, since it helps catch flaky tests (should 50 | // probably run it even more). 51 | var testBuffered = func() uint { 52 | s, ok := os.LookupEnv("FSNOTIFY_BUFFER") 53 | if ok { 54 | i, err := strconv.ParseUint(s, 0, 0) 55 | if err != nil { 56 | panic(fmt.Sprintf("FSNOTIFY_BUFFER: %s", err)) 57 | } 58 | return uint(i) 59 | } 60 | return 0 61 | }() 62 | 63 | // newWatcher initializes an fsnotify Watcher instance. 64 | func newWatcher(t *testing.T, add ...string) *Watcher { 65 | t.Helper() 66 | 67 | var ( 68 | w *Watcher 69 | err error 70 | ) 71 | if testBuffered > 0 { 72 | w, err = NewBufferedWatcher(testBuffered) 73 | } else { 74 | w, err = NewWatcher() 75 | } 76 | if err != nil { 77 | t.Fatalf("newWatcher: %s", err) 78 | } 79 | for _, a := range add { 80 | err := w.Add(a) 81 | if err != nil { 82 | t.Fatalf("newWatcher: add %q: %s", a, err) 83 | } 84 | } 85 | return w 86 | } 87 | 88 | // addWatch adds a watch for a directory 89 | func addWatch(t *testing.T, w *Watcher, path ...string) { 90 | t.Helper() 91 | if len(path) < 1 { 92 | t.Fatalf("addWatch: path must have at least one element: %s", path) 93 | } 94 | err := w.Add(join(path...)) 95 | if err != nil { 96 | t.Fatalf("addWatch(%q): %s", join(path...), err) 97 | } 98 | } 99 | 100 | // rmWatch removes a watch. 101 | func rmWatch(t *testing.T, watcher *Watcher, path ...string) { 102 | t.Helper() 103 | if len(path) < 1 { 104 | t.Fatalf("rmWatch: path must have at least one element: %s", path) 105 | } 106 | err := watcher.Remove(join(path...)) 107 | if err != nil { 108 | t.Fatalf("rmWatch(%q): %s", join(path...), err) 109 | } 110 | } 111 | 112 | const noWait = "" 113 | 114 | func shouldWait(path ...string) bool { 115 | // Take advantage of the fact that join skips empty parameters. 116 | for _, p := range path { 117 | if p == "" { 118 | return false 119 | } 120 | } 121 | return true 122 | } 123 | 124 | // Create n empty files with the prefix in the directory dir. 125 | func createFiles(t *testing.T, dir, prefix string, n int, d time.Duration) int { 126 | t.Helper() 127 | 128 | if d == 0 { 129 | d = 9 * time.Minute 130 | } 131 | 132 | fmtNum := func(n int) string { 133 | s := fmt.Sprintf("%09d", n) 134 | return s[:3] + "_" + s[3:6] + "_" + s[6:] 135 | } 136 | 137 | var ( 138 | max = time.After(d) 139 | created int 140 | ) 141 | for i := 0; i < n; i++ { 142 | select { 143 | case <-max: 144 | t.Logf("createFiles: stopped at %s files because it took longer than %s", fmtNum(created), d) 145 | return created 146 | default: 147 | path := join(dir, prefix+fmtNum(i)) 148 | fp, err := os.Create(path) 149 | if err != nil { 150 | t.Errorf("create failed for %s: %s", fmtNum(i), err) 151 | continue 152 | } 153 | if err := fp.Close(); err != nil { 154 | t.Errorf("close failed for %s: %s", fmtNum(i), err) 155 | } 156 | if err := os.Remove(path); err != nil { 157 | t.Errorf("remove failed for %s: %s", fmtNum(i), err) 158 | } 159 | if i%10_000 == 0 { 160 | t.Logf("createFiles: %s", fmtNum(i)) 161 | } 162 | created++ 163 | } 164 | } 165 | return created 166 | } 167 | 168 | // mkdir 169 | func mkdir(t *testing.T, path ...string) { 170 | t.Helper() 171 | if len(path) < 1 { 172 | t.Fatalf("mkdir: path must have at least one element: %s", path) 173 | } 174 | err := os.Mkdir(join(path...), 0o0755) 175 | if err != nil { 176 | t.Fatalf("mkdir(%q): %s", join(path...), err) 177 | } 178 | if shouldWait(path...) { 179 | eventSeparator() 180 | } 181 | } 182 | 183 | // mkdir -p 184 | func mkdirAll(t *testing.T, path ...string) { 185 | t.Helper() 186 | if len(path) < 1 { 187 | t.Fatalf("mkdirAll: path must have at least one element: %s", path) 188 | } 189 | err := os.MkdirAll(join(path...), 0o0755) 190 | if err != nil { 191 | t.Fatalf("mkdirAll(%q): %s", join(path...), err) 192 | } 193 | if shouldWait(path...) { 194 | eventSeparator() 195 | } 196 | } 197 | 198 | // ln -s 199 | func symlink(t *testing.T, target string, link ...string) { 200 | t.Helper() 201 | if len(link) < 1 { 202 | t.Fatalf("symlink: link must have at least one element: %s", link) 203 | } 204 | err := os.Symlink(target, join(link...)) 205 | if err != nil { 206 | t.Fatalf("symlink(%q, %q): %s", target, join(link...), err) 207 | } 208 | if shouldWait(link...) { 209 | eventSeparator() 210 | } 211 | } 212 | 213 | // mkfifo 214 | func mkfifo(t *testing.T, path ...string) { 215 | t.Helper() 216 | if len(path) < 1 { 217 | t.Fatalf("mkfifo: path must have at least one element: %s", path) 218 | } 219 | err := internal.Mkfifo(join(path...), 0o644) 220 | if err != nil { 221 | t.Fatalf("mkfifo(%q): %s", join(path...), err) 222 | } 223 | if shouldWait(path...) { 224 | eventSeparator() 225 | } 226 | } 227 | 228 | // mknod 229 | func mknod(t *testing.T, dev int, path ...string) { 230 | t.Helper() 231 | if len(path) < 1 { 232 | t.Fatalf("mknod: path must have at least one element: %s", path) 233 | } 234 | err := internal.Mknod(join(path...), 0o644, dev) 235 | if err != nil { 236 | t.Fatalf("mknod(%d, %q): %s", dev, join(path...), err) 237 | } 238 | if shouldWait(path...) { 239 | eventSeparator() 240 | } 241 | } 242 | 243 | // cat 244 | func cat(t *testing.T, data string, path ...string) { 245 | t.Helper() 246 | if len(path) < 1 { 247 | t.Fatalf("cat: path must have at least one element: %s", path) 248 | } 249 | 250 | err := func() error { 251 | fp, err := os.OpenFile(join(path...), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 252 | if err != nil { 253 | return err 254 | } 255 | if err := fp.Sync(); err != nil { 256 | return err 257 | } 258 | if shouldWait(path...) { 259 | eventSeparator() 260 | } 261 | if _, err := fp.WriteString(data); err != nil { 262 | return err 263 | } 264 | if err := fp.Sync(); err != nil { 265 | return err 266 | } 267 | if shouldWait(path...) { 268 | eventSeparator() 269 | } 270 | return fp.Close() 271 | }() 272 | if err != nil { 273 | t.Fatalf("cat(%q): %s", join(path...), err) 274 | } 275 | } 276 | 277 | // touch 278 | func touch(t *testing.T, path ...string) { 279 | t.Helper() 280 | if len(path) < 1 { 281 | t.Fatalf("touch: path must have at least one element: %s", path) 282 | } 283 | fp, err := os.Create(join(path...)) 284 | if err != nil { 285 | t.Fatalf("touch(%q): %s", join(path...), err) 286 | } 287 | err = fp.Close() 288 | if err != nil { 289 | t.Fatalf("touch(%q): %s", join(path...), err) 290 | } 291 | if shouldWait(path...) { 292 | eventSeparator() 293 | } 294 | } 295 | 296 | // mv 297 | func mv(t *testing.T, src string, dst ...string) { 298 | t.Helper() 299 | if len(dst) < 1 { 300 | t.Fatalf("mv: dst must have at least one element: %s", dst) 301 | } 302 | 303 | err := os.Rename(src, join(dst...)) 304 | if err != nil { 305 | t.Fatalf("mv(%q, %q): %s", src, join(dst...), err) 306 | } 307 | if shouldWait(dst...) { 308 | eventSeparator() 309 | } 310 | } 311 | 312 | // rm 313 | func rm(t *testing.T, path ...string) { 314 | t.Helper() 315 | if len(path) < 1 { 316 | t.Fatalf("rm: path must have at least one element: %s", path) 317 | } 318 | err := os.Remove(join(path...)) 319 | if err != nil { 320 | t.Fatalf("rm(%q): %s", join(path...), err) 321 | } 322 | if shouldWait(path...) { 323 | eventSeparator() 324 | } 325 | } 326 | 327 | // rm -r 328 | func rmAll(t *testing.T, path ...string) { 329 | t.Helper() 330 | if len(path) < 1 { 331 | t.Fatalf("rmAll: path must have at least one element: %s", path) 332 | } 333 | err := os.RemoveAll(join(path...)) 334 | if err != nil { 335 | t.Fatalf("rmAll(%q): %s", join(path...), err) 336 | } 337 | if shouldWait(path...) { 338 | eventSeparator() 339 | } 340 | } 341 | 342 | // chmod 343 | func chmod(t *testing.T, mode fs.FileMode, path ...string) { 344 | t.Helper() 345 | if len(path) < 1 { 346 | t.Fatalf("chmod: path must have at least one element: %s", path) 347 | } 348 | err := os.Chmod(join(path...), mode) 349 | if err != nil { 350 | t.Fatalf("chmod(%q): %s", join(path...), err) 351 | } 352 | if shouldWait(path...) { 353 | eventSeparator() 354 | } 355 | } 356 | 357 | // Collect all events in an array. 358 | // 359 | // w := newCollector(t) 360 | // w.collect(r) 361 | // 362 | // .. do stuff .. 363 | // 364 | // events := w.stop(t) 365 | type eventCollector struct { 366 | w *Watcher 367 | e Events 368 | mu sync.Mutex 369 | done chan struct{} 370 | } 371 | 372 | func newCollector(t *testing.T, add ...string) *eventCollector { 373 | return &eventCollector{ 374 | w: newWatcher(t, add...), 375 | done: make(chan struct{}), 376 | e: make(Events, 0, 8), 377 | } 378 | } 379 | 380 | // stop collecting events and return what we've got. 381 | func (w *eventCollector) stop(t *testing.T) Events { 382 | return w.stopWait(t, time.Second) 383 | } 384 | 385 | func (w *eventCollector) stopWait(t *testing.T, waitFor time.Duration) Events { 386 | waitForEvents() 387 | 388 | go func() { 389 | err := w.w.Close() 390 | if err != nil { 391 | t.Error(err) 392 | } 393 | }() 394 | 395 | select { 396 | case <-time.After(waitFor): 397 | t.Fatalf("event stream was not closed after %s", waitFor) 398 | case <-w.done: 399 | } 400 | 401 | w.mu.Lock() 402 | defer w.mu.Unlock() 403 | return w.e 404 | } 405 | 406 | // Get all events we've found up to now and clear the event buffer. 407 | func (w *eventCollector) events(t *testing.T) Events { 408 | w.mu.Lock() 409 | defer w.mu.Unlock() 410 | 411 | e := make(Events, len(w.e)) 412 | copy(e, w.e) 413 | w.e = make(Events, 0, 16) 414 | return e 415 | } 416 | 417 | // Start collecting events. 418 | func (w *eventCollector) collect(t *testing.T) { 419 | go func() { 420 | for { 421 | select { 422 | case e, ok := <-w.w.Errors: 423 | if !ok { 424 | w.done <- struct{}{} 425 | return 426 | } 427 | t.Error(e) 428 | w.done <- struct{}{} 429 | return 430 | case e, ok := <-w.w.Events: 431 | if !ok { 432 | w.done <- struct{}{} 433 | return 434 | } 435 | w.mu.Lock() 436 | w.e = append(w.e, e) 437 | w.mu.Unlock() 438 | } 439 | } 440 | }() 441 | } 442 | 443 | type Events []Event 444 | 445 | func (e Events) String() string { 446 | b := new(strings.Builder) 447 | for i, ee := range e { 448 | if i > 0 { 449 | b.WriteString("\n") 450 | } 451 | fmt.Fprintf(b, "%-20s %q", ee.Op.String(), filepath.ToSlash(ee.Name)) 452 | } 453 | return b.String() 454 | } 455 | 456 | func (e Events) TrimPrefix(prefix string) Events { 457 | for i := range e { 458 | if e[i].Name == prefix { 459 | e[i].Name = "/" 460 | } else { 461 | e[i].Name = strings.TrimPrefix(e[i].Name, prefix) 462 | } 463 | } 464 | return e 465 | } 466 | 467 | func (e Events) copy() Events { 468 | cp := make(Events, len(e)) 469 | copy(cp, e) 470 | return cp 471 | } 472 | 473 | // Create a new Events list from a string; for example: 474 | // 475 | // CREATE path 476 | // CREATE|WRITE path 477 | // 478 | // Every event is one line, and any whitespace between the event and path are 479 | // ignored. The path can optionally be surrounded in ". Anything after a "#" is 480 | // ignored. 481 | // 482 | // Platform-specific tests can be added after GOOS: 483 | // 484 | // # Tested if nothing else matches 485 | // CREATE path 486 | // 487 | // # Windows-specific test. 488 | // windows: 489 | // WRITE path 490 | // 491 | // You can specify multiple platforms with a comma (e.g. "windows, linux:"). 492 | // "kqueue" is a shortcut for all kqueue systems (BSD, macOS). 493 | func newEvents(t *testing.T, s string) Events { 494 | t.Helper() 495 | 496 | var ( 497 | lines = strings.Split(s, "\n") 498 | groups = []string{""} 499 | events = make(map[string]Events) 500 | ) 501 | for no, line := range lines { 502 | if i := strings.IndexByte(line, '#'); i > -1 { 503 | line = line[:i] 504 | } 505 | line = strings.TrimSpace(line) 506 | if line == "" { 507 | continue 508 | } 509 | if strings.HasSuffix(line, ":") { 510 | groups = strings.Split(strings.TrimRight(line, ":"), ",") 511 | for i := range groups { 512 | groups[i] = strings.TrimSpace(groups[i]) 513 | } 514 | continue 515 | } 516 | 517 | fields := strings.Fields(line) 518 | if len(fields) < 2 { 519 | if strings.ToUpper(fields[0]) == "EMPTY" { 520 | for _, g := range groups { 521 | events[g] = Events{} 522 | } 523 | continue 524 | } 525 | 526 | t.Fatalf("newEvents: line %d has less than 2 fields: %s", no, line) 527 | } 528 | 529 | path := strings.Trim(fields[len(fields)-1], `"`) 530 | 531 | var op Op 532 | for _, e := range fields[:len(fields)-1] { 533 | if e == "|" { 534 | continue 535 | } 536 | for _, ee := range strings.Split(e, "|") { 537 | switch strings.ToUpper(ee) { 538 | case "CREATE": 539 | op |= Create 540 | case "WRITE": 541 | op |= Write 542 | case "REMOVE": 543 | op |= Remove 544 | case "RENAME": 545 | op |= Rename 546 | case "CHMOD": 547 | op |= Chmod 548 | default: 549 | t.Fatalf("newEvents: line %d has unknown event %q: %s", no, ee, line) 550 | } 551 | } 552 | } 553 | 554 | for _, g := range groups { 555 | events[g] = append(events[g], Event{Name: path, Op: op}) 556 | } 557 | } 558 | 559 | if e, ok := events[runtime.GOOS]; ok { 560 | return e 561 | } 562 | switch runtime.GOOS { 563 | // kqueue shortcut 564 | case "freebsd", "netbsd", "openbsd", "dragonfly", "darwin": 565 | if e, ok := events["kqueue"]; ok { 566 | return e 567 | } 568 | // fen shortcut 569 | case "solaris", "illumos": 570 | if e, ok := events["fen"]; ok { 571 | return e 572 | } 573 | } 574 | return events[""] 575 | } 576 | 577 | func cmpEvents(t *testing.T, tmp string, have, want Events) { 578 | t.Helper() 579 | 580 | have = have.TrimPrefix(tmp) 581 | 582 | haveSort, wantSort := have.copy(), want.copy() 583 | sort.Slice(haveSort, func(i, j int) bool { 584 | return haveSort[i].String() > haveSort[j].String() 585 | }) 586 | sort.Slice(wantSort, func(i, j int) bool { 587 | return wantSort[i].String() > wantSort[j].String() 588 | }) 589 | 590 | if haveSort.String() != wantSort.String() { 591 | //t.Error("\n" + ztest.Diff(indent(haveSort), indent(wantSort))) 592 | t.Errorf("\nhave:\n%s\nwant:\n%s", indent(have), indent(want)) 593 | } 594 | } 595 | 596 | func indent(s fmt.Stringer) string { 597 | return "\t" + strings.ReplaceAll(s.String(), "\n", "\n\t") 598 | } 599 | 600 | var join = filepath.Join 601 | 602 | func isCI() bool { 603 | _, ok := os.LookupEnv("CI") 604 | return ok 605 | } 606 | 607 | func isKqueue() bool { 608 | switch runtime.GOOS { 609 | case "darwin", "freebsd", "openbsd", "netbsd", "dragonfly": 610 | return true 611 | } 612 | return false 613 | } 614 | 615 | func isSolaris() bool { 616 | switch runtime.GOOS { 617 | case "illumos", "solaris": 618 | return true 619 | } 620 | return false 621 | } 622 | 623 | func recurseOnly(t *testing.T) { 624 | switch runtime.GOOS { 625 | //case "windows": 626 | // Run test. 627 | default: 628 | t.Skip("recursion not yet supported on " + runtime.GOOS) 629 | } 630 | } 631 | -------------------------------------------------------------------------------- /internal/darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package internal 5 | 6 | import ( 7 | "syscall" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | var ( 13 | SyscallEACCES = syscall.EACCES 14 | UnixEACCES = unix.EACCES 15 | ) 16 | 17 | var maxfiles uint64 18 | 19 | // Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/ 20 | func SetRlimit() { 21 | var l syscall.Rlimit 22 | err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l) 23 | if err == nil && l.Cur != l.Max { 24 | l.Cur = l.Max 25 | syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l) 26 | } 27 | maxfiles = l.Cur 28 | 29 | if n, err := syscall.SysctlUint32("kern.maxfiles"); err == nil && uint64(n) < maxfiles { 30 | maxfiles = uint64(n) 31 | } 32 | 33 | if n, err := syscall.SysctlUint32("kern.maxfilesperproc"); err == nil && uint64(n) < maxfiles { 34 | maxfiles = uint64(n) 35 | } 36 | } 37 | 38 | func Maxfiles() uint64 { return maxfiles } 39 | func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) } 40 | func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, dev) } 41 | -------------------------------------------------------------------------------- /internal/debug_darwin.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "golang.org/x/sys/unix" 4 | 5 | var names = []struct { 6 | n string 7 | m uint32 8 | }{ 9 | {"NOTE_ABSOLUTE", unix.NOTE_ABSOLUTE}, 10 | {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, 11 | {"NOTE_BACKGROUND", unix.NOTE_BACKGROUND}, 12 | {"NOTE_CHILD", unix.NOTE_CHILD}, 13 | {"NOTE_CRITICAL", unix.NOTE_CRITICAL}, 14 | {"NOTE_DELETE", unix.NOTE_DELETE}, 15 | {"NOTE_EXEC", unix.NOTE_EXEC}, 16 | {"NOTE_EXIT", unix.NOTE_EXIT}, 17 | {"NOTE_EXITSTATUS", unix.NOTE_EXITSTATUS}, 18 | {"NOTE_EXIT_CSERROR", unix.NOTE_EXIT_CSERROR}, 19 | {"NOTE_EXIT_DECRYPTFAIL", unix.NOTE_EXIT_DECRYPTFAIL}, 20 | {"NOTE_EXIT_DETAIL", unix.NOTE_EXIT_DETAIL}, 21 | {"NOTE_EXIT_DETAIL_MASK", unix.NOTE_EXIT_DETAIL_MASK}, 22 | {"NOTE_EXIT_MEMORY", unix.NOTE_EXIT_MEMORY}, 23 | {"NOTE_EXIT_REPARENTED", unix.NOTE_EXIT_REPARENTED}, 24 | {"NOTE_EXTEND", unix.NOTE_EXTEND}, 25 | {"NOTE_FFAND", unix.NOTE_FFAND}, 26 | {"NOTE_FFCOPY", unix.NOTE_FFCOPY}, 27 | {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK}, 28 | {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK}, 29 | {"NOTE_FFNOP", unix.NOTE_FFNOP}, 30 | {"NOTE_FFOR", unix.NOTE_FFOR}, 31 | {"NOTE_FORK", unix.NOTE_FORK}, 32 | {"NOTE_FUNLOCK", unix.NOTE_FUNLOCK}, 33 | {"NOTE_LEEWAY", unix.NOTE_LEEWAY}, 34 | {"NOTE_LINK", unix.NOTE_LINK}, 35 | {"NOTE_LOWAT", unix.NOTE_LOWAT}, 36 | {"NOTE_MACHTIME", unix.NOTE_MACHTIME}, 37 | {"NOTE_MACH_CONTINUOUS_TIME", unix.NOTE_MACH_CONTINUOUS_TIME}, 38 | {"NOTE_NONE", unix.NOTE_NONE}, 39 | {"NOTE_NSECONDS", unix.NOTE_NSECONDS}, 40 | {"NOTE_OOB", unix.NOTE_OOB}, 41 | //{"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, -0x100000 (?!) 42 | {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, 43 | {"NOTE_REAP", unix.NOTE_REAP}, 44 | {"NOTE_RENAME", unix.NOTE_RENAME}, 45 | {"NOTE_REVOKE", unix.NOTE_REVOKE}, 46 | {"NOTE_SECONDS", unix.NOTE_SECONDS}, 47 | {"NOTE_SIGNAL", unix.NOTE_SIGNAL}, 48 | {"NOTE_TRACK", unix.NOTE_TRACK}, 49 | {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, 50 | {"NOTE_TRIGGER", unix.NOTE_TRIGGER}, 51 | {"NOTE_USECONDS", unix.NOTE_USECONDS}, 52 | {"NOTE_VM_ERROR", unix.NOTE_VM_ERROR}, 53 | {"NOTE_VM_PRESSURE", unix.NOTE_VM_PRESSURE}, 54 | {"NOTE_VM_PRESSURE_SUDDEN_TERMINATE", unix.NOTE_VM_PRESSURE_SUDDEN_TERMINATE}, 55 | {"NOTE_VM_PRESSURE_TERMINATE", unix.NOTE_VM_PRESSURE_TERMINATE}, 56 | {"NOTE_WRITE", unix.NOTE_WRITE}, 57 | } 58 | -------------------------------------------------------------------------------- /internal/debug_dragonfly.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "golang.org/x/sys/unix" 4 | 5 | var names = []struct { 6 | n string 7 | m uint32 8 | }{ 9 | {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, 10 | {"NOTE_CHILD", unix.NOTE_CHILD}, 11 | {"NOTE_DELETE", unix.NOTE_DELETE}, 12 | {"NOTE_EXEC", unix.NOTE_EXEC}, 13 | {"NOTE_EXIT", unix.NOTE_EXIT}, 14 | {"NOTE_EXTEND", unix.NOTE_EXTEND}, 15 | {"NOTE_FFAND", unix.NOTE_FFAND}, 16 | {"NOTE_FFCOPY", unix.NOTE_FFCOPY}, 17 | {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK}, 18 | {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK}, 19 | {"NOTE_FFNOP", unix.NOTE_FFNOP}, 20 | {"NOTE_FFOR", unix.NOTE_FFOR}, 21 | {"NOTE_FORK", unix.NOTE_FORK}, 22 | {"NOTE_LINK", unix.NOTE_LINK}, 23 | {"NOTE_LOWAT", unix.NOTE_LOWAT}, 24 | {"NOTE_OOB", unix.NOTE_OOB}, 25 | {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, 26 | {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, 27 | {"NOTE_RENAME", unix.NOTE_RENAME}, 28 | {"NOTE_REVOKE", unix.NOTE_REVOKE}, 29 | {"NOTE_TRACK", unix.NOTE_TRACK}, 30 | {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, 31 | {"NOTE_TRIGGER", unix.NOTE_TRIGGER}, 32 | {"NOTE_WRITE", unix.NOTE_WRITE}, 33 | } 34 | -------------------------------------------------------------------------------- /internal/debug_freebsd.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "golang.org/x/sys/unix" 4 | 5 | var names = []struct { 6 | n string 7 | m uint32 8 | }{ 9 | {"NOTE_ABSTIME", unix.NOTE_ABSTIME}, 10 | {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, 11 | {"NOTE_CHILD", unix.NOTE_CHILD}, 12 | {"NOTE_CLOSE", unix.NOTE_CLOSE}, 13 | {"NOTE_CLOSE_WRITE", unix.NOTE_CLOSE_WRITE}, 14 | {"NOTE_DELETE", unix.NOTE_DELETE}, 15 | {"NOTE_EXEC", unix.NOTE_EXEC}, 16 | {"NOTE_EXIT", unix.NOTE_EXIT}, 17 | {"NOTE_EXTEND", unix.NOTE_EXTEND}, 18 | {"NOTE_FFAND", unix.NOTE_FFAND}, 19 | {"NOTE_FFCOPY", unix.NOTE_FFCOPY}, 20 | {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK}, 21 | {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK}, 22 | {"NOTE_FFNOP", unix.NOTE_FFNOP}, 23 | {"NOTE_FFOR", unix.NOTE_FFOR}, 24 | {"NOTE_FILE_POLL", unix.NOTE_FILE_POLL}, 25 | {"NOTE_FORK", unix.NOTE_FORK}, 26 | {"NOTE_LINK", unix.NOTE_LINK}, 27 | {"NOTE_LOWAT", unix.NOTE_LOWAT}, 28 | {"NOTE_MSECONDS", unix.NOTE_MSECONDS}, 29 | {"NOTE_NSECONDS", unix.NOTE_NSECONDS}, 30 | {"NOTE_OPEN", unix.NOTE_OPEN}, 31 | {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, 32 | {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, 33 | {"NOTE_READ", unix.NOTE_READ}, 34 | {"NOTE_RENAME", unix.NOTE_RENAME}, 35 | {"NOTE_REVOKE", unix.NOTE_REVOKE}, 36 | {"NOTE_SECONDS", unix.NOTE_SECONDS}, 37 | {"NOTE_TRACK", unix.NOTE_TRACK}, 38 | {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, 39 | {"NOTE_TRIGGER", unix.NOTE_TRIGGER}, 40 | {"NOTE_USECONDS", unix.NOTE_USECONDS}, 41 | {"NOTE_WRITE", unix.NOTE_WRITE}, 42 | } 43 | -------------------------------------------------------------------------------- /internal/debug_kqueue.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd || openbsd || netbsd || dragonfly || darwin 2 | // +build freebsd openbsd netbsd dragonfly darwin 3 | 4 | package internal 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "golang.org/x/sys/unix" 13 | ) 14 | 15 | func Debug(name string, kevent *unix.Kevent_t) { 16 | mask := uint32(kevent.Fflags) 17 | var l []string 18 | for _, n := range names { 19 | if mask&n.m == n.m { 20 | l = append(l, n.n) 21 | } 22 | } 23 | 24 | fmt.Fprintf(os.Stderr, "%s %-20s → %s\n", 25 | time.Now().Format("15:04:05.0000"), 26 | strings.Join(l, " | "), name) 27 | } 28 | -------------------------------------------------------------------------------- /internal/debug_linux.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func Debug(name string, mask uint32) { 13 | names := []struct { 14 | n string 15 | m uint32 16 | }{ 17 | {"IN_ACCESS", unix.IN_ACCESS}, 18 | {"IN_ALL_EVENTS", unix.IN_ALL_EVENTS}, 19 | {"IN_ATTRIB", unix.IN_ATTRIB}, 20 | {"IN_CLASSA_HOST", unix.IN_CLASSA_HOST}, 21 | {"IN_CLASSA_MAX", unix.IN_CLASSA_MAX}, 22 | {"IN_CLASSA_NET", unix.IN_CLASSA_NET}, 23 | {"IN_CLASSA_NSHIFT", unix.IN_CLASSA_NSHIFT}, 24 | {"IN_CLASSB_HOST", unix.IN_CLASSB_HOST}, 25 | {"IN_CLASSB_MAX", unix.IN_CLASSB_MAX}, 26 | {"IN_CLASSB_NET", unix.IN_CLASSB_NET}, 27 | {"IN_CLASSB_NSHIFT", unix.IN_CLASSB_NSHIFT}, 28 | {"IN_CLASSC_HOST", unix.IN_CLASSC_HOST}, 29 | {"IN_CLASSC_NET", unix.IN_CLASSC_NET}, 30 | {"IN_CLASSC_NSHIFT", unix.IN_CLASSC_NSHIFT}, 31 | {"IN_CLOSE", unix.IN_CLOSE}, 32 | {"IN_CLOSE_NOWRITE", unix.IN_CLOSE_NOWRITE}, 33 | {"IN_CLOSE_WRITE", unix.IN_CLOSE_WRITE}, 34 | {"IN_CREATE", unix.IN_CREATE}, 35 | {"IN_DELETE", unix.IN_DELETE}, 36 | {"IN_DELETE_SELF", unix.IN_DELETE_SELF}, 37 | {"IN_DONT_FOLLOW", unix.IN_DONT_FOLLOW}, 38 | {"IN_EXCL_UNLINK", unix.IN_EXCL_UNLINK}, 39 | {"IN_IGNORED", unix.IN_IGNORED}, 40 | {"IN_ISDIR", unix.IN_ISDIR}, 41 | {"IN_LOOPBACKNET", unix.IN_LOOPBACKNET}, 42 | {"IN_MASK_ADD", unix.IN_MASK_ADD}, 43 | {"IN_MASK_CREATE", unix.IN_MASK_CREATE}, 44 | {"IN_MODIFY", unix.IN_MODIFY}, 45 | {"IN_MOVE", unix.IN_MOVE}, 46 | {"IN_MOVED_FROM", unix.IN_MOVED_FROM}, 47 | {"IN_MOVED_TO", unix.IN_MOVED_TO}, 48 | {"IN_MOVE_SELF", unix.IN_MOVE_SELF}, 49 | {"IN_ONESHOT", unix.IN_ONESHOT}, 50 | {"IN_ONLYDIR", unix.IN_ONLYDIR}, 51 | {"IN_OPEN", unix.IN_OPEN}, 52 | {"IN_Q_OVERFLOW", unix.IN_Q_OVERFLOW}, 53 | {"IN_UNMOUNT", unix.IN_UNMOUNT}, 54 | } 55 | 56 | var l []string 57 | for _, n := range names { 58 | if mask&n.m == n.m { 59 | l = append(l, n.n) 60 | } 61 | } 62 | fmt.Fprintf(os.Stderr, "%s %-20s → %s\n", time.Now().Format("15:04:05.0000"), strings.Join(l, " | "), name) 63 | } 64 | -------------------------------------------------------------------------------- /internal/debug_netbsd.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "golang.org/x/sys/unix" 4 | 5 | var names = []struct { 6 | n string 7 | m uint32 8 | }{ 9 | {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, 10 | {"NOTE_CHILD", unix.NOTE_CHILD}, 11 | {"NOTE_DELETE", unix.NOTE_DELETE}, 12 | {"NOTE_EXEC", unix.NOTE_EXEC}, 13 | {"NOTE_EXIT", unix.NOTE_EXIT}, 14 | {"NOTE_EXTEND", unix.NOTE_EXTEND}, 15 | {"NOTE_FORK", unix.NOTE_FORK}, 16 | {"NOTE_LINK", unix.NOTE_LINK}, 17 | {"NOTE_LOWAT", unix.NOTE_LOWAT}, 18 | {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, 19 | {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, 20 | {"NOTE_RENAME", unix.NOTE_RENAME}, 21 | {"NOTE_REVOKE", unix.NOTE_REVOKE}, 22 | {"NOTE_TRACK", unix.NOTE_TRACK}, 23 | {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, 24 | {"NOTE_WRITE", unix.NOTE_WRITE}, 25 | } 26 | -------------------------------------------------------------------------------- /internal/debug_openbsd.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "golang.org/x/sys/unix" 4 | 5 | var names = []struct { 6 | n string 7 | m uint32 8 | }{ 9 | {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, 10 | // {"NOTE_CHANGE", unix.NOTE_CHANGE}, // Not on 386? 11 | {"NOTE_CHILD", unix.NOTE_CHILD}, 12 | {"NOTE_DELETE", unix.NOTE_DELETE}, 13 | {"NOTE_EOF", unix.NOTE_EOF}, 14 | {"NOTE_EXEC", unix.NOTE_EXEC}, 15 | {"NOTE_EXIT", unix.NOTE_EXIT}, 16 | {"NOTE_EXTEND", unix.NOTE_EXTEND}, 17 | {"NOTE_FORK", unix.NOTE_FORK}, 18 | {"NOTE_LINK", unix.NOTE_LINK}, 19 | {"NOTE_LOWAT", unix.NOTE_LOWAT}, 20 | {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, 21 | {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, 22 | {"NOTE_RENAME", unix.NOTE_RENAME}, 23 | {"NOTE_REVOKE", unix.NOTE_REVOKE}, 24 | {"NOTE_TRACK", unix.NOTE_TRACK}, 25 | {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, 26 | {"NOTE_TRUNCATE", unix.NOTE_TRUNCATE}, 27 | {"NOTE_WRITE", unix.NOTE_WRITE}, 28 | } 29 | -------------------------------------------------------------------------------- /internal/debug_solaris.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func Debug(name string, mask int32) { 13 | names := []struct { 14 | n string 15 | m int32 16 | }{ 17 | {"FILE_ACCESS", unix.FILE_ACCESS}, 18 | {"FILE_MODIFIED", unix.FILE_MODIFIED}, 19 | {"FILE_ATTRIB", unix.FILE_ATTRIB}, 20 | {"FILE_TRUNC", unix.FILE_TRUNC}, 21 | {"FILE_NOFOLLOW", unix.FILE_NOFOLLOW}, 22 | {"FILE_DELETE", unix.FILE_DELETE}, 23 | {"FILE_RENAME_TO", unix.FILE_RENAME_TO}, 24 | {"FILE_RENAME_FROM", unix.FILE_RENAME_FROM}, 25 | {"UNMOUNTED", unix.UNMOUNTED}, 26 | {"MOUNTEDOVER", unix.MOUNTEDOVER}, 27 | {"FILE_EXCEPTION", unix.FILE_EXCEPTION}, 28 | } 29 | 30 | var l []string 31 | for _, n := range names { 32 | if mask&n.m == n.m { 33 | l = append(l, n.n) 34 | } 35 | } 36 | fmt.Fprintf(os.Stderr, "%s %-20s → %s\n", time.Now().Format("15:04:05.0000"), strings.Join(l, " | "), name) 37 | } 38 | -------------------------------------------------------------------------------- /internal/debug_windows.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | "golang.org/x/sys/windows" 10 | ) 11 | 12 | func Debug(name string, mask uint32) { 13 | names := []struct { 14 | n string 15 | m uint32 16 | }{ 17 | //{"FILE_NOTIFY_CHANGE_FILE_NAME", windows.FILE_NOTIFY_CHANGE_FILE_NAME}, 18 | //{"FILE_NOTIFY_CHANGE_DIR_NAME", windows.FILE_NOTIFY_CHANGE_DIR_NAME}, 19 | //{"FILE_NOTIFY_CHANGE_ATTRIBUTES", windows.FILE_NOTIFY_CHANGE_ATTRIBUTES}, 20 | //{"FILE_NOTIFY_CHANGE_SIZE", windows.FILE_NOTIFY_CHANGE_SIZE}, 21 | //{"FILE_NOTIFY_CHANGE_LAST_WRITE", windows.FILE_NOTIFY_CHANGE_LAST_WRITE}, 22 | //{"FILE_NOTIFY_CHANGE_LAST_ACCESS", windows.FILE_NOTIFY_CHANGE_LAST_ACCESS}, 23 | //{"FILE_NOTIFY_CHANGE_CREATION", windows.FILE_NOTIFY_CHANGE_CREATION}, 24 | //{"FILE_NOTIFY_CHANGE_SECURITY", windows.FILE_NOTIFY_CHANGE_SECURITY}, 25 | {"FILE_ACTION_ADDED", windows.FILE_ACTION_ADDED}, 26 | {"FILE_ACTION_REMOVED", windows.FILE_ACTION_REMOVED}, 27 | {"FILE_ACTION_MODIFIED", windows.FILE_ACTION_MODIFIED}, 28 | {"FILE_ACTION_RENAMED_OLD_NAME", windows.FILE_ACTION_RENAMED_OLD_NAME}, 29 | {"FILE_ACTION_RENAMED_NEW_NAME", windows.FILE_ACTION_RENAMED_NEW_NAME}, 30 | } 31 | 32 | var l []string 33 | for _, n := range names { 34 | if mask&n.m == n.m { 35 | l = append(l, n.n) 36 | } 37 | } 38 | fmt.Fprintf(os.Stderr, "%s %-20s → %s\n", time.Now().Format("15:04:05.0000"), strings.Join(l, " | "), name) 39 | } 40 | -------------------------------------------------------------------------------- /internal/freebsd.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd 2 | // +build freebsd 3 | 4 | package internal 5 | 6 | import ( 7 | "syscall" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | var ( 13 | SyscallEACCES = syscall.EACCES 14 | UnixEACCES = unix.EACCES 15 | ) 16 | 17 | var maxfiles uint64 18 | 19 | func SetRlimit() { 20 | // Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/ 21 | var l syscall.Rlimit 22 | err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l) 23 | if err == nil && l.Cur != l.Max { 24 | l.Cur = l.Max 25 | syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l) 26 | } 27 | maxfiles = uint64(l.Cur) 28 | } 29 | 30 | func Maxfiles() uint64 { return maxfiles } 31 | func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) } 32 | func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, uint64(dev)) } 33 | -------------------------------------------------------------------------------- /internal/internal.go: -------------------------------------------------------------------------------- 1 | // Package internal contains some helpers. 2 | package internal 3 | -------------------------------------------------------------------------------- /internal/unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !darwin && !freebsd 2 | // +build !windows,!darwin,!freebsd 3 | 4 | package internal 5 | 6 | import ( 7 | "syscall" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | var ( 13 | SyscallEACCES = syscall.EACCES 14 | UnixEACCES = unix.EACCES 15 | ) 16 | 17 | var maxfiles uint64 18 | 19 | func SetRlimit() { 20 | // Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/ 21 | var l syscall.Rlimit 22 | err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l) 23 | if err == nil && l.Cur != l.Max { 24 | l.Cur = l.Max 25 | syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l) 26 | } 27 | maxfiles = uint64(l.Cur) 28 | } 29 | 30 | func Maxfiles() uint64 { return maxfiles } 31 | func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) } 32 | func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, dev) } 33 | -------------------------------------------------------------------------------- /internal/unix2.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package internal 5 | 6 | func HasPrivilegesForSymlink() bool { 7 | return true 8 | } 9 | -------------------------------------------------------------------------------- /internal/windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package internal 5 | 6 | import ( 7 | "errors" 8 | 9 | "golang.org/x/sys/windows" 10 | ) 11 | 12 | // Just a dummy. 13 | var ( 14 | SyscallEACCES = errors.New("dummy") 15 | UnixEACCES = errors.New("dummy") 16 | ) 17 | 18 | func SetRlimit() {} 19 | func Maxfiles() uint64 { return 1<<64 - 1 } 20 | func Mkfifo(path string, mode uint32) error { return errors.New("no FIFOs on Windows") } 21 | func Mknod(path string, mode uint32, dev int) error { return errors.New("no device nodes on Windows") } 22 | 23 | func HasPrivilegesForSymlink() bool { 24 | var sid *windows.SID 25 | err := windows.AllocateAndInitializeSid( 26 | &windows.SECURITY_NT_AUTHORITY, 27 | 2, 28 | windows.SECURITY_BUILTIN_DOMAIN_RID, 29 | windows.DOMAIN_ALIAS_RID_ADMINS, 30 | 0, 0, 0, 0, 0, 0, 31 | &sid) 32 | if err != nil { 33 | return false 34 | } 35 | defer windows.FreeSid(sid) 36 | token := windows.Token(0) 37 | member, err := token.IsMember(sid) 38 | if err != nil { 39 | return false 40 | } 41 | return member || token.IsElevated() 42 | } 43 | -------------------------------------------------------------------------------- /mkdoc.zsh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | [ "${ZSH_VERSION:-}" = "" ] && echo >&2 "Only works with zsh" && exit 1 3 | setopt err_exit no_unset pipefail extended_glob 4 | 5 | # Simple script to update the godoc comments on all watchers so you don't need 6 | # to update the same comment 5 times. 7 | 8 | watcher=$(</tmp/x 238 | print -r -- $cmt >>/tmp/x 239 | tail -n+$(( end + 1 )) $file >>/tmp/x 240 | mv /tmp/x $file 241 | done 242 | } 243 | 244 | set-cmt '^type Watcher struct ' $watcher 245 | set-cmt '^func NewWatcher(' $new 246 | set-cmt '^func NewBufferedWatcher(' $newbuffered 247 | set-cmt '^func (w \*Watcher) Add(' $add 248 | set-cmt '^func (w \*Watcher) AddWith(' $addwith 249 | set-cmt '^func (w \*Watcher) Remove(' $remove 250 | set-cmt '^func (w \*Watcher) Close(' $close 251 | set-cmt '^func (w \*Watcher) WatchList(' $watchlist 252 | set-cmt '^[[:space:]]*Events *chan Event$' $events 253 | set-cmt '^[[:space:]]*Errors *chan error$' $errors 254 | -------------------------------------------------------------------------------- /system_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd || openbsd || netbsd || dragonfly 2 | // +build freebsd openbsd netbsd dragonfly 3 | 4 | package fsnotify 5 | 6 | import "golang.org/x/sys/unix" 7 | 8 | const openMode = unix.O_NONBLOCK | unix.O_RDONLY | unix.O_CLOEXEC 9 | -------------------------------------------------------------------------------- /system_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package fsnotify 5 | 6 | import "golang.org/x/sys/unix" 7 | 8 | // note: this constant is not defined on BSD 9 | const openMode = unix.O_EVTONLY | unix.O_CLOEXEC 10 | -------------------------------------------------------------------------------- /test/kqueue.c: -------------------------------------------------------------------------------- 1 | // This is an example kqueue program which watches a directory and all paths in 2 | // it with the same flags as those fsnotify uses. This is useful sometimes to 3 | // test what events kqueue sends with as little abstraction as possible. 4 | // 5 | // Note this does *not* set up monitoring on new files as they're created. 6 | // 7 | // Usage: 8 | // cc kqueue.c -o kqueue 9 | // ./kqueue /path/to/dir 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | void die(const char *fmt, ...) { 22 | va_list ap; 23 | va_start(ap, fmt); 24 | vfprintf(stderr, fmt, ap); 25 | va_end(ap); 26 | 27 | if (fmt[0] && fmt[strlen(fmt)-1] == ':') { 28 | fputc(' ', stderr); 29 | perror(NULL); 30 | } 31 | else 32 | fputc('\n', stderr); 33 | 34 | exit(1); 35 | } 36 | 37 | int main(int argc, char* argv[]) { 38 | if (argc < 2) { 39 | fprintf(stderr, "usage: %s path/to/dir\n", argv[0]); 40 | return 1; 41 | } 42 | char *dir = argv[1]; 43 | 44 | int kq = kqueue(); 45 | if (kq == -1) 46 | die("kqueue:"); 47 | 48 | int fp = open(dir, O_RDONLY); 49 | if (fp == -1) 50 | die("open: %s:", dir); 51 | DIR *dp = fdopendir(fp); 52 | if (dp == NULL) 53 | die("fdopendir:"); 54 | 55 | int fds[1024] = {fp}; 56 | char *names[1024] = {dir}; 57 | int n_fds = 0; 58 | struct dirent *ls; 59 | while ((ls = readdir(dp)) != NULL) { 60 | if (ls->d_name[0] == '.') 61 | continue; 62 | 63 | char *path = malloc(strlen(dir) + strlen(ls->d_name) + 2); 64 | sprintf(path, "%s/%s", dir, ls->d_name); 65 | 66 | int fp = open(path, O_RDONLY); 67 | if (fp == -1) 68 | die("open: %s:", path); 69 | fds[++n_fds] = fp; 70 | names[n_fds] = path; 71 | } 72 | 73 | for (int i=0; i<=n_fds; i++) { 74 | struct kevent changes; 75 | EV_SET(&changes, fds[i], EVFILT_VNODE, 76 | EV_ADD | EV_CLEAR | EV_ENABLE, 77 | NOTE_DELETE | NOTE_WRITE | NOTE_ATTRIB | NOTE_RENAME, 78 | 0, 0); 79 | 80 | int n = kevent(kq, &changes, 1, NULL, 0, NULL); 81 | if (n == -1) 82 | die("register kevent changes:"); 83 | } 84 | 85 | printf("Ready; press ^C to exit\n"); 86 | for (;;) { 87 | struct kevent event; 88 | int n = kevent(kq, NULL, 0, &event, 1, NULL); 89 | if (n == -1) 90 | die("kevent:"); 91 | if (n == 0) 92 | continue; 93 | 94 | char *ev_name = malloc(128); 95 | if (event.fflags & NOTE_WRITE) 96 | strncat(ev_name, "WRITE ", 6); 97 | if (event.fflags & NOTE_RENAME) 98 | strncat(ev_name, "RENAME ", 6); 99 | if (event.fflags & NOTE_ATTRIB) 100 | strncat(ev_name, "CHMOD ", 5); 101 | if (event.fflags & NOTE_DELETE) { 102 | strncat(ev_name, "DELETE ", 7); 103 | struct kevent changes; 104 | EV_SET(&changes, event.ident, EVFILT_VNODE, 105 | EV_DELETE, 106 | NOTE_DELETE | NOTE_WRITE | NOTE_ATTRIB | NOTE_RENAME, 107 | 0, 0); 108 | int n = kevent(kq, &changes, 1, NULL, 0, NULL); 109 | if (n == -1) 110 | die("remove kevent on delete:"); 111 | } 112 | 113 | char *name; 114 | for (int i=0; i<=n_fds; i++) 115 | if (fds[i] == event.ident) { 116 | name = names[i]; 117 | break; 118 | } 119 | 120 | printf("%-13s %s\n", ev_name, name); 121 | } 122 | return 0; 123 | } 124 | --------------------------------------------------------------------------------