pax_global_header00006660000000000000000000000064145151400000014500gustar00rootroot0000000000000052 comment=cfc9c4f277ea6ec18de92444b31983b183deb4fb fsnotify-1.7.0/000077500000000000000000000000001451514000000133465ustar00rootroot00000000000000fsnotify-1.7.0/.circleci/000077500000000000000000000000001451514000000152015ustar00rootroot00000000000000fsnotify-1.7.0/.circleci/config.yml000066400000000000000000000043041451514000000171720ustar00rootroot00000000000000version: 2.1 workflows: main: jobs: ['linux-arm64', 'ios'] jobs: # linux/arm64 linux-arm64: machine: image: ubuntu-2204:2022.07.1 resource_class: arm.medium working_directory: ~/repo steps: - checkout - run: name: install-go command: | sudo apt -y install golang - run: name: test command: | uname -a go version FSNOTIFY_BUFFER=4096 go test -parallel 1 -race ./... go test -parallel 1 -race ./... # iOS ios: macos: xcode: 13.4.1 working_directory: ~/repo steps: - checkout - run: name: install-go command: | export HOMEBREW_NO_AUTO_UPDATE=1 brew install go - run: name: test environment: SCAN_DEVICE: iPhone 6 SCAN_SCHEME: WebTests command: | export PATH=$PATH:/usr/local/Cellar/go/*/bin uname -a go version FSNOTIFY_BUFFER=4096 go test -parallel 1 -race ./... go test -parallel 1 -race ./... # This is just Linux x86_64; also need to get a Go with GOOS=android, but # there aren't any pre-built versions of that on the Go site. Idk, disable for # now; number of people using Go on Android is probably very tiny, and the # number of people using Go with this lib smaller still. # android: # machine: # image: android:2022.01.1 # working_directory: ~/repo # steps: # - checkout # - run: # name: install-go # command: | # v=1.19.2 # curl --silent --show-error --location --fail --retry 3 --output /tmp/go${v}.tgz \ # "https://go.dev/dl/go$v.linux-arm64.tar.gz" # sudo tar -C /usr/local -xzf /tmp/go${v}.tgz # rm /tmp/go${v}.tgz # - run: # name: test # command: | # uname -a # export PATH=/usr/local/go/bin:$PATH # go version # FSNOTIFY_BUFFER=4096 go test -parallel 1 -race ./... # go test -parallel 1 -race ./... # fsnotify-1.7.0/.cirrus.yml000066400000000000000000000007371451514000000154650ustar00rootroot00000000000000freebsd_task: name: 'FreeBSD' freebsd_instance: image_family: freebsd-13-2 install_script: - pkg update -f - pkg install -y go test_script: # run tests as user "cirrus" instead of root - pw useradd cirrus -m - chown -R cirrus:cirrus . - FSNOTIFY_BUFFER=4096 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./... - sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./... fsnotify-1.7.0/.editorconfig000066400000000000000000000003041451514000000160200ustar00rootroot00000000000000root = true [*.go] indent_style = tab indent_size = 4 insert_final_newline = true [*.{yml,yaml}] indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true fsnotify-1.7.0/.gitattributes000066400000000000000000000000321451514000000162340ustar00rootroot00000000000000go.sum linguist-generated fsnotify-1.7.0/.github/000077500000000000000000000000001451514000000147065ustar00rootroot00000000000000fsnotify-1.7.0/.github/FUNDING.yml000066400000000000000000000000171451514000000165210ustar00rootroot00000000000000github: arp242 fsnotify-1.7.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001451514000000170715ustar00rootroot00000000000000fsnotify-1.7.0/.github/ISSUE_TEMPLATE/bug.yml000066400000000000000000000024641451514000000203770ustar00rootroot00000000000000--- name: 'Bug report' description: 'Create a bug report' labels: ['bug'] body: - type: 'textarea' validations: {"required": true} attributes: label: 'Describe the bug' placeholder: 'A clear and concise description of what the bug is: expected behaviour, observed behaviour, etc.' - type: 'textarea' validations: {"required": true} attributes: label: 'To Reproduce' 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.' - type: 'textarea' validations: {"required": true} attributes: label: 'Which operating system and version are you using?' description: | ``` Linux: lsb_release -a macOS: sw_vers Windows: systeminfo | findstr /B /C:OS BSD: uname -a ``` - type: 'input' validations: {"required": true} attributes: label: 'Which fsnotify version are you using?' - type: 'dropdown' validations: {"required": true} attributes: label: 'Did you try the latest main branch?' description: 'Please try the latest main branch as well, with e.g.:
`go get github.com/fsnotify/fsnotify@main`' options: ['No', 'Yes'] fsnotify-1.7.0/.github/ISSUE_TEMPLATE/other.md000066400000000000000000000002101451514000000205250ustar00rootroot00000000000000--- name: 'Other' about: "Anything that's not a bug such as feature requests, questions, etc." title: '' labels: '' assignees: '' --- fsnotify-1.7.0/.github/workflows/000077500000000000000000000000001451514000000167435ustar00rootroot00000000000000fsnotify-1.7.0/.github/workflows/Vagrantfile.debian6000066400000000000000000000002201451514000000224310ustar00rootroot00000000000000Vagrant.configure("2") do |config| config.vm.box = "threatstack/debian6" config.vm.box_version = "1.0.0" config.vm.define 'debian6' end fsnotify-1.7.0/.github/workflows/build.yml000066400000000000000000000025631451514000000205730ustar00rootroot00000000000000name: 'build' on: pull_request: paths: ['**.go', 'go.mod', '.github/workflows/*'] push: branches: ['main', 'aix'] jobs: cross-compile: strategy: fail-fast: false matrix: go: ['1.17', '1.21'] runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v3 - name: setup Go uses: actions/setup-go@v4 with: go-version: ${{ matrix.go }} - name: build run: | for a in $(go tool dist list); do export GOOS=${a%%/*} export GOARCH=${a#*/} case "$GOOS" in (android|ios) exit 0 ;; # Requires cgo to link. (js) exit 0 ;; # No build tags in internal/ TODO: should maybe fix? (openbsd) case "$GOARCH" in # Fails with: # golang.org/x/sys/unix.syscall_syscall9: relocation target syscall.syscall10 not defined # golang.org/x/sys/unix.kevent: relocation target syscall.syscall6 not defined # golang.org/x/sys/unix.pipe2: relocation target syscall.rawSyscall not defined # ... # Works for me locally though, hmm... (386) exit 0 ;; esac esac go test -c go build ./cmd/fsnotify done fsnotify-1.7.0/.github/workflows/staticcheck.yml000066400000000000000000000017231451514000000217560ustar00rootroot00000000000000name: 'staticcheck' on: pull_request: paths: ['**.go', 'go.mod', '.github/workflows/*'] push: branches: ['main', 'aix'] jobs: staticcheck: name: 'staticcheck' runs-on: ubuntu-latest steps: - id: install_go uses: WillAbides/setup-go-faster@v1.7.0 with: go-version: "1.19.x" - uses: actions/cache@v3 with: key: ${{ runner.os }}-staticcheck path: | ${{ runner.temp }}/staticcheck ${{ steps.install_go.outputs.GOCACHE || '' }} - run: | export STATICCHECK_CACHE="${{ runner.temp }}/staticcheck" go install honnef.co/go/tools/cmd/staticcheck@latest $(go env GOPATH)/bin/staticcheck -matrix < Nathan Youngman <4566+nathany@users.noreply.github.com> fsnotify-1.7.0/CHANGELOG.md000066400000000000000000000513721451514000000151670ustar00rootroot00000000000000# Changelog Unreleased ---------- Nothing yet. 1.7.0 - 2023-10-22 ------------------ This version of fsnotify needs Go 1.17. ### Additions - illumos: add FEN backend to support illumos and Solaris. ([#371]) - all: add `NewBufferedWatcher()` to use a buffered channel, which can be useful in cases where you can't control the kernel buffer and receive a large number of events in bursts. ([#550], [#572]) - all: add `AddWith()`, which is identical to `Add()` but allows passing options. ([#521]) - windows: allow setting the ReadDirectoryChangesW() buffer size with `fsnotify.WithBufferSize()`; the default of 64K is the highest value that works on all platforms and is enough for most purposes, but in some cases a highest buffer is needed. ([#521]) ### Changes and fixes - inotify: remove watcher if a watched path is renamed ([#518]) After a rename the reported name wasn't updated, or even an empty string. Inotify doesn't provide any good facilities to update it, so just remove the watcher. This is already how it worked on kqueue and FEN. On Windows this does work, and remains working. - windows: don't listen for file attribute changes ([#520]) File attribute changes are sent as `FILE_ACTION_MODIFIED` by the Windows API, with no way to see if they're a file write or attribute change, so would show up as a fsnotify.Write event. This is never useful, and could result in many spurious Write events. - windows: return `ErrEventOverflow` if the buffer is full ([#525]) Before it would merely return "short read", making it hard to detect this error. - kqueue: make sure events for all files are delivered properly when removing a watched directory ([#526]) Previously they would get sent with `""` (empty string) or `"."` as the path name. - kqueue: don't emit spurious Create events for symbolic links ([#524]) The link would get resolved but kqueue would "forget" it already saw the link itself, resulting on a Create for every Write event for the directory. - all: return `ErrClosed` on `Add()` when the watcher is closed ([#516]) - other: add `Watcher.Errors` and `Watcher.Events` to the no-op `Watcher` in `backend_other.go`, making it easier to use on unsupported platforms such as WASM, AIX, etc. ([#528]) - other: use the `backend_other.go` no-op if the `appengine` build tag is set; Google AppEngine forbids usage of the unsafe package so the inotify backend won't compile there. [#371]: https://github.com/fsnotify/fsnotify/pull/371 [#516]: https://github.com/fsnotify/fsnotify/pull/516 [#518]: https://github.com/fsnotify/fsnotify/pull/518 [#520]: https://github.com/fsnotify/fsnotify/pull/520 [#521]: https://github.com/fsnotify/fsnotify/pull/521 [#524]: https://github.com/fsnotify/fsnotify/pull/524 [#525]: https://github.com/fsnotify/fsnotify/pull/525 [#526]: https://github.com/fsnotify/fsnotify/pull/526 [#528]: https://github.com/fsnotify/fsnotify/pull/528 [#537]: https://github.com/fsnotify/fsnotify/pull/537 [#550]: https://github.com/fsnotify/fsnotify/pull/550 [#572]: https://github.com/fsnotify/fsnotify/pull/572 1.6.0 - 2022-10-13 ------------------ This version of fsnotify needs Go 1.16 (this was already the case since 1.5.1, but not documented). It also increases the minimum Linux version to 2.6.32. ### Additions - all: add `Event.Has()` and `Op.Has()` ([#477]) This makes checking events a lot easier; for example: if event.Op&Write == Write && !(event.Op&Remove == Remove) { } Becomes: if event.Has(Write) && !event.Has(Remove) { } - all: add cmd/fsnotify ([#463]) A command-line utility for testing and some examples. ### Changes and fixes - inotify: don't ignore events for files that don't exist ([#260], [#470]) Previously the inotify watcher would call `os.Lstat()` to check if a file still exists before emitting events. This was inconsistent with other platforms and resulted in inconsistent event reporting (e.g. when a file is quickly removed and re-created), and generally a source of confusion. It was added in 2013 to fix a memory leak that no longer exists. - all: return `ErrNonExistentWatch` when `Remove()` is called on a path that's not watched ([#460]) - inotify: replace epoll() with non-blocking inotify ([#434]) Non-blocking inotify was not generally available at the time this library was written in 2014, but now it is. As a result, the minimum Linux version is bumped from 2.6.27 to 2.6.32. This hugely simplifies the code and is faster. - kqueue: don't check for events every 100ms ([#480]) The watcher would wake up every 100ms, even when there was nothing to do. Now it waits until there is something to do. - macos: retry opening files on EINTR ([#475]) - kqueue: skip unreadable files ([#479]) kqueue requires a file descriptor for every file in a directory; this would fail if a file was unreadable by the current user. Now these files are simply skipped. - windows: fix renaming a watched directory if the parent is also watched ([#370]) - windows: increase buffer size from 4K to 64K ([#485]) - windows: close file handle on Remove() ([#288]) - kqueue: put pathname in the error if watching a file fails ([#471]) - inotify, windows: calling Close() more than once could race ([#465]) - kqueue: improve Close() performance ([#233]) - all: various documentation additions and clarifications. [#233]: https://github.com/fsnotify/fsnotify/pull/233 [#260]: https://github.com/fsnotify/fsnotify/pull/260 [#288]: https://github.com/fsnotify/fsnotify/pull/288 [#370]: https://github.com/fsnotify/fsnotify/pull/370 [#434]: https://github.com/fsnotify/fsnotify/pull/434 [#460]: https://github.com/fsnotify/fsnotify/pull/460 [#463]: https://github.com/fsnotify/fsnotify/pull/463 [#465]: https://github.com/fsnotify/fsnotify/pull/465 [#470]: https://github.com/fsnotify/fsnotify/pull/470 [#471]: https://github.com/fsnotify/fsnotify/pull/471 [#475]: https://github.com/fsnotify/fsnotify/pull/475 [#477]: https://github.com/fsnotify/fsnotify/pull/477 [#479]: https://github.com/fsnotify/fsnotify/pull/479 [#480]: https://github.com/fsnotify/fsnotify/pull/480 [#485]: https://github.com/fsnotify/fsnotify/pull/485 ## [1.5.4] - 2022-04-25 * Windows: add missing defer to `Watcher.WatchList` [#447](https://github.com/fsnotify/fsnotify/pull/447) * go.mod: use latest x/sys [#444](https://github.com/fsnotify/fsnotify/pull/444) * Fix compilation for OpenBSD [#443](https://github.com/fsnotify/fsnotify/pull/443) ## [1.5.3] - 2022-04-22 * This version is retracted. An incorrect branch is published accidentally [#445](https://github.com/fsnotify/fsnotify/issues/445) ## [1.5.2] - 2022-04-21 * Add a feature to return the directories and files that are being monitored [#374](https://github.com/fsnotify/fsnotify/pull/374) * Fix potential crash on windows if `raw.FileNameLength` exceeds `syscall.MAX_PATH` [#361](https://github.com/fsnotify/fsnotify/pull/361) * Allow build on unsupported GOOS [#424](https://github.com/fsnotify/fsnotify/pull/424) * Don't set `poller.fd` twice in `newFdPoller` [#406](https://github.com/fsnotify/fsnotify/pull/406) * fix go vet warnings: call to `(*T).Fatalf` from a non-test goroutine [#416](https://github.com/fsnotify/fsnotify/pull/416) ## [1.5.1] - 2021-08-24 * Revert Add AddRaw to not follow symlinks [#394](https://github.com/fsnotify/fsnotify/pull/394) ## [1.5.0] - 2021-08-20 * Go: Increase minimum required version to Go 1.12 [#381](https://github.com/fsnotify/fsnotify/pull/381) * Feature: Add AddRaw method which does not follow symlinks when adding a watch [#289](https://github.com/fsnotify/fsnotify/pull/298) * Windows: Follow symlinks by default like on all other systems [#289](https://github.com/fsnotify/fsnotify/pull/289) * CI: Use GitHub Actions for CI and cover go 1.12-1.17 [#378](https://github.com/fsnotify/fsnotify/pull/378) [#381](https://github.com/fsnotify/fsnotify/pull/381) [#385](https://github.com/fsnotify/fsnotify/pull/385) * Go 1.14+: Fix unsafe pointer conversion [#325](https://github.com/fsnotify/fsnotify/pull/325) ## [1.4.9] - 2020-03-11 * Move example usage to the readme #329. This may resolve #328. ## [1.4.8] - 2020-03-10 * CI: test more go versions (@nathany 1d13583d846ea9d66dcabbfefbfb9d8e6fb05216) * Tests: Queued inotify events could have been read by the test before max_queued_events was hit (@matthias-stone #265) * Tests: t.Fatalf -> t.Errorf in go routines (@gdey #266) * CI: Less verbosity (@nathany #267) * Tests: Darwin: Exchangedata is deprecated on 10.13 (@nathany #267) * Tests: Check if channels are closed in the example (@alexeykazakov #244) * CI: Only run golint on latest version of go and fix issues (@cpuguy83 #284) * CI: Add windows to travis matrix (@cpuguy83 #284) * Docs: Remover appveyor badge (@nathany 11844c0959f6fff69ba325d097fce35bd85a8e93) * Linux: create epoll and pipe fds with close-on-exec (@JohannesEbke #219) * Linux: open files with close-on-exec (@linxiulei #273) * Docs: Plan to support fanotify (@nathany ab058b44498e8b7566a799372a39d150d9ea0119 ) * Project: Add go.mod (@nathany #309) * Project: Revise editor config (@nathany #309) * Project: Update copyright for 2019 (@nathany #309) * CI: Drop go1.8 from CI matrix (@nathany #309) * Docs: Updating the FAQ section for supportability with NFS & FUSE filesystems (@Pratik32 4bf2d1fec78374803a39307bfb8d340688f4f28e ) ## [1.4.7] - 2018-01-09 * BSD/macOS: Fix possible deadlock on closing the watcher on kqueue (thanks @nhooyr and @glycerine) * Tests: Fix missing verb on format string (thanks @rchiossi) * Linux: Fix deadlock in Remove (thanks @aarondl) * Linux: Watch.Add improvements (avoid race, fix consistency, reduce garbage) (thanks @twpayne) * Docs: Moved FAQ into the README (thanks @vahe) * Linux: Properly handle inotify's IN_Q_OVERFLOW event (thanks @zeldovich) * Docs: replace references to OS X with macOS ## [1.4.2] - 2016-10-10 * 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) ## [1.4.1] - 2016-10-04 * Fix flaky inotify stress test on Linux [#177](https://github.com/fsnotify/fsnotify/pull/177) (thanks @pattyshack) ## [1.4.0] - 2016-10-01 * add a String() method to Event.Op [#165](https://github.com/fsnotify/fsnotify/pull/165) (thanks @oozie) ## [1.3.1] - 2016-06-28 * Windows: fix for double backslash when watching the root of a drive [#151](https://github.com/fsnotify/fsnotify/issues/151) (thanks @brunoqc) ## [1.3.0] - 2016-04-19 * 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) ## [1.2.10] - 2016-03-02 * Fix golint errors in windows.go [#121](https://github.com/fsnotify/fsnotify/pull/121) (thanks @tiffanyfj) ## [1.2.9] - 2016-01-13 kqueue: Fix logic for CREATE after REMOVE [#111](https://github.com/fsnotify/fsnotify/pull/111) (thanks @bep) ## [1.2.8] - 2015-12-17 * 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) * inotify: fix race in test * enable race detection for continuous integration (Linux, Mac, Windows) ## [1.2.5] - 2015-10-17 * inotify: use epoll_create1 for arm64 support (requires Linux 2.6.27 or later) [#100](https://github.com/fsnotify/fsnotify/pull/100) (thanks @suihkulokki) * inotify: fix path leaks [#73](https://github.com/fsnotify/fsnotify/pull/73) (thanks @chamaken) * kqueue: watch for rename events on subdirectories [#83](https://github.com/fsnotify/fsnotify/pull/83) (thanks @guotie) * kqueue: avoid infinite loops from symlinks cycles [#101](https://github.com/fsnotify/fsnotify/pull/101) (thanks @illicitonion) ## [1.2.1] - 2015-10-14 * kqueue: don't watch named pipes [#98](https://github.com/fsnotify/fsnotify/pull/98) (thanks @evanphx) ## [1.2.0] - 2015-02-08 * inotify: use epoll to wake up readEvents [#66](https://github.com/fsnotify/fsnotify/pull/66) (thanks @PieterD) * inotify: closing watcher should now always shut down goroutine [#63](https://github.com/fsnotify/fsnotify/pull/63) (thanks @PieterD) * kqueue: close kqueue after removing watches, fixes [#59](https://github.com/fsnotify/fsnotify/issues/59) ## [1.1.1] - 2015-02-05 * inotify: Retry read on EINTR [#61](https://github.com/fsnotify/fsnotify/issues/61) (thanks @PieterD) ## [1.1.0] - 2014-12-12 * kqueue: rework internals [#43](https://github.com/fsnotify/fsnotify/pull/43) * add low-level functions * only need to store flags on directories * less mutexes [#13](https://github.com/fsnotify/fsnotify/issues/13) * done can be an unbuffered channel * remove calls to os.NewSyscallError * More efficient string concatenation for Event.String() [#52](https://github.com/fsnotify/fsnotify/pull/52) (thanks @mdlayher) * kqueue: fix regression in rework causing subdirectories to be watched [#48](https://github.com/fsnotify/fsnotify/issues/48) * kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51) ## [1.0.4] - 2014-09-07 * kqueue: add dragonfly to the build tags. * Rename source code files, rearrange code so exported APIs are at the top. * Add done channel to example code. [#37](https://github.com/fsnotify/fsnotify/pull/37) (thanks @chenyukang) ## [1.0.3] - 2014-08-19 * [Fix] Windows MOVED_TO now translates to Create like on BSD and Linux. [#36](https://github.com/fsnotify/fsnotify/issues/36) ## [1.0.2] - 2014-08-17 * [Fix] Missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso) * [Fix] Make ./path and path equivalent. (thanks @zhsso) ## [1.0.0] - 2014-08-15 * [API] Remove AddWatch on Windows, use Add. * Improve documentation for exported identifiers. [#30](https://github.com/fsnotify/fsnotify/issues/30) * Minor updates based on feedback from golint. ## dev / 2014-07-09 * Moved to [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify). * Use os.NewSyscallError instead of returning errno (thanks @hariharan-uno) ## dev / 2014-07-04 * kqueue: fix incorrect mutex used in Close() * Update example to demonstrate usage of Op. ## dev / 2014-06-28 * [API] Don't set the Write Op for attribute notifications [#4](https://github.com/fsnotify/fsnotify/issues/4) * Fix for String() method on Event (thanks Alex Brainman) * Don't build on Plan 9 or Solaris (thanks @4ad) ## dev / 2014-06-21 * Events channel of type Event rather than *Event. * [internal] use syscall constants directly for inotify and kqueue. * [internal] kqueue: rename events to kevents and fileEvent to event. ## dev / 2014-06-19 * Go 1.3+ required on Windows (uses syscall.ERROR_MORE_DATA internally). * [internal] remove cookie from Event struct (unused). * [internal] Event struct has the same definition across every OS. * [internal] remove internal watch and removeWatch methods. ## dev / 2014-06-12 * [API] Renamed Watch() to Add() and RemoveWatch() to Remove(). * [API] Pluralized channel names: Events and Errors. * [API] Renamed FileEvent struct to Event. * [API] Op constants replace methods like IsCreate(). ## dev / 2014-06-12 * Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98) ## dev / 2014-05-23 * [API] Remove current implementation of WatchFlags. * current implementation doesn't take advantage of OS for efficiency * provides little benefit over filtering events as they are received, but has extra bookkeeping and mutexes * no tests for the current implementation * not fully implemented on Windows [#93](https://github.com/howeyc/fsnotify/issues/93#issuecomment-39285195) ## [0.9.3] - 2014-12-31 * kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51) ## [0.9.2] - 2014-08-17 * [Backport] Fix missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso) ## [0.9.1] - 2014-06-12 * Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98) ## [0.9.0] - 2014-01-17 * IsAttrib() for events that only concern a file's metadata [#79][] (thanks @abustany) * [Fix] kqueue: fix deadlock [#77][] (thanks @cespare) * [NOTICE] Development has moved to `code.google.com/p/go.exp/fsnotify` in preparation for inclusion in the Go standard library. ## [0.8.12] - 2013-11-13 * [API] Remove FD_SET and friends from Linux adapter ## [0.8.11] - 2013-11-02 * [Doc] Add Changelog [#72][] (thanks @nathany) * [Doc] Spotlight and double modify events on macOS [#62][] (reported by @paulhammond) ## [0.8.10] - 2013-10-19 * [Fix] kqueue: remove file watches when parent directory is removed [#71][] (reported by @mdwhatcott) * [Fix] kqueue: race between Close and readEvents [#70][] (reported by @bernerdschaefer) * [Doc] specify OS-specific limits in README (thanks @debrando) ## [0.8.9] - 2013-09-08 * [Doc] Contributing (thanks @nathany) * [Doc] update package path in example code [#63][] (thanks @paulhammond) * [Doc] GoCI badge in README (Linux only) [#60][] * [Doc] Cross-platform testing with Vagrant [#59][] (thanks @nathany) ## [0.8.8] - 2013-06-17 * [Fix] Windows: handle `ERROR_MORE_DATA` on Windows [#49][] (thanks @jbowtie) ## [0.8.7] - 2013-06-03 * [API] Make syscall flags internal * [Fix] inotify: ignore event changes * [Fix] race in symlink test [#45][] (reported by @srid) * [Fix] tests on Windows * lower case error messages ## [0.8.6] - 2013-05-23 * kqueue: Use EVT_ONLY flag on Darwin * [Doc] Update README with full example ## [0.8.5] - 2013-05-09 * [Fix] inotify: allow monitoring of "broken" symlinks (thanks @tsg) ## [0.8.4] - 2013-04-07 * [Fix] kqueue: watch all file events [#40][] (thanks @ChrisBuchholz) ## [0.8.3] - 2013-03-13 * [Fix] inoitfy/kqueue memory leak [#36][] (reported by @nbkolchin) * [Fix] kqueue: use fsnFlags for watching a directory [#33][] (reported by @nbkolchin) ## [0.8.2] - 2013-02-07 * [Doc] add Authors * [Fix] fix data races for map access [#29][] (thanks @fsouza) ## [0.8.1] - 2013-01-09 * [Fix] Windows path separators * [Doc] BSD License ## [0.8.0] - 2012-11-09 * kqueue: directory watching improvements (thanks @vmirage) * inotify: add `IN_MOVED_TO` [#25][] (requested by @cpisto) * [Fix] kqueue: deleting watched directory [#24][] (reported by @jakerr) ## [0.7.4] - 2012-10-09 * [Fix] inotify: fixes from https://codereview.appspot.com/5418045/ (ugorji) * [Fix] kqueue: preserve watch flags when watching for delete [#21][] (reported by @robfig) * [Fix] kqueue: watch the directory even if it isn't a new watch (thanks @robfig) * [Fix] kqueue: modify after recreation of file ## [0.7.3] - 2012-09-27 * [Fix] kqueue: watch with an existing folder inside the watched folder (thanks @vmirage) * [Fix] kqueue: no longer get duplicate CREATE events ## [0.7.2] - 2012-09-01 * kqueue: events for created directories ## [0.7.1] - 2012-07-14 * [Fix] for renaming files ## [0.7.0] - 2012-07-02 * [Feature] FSNotify flags * [Fix] inotify: Added file name back to event path ## [0.6.0] - 2012-06-06 * kqueue: watch files after directory created (thanks @tmc) ## [0.5.1] - 2012-05-22 * [Fix] inotify: remove all watches before Close() ## [0.5.0] - 2012-05-03 * [API] kqueue: return errors during watch instead of sending over channel * kqueue: match symlink behavior on Linux * inotify: add `DELETE_SELF` (requested by @taralx) * [Fix] kqueue: handle EINTR (reported by @robfig) * [Doc] Godoc example [#1][] (thanks @davecheney) ## [0.4.0] - 2012-03-30 * Go 1 released: build with go tool * [Feature] Windows support using winfsnotify * Windows does not have attribute change notifications * Roll attribute notifications into IsModify ## [0.3.0] - 2012-02-19 * kqueue: add files when watch directory ## [0.2.0] - 2011-12-30 * update to latest Go weekly code ## [0.1.0] - 2011-10-19 * kqueue: add watch on file creation to match inotify * kqueue: create file event * inotify: ignore `IN_IGNORED` events * event String() * linux: common FileEvent functions * initial commit [#79]: https://github.com/howeyc/fsnotify/pull/79 [#77]: https://github.com/howeyc/fsnotify/pull/77 [#72]: https://github.com/howeyc/fsnotify/issues/72 [#71]: https://github.com/howeyc/fsnotify/issues/71 [#70]: https://github.com/howeyc/fsnotify/issues/70 [#63]: https://github.com/howeyc/fsnotify/issues/63 [#62]: https://github.com/howeyc/fsnotify/issues/62 [#60]: https://github.com/howeyc/fsnotify/issues/60 [#59]: https://github.com/howeyc/fsnotify/issues/59 [#49]: https://github.com/howeyc/fsnotify/issues/49 [#45]: https://github.com/howeyc/fsnotify/issues/45 [#40]: https://github.com/howeyc/fsnotify/issues/40 [#36]: https://github.com/howeyc/fsnotify/issues/36 [#33]: https://github.com/howeyc/fsnotify/issues/33 [#29]: https://github.com/howeyc/fsnotify/issues/29 [#25]: https://github.com/howeyc/fsnotify/issues/25 [#24]: https://github.com/howeyc/fsnotify/issues/24 [#21]: https://github.com/howeyc/fsnotify/issues/21 fsnotify-1.7.0/CONTRIBUTING.md000066400000000000000000000017651451514000000156100ustar00rootroot00000000000000Thank you for your interest in contributing to fsnotify! We try to review and merge PRs in a reasonable timeframe, but please be aware that: - To avoid "wasted" work, please discus changes on the issue tracker first. You can just send PRs, but they may end up being rejected for one reason or the other. - fsnotify is a cross-platform library, and changes must work reasonably well on all supported platforms. - Changes will need to be compatible; old code should still compile, and the runtime behaviour can't change in ways that are likely to lead to problems for users. Testing ------- Just `go test ./...` runs all the tests; the CI runs this on all supported platforms. Testing different platforms locally can be done with something like [goon] or [Vagrant], but this isn't super-easy to set up at the moment. Use the `-short` flag to make the "stress test" run faster. [goon]: https://github.com/arp242/goon [Vagrant]: https://www.vagrantup.com/ [integration_test.go]: /integration_test.go fsnotify-1.7.0/LICENSE000066400000000000000000000027731451514000000143640ustar00rootroot00000000000000Copyright © 2012 The Go Authors. All rights reserved. Copyright © fsnotify Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. fsnotify-1.7.0/README.md000066400000000000000000000155751451514000000146420ustar00rootroot00000000000000fsnotify is a Go library to provide cross-platform filesystem notifications on Windows, Linux, macOS, BSD, and illumos. Go 1.17 or newer is required; the full documentation is at https://pkg.go.dev/github.com/fsnotify/fsnotify --- Platform support: | Backend | OS | Status | | :-------------------- | :--------- | :------------------------------------------------------------------------ | | inotify | Linux | Supported | | kqueue | BSD, macOS | Supported | | ReadDirectoryChangesW | Windows | Supported | | FEN | illumos | Supported | | fanotify | Linux 5.9+ | [Not yet](https://github.com/fsnotify/fsnotify/issues/114) | | AHAFS | AIX | [aix branch]; experimental due to lack of maintainer and test environment | | FSEvents | macOS | [Needs support in x/sys/unix][fsevents] | | USN Journals | Windows | [Needs support in x/sys/windows][usn] | | Polling | *All* | [Not yet](https://github.com/fsnotify/fsnotify/issues/9) | Linux and illumos should include Android and Solaris, but these are currently untested. [fsevents]: https://github.com/fsnotify/fsnotify/issues/11#issuecomment-1279133120 [usn]: https://github.com/fsnotify/fsnotify/issues/53#issuecomment-1279829847 [aix branch]: https://github.com/fsnotify/fsnotify/issues/353#issuecomment-1284590129 Usage ----- A basic example: ```go package main import ( "log" "github.com/fsnotify/fsnotify" ) func main() { // Create new watcher. watcher, err := fsnotify.NewWatcher() if err != nil { log.Fatal(err) } defer watcher.Close() // Start listening for events. go func() { for { select { case event, ok := <-watcher.Events: if !ok { return } log.Println("event:", event) if event.Has(fsnotify.Write) { log.Println("modified file:", event.Name) } case err, ok := <-watcher.Errors: if !ok { return } log.Println("error:", err) } } }() // Add a path. err = watcher.Add("/tmp") if err != nil { log.Fatal(err) } // Block main goroutine forever. <-make(chan struct{}) } ``` Some more examples can be found in [cmd/fsnotify](cmd/fsnotify), which can be run with: % go run ./cmd/fsnotify Further detailed documentation can be found in godoc: https://pkg.go.dev/github.com/fsnotify/fsnotify FAQ --- ### Will a file still be watched when it's moved to another directory? No, not unless you are watching the location it was moved to. ### Are subdirectories watched? No, you must add watches for any directory you want to watch (a recursive watcher is on the roadmap: [#18]). [#18]: https://github.com/fsnotify/fsnotify/issues/18 ### Do I have to watch the Error and Event channels in a goroutine? Yes. You can read both channels in the same goroutine using `select` (you don't need a separate goroutine for both channels; see the example). ### Why don't notifications work with NFS, SMB, FUSE, /proc, or /sys? fsnotify requires support from underlying OS to work. The current NFS and SMB protocols does not provide network level support for file notifications, and neither do the /proc and /sys virtual filesystems. This could be fixed with a polling watcher ([#9]), but it's not yet implemented. [#9]: https://github.com/fsnotify/fsnotify/issues/9 ### Why do I get many Chmod events? Some programs may generate a lot of attribute changes; for example Spotlight on macOS, anti-virus programs, backup applications, and some others are known to do this. As a rule, it's typically best to ignore Chmod events. They're often not useful, and tend to cause problems. Spotlight indexing on macOS can result in multiple events (see [#15]). A temporary workaround is to add your folder(s) to the *Spotlight Privacy settings* until we have a native FSEvents implementation (see [#11]). [#11]: https://github.com/fsnotify/fsnotify/issues/11 [#15]: https://github.com/fsnotify/fsnotify/issues/15 ### Watching a file doesn't work well Watching individual files (rather than directories) is generally not recommended as many programs (especially editors) update files atomically: it will write to a temporary file which is then moved to to destination, overwriting the original (or some variant thereof). The watcher on the original file is now lost, as that no longer exists. The upshot of this is that a power failure or crash won't leave a half-written file. Watch the parent directory and use `Event.Name` to filter out files you're not interested in. There is an example of this in `cmd/fsnotify/file.go`. Platform-specific notes ----------------------- ### Linux When a file is removed a REMOVE event won't be emitted until all file descriptors are closed; it will emit a CHMOD instead: fp := os.Open("file") os.Remove("file") // CHMOD fp.Close() // REMOVE This is the event that inotify sends, so not much can be changed about this. The `fs.inotify.max_user_watches` sysctl variable specifies the upper limit for the number of watches per user, and `fs.inotify.max_user_instances` specifies the maximum number of inotify instances per user. Every Watcher you create is an "instance", and every path you add is a "watch". These are also exposed in `/proc` as `/proc/sys/fs/inotify/max_user_watches` and `/proc/sys/fs/inotify/max_user_instances` To increase them you can use `sysctl` or write the value to proc file: # The default values on Linux 5.18 sysctl fs.inotify.max_user_watches=124983 sysctl fs.inotify.max_user_instances=128 To make the changes persist on reboot edit `/etc/sysctl.conf` or `/usr/lib/sysctl.d/50-default.conf` (details differ per Linux distro; check your distro's documentation): fs.inotify.max_user_watches=124983 fs.inotify.max_user_instances=128 Reaching the limit will result in a "no space left on device" or "too many open files" error. ### kqueue (macOS, all BSD systems) kqueue requires opening a file descriptor for every file that's being watched; so if you're watching a directory with five files then that's six file descriptors. You will run in to your system's "max open files" limit faster on these platforms. The sysctl variables `kern.maxfiles` and `kern.maxfilesperproc` can be used to control the maximum number of open files. fsnotify-1.7.0/backend_fen.go000066400000000000000000000452561451514000000161300ustar00rootroot00000000000000//go:build solaris // +build solaris // Note: the documentation on the Watcher type and methods is generated from // mkdoc.zsh package fsnotify import ( "errors" "fmt" "os" "path/filepath" "sync" "golang.org/x/sys/unix" ) // Watcher watches a set of paths, delivering events on a channel. // // A watcher should not be copied (e.g. pass it by pointer, rather than by // value). // // # Linux notes // // When a file is removed a Remove event won't be emitted until all file // descriptors are closed, and deletes will always emit a Chmod. For example: // // fp := os.Open("file") // os.Remove("file") // Triggers Chmod // fp.Close() // Triggers Remove // // This is the event that inotify sends, so not much can be changed about this. // // The fs.inotify.max_user_watches sysctl variable specifies the upper limit // for the number of watches per user, and fs.inotify.max_user_instances // specifies the maximum number of inotify instances per user. Every Watcher you // create is an "instance", and every path you add is a "watch". // // These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and // /proc/sys/fs/inotify/max_user_instances // // To increase them you can use sysctl or write the value to the /proc file: // // # Default values on Linux 5.18 // sysctl fs.inotify.max_user_watches=124983 // sysctl fs.inotify.max_user_instances=128 // // To make the changes persist on reboot edit /etc/sysctl.conf or // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check // your distro's documentation): // // fs.inotify.max_user_watches=124983 // fs.inotify.max_user_instances=128 // // Reaching the limit will result in a "no space left on device" or "too many open // files" error. // // # kqueue notes (macOS, BSD) // // kqueue requires opening a file descriptor for every file that's being watched; // so if you're watching a directory with five files then that's six file // descriptors. You will run in to your system's "max open files" limit faster on // these platforms. // // The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to // control the maximum number of open files, as well as /etc/login.conf on BSD // systems. // // # Windows notes // // Paths can be added as "C:\path\to\dir", but forward slashes // ("C:/path/to/dir") will also work. // // When a watched directory is removed it will always send an event for the // directory itself, but may not send events for all files in that directory. // Sometimes it will send events for all times, sometimes it will send no // events, and often only for some files. // // The default ReadDirectoryChangesW() buffer size is 64K, which is the largest // value that is guaranteed to work with SMB filesystems. If you have many // events in quick succession this may not be enough, and you will have to use // [WithBufferSize] to increase the value. type Watcher struct { // Events sends the filesystem change events. // // fsnotify can send the following events; a "path" here can refer to a // file, directory, symbolic link, or special file like a FIFO. // // fsnotify.Create A new path was created; this may be followed by one // or more Write events if data also gets written to a // file. // // fsnotify.Remove A path was removed. // // fsnotify.Rename A path was renamed. A rename is always sent with the // old path as Event.Name, and a Create event will be // sent with the new name. Renames are only sent for // paths that are currently watched; e.g. moving an // unmonitored file into a monitored directory will // show up as just a Create. Similarly, renaming a file // to outside a monitored directory will show up as // only a Rename. // // fsnotify.Write A file or named pipe was written to. A Truncate will // also trigger a Write. A single "write action" // initiated by the user may show up as one or multiple // writes, depending on when the system syncs things to // disk. For example when compiling a large Go program // you may get hundreds of Write events, and you may // want to wait until you've stopped receiving them // (see the dedup example in cmd/fsnotify). // // Some systems may send Write event for directories // when the directory content changes. // // fsnotify.Chmod Attributes were changed. On Linux this is also sent // when a file is removed (or more accurately, when a // link to an inode is removed). On kqueue it's sent // when a file is truncated. On Windows it's never // sent. Events chan Event // Errors sends any errors. // // ErrEventOverflow is used to indicate there are too many events: // // - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl) // - windows: The buffer size is too small; WithBufferSize() can be used to increase it. // - kqueue, fen: Not used. Errors chan error mu sync.Mutex port *unix.EventPort done chan struct{} // Channel for sending a "quit message" to the reader goroutine dirs map[string]struct{} // Explicitly watched directories watches map[string]struct{} // Explicitly watched non-directories } // NewWatcher creates a new Watcher. func NewWatcher() (*Watcher, error) { return NewBufferedWatcher(0) } // NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events // channel. // // The main use case for this is situations with a very large number of events // where the kernel buffer size can't be increased (e.g. due to lack of // permissions). An unbuffered Watcher will perform better for almost all use // cases, and whenever possible you will be better off increasing the kernel // buffers instead of adding a large userspace buffer. func NewBufferedWatcher(sz uint) (*Watcher, error) { w := &Watcher{ Events: make(chan Event, sz), Errors: make(chan error), dirs: make(map[string]struct{}), watches: make(map[string]struct{}), done: make(chan struct{}), } var err error w.port, err = unix.NewEventPort() if err != nil { return nil, fmt.Errorf("fsnotify.NewWatcher: %w", err) } go w.readEvents() return w, nil } // sendEvent attempts to send an event to the user, returning true if the event // was put in the channel successfully and false if the watcher has been closed. func (w *Watcher) sendEvent(name string, op Op) (sent bool) { select { case w.Events <- Event{Name: name, Op: op}: return true case <-w.done: return false } } // sendError attempts to send an error to the user, returning true if the error // was put in the channel successfully and false if the watcher has been closed. func (w *Watcher) sendError(err error) (sent bool) { select { case w.Errors <- err: return true case <-w.done: return false } } func (w *Watcher) isClosed() bool { select { case <-w.done: return true default: return false } } // Close removes all watches and closes the Events channel. func (w *Watcher) Close() error { // Take the lock used by associateFile to prevent lingering events from // being processed after the close w.mu.Lock() defer w.mu.Unlock() if w.isClosed() { return nil } close(w.done) return w.port.Close() } // Add starts monitoring the path for changes. // // A path can only be watched once; watching it more than once is a no-op and will // not return an error. Paths that do not yet exist on the filesystem cannot be // watched. // // A watch will be automatically removed if the watched path is deleted or // renamed. The exception is the Windows backend, which doesn't remove the // watcher on renames. // // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // filesystems (/proc, /sys, etc.) generally don't work. // // Returns [ErrClosed] if [Watcher.Close] was called. // // See [Watcher.AddWith] for a version that allows adding options. // // # Watching directories // // All files in a directory are monitored, including new files that are created // after the watcher is started. Subdirectories are not watched (i.e. it's // non-recursive). // // # Watching files // // Watching individual files (rather than directories) is generally not // recommended as many programs (especially editors) update files atomically: it // will write to a temporary file which is then moved to to destination, // overwriting the original (or some variant thereof). The watcher on the // original file is now lost, as that no longer exists. // // The upshot of this is that a power failure or crash won't leave a // half-written file. // // Watch the parent directory and use Event.Name to filter out files you're not // interested in. There is an example of this in cmd/fsnotify/file.go. func (w *Watcher) Add(name string) error { return w.AddWith(name) } // AddWith is like [Watcher.Add], but allows adding options. When using Add() // the defaults described below are used. // // Possible options are: // // - [WithBufferSize] sets the buffer size for the Windows backend; no-op on // other platforms. The default is 64K (65536 bytes). func (w *Watcher) AddWith(name string, opts ...addOpt) error { if w.isClosed() { return ErrClosed } if w.port.PathIsWatched(name) { return nil } _ = getOptions(opts...) // Currently we resolve symlinks that were explicitly requested to be // watched. Otherwise we would use LStat here. stat, err := os.Stat(name) if err != nil { return err } // Associate all files in the directory. if stat.IsDir() { err := w.handleDirectory(name, stat, true, w.associateFile) if err != nil { return err } w.mu.Lock() w.dirs[name] = struct{}{} w.mu.Unlock() return nil } err = w.associateFile(name, stat, true) if err != nil { return err } w.mu.Lock() w.watches[name] = struct{}{} w.mu.Unlock() return nil } // Remove stops monitoring the path for changes. // // Directories are always removed non-recursively. For example, if you added // /tmp/dir and /tmp/dir/subdir then you will need to remove both. // // Removing a path that has not yet been added returns [ErrNonExistentWatch]. // // Returns nil if [Watcher.Close] was called. func (w *Watcher) Remove(name string) error { if w.isClosed() { return nil } if !w.port.PathIsWatched(name) { return fmt.Errorf("%w: %s", ErrNonExistentWatch, name) } // The user has expressed an intent. Immediately remove this name from // whichever watch list it might be in. If it's not in there the delete // doesn't cause harm. w.mu.Lock() delete(w.watches, name) delete(w.dirs, name) w.mu.Unlock() stat, err := os.Stat(name) if err != nil { return err } // Remove associations for every file in the directory. if stat.IsDir() { err := w.handleDirectory(name, stat, false, w.dissociateFile) if err != nil { return err } return nil } err = w.port.DissociatePath(name) if err != nil { return err } return nil } // readEvents contains the main loop that runs in a goroutine watching for events. func (w *Watcher) readEvents() { // If this function returns, the watcher has been closed and we can close // these channels defer func() { close(w.Errors) close(w.Events) }() pevents := make([]unix.PortEvent, 8) for { count, err := w.port.Get(pevents, 1, nil) if err != nil && err != unix.ETIME { // Interrupted system call (count should be 0) ignore and continue if errors.Is(err, unix.EINTR) && count == 0 { continue } // Get failed because we called w.Close() if errors.Is(err, unix.EBADF) && w.isClosed() { return } // There was an error not caused by calling w.Close() if !w.sendError(err) { return } } p := pevents[:count] for _, pevent := range p { if pevent.Source != unix.PORT_SOURCE_FILE { // Event from unexpected source received; should never happen. if !w.sendError(errors.New("Event from unexpected source received")) { return } continue } err = w.handleEvent(&pevent) if err != nil { if !w.sendError(err) { return } } } } } func (w *Watcher) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error { files, err := os.ReadDir(path) if err != nil { return err } // Handle all children of the directory. for _, entry := range files { finfo, err := entry.Info() if err != nil { return err } err = handler(filepath.Join(path, finfo.Name()), finfo, false) if err != nil { return err } } // And finally handle the directory itself. return handler(path, stat, follow) } // handleEvent might need to emit more than one fsnotify event if the events // bitmap matches more than one event type (e.g. the file was both modified and // had the attributes changed between when the association was created and the // when event was returned) func (w *Watcher) handleEvent(event *unix.PortEvent) error { var ( events = event.Events path = event.Path fmode = event.Cookie.(os.FileMode) reRegister = true ) w.mu.Lock() _, watchedDir := w.dirs[path] _, watchedPath := w.watches[path] w.mu.Unlock() isWatched := watchedDir || watchedPath if events&unix.FILE_DELETE != 0 { if !w.sendEvent(path, Remove) { return nil } reRegister = false } if events&unix.FILE_RENAME_FROM != 0 { if !w.sendEvent(path, Rename) { return nil } // Don't keep watching the new file name reRegister = false } if events&unix.FILE_RENAME_TO != 0 { // We don't report a Rename event for this case, because Rename events // are interpreted as referring to the _old_ name of the file, and in // this case the event would refer to the new name of the file. This // type of rename event is not supported by fsnotify. // inotify reports a Remove event in this case, so we simulate this // here. if !w.sendEvent(path, Remove) { return nil } // Don't keep watching the file that was removed reRegister = false } // The file is gone, nothing left to do. if !reRegister { if watchedDir { w.mu.Lock() delete(w.dirs, path) w.mu.Unlock() } if watchedPath { w.mu.Lock() delete(w.watches, path) w.mu.Unlock() } return nil } // If we didn't get a deletion the file still exists and we're going to have // to watch it again. Let's Stat it now so that we can compare permissions // and have what we need to continue watching the file stat, err := os.Lstat(path) if err != nil { // This is unexpected, but we should still emit an event. This happens // most often on "rm -r" of a subdirectory inside a watched directory We // get a modify event of something happening inside, but by the time we // get here, the sudirectory is already gone. Clearly we were watching // this path but now it is gone. Let's tell the user that it was // removed. if !w.sendEvent(path, Remove) { return nil } // Suppress extra write events on removed directories; they are not // informative and can be confusing. return nil } // resolve symlinks that were explicitly watched as we would have at Add() // time. this helps suppress spurious Chmod events on watched symlinks if isWatched { stat, err = os.Stat(path) if err != nil { // The symlink still exists, but the target is gone. Report the // Remove similar to above. if !w.sendEvent(path, Remove) { return nil } // Don't return the error } } if events&unix.FILE_MODIFIED != 0 { if fmode.IsDir() { if watchedDir { if err := w.updateDirectory(path); err != nil { return err } } else { if !w.sendEvent(path, Write) { return nil } } } else { if !w.sendEvent(path, Write) { return nil } } } if events&unix.FILE_ATTRIB != 0 && stat != nil { // Only send Chmod if perms changed if stat.Mode().Perm() != fmode.Perm() { if !w.sendEvent(path, Chmod) { return nil } } } if stat != nil { // If we get here, it means we've hit an event above that requires us to // continue watching the file or directory return w.associateFile(path, stat, isWatched) } return nil } func (w *Watcher) updateDirectory(path string) error { // The directory was modified, so we must find unwatched entities and watch // them. If something was removed from the directory, nothing will happen, // as everything else should still be watched. files, err := os.ReadDir(path) if err != nil { return err } for _, entry := range files { path := filepath.Join(path, entry.Name()) if w.port.PathIsWatched(path) { continue } finfo, err := entry.Info() if err != nil { return err } err = w.associateFile(path, finfo, false) if err != nil { if !w.sendError(err) { return nil } } if !w.sendEvent(path, Create) { return nil } } return nil } func (w *Watcher) associateFile(path string, stat os.FileInfo, follow bool) error { if w.isClosed() { return ErrClosed } // This is primarily protecting the call to AssociatePath but it is // important and intentional that the call to PathIsWatched is also // protected by this mutex. Without this mutex, AssociatePath has been seen // to error out that the path is already associated. w.mu.Lock() defer w.mu.Unlock() if w.port.PathIsWatched(path) { // Remove the old association in favor of this one If we get ENOENT, // then while the x/sys/unix wrapper still thought that this path was // associated, the underlying event port did not. This call will have // cleared up that discrepancy. The most likely cause is that the event // has fired but we haven't processed it yet. err := w.port.DissociatePath(path) if err != nil && err != unix.ENOENT { return err } } // FILE_NOFOLLOW means we watch symlinks themselves rather than their // targets. events := unix.FILE_MODIFIED | unix.FILE_ATTRIB | unix.FILE_NOFOLLOW if follow { // We *DO* follow symlinks for explicitly watched entries. events = unix.FILE_MODIFIED | unix.FILE_ATTRIB } return w.port.AssociatePath(path, stat, events, stat.Mode()) } func (w *Watcher) dissociateFile(path string, stat os.FileInfo, unused bool) error { if !w.port.PathIsWatched(path) { return nil } return w.port.DissociatePath(path) } // WatchList returns all paths explicitly added with [Watcher.Add] (and are not // yet removed). // // Returns nil if [Watcher.Close] was called. func (w *Watcher) WatchList() []string { if w.isClosed() { return nil } w.mu.Lock() defer w.mu.Unlock() entries := make([]string, 0, len(w.watches)+len(w.dirs)) for pathname := range w.dirs { entries = append(entries, pathname) } for pathname := range w.watches { entries = append(entries, pathname) } return entries } fsnotify-1.7.0/backend_fen_test.go000066400000000000000000000020721451514000000171540ustar00rootroot00000000000000//go:build solaris // +build solaris package fsnotify import ( "fmt" "strings" "testing" ) func TestRemoveState(t *testing.T) { var ( tmp = t.TempDir() dir = join(tmp, "dir") file = join(dir, "file") ) mkdir(t, dir) touch(t, file) w := newWatcher(t, tmp) addWatch(t, w, tmp) addWatch(t, w, file) check := func(wantDirs, wantFiles int) { t.Helper() if len(w.watches) != wantFiles { var d []string for k, v := range w.watches { d = append(d, fmt.Sprintf("%#v = %#v", k, v)) } t.Errorf("unexpected number of entries in w.watches (have %d, want %d):\n%v", len(w.watches), wantFiles, strings.Join(d, "\n")) } if len(w.dirs) != wantDirs { var d []string for k, v := range w.dirs { d = append(d, fmt.Sprintf("%#v = %#v", k, v)) } t.Errorf("unexpected number of entries in w.dirs (have %d, want %d):\n%v", len(w.dirs), wantDirs, strings.Join(d, "\n")) } } check(1, 1) if err := w.Remove(file); err != nil { t.Fatal(err) } check(1, 0) if err := w.Remove(tmp); err != nil { t.Fatal(err) } check(0, 0) } fsnotify-1.7.0/backend_inotify.go000066400000000000000000000422211451514000000170260ustar00rootroot00000000000000//go:build linux && !appengine // +build linux,!appengine // Note: the documentation on the Watcher type and methods is generated from // mkdoc.zsh package fsnotify import ( "errors" "fmt" "io" "os" "path/filepath" "strings" "sync" "unsafe" "golang.org/x/sys/unix" ) // Watcher watches a set of paths, delivering events on a channel. // // A watcher should not be copied (e.g. pass it by pointer, rather than by // value). // // # Linux notes // // When a file is removed a Remove event won't be emitted until all file // descriptors are closed, and deletes will always emit a Chmod. For example: // // fp := os.Open("file") // os.Remove("file") // Triggers Chmod // fp.Close() // Triggers Remove // // This is the event that inotify sends, so not much can be changed about this. // // The fs.inotify.max_user_watches sysctl variable specifies the upper limit // for the number of watches per user, and fs.inotify.max_user_instances // specifies the maximum number of inotify instances per user. Every Watcher you // create is an "instance", and every path you add is a "watch". // // These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and // /proc/sys/fs/inotify/max_user_instances // // To increase them you can use sysctl or write the value to the /proc file: // // # Default values on Linux 5.18 // sysctl fs.inotify.max_user_watches=124983 // sysctl fs.inotify.max_user_instances=128 // // To make the changes persist on reboot edit /etc/sysctl.conf or // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check // your distro's documentation): // // fs.inotify.max_user_watches=124983 // fs.inotify.max_user_instances=128 // // Reaching the limit will result in a "no space left on device" or "too many open // files" error. // // # kqueue notes (macOS, BSD) // // kqueue requires opening a file descriptor for every file that's being watched; // so if you're watching a directory with five files then that's six file // descriptors. You will run in to your system's "max open files" limit faster on // these platforms. // // The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to // control the maximum number of open files, as well as /etc/login.conf on BSD // systems. // // # Windows notes // // Paths can be added as "C:\path\to\dir", but forward slashes // ("C:/path/to/dir") will also work. // // When a watched directory is removed it will always send an event for the // directory itself, but may not send events for all files in that directory. // Sometimes it will send events for all times, sometimes it will send no // events, and often only for some files. // // The default ReadDirectoryChangesW() buffer size is 64K, which is the largest // value that is guaranteed to work with SMB filesystems. If you have many // events in quick succession this may not be enough, and you will have to use // [WithBufferSize] to increase the value. type Watcher struct { // Events sends the filesystem change events. // // fsnotify can send the following events; a "path" here can refer to a // file, directory, symbolic link, or special file like a FIFO. // // fsnotify.Create A new path was created; this may be followed by one // or more Write events if data also gets written to a // file. // // fsnotify.Remove A path was removed. // // fsnotify.Rename A path was renamed. A rename is always sent with the // old path as Event.Name, and a Create event will be // sent with the new name. Renames are only sent for // paths that are currently watched; e.g. moving an // unmonitored file into a monitored directory will // show up as just a Create. Similarly, renaming a file // to outside a monitored directory will show up as // only a Rename. // // fsnotify.Write A file or named pipe was written to. A Truncate will // also trigger a Write. A single "write action" // initiated by the user may show up as one or multiple // writes, depending on when the system syncs things to // disk. For example when compiling a large Go program // you may get hundreds of Write events, and you may // want to wait until you've stopped receiving them // (see the dedup example in cmd/fsnotify). // // Some systems may send Write event for directories // when the directory content changes. // // fsnotify.Chmod Attributes were changed. On Linux this is also sent // when a file is removed (or more accurately, when a // link to an inode is removed). On kqueue it's sent // when a file is truncated. On Windows it's never // sent. Events chan Event // Errors sends any errors. // // ErrEventOverflow is used to indicate there are too many events: // // - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl) // - windows: The buffer size is too small; WithBufferSize() can be used to increase it. // - kqueue, fen: Not used. Errors chan error // Store fd here as os.File.Read() will no longer return on close after // calling Fd(). See: https://github.com/golang/go/issues/26439 fd int inotifyFile *os.File watches *watches done chan struct{} // Channel for sending a "quit message" to the reader goroutine closeMu sync.Mutex doneResp chan struct{} // Channel to respond to Close } type ( watches struct { mu sync.RWMutex wd map[uint32]*watch // wd → watch path map[string]uint32 // pathname → wd } watch struct { wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall) flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags) path string // Watch path. } ) func newWatches() *watches { return &watches{ wd: make(map[uint32]*watch), path: make(map[string]uint32), } } func (w *watches) len() int { w.mu.RLock() defer w.mu.RUnlock() return len(w.wd) } func (w *watches) add(ww *watch) { w.mu.Lock() defer w.mu.Unlock() w.wd[ww.wd] = ww w.path[ww.path] = ww.wd } func (w *watches) remove(wd uint32) { w.mu.Lock() defer w.mu.Unlock() delete(w.path, w.wd[wd].path) delete(w.wd, wd) } func (w *watches) removePath(path string) (uint32, bool) { w.mu.Lock() defer w.mu.Unlock() wd, ok := w.path[path] if !ok { return 0, false } delete(w.path, path) delete(w.wd, wd) return wd, true } func (w *watches) byPath(path string) *watch { w.mu.RLock() defer w.mu.RUnlock() return w.wd[w.path[path]] } func (w *watches) byWd(wd uint32) *watch { w.mu.RLock() defer w.mu.RUnlock() return w.wd[wd] } func (w *watches) updatePath(path string, f func(*watch) (*watch, error)) error { w.mu.Lock() defer w.mu.Unlock() var existing *watch wd, ok := w.path[path] if ok { existing = w.wd[wd] } upd, err := f(existing) if err != nil { return err } if upd != nil { w.wd[upd.wd] = upd w.path[upd.path] = upd.wd if upd.wd != wd { delete(w.wd, wd) } } return nil } // NewWatcher creates a new Watcher. func NewWatcher() (*Watcher, error) { return NewBufferedWatcher(0) } // NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events // channel. // // The main use case for this is situations with a very large number of events // where the kernel buffer size can't be increased (e.g. due to lack of // permissions). An unbuffered Watcher will perform better for almost all use // cases, and whenever possible you will be better off increasing the kernel // buffers instead of adding a large userspace buffer. func NewBufferedWatcher(sz uint) (*Watcher, error) { // Need to set nonblocking mode for SetDeadline to work, otherwise blocking // I/O operations won't terminate on close. fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK) if fd == -1 { return nil, errno } w := &Watcher{ fd: fd, inotifyFile: os.NewFile(uintptr(fd), ""), watches: newWatches(), Events: make(chan Event, sz), Errors: make(chan error), done: make(chan struct{}), doneResp: make(chan struct{}), } go w.readEvents() return w, nil } // Returns true if the event was sent, or false if watcher is closed. func (w *Watcher) sendEvent(e Event) bool { select { case w.Events <- e: return true case <-w.done: return false } } // Returns true if the error was sent, or false if watcher is closed. func (w *Watcher) sendError(err error) bool { select { case w.Errors <- err: return true case <-w.done: return false } } func (w *Watcher) isClosed() bool { select { case <-w.done: return true default: return false } } // Close removes all watches and closes the Events channel. func (w *Watcher) Close() error { w.closeMu.Lock() if w.isClosed() { w.closeMu.Unlock() return nil } close(w.done) w.closeMu.Unlock() // Causes any blocking reads to return with an error, provided the file // still supports deadline operations. err := w.inotifyFile.Close() if err != nil { return err } // Wait for goroutine to close <-w.doneResp return nil } // Add starts monitoring the path for changes. // // A path can only be watched once; watching it more than once is a no-op and will // not return an error. Paths that do not yet exist on the filesystem cannot be // watched. // // A watch will be automatically removed if the watched path is deleted or // renamed. The exception is the Windows backend, which doesn't remove the // watcher on renames. // // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // filesystems (/proc, /sys, etc.) generally don't work. // // Returns [ErrClosed] if [Watcher.Close] was called. // // See [Watcher.AddWith] for a version that allows adding options. // // # Watching directories // // All files in a directory are monitored, including new files that are created // after the watcher is started. Subdirectories are not watched (i.e. it's // non-recursive). // // # Watching files // // Watching individual files (rather than directories) is generally not // recommended as many programs (especially editors) update files atomically: it // will write to a temporary file which is then moved to to destination, // overwriting the original (or some variant thereof). The watcher on the // original file is now lost, as that no longer exists. // // The upshot of this is that a power failure or crash won't leave a // half-written file. // // Watch the parent directory and use Event.Name to filter out files you're not // interested in. There is an example of this in cmd/fsnotify/file.go. func (w *Watcher) Add(name string) error { return w.AddWith(name) } // AddWith is like [Watcher.Add], but allows adding options. When using Add() // the defaults described below are used. // // Possible options are: // // - [WithBufferSize] sets the buffer size for the Windows backend; no-op on // other platforms. The default is 64K (65536 bytes). func (w *Watcher) AddWith(name string, opts ...addOpt) error { if w.isClosed() { return ErrClosed } name = filepath.Clean(name) _ = getOptions(opts...) var flags uint32 = unix.IN_MOVED_TO | unix.IN_MOVED_FROM | unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY | unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF return w.watches.updatePath(name, func(existing *watch) (*watch, error) { if existing != nil { flags |= existing.flags | unix.IN_MASK_ADD } wd, err := unix.InotifyAddWatch(w.fd, name, flags) if wd == -1 { return nil, err } if existing == nil { return &watch{ wd: uint32(wd), path: name, flags: flags, }, nil } existing.wd = uint32(wd) existing.flags = flags return existing, nil }) } // Remove stops monitoring the path for changes. // // Directories are always removed non-recursively. For example, if you added // /tmp/dir and /tmp/dir/subdir then you will need to remove both. // // Removing a path that has not yet been added returns [ErrNonExistentWatch]. // // Returns nil if [Watcher.Close] was called. func (w *Watcher) Remove(name string) error { if w.isClosed() { return nil } return w.remove(filepath.Clean(name)) } func (w *Watcher) remove(name string) error { wd, ok := w.watches.removePath(name) if !ok { return fmt.Errorf("%w: %s", ErrNonExistentWatch, name) } success, errno := unix.InotifyRmWatch(w.fd, wd) if success == -1 { // TODO: Perhaps it's not helpful to return an error here in every case; // The only two possible errors are: // // - EBADF, which happens when w.fd is not a valid file descriptor // of any kind. // - EINVAL, which is when fd is not an inotify descriptor or wd // is not a valid watch descriptor. Watch descriptors are // invalidated when they are removed explicitly or implicitly; // explicitly by inotify_rm_watch, implicitly when the file they // are watching is deleted. return errno } return nil } // WatchList returns all paths explicitly added with [Watcher.Add] (and are not // yet removed). // // Returns nil if [Watcher.Close] was called. func (w *Watcher) WatchList() []string { if w.isClosed() { return nil } entries := make([]string, 0, w.watches.len()) w.watches.mu.RLock() for pathname := range w.watches.path { entries = append(entries, pathname) } w.watches.mu.RUnlock() return entries } // readEvents reads from the inotify file descriptor, converts the // received events into Event objects and sends them via the Events channel func (w *Watcher) readEvents() { defer func() { close(w.doneResp) close(w.Errors) close(w.Events) }() var ( buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events errno error // Syscall errno ) for { // See if we have been closed. if w.isClosed() { return } n, err := w.inotifyFile.Read(buf[:]) switch { case errors.Unwrap(err) == os.ErrClosed: return case err != nil: if !w.sendError(err) { return } continue } if n < unix.SizeofInotifyEvent { var err error if n == 0 { err = io.EOF // If EOF is received. This should really never happen. } else if n < 0 { err = errno // If an error occurred while reading. } else { err = errors.New("notify: short read in readEvents()") // Read was too short. } if !w.sendError(err) { return } continue } var offset uint32 // We don't know how many events we just read into the buffer // While the offset points to at least one whole event... for offset <= uint32(n-unix.SizeofInotifyEvent) { var ( // Point "raw" to the event in the buffer raw = (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset])) mask = uint32(raw.Mask) nameLen = uint32(raw.Len) ) if mask&unix.IN_Q_OVERFLOW != 0 { if !w.sendError(ErrEventOverflow) { return } } // If the event happened to the watched directory or the watched file, the kernel // doesn't append the filename to the event, but we would like to always fill the // the "Name" field with a valid filename. We retrieve the path of the watch from // the "paths" map. watch := w.watches.byWd(uint32(raw.Wd)) // inotify will automatically remove the watch on deletes; just need // to clean our state here. if watch != nil && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF { w.watches.remove(watch.wd) } // We can't really update the state when a watched path is moved; // only IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove // the watch. if watch != nil && mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF { err := w.remove(watch.path) if err != nil && !errors.Is(err, ErrNonExistentWatch) { if !w.sendError(err) { return } } } var name string if watch != nil { name = watch.path } if nameLen > 0 { // Point "bytes" at the first byte of the filename bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen] // The filename is padded with NULL bytes. TrimRight() gets rid of those. name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000") } event := w.newEvent(name, mask) // Send the events that are not ignored on the events channel if mask&unix.IN_IGNORED == 0 { if !w.sendEvent(event) { return } } // Move to the next event in the buffer offset += unix.SizeofInotifyEvent + nameLen } } } // newEvent returns an platform-independent Event based on an inotify mask. func (w *Watcher) newEvent(name string, mask uint32) Event { e := Event{Name: name} if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO { e.Op |= Create } if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF || mask&unix.IN_DELETE == unix.IN_DELETE { e.Op |= Remove } if mask&unix.IN_MODIFY == unix.IN_MODIFY { e.Op |= Write } if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM { e.Op |= Rename } if mask&unix.IN_ATTRIB == unix.IN_ATTRIB { e.Op |= Chmod } return e } fsnotify-1.7.0/backend_inotify_test.go000066400000000000000000000047241451514000000200730ustar00rootroot00000000000000//go:build linux // +build linux package fsnotify import ( "errors" "os" "strconv" "strings" "sync" "testing" "time" ) // Ensure that the correct error is returned on overflows. func TestInotifyOverflow(t *testing.T) { t.Parallel() tmp := t.TempDir() w := newWatcher(t) defer w.Close() // We need to generate many more events than the // fs.inotify.max_queued_events sysctl setting. numDirs, numFiles := 128, 1024 // All events need to be in the inotify queue before pulling events off it // to trigger this error. var wg sync.WaitGroup for i := 0; i < numDirs; i++ { wg.Add(1) go func(i int) { defer wg.Done() dir := join(tmp, strconv.Itoa(i)) mkdir(t, dir, noWait) addWatch(t, w, dir) createFiles(t, dir, "", numFiles, 10*time.Second) }(i) } wg.Wait() var ( creates = 0 overflows = 0 ) for overflows == 0 && creates < numDirs*numFiles { select { case <-time.After(10 * time.Second): t.Fatalf("Not done") case err := <-w.Errors: if !errors.Is(err, ErrEventOverflow) { t.Fatalf("unexpected error from watcher: %v", err) } overflows++ case e := <-w.Events: if !strings.HasPrefix(e.Name, tmp) { t.Fatalf("Event for unknown file: %s", e.Name) } if e.Op == Create { creates++ } } } if creates == numDirs*numFiles { t.Fatalf("could not trigger overflow") } if overflows == 0 { t.Fatalf("no overflow and not enough CREATE events (expected %d, got %d)", numDirs*numFiles, creates) } } // Test inotify's "we don't send REMOVE until all file descriptors are removed" // behaviour. func TestInotifyDeleteOpenFile(t *testing.T) { t.Parallel() tmp := t.TempDir() file := join(tmp, "file") touch(t, file) fp, err := os.Open(file) if err != nil { t.Fatalf("Create failed: %v", err) } defer fp.Close() w := newCollector(t, file) w.collect(t) rm(t, file) waitForEvents() e := w.events(t) cmpEvents(t, tmp, e, newEvents(t, `chmod /file`)) fp.Close() e = w.stop(t) cmpEvents(t, tmp, e, newEvents(t, `remove /file`)) } func TestRemoveState(t *testing.T) { var ( tmp = t.TempDir() dir = join(tmp, "dir") file = join(dir, "file") ) mkdir(t, dir) touch(t, file) w := newWatcher(t, tmp) addWatch(t, w, tmp) addWatch(t, w, file) check := func(want int) { t.Helper() if w.watches.len() != want { t.Error(w.watches) } } check(2) if err := w.Remove(file); err != nil { t.Fatal(err) } check(1) if err := w.Remove(tmp); err != nil { t.Fatal(err) } check(0) } fsnotify-1.7.0/backend_kqueue.go000066400000000000000000000555011451514000000166510ustar00rootroot00000000000000//go:build freebsd || openbsd || netbsd || dragonfly || darwin // +build freebsd openbsd netbsd dragonfly darwin // Note: the documentation on the Watcher type and methods is generated from // mkdoc.zsh package fsnotify import ( "errors" "fmt" "os" "path/filepath" "sync" "golang.org/x/sys/unix" ) // Watcher watches a set of paths, delivering events on a channel. // // A watcher should not be copied (e.g. pass it by pointer, rather than by // value). // // # Linux notes // // When a file is removed a Remove event won't be emitted until all file // descriptors are closed, and deletes will always emit a Chmod. For example: // // fp := os.Open("file") // os.Remove("file") // Triggers Chmod // fp.Close() // Triggers Remove // // This is the event that inotify sends, so not much can be changed about this. // // The fs.inotify.max_user_watches sysctl variable specifies the upper limit // for the number of watches per user, and fs.inotify.max_user_instances // specifies the maximum number of inotify instances per user. Every Watcher you // create is an "instance", and every path you add is a "watch". // // These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and // /proc/sys/fs/inotify/max_user_instances // // To increase them you can use sysctl or write the value to the /proc file: // // # Default values on Linux 5.18 // sysctl fs.inotify.max_user_watches=124983 // sysctl fs.inotify.max_user_instances=128 // // To make the changes persist on reboot edit /etc/sysctl.conf or // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check // your distro's documentation): // // fs.inotify.max_user_watches=124983 // fs.inotify.max_user_instances=128 // // Reaching the limit will result in a "no space left on device" or "too many open // files" error. // // # kqueue notes (macOS, BSD) // // kqueue requires opening a file descriptor for every file that's being watched; // so if you're watching a directory with five files then that's six file // descriptors. You will run in to your system's "max open files" limit faster on // these platforms. // // The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to // control the maximum number of open files, as well as /etc/login.conf on BSD // systems. // // # Windows notes // // Paths can be added as "C:\path\to\dir", but forward slashes // ("C:/path/to/dir") will also work. // // When a watched directory is removed it will always send an event for the // directory itself, but may not send events for all files in that directory. // Sometimes it will send events for all times, sometimes it will send no // events, and often only for some files. // // The default ReadDirectoryChangesW() buffer size is 64K, which is the largest // value that is guaranteed to work with SMB filesystems. If you have many // events in quick succession this may not be enough, and you will have to use // [WithBufferSize] to increase the value. type Watcher struct { // Events sends the filesystem change events. // // fsnotify can send the following events; a "path" here can refer to a // file, directory, symbolic link, or special file like a FIFO. // // fsnotify.Create A new path was created; this may be followed by one // or more Write events if data also gets written to a // file. // // fsnotify.Remove A path was removed. // // fsnotify.Rename A path was renamed. A rename is always sent with the // old path as Event.Name, and a Create event will be // sent with the new name. Renames are only sent for // paths that are currently watched; e.g. moving an // unmonitored file into a monitored directory will // show up as just a Create. Similarly, renaming a file // to outside a monitored directory will show up as // only a Rename. // // fsnotify.Write A file or named pipe was written to. A Truncate will // also trigger a Write. A single "write action" // initiated by the user may show up as one or multiple // writes, depending on when the system syncs things to // disk. For example when compiling a large Go program // you may get hundreds of Write events, and you may // want to wait until you've stopped receiving them // (see the dedup example in cmd/fsnotify). // // Some systems may send Write event for directories // when the directory content changes. // // fsnotify.Chmod Attributes were changed. On Linux this is also sent // when a file is removed (or more accurately, when a // link to an inode is removed). On kqueue it's sent // when a file is truncated. On Windows it's never // sent. Events chan Event // Errors sends any errors. // // ErrEventOverflow is used to indicate there are too many events: // // - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl) // - windows: The buffer size is too small; WithBufferSize() can be used to increase it. // - kqueue, fen: Not used. Errors chan error done chan struct{} kq int // File descriptor (as returned by the kqueue() syscall). closepipe [2]int // Pipe used for closing. mu sync.Mutex // Protects access to watcher data watches map[string]int // Watched file descriptors (key: path). watchesByDir map[string]map[int]struct{} // Watched file descriptors indexed by the parent directory (key: dirname(path)). userWatches map[string]struct{} // Watches added with Watcher.Add() dirFlags map[string]uint32 // Watched directories to fflags used in kqueue. paths map[int]pathInfo // File descriptors to path names for processing kqueue events. fileExists map[string]struct{} // Keep track of if we know this file exists (to stop duplicate create events). isClosed bool // Set to true when Close() is first called } type pathInfo struct { name string isDir bool } // NewWatcher creates a new Watcher. func NewWatcher() (*Watcher, error) { return NewBufferedWatcher(0) } // NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events // channel. // // The main use case for this is situations with a very large number of events // where the kernel buffer size can't be increased (e.g. due to lack of // permissions). An unbuffered Watcher will perform better for almost all use // cases, and whenever possible you will be better off increasing the kernel // buffers instead of adding a large userspace buffer. func NewBufferedWatcher(sz uint) (*Watcher, error) { kq, closepipe, err := newKqueue() if err != nil { return nil, err } w := &Watcher{ kq: kq, closepipe: closepipe, watches: make(map[string]int), watchesByDir: make(map[string]map[int]struct{}), dirFlags: make(map[string]uint32), paths: make(map[int]pathInfo), fileExists: make(map[string]struct{}), userWatches: make(map[string]struct{}), Events: make(chan Event, sz), Errors: make(chan error), done: make(chan struct{}), } go w.readEvents() return w, nil } // newKqueue creates a new kernel event queue and returns a descriptor. // // This registers a new event on closepipe, which will trigger an event when // it's closed. This way we can use kevent() without timeout/polling; without // the closepipe, it would block forever and we wouldn't be able to stop it at // all. func newKqueue() (kq int, closepipe [2]int, err error) { kq, err = unix.Kqueue() if kq == -1 { return kq, closepipe, err } // Register the close pipe. err = unix.Pipe(closepipe[:]) if err != nil { unix.Close(kq) return kq, closepipe, err } // Register changes to listen on the closepipe. changes := make([]unix.Kevent_t, 1) // SetKevent converts int to the platform-specific types. unix.SetKevent(&changes[0], closepipe[0], unix.EVFILT_READ, unix.EV_ADD|unix.EV_ENABLE|unix.EV_ONESHOT) ok, err := unix.Kevent(kq, changes, nil, nil) if ok == -1 { unix.Close(kq) unix.Close(closepipe[0]) unix.Close(closepipe[1]) return kq, closepipe, err } return kq, closepipe, nil } // Returns true if the event was sent, or false if watcher is closed. func (w *Watcher) sendEvent(e Event) bool { select { case w.Events <- e: return true case <-w.done: return false } } // Returns true if the error was sent, or false if watcher is closed. func (w *Watcher) sendError(err error) bool { select { case w.Errors <- err: return true case <-w.done: return false } } // Close removes all watches and closes the Events channel. func (w *Watcher) Close() error { w.mu.Lock() if w.isClosed { w.mu.Unlock() return nil } w.isClosed = true // copy paths to remove while locked pathsToRemove := make([]string, 0, len(w.watches)) for name := range w.watches { pathsToRemove = append(pathsToRemove, name) } w.mu.Unlock() // Unlock before calling Remove, which also locks for _, name := range pathsToRemove { w.Remove(name) } // Send "quit" message to the reader goroutine. unix.Close(w.closepipe[1]) close(w.done) return nil } // Add starts monitoring the path for changes. // // A path can only be watched once; watching it more than once is a no-op and will // not return an error. Paths that do not yet exist on the filesystem cannot be // watched. // // A watch will be automatically removed if the watched path is deleted or // renamed. The exception is the Windows backend, which doesn't remove the // watcher on renames. // // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // filesystems (/proc, /sys, etc.) generally don't work. // // Returns [ErrClosed] if [Watcher.Close] was called. // // See [Watcher.AddWith] for a version that allows adding options. // // # Watching directories // // All files in a directory are monitored, including new files that are created // after the watcher is started. Subdirectories are not watched (i.e. it's // non-recursive). // // # Watching files // // Watching individual files (rather than directories) is generally not // recommended as many programs (especially editors) update files atomically: it // will write to a temporary file which is then moved to to destination, // overwriting the original (or some variant thereof). The watcher on the // original file is now lost, as that no longer exists. // // The upshot of this is that a power failure or crash won't leave a // half-written file. // // Watch the parent directory and use Event.Name to filter out files you're not // interested in. There is an example of this in cmd/fsnotify/file.go. func (w *Watcher) Add(name string) error { return w.AddWith(name) } // AddWith is like [Watcher.Add], but allows adding options. When using Add() // the defaults described below are used. // // Possible options are: // // - [WithBufferSize] sets the buffer size for the Windows backend; no-op on // other platforms. The default is 64K (65536 bytes). func (w *Watcher) AddWith(name string, opts ...addOpt) error { _ = getOptions(opts...) w.mu.Lock() w.userWatches[name] = struct{}{} w.mu.Unlock() _, err := w.addWatch(name, noteAllEvents) return err } // Remove stops monitoring the path for changes. // // Directories are always removed non-recursively. For example, if you added // /tmp/dir and /tmp/dir/subdir then you will need to remove both. // // Removing a path that has not yet been added returns [ErrNonExistentWatch]. // // Returns nil if [Watcher.Close] was called. func (w *Watcher) Remove(name string) error { return w.remove(name, true) } func (w *Watcher) remove(name string, unwatchFiles bool) error { name = filepath.Clean(name) w.mu.Lock() if w.isClosed { w.mu.Unlock() return nil } watchfd, ok := w.watches[name] w.mu.Unlock() if !ok { return fmt.Errorf("%w: %s", ErrNonExistentWatch, name) } err := w.register([]int{watchfd}, unix.EV_DELETE, 0) if err != nil { return err } unix.Close(watchfd) w.mu.Lock() isDir := w.paths[watchfd].isDir delete(w.watches, name) delete(w.userWatches, name) parentName := filepath.Dir(name) delete(w.watchesByDir[parentName], watchfd) if len(w.watchesByDir[parentName]) == 0 { delete(w.watchesByDir, parentName) } delete(w.paths, watchfd) delete(w.dirFlags, name) delete(w.fileExists, name) w.mu.Unlock() // Find all watched paths that are in this directory that are not external. if unwatchFiles && isDir { var pathsToRemove []string w.mu.Lock() for fd := range w.watchesByDir[name] { path := w.paths[fd] if _, ok := w.userWatches[path.name]; !ok { pathsToRemove = append(pathsToRemove, path.name) } } w.mu.Unlock() for _, name := range pathsToRemove { // Since these are internal, not much sense in propagating error to // the user, as that will just confuse them with an error about a // path they did not explicitly watch themselves. w.Remove(name) } } return nil } // WatchList returns all paths explicitly added with [Watcher.Add] (and are not // yet removed). // // Returns nil if [Watcher.Close] was called. func (w *Watcher) WatchList() []string { w.mu.Lock() defer w.mu.Unlock() if w.isClosed { return nil } entries := make([]string, 0, len(w.userWatches)) for pathname := range w.userWatches { entries = append(entries, pathname) } return entries } // Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE) const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME // addWatch adds name to the watched file set; the flags are interpreted as // described in kevent(2). // // Returns the real path to the file which was added, with symlinks resolved. func (w *Watcher) addWatch(name string, flags uint32) (string, error) { var isDir bool name = filepath.Clean(name) w.mu.Lock() if w.isClosed { w.mu.Unlock() return "", ErrClosed } watchfd, alreadyWatching := w.watches[name] // We already have a watch, but we can still override flags. if alreadyWatching { isDir = w.paths[watchfd].isDir } w.mu.Unlock() if !alreadyWatching { fi, err := os.Lstat(name) if err != nil { return "", err } // Don't watch sockets or named pipes if (fi.Mode()&os.ModeSocket == os.ModeSocket) || (fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe) { return "", nil } // Follow Symlinks. if fi.Mode()&os.ModeSymlink == os.ModeSymlink { link, err := os.Readlink(name) if err != nil { // Return nil because Linux can add unresolvable symlinks to the // watch list without problems, so maintain consistency with // that. There will be no file events for broken symlinks. // TODO: more specific check; returns os.PathError; ENOENT? return "", nil } w.mu.Lock() _, alreadyWatching = w.watches[link] w.mu.Unlock() if alreadyWatching { // Add to watches so we don't get spurious Create events later // on when we diff the directories. w.watches[name] = 0 w.fileExists[name] = struct{}{} return link, nil } name = link fi, err = os.Lstat(name) if err != nil { return "", nil } } // Retry on EINTR; open() can return EINTR in practice on macOS. // See #354, and Go issues 11180 and 39237. for { watchfd, err = unix.Open(name, openMode, 0) if err == nil { break } if errors.Is(err, unix.EINTR) { continue } return "", err } isDir = fi.IsDir() } err := w.register([]int{watchfd}, unix.EV_ADD|unix.EV_CLEAR|unix.EV_ENABLE, flags) if err != nil { unix.Close(watchfd) return "", err } if !alreadyWatching { w.mu.Lock() parentName := filepath.Dir(name) w.watches[name] = watchfd watchesByDir, ok := w.watchesByDir[parentName] if !ok { watchesByDir = make(map[int]struct{}, 1) w.watchesByDir[parentName] = watchesByDir } watchesByDir[watchfd] = struct{}{} w.paths[watchfd] = pathInfo{name: name, isDir: isDir} w.mu.Unlock() } if isDir { // Watch the directory if it has not been watched before, or if it was // watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles) w.mu.Lock() watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE && (!alreadyWatching || (w.dirFlags[name]&unix.NOTE_WRITE) != unix.NOTE_WRITE) // Store flags so this watch can be updated later w.dirFlags[name] = flags w.mu.Unlock() if watchDir { if err := w.watchDirectoryFiles(name); err != nil { return "", err } } } return name, nil } // readEvents reads from kqueue and converts the received kevents into // Event values that it sends down the Events channel. func (w *Watcher) readEvents() { defer func() { close(w.Events) close(w.Errors) _ = unix.Close(w.kq) unix.Close(w.closepipe[0]) }() eventBuffer := make([]unix.Kevent_t, 10) for closed := false; !closed; { kevents, err := w.read(eventBuffer) // EINTR is okay, the syscall was interrupted before timeout expired. if err != nil && err != unix.EINTR { if !w.sendError(fmt.Errorf("fsnotify.readEvents: %w", err)) { closed = true } continue } // Flush the events we received to the Events channel for _, kevent := range kevents { var ( watchfd = int(kevent.Ident) mask = uint32(kevent.Fflags) ) // Shut down the loop when the pipe is closed, but only after all // other events have been processed. if watchfd == w.closepipe[0] { closed = true continue } w.mu.Lock() path := w.paths[watchfd] w.mu.Unlock() event := w.newEvent(path.name, mask) if event.Has(Rename) || event.Has(Remove) { w.remove(event.Name, false) w.mu.Lock() delete(w.fileExists, event.Name) w.mu.Unlock() } if path.isDir && event.Has(Write) && !event.Has(Remove) { w.sendDirectoryChangeEvents(event.Name) } else { if !w.sendEvent(event) { closed = true continue } } if event.Has(Remove) { // Look for a file that may have overwritten this; for example, // mv f1 f2 will delete f2, then create f2. if path.isDir { fileDir := filepath.Clean(event.Name) w.mu.Lock() _, found := w.watches[fileDir] w.mu.Unlock() if found { err := w.sendDirectoryChangeEvents(fileDir) if err != nil { if !w.sendError(err) { closed = true } } } } else { filePath := filepath.Clean(event.Name) if fi, err := os.Lstat(filePath); err == nil { err := w.sendFileCreatedEventIfNew(filePath, fi) if err != nil { if !w.sendError(err) { closed = true } } } } } } } } // newEvent returns an platform-independent Event based on kqueue Fflags. func (w *Watcher) newEvent(name string, mask uint32) Event { e := Event{Name: name} if mask&unix.NOTE_DELETE == unix.NOTE_DELETE { e.Op |= Remove } if mask&unix.NOTE_WRITE == unix.NOTE_WRITE { e.Op |= Write } if mask&unix.NOTE_RENAME == unix.NOTE_RENAME { e.Op |= Rename } if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB { e.Op |= Chmod } // No point sending a write and delete event at the same time: if it's gone, // then it's gone. if e.Op.Has(Write) && e.Op.Has(Remove) { e.Op &^= Write } return e } // watchDirectoryFiles to mimic inotify when adding a watch on a directory func (w *Watcher) watchDirectoryFiles(dirPath string) error { // Get all files files, err := os.ReadDir(dirPath) if err != nil { return err } for _, f := range files { path := filepath.Join(dirPath, f.Name()) fi, err := f.Info() if err != nil { return fmt.Errorf("%q: %w", path, err) } cleanPath, err := w.internalWatch(path, fi) if err != nil { // No permission to read the file; that's not a problem: just skip. // But do add it to w.fileExists to prevent it from being picked up // as a "new" file later (it still shows up in the directory // listing). switch { case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM): cleanPath = filepath.Clean(path) default: return fmt.Errorf("%q: %w", path, err) } } w.mu.Lock() w.fileExists[cleanPath] = struct{}{} w.mu.Unlock() } return nil } // Search the directory for new files and send an event for them. // // This functionality is to have the BSD watcher match the inotify, which sends // a create event for files created in a watched directory. func (w *Watcher) sendDirectoryChangeEvents(dir string) error { files, err := os.ReadDir(dir) if err != nil { // Directory no longer exists: we can ignore this safely. kqueue will // still give us the correct events. if errors.Is(err, os.ErrNotExist) { return nil } return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err) } for _, f := range files { fi, err := f.Info() if err != nil { return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err) } err = w.sendFileCreatedEventIfNew(filepath.Join(dir, fi.Name()), fi) if err != nil { // Don't need to send an error if this file isn't readable. if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) { return nil } return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err) } } return nil } // sendFileCreatedEvent sends a create event if the file isn't already being tracked. func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fi os.FileInfo) (err error) { w.mu.Lock() _, doesExist := w.fileExists[filePath] w.mu.Unlock() if !doesExist { if !w.sendEvent(Event{Name: filePath, Op: Create}) { return } } // like watchDirectoryFiles (but without doing another ReadDir) filePath, err = w.internalWatch(filePath, fi) if err != nil { return err } w.mu.Lock() w.fileExists[filePath] = struct{}{} w.mu.Unlock() return nil } func (w *Watcher) internalWatch(name string, fi os.FileInfo) (string, error) { if fi.IsDir() { // mimic Linux providing delete events for subdirectories, but preserve // the flags used if currently watching subdirectory w.mu.Lock() flags := w.dirFlags[name] w.mu.Unlock() flags |= unix.NOTE_DELETE | unix.NOTE_RENAME return w.addWatch(name, flags) } // watch file to mimic Linux inotify return w.addWatch(name, noteAllEvents) } // Register events with the queue. func (w *Watcher) register(fds []int, flags int, fflags uint32) error { changes := make([]unix.Kevent_t, len(fds)) for i, fd := range fds { // SetKevent converts int to the platform-specific types. unix.SetKevent(&changes[i], fd, unix.EVFILT_VNODE, flags) changes[i].Fflags = fflags } // Register the events. success, err := unix.Kevent(w.kq, changes, nil, nil) if success == -1 { return err } return nil } // read retrieves pending events, or waits until an event occurs. func (w *Watcher) read(events []unix.Kevent_t) ([]unix.Kevent_t, error) { n, err := unix.Kevent(w.kq, nil, events, nil) if err != nil { return nil, err } return events[0:n], nil } fsnotify-1.7.0/backend_kqueue_test.go000066400000000000000000000046571451514000000177160ustar00rootroot00000000000000//go:build freebsd || openbsd || netbsd || dragonfly || darwin // +build freebsd openbsd netbsd dragonfly darwin package fsnotify import ( "fmt" "strings" "testing" ) func TestRemoveState(t *testing.T) { var ( tmp = t.TempDir() dir = join(tmp, "dir") file = join(dir, "file") ) mkdir(t, dir) touch(t, file) w := newWatcher(t, tmp) addWatch(t, w, tmp) addWatch(t, w, file) check := func(wantUser, wantTotal int) { t.Helper() if len(w.watches) != wantTotal { var d []string for k, v := range w.watches { d = append(d, fmt.Sprintf("%#v = %#v", k, v)) } t.Errorf("unexpected number of entries in w.watches (have %d, want %d):\n%v", len(w.watches), wantTotal, strings.Join(d, "\n")) } if len(w.paths) != wantTotal { var d []string for k, v := range w.paths { d = append(d, fmt.Sprintf("%#v = %#v", k, v)) } t.Errorf("unexpected number of entries in w.paths (have %d, want %d):\n%v", len(w.paths), wantTotal, strings.Join(d, "\n")) } if len(w.userWatches) != wantUser { var d []string for k, v := range w.userWatches { d = append(d, fmt.Sprintf("%#v = %#v", k, v)) } t.Errorf("unexpected number of entries in w.userWatches (have %d, want %d):\n%v", len(w.userWatches), wantUser, strings.Join(d, "\n")) } } check(2, 3) if err := w.Remove(file); err != nil { t.Fatal(err) } check(1, 2) if err := w.Remove(tmp); err != nil { t.Fatal(err) } check(0, 0) // Don't check these after ever remove since they don't map easily to number // of files watches. Just make sure they're 0 after everything is removed. { want := 0 if len(w.watchesByDir) != want { var d []string for k, v := range w.watchesByDir { d = append(d, fmt.Sprintf("%#v = %#v", k, v)) } t.Errorf("unexpected number of entries in w.watchesByDir (have %d, want %d):\n%v", len(w.watchesByDir), want, strings.Join(d, "\n")) } if len(w.dirFlags) != want { var d []string for k, v := range w.dirFlags { d = append(d, fmt.Sprintf("%#v = %#v", k, v)) } t.Errorf("unexpected number of entries in w.dirFlags (have %d, want %d):\n%v", len(w.dirFlags), want, strings.Join(d, "\n")) } if len(w.fileExists) != want { var d []string for k, v := range w.fileExists { d = append(d, fmt.Sprintf("%#v = %#v", k, v)) } t.Errorf("unexpected number of entries in w.fileExists (have %d, want %d):\n%v", len(w.fileExists), want, strings.Join(d, "\n")) } } } fsnotify-1.7.0/backend_other.go000066400000000000000000000210731451514000000164700ustar00rootroot00000000000000//go:build appengine || (!darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows) // +build appengine !darwin,!dragonfly,!freebsd,!openbsd,!linux,!netbsd,!solaris,!windows // Note: the documentation on the Watcher type and methods is generated from // mkdoc.zsh package fsnotify import "errors" // Watcher watches a set of paths, delivering events on a channel. // // A watcher should not be copied (e.g. pass it by pointer, rather than by // value). // // # Linux notes // // When a file is removed a Remove event won't be emitted until all file // descriptors are closed, and deletes will always emit a Chmod. For example: // // fp := os.Open("file") // os.Remove("file") // Triggers Chmod // fp.Close() // Triggers Remove // // This is the event that inotify sends, so not much can be changed about this. // // The fs.inotify.max_user_watches sysctl variable specifies the upper limit // for the number of watches per user, and fs.inotify.max_user_instances // specifies the maximum number of inotify instances per user. Every Watcher you // create is an "instance", and every path you add is a "watch". // // These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and // /proc/sys/fs/inotify/max_user_instances // // To increase them you can use sysctl or write the value to the /proc file: // // # Default values on Linux 5.18 // sysctl fs.inotify.max_user_watches=124983 // sysctl fs.inotify.max_user_instances=128 // // To make the changes persist on reboot edit /etc/sysctl.conf or // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check // your distro's documentation): // // fs.inotify.max_user_watches=124983 // fs.inotify.max_user_instances=128 // // Reaching the limit will result in a "no space left on device" or "too many open // files" error. // // # kqueue notes (macOS, BSD) // // kqueue requires opening a file descriptor for every file that's being watched; // so if you're watching a directory with five files then that's six file // descriptors. You will run in to your system's "max open files" limit faster on // these platforms. // // The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to // control the maximum number of open files, as well as /etc/login.conf on BSD // systems. // // # Windows notes // // Paths can be added as "C:\path\to\dir", but forward slashes // ("C:/path/to/dir") will also work. // // When a watched directory is removed it will always send an event for the // directory itself, but may not send events for all files in that directory. // Sometimes it will send events for all times, sometimes it will send no // events, and often only for some files. // // The default ReadDirectoryChangesW() buffer size is 64K, which is the largest // value that is guaranteed to work with SMB filesystems. If you have many // events in quick succession this may not be enough, and you will have to use // [WithBufferSize] to increase the value. type Watcher struct { // Events sends the filesystem change events. // // fsnotify can send the following events; a "path" here can refer to a // file, directory, symbolic link, or special file like a FIFO. // // fsnotify.Create A new path was created; this may be followed by one // or more Write events if data also gets written to a // file. // // fsnotify.Remove A path was removed. // // fsnotify.Rename A path was renamed. A rename is always sent with the // old path as Event.Name, and a Create event will be // sent with the new name. Renames are only sent for // paths that are currently watched; e.g. moving an // unmonitored file into a monitored directory will // show up as just a Create. Similarly, renaming a file // to outside a monitored directory will show up as // only a Rename. // // fsnotify.Write A file or named pipe was written to. A Truncate will // also trigger a Write. A single "write action" // initiated by the user may show up as one or multiple // writes, depending on when the system syncs things to // disk. For example when compiling a large Go program // you may get hundreds of Write events, and you may // want to wait until you've stopped receiving them // (see the dedup example in cmd/fsnotify). // // Some systems may send Write event for directories // when the directory content changes. // // fsnotify.Chmod Attributes were changed. On Linux this is also sent // when a file is removed (or more accurately, when a // link to an inode is removed). On kqueue it's sent // when a file is truncated. On Windows it's never // sent. Events chan Event // Errors sends any errors. // // ErrEventOverflow is used to indicate there are too many events: // // - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl) // - windows: The buffer size is too small; WithBufferSize() can be used to increase it. // - kqueue, fen: Not used. Errors chan error } // NewWatcher creates a new Watcher. func NewWatcher() (*Watcher, error) { return nil, errors.New("fsnotify not supported on the current platform") } // NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events // channel. // // The main use case for this is situations with a very large number of events // where the kernel buffer size can't be increased (e.g. due to lack of // permissions). An unbuffered Watcher will perform better for almost all use // cases, and whenever possible you will be better off increasing the kernel // buffers instead of adding a large userspace buffer. func NewBufferedWatcher(sz uint) (*Watcher, error) { return NewWatcher() } // Close removes all watches and closes the Events channel. func (w *Watcher) Close() error { return nil } // WatchList returns all paths explicitly added with [Watcher.Add] (and are not // yet removed). // // Returns nil if [Watcher.Close] was called. func (w *Watcher) WatchList() []string { return nil } // Add starts monitoring the path for changes. // // A path can only be watched once; watching it more than once is a no-op and will // not return an error. Paths that do not yet exist on the filesystem cannot be // watched. // // A watch will be automatically removed if the watched path is deleted or // renamed. The exception is the Windows backend, which doesn't remove the // watcher on renames. // // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // filesystems (/proc, /sys, etc.) generally don't work. // // Returns [ErrClosed] if [Watcher.Close] was called. // // See [Watcher.AddWith] for a version that allows adding options. // // # Watching directories // // All files in a directory are monitored, including new files that are created // after the watcher is started. Subdirectories are not watched (i.e. it's // non-recursive). // // # Watching files // // Watching individual files (rather than directories) is generally not // recommended as many programs (especially editors) update files atomically: it // will write to a temporary file which is then moved to to destination, // overwriting the original (or some variant thereof). The watcher on the // original file is now lost, as that no longer exists. // // The upshot of this is that a power failure or crash won't leave a // half-written file. // // Watch the parent directory and use Event.Name to filter out files you're not // interested in. There is an example of this in cmd/fsnotify/file.go. func (w *Watcher) Add(name string) error { return nil } // AddWith is like [Watcher.Add], but allows adding options. When using Add() // the defaults described below are used. // // Possible options are: // // - [WithBufferSize] sets the buffer size for the Windows backend; no-op on // other platforms. The default is 64K (65536 bytes). func (w *Watcher) AddWith(name string, opts ...addOpt) error { return nil } // Remove stops monitoring the path for changes. // // Directories are always removed non-recursively. For example, if you added // /tmp/dir and /tmp/dir/subdir then you will need to remove both. // // Removing a path that has not yet been added returns [ErrNonExistentWatch]. // // Returns nil if [Watcher.Close] was called. func (w *Watcher) Remove(name string) error { return nil } fsnotify-1.7.0/backend_windows.go000066400000000000000000000556731451514000000170560ustar00rootroot00000000000000//go:build windows // +build windows // Windows backend based on ReadDirectoryChangesW() // // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw // // Note: the documentation on the Watcher type and methods is generated from // mkdoc.zsh package fsnotify import ( "errors" "fmt" "os" "path/filepath" "reflect" "runtime" "strings" "sync" "unsafe" "golang.org/x/sys/windows" ) // Watcher watches a set of paths, delivering events on a channel. // // A watcher should not be copied (e.g. pass it by pointer, rather than by // value). // // # Linux notes // // When a file is removed a Remove event won't be emitted until all file // descriptors are closed, and deletes will always emit a Chmod. For example: // // fp := os.Open("file") // os.Remove("file") // Triggers Chmod // fp.Close() // Triggers Remove // // This is the event that inotify sends, so not much can be changed about this. // // The fs.inotify.max_user_watches sysctl variable specifies the upper limit // for the number of watches per user, and fs.inotify.max_user_instances // specifies the maximum number of inotify instances per user. Every Watcher you // create is an "instance", and every path you add is a "watch". // // These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and // /proc/sys/fs/inotify/max_user_instances // // To increase them you can use sysctl or write the value to the /proc file: // // # Default values on Linux 5.18 // sysctl fs.inotify.max_user_watches=124983 // sysctl fs.inotify.max_user_instances=128 // // To make the changes persist on reboot edit /etc/sysctl.conf or // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check // your distro's documentation): // // fs.inotify.max_user_watches=124983 // fs.inotify.max_user_instances=128 // // Reaching the limit will result in a "no space left on device" or "too many open // files" error. // // # kqueue notes (macOS, BSD) // // kqueue requires opening a file descriptor for every file that's being watched; // so if you're watching a directory with five files then that's six file // descriptors. You will run in to your system's "max open files" limit faster on // these platforms. // // The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to // control the maximum number of open files, as well as /etc/login.conf on BSD // systems. // // # Windows notes // // Paths can be added as "C:\path\to\dir", but forward slashes // ("C:/path/to/dir") will also work. // // When a watched directory is removed it will always send an event for the // directory itself, but may not send events for all files in that directory. // Sometimes it will send events for all times, sometimes it will send no // events, and often only for some files. // // The default ReadDirectoryChangesW() buffer size is 64K, which is the largest // value that is guaranteed to work with SMB filesystems. If you have many // events in quick succession this may not be enough, and you will have to use // [WithBufferSize] to increase the value. type Watcher struct { // Events sends the filesystem change events. // // fsnotify can send the following events; a "path" here can refer to a // file, directory, symbolic link, or special file like a FIFO. // // fsnotify.Create A new path was created; this may be followed by one // or more Write events if data also gets written to a // file. // // fsnotify.Remove A path was removed. // // fsnotify.Rename A path was renamed. A rename is always sent with the // old path as Event.Name, and a Create event will be // sent with the new name. Renames are only sent for // paths that are currently watched; e.g. moving an // unmonitored file into a monitored directory will // show up as just a Create. Similarly, renaming a file // to outside a monitored directory will show up as // only a Rename. // // fsnotify.Write A file or named pipe was written to. A Truncate will // also trigger a Write. A single "write action" // initiated by the user may show up as one or multiple // writes, depending on when the system syncs things to // disk. For example when compiling a large Go program // you may get hundreds of Write events, and you may // want to wait until you've stopped receiving them // (see the dedup example in cmd/fsnotify). // // Some systems may send Write event for directories // when the directory content changes. // // fsnotify.Chmod Attributes were changed. On Linux this is also sent // when a file is removed (or more accurately, when a // link to an inode is removed). On kqueue it's sent // when a file is truncated. On Windows it's never // sent. Events chan Event // Errors sends any errors. // // ErrEventOverflow is used to indicate there are too many events: // // - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl) // - windows: The buffer size is too small; WithBufferSize() can be used to increase it. // - kqueue, fen: Not used. Errors chan error port windows.Handle // Handle to completion port input chan *input // Inputs to the reader are sent on this channel quit chan chan<- error mu sync.Mutex // Protects access to watches, closed watches watchMap // Map of watches (key: i-number) closed bool // Set to true when Close() is first called } // NewWatcher creates a new Watcher. func NewWatcher() (*Watcher, error) { return NewBufferedWatcher(50) } // NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events // channel. // // The main use case for this is situations with a very large number of events // where the kernel buffer size can't be increased (e.g. due to lack of // permissions). An unbuffered Watcher will perform better for almost all use // cases, and whenever possible you will be better off increasing the kernel // buffers instead of adding a large userspace buffer. func NewBufferedWatcher(sz uint) (*Watcher, error) { port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0) if err != nil { return nil, os.NewSyscallError("CreateIoCompletionPort", err) } w := &Watcher{ port: port, watches: make(watchMap), input: make(chan *input, 1), Events: make(chan Event, sz), Errors: make(chan error), quit: make(chan chan<- error, 1), } go w.readEvents() return w, nil } func (w *Watcher) isClosed() bool { w.mu.Lock() defer w.mu.Unlock() return w.closed } func (w *Watcher) sendEvent(name string, mask uint64) bool { if mask == 0 { return false } event := w.newEvent(name, uint32(mask)) select { case ch := <-w.quit: w.quit <- ch case w.Events <- event: } return true } // Returns true if the error was sent, or false if watcher is closed. func (w *Watcher) sendError(err error) bool { select { case w.Errors <- err: return true case <-w.quit: } return false } // Close removes all watches and closes the Events channel. func (w *Watcher) Close() error { if w.isClosed() { return nil } w.mu.Lock() w.closed = true w.mu.Unlock() // Send "quit" message to the reader goroutine ch := make(chan error) w.quit <- ch if err := w.wakeupReader(); err != nil { return err } return <-ch } // Add starts monitoring the path for changes. // // A path can only be watched once; watching it more than once is a no-op and will // not return an error. Paths that do not yet exist on the filesystem cannot be // watched. // // A watch will be automatically removed if the watched path is deleted or // renamed. The exception is the Windows backend, which doesn't remove the // watcher on renames. // // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // filesystems (/proc, /sys, etc.) generally don't work. // // Returns [ErrClosed] if [Watcher.Close] was called. // // See [Watcher.AddWith] for a version that allows adding options. // // # Watching directories // // All files in a directory are monitored, including new files that are created // after the watcher is started. Subdirectories are not watched (i.e. it's // non-recursive). // // # Watching files // // Watching individual files (rather than directories) is generally not // recommended as many programs (especially editors) update files atomically: it // will write to a temporary file which is then moved to to destination, // overwriting the original (or some variant thereof). The watcher on the // original file is now lost, as that no longer exists. // // The upshot of this is that a power failure or crash won't leave a // half-written file. // // Watch the parent directory and use Event.Name to filter out files you're not // interested in. There is an example of this in cmd/fsnotify/file.go. func (w *Watcher) Add(name string) error { return w.AddWith(name) } // AddWith is like [Watcher.Add], but allows adding options. When using Add() // the defaults described below are used. // // Possible options are: // // - [WithBufferSize] sets the buffer size for the Windows backend; no-op on // other platforms. The default is 64K (65536 bytes). func (w *Watcher) AddWith(name string, opts ...addOpt) error { if w.isClosed() { return ErrClosed } with := getOptions(opts...) if with.bufsize < 4096 { return fmt.Errorf("fsnotify.WithBufferSize: buffer size cannot be smaller than 4096 bytes") } in := &input{ op: opAddWatch, path: filepath.Clean(name), flags: sysFSALLEVENTS, reply: make(chan error), bufsize: with.bufsize, } w.input <- in if err := w.wakeupReader(); err != nil { return err } return <-in.reply } // Remove stops monitoring the path for changes. // // Directories are always removed non-recursively. For example, if you added // /tmp/dir and /tmp/dir/subdir then you will need to remove both. // // Removing a path that has not yet been added returns [ErrNonExistentWatch]. // // Returns nil if [Watcher.Close] was called. func (w *Watcher) Remove(name string) error { if w.isClosed() { return nil } in := &input{ op: opRemoveWatch, path: filepath.Clean(name), reply: make(chan error), } w.input <- in if err := w.wakeupReader(); err != nil { return err } return <-in.reply } // WatchList returns all paths explicitly added with [Watcher.Add] (and are not // yet removed). // // Returns nil if [Watcher.Close] was called. func (w *Watcher) WatchList() []string { if w.isClosed() { return nil } w.mu.Lock() defer w.mu.Unlock() entries := make([]string, 0, len(w.watches)) for _, entry := range w.watches { for _, watchEntry := range entry { entries = append(entries, watchEntry.path) } } return entries } // These options are from the old golang.org/x/exp/winfsnotify, where you could // add various options to the watch. This has long since been removed. // // The "sys" in the name is misleading as they're not part of any "system". // // This should all be removed at some point, and just use windows.FILE_NOTIFY_* const ( sysFSALLEVENTS = 0xfff sysFSCREATE = 0x100 sysFSDELETE = 0x200 sysFSDELETESELF = 0x400 sysFSMODIFY = 0x2 sysFSMOVE = 0xc0 sysFSMOVEDFROM = 0x40 sysFSMOVEDTO = 0x80 sysFSMOVESELF = 0x800 sysFSIGNORED = 0x8000 ) func (w *Watcher) newEvent(name string, mask uint32) Event { e := Event{Name: name} if mask&sysFSCREATE == sysFSCREATE || mask&sysFSMOVEDTO == sysFSMOVEDTO { e.Op |= Create } if mask&sysFSDELETE == sysFSDELETE || mask&sysFSDELETESELF == sysFSDELETESELF { e.Op |= Remove } if mask&sysFSMODIFY == sysFSMODIFY { e.Op |= Write } if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM { e.Op |= Rename } return e } const ( opAddWatch = iota opRemoveWatch ) const ( provisional uint64 = 1 << (32 + iota) ) type input struct { op int path string flags uint32 bufsize int reply chan error } type inode struct { handle windows.Handle volume uint32 index uint64 } type watch struct { ov windows.Overlapped ino *inode // i-number recurse bool // Recursive watch? path string // Directory path mask uint64 // Directory itself is being watched with these notify flags names map[string]uint64 // Map of names being watched and their notify flags rename string // Remembers the old name while renaming a file buf []byte // buffer, allocated later } type ( indexMap map[uint64]*watch watchMap map[uint32]indexMap ) func (w *Watcher) wakeupReader() error { err := windows.PostQueuedCompletionStatus(w.port, 0, 0, nil) if err != nil { return os.NewSyscallError("PostQueuedCompletionStatus", err) } return nil } func (w *Watcher) getDir(pathname string) (dir string, err error) { attr, err := windows.GetFileAttributes(windows.StringToUTF16Ptr(pathname)) if err != nil { return "", os.NewSyscallError("GetFileAttributes", err) } if attr&windows.FILE_ATTRIBUTE_DIRECTORY != 0 { dir = pathname } else { dir, _ = filepath.Split(pathname) dir = filepath.Clean(dir) } return } func (w *Watcher) getIno(path string) (ino *inode, err error) { h, err := windows.CreateFile(windows.StringToUTF16Ptr(path), windows.FILE_LIST_DIRECTORY, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_BACKUP_SEMANTICS|windows.FILE_FLAG_OVERLAPPED, 0) if err != nil { return nil, os.NewSyscallError("CreateFile", err) } var fi windows.ByHandleFileInformation err = windows.GetFileInformationByHandle(h, &fi) if err != nil { windows.CloseHandle(h) return nil, os.NewSyscallError("GetFileInformationByHandle", err) } ino = &inode{ handle: h, volume: fi.VolumeSerialNumber, index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow), } return ino, nil } // Must run within the I/O thread. func (m watchMap) get(ino *inode) *watch { if i := m[ino.volume]; i != nil { return i[ino.index] } return nil } // Must run within the I/O thread. func (m watchMap) set(ino *inode, watch *watch) { i := m[ino.volume] if i == nil { i = make(indexMap) m[ino.volume] = i } i[ino.index] = watch } // Must run within the I/O thread. func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error { //pathname, recurse := recursivePath(pathname) recurse := false dir, err := w.getDir(pathname) if err != nil { return err } ino, err := w.getIno(dir) if err != nil { return err } w.mu.Lock() watchEntry := w.watches.get(ino) w.mu.Unlock() if watchEntry == nil { _, err := windows.CreateIoCompletionPort(ino.handle, w.port, 0, 0) if err != nil { windows.CloseHandle(ino.handle) return os.NewSyscallError("CreateIoCompletionPort", err) } watchEntry = &watch{ ino: ino, path: dir, names: make(map[string]uint64), recurse: recurse, buf: make([]byte, bufsize), } w.mu.Lock() w.watches.set(ino, watchEntry) w.mu.Unlock() flags |= provisional } else { windows.CloseHandle(ino.handle) } if pathname == dir { watchEntry.mask |= flags } else { watchEntry.names[filepath.Base(pathname)] |= flags } err = w.startRead(watchEntry) if err != nil { return err } if pathname == dir { watchEntry.mask &= ^provisional } else { watchEntry.names[filepath.Base(pathname)] &= ^provisional } return nil } // Must run within the I/O thread. func (w *Watcher) remWatch(pathname string) error { pathname, recurse := recursivePath(pathname) dir, err := w.getDir(pathname) if err != nil { return err } ino, err := w.getIno(dir) if err != nil { return err } w.mu.Lock() watch := w.watches.get(ino) w.mu.Unlock() if recurse && !watch.recurse { return fmt.Errorf("can't use \\... with non-recursive watch %q", pathname) } err = windows.CloseHandle(ino.handle) if err != nil { w.sendError(os.NewSyscallError("CloseHandle", err)) } if watch == nil { return fmt.Errorf("%w: %s", ErrNonExistentWatch, pathname) } if pathname == dir { w.sendEvent(watch.path, watch.mask&sysFSIGNORED) watch.mask = 0 } else { name := filepath.Base(pathname) w.sendEvent(filepath.Join(watch.path, name), watch.names[name]&sysFSIGNORED) delete(watch.names, name) } return w.startRead(watch) } // Must run within the I/O thread. func (w *Watcher) deleteWatch(watch *watch) { for name, mask := range watch.names { if mask&provisional == 0 { w.sendEvent(filepath.Join(watch.path, name), mask&sysFSIGNORED) } delete(watch.names, name) } if watch.mask != 0 { if watch.mask&provisional == 0 { w.sendEvent(watch.path, watch.mask&sysFSIGNORED) } watch.mask = 0 } } // Must run within the I/O thread. func (w *Watcher) startRead(watch *watch) error { err := windows.CancelIo(watch.ino.handle) if err != nil { w.sendError(os.NewSyscallError("CancelIo", err)) w.deleteWatch(watch) } mask := w.toWindowsFlags(watch.mask) for _, m := range watch.names { mask |= w.toWindowsFlags(m) } if mask == 0 { err := windows.CloseHandle(watch.ino.handle) if err != nil { w.sendError(os.NewSyscallError("CloseHandle", err)) } w.mu.Lock() delete(w.watches[watch.ino.volume], watch.ino.index) w.mu.Unlock() return nil } // We need to pass the array, rather than the slice. hdr := (*reflect.SliceHeader)(unsafe.Pointer(&watch.buf)) rdErr := windows.ReadDirectoryChanges(watch.ino.handle, (*byte)(unsafe.Pointer(hdr.Data)), uint32(hdr.Len), watch.recurse, mask, nil, &watch.ov, 0) if rdErr != nil { err := os.NewSyscallError("ReadDirectoryChanges", rdErr) if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 { // Watched directory was probably removed w.sendEvent(watch.path, watch.mask&sysFSDELETESELF) err = nil } w.deleteWatch(watch) w.startRead(watch) return err } return nil } // readEvents reads from the I/O completion port, converts the // received events into Event objects and sends them via the Events channel. // Entry point to the I/O thread. func (w *Watcher) readEvents() { var ( n uint32 key uintptr ov *windows.Overlapped ) runtime.LockOSThread() for { // This error is handled after the watch == nil check below. qErr := windows.GetQueuedCompletionStatus(w.port, &n, &key, &ov, windows.INFINITE) watch := (*watch)(unsafe.Pointer(ov)) if watch == nil { select { case ch := <-w.quit: w.mu.Lock() var indexes []indexMap for _, index := range w.watches { indexes = append(indexes, index) } w.mu.Unlock() for _, index := range indexes { for _, watch := range index { w.deleteWatch(watch) w.startRead(watch) } } err := windows.CloseHandle(w.port) if err != nil { err = os.NewSyscallError("CloseHandle", err) } close(w.Events) close(w.Errors) ch <- err return case in := <-w.input: switch in.op { case opAddWatch: in.reply <- w.addWatch(in.path, uint64(in.flags), in.bufsize) case opRemoveWatch: in.reply <- w.remWatch(in.path) } default: } continue } switch qErr { case nil: // No error case windows.ERROR_MORE_DATA: if watch == nil { w.sendError(errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer")) } else { // The i/o succeeded but the buffer is full. // In theory we should be building up a full packet. // In practice we can get away with just carrying on. n = uint32(unsafe.Sizeof(watch.buf)) } case windows.ERROR_ACCESS_DENIED: // Watched directory was probably removed w.sendEvent(watch.path, watch.mask&sysFSDELETESELF) w.deleteWatch(watch) w.startRead(watch) continue case windows.ERROR_OPERATION_ABORTED: // CancelIo was called on this handle continue default: w.sendError(os.NewSyscallError("GetQueuedCompletionPort", qErr)) continue } var offset uint32 for { if n == 0 { w.sendError(ErrEventOverflow) break } // Point "raw" to the event in the buffer raw := (*windows.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset])) // Create a buf that is the size of the path name size := int(raw.FileNameLength / 2) var buf []uint16 // TODO: Use unsafe.Slice in Go 1.17; https://stackoverflow.com/questions/51187973 sh := (*reflect.SliceHeader)(unsafe.Pointer(&buf)) sh.Data = uintptr(unsafe.Pointer(&raw.FileName)) sh.Len = size sh.Cap = size name := windows.UTF16ToString(buf) fullname := filepath.Join(watch.path, name) var mask uint64 switch raw.Action { case windows.FILE_ACTION_REMOVED: mask = sysFSDELETESELF case windows.FILE_ACTION_MODIFIED: mask = sysFSMODIFY case windows.FILE_ACTION_RENAMED_OLD_NAME: watch.rename = name case windows.FILE_ACTION_RENAMED_NEW_NAME: // Update saved path of all sub-watches. old := filepath.Join(watch.path, watch.rename) w.mu.Lock() for _, watchMap := range w.watches { for _, ww := range watchMap { if strings.HasPrefix(ww.path, old) { ww.path = filepath.Join(fullname, strings.TrimPrefix(ww.path, old)) } } } w.mu.Unlock() if watch.names[watch.rename] != 0 { watch.names[name] |= watch.names[watch.rename] delete(watch.names, watch.rename) mask = sysFSMOVESELF } } sendNameEvent := func() { w.sendEvent(fullname, watch.names[name]&mask) } if raw.Action != windows.FILE_ACTION_RENAMED_NEW_NAME { sendNameEvent() } if raw.Action == windows.FILE_ACTION_REMOVED { w.sendEvent(fullname, watch.names[name]&sysFSIGNORED) delete(watch.names, name) } w.sendEvent(fullname, watch.mask&w.toFSnotifyFlags(raw.Action)) if raw.Action == windows.FILE_ACTION_RENAMED_NEW_NAME { fullname = filepath.Join(watch.path, watch.rename) sendNameEvent() } // Move to the next event in the buffer if raw.NextEntryOffset == 0 { break } offset += raw.NextEntryOffset // Error! if offset >= n { //lint:ignore ST1005 Windows should be capitalized w.sendError(errors.New( "Windows system assumed buffer larger than it is, events have likely been missed")) break } } if err := w.startRead(watch); err != nil { w.sendError(err) } } } func (w *Watcher) toWindowsFlags(mask uint64) uint32 { var m uint32 if mask&sysFSMODIFY != 0 { m |= windows.FILE_NOTIFY_CHANGE_LAST_WRITE } if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 { m |= windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME } return m } func (w *Watcher) toFSnotifyFlags(action uint32) uint64 { switch action { case windows.FILE_ACTION_ADDED: return sysFSCREATE case windows.FILE_ACTION_REMOVED: return sysFSDELETE case windows.FILE_ACTION_MODIFIED: return sysFSMODIFY case windows.FILE_ACTION_RENAMED_OLD_NAME: return sysFSMOVEDFROM case windows.FILE_ACTION_RENAMED_NEW_NAME: return sysFSMOVEDTO } return 0 } fsnotify-1.7.0/backend_windows_test.go000066400000000000000000000022641451514000000201010ustar00rootroot00000000000000//go:build windows // +build windows package fsnotify import ( "fmt" "strings" "testing" ) func TestRemoveState(t *testing.T) { // TODO: the Windows backend is too confusing; needs some serious attention. return var ( tmp = t.TempDir() dir = join(tmp, "dir") file = join(dir, "file") ) mkdir(t, dir) touch(t, file) w := newWatcher(t, tmp) addWatch(t, w, tmp) addWatch(t, w, file) check := func(want int) { t.Helper() if len(w.watches) != want { var d []string for k, v := range w.watches { d = append(d, fmt.Sprintf("%#v = %#v", k, v)) } t.Errorf("unexpected number of entries in w.watches (have %d, want %d):\n%v", len(w.watches), want, strings.Join(d, "\n")) } } check(2) if err := w.Remove(file); err != nil { t.Fatal(err) } check(1) if err := w.Remove(tmp); err != nil { t.Fatal(err) } check(0) } func TestWindowsRemWatch(t *testing.T) { tmp := t.TempDir() touch(t, tmp, "file") w := newWatcher(t) defer w.Close() addWatch(t, w, tmp) if err := w.Remove(tmp); err != nil { t.Fatalf("Could not remove the watch: %v\n", err) } if err := w.remWatch(tmp); err == nil { t.Fatal("Should be fail with closed handle\n") } } fsnotify-1.7.0/cmd/000077500000000000000000000000001451514000000141115ustar00rootroot00000000000000fsnotify-1.7.0/cmd/fsnotify/000077500000000000000000000000001451514000000157525ustar00rootroot00000000000000fsnotify-1.7.0/cmd/fsnotify/dedup.go000066400000000000000000000043151451514000000174050ustar00rootroot00000000000000package main import ( "math" "sync" "time" "github.com/fsnotify/fsnotify" ) // Depending on the system, a single "write" can generate many Write events; for // example compiling a large Go program can generate hundreds of Write events on // the binary. // // The general strategy to deal with this is to wait a short time for more write // events, resetting the wait period for every new event. func dedup(paths ...string) { if len(paths) < 1 { exit("must specify at least one path to watch") } // Create a new watcher. w, err := fsnotify.NewWatcher() if err != nil { exit("creating a new watcher: %s", err) } defer w.Close() // Start listening for events. go dedupLoop(w) // Add all paths from the commandline. for _, p := range paths { err = w.Add(p) if err != nil { exit("%q: %s", p, err) } } printTime("ready; press ^C to exit") <-make(chan struct{}) // Block forever } func dedupLoop(w *fsnotify.Watcher) { var ( // Wait 100ms for new events; each new event resets the timer. waitFor = 100 * time.Millisecond // Keep track of the timers, as path → timer. mu sync.Mutex timers = make(map[string]*time.Timer) // Callback we run. printEvent = func(e fsnotify.Event) { printTime(e.String()) // Don't need to remove the timer if you don't have a lot of files. mu.Lock() delete(timers, e.Name) mu.Unlock() } ) for { select { // Read from Errors. case err, ok := <-w.Errors: if !ok { // Channel was closed (i.e. Watcher.Close() was called). return } printTime("ERROR: %s", err) // Read from Events. case e, ok := <-w.Events: if !ok { // Channel was closed (i.e. Watcher.Close() was called). return } // We just want to watch for file creation, so ignore everything // outside of Create and Write. if !e.Has(fsnotify.Create) && !e.Has(fsnotify.Write) { continue } // Get timer. mu.Lock() t, ok := timers[e.Name] mu.Unlock() // No timer yet, so create one. if !ok { t = time.AfterFunc(math.MaxInt64, func() { printEvent(e) }) t.Stop() mu.Lock() timers[e.Name] = t mu.Unlock() } // Reset the timer for this path, so it will start from 100ms again. t.Reset(waitFor) } } } fsnotify-1.7.0/cmd/fsnotify/file.go000066400000000000000000000034411451514000000172220ustar00rootroot00000000000000package main import ( "os" "path/filepath" "github.com/fsnotify/fsnotify" ) // Watch one or more files, but instead of watching the file directly it watches // the parent directory. This solves various issues where files are frequently // renamed, such as editors saving them. func file(files ...string) { if len(files) < 1 { exit("must specify at least one file to watch") } // Create a new watcher. w, err := fsnotify.NewWatcher() if err != nil { exit("creating a new watcher: %s", err) } defer w.Close() // Start listening for events. go fileLoop(w, files) // Add all files from the commandline. for _, p := range files { st, err := os.Lstat(p) if err != nil { exit("%s", err) } if st.IsDir() { exit("%q is a directory, not a file", p) } // Watch the directory, not the file itself. err = w.Add(filepath.Dir(p)) if err != nil { exit("%q: %s", p, err) } } printTime("ready; press ^C to exit") <-make(chan struct{}) // Block forever } func fileLoop(w *fsnotify.Watcher, files []string) { i := 0 for { select { // Read from Errors. case err, ok := <-w.Errors: if !ok { // Channel was closed (i.e. Watcher.Close() was called). return } printTime("ERROR: %s", err) // Read from Events. case e, ok := <-w.Events: if !ok { // Channel was closed (i.e. Watcher.Close() was called). return } // Ignore files we're not interested in. Can use a // map[string]struct{} if you have a lot of files, but for just a // few files simply looping over a slice is faster. var found bool for _, f := range files { if f == e.Name { found = true } } if !found { continue } // Just print the event nicely aligned, and keep track how many // events we've seen. i++ printTime("%3d %s", i, e) } } } fsnotify-1.7.0/cmd/fsnotify/main.go000066400000000000000000000027121451514000000172270ustar00rootroot00000000000000// Command fsnotify provides example usage of the fsnotify library. package main import ( "fmt" "os" "path/filepath" "time" ) var usage = ` fsnotify is a Go library to provide cross-platform file system notifications. This command serves as an example and debugging tool. https://github.com/fsnotify/fsnotify Commands: watch [paths] Watch the paths for changes and print the events. file [file] Watch a single file for changes. dedup [paths] Watch the paths for changes, suppressing duplicate events. `[1:] func exit(format string, a ...interface{}) { fmt.Fprintf(os.Stderr, filepath.Base(os.Args[0])+": "+format+"\n", a...) fmt.Print("\n" + usage) os.Exit(1) } func help() { fmt.Printf("%s [command] [arguments]\n\n", filepath.Base(os.Args[0])) fmt.Print(usage) os.Exit(0) } // Print line prefixed with the time (a bit shorter than log.Print; we don't // really need the date and ms is useful here). func printTime(s string, args ...interface{}) { fmt.Printf(time.Now().Format("15:04:05.0000")+" "+s+"\n", args...) } func main() { if len(os.Args) == 1 { help() } // Always show help if -h[elp] appears anywhere before we do anything else. for _, f := range os.Args[1:] { switch f { case "help", "-h", "-help", "--help": help() } } cmd, args := os.Args[1], os.Args[2:] switch cmd { default: exit("unknown command: %q", cmd) case "watch": watch(args...) case "file": file(args...) case "dedup": dedup(args...) } } fsnotify-1.7.0/cmd/fsnotify/watch.go000066400000000000000000000022121451514000000174040ustar00rootroot00000000000000package main import "github.com/fsnotify/fsnotify" // This is the most basic example: it prints events to the terminal as we // receive them. func watch(paths ...string) { if len(paths) < 1 { exit("must specify at least one path to watch") } // Create a new watcher. w, err := fsnotify.NewWatcher() if err != nil { exit("creating a new watcher: %s", err) } defer w.Close() // Start listening for events. go watchLoop(w) // Add all paths from the commandline. for _, p := range paths { err = w.Add(p) if err != nil { exit("%q: %s", p, err) } } printTime("ready; press ^C to exit") <-make(chan struct{}) // Block forever } func watchLoop(w *fsnotify.Watcher) { i := 0 for { select { // Read from Errors. case err, ok := <-w.Errors: if !ok { // Channel was closed (i.e. Watcher.Close() was called). return } printTime("ERROR: %s", err) // Read from Events. case e, ok := <-w.Events: if !ok { // Channel was closed (i.e. Watcher.Close() was called). return } // Just print the event nicely aligned, and keep track how many // events we've seen. i++ printTime("%3d %s", i, e) } } } fsnotify-1.7.0/fsnotify.go000066400000000000000000000076521451514000000155500ustar00rootroot00000000000000// Package fsnotify provides a cross-platform interface for file system // notifications. // // Currently supported systems: // // Linux 2.6.32+ via inotify // BSD, macOS via kqueue // Windows via ReadDirectoryChangesW // illumos via FEN package fsnotify import ( "errors" "fmt" "path/filepath" "strings" ) // Event represents a file system notification. type Event struct { // Path to the file or directory. // // Paths are relative to the input; for example with Add("dir") the Name // will be set to "dir/file" if you create that file, but if you use // Add("/path/to/dir") it will be "/path/to/dir/file". Name string // File operation that triggered the event. // // This is a bitmask and some systems may send multiple operations at once. // Use the Event.Has() method instead of comparing with ==. Op Op } // Op describes a set of file operations. type Op uint32 // The operations fsnotify can trigger; see the documentation on [Watcher] for a // full description, and check them with [Event.Has]. const ( // A new pathname was created. Create Op = 1 << iota // The pathname was written to; this does *not* mean the write has finished, // and a write can be followed by more writes. Write // The path was removed; any watches on it will be removed. Some "remove" // operations may trigger a Rename if the file is actually moved (for // example "remove to trash" is often a rename). Remove // The path was renamed to something else; any watched on it will be // removed. Rename // File attributes were changed. // // It's generally not recommended to take action on this event, as it may // get triggered very frequently by some software. For example, Spotlight // indexing on macOS, anti-virus software, backup software, etc. Chmod ) // Common errors that can be reported. var ( ErrNonExistentWatch = errors.New("fsnotify: can't remove non-existent watch") ErrEventOverflow = errors.New("fsnotify: queue or buffer overflow") ErrClosed = errors.New("fsnotify: watcher already closed") ) func (o Op) String() string { var b strings.Builder if o.Has(Create) { b.WriteString("|CREATE") } if o.Has(Remove) { b.WriteString("|REMOVE") } if o.Has(Write) { b.WriteString("|WRITE") } if o.Has(Rename) { b.WriteString("|RENAME") } if o.Has(Chmod) { b.WriteString("|CHMOD") } if b.Len() == 0 { return "[no events]" } return b.String()[1:] } // Has reports if this operation has the given operation. func (o Op) Has(h Op) bool { return o&h != 0 } // Has reports if this event has the given operation. func (e Event) Has(op Op) bool { return e.Op.Has(op) } // String returns a string representation of the event with their path. func (e Event) String() string { return fmt.Sprintf("%-13s %q", e.Op.String(), e.Name) } type ( addOpt func(opt *withOpts) withOpts struct { bufsize int } ) var defaultOpts = withOpts{ bufsize: 65536, // 64K } func getOptions(opts ...addOpt) withOpts { with := defaultOpts for _, o := range opts { o(&with) } return with } // WithBufferSize sets the [ReadDirectoryChangesW] buffer size. // // This only has effect on Windows systems, and is a no-op for other backends. // // The default value is 64K (65536 bytes) which is the highest value that works // on all filesystems and should be enough for most applications, but if you // have a large burst of events it may not be enough. You can increase it if // you're hitting "queue or buffer overflow" errors ([ErrEventOverflow]). // // [ReadDirectoryChangesW]: https://learn.microsoft.com/en-gb/windows/win32/api/winbase/nf-winbase-readdirectorychangesw func WithBufferSize(bytes int) addOpt { return func(opt *withOpts) { opt.bufsize = bytes } } // Check if this path is recursive (ends with "/..." or "\..."), and return the // path with the /... stripped. func recursivePath(path string) (string, bool) { if filepath.Base(path) == "..." { return filepath.Dir(path), true } return path, false } fsnotify-1.7.0/fsnotify_test.go000066400000000000000000001145161451514000000166050ustar00rootroot00000000000000package fsnotify import ( "errors" "fmt" "io/fs" "os" "path/filepath" "reflect" "runtime" "sort" "strings" "sync" "sync/atomic" "syscall" "testing" "time" "github.com/fsnotify/fsnotify/internal" ) // Set soft open file limit to the maximum; on e.g. OpenBSD it's 512/1024. // // Go 1.19 will always do this when the os package is imported. // // https://go-review.googlesource.com/c/go/+/393354/ func init() { internal.SetRlimit() } func TestWatch(t *testing.T) { tests := []testCase{ {"multiple creates", func(t *testing.T, w *Watcher, tmp string) { file := join(tmp, "file") addWatch(t, w, tmp) cat(t, "data", file) rm(t, file) touch(t, file) // Recreate the file cat(t, "data", file) // Modify cat(t, "data", file) // Modify }, ` create /file write /file remove /file create /file write /file write /file `}, {"dir only", func(t *testing.T, w *Watcher, tmp string) { beforeWatch := join(tmp, "beforewatch") file := join(tmp, "file") touch(t, beforeWatch) addWatch(t, w, tmp) cat(t, "data", file) rm(t, file) rm(t, beforeWatch) }, ` create /file write /file remove /file remove /beforewatch `}, {"subdir", func(t *testing.T, w *Watcher, tmp string) { addWatch(t, w, tmp) file := join(tmp, "file") dir := join(tmp, "sub") dirfile := join(tmp, "sub/file2") mkdir(t, dir) // Create sub-directory touch(t, file) // Create a file touch(t, dirfile) // Create a file (Should not see this! we are not watching subdir) time.Sleep(200 * time.Millisecond) rmAll(t, dir) // Make sure receive deletes for both file and sub-directory rm(t, file) }, ` create /sub create /file remove /sub remove /file # TODO: not sure why the REMOVE /sub is dropped. dragonfly: create /sub create /file remove /file fen: create /sub create /file write /sub remove /sub remove /file # Windows includes a write for the /sub dir too, two of them even(?) windows: create /sub create /file write /sub write /sub remove /sub remove /file `}, {"file in directory is not readable", func(t *testing.T, w *Watcher, tmp string) { if runtime.GOOS == "windows" { t.Skip("attributes don't work on Windows") // TODO: figure out how to make a file unreadable } touch(t, tmp, "file-unreadable") chmod(t, 0, tmp, "file-unreadable") touch(t, tmp, "file") addWatch(t, w, tmp) cat(t, "hello", tmp, "file") rm(t, tmp, "file") rm(t, tmp, "file-unreadable") }, ` WRITE "/file" REMOVE "/file" REMOVE "/file-unreadable" # We never set up a watcher on the unreadable file, so we don't get # the REMOVE. kqueue: WRITE "/file" REMOVE "/file" windows: empty `}, {"watch same dir twice", func(t *testing.T, w *Watcher, tmp string) { addWatch(t, w, tmp) addWatch(t, w, tmp) touch(t, tmp, "file") cat(t, "hello", tmp, "file") rm(t, tmp, "file") mkdir(t, tmp, "dir") }, ` create /file write /file remove /file create /dir `}, {"watch same file twice", func(t *testing.T, w *Watcher, tmp string) { file := join(tmp, "file") touch(t, file) addWatch(t, w, file) addWatch(t, w, file) cat(t, "hello", tmp, "file") }, ` write /file `}, } for _, tt := range tests { tt := tt tt.run(t) } } func TestWatchCreate(t *testing.T) { tests := []testCase{ // Files {"create empty file", func(t *testing.T, w *Watcher, tmp string) { addWatch(t, w, tmp) touch(t, tmp, "file") }, ` create /file `}, {"create file with data", func(t *testing.T, w *Watcher, tmp string) { addWatch(t, w, tmp) cat(t, "data", tmp, "file") }, ` create /file write /file `}, // Directories {"create new directory", func(t *testing.T, w *Watcher, tmp string) { addWatch(t, w, tmp) mkdir(t, tmp, "dir") }, ` create /dir `}, // Links {"create new symlink to file", func(t *testing.T, w *Watcher, tmp string) { if !internal.HasPrivilegesForSymlink() { t.Skip("does not have privileges for symlink on this OS") } touch(t, tmp, "file") addWatch(t, w, tmp) symlink(t, join(tmp, "file"), tmp, "link") }, ` create /link `}, {"create new symlink to directory", func(t *testing.T, w *Watcher, tmp string) { if !internal.HasPrivilegesForSymlink() { t.Skip("does not have privileges for symlink on this OS") } addWatch(t, w, tmp) symlink(t, tmp, tmp, "link") }, ` create /link `}, // FIFO {"create new named pipe", func(t *testing.T, w *Watcher, tmp string) { if runtime.GOOS == "windows" { t.Skip("No named pipes on Windows") } touch(t, tmp, "file") addWatch(t, w, tmp) mkfifo(t, tmp, "fifo") }, ` create /fifo `}, // Device node {"create new device node pipe", func(t *testing.T, w *Watcher, tmp string) { if runtime.GOOS == "windows" { t.Skip("No device nodes on Windows") } if isKqueue() { // Don't want to use os/user to check uid, since that pulls in // cgo by default and stuff that uses fsnotify won't be // statically linked by default. t.Skip("needs root on BSD") } if isSolaris() { t.Skip(`"mknod fails with "not owner"`) } touch(t, tmp, "file") addWatch(t, w, tmp) mknod(t, 0, tmp, "dev") }, ` create /dev `}, } for _, tt := range tests { tt := tt tt.run(t) } } func TestWatchWrite(t *testing.T) { tests := []testCase{ // Files {"truncate file", func(t *testing.T, w *Watcher, tmp string) { file := join(tmp, "file") cat(t, "data", file) addWatch(t, w, tmp) fp, err := os.Create(file) if err != nil { t.Fatal(err) } if err := fp.Sync(); err != nil { t.Fatal(err) } eventSeparator() if _, err := fp.Write([]byte("X")); err != nil { t.Fatal(err) } if err := fp.Close(); err != nil { t.Fatal(err) } }, ` write /file # truncate write /file # write # Truncate is chmod on kqueue, except NetBSD netbsd: write /file kqueue: chmod /file write /file `}, {"multiple writes to a file", func(t *testing.T, w *Watcher, tmp string) { file := join(tmp, "file") cat(t, "data", file) addWatch(t, w, tmp) fp, err := os.OpenFile(file, os.O_RDWR, 0) if err != nil { t.Fatal(err) } if _, err := fp.Write([]byte("X")); err != nil { t.Fatal(err) } if err := fp.Sync(); err != nil { t.Fatal(err) } eventSeparator() if _, err := fp.Write([]byte("Y")); err != nil { t.Fatal(err) } if err := fp.Close(); err != nil { t.Fatal(err) } }, ` write /file # write X write /file # write Y `}, } for _, tt := range tests { tt := tt tt.run(t) } } func TestWatchRename(t *testing.T) { tests := []testCase{ {"rename file in watched dir", func(t *testing.T, w *Watcher, tmp string) { file := join(tmp, "file") cat(t, "asd", file) addWatch(t, w, tmp) mv(t, file, tmp, "renamed") }, ` rename /file create /renamed `}, {"rename from unwatched dir", func(t *testing.T, w *Watcher, tmp string) { unwatched := t.TempDir() addWatch(t, w, tmp) touch(t, unwatched, "file") mv(t, join(unwatched, "file"), tmp, "file") }, ` create /file `}, {"rename to unwatched dir", func(t *testing.T, w *Watcher, tmp string) { if runtime.GOOS == "netbsd" && isCI() { t.Skip("fails in CI; see #488") // TODO } unwatched := t.TempDir() file := join(tmp, "file") renamed := join(unwatched, "renamed") addWatch(t, w, tmp) cat(t, "data", file) mv(t, file, renamed) cat(t, "data", renamed) // Modify the file outside of the watched dir touch(t, file) // Recreate the file that was moved }, ` create /file # cat data >file write /file # ^ rename /file # mv file ../renamed create /file # touch file # Windows has REMOVE /file, rather than CREATE /file windows: create /file write /file remove /file create /file `}, {"rename overwriting existing file", func(t *testing.T, w *Watcher, tmp string) { unwatched := t.TempDir() file := join(unwatched, "file") touch(t, tmp, "renamed") touch(t, file) addWatch(t, w, tmp) mv(t, file, tmp, "renamed") }, ` # TODO: this should really be RENAME. remove /renamed create /renamed # No remove event for inotify; inotify just sends MOVE_SELF. linux: create /renamed # TODO: this is broken. dragonfly: REMOVE "/" `}, {"rename watched directory", func(t *testing.T, w *Watcher, tmp string) { dir := join(tmp, "dir") mkdir(t, dir) addWatch(t, w, dir) mv(t, dir, tmp, "dir-renamed") touch(t, tmp, "dir-renamed/file") }, ` rename /dir # TODO(v2): Windows should behave the same by default. See #518 windows: create /dir/file `}, {"rename watched file", func(t *testing.T, w *Watcher, tmp string) { file := join(tmp, "file") rename := join(tmp, "rename-one") touch(t, file) addWatch(t, w, file) mv(t, file, rename) mv(t, rename, tmp, "rename-two") }, ` rename /file # TODO(v2): Windows should behave the same by default. See #518 windows: rename /file rename /rename-one `}, {"re-add renamed file", func(t *testing.T, w *Watcher, tmp string) { file := join(tmp, "file") rename := join(tmp, "rename") touch(t, file) addWatch(t, w, file) mv(t, file, rename) touch(t, file) addWatch(t, w, file) cat(t, "hello", rename) cat(t, "hello", file) }, ` rename /file # mv file rename # Watcher gets removed on rename, so no write for /rename write /file # cat hello >file # TODO(v2): Windows should behave the same by default. See #518 windows: rename /file write /rename write /file `}, } for _, tt := range tests { tt := tt tt.run(t) } } func TestWatchSymlink(t *testing.T) { if !internal.HasPrivilegesForSymlink() { t.Skip("does not have privileges for symlink on this OS") } tests := []testCase{ {"watch a symlink to a file", func(t *testing.T, w *Watcher, tmp string) { file := join(tmp, "file") link := join(tmp, "link") touch(t, file) symlink(t, file, link) addWatch(t, w, link) cat(t, "hello", file) }, ` write /link # TODO: Symlinks followed on kqueue; it shouldn't do this, but I'm # afraid changing it will break stuff. See #227, #390 kqueue: write /file # TODO: see if we can fix this. windows: empty `}, {"watch a symlink to a dir", func(t *testing.T, w *Watcher, tmp string) { dir := join(tmp, "dir") link := join(tmp, "link") mkdir(t, dir) symlink(t, dir, link) addWatch(t, w, link) touch(t, dir, "file") }, ` create /link/file # TODO: Symlinks followed on kqueue; it shouldn't do this, but I'm # afraid changing it will break stuff. See #227, #390 kqueue: create /dir/file `}, {"create unresolvable symlink", func(t *testing.T, w *Watcher, tmp string) { addWatch(t, w, tmp) symlink(t, join(tmp, "target"), tmp, "link") }, ` create /link # No events at all on Dragonfly # TODO: should fix this. dragonfly: empty `}, {"cyclic symlink", func(t *testing.T, w *Watcher, tmp string) { symlink(t, ".", tmp, "link") addWatch(t, w, tmp) rm(t, tmp, "link") cat(t, "foo", tmp, "link") }, ` write /link create /link linux, windows, fen: remove /link create /link write /link `}, // Bug #277 {"277", func(t *testing.T, w *Watcher, tmp string) { if runtime.GOOS == "netbsd" && isCI() { t.Skip("fails in CI") // TODO } // TODO: there is some strange fuckery going on if I use go test // -count=2; the second test run has unix.Kqueue() in newKqueue() // return 0, which is a very odd fd number, but the first event does // work (create /foo). After that we get EBADF (Bad file // descriptor). // // This is *only* for this test, and *only* if we have the symlinks // below. kqueue(2) doesn't document returning fd 0. // // This happens on all the BSDs, but *not* macOS. touch(t, tmp, "file1") touch(t, tmp, "file2") symlink(t, join(tmp, "file1"), tmp, "link1") symlink(t, join(tmp, "file2"), tmp, "link2") addWatch(t, w, tmp) touch(t, tmp, "foo") rm(t, tmp, "foo") mkdir(t, tmp, "apple") mv(t, join(tmp, "apple"), tmp, "pear") rmAll(t, tmp, "pear") }, ` create /foo # touch foo remove /foo # rm foo create /apple # mkdir apple rename /apple # mv apple pear create /pear remove /pear # rm -r pear # TODO: the CREATE/REMOVE for /pear don't show; I'm not entirely # sure why; sendDirectoryChangeEvents() doesn't pick it up. It does # seem consistent though, both locally and in CI. freebsd, netbsd, dragonfly: create /foo # touch foo remove /foo # rm foo create /apple # mkdir apple rename /apple # mv apple pear `}, } for _, tt := range tests { tt := tt tt.run(t) } } func TestWatchAttrib(t *testing.T) { tests := []testCase{ {"chmod", func(t *testing.T, w *Watcher, tmp string) { file := join(tmp, "file") cat(t, "data", file) addWatch(t, w, file) chmod(t, 0o700, file) }, ` CHMOD "/file" windows: empty `}, {"write does not trigger CHMOD", func(t *testing.T, w *Watcher, tmp string) { file := join(tmp, "file") cat(t, "data", file) addWatch(t, w, file) chmod(t, 0o700, file) cat(t, "more data", file) }, ` CHMOD "/file" WRITE "/file" windows: write /file `}, {"chmod after write", func(t *testing.T, w *Watcher, tmp string) { file := join(tmp, "file") cat(t, "data", file) addWatch(t, w, file) chmod(t, 0o700, file) cat(t, "more data", file) chmod(t, 0o600, file) }, ` CHMOD "/file" WRITE "/file" CHMOD "/file" windows: write /file `}, } for _, tt := range tests { tt := tt tt.run(t) } } func TestWatchRemove(t *testing.T) { tests := []testCase{ {"remove watched file", func(t *testing.T, w *Watcher, tmp string) { file := join(tmp, "file") touch(t, file) addWatch(t, w, file) rm(t, file) }, ` REMOVE "/file" # unlink always emits a CHMOD on Linux. linux: CHMOD "/file" REMOVE "/file" `}, {"remove watched file with open fd", func(t *testing.T, w *Watcher, tmp string) { if runtime.GOOS == "windows" { t.Skip("Windows hard-locks open files so this will never work") } file := join(tmp, "file") touch(t, file) // Intentionally don't close the descriptor here so it stays around. _, err := os.Open(file) if err != nil { t.Fatal(err) } addWatch(t, w, file) rm(t, file) }, ` REMOVE "/file" # inotify will just emit a CHMOD for the unlink, but won't actually # emit a REMOVE until the descriptor is closed. Bit odd, but not much # we can do about it. The REMOVE is tested in TestInotifyDeleteOpenFile() linux: CHMOD "/file" `}, {"remove recursive", func(t *testing.T, w *Watcher, tmp string) { recurseOnly(t) mkdirAll(t, tmp, "dir1", "subdir") mkdirAll(t, tmp, "dir2", "subdir") touch(t, tmp, "dir1", "subdir", "file") touch(t, tmp, "dir2", "subdir", "file") addWatch(t, w, tmp, "dir1", "...") addWatch(t, w, tmp, "dir2", "...") cat(t, "asd", tmp, "dir1", "subdir", "file") cat(t, "asd", tmp, "dir2", "subdir", "file") if err := w.Remove(join(tmp, "dir1")); err != nil { t.Fatal(err) } if err := w.Remove(join(tmp, "dir2", "...")); err != nil { t.Fatal(err) } if w := w.WatchList(); len(w) != 0 { t.Errorf("WatchList not empty: %s", w) } cat(t, "asd", tmp, "dir1", "subdir", "file") cat(t, "asd", tmp, "dir2", "subdir", "file") }, ` write /dir1/subdir write /dir1/subdir/file write /dir2/subdir write /dir2/subdir/file `}, } for _, tt := range tests { tt := tt tt.run(t) } t.Run("remove watched directory", func(t *testing.T) { t.Parallel() tmp := t.TempDir() w := newCollector(t) w.collect(t) touch(t, tmp, "a") touch(t, tmp, "b") touch(t, tmp, "c") touch(t, tmp, "d") touch(t, tmp, "e") touch(t, tmp, "f") touch(t, tmp, "g") mkdir(t, tmp, "h") mkdir(t, tmp, "h", "a") mkdir(t, tmp, "i") mkdir(t, tmp, "i", "a") mkdir(t, tmp, "j") mkdir(t, tmp, "j", "a") addWatch(t, w.w, tmp) rmAll(t, tmp) if runtime.GOOS != "windows" { cmpEvents(t, tmp, w.stop(t), newEvents(t, ` remove / remove /a remove /b remove /c remove /d remove /e remove /f remove /g remove /h remove /i remove /j`)) return } // ReadDirectoryChangesW gives undefined results: not all files are // always present. So test only that 1) we got the directory itself, and // 2) we don't get events for unspected files. var ( events = w.stop(t) found bool ) for _, e := range events { if e.Name == tmp && e.Has(Remove) { found = true continue } if filepath.Dir(e.Name) != tmp { t.Errorf("unexpected event: %s", e) } } if !found { t.Fatalf("didn't see directory in:\n%s", events) } }) } func TestWatchRecursive(t *testing.T) { recurseOnly(t) tests := []testCase{ // Make a nested directory tree, then write some files there. {"basic", func(t *testing.T, w *Watcher, tmp string) { mkdirAll(t, tmp, "/one/two/three/four") addWatch(t, w, tmp, "/...") cat(t, "asd", tmp, "/file.txt") cat(t, "asd", tmp, "/one/two/three/file.txt") }, ` create /file.txt # cat asd >file.txt write /file.txt write /one/two/three # cat asd >one/two/three/file.txt create /one/two/three/file.txt write /one/two/three/file.txt `}, // Create a new directory tree and then some files under that. {"add directory", func(t *testing.T, w *Watcher, tmp string) { mkdirAll(t, tmp, "/one/two/three/four") addWatch(t, w, tmp, "/...") mkdirAll(t, tmp, "/one/two/new/dir") touch(t, tmp, "/one/two/new/file") touch(t, tmp, "/one/two/new/dir/file") }, ` write /one/two # mkdir -p one/two/new/dir create /one/two/new create /one/two/new/dir write /one/two/new # touch one/two/new/file create /one/two/new/file create /one/two/new/dir/file # touch one/two/new/dir/file `}, // Remove nested directory {"remove directory", func(t *testing.T, w *Watcher, tmp string) { mkdirAll(t, tmp, "one/two/three/four") addWatch(t, w, tmp, "...") cat(t, "asd", tmp, "one/two/three/file.txt") rmAll(t, tmp, "one/two") }, ` write /one/two/three # cat asd >one/two/three/file.txt create /one/two/three/file.txt write /one/two/three/file.txt write /one/two # rm -r one/two write /one/two/three remove /one/two/three/file.txt remove /one/two/three/four write /one/two/three remove /one/two/three write /one/two remove /one/two `}, // Rename nested directory {"rename directory", func(t *testing.T, w *Watcher, tmp string) { mkdirAll(t, tmp, "/one/two/three/four") addWatch(t, w, tmp, "...") mv(t, join(tmp, "one"), tmp, "one-rename") touch(t, tmp, "one-rename/file") touch(t, tmp, "one-rename/two/three/file") }, ` rename "/one" # mv one one-rename create "/one-rename" write "/one-rename" # touch one-rename/file create "/one-rename/file" write "/one-rename/two/three" # touch one-rename/two/three/file create "/one-rename/two/three/file" `}, {"remove watched directory", func(t *testing.T, w *Watcher, tmp string) { mk := func(r string) { touch(t, r, "a") touch(t, r, "b") touch(t, r, "c") touch(t, r, "d") touch(t, r, "e") touch(t, r, "f") touch(t, r, "g") mkdir(t, r, "h") mkdir(t, r, "h", "a") mkdir(t, r, "i") mkdir(t, r, "i", "a") mkdir(t, r, "j") mkdir(t, r, "j", "a") } mk(tmp) mkdir(t, tmp, "sub") mk(join(tmp, "sub")) addWatch(t, w, tmp, "...") rmAll(t, tmp) }, ` remove "/a" remove "/b" remove "/c" remove "/d" remove "/e" remove "/f" remove "/g" write "/h" remove "/h/a" write "/h" remove "/h" write "/i" remove "/i/a" write "/i" remove "/i" write "/j" remove "/j/a" write "/j" remove "/j" write "/sub" remove "/sub/a" remove "/sub/b" remove "/sub/c" remove "/sub/d" remove "/sub/e" remove "/sub/f" remove "/sub/g" write "/sub/h" remove "/sub/h/a" write "/sub/h" remove "/sub/h" write "/sub/i" remove "/sub/i/a" write "/sub/i" remove "/sub/i" write "/sub/j" remove "/sub/j/a" write "/sub/j" remove "/sub/j" write "/sub" remove "/sub" remove "/" `}, } for _, tt := range tests { tt := tt tt.run(t) } } // TODO: this fails reguarly in the CI; not sure if it's a bug with the test or // code; need to look in to it. func TestClose(t *testing.T) { chanClosed := func(t *testing.T, w *Watcher) { t.Helper() // Need a small sleep as Close() on kqueue does all sorts of things, // which may take a little bit. switch runtime.GOOS { case "freebsd", "openbsd", "netbsd", "dragonfly", "darwin", "solaris", "illumos": time.Sleep(50 * time.Millisecond) } tim := time.NewTimer(50 * time.Millisecond) loop: for { select { default: t.Fatal("blocking on Events") case <-tim.C: t.Fatalf("Events not closed") case _, ok := <-w.Events: if !ok { break loop } } } select { default: t.Fatal("blocking on Errors") case err, ok := <-w.Errors: if ok { t.Fatalf("Errors not closed; read:\n\t%s", err) } } } t.Run("close", func(t *testing.T) { t.Parallel() w := newWatcher(t) if err := w.Close(); err != nil { t.Fatal(err) } chanClosed(t, w) var done int32 go func() { w.Close() atomic.StoreInt32(&done, 1) }() eventSeparator() if atomic.LoadInt32(&done) == 0 { t.Fatal("double Close() test failed: second Close() call didn't return") } if err := w.Add(t.TempDir()); err == nil { t.Fatal("expected error on Watch() after Close(), got nil") } }) // Make sure that Close() works even when the Events channel isn't being // read. t.Run("events not read", func(t *testing.T) { t.Parallel() tmp := t.TempDir() w := newWatcher(t, tmp) touch(t, tmp, "file") rm(t, tmp, "file") eventSeparator() if err := w.Close(); err != nil { t.Fatal(err) } // TODO: windows backend doesn't work well here; can't easily fix it. // Need to rewrite things a bit. if runtime.GOOS != "windows" { chanClosed(t, w) } }) // Make sure that calling Close() while REMOVE events are emitted doesn't race. t.Run("close while removing files", func(t *testing.T) { t.Parallel() tmp := t.TempDir() files := make([]string, 0, 200) for i := 0; i < 200; i++ { f := join(tmp, fmt.Sprintf("file-%03d", i)) touch(t, f, noWait) files = append(files, f) } w := newWatcher(t, tmp) startC, stopC, errC := make(chan struct{}), make(chan struct{}), make(chan error) go func() { for { select { case <-w.Errors: case <-w.Events: case <-stopC: return } } }() rmDone := make(chan struct{}) go func() { <-startC for _, f := range files { rm(t, f, noWait) } rmDone <- struct{}{} }() go func() { <-startC errC <- w.Close() }() close(startC) defer close(stopC) if err := <-errC; err != nil { t.Fatal(err) } <-rmDone }) // Make sure Close() doesn't race when called more than once; hard to write // a good reproducible test for this, but running it 150 times seems to // reproduce it in ~75% of cases and isn't too slow (~0.06s on my system). t.Run("double close", func(t *testing.T) { t.Run("default", func(t *testing.T) { t.Parallel() for i := 0; i < 150; i++ { w, err := NewWatcher() if err != nil { if strings.Contains(err.Error(), "too many") { // syscall.EMFILE time.Sleep(100 * time.Millisecond) continue } t.Fatal(err) } go w.Close() go w.Close() go w.Close() } }) t.Run("buffered=4096", func(t *testing.T) { t.Parallel() for i := 0; i < 150; i++ { w, err := NewBufferedWatcher(4096) if err != nil { if strings.Contains(err.Error(), "too many") { // syscall.EMFILE time.Sleep(100 * time.Millisecond) continue } t.Fatal(err) } go w.Close() go w.Close() go w.Close() } }) }) t.Run("closes channels after read", func(t *testing.T) { if runtime.GOOS == "netbsd" { t.Skip("flaky") // TODO } t.Parallel() tmp := t.TempDir() w := newCollector(t, tmp) w.collect(t) touch(t, tmp, "qwe") touch(t, tmp, "asd") if err := w.w.Close(); err != nil { t.Fatal(err) } chanClosed(t, w.w) }) t.Run("error after closed", func(t *testing.T) { t.Parallel() tmp := t.TempDir() w := newWatcher(t, tmp) if err := w.Close(); err != nil { t.Fatal(err) } file := join(tmp, "file") touch(t, file) if err := w.Add(file); !errors.Is(err, ErrClosed) { t.Fatalf("wrong error for Add: %#v", err) } if err := w.Remove(file); err != nil { t.Fatalf("wrong error for Remove: %#v", err) } if l := w.WatchList(); l != nil { // Should return an error, but meh :-/ t.Fatalf("WatchList not nil: %#v", l) } }) } func TestAdd(t *testing.T) { t.Run("doesn't exist", func(t *testing.T) { t.Parallel() tmp := t.TempDir() w := newWatcher(t) err := w.Add(join(tmp, "non-existent")) if err == nil { t.Fatal("err is nil") } // Errors for this are inconsistent; should be fixed in v2. See #144 switch runtime.GOOS { case "linux": if _, ok := err.(syscall.Errno); !ok { t.Errorf("wrong error type: %[1]T: %#[1]v", err) } case "windows": if _, ok := err.(*os.SyscallError); !ok { t.Errorf("wrong error type: %[1]T: %#[1]v", err) } default: if _, ok := err.(*fs.PathError); !ok { t.Errorf("wrong error type: %[1]T: %#[1]v", err) } } }) t.Run("permission denied", func(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("chmod doesn't work on Windows") // TODO: see if we can make a file unreadable } t.Parallel() tmp := t.TempDir() dir := join(tmp, "dir-unreadable") mkdir(t, dir) touch(t, dir, "/file") chmod(t, 0, dir) w := newWatcher(t) defer func() { w.Close() chmod(t, 0o755, dir) // Make TempDir() cleanup work }() err := w.Add(dir) if err == nil { t.Fatal("error is nil") } if !errors.Is(err, internal.UnixEACCES) { t.Errorf("not unix.EACCESS: %T %#[1]v", err) } if !errors.Is(err, internal.SyscallEACCES) { t.Errorf("not syscall.EACCESS: %T %#[1]v", err) } }) t.Run("add same path twice", func(t *testing.T) { tmp := t.TempDir() w := newCollector(t) if err := w.w.Add(tmp); err != nil { t.Fatal(err) } if err := w.w.Add(tmp); err != nil { t.Fatal(err) } w.collect(t) touch(t, tmp, "file") rm(t, tmp, "file") cmpEvents(t, tmp, w.events(t), newEvents(t, ` create /file remove /file `)) }) } // TODO: should also check internal state is correct/cleaned up; e.g. no // left-over file descriptors or whatnot. func TestRemove(t *testing.T) { t.Run("works", func(t *testing.T) { t.Parallel() tmp := t.TempDir() touch(t, tmp, "file") w := newCollector(t) w.collect(t) addWatch(t, w.w, tmp) if err := w.w.Remove(tmp); err != nil { t.Fatal(err) } time.Sleep(200 * time.Millisecond) cat(t, "data", tmp, "file") chmod(t, 0o700, tmp, "file") have := w.stop(t) if len(have) > 0 { t.Errorf("received events; expected none:\n%s", have) } }) t.Run("remove same dir twice", func(t *testing.T) { t.Parallel() tmp := t.TempDir() touch(t, tmp, "file") w := newWatcher(t) defer w.Close() addWatch(t, w, tmp) if err := w.Remove(tmp); err != nil { t.Fatal(err) } err := w.Remove(tmp) if err == nil { t.Fatal("no error") } if !errors.Is(err, ErrNonExistentWatch) { t.Fatalf("wrong error: %T", err) } }) // Make sure that concurrent calls to Remove() don't race. t.Run("no race", func(t *testing.T) { t.Parallel() tmp := t.TempDir() touch(t, tmp, "file") for i := 0; i < 10; i++ { w := newWatcher(t) defer w.Close() addWatch(t, w, tmp) done := make(chan struct{}) go func() { defer func() { done <- struct{}{} }() w.Remove(tmp) }() go func() { defer func() { done <- struct{}{} }() w.Remove(tmp) }() <-done <-done w.Close() } }) // Make sure file handles are correctly released. // // regression test for #42 see https://gist.github.com/timshannon/603f92824c5294269797 t.Run("", func(t *testing.T) { w := newWatcher(t) defer w.Close() // consume the events var werr error var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() for { select { case werr = <-w.Errors: return case <-w.Events: } } }() tmp := t.TempDir() dir := join(tmp, "child") addWatch(t, w, tmp) mkdir(t, dir) addWatch(t, w, dir) // start watching child rmWatch(t, w, dir) // stop watching child rmAll(t, dir) // delete child dir // Child dir should no longer exist _, err := os.Stat(dir) if err == nil { t.Fatalf("dir %q should no longer exist!", dir) } if _, ok := err.(*os.PathError); err != nil && !ok { t.Errorf("Expected a PathError, got %v", err) } w.Close() wg.Wait() if werr != nil { t.Fatal(werr) } }) t.Run("remove with ... when non-recursive", func(t *testing.T) { recurseOnly(t) t.Parallel() tmp := t.TempDir() w := newWatcher(t) addWatch(t, w, tmp) if err := w.Remove(join(tmp, "...")); err == nil { t.Fatal("err was nil") } if err := w.Remove(tmp); err != nil { t.Fatal(err) } }) } func TestEventString(t *testing.T) { tests := []struct { in Event want string }{ {Event{}, `[no events] ""`}, {Event{"/file", 0}, `[no events] "/file"`}, {Event{"/file", Chmod | Create}, `CREATE|CHMOD "/file"`}, {Event{"/file", Rename}, `RENAME "/file"`}, {Event{"/file", Remove}, `REMOVE "/file"`}, {Event{"/file", Write | Chmod}, `WRITE|CHMOD "/file"`}, } for _, tt := range tests { t.Run("", func(t *testing.T) { have := tt.in.String() if have != tt.want { t.Errorf("\nhave: %q\nwant: %q", have, tt.want) } }) } } // Verify the watcher can keep up with file creations/deletions when under load. func TestWatchStress(t *testing.T) { if isCI() { t.Skip("fails too often on the CI") // TODO } // On NetBSD ioutil.ReadDir in sendDirectoryChangeEvents() returns EINVAL // ~80% of the time: // // readdirent /tmp/TestWatchStress3584363325/001: invalid argument // // This ends up calling getdents(), the manpage says: // // [EINVAL] A directory was being read on NFS, but it was modified on the // server while it was being read. // // Which is, eh, odd? Maybe I read the code wrong and it's calling another // function too(?) // // Because this happens on the Errors channel we can't "skip" it like with // other kqueue platorms, so just skip the entire test for now. // // TODO: fix this. if runtime.GOOS == "netbsd" { t.Skip("broken on NetBSD") } Errorf := func(t *testing.T, msg string, args ...interface{}) { if !isKqueue() { t.Errorf(msg, args...) return } // On kqueue platforms it doesn't seem to sync properly; see comment for // the sleep below. // // TODO: fix this. t.Logf(msg, args...) t.Skip("flaky on kqueue; allowed to fail") } tmp := t.TempDir() w := newCollector(t, tmp) w.collect(t) fmtNum := func(n int) string { s := fmt.Sprintf("%09d", n) return s[:3] + "_" + s[3:6] + "_" + s[6:] } var ( numFiles = 1_500_000 runFor = 30 * time.Second ) if testing.Short() { runFor = time.Second } // Otherwise platforms with low limits such as as OpenBSD and NetBSD will // fail, since every watched file uses a file descriptor. Need superuser // permissions and twiddling with /etc/login.conf to adjust them, so we // can't "just increase it". if isKqueue() && uint64(numFiles) > internal.Maxfiles() { numFiles = int(internal.Maxfiles()) - 100 t.Logf("limiting files to %d due to max open files limit", numFiles) } var ( prefix = "xyz-prefix-" done = make(chan struct{}) ) // testing.Short() go func() { numFiles = createFiles(t, tmp, prefix, numFiles, runFor) // TODO: this shouldn't be needed; and if this is too short some very // odd events happen: // // fsnotify_test.go:837: saw 42 unexpected events: // REMOVE "" // CREATE "." // REMOVE "" // CREATE "." // REMOVE "" // ... // // fsnotify_test.go:848: expected the following 3175 events, but didn't see them (showing first 100 only) // REMOVE "/xyz-prefix-000_015_080" // REMOVE "/xyz-prefix-000_014_536" // CREATE "/xyz-prefix-000_015_416" // CREATE "/xyz-prefix-000_015_406" // ... // // Should really add a Sync() method which processes all outstanding // events. if isKqueue() { time.Sleep(1000 * time.Millisecond) if !testing.Short() { time.Sleep(1000 * time.Millisecond) } } close(done) }() <-done have := w.stopWait(t, 10*time.Second) // Do some work to get reasonably nice error reports; what cmpEvents() gives // us is nice if you have just a few events, but with thousands it qiuckly // gets unwieldy. want := make(map[Event]struct{}) for i := 0; i < numFiles; i++ { n := "/" + prefix + fmtNum(i) want[Event{Name: n, Op: Remove}] = struct{}{} want[Event{Name: n, Op: Create}] = struct{}{} } var extra Events for _, h := range have { h.Name = filepath.ToSlash(strings.TrimPrefix(h.Name, tmp)) _, ok := want[h] if ok { delete(want, h) } else { extra = append(extra, h) } } if len(extra) > 0 { if len(extra) > 100 { Errorf(t, "saw %d unexpected events (showing first 100 only):\n%s", len(extra), extra[:100]) } else { Errorf(t, "saw %d unexpected events:\n%s", len(extra), extra) } } if len(want) != 0 { wantE := make(Events, 0, len(want)) for k := range want { wantE = append(wantE, k) } if len(wantE) > 100 { Errorf(t, "expected the following %d events, but didn't see them (showing first 100 only)\n%s", len(wantE), wantE[:100]) } else { Errorf(t, "expected the following %d events, but didn't see them\n%s", len(wantE), wantE) } } } func TestWatchList(t *testing.T) { if runtime.GOOS == "windows" { // TODO: probably should I guess... t.Skip("WatchList has always been broken on Windows and I don't feel like fixing it") } t.Parallel() tmp := t.TempDir() file := join(tmp, "file") other := join(tmp, "other") touch(t, file) touch(t, other) w := newWatcher(t, file, tmp) defer w.Close() have := w.WatchList() sort.Strings(have) want := []string{tmp, file} if !reflect.DeepEqual(have, want) { t.Errorf("\nhave: %s\nwant: %s", have, want) } } func TestOpHas(t *testing.T) { tests := []struct { name string o Op h Op want bool }{ { name: "single bit match", o: Remove, h: Remove, want: true, }, { name: "single bit no match", o: Remove, h: Create, want: false, }, { name: "two bits match", o: Remove | Create, h: Create, want: true, }, { name: "two bits no match", o: Remove | Create, h: Chmod, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.o.Has(tt.h); got != tt.want { t.Errorf("Has() = %v, want %v", got, tt.want) } }) } } func BenchmarkWatch(b *testing.B) { do := func(b *testing.B, w *Watcher) { tmp := b.TempDir() file := join(tmp, "file") err := w.Add(tmp) if err != nil { b.Fatal(err) } var wg sync.WaitGroup wg.Add(1) go func() { for { select { case err, ok := <-w.Errors: if !ok { wg.Done() return } b.Error(err) case _, ok := <-w.Events: if !ok { wg.Done() return } } } }() b.ResetTimer() for n := 0; n < b.N; n++ { fp, err := os.Create(file) if err != nil { b.Fatal(err) } err = fp.Close() if err != nil { b.Fatal(err) } } err = w.Close() if err != nil { b.Fatal(err) } wg.Wait() } b.Run("default", func(b *testing.B) { w, err := NewWatcher() if err != nil { b.Fatal(err) } do(b, w) }) b.Run("buffered=1", func(b *testing.B) { w, err := NewBufferedWatcher(1) if err != nil { b.Fatal(err) } do(b, w) }) b.Run("buffered=1024", func(b *testing.B) { w, err := NewBufferedWatcher(1024) if err != nil { b.Fatal(err) } do(b, w) }) b.Run("buffered=4096", func(b *testing.B) { w, err := NewBufferedWatcher(4096) if err != nil { b.Fatal(err) } do(b, w) }) } func BenchmarkAddRemove(b *testing.B) { do := func(b *testing.B, w *Watcher) { tmp := b.TempDir() b.ResetTimer() for n := 0; n < b.N; n++ { if err := w.Add(tmp); err != nil { b.Fatal(err) } if err := w.Remove(tmp); err != nil { b.Fatal(err) } } } b.Run("default", func(b *testing.B) { w, err := NewWatcher() if err != nil { b.Fatal(err) } do(b, w) }) b.Run("buffered=1", func(b *testing.B) { w, err := NewBufferedWatcher(1) if err != nil { b.Fatal(err) } do(b, w) }) b.Run("buffered=1024", func(b *testing.B) { w, err := NewBufferedWatcher(1024) if err != nil { b.Fatal(err) } do(b, w) }) b.Run("buffered=4096", func(b *testing.B) { w, err := NewBufferedWatcher(4096) if err != nil { b.Fatal(err) } do(b, w) }) } fsnotify-1.7.0/go.mod000066400000000000000000000004261451514000000144560ustar00rootroot00000000000000module github.com/fsnotify/fsnotify go 1.17 require golang.org/x/sys v0.4.0 retract ( v1.5.3 // Published an incorrect branch accidentally https://github.com/fsnotify/fsnotify/issues/445 v1.5.0 // Contains symlink regression https://github.com/fsnotify/fsnotify/pull/394 ) fsnotify-1.7.0/go.sum000066400000000000000000000002271451514000000145020ustar00rootroot00000000000000golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= fsnotify-1.7.0/helpers_test.go000066400000000000000000000315211451514000000164000ustar00rootroot00000000000000package fsnotify import ( "fmt" "io/fs" "os" "path/filepath" "runtime" "sort" "strconv" "strings" "sync" "testing" "time" "github.com/fsnotify/fsnotify/internal" ) type testCase struct { name string ops func(*testing.T, *Watcher, string) want string } func (tt testCase) run(t *testing.T) { t.Helper() t.Run(tt.name, func(t *testing.T) { t.Helper() t.Parallel() tmp := t.TempDir() w := newCollector(t) w.collect(t) tt.ops(t, w.w, tmp) cmpEvents(t, tmp, w.stop(t), newEvents(t, tt.want)) }) } // We wait a little bit after most commands; gives the system some time to sync // things and makes things more consistent across platforms. func eventSeparator() { time.Sleep(50 * time.Millisecond) } func waitForEvents() { time.Sleep(500 * time.Millisecond) } // To test the buffered watcher we run the tests twice in the CI: once as "go // test" and once with FSNOTIFY_BUFFER set. This is a bit hacky, but saves // having to refactor a lot of this code. Besides, running the tests in the CI // more than once isn't a bad thing, since it helps catch flaky tests (should // probably run it even more). var testBuffered = func() uint { s, ok := os.LookupEnv("FSNOTIFY_BUFFER") if ok { i, err := strconv.ParseUint(s, 0, 0) if err != nil { panic(fmt.Sprintf("FSNOTIFY_BUFFER: %s", err)) } return uint(i) } return 0 }() // newWatcher initializes an fsnotify Watcher instance. func newWatcher(t *testing.T, add ...string) *Watcher { t.Helper() var ( w *Watcher err error ) if testBuffered > 0 { w, err = NewBufferedWatcher(testBuffered) } else { w, err = NewWatcher() } if err != nil { t.Fatalf("newWatcher: %s", err) } for _, a := range add { err := w.Add(a) if err != nil { t.Fatalf("newWatcher: add %q: %s", a, err) } } return w } // addWatch adds a watch for a directory func addWatch(t *testing.T, w *Watcher, path ...string) { t.Helper() if len(path) < 1 { t.Fatalf("addWatch: path must have at least one element: %s", path) } err := w.Add(join(path...)) if err != nil { t.Fatalf("addWatch(%q): %s", join(path...), err) } } // rmWatch removes a watch. func rmWatch(t *testing.T, watcher *Watcher, path ...string) { t.Helper() if len(path) < 1 { t.Fatalf("rmWatch: path must have at least one element: %s", path) } err := watcher.Remove(join(path...)) if err != nil { t.Fatalf("rmWatch(%q): %s", join(path...), err) } } const noWait = "" func shouldWait(path ...string) bool { // Take advantage of the fact that join skips empty parameters. for _, p := range path { if p == "" { return false } } return true } // Create n empty files with the prefix in the directory dir. func createFiles(t *testing.T, dir, prefix string, n int, d time.Duration) int { t.Helper() if d == 0 { d = 9 * time.Minute } fmtNum := func(n int) string { s := fmt.Sprintf("%09d", n) return s[:3] + "_" + s[3:6] + "_" + s[6:] } var ( max = time.After(d) created int ) for i := 0; i < n; i++ { select { case <-max: t.Logf("createFiles: stopped at %s files because it took longer than %s", fmtNum(created), d) return created default: path := join(dir, prefix+fmtNum(i)) fp, err := os.Create(path) if err != nil { t.Errorf("create failed for %s: %s", fmtNum(i), err) continue } if err := fp.Close(); err != nil { t.Errorf("close failed for %s: %s", fmtNum(i), err) } if err := os.Remove(path); err != nil { t.Errorf("remove failed for %s: %s", fmtNum(i), err) } if i%10_000 == 0 { t.Logf("createFiles: %s", fmtNum(i)) } created++ } } return created } // mkdir func mkdir(t *testing.T, path ...string) { t.Helper() if len(path) < 1 { t.Fatalf("mkdir: path must have at least one element: %s", path) } err := os.Mkdir(join(path...), 0o0755) if err != nil { t.Fatalf("mkdir(%q): %s", join(path...), err) } if shouldWait(path...) { eventSeparator() } } // mkdir -p func mkdirAll(t *testing.T, path ...string) { t.Helper() if len(path) < 1 { t.Fatalf("mkdirAll: path must have at least one element: %s", path) } err := os.MkdirAll(join(path...), 0o0755) if err != nil { t.Fatalf("mkdirAll(%q): %s", join(path...), err) } if shouldWait(path...) { eventSeparator() } } // ln -s func symlink(t *testing.T, target string, link ...string) { t.Helper() if len(link) < 1 { t.Fatalf("symlink: link must have at least one element: %s", link) } err := os.Symlink(target, join(link...)) if err != nil { t.Fatalf("symlink(%q, %q): %s", target, join(link...), err) } if shouldWait(link...) { eventSeparator() } } // mkfifo func mkfifo(t *testing.T, path ...string) { t.Helper() if len(path) < 1 { t.Fatalf("mkfifo: path must have at least one element: %s", path) } err := internal.Mkfifo(join(path...), 0o644) if err != nil { t.Fatalf("mkfifo(%q): %s", join(path...), err) } if shouldWait(path...) { eventSeparator() } } // mknod func mknod(t *testing.T, dev int, path ...string) { t.Helper() if len(path) < 1 { t.Fatalf("mknod: path must have at least one element: %s", path) } err := internal.Mknod(join(path...), 0o644, dev) if err != nil { t.Fatalf("mknod(%d, %q): %s", dev, join(path...), err) } if shouldWait(path...) { eventSeparator() } } // cat func cat(t *testing.T, data string, path ...string) { t.Helper() if len(path) < 1 { t.Fatalf("cat: path must have at least one element: %s", path) } err := func() error { fp, err := os.OpenFile(join(path...), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { return err } if err := fp.Sync(); err != nil { return err } if shouldWait(path...) { eventSeparator() } if _, err := fp.WriteString(data); err != nil { return err } if err := fp.Sync(); err != nil { return err } if shouldWait(path...) { eventSeparator() } return fp.Close() }() if err != nil { t.Fatalf("cat(%q): %s", join(path...), err) } } // touch func touch(t *testing.T, path ...string) { t.Helper() if len(path) < 1 { t.Fatalf("touch: path must have at least one element: %s", path) } fp, err := os.Create(join(path...)) if err != nil { t.Fatalf("touch(%q): %s", join(path...), err) } err = fp.Close() if err != nil { t.Fatalf("touch(%q): %s", join(path...), err) } if shouldWait(path...) { eventSeparator() } } // mv func mv(t *testing.T, src string, dst ...string) { t.Helper() if len(dst) < 1 { t.Fatalf("mv: dst must have at least one element: %s", dst) } err := os.Rename(src, join(dst...)) if err != nil { t.Fatalf("mv(%q, %q): %s", src, join(dst...), err) } if shouldWait(dst...) { eventSeparator() } } // rm func rm(t *testing.T, path ...string) { t.Helper() if len(path) < 1 { t.Fatalf("rm: path must have at least one element: %s", path) } err := os.Remove(join(path...)) if err != nil { t.Fatalf("rm(%q): %s", join(path...), err) } if shouldWait(path...) { eventSeparator() } } // rm -r func rmAll(t *testing.T, path ...string) { t.Helper() if len(path) < 1 { t.Fatalf("rmAll: path must have at least one element: %s", path) } err := os.RemoveAll(join(path...)) if err != nil { t.Fatalf("rmAll(%q): %s", join(path...), err) } if shouldWait(path...) { eventSeparator() } } // chmod func chmod(t *testing.T, mode fs.FileMode, path ...string) { t.Helper() if len(path) < 1 { t.Fatalf("chmod: path must have at least one element: %s", path) } err := os.Chmod(join(path...), mode) if err != nil { t.Fatalf("chmod(%q): %s", join(path...), err) } if shouldWait(path...) { eventSeparator() } } // Collect all events in an array. // // w := newCollector(t) // w.collect(r) // // .. do stuff .. // // events := w.stop(t) type eventCollector struct { w *Watcher e Events mu sync.Mutex done chan struct{} } func newCollector(t *testing.T, add ...string) *eventCollector { return &eventCollector{ w: newWatcher(t, add...), done: make(chan struct{}), e: make(Events, 0, 8), } } // stop collecting events and return what we've got. func (w *eventCollector) stop(t *testing.T) Events { return w.stopWait(t, time.Second) } func (w *eventCollector) stopWait(t *testing.T, waitFor time.Duration) Events { waitForEvents() go func() { err := w.w.Close() if err != nil { t.Error(err) } }() select { case <-time.After(waitFor): t.Fatalf("event stream was not closed after %s", waitFor) case <-w.done: } w.mu.Lock() defer w.mu.Unlock() return w.e } // Get all events we've found up to now and clear the event buffer. func (w *eventCollector) events(t *testing.T) Events { w.mu.Lock() defer w.mu.Unlock() e := make(Events, len(w.e)) copy(e, w.e) w.e = make(Events, 0, 16) return e } // Start collecting events. func (w *eventCollector) collect(t *testing.T) { go func() { for { select { case e, ok := <-w.w.Errors: if !ok { w.done <- struct{}{} return } t.Error(e) w.done <- struct{}{} return case e, ok := <-w.w.Events: if !ok { w.done <- struct{}{} return } w.mu.Lock() w.e = append(w.e, e) w.mu.Unlock() } } }() } type Events []Event func (e Events) String() string { b := new(strings.Builder) for i, ee := range e { if i > 0 { b.WriteString("\n") } fmt.Fprintf(b, "%-20s %q", ee.Op.String(), filepath.ToSlash(ee.Name)) } return b.String() } func (e Events) TrimPrefix(prefix string) Events { for i := range e { if e[i].Name == prefix { e[i].Name = "/" } else { e[i].Name = strings.TrimPrefix(e[i].Name, prefix) } } return e } func (e Events) copy() Events { cp := make(Events, len(e)) copy(cp, e) return cp } // Create a new Events list from a string; for example: // // CREATE path // CREATE|WRITE path // // Every event is one line, and any whitespace between the event and path are // ignored. The path can optionally be surrounded in ". Anything after a "#" is // ignored. // // Platform-specific tests can be added after GOOS: // // # Tested if nothing else matches // CREATE path // // # Windows-specific test. // windows: // WRITE path // // You can specify multiple platforms with a comma (e.g. "windows, linux:"). // "kqueue" is a shortcut for all kqueue systems (BSD, macOS). func newEvents(t *testing.T, s string) Events { t.Helper() var ( lines = strings.Split(s, "\n") groups = []string{""} events = make(map[string]Events) ) for no, line := range lines { if i := strings.IndexByte(line, '#'); i > -1 { line = line[:i] } line = strings.TrimSpace(line) if line == "" { continue } if strings.HasSuffix(line, ":") { groups = strings.Split(strings.TrimRight(line, ":"), ",") for i := range groups { groups[i] = strings.TrimSpace(groups[i]) } continue } fields := strings.Fields(line) if len(fields) < 2 { if strings.ToUpper(fields[0]) == "EMPTY" { for _, g := range groups { events[g] = Events{} } continue } t.Fatalf("newEvents: line %d has less than 2 fields: %s", no, line) } path := strings.Trim(fields[len(fields)-1], `"`) var op Op for _, e := range fields[:len(fields)-1] { if e == "|" { continue } for _, ee := range strings.Split(e, "|") { switch strings.ToUpper(ee) { case "CREATE": op |= Create case "WRITE": op |= Write case "REMOVE": op |= Remove case "RENAME": op |= Rename case "CHMOD": op |= Chmod default: t.Fatalf("newEvents: line %d has unknown event %q: %s", no, ee, line) } } } for _, g := range groups { events[g] = append(events[g], Event{Name: path, Op: op}) } } if e, ok := events[runtime.GOOS]; ok { return e } switch runtime.GOOS { // kqueue shortcut case "freebsd", "netbsd", "openbsd", "dragonfly", "darwin": if e, ok := events["kqueue"]; ok { return e } // fen shortcut case "solaris", "illumos": if e, ok := events["fen"]; ok { return e } } return events[""] } func cmpEvents(t *testing.T, tmp string, have, want Events) { t.Helper() have = have.TrimPrefix(tmp) haveSort, wantSort := have.copy(), want.copy() sort.Slice(haveSort, func(i, j int) bool { return haveSort[i].String() > haveSort[j].String() }) sort.Slice(wantSort, func(i, j int) bool { return wantSort[i].String() > wantSort[j].String() }) if haveSort.String() != wantSort.String() { //t.Error("\n" + ztest.Diff(indent(haveSort), indent(wantSort))) t.Errorf("\nhave:\n%s\nwant:\n%s", indent(have), indent(want)) } } func indent(s fmt.Stringer) string { return "\t" + strings.ReplaceAll(s.String(), "\n", "\n\t") } var join = filepath.Join func isCI() bool { _, ok := os.LookupEnv("CI") return ok } func isKqueue() bool { switch runtime.GOOS { case "darwin", "freebsd", "openbsd", "netbsd", "dragonfly": return true } return false } func isSolaris() bool { switch runtime.GOOS { case "illumos", "solaris": return true } return false } func recurseOnly(t *testing.T) { switch runtime.GOOS { //case "windows": // Run test. default: t.Skip("recursion not yet supported on " + runtime.GOOS) } } fsnotify-1.7.0/internal/000077500000000000000000000000001451514000000151625ustar00rootroot00000000000000fsnotify-1.7.0/internal/darwin.go000066400000000000000000000017361451514000000170040ustar00rootroot00000000000000//go:build darwin // +build darwin package internal import ( "syscall" "golang.org/x/sys/unix" ) var ( SyscallEACCES = syscall.EACCES UnixEACCES = unix.EACCES ) var maxfiles uint64 // Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/ func SetRlimit() { var l syscall.Rlimit err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l) if err == nil && l.Cur != l.Max { l.Cur = l.Max syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l) } maxfiles = l.Cur if n, err := syscall.SysctlUint32("kern.maxfiles"); err == nil && uint64(n) < maxfiles { maxfiles = uint64(n) } if n, err := syscall.SysctlUint32("kern.maxfilesperproc"); err == nil && uint64(n) < maxfiles { maxfiles = uint64(n) } } func Maxfiles() uint64 { return maxfiles } func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) } func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, dev) } fsnotify-1.7.0/internal/debug_darwin.go000066400000000000000000000040351451514000000201450ustar00rootroot00000000000000package internal import "golang.org/x/sys/unix" var names = []struct { n string m uint32 }{ {"NOTE_ABSOLUTE", unix.NOTE_ABSOLUTE}, {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, {"NOTE_BACKGROUND", unix.NOTE_BACKGROUND}, {"NOTE_CHILD", unix.NOTE_CHILD}, {"NOTE_CRITICAL", unix.NOTE_CRITICAL}, {"NOTE_DELETE", unix.NOTE_DELETE}, {"NOTE_EXEC", unix.NOTE_EXEC}, {"NOTE_EXIT", unix.NOTE_EXIT}, {"NOTE_EXITSTATUS", unix.NOTE_EXITSTATUS}, {"NOTE_EXIT_CSERROR", unix.NOTE_EXIT_CSERROR}, {"NOTE_EXIT_DECRYPTFAIL", unix.NOTE_EXIT_DECRYPTFAIL}, {"NOTE_EXIT_DETAIL", unix.NOTE_EXIT_DETAIL}, {"NOTE_EXIT_DETAIL_MASK", unix.NOTE_EXIT_DETAIL_MASK}, {"NOTE_EXIT_MEMORY", unix.NOTE_EXIT_MEMORY}, {"NOTE_EXIT_REPARENTED", unix.NOTE_EXIT_REPARENTED}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, {"NOTE_FFAND", unix.NOTE_FFAND}, {"NOTE_FFCOPY", unix.NOTE_FFCOPY}, {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK}, {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK}, {"NOTE_FFNOP", unix.NOTE_FFNOP}, {"NOTE_FFOR", unix.NOTE_FFOR}, {"NOTE_FORK", unix.NOTE_FORK}, {"NOTE_FUNLOCK", unix.NOTE_FUNLOCK}, {"NOTE_LEEWAY", unix.NOTE_LEEWAY}, {"NOTE_LINK", unix.NOTE_LINK}, {"NOTE_LOWAT", unix.NOTE_LOWAT}, {"NOTE_MACHTIME", unix.NOTE_MACHTIME}, {"NOTE_MACH_CONTINUOUS_TIME", unix.NOTE_MACH_CONTINUOUS_TIME}, {"NOTE_NONE", unix.NOTE_NONE}, {"NOTE_NSECONDS", unix.NOTE_NSECONDS}, {"NOTE_OOB", unix.NOTE_OOB}, //{"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, -0x100000 (?!) {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, {"NOTE_REAP", unix.NOTE_REAP}, {"NOTE_RENAME", unix.NOTE_RENAME}, {"NOTE_REVOKE", unix.NOTE_REVOKE}, {"NOTE_SECONDS", unix.NOTE_SECONDS}, {"NOTE_SIGNAL", unix.NOTE_SIGNAL}, {"NOTE_TRACK", unix.NOTE_TRACK}, {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, {"NOTE_TRIGGER", unix.NOTE_TRIGGER}, {"NOTE_USECONDS", unix.NOTE_USECONDS}, {"NOTE_VM_ERROR", unix.NOTE_VM_ERROR}, {"NOTE_VM_PRESSURE", unix.NOTE_VM_PRESSURE}, {"NOTE_VM_PRESSURE_SUDDEN_TERMINATE", unix.NOTE_VM_PRESSURE_SUDDEN_TERMINATE}, {"NOTE_VM_PRESSURE_TERMINATE", unix.NOTE_VM_PRESSURE_TERMINATE}, {"NOTE_WRITE", unix.NOTE_WRITE}, } fsnotify-1.7.0/internal/debug_dragonfly.go000066400000000000000000000016761451514000000206560ustar00rootroot00000000000000package internal import "golang.org/x/sys/unix" var names = []struct { n string m uint32 }{ {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, {"NOTE_CHILD", unix.NOTE_CHILD}, {"NOTE_DELETE", unix.NOTE_DELETE}, {"NOTE_EXEC", unix.NOTE_EXEC}, {"NOTE_EXIT", unix.NOTE_EXIT}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, {"NOTE_FFAND", unix.NOTE_FFAND}, {"NOTE_FFCOPY", unix.NOTE_FFCOPY}, {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK}, {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK}, {"NOTE_FFNOP", unix.NOTE_FFNOP}, {"NOTE_FFOR", unix.NOTE_FFOR}, {"NOTE_FORK", unix.NOTE_FORK}, {"NOTE_LINK", unix.NOTE_LINK}, {"NOTE_LOWAT", unix.NOTE_LOWAT}, {"NOTE_OOB", unix.NOTE_OOB}, {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, {"NOTE_RENAME", unix.NOTE_RENAME}, {"NOTE_REVOKE", unix.NOTE_REVOKE}, {"NOTE_TRACK", unix.NOTE_TRACK}, {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, {"NOTE_TRIGGER", unix.NOTE_TRIGGER}, {"NOTE_WRITE", unix.NOTE_WRITE}, } fsnotify-1.7.0/internal/debug_freebsd.go000066400000000000000000000024361451514000000202760ustar00rootroot00000000000000package internal import "golang.org/x/sys/unix" var names = []struct { n string m uint32 }{ {"NOTE_ABSTIME", unix.NOTE_ABSTIME}, {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, {"NOTE_CHILD", unix.NOTE_CHILD}, {"NOTE_CLOSE", unix.NOTE_CLOSE}, {"NOTE_CLOSE_WRITE", unix.NOTE_CLOSE_WRITE}, {"NOTE_DELETE", unix.NOTE_DELETE}, {"NOTE_EXEC", unix.NOTE_EXEC}, {"NOTE_EXIT", unix.NOTE_EXIT}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, {"NOTE_FFAND", unix.NOTE_FFAND}, {"NOTE_FFCOPY", unix.NOTE_FFCOPY}, {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK}, {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK}, {"NOTE_FFNOP", unix.NOTE_FFNOP}, {"NOTE_FFOR", unix.NOTE_FFOR}, {"NOTE_FILE_POLL", unix.NOTE_FILE_POLL}, {"NOTE_FORK", unix.NOTE_FORK}, {"NOTE_LINK", unix.NOTE_LINK}, {"NOTE_LOWAT", unix.NOTE_LOWAT}, {"NOTE_MSECONDS", unix.NOTE_MSECONDS}, {"NOTE_NSECONDS", unix.NOTE_NSECONDS}, {"NOTE_OPEN", unix.NOTE_OPEN}, {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, {"NOTE_READ", unix.NOTE_READ}, {"NOTE_RENAME", unix.NOTE_RENAME}, {"NOTE_REVOKE", unix.NOTE_REVOKE}, {"NOTE_SECONDS", unix.NOTE_SECONDS}, {"NOTE_TRACK", unix.NOTE_TRACK}, {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, {"NOTE_TRIGGER", unix.NOTE_TRIGGER}, {"NOTE_USECONDS", unix.NOTE_USECONDS}, {"NOTE_WRITE", unix.NOTE_WRITE}, } fsnotify-1.7.0/internal/debug_kqueue.go000066400000000000000000000007601451514000000201610ustar00rootroot00000000000000//go:build freebsd || openbsd || netbsd || dragonfly || darwin // +build freebsd openbsd netbsd dragonfly darwin package internal import ( "fmt" "os" "strings" "time" "golang.org/x/sys/unix" ) func Debug(name string, kevent *unix.Kevent_t) { mask := uint32(kevent.Fflags) var l []string for _, n := range names { if mask&n.m == n.m { l = append(l, n.n) } } fmt.Fprintf(os.Stderr, "%s %-20s → %s\n", time.Now().Format("15:04:05.0000"), strings.Join(l, " | "), name) } fsnotify-1.7.0/internal/debug_linux.go000066400000000000000000000034441451514000000200230ustar00rootroot00000000000000package internal import ( "fmt" "os" "strings" "time" "golang.org/x/sys/unix" ) func Debug(name string, mask uint32) { names := []struct { n string m uint32 }{ {"IN_ACCESS", unix.IN_ACCESS}, {"IN_ALL_EVENTS", unix.IN_ALL_EVENTS}, {"IN_ATTRIB", unix.IN_ATTRIB}, {"IN_CLASSA_HOST", unix.IN_CLASSA_HOST}, {"IN_CLASSA_MAX", unix.IN_CLASSA_MAX}, {"IN_CLASSA_NET", unix.IN_CLASSA_NET}, {"IN_CLASSA_NSHIFT", unix.IN_CLASSA_NSHIFT}, {"IN_CLASSB_HOST", unix.IN_CLASSB_HOST}, {"IN_CLASSB_MAX", unix.IN_CLASSB_MAX}, {"IN_CLASSB_NET", unix.IN_CLASSB_NET}, {"IN_CLASSB_NSHIFT", unix.IN_CLASSB_NSHIFT}, {"IN_CLASSC_HOST", unix.IN_CLASSC_HOST}, {"IN_CLASSC_NET", unix.IN_CLASSC_NET}, {"IN_CLASSC_NSHIFT", unix.IN_CLASSC_NSHIFT}, {"IN_CLOSE", unix.IN_CLOSE}, {"IN_CLOSE_NOWRITE", unix.IN_CLOSE_NOWRITE}, {"IN_CLOSE_WRITE", unix.IN_CLOSE_WRITE}, {"IN_CREATE", unix.IN_CREATE}, {"IN_DELETE", unix.IN_DELETE}, {"IN_DELETE_SELF", unix.IN_DELETE_SELF}, {"IN_DONT_FOLLOW", unix.IN_DONT_FOLLOW}, {"IN_EXCL_UNLINK", unix.IN_EXCL_UNLINK}, {"IN_IGNORED", unix.IN_IGNORED}, {"IN_ISDIR", unix.IN_ISDIR}, {"IN_LOOPBACKNET", unix.IN_LOOPBACKNET}, {"IN_MASK_ADD", unix.IN_MASK_ADD}, {"IN_MASK_CREATE", unix.IN_MASK_CREATE}, {"IN_MODIFY", unix.IN_MODIFY}, {"IN_MOVE", unix.IN_MOVE}, {"IN_MOVED_FROM", unix.IN_MOVED_FROM}, {"IN_MOVED_TO", unix.IN_MOVED_TO}, {"IN_MOVE_SELF", unix.IN_MOVE_SELF}, {"IN_ONESHOT", unix.IN_ONESHOT}, {"IN_ONLYDIR", unix.IN_ONLYDIR}, {"IN_OPEN", unix.IN_OPEN}, {"IN_Q_OVERFLOW", unix.IN_Q_OVERFLOW}, {"IN_UNMOUNT", unix.IN_UNMOUNT}, } var l []string for _, n := range names { if mask&n.m == n.m { l = append(l, n.n) } } fmt.Fprintf(os.Stderr, "%s %-20s → %s\n", time.Now().Format("15:04:05.0000"), strings.Join(l, " | "), name) } fsnotify-1.7.0/internal/debug_netbsd.go000066400000000000000000000012321451514000000201340ustar00rootroot00000000000000package internal import "golang.org/x/sys/unix" var names = []struct { n string m uint32 }{ {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, {"NOTE_CHILD", unix.NOTE_CHILD}, {"NOTE_DELETE", unix.NOTE_DELETE}, {"NOTE_EXEC", unix.NOTE_EXEC}, {"NOTE_EXIT", unix.NOTE_EXIT}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, {"NOTE_FORK", unix.NOTE_FORK}, {"NOTE_LINK", unix.NOTE_LINK}, {"NOTE_LOWAT", unix.NOTE_LOWAT}, {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, {"NOTE_RENAME", unix.NOTE_RENAME}, {"NOTE_REVOKE", unix.NOTE_REVOKE}, {"NOTE_TRACK", unix.NOTE_TRACK}, {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, {"NOTE_WRITE", unix.NOTE_WRITE}, } fsnotify-1.7.0/internal/debug_openbsd.go000066400000000000000000000014261451514000000203140ustar00rootroot00000000000000package internal import "golang.org/x/sys/unix" var names = []struct { n string m uint32 }{ {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, // {"NOTE_CHANGE", unix.NOTE_CHANGE}, // Not on 386? {"NOTE_CHILD", unix.NOTE_CHILD}, {"NOTE_DELETE", unix.NOTE_DELETE}, {"NOTE_EOF", unix.NOTE_EOF}, {"NOTE_EXEC", unix.NOTE_EXEC}, {"NOTE_EXIT", unix.NOTE_EXIT}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, {"NOTE_FORK", unix.NOTE_FORK}, {"NOTE_LINK", unix.NOTE_LINK}, {"NOTE_LOWAT", unix.NOTE_LOWAT}, {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, {"NOTE_RENAME", unix.NOTE_RENAME}, {"NOTE_REVOKE", unix.NOTE_REVOKE}, {"NOTE_TRACK", unix.NOTE_TRACK}, {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, {"NOTE_TRUNCATE", unix.NOTE_TRUNCATE}, {"NOTE_WRITE", unix.NOTE_WRITE}, } fsnotify-1.7.0/internal/debug_solaris.go000066400000000000000000000014601451514000000203340ustar00rootroot00000000000000package internal import ( "fmt" "os" "strings" "time" "golang.org/x/sys/unix" ) func Debug(name string, mask int32) { names := []struct { n string m int32 }{ {"FILE_ACCESS", unix.FILE_ACCESS}, {"FILE_MODIFIED", unix.FILE_MODIFIED}, {"FILE_ATTRIB", unix.FILE_ATTRIB}, {"FILE_TRUNC", unix.FILE_TRUNC}, {"FILE_NOFOLLOW", unix.FILE_NOFOLLOW}, {"FILE_DELETE", unix.FILE_DELETE}, {"FILE_RENAME_TO", unix.FILE_RENAME_TO}, {"FILE_RENAME_FROM", unix.FILE_RENAME_FROM}, {"UNMOUNTED", unix.UNMOUNTED}, {"MOUNTEDOVER", unix.MOUNTEDOVER}, {"FILE_EXCEPTION", unix.FILE_EXCEPTION}, } var l []string for _, n := range names { if mask&n.m == n.m { l = append(l, n.n) } } fmt.Fprintf(os.Stderr, "%s %-20s → %s\n", time.Now().Format("15:04:05.0000"), strings.Join(l, " | "), name) } fsnotify-1.7.0/internal/debug_windows.go000066400000000000000000000024301451514000000203500ustar00rootroot00000000000000package internal import ( "fmt" "os" "strings" "time" "golang.org/x/sys/windows" ) func Debug(name string, mask uint32) { names := []struct { n string m uint32 }{ //{"FILE_NOTIFY_CHANGE_FILE_NAME", windows.FILE_NOTIFY_CHANGE_FILE_NAME}, //{"FILE_NOTIFY_CHANGE_DIR_NAME", windows.FILE_NOTIFY_CHANGE_DIR_NAME}, //{"FILE_NOTIFY_CHANGE_ATTRIBUTES", windows.FILE_NOTIFY_CHANGE_ATTRIBUTES}, //{"FILE_NOTIFY_CHANGE_SIZE", windows.FILE_NOTIFY_CHANGE_SIZE}, //{"FILE_NOTIFY_CHANGE_LAST_WRITE", windows.FILE_NOTIFY_CHANGE_LAST_WRITE}, //{"FILE_NOTIFY_CHANGE_LAST_ACCESS", windows.FILE_NOTIFY_CHANGE_LAST_ACCESS}, //{"FILE_NOTIFY_CHANGE_CREATION", windows.FILE_NOTIFY_CHANGE_CREATION}, //{"FILE_NOTIFY_CHANGE_SECURITY", windows.FILE_NOTIFY_CHANGE_SECURITY}, {"FILE_ACTION_ADDED", windows.FILE_ACTION_ADDED}, {"FILE_ACTION_REMOVED", windows.FILE_ACTION_REMOVED}, {"FILE_ACTION_MODIFIED", windows.FILE_ACTION_MODIFIED}, {"FILE_ACTION_RENAMED_OLD_NAME", windows.FILE_ACTION_RENAMED_OLD_NAME}, {"FILE_ACTION_RENAMED_NEW_NAME", windows.FILE_ACTION_RENAMED_NEW_NAME}, } var l []string for _, n := range names { if mask&n.m == n.m { l = append(l, n.n) } } fmt.Fprintf(os.Stderr, "%s %-20s → %s\n", time.Now().Format("15:04:05.0000"), strings.Join(l, " | "), name) } fsnotify-1.7.0/internal/freebsd.go000066400000000000000000000014001451514000000171160ustar00rootroot00000000000000//go:build freebsd // +build freebsd package internal import ( "syscall" "golang.org/x/sys/unix" ) var ( SyscallEACCES = syscall.EACCES UnixEACCES = unix.EACCES ) var maxfiles uint64 func SetRlimit() { // Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/ var l syscall.Rlimit err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l) if err == nil && l.Cur != l.Max { l.Cur = l.Max syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l) } maxfiles = uint64(l.Cur) } func Maxfiles() uint64 { return maxfiles } func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) } func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, uint64(dev)) } fsnotify-1.7.0/internal/internal.go000066400000000000000000000000741451514000000173260ustar00rootroot00000000000000// Package internal contains some helpers. package internal fsnotify-1.7.0/internal/unix.go000066400000000000000000000014421451514000000164750ustar00rootroot00000000000000//go:build !windows && !darwin && !freebsd // +build !windows,!darwin,!freebsd package internal import ( "syscall" "golang.org/x/sys/unix" ) var ( SyscallEACCES = syscall.EACCES UnixEACCES = unix.EACCES ) var maxfiles uint64 func SetRlimit() { // Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/ var l syscall.Rlimit err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l) if err == nil && l.Cur != l.Max { l.Cur = l.Max syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l) } maxfiles = uint64(l.Cur) } func Maxfiles() uint64 { return maxfiles } func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) } func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, dev) } fsnotify-1.7.0/internal/unix2.go000066400000000000000000000001571451514000000165610ustar00rootroot00000000000000//go:build !windows // +build !windows package internal func HasPrivilegesForSymlink() bool { return true } fsnotify-1.7.0/internal/windows.go000066400000000000000000000017241451514000000172070ustar00rootroot00000000000000//go:build windows // +build windows package internal import ( "errors" "golang.org/x/sys/windows" ) // Just a dummy. var ( SyscallEACCES = errors.New("dummy") UnixEACCES = errors.New("dummy") ) func SetRlimit() {} func Maxfiles() uint64 { return 1<<64 - 1 } func Mkfifo(path string, mode uint32) error { return errors.New("no FIFOs on Windows") } func Mknod(path string, mode uint32, dev int) error { return errors.New("no device nodes on Windows") } func HasPrivilegesForSymlink() bool { var sid *windows.SID err := windows.AllocateAndInitializeSid( &windows.SECURITY_NT_AUTHORITY, 2, windows.SECURITY_BUILTIN_DOMAIN_RID, windows.DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &sid) if err != nil { return false } defer windows.FreeSid(sid) token := windows.Token(0) member, err := token.IsMember(sid) if err != nil { return false } return member || token.IsElevated() } fsnotify-1.7.0/mkdoc.zsh000077500000000000000000000224501451514000000151770ustar00rootroot00000000000000#!/usr/bin/env zsh [ "${ZSH_VERSION:-}" = "" ] && echo >&2 "Only works with zsh" && exit 1 setopt err_exit no_unset pipefail extended_glob # Simple script to update the godoc comments on all watchers so you don't need # to update the same comment 5 times. watcher=$(</tmp/x print -r -- $cmt >>/tmp/x tail -n+$(( end + 1 )) $file >>/tmp/x mv /tmp/x $file done } set-cmt '^type Watcher struct ' $watcher set-cmt '^func NewWatcher(' $new set-cmt '^func NewBufferedWatcher(' $newbuffered set-cmt '^func (w \*Watcher) Add(' $add set-cmt '^func (w \*Watcher) AddWith(' $addwith set-cmt '^func (w \*Watcher) Remove(' $remove set-cmt '^func (w \*Watcher) Close(' $close set-cmt '^func (w \*Watcher) WatchList(' $watchlist set-cmt '^[[:space:]]*Events *chan Event$' $events set-cmt '^[[:space:]]*Errors *chan error$' $errors fsnotify-1.7.0/system_bsd.go000066400000000000000000000003251451514000000160510ustar00rootroot00000000000000//go:build freebsd || openbsd || netbsd || dragonfly // +build freebsd openbsd netbsd dragonfly package fsnotify import "golang.org/x/sys/unix" const openMode = unix.O_NONBLOCK | unix.O_RDONLY | unix.O_CLOEXEC fsnotify-1.7.0/system_darwin.go000066400000000000000000000002641451514000000165670ustar00rootroot00000000000000//go:build darwin // +build darwin package fsnotify import "golang.org/x/sys/unix" // note: this constant is not defined on BSD const openMode = unix.O_EVTONLY | unix.O_CLOEXEC fsnotify-1.7.0/test/000077500000000000000000000000001451514000000143255ustar00rootroot00000000000000fsnotify-1.7.0/test/kqueue.c000066400000000000000000000053411451514000000157730ustar00rootroot00000000000000// This is an example kqueue program which watches a directory and all paths in // it with the same flags as those fsnotify uses. This is useful sometimes to // test what events kqueue sends with as little abstraction as possible. // // Note this does *not* set up monitoring on new files as they're created. // // Usage: // cc kqueue.c -o kqueue // ./kqueue /path/to/dir #include #include #include #include #include #include #include #include #include void die(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); if (fmt[0] && fmt[strlen(fmt)-1] == ':') { fputc(' ', stderr); perror(NULL); } else fputc('\n', stderr); exit(1); } int main(int argc, char* argv[]) { if (argc < 2) { fprintf(stderr, "usage: %s path/to/dir\n", argv[0]); return 1; } char *dir = argv[1]; int kq = kqueue(); if (kq == -1) die("kqueue:"); int fp = open(dir, O_RDONLY); if (fp == -1) die("open: %s:", dir); DIR *dp = fdopendir(fp); if (dp == NULL) die("fdopendir:"); int fds[1024] = {fp}; char *names[1024] = {dir}; int n_fds = 0; struct dirent *ls; while ((ls = readdir(dp)) != NULL) { if (ls->d_name[0] == '.') continue; char *path = malloc(strlen(dir) + strlen(ls->d_name) + 2); sprintf(path, "%s/%s", dir, ls->d_name); int fp = open(path, O_RDONLY); if (fp == -1) die("open: %s:", path); fds[++n_fds] = fp; names[n_fds] = path; } for (int i=0; i<=n_fds; i++) { struct kevent changes; EV_SET(&changes, fds[i], EVFILT_VNODE, EV_ADD | EV_CLEAR | EV_ENABLE, NOTE_DELETE | NOTE_WRITE | NOTE_ATTRIB | NOTE_RENAME, 0, 0); int n = kevent(kq, &changes, 1, NULL, 0, NULL); if (n == -1) die("register kevent changes:"); } printf("Ready; press ^C to exit\n"); for (;;) { struct kevent event; int n = kevent(kq, NULL, 0, &event, 1, NULL); if (n == -1) die("kevent:"); if (n == 0) continue; char *ev_name = malloc(128); if (event.fflags & NOTE_WRITE) strncat(ev_name, "WRITE ", 6); if (event.fflags & NOTE_RENAME) strncat(ev_name, "RENAME ", 6); if (event.fflags & NOTE_ATTRIB) strncat(ev_name, "CHMOD ", 5); if (event.fflags & NOTE_DELETE) { strncat(ev_name, "DELETE ", 7); struct kevent changes; EV_SET(&changes, event.ident, EVFILT_VNODE, EV_DELETE, NOTE_DELETE | NOTE_WRITE | NOTE_ATTRIB | NOTE_RENAME, 0, 0); int n = kevent(kq, &changes, 1, NULL, 0, NULL); if (n == -1) die("remove kevent on delete:"); } char *name; for (int i=0; i<=n_fds; i++) if (fds[i] == event.ident) { name = names[i]; break; } printf("%-13s %s\n", ev_name, name); } return 0; }