pax_global_header00006660000000000000000000000064147106543270014523gustar00rootroot0000000000000052 comment=a9bc2e01792f868516acf80817f7d7d7b3315409 fsnotify-1.8.0/000077500000000000000000000000001471065432700133725ustar00rootroot00000000000000fsnotify-1.8.0/.circleci/000077500000000000000000000000001471065432700152255ustar00rootroot00000000000000fsnotify-1.8.0/.circleci/config.yml000066400000000000000000000044161471065432700172220ustar00rootroot00000000000000version: 2.1 workflows: main: jobs: ['linux-arm64', 'ios'] jobs: # linux/arm64 linux-arm64: machine: image: ubuntu-2204:2024.05.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 ./... FSNOTIFY_DEBUG=1 go test -parallel 1 -race -v ./... # 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.8.0/.cirrus.yml000066400000000000000000000011211471065432700154750ustar00rootroot00000000000000freebsd_task: name: 'FreeBSD' freebsd_instance: image_family: freebsd-14-1 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_DEBUG=1 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race -v ./... fsnotify-1.8.0/.github/000077500000000000000000000000001471065432700147325ustar00rootroot00000000000000fsnotify-1.8.0/.github/FUNDING.yml000066400000000000000000000000171471065432700165450ustar00rootroot00000000000000github: arp242 fsnotify-1.8.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001471065432700171155ustar00rootroot00000000000000fsnotify-1.8.0/.github/ISSUE_TEMPLATE/bug.yml000066400000000000000000000030001471065432700204060ustar00rootroot00000000000000--- 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: 'Code 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.' - type: 'textarea' validations: {"required": true} attributes: label: 'File operations to reproduce' placeholder: 'Full details on which file operations you did; simply "changed file" is not enough, as there are many ways to change a file.' - type: 'textarea' validations: {"required": true} attributes: label: 'Which operating system and version are you using?' description: | ``` Linux, BSD: uname -a macOS: sw_vers Windows: systeminfo | findstr /B /C:OS ``` - 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.8.0/.github/ISSUE_TEMPLATE/other.md000066400000000000000000000002101471065432700205510ustar00rootroot00000000000000--- name: 'Other' about: "Anything that's not a bug such as feature requests, questions, etc." title: '' labels: '' assignees: '' --- fsnotify-1.8.0/.github/workflows/000077500000000000000000000000001471065432700167675ustar00rootroot00000000000000fsnotify-1.8.0/.github/workflows/build.yml000066400000000000000000000025131471065432700206120ustar00rootroot00000000000000name: '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.23'] runs-on: 'ubuntu-latest' steps: - uses: 'actions/checkout@v4' - uses: 'actions/setup-go@v5' 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.8.0/.github/workflows/staticcheck.yml000066400000000000000000000016641471065432700220060ustar00rootroot00000000000000name: 'staticcheck' on: pull_request: paths: ['**.go', 'go.mod', '.github/workflows/*'] push: branches: ['main', 'aix'] jobs: staticcheck: name: 'staticcheck' runs-on: 'ubuntu-latest' steps: - uses: 'actions/setup-go@v5' with: go-version: '1.23' - uses: 'actions/cache@v4' 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.8.0/CHANGELOG.md000066400000000000000000000534701471065432700152140ustar00rootroot00000000000000# Changelog 1.8.0 2023-10-31 ---------------- ### Additions - all: add `FSNOTIFY_DEBUG` to print debug logs to stderr ([#619]) ### Changes and fixes - windows: fix behaviour of `WatchList()` to be consistent with other platforms ([#610]) - kqueue: ignore events with Ident=0 ([#590]) - kqueue: set O_CLOEXEC to prevent passing file descriptors to children ([#617]) - kqueue: emit events as "/path/dir/file" instead of "path/link/file" when watching a symlink ([#625]) - inotify: don't send event for IN_DELETE_SELF when also watching the parent ([#620]) - inotify: fix panic when calling Remove() in a goroutine ([#650]) - fen: allow watching subdirectories of watched directories ([#621]) [#590]: https://github.com/fsnotify/fsnotify/pull/590 [#610]: https://github.com/fsnotify/fsnotify/pull/610 [#617]: https://github.com/fsnotify/fsnotify/pull/617 [#619]: https://github.com/fsnotify/fsnotify/pull/619 [#620]: https://github.com/fsnotify/fsnotify/pull/620 [#621]: https://github.com/fsnotify/fsnotify/pull/621 [#625]: https://github.com/fsnotify/fsnotify/pull/625 [#650]: https://github.com/fsnotify/fsnotify/pull/650 1.7.0 - 2023-10-22 ------------------ 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.8.0/CONTRIBUTING.md000066400000000000000000000104021471065432700156200ustar00rootroot00000000000000Thank 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 discuss changes on the issue tracker first. You can just send PRs, but they may end up being rejected for one reason or the other. - 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. Writing new tests ----------------- Scripts in the testdata directory allow creating test cases in a "shell-like" syntax. The basic format is: script Output: desired output For example: # Create a new empty file with some data. watch / echo data >/file Output: create /file write /file Just create a new file to add a new test; select which tests to run with `-run TestScript/[path]`. script ------ The script is a "shell-like" script: cmd arg arg Comments are supported with `#`: # Comment cmd arg arg # Comment All operations are done in a temp directory; a path like "/foo" is rewritten to "/tmp/TestFoo/foo". Arguments can be quoted with `"` or `'`; there are no escapes and they're functionally identical right now, but this may change in the future, so best to assume shell-like rules. touch "/file with spaces" End-of-line escapes with `\` are not supported. ### Supported commands watch path [ops] # Watch the path, reporting events for it. Nothing is # watched by default. Optionally a list of ops can be # given, as with AddWith(path, WithOps(...)). unwatch path # Stop watching the path. watchlist n # Assert watchlist length. stop # Stop running the script; for debugging. debug [yes/no] # Enable/disable FSNOTIFY_DEBUG (tests are run in parallel by default, so -parallel=1 is probably a good idea). touch path mkdir [-p] dir ln -s target link # Only ln -s supported. mkfifo path mknod dev path mv src dst rm [-r] path chmod mode path # Octal only sleep time-in-ms cat path # Read path (does nothing with the data; just reads it). echo str >>path # Append "str" to "path". echo str >path # Truncate "path" and write "str". require reason # Skip the test if "reason" is true; "skip" and skip reason # "require" behave identical; it supports both for # readability. Possible reasons are: # # always Always skip this test. # symlink Symlinks are supported (requires admin # permissions on Windows). # mkfifo Platform doesn't support FIFO named sockets. # mknod Platform doesn't support device nodes. output ------ After `Output:` the desired output is given; this is indented by convention, but that's not required. The format of that is: # Comment event path # Comment system: event path system2: event path Every event is one line, and any whitespace between the event and path are ignored. The path can optionally be surrounded in ". Anything after a "#" is ignored. Platform-specific tests can be added after GOOS; for example: watch / touch /file Output: # Tested if nothing else matches create /file # Windows-specific test. windows: write /file You can specify multiple platforms with a comma (e.g. "windows, linux:"). "kqueue" is a shortcut for all kqueue systems (BSD, macOS). [goon]: https://github.com/arp242/goon [Vagrant]: https://www.vagrantup.com/ [integration_test.go]: /integration_test.go fsnotify-1.8.0/LICENSE000066400000000000000000000027731471065432700144100ustar00rootroot00000000000000Copyright © 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.8.0/README.md000066400000000000000000000155751471065432700146660ustar00rootroot00000000000000fsnotify 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.8.0/backend_fen.go000066400000000000000000000267511471065432700161530ustar00rootroot00000000000000//go:build solaris // FEN backend for illumos (supported) and Solaris (untested, but should work). // // See port_create(3c) etc. for docs. https://www.illumos.org/man/3C/port_create package fsnotify import ( "errors" "fmt" "os" "path/filepath" "sync" "time" "github.com/fsnotify/fsnotify/internal" "golang.org/x/sys/unix" ) type fen struct { Events chan Event 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]Op // Explicitly watched directories watches map[string]Op // Explicitly watched non-directories } func newBackend(ev chan Event, errs chan error) (backend, error) { return newBufferedBackend(0, ev, errs) } func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) { w := &fen{ Events: ev, Errors: errs, dirs: make(map[string]Op), watches: make(map[string]Op), 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 *fen) sendEvent(name string, op Op) (sent bool) { select { case <-w.done: return false case w.Events <- Event{Name: name, Op: op}: return true } } // sendError attempts to send an error to the user, returning true if the error // was put in the channel successfully and false if the watcher has been closed. func (w *fen) sendError(err error) (sent bool) { if err == nil { return true } select { case <-w.done: return false case w.Errors <- err: return true } } func (w *fen) isClosed() bool { select { case <-w.done: return true default: return false } } func (w *fen) 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() } func (w *fen) Add(name string) error { return w.AddWith(name) } func (w *fen) AddWith(name string, opts ...addOpt) error { if w.isClosed() { return ErrClosed } if debug { fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s AddWith(%q)\n", time.Now().Format("15:04:05.000000000"), name) } with := getOptions(opts...) if !w.xSupports(with.op) { return fmt.Errorf("%w: %s", xErrUnsupported, with.op) } // Currently we resolve symlinks that were explicitly requested to be // watched. Otherwise we would use LStat here. 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] = with.op w.mu.Unlock() return nil } err = w.associateFile(name, stat, true) if err != nil { return err } w.mu.Lock() w.watches[name] = with.op w.mu.Unlock() return nil } func (w *fen) Remove(name string) error { if w.isClosed() { return nil } if !w.port.PathIsWatched(name) { return fmt.Errorf("%w: %s", ErrNonExistentWatch, name) } if debug { fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s Remove(%q)\n", time.Now().Format("15:04:05.000000000"), name) } // The user has expressed an intent. Immediately remove this name from // whichever watch list it might be in. If it's not in there the delete // 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 *fen) 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 } if debug { internal.Debug(pevent.Path, pevent.Events) } err = w.handleEvent(&pevent) if !w.sendError(err) { return } } } } func (w *fen) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error { files, err := os.ReadDir(path) if err != nil { return err } // 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 *fen) 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() && watchedDir { if err := w.updateDirectory(path); err != nil { return err } } 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 *fen) updateDirectory(path string) error { // The directory was modified, so we must find unwatched entities and watch // them. If something was removed from the directory, nothing will happen, // as everything else should still be watched. 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 !w.sendError(err) { return nil } if !w.sendEvent(path, Create) { return nil } } return nil } func (w *fen) 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 && !errors.Is(err, unix.ENOENT) { return err } } var events int if !follow { // Watch symlinks themselves rather than their targets unless this entry // is explicitly watched. events |= unix.FILE_NOFOLLOW } if true { // TODO: implement withOps() events |= unix.FILE_MODIFIED } if true { events |= unix.FILE_ATTRIB } return w.port.AssociatePath(path, stat, events, stat.Mode()) } func (w *fen) dissociateFile(path string, stat os.FileInfo, unused bool) error { if !w.port.PathIsWatched(path) { return nil } return w.port.DissociatePath(path) } func (w *fen) 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 } func (w *fen) xSupports(op Op) bool { if op.Has(xUnportableOpen) || op.Has(xUnportableRead) || op.Has(xUnportableCloseWrite) || op.Has(xUnportableCloseRead) { return false } return true } fsnotify-1.8.0/backend_fen_test.go000066400000000000000000000023301471065432700171750ustar00rootroot00000000000000//go: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.b.(*fen).watches) != wantFiles { var d []string for k, v := range w.b.(*fen).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.b.(*fen).watches), wantFiles, strings.Join(d, "\n")) } if len(w.b.(*fen).dirs) != wantDirs { var d []string for k, v := range w.b.(*fen).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.b.(*fen).dirs), wantDirs, strings.Join(d, "\n")) } } check(1, 1) // Shouldn't change internal state. if err := w.Add("/path-doesnt-exist"); err == nil { t.Fatal(err) } 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.8.0/backend_inotify.go000066400000000000000000000376021471065432700170610ustar00rootroot00000000000000//go:build linux && !appengine package fsnotify import ( "errors" "fmt" "io" "io/fs" "os" "path/filepath" "strings" "sync" "time" "unsafe" "github.com/fsnotify/fsnotify/internal" "golang.org/x/sys/unix" ) type inotify struct { Events chan Event 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 doneMu sync.Mutex doneResp chan struct{} // Channel to respond to Close // Store rename cookies in an array, with the index wrapping to 0. Almost // all of the time what we get is a MOVED_FROM to set the cookie and the // next event inotify sends will be MOVED_TO to read it. However, this is // not guaranteed – as described in inotify(7) – and we may get other events // between the two MOVED_* events (including other MOVED_* ones). // // A second issue is that moving a file outside the watched directory will // trigger a MOVED_FROM to set the cookie, but we never see the MOVED_TO to // read and delete it. So just storing it in a map would slowly leak memory. // // Doing it like this gives us a simple fast LRU-cache that won't allocate. // Ten items should be more than enough for our purpose, and a loop over // such a short array is faster than a map access anyway (not that it hugely // matters since we're talking about hundreds of ns at the most, but still). cookies [10]koekje cookieIndex uint8 cookiesMu sync.Mutex } type ( 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. recurse bool // Recursion with ./...? } koekje struct { cookie uint32 path string } ) 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() watch := w.wd[wd] // Could have had Remove() called. See #616. if watch == nil { return } delete(w.path, watch.path) delete(w.wd, wd) } func (w *watches) removePath(path string) ([]uint32, error) { w.mu.Lock() defer w.mu.Unlock() path, recurse := recursivePath(path) wd, ok := w.path[path] if !ok { return nil, fmt.Errorf("%w: %s", ErrNonExistentWatch, path) } watch := w.wd[wd] if recurse && !watch.recurse { return nil, fmt.Errorf("can't use /... with non-recursive watch %q", path) } delete(w.path, path) delete(w.wd, wd) if !watch.recurse { return []uint32{wd}, nil } wds := make([]uint32, 0, 8) wds = append(wds, wd) for p, rwd := range w.path { if filepath.HasPrefix(p, path) { delete(w.path, p) delete(w.wd, rwd) wds = append(wds, rwd) } } return wds, nil } func (w *watches) byPath(path string) *watch { 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 } func newBackend(ev chan Event, errs chan error) (backend, error) { return newBufferedBackend(0, ev, errs) } func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) { // 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 := &inotify{ Events: ev, Errors: errs, fd: fd, inotifyFile: os.NewFile(uintptr(fd), ""), watches: newWatches(), 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 *inotify) sendEvent(e Event) bool { select { case <-w.done: return false case w.Events <- e: return true } } // Returns true if the error was sent, or false if watcher is closed. func (w *inotify) sendError(err error) bool { if err == nil { return true } select { case <-w.done: return false case w.Errors <- err: return true } } func (w *inotify) isClosed() bool { select { case <-w.done: return true default: return false } } func (w *inotify) Close() error { w.doneMu.Lock() if w.isClosed() { w.doneMu.Unlock() return nil } close(w.done) w.doneMu.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 } func (w *inotify) Add(name string) error { return w.AddWith(name) } func (w *inotify) AddWith(path string, opts ...addOpt) error { if w.isClosed() { return ErrClosed } if debug { fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s AddWith(%q)\n", time.Now().Format("15:04:05.000000000"), path) } with := getOptions(opts...) if !w.xSupports(with.op) { return fmt.Errorf("%w: %s", xErrUnsupported, with.op) } path, recurse := recursivePath(path) if recurse { return filepath.WalkDir(path, func(root string, d fs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() { if root == path { return fmt.Errorf("fsnotify: not a directory: %q", path) } return nil } // Send a Create event when adding new directory from a recursive // watch; this is for "mkdir -p one/two/three". Usually all those // directories will be created before we can set up watchers on the // subdirectories, so only "one" would be sent as a Create event and // not "one/two" and "one/two/three" (inotifywait -r has the same // problem). if with.sendCreate && root != path { w.sendEvent(Event{Name: root, Op: Create}) } return w.add(root, with, true) }) } return w.add(path, with, false) } func (w *inotify) add(path string, with withOpts, recurse bool) error { var flags uint32 if with.noFollow { flags |= unix.IN_DONT_FOLLOW } if with.op.Has(Create) { flags |= unix.IN_CREATE } if with.op.Has(Write) { flags |= unix.IN_MODIFY } if with.op.Has(Remove) { flags |= unix.IN_DELETE | unix.IN_DELETE_SELF } if with.op.Has(Rename) { flags |= unix.IN_MOVED_TO | unix.IN_MOVED_FROM | unix.IN_MOVE_SELF } if with.op.Has(Chmod) { flags |= unix.IN_ATTRIB } if with.op.Has(xUnportableOpen) { flags |= unix.IN_OPEN } if with.op.Has(xUnportableRead) { flags |= unix.IN_ACCESS } if with.op.Has(xUnportableCloseWrite) { flags |= unix.IN_CLOSE_WRITE } if with.op.Has(xUnportableCloseRead) { flags |= unix.IN_CLOSE_NOWRITE } return w.register(path, flags, recurse) } func (w *inotify) register(path string, flags uint32, recurse bool) error { return w.watches.updatePath(path, func(existing *watch) (*watch, error) { if existing != nil { flags |= existing.flags | unix.IN_MASK_ADD } wd, err := unix.InotifyAddWatch(w.fd, path, flags) if wd == -1 { return nil, err } if existing == nil { return &watch{ wd: uint32(wd), path: path, flags: flags, recurse: recurse, }, nil } existing.wd = uint32(wd) existing.flags = flags return existing, nil }) } func (w *inotify) Remove(name string) error { if w.isClosed() { return nil } if debug { fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s Remove(%q)\n", time.Now().Format("15:04:05.000000000"), name) } return w.remove(filepath.Clean(name)) } func (w *inotify) remove(name string) error { wds, err := w.watches.removePath(name) if err != nil { return err } for _, wd := range wds { _, err := unix.InotifyRmWatch(w.fd, wd) if err != nil { // 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 err } } return nil } func (w *inotify) 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 *inotify) 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 } // We don't know how many events we just read into the buffer // While the offset points to at least one whole event... var offset uint32 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) // Move to the next event in the buffer next = func() { offset += unix.SizeofInotifyEvent + nameLen } ) 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)) /// Can be nil if Remove() was called in another goroutine for this /// path inbetween reading the events from the kernel and reading /// the internal state. Not much we can do about it, so just skip. /// See #616. if watch == nil { next() continue } name := watch.path if nameLen > 0 { /// Point "bytes" at the first byte of the filename bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen] /// The filename is padded with NULL bytes. TrimRight() gets rid of those. name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000") } if debug { internal.Debug(name, raw.Mask, raw.Cookie) } if mask&unix.IN_IGNORED != 0 { //&& event.Op != 0 next() continue } // inotify will automatically remove the watch on deletes; just need // to clean our state here. if 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 mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF { if watch.recurse { next() // Do nothing continue } err := w.remove(watch.path) if err != nil && !errors.Is(err, ErrNonExistentWatch) { if !w.sendError(err) { return } } } /// Skip if we're watching both this path and the parent; the parent /// will already send a delete so no need to do it twice. if mask&unix.IN_DELETE_SELF != 0 { if _, ok := w.watches.path[filepath.Dir(watch.path)]; ok { next() continue } } ev := w.newEvent(name, mask, raw.Cookie) // Need to update watch path for recurse. if watch.recurse { isDir := mask&unix.IN_ISDIR == unix.IN_ISDIR /// New directory created: set up watch on it. if isDir && ev.Has(Create) { err := w.register(ev.Name, watch.flags, true) if !w.sendError(err) { return } // This was a directory rename, so we need to update all // the children. // // TODO: this is of course pretty slow; we should use a // better data structure for storing all of this, e.g. store // children in the watch. I have some code for this in my // kqueue refactor we can use in the future. For now I'm // okay with this as it's not publicly available. // Correctness first, performance second. if ev.renamedFrom != "" { w.watches.mu.Lock() for k, ww := range w.watches.wd { if k == watch.wd || ww.path == ev.Name { continue } if strings.HasPrefix(ww.path, ev.renamedFrom) { ww.path = strings.Replace(ww.path, ev.renamedFrom, ev.Name, 1) w.watches.wd[k] = ww } } w.watches.mu.Unlock() } } } /// Send the events that are not ignored on the events channel if !w.sendEvent(ev) { return } next() } } } func (w *inotify) isRecursive(path string) bool { ww := w.watches.byPath(path) if ww == nil { // path could be a file, so also check the Dir. ww = w.watches.byPath(filepath.Dir(path)) } return ww != nil && ww.recurse } func (w *inotify) newEvent(name string, mask, cookie uint32) Event { e := Event{Name: name} 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_OPEN == unix.IN_OPEN { e.Op |= xUnportableOpen } if mask&unix.IN_ACCESS == unix.IN_ACCESS { e.Op |= xUnportableRead } if mask&unix.IN_CLOSE_WRITE == unix.IN_CLOSE_WRITE { e.Op |= xUnportableCloseWrite } if mask&unix.IN_CLOSE_NOWRITE == unix.IN_CLOSE_NOWRITE { e.Op |= xUnportableCloseRead } if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM { e.Op |= Rename } if mask&unix.IN_ATTRIB == unix.IN_ATTRIB { e.Op |= Chmod } if cookie != 0 { if mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM { w.cookiesMu.Lock() w.cookies[w.cookieIndex] = koekje{cookie: cookie, path: e.Name} w.cookieIndex++ if w.cookieIndex > 9 { w.cookieIndex = 0 } w.cookiesMu.Unlock() } else if mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO { w.cookiesMu.Lock() var prev string for _, c := range w.cookies { if c.cookie == cookie { prev = c.path break } } w.cookiesMu.Unlock() e.renamedFrom = prev } } return e } func (w *inotify) xSupports(op Op) bool { return true // Supports everything. } func (w *inotify) state() { w.watches.mu.Lock() defer w.watches.mu.Unlock() for wd, ww := range w.watches.wd { fmt.Fprintf(os.Stderr, "%4d: recurse=%t %q\n", wd, ww.recurse, ww.path) } } fsnotify-1.8.0/backend_inotify_test.go000066400000000000000000000051251471065432700201130ustar00rootroot00000000000000//go:build linux package fsnotify import ( "errors" "os" "strconv" "strings" "sync" "testing" "time" ) 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.b.(*inotify).watches.len() != want { t.Error(w.b.(*inotify).watches) } } check(2) // Shouldn't change internal state. if err := w.Add("/path-doesnt-exist"); err == nil { t.Fatal(err) } 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) } // 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`)) } fsnotify-1.8.0/backend_kqueue.go000066400000000000000000000433521471065432700166760ustar00rootroot00000000000000//go:build freebsd || openbsd || netbsd || dragonfly || darwin package fsnotify import ( "errors" "fmt" "os" "path/filepath" "runtime" "sync" "time" "github.com/fsnotify/fsnotify/internal" "golang.org/x/sys/unix" ) type kqueue struct { Events chan Event Errors chan error kq int // File descriptor (as returned by the kqueue() syscall). closepipe [2]int // Pipe used for closing kq. watches *watches done chan struct{} doneMu sync.Mutex } type ( watches struct { mu sync.RWMutex wd map[int]watch // wd → watch path map[string]int // pathname → wd byDir map[string]map[int]struct{} // dirname(path) → wd seen map[string]struct{} // Keep track of if we know this file exists. byUser map[string]struct{} // Watches added with Watcher.Add() } watch struct { wd int name string linkName string // In case of links; name is the target, and this is the link. isDir bool dirFlags uint32 } ) func newWatches() *watches { return &watches{ wd: make(map[int]watch), path: make(map[string]int), byDir: make(map[string]map[int]struct{}), seen: make(map[string]struct{}), byUser: make(map[string]struct{}), } } func (w *watches) listPaths(userOnly bool) []string { w.mu.RLock() defer w.mu.RUnlock() if userOnly { l := make([]string, 0, len(w.byUser)) for p := range w.byUser { l = append(l, p) } return l } l := make([]string, 0, len(w.path)) for p := range w.path { l = append(l, p) } return l } func (w *watches) watchesInDir(path string) []string { w.mu.RLock() defer w.mu.RUnlock() l := make([]string, 0, 4) for fd := range w.byDir[path] { info := w.wd[fd] if _, ok := w.byUser[info.name]; !ok { l = append(l, info.name) } } return l } // Mark path as added by the user. func (w *watches) addUserWatch(path string) { w.mu.Lock() defer w.mu.Unlock() w.byUser[path] = struct{}{} } func (w *watches) addLink(path string, fd int) { w.mu.Lock() defer w.mu.Unlock() w.path[path] = fd w.seen[path] = struct{}{} } func (w *watches) add(path, linkPath string, fd int, isDir bool) { w.mu.Lock() defer w.mu.Unlock() w.path[path] = fd w.wd[fd] = watch{wd: fd, name: path, linkName: linkPath, isDir: isDir} parent := filepath.Dir(path) byDir, ok := w.byDir[parent] if !ok { byDir = make(map[int]struct{}, 1) w.byDir[parent] = byDir } byDir[fd] = struct{}{} } func (w *watches) byWd(fd int) (watch, bool) { w.mu.RLock() defer w.mu.RUnlock() info, ok := w.wd[fd] return info, ok } func (w *watches) byPath(path string) (watch, bool) { w.mu.RLock() defer w.mu.RUnlock() info, ok := w.wd[w.path[path]] return info, ok } func (w *watches) updateDirFlags(path string, flags uint32) { w.mu.Lock() defer w.mu.Unlock() fd := w.path[path] info := w.wd[fd] info.dirFlags = flags w.wd[fd] = info } func (w *watches) remove(fd int, path string) bool { w.mu.Lock() defer w.mu.Unlock() isDir := w.wd[fd].isDir delete(w.path, path) delete(w.byUser, path) parent := filepath.Dir(path) delete(w.byDir[parent], fd) if len(w.byDir[parent]) == 0 { delete(w.byDir, parent) } delete(w.wd, fd) delete(w.seen, path) return isDir } func (w *watches) markSeen(path string, exists bool) { w.mu.Lock() defer w.mu.Unlock() if exists { w.seen[path] = struct{}{} } else { delete(w.seen, path) } } func (w *watches) seenBefore(path string) bool { w.mu.RLock() defer w.mu.RUnlock() _, ok := w.seen[path] return ok } func newBackend(ev chan Event, errs chan error) (backend, error) { return newBufferedBackend(0, ev, errs) } func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) { kq, closepipe, err := newKqueue() if err != nil { return nil, err } w := &kqueue{ Events: ev, Errors: errs, kq: kq, closepipe: closepipe, done: make(chan struct{}), watches: newWatches(), } 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 } unix.CloseOnExec(closepipe[0]) unix.CloseOnExec(closepipe[1]) // 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 *kqueue) sendEvent(e Event) bool { select { case <-w.done: return false case w.Events <- e: return true } } // Returns true if the error was sent, or false if watcher is closed. func (w *kqueue) sendError(err error) bool { if err == nil { return true } select { case <-w.done: return false case w.Errors <- err: return true } } func (w *kqueue) isClosed() bool { select { case <-w.done: return true default: return false } } func (w *kqueue) Close() error { w.doneMu.Lock() if w.isClosed() { w.doneMu.Unlock() return nil } close(w.done) w.doneMu.Unlock() pathsToRemove := w.watches.listPaths(false) for _, name := range pathsToRemove { w.Remove(name) } // Send "quit" message to the reader goroutine. unix.Close(w.closepipe[1]) return nil } func (w *kqueue) Add(name string) error { return w.AddWith(name) } func (w *kqueue) AddWith(name string, opts ...addOpt) error { if debug { fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s AddWith(%q)\n", time.Now().Format("15:04:05.000000000"), name) } with := getOptions(opts...) if !w.xSupports(with.op) { return fmt.Errorf("%w: %s", xErrUnsupported, with.op) } _, err := w.addWatch(name, noteAllEvents) if err != nil { return err } w.watches.addUserWatch(name) return nil } func (w *kqueue) Remove(name string) error { if debug { fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s Remove(%q)\n", time.Now().Format("15:04:05.000000000"), name) } return w.remove(name, true) } func (w *kqueue) remove(name string, unwatchFiles bool) error { if w.isClosed() { return nil } name = filepath.Clean(name) info, ok := w.watches.byPath(name) if !ok { return fmt.Errorf("%w: %s", ErrNonExistentWatch, name) } err := w.register([]int{info.wd}, unix.EV_DELETE, 0) if err != nil { return err } unix.Close(info.wd) isDir := w.watches.remove(info.wd, name) // Find all watched paths that are in this directory that are not external. if unwatchFiles && isDir { pathsToRemove := w.watches.watchesInDir(name) 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 } func (w *kqueue) WatchList() []string { if w.isClosed() { return nil } return w.watches.listPaths(true) } // 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 *kqueue) addWatch(name string, flags uint32) (string, error) { if w.isClosed() { return "", ErrClosed } name = filepath.Clean(name) info, alreadyWatching := w.watches.byPath(name) 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 } _, alreadyWatching = w.watches.byPath(link) if alreadyWatching { // Add to watches so we don't get spurious Create events later // on when we diff the directories. w.watches.addLink(name, 0) return link, nil } info.linkName = name 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 { info.wd, err = unix.Open(name, openMode, 0) if err == nil { break } if errors.Is(err, unix.EINTR) { continue } return "", err } info.isDir = fi.IsDir() } err := w.register([]int{info.wd}, unix.EV_ADD|unix.EV_CLEAR|unix.EV_ENABLE, flags) if err != nil { unix.Close(info.wd) return "", err } if !alreadyWatching { w.watches.add(name, info.linkName, info.wd, info.isDir) } // Watch the directory if it has not been watched before, or if it was // watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles) if info.isDir { watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE && (!alreadyWatching || (info.dirFlags&unix.NOTE_WRITE) != unix.NOTE_WRITE) w.watches.updateDirFlags(name, flags) 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 *kqueue) 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 { 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)) { return } } for _, kevent := range kevents { var ( wd = 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 wd == w.closepipe[0] { return } path, ok := w.watches.byWd(wd) if debug { internal.Debug(path.name, &kevent) } // On macOS it seems that sometimes an event with Ident=0 is // delivered, and no other flags/information beyond that, even // though we never saw such a file descriptor. For example in // TestWatchSymlink/277 (usually at the end, but sometimes sooner): // // fmt.Printf("READ: %2d %#v\n", kevent.Ident, kevent) // unix.Kevent_t{Ident:0x2a, Filter:-4, Flags:0x25, Fflags:0x2, Data:0, Udata:(*uint8)(nil)} // unix.Kevent_t{Ident:0x0, Filter:-4, Flags:0x25, Fflags:0x2, Data:0, Udata:(*uint8)(nil)} // // The first is a normal event, the second with Ident 0. No error // flag, no data, no ... nothing. // // I read a bit through bsd/kern_event.c from the xnu source, but I // don't really see an obvious location where this is triggered – // this doesn't seem intentional, but idk... // // Technically fd 0 is a valid descriptor, so only skip it if // there's no path, and if we're on macOS. if !ok && kevent.Ident == 0 && runtime.GOOS == "darwin" { continue } event := w.newEvent(path.name, path.linkName, mask) if event.Has(Rename) || event.Has(Remove) { w.remove(event.Name, false) w.watches.markSeen(event.Name, false) } if path.isDir && event.Has(Write) && !event.Has(Remove) { w.dirChange(event.Name) } else if !w.sendEvent(event) { return } 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) _, found := w.watches.byPath(fileDir) if found { // TODO: this branch is never triggered in any test. // Added in d6220df (2012). // isDir check added in 8611c35 (2016): https://github.com/fsnotify/fsnotify/pull/111 // // I don't really get how this can be triggered either. // And it wasn't triggered in the patch that added it, // either. // // Original also had a comment: // make sure the directory exists before we watch for // changes. When we do a recursive watch and perform // rm -rf, the parent directory might have gone // missing, ignore the missing directory and let the // upcoming delete event remove the watch from the // parent directory. err := w.dirChange(fileDir) if !w.sendError(err) { return } } } else { path := filepath.Clean(event.Name) if fi, err := os.Lstat(path); err == nil { err := w.sendCreateIfNew(path, fi) if !w.sendError(err) { return } } } } } } } // newEvent returns an platform-independent Event based on kqueue Fflags. func (w *kqueue) newEvent(name, linkName string, mask uint32) Event { e := Event{Name: name} if linkName != "" { // If the user watched "/path/link" then emit events as "/path/link" // rather than "/path/target". e.Name = linkName } if mask&unix.NOTE_DELETE == unix.NOTE_DELETE { 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 *kqueue) watchDirectoryFiles(dirPath string) error { 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.watches.markSeen(cleanPath, true) } 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 *kqueue) dirChange(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.dirChange: %w", err) } for _, f := range files { fi, err := f.Info() if err != nil { return fmt.Errorf("fsnotify.dirChange: %w", err) } err = w.sendCreateIfNew(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.dirChange: %w", err) } } return nil } // Send a create event if the file isn't already being tracked, and start // watching this file. func (w *kqueue) sendCreateIfNew(path string, fi os.FileInfo) error { if !w.watches.seenBefore(path) { if !w.sendEvent(Event{Name: path, Op: Create}) { return nil } } // Like watchDirectoryFiles, but without doing another ReadDir. path, err := w.internalWatch(path, fi) if err != nil { return err } w.watches.markSeen(path, true) return nil } func (w *kqueue) 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 info, _ := w.watches.byPath(name) return w.addWatch(name, info.dirFlags|unix.NOTE_DELETE|unix.NOTE_RENAME) } // watch file to mimic Linux inotify return w.addWatch(name, noteAllEvents) } // Register events with the queue. func (w *kqueue) 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 *kqueue) 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 } func (w *kqueue) xSupports(op Op) bool { if runtime.GOOS == "freebsd" { //return true // Supports everything. } if op.Has(xUnportableOpen) || op.Has(xUnportableRead) || op.Has(xUnportableCloseWrite) || op.Has(xUnportableCloseRead) { return false } return true } fsnotify-1.8.0/backend_kqueue_test.go000066400000000000000000000045231471065432700177320ustar00rootroot00000000000000//go: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) kq := w.b.(*kqueue) addWatch(t, w, tmp) addWatch(t, w, file) check := func(wantUser, wantTotal int) { t.Helper() if len(kq.watches.path) != wantTotal { var d []string for k, v := range kq.watches.path { d = append(d, fmt.Sprintf("%#v = %#v", k, v)) } t.Errorf("unexpected number of entries in w.watches.path (have %d, want %d):\n%v", len(kq.watches.path), wantTotal, strings.Join(d, "\n")) } if len(kq.watches.wd) != wantTotal { var d []string for k, v := range kq.watches.wd { d = append(d, fmt.Sprintf("%#v = %#v", k, v)) } t.Errorf("unexpected number of entries in w.watches.wd (have %d, want %d):\n%v", len(kq.watches.wd), wantTotal, strings.Join(d, "\n")) } if len(kq.watches.byUser) != wantUser { var d []string for k, v := range kq.watches.byUser { d = append(d, fmt.Sprintf("%#v = %#v", k, v)) } t.Errorf("unexpected number of entries in w.watches.byUser (have %d, want %d):\n%v", len(kq.watches.byUser), wantUser, strings.Join(d, "\n")) } } check(2, 3) // Shouldn't change internal state. if err := w.Add("/path-doesnt-exist"); err == nil { t.Fatal(err) } 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(kq.watches.byDir) != want { var d []string for k, v := range kq.watches.byDir { d = append(d, fmt.Sprintf("%#v = %#v", k, v)) } t.Errorf("unexpected number of entries in w.watches.byDir (have %d, want %d):\n%v", len(kq.watches.byDir), want, strings.Join(d, "\n")) } if len(kq.watches.seen) != want { var d []string for k, v := range kq.watches.seen { d = append(d, fmt.Sprintf("%#v = %#v", k, v)) } t.Errorf("unexpected number of entries in w.watches.seen (have %d, want %d):\n%v", len(kq.watches.seen), want, strings.Join(d, "\n")) return } } } fsnotify-1.8.0/backend_other.go000066400000000000000000000016261471065432700165160ustar00rootroot00000000000000//go:build appengine || (!darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows) package fsnotify import "errors" type other struct { Events chan Event Errors chan error } func newBackend(ev chan Event, errs chan error) (backend, error) { return nil, errors.New("fsnotify not supported on the current platform") } func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) { return newBackend(ev, errs) } func (w *other) Close() error { return nil } func (w *other) WatchList() []string { return nil } func (w *other) Add(name string) error { return nil } func (w *other) AddWith(name string, opts ...addOpt) error { return nil } func (w *other) Remove(name string) error { return nil } func (w *other) xSupports(op Op) bool { return false } fsnotify-1.8.0/backend_windows.go000066400000000000000000000407131471065432700170670ustar00rootroot00000000000000//go:build windows // Windows backend based on ReadDirectoryChangesW() // // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw package fsnotify import ( "errors" "fmt" "os" "path/filepath" "reflect" "runtime" "strings" "sync" "time" "unsafe" "github.com/fsnotify/fsnotify/internal" "golang.org/x/sys/windows" ) type readDirChangesW struct { Events chan Event 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 } func newBackend(ev chan Event, errs chan error) (backend, error) { return newBufferedBackend(50, ev, errs) } func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) { port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0) if err != nil { return nil, os.NewSyscallError("CreateIoCompletionPort", err) } w := &readDirChangesW{ Events: ev, Errors: errs, port: port, watches: make(watchMap), input: make(chan *input, 1), quit: make(chan chan<- error, 1), } go w.readEvents() return w, nil } func (w *readDirChangesW) isClosed() bool { w.mu.Lock() defer w.mu.Unlock() return w.closed } func (w *readDirChangesW) sendEvent(name, renamedFrom string, mask uint64) bool { if mask == 0 { return false } event := w.newEvent(name, uint32(mask)) event.renamedFrom = renamedFrom select { case ch := <-w.quit: w.quit <- ch case w.Events <- event: } return true } // Returns true if the error was sent, or false if watcher is closed. func (w *readDirChangesW) sendError(err error) bool { if err == nil { return true } select { case w.Errors <- err: return true case <-w.quit: return false } } func (w *readDirChangesW) 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 } func (w *readDirChangesW) Add(name string) error { return w.AddWith(name) } func (w *readDirChangesW) AddWith(name string, opts ...addOpt) error { if w.isClosed() { return ErrClosed } if debug { fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s AddWith(%q)\n", time.Now().Format("15:04:05.000000000"), filepath.ToSlash(name)) } with := getOptions(opts...) if !w.xSupports(with.op) { return fmt.Errorf("%w: %s", xErrUnsupported, with.op) } if with.bufsize < 4096 { return fmt.Errorf("fsnotify.WithBufferSize: buffer size cannot be smaller than 4096 bytes") } 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 } func (w *readDirChangesW) Remove(name string) error { if w.isClosed() { return nil } if debug { fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s Remove(%q)\n", time.Now().Format("15:04:05.000000000"), filepath.ToSlash(name)) } in := &input{ op: opRemoveWatch, path: filepath.Clean(name), reply: make(chan error), } w.input <- in if err := w.wakeupReader(); err != nil { return err } return <-in.reply } func (w *readDirChangesW) 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 { for name := range watchEntry.names { entries = append(entries, filepath.Join(watchEntry.path, name)) } // the directory itself is being watched if watchEntry.mask != 0 { entries = append(entries, watchEntry.path) } } } return entries } // 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 *readDirChangesW) 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 *readDirChangesW) wakeupReader() error { err := windows.PostQueuedCompletionStatus(w.port, 0, 0, nil) if err != nil { return os.NewSyscallError("PostQueuedCompletionStatus", err) } return nil } func (w *readDirChangesW) getDir(pathname string) (dir string, err error) { attr, err := windows.GetFileAttributes(windows.StringToUTF16Ptr(pathname)) if err != nil { return "", os.NewSyscallError("GetFileAttributes", err) } if attr&windows.FILE_ATTRIBUTE_DIRECTORY != 0 { dir = pathname } else { dir, _ = filepath.Split(pathname) dir = filepath.Clean(dir) } return } func (w *readDirChangesW) getIno(path string) (ino *inode, err error) { h, err := windows.CreateFile(windows.StringToUTF16Ptr(path), windows.FILE_LIST_DIRECTORY, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, 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 *readDirChangesW) addWatch(pathname string, flags uint64, bufsize int) 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() 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 *readDirChangesW) 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 *readDirChangesW) 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 *readDirChangesW) 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 *readDirChangesW) 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) if debug { internal.Debug(fullname, raw.Action) } 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 } } if raw.Action != windows.FILE_ACTION_RENAMED_NEW_NAME { w.sendEvent(fullname, "", watch.names[name]&mask) } if raw.Action == windows.FILE_ACTION_REMOVED { w.sendEvent(fullname, "", watch.names[name]&sysFSIGNORED) delete(watch.names, name) } if watch.rename != "" && raw.Action == windows.FILE_ACTION_RENAMED_NEW_NAME { w.sendEvent(fullname, filepath.Join(watch.path, watch.rename), watch.mask&w.toFSnotifyFlags(raw.Action)) } else { w.sendEvent(fullname, "", watch.mask&w.toFSnotifyFlags(raw.Action)) } if raw.Action == windows.FILE_ACTION_RENAMED_NEW_NAME { w.sendEvent(filepath.Join(watch.path, watch.rename), "", watch.names[name]&mask) } // 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 *readDirChangesW) 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 *readDirChangesW) 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 } func (w *readDirChangesW) xSupports(op Op) bool { if op.Has(xUnportableOpen) || op.Has(xUnportableRead) || op.Has(xUnportableCloseWrite) || op.Has(xUnportableCloseRead) { return false } return true } fsnotify-1.8.0/backend_windows_test.go000066400000000000000000000025551471065432700201300ustar00rootroot00000000000000//go: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.b.(*readDirChangesW).watches) != want { var d []string for k, v := range w.b.(*readDirChangesW).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.b.(*readDirChangesW).watches), want, strings.Join(d, "\n")) } } check(2) // Shouldn't change internal state. if err := w.Add("/path-doesnt-exist"); err == nil { t.Fatal(err) } 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.b.(*readDirChangesW).remWatch(tmp); err == nil { t.Fatal("Should be fail with closed handle\n") } } fsnotify-1.8.0/cmd/000077500000000000000000000000001471065432700141355ustar00rootroot00000000000000fsnotify-1.8.0/cmd/fsnotify/000077500000000000000000000000001471065432700157765ustar00rootroot00000000000000fsnotify-1.8.0/cmd/fsnotify/closewrite.go000066400000000000000000000027671471065432700205210ustar00rootroot00000000000000package main /* func closeWrite(paths ...string) { if len(paths) < 1 { exit("must specify at least one path to watch") } w, err := fsnotify.NewWatcher() if err != nil { exit("creating a new watcher: %s", err) } defer w.Close() var ( op fsnotify.Op cw = w.Supports(fsnotify.UnportableCloseWrite) ) if cw { op |= fsnotify.UnportableCloseWrite } else { op |= fsnotify.Create | fsnotify.Write } go closeWriteLoop(w, cw) for _, p := range paths { err := w.AddWith(p, fsnotify.WithOps(op)) if err != nil { exit("%q: %s", p, err) } } printTime("ready; press ^C to exit") <-make(chan struct{}) } func closeWriteLoop(w *fsnotify.Watcher, cw bool) { var ( waitFor = 100 * time.Millisecond mu sync.Mutex timers = make(map[string]*time.Timer) ) for { select { case err, ok := <-w.Errors: if !ok { return } panic(err) case e, ok := <-w.Events: if !ok { return } // CloseWrite is supported: easy case. if cw { if e.Has(fsnotify.UnportableCloseWrite) { printTime(e.String()) } 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() { printTime(e.String()) mu.Lock() delete(timers, e.Name) mu.Unlock() }) 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.8.0/cmd/fsnotify/dedup.go000066400000000000000000000043151471065432700174310ustar00rootroot00000000000000package 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.8.0/cmd/fsnotify/file.go000066400000000000000000000034411471065432700172460ustar00rootroot00000000000000package 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.8.0/cmd/fsnotify/main.go000066400000000000000000000027121471065432700172530ustar00rootroot00000000000000// 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.8.0/cmd/fsnotify/watch.go000066400000000000000000000022121471065432700174300ustar00rootroot00000000000000package 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.8.0/fsnotify.go000066400000000000000000000416771471065432700156010ustar00rootroot00000000000000// Package fsnotify provides a cross-platform interface for file system // notifications. // // Currently supported systems: // // - Linux via inotify // - BSD, macOS via kqueue // - Windows via ReadDirectoryChangesW // - illumos via FEN // // # FSNOTIFY_DEBUG // // Set the FSNOTIFY_DEBUG environment variable to "1" to print debug messages to // stderr. This can be useful to track down some problems, especially in cases // where fsnotify is used as an indirect dependency. // // Every event will be printed as soon as there's something useful to print, // with as little processing from fsnotify. // // Example output: // // FSNOTIFY_DEBUG: 11:34:23.633087586 256:IN_CREATE → "/tmp/file-1" // FSNOTIFY_DEBUG: 11:34:23.633202319 4:IN_ATTRIB → "/tmp/file-1" // FSNOTIFY_DEBUG: 11:34:28.989728764 512:IN_DELETE → "/tmp/file-1" package fsnotify import ( "errors" "fmt" "os" "path/filepath" "strings" ) // Watcher watches a set of paths, delivering events on a channel. // // A watcher should not be copied (e.g. pass it by pointer, rather than by // value). // // # Linux notes // // When a file is removed a Remove event won't be emitted until all file // descriptors are closed, and deletes will always emit a Chmod. For example: // // fp := os.Open("file") // os.Remove("file") // Triggers Chmod // fp.Close() // Triggers Remove // // This is the event that inotify sends, so not much can be changed about this. // // The fs.inotify.max_user_watches sysctl variable specifies the upper limit // for the number of watches per user, and fs.inotify.max_user_instances // specifies the maximum number of inotify instances per user. Every Watcher you // create is an "instance", and every path you add is a "watch". // // These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and // /proc/sys/fs/inotify/max_user_instances // // To increase them you can use sysctl or write the value to the /proc file: // // # Default values on Linux 5.18 // sysctl fs.inotify.max_user_watches=124983 // sysctl fs.inotify.max_user_instances=128 // // To make the changes persist on reboot edit /etc/sysctl.conf or // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check // your distro's documentation): // // fs.inotify.max_user_watches=124983 // fs.inotify.max_user_instances=128 // // Reaching the limit will result in a "no space left on device" or "too many open // files" error. // // # kqueue notes (macOS, BSD) // // kqueue requires opening a file descriptor for every file that's being watched; // so if you're watching a directory with five files then that's six file // descriptors. You will run in to your system's "max open files" limit faster on // these platforms. // // The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to // control the maximum number of open files, as well as /etc/login.conf on BSD // systems. // // # Windows notes // // Paths can be added as "C:\\path\\to\\dir", but forward slashes // ("C:/path/to/dir") will also work. // // When a watched directory is removed it will always send an event for the // directory itself, but may not send events for all files in that directory. // Sometimes it will send events for all files, sometimes it will send no // events, and often only for some files. // // The default ReadDirectoryChangesW() buffer size is 64K, which is the largest // value that is guaranteed to work with SMB filesystems. If you have many // events in quick succession this may not be enough, and you will have to use // [WithBufferSize] to increase the value. type Watcher struct { b backend // Events sends the filesystem change events. // // fsnotify can send the following events; a "path" here can refer to a // file, directory, symbolic link, or special file like a FIFO. // // fsnotify.Create A new path was created; this may be followed by one // or more Write events if data also gets written to a // file. // // fsnotify.Remove A path was removed. // // fsnotify.Rename A path was renamed. A rename is always sent with the // old path as Event.Name, and a Create event will be // sent with the new name. Renames are only sent for // paths that are currently watched; e.g. moving an // unmonitored file into a monitored directory will // show up as just a Create. Similarly, renaming a file // to outside a monitored directory will show up as // only a Rename. // // fsnotify.Write A file or named pipe was written to. A Truncate will // also trigger a Write. A single "write action" // initiated by the user may show up as one or multiple // writes, depending on when the system syncs things to // disk. For example when compiling a large Go program // you may get hundreds of Write events, and you may // want to wait until you've stopped receiving them // (see the dedup example in cmd/fsnotify). // // Some systems may send Write event for directories // when the directory content changes. // // fsnotify.Chmod Attributes were changed. On Linux this is also sent // when a file is removed (or more accurately, when a // link to an inode is removed). On kqueue it's sent // when a file is truncated. On Windows it's never // sent. Events chan Event // Errors sends any errors. Errors chan error } // Event represents a file system notification. type Event struct { // Path to the file or directory. // // 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 // Create events will have this set to the old path if it's a rename. This // only works when both the source and destination are watched. It's not // reliable when watching individual files, only directories. // // For example "mv /tmp/file /tmp/rename" will emit: // // Event{Op: Rename, Name: "/tmp/file"} // Event{Op: Create, Name: "/tmp/rename", RenamedFrom: "/tmp/file"} renamedFrom string } // Op describes a set of file operations. 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 watches 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 // File descriptor was opened. // // Only works on Linux and FreeBSD. xUnportableOpen // File was read from. // // Only works on Linux and FreeBSD. xUnportableRead // File opened for writing was closed. // // Only works on Linux and FreeBSD. // // The advantage of using this over Write is that it's more reliable than // waiting for Write events to stop. It's also faster (if you're not // listening to Write events): copying a file of a few GB can easily // generate tens of thousands of Write events in a short span of time. xUnportableCloseWrite // File opened for reading was closed. // // Only works on Linux and FreeBSD. xUnportableCloseRead ) var ( // ErrNonExistentWatch is used when Remove() is called on a path that's not // added. ErrNonExistentWatch = errors.New("fsnotify: can't remove non-existent watch") // ErrClosed is used when trying to operate on a closed Watcher. ErrClosed = errors.New("fsnotify: watcher already closed") // ErrEventOverflow is reported from the Errors channel when there are too // many events: // // - inotify: inotify returns IN_Q_OVERFLOW – because there are too // many queued events (the fs.inotify.max_queued_events // sysctl can be used to increase this). // - windows: The buffer size is too small; WithBufferSize() can be used to increase it. // - kqueue, fen: Not used. ErrEventOverflow = errors.New("fsnotify: queue or buffer overflow") // ErrUnsupported is returned by AddWith() when WithOps() specified an // Unportable event that's not supported on this platform. xErrUnsupported = errors.New("fsnotify: not supported with this backend") ) // NewWatcher creates a new Watcher. func NewWatcher() (*Watcher, error) { ev, errs := make(chan Event), make(chan error) b, err := newBackend(ev, errs) if err != nil { return nil, err } return &Watcher{b: b, Events: ev, Errors: errs}, nil } // NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events // channel. // // The main use case for this is situations with a very large number of events // where the kernel buffer size can't be increased (e.g. due to lack of // permissions). An unbuffered Watcher will perform better for almost all use // cases, and whenever possible you will be better off increasing the kernel // buffers instead of adding a large userspace buffer. func NewBufferedWatcher(sz uint) (*Watcher, error) { ev, errs := make(chan Event), make(chan error) b, err := newBufferedBackend(sz, ev, errs) if err != nil { return nil, err } return &Watcher{b: b, Events: ev, Errors: errs}, nil } // Add starts monitoring the path for changes. // // A path can only be watched once; watching it more than once is a no-op and will // not return an error. Paths that do not yet exist on the filesystem cannot be // watched. // // A watch will be automatically removed if the watched path is deleted or // renamed. The exception is the Windows backend, which doesn't remove the // watcher on renames. // // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // filesystems (/proc, /sys, etc.) generally don't work. // // Returns [ErrClosed] if [Watcher.Close] was called. // // See [Watcher.AddWith] for a version that allows adding options. // // # Watching directories // // All files in a directory are monitored, including new files that are created // after the watcher is started. Subdirectories are not watched (i.e. it's // non-recursive). // // # Watching files // // Watching individual files (rather than directories) is generally not // recommended as many programs (especially editors) update files atomically: it // will write to a temporary file which is then moved to destination, // overwriting the original (or some variant thereof). The watcher on the // original file is now lost, as that no longer exists. // // The upshot of this is that a power failure or crash won't leave a // half-written file. // // Watch the parent directory and use Event.Name to filter out files you're not // interested in. There is an example of this in cmd/fsnotify/file.go. func (w *Watcher) Add(path string) error { return w.b.Add(path) } // AddWith is like [Watcher.Add], but allows adding options. When using Add() // the defaults described below are used. // // Possible options are: // // - [WithBufferSize] sets the buffer size for the Windows backend; no-op on // other platforms. The default is 64K (65536 bytes). func (w *Watcher) AddWith(path string, opts ...addOpt) error { return w.b.AddWith(path, opts...) } // Remove stops monitoring the path for changes. // // Directories are always removed non-recursively. For example, if you added // /tmp/dir and /tmp/dir/subdir then you will need to remove both. // // Removing a path that has not yet been added returns [ErrNonExistentWatch]. // // Returns nil if [Watcher.Close] was called. func (w *Watcher) Remove(path string) error { return w.b.Remove(path) } // Close removes all watches and closes the Events channel. func (w *Watcher) Close() error { return w.b.Close() } // WatchList returns all paths explicitly added with [Watcher.Add] (and are not // yet removed). // // Returns nil if [Watcher.Close] was called. func (w *Watcher) WatchList() []string { return w.b.WatchList() } // Supports reports if all the listed operations are supported by this platform. // // Create, Write, Remove, Rename, and Chmod are always supported. It can only // return false for an Op starting with Unportable. func (w *Watcher) xSupports(op Op) bool { return w.b.xSupports(op) } func (o Op) String() string { var b strings.Builder if o.Has(Create) { b.WriteString("|CREATE") } if o.Has(Remove) { b.WriteString("|REMOVE") } if o.Has(Write) { b.WriteString("|WRITE") } if o.Has(xUnportableOpen) { b.WriteString("|OPEN") } if o.Has(xUnportableRead) { b.WriteString("|READ") } if o.Has(xUnportableCloseWrite) { b.WriteString("|CLOSE_WRITE") } if o.Has(xUnportableCloseRead) { b.WriteString("|CLOSE_READ") } if o.Has(Rename) { b.WriteString("|RENAME") } 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 { if e.renamedFrom != "" { return fmt.Sprintf("%-13s %q ← %q", e.Op.String(), e.Name, e.renamedFrom) } return fmt.Sprintf("%-13s %q", e.Op.String(), e.Name) } type ( backend interface { Add(string) error AddWith(string, ...addOpt) error Remove(string) error WatchList() []string Close() error xSupports(Op) bool } addOpt func(opt *withOpts) withOpts struct { bufsize int op Op noFollow bool sendCreate bool } ) var debug = func() bool { // Check for exactly "1" (rather than mere existence) so we can add // options/flags in the future. I don't know if we ever want that, but it's // nice to leave the option open. return os.Getenv("FSNOTIFY_DEBUG") == "1" }() var defaultOpts = withOpts{ bufsize: 65536, // 64K op: Create | Write | Remove | Rename | Chmod, } func getOptions(opts ...addOpt) withOpts { with := defaultOpts for _, o := range opts { if o != nil { o(&with) } } return with } // 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 } } // WithOps sets which operations to listen for. The default is [Create], // [Write], [Remove], [Rename], and [Chmod]. // // Excluding operations you're not interested in can save quite a bit of CPU // time; in some use cases there may be hundreds of thousands of useless Write // or Chmod operations per second. // // This can also be used to add unportable operations not supported by all // platforms; unportable operations all start with "Unportable": // [UnportableOpen], [UnportableRead], [UnportableCloseWrite], and // [UnportableCloseRead]. // // AddWith returns an error when using an unportable operation that's not // supported. Use [Watcher.Support] to check for support. func withOps(op Op) addOpt { return func(opt *withOpts) { opt.op = op } } // WithNoFollow disables following symlinks, so the symlinks themselves are // watched. func withNoFollow() addOpt { return func(opt *withOpts) { opt.noFollow = true } } // "Internal" option for recursive watches on inotify. func withCreate() addOpt { return func(opt *withOpts) { opt.sendCreate = true } } var enableRecurse = false // Check if this path is recursive (ends with "/..." or "\..."), and return the // path with the /... stripped. func recursivePath(path string) (string, bool) { path = filepath.Clean(path) if !enableRecurse { // Only enabled in tests for now. return path, false } if filepath.Base(path) == "..." { return filepath.Dir(path), true } return path, false } fsnotify-1.8.0/fsnotify_test.go000066400000000000000000000403311471065432700166220ustar00rootroot00000000000000package 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() enableRecurse = true } func TestScript(t *testing.T) { err := filepath.Walk("./testdata", func(path string, info fs.FileInfo, err error) error { if err != nil || info.IsDir() { return err } n := strings.Split(filepath.ToSlash(path), "/") t.Run(strings.Join(n[1:], "/"), func(t *testing.T) { t.Parallel() d, err := os.ReadFile(path) if err != nil { t.Fatal(err) } parseScript(t, string(d)) }) return nil }) if err != nil { t.Fatal(err) } } // Multiple writes to a file with the same fd. func TestWatchMultipleWrite(t *testing.T) { t.Parallel() w := newCollector(t) w.collect(t) tmp := t.TempDir() echoAppend(t, "data", tmp, "file") addWatch(t, w.w, tmp) fp, err := os.OpenFile(join(tmp, "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) } cmpEvents(t, tmp, w.stop(t), newEvents(t, ` write /file # write X write /file # write Y `)) } // Remove watched file with open fd func TestWatchRemoveOpenFd(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("Windows hard-locks open files so this will never work") } t.Parallel() tmp := t.TempDir() w := newCollector(t) w.collect(t) touch(t, tmp, "/file") fp, err := os.Open(join(tmp, "/file")) if err != nil { t.Fatal(err) } defer fp.Close() addWatch(t, w.w, tmp, "/file") rm(t, tmp, "/file") cmpEvents(t, tmp, w.stop(t), newEvents(t, ` 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 watched directory func TestWatchRemoveWatchedDir(t *testing.T) { if runtime.GOOS == "dragonfly" { t.Skip("broken: inconsistent events") // TODO } 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 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") } // TODO(v2): 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 `)) }) } 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) echoAppend(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) { supportsRecurse(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{Name: "/file", Op: 0}, `[no events] "/file"`}, {Event{Name: "/file", Op: Chmod | Create}, `CREATE|CHMOD "/file"`}, {Event{Name: "/file", Op: Rename}, `RENAME "/file"`}, {Event{Name: "/file", Op: Remove}, `REMOVE "/file"`}, {Event{Name: "/file", Op: 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) } }) } } func TestWatchList(t *testing.T) { 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) }) } // Would panic on inotify: https://github.com/fsnotify/fsnotify/issues/616 func TestRemoveRace(t *testing.T) { t.Parallel() tmp := t.TempDir() w := newCollector(t, tmp) w.collect(t) dir := join(tmp, "/dir") for i := 0; i < 100; i++ { go os.MkdirAll(dir, 0o0755) go os.RemoveAll(dir) go w.w.Add(dir) go w.w.Remove(dir) } time.Sleep(100 * time.Millisecond) w.stop(t) } fsnotify-1.8.0/go.mod000066400000000000000000000004271471065432700145030ustar00rootroot00000000000000module github.com/fsnotify/fsnotify go 1.17 require golang.org/x/sys v0.13.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.8.0/go.sum000066400000000000000000000002311471065432700145210ustar00rootroot00000000000000golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= fsnotify-1.8.0/helpers_test.go000066400000000000000000000554561471065432700164410ustar00rootroot00000000000000package fsnotify import ( "fmt" "io/fs" "os" "path/filepath" "runtime" "sort" "strconv" "strings" "sync" "testing" "time" "github.com/fsnotify/fsnotify/internal" "github.com/fsnotify/fsnotify/internal/ztest" ) // 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() } } // echo > and echo >> func echoAppend(t *testing.T, data string, path ...string) { t.Helper(); echo(t, false, data, path...) } func echoTrunc(t *testing.T, data string, path ...string) { t.Helper(); echo(t, true, data, path...) } func echo(t *testing.T, trunc bool, data string, path ...string) { n := "echoAppend" if trunc { n = "echoTrunc" } t.Helper() if len(path) < 1 { t.Fatalf("%s: path must have at least one element: %s", n, path) } err := func() error { var ( fp *os.File err error ) if trunc { fp, err = os.Create(join(path...)) } else { 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("%s(%q): %s", n, 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() } } // cat func cat(t *testing.T, path ...string) { t.Helper() if len(path) < 1 { t.Fatalf("cat: path must have at least one element: %s", path) } _, err := os.ReadFile(join(path...)) if err != nil { t.Fatalf("cat(%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") } if ee.renamedFrom != "" { fmt.Fprintf(b, "%-8s %s ← %s", ee.Op.String(), filepath.ToSlash(ee.Name), filepath.ToSlash(ee.renamedFrom)) } else { fmt.Fprintf(b, "%-8s %s", 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) } if e[i].renamedFrom == prefix { e[i].renamedFrom = "/" } else { e[i].renamedFrom = strings.TrimPrefix(e[i].renamedFrom, 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 && len(fields) != 4 { if strings.ToLower(fields[0]) == "empty" || strings.ToLower(fields[0]) == "no-events" { for _, g := range groups { events[g] = Events{} } continue } t.Fatalf("newEvents: line %d: needs 2 or 4 fields: %s", no+1, line) } var op Op for _, ee := range strings.Split(fields[0], "|") { switch strings.ToUpper(ee) { case "CREATE": op |= Create case "WRITE": op |= Write case "REMOVE": op |= Remove case "RENAME": op |= Rename case "CHMOD": op |= Chmod case "OPEN": op |= xUnportableOpen case "READ": op |= xUnportableRead case "CLOSE_WRITE": op |= xUnportableCloseWrite case "CLOSE_READ": op |= xUnportableCloseRead default: t.Fatalf("newEvents: line %d has unknown event %q: %s", no+1, ee, line) } } var from string if len(fields) > 2 { if fields[2] != "←" { t.Fatalf("newEvents: line %d: invalid format: %s", no+1, line) } from = strings.Trim(fields[3], `"`) } if !supportsRename() { from = "" } for _, g := range groups { events[g] = append(events[g], Event{Name: strings.Trim(fields[1], `"`), renamedFrom: from, 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() { b := new(strings.Builder) b.WriteString(strings.TrimSpace(ztest.Diff(indent(haveSort), indent(wantSort)))) t.Errorf("\nhave:\n%s\nwant:\n%s\ndiff:\n%s", indent(have), indent(want), indent(b)) } } func indent(s fmt.Stringer) string { return "\t" + strings.ReplaceAll(s.String(), "\n", "\n\t") } var join = filepath.Join 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 supportsRecurse(t *testing.T) { switch runtime.GOOS { case "windows", "linux": // Run test. default: t.Skip("recursion not yet supported on " + runtime.GOOS) } } func supportsFilter(t *testing.T) { switch runtime.GOOS { case "linux": // Run test. default: t.Skip("withOps() not yet supported on " + runtime.GOOS) } } func supportsRename() bool { switch runtime.GOOS { case "linux", "windows": return true default: return false } } func supportsNofollow(t *testing.T) { switch runtime.GOOS { case "linux": // Run test. default: t.Skip("withNoFollow() not yet supported on " + runtime.GOOS) } } func tmppath(tmp, s string) string { if len(s) == 0 { return "" } if !strings.HasPrefix(s, "./") { return filepath.Join(tmp, s) } // Needed for creating relative links. Support that only with explicit "./" // – otherwise too easy to forget leading "/" and create files outside of // the tmp dir. return s } type command struct { line int cmd string args []string } func parseScript(t *testing.T, in string) { var ( lines = strings.Split(in, "\n") cmds = make([]command, 0, 8) readW bool want string tmp = t.TempDir() ) for i, line := range lines { line = strings.TrimSpace(line) if line == "" || line[0] == '#' { continue } if i := strings.IndexByte(line, '#'); i > -1 { line = strings.TrimSpace(line[:i]) } if line == "Output:" { readW = true continue } if readW { want += line + "\n" continue } cmd := command{line: i + 1, args: make([]string, 0, 4)} var ( q bool cur = make([]rune, 0, 16) app = func() { if len(cur) == 0 { return } if cmd.cmd == "" { cmd.cmd = string(cur) } else { cmd.args = append(cmd.args, string(cur)) } cur = cur[:0] } ) for _, c := range line { switch c { case ' ', '\t': if q { cur = append(cur, c) } else { app() } case '"', '\'': // ' q = !q default: cur = append(cur, c) } } app() cmds = append(cmds, cmd) } var ( do = make([]func(), 0, len(cmds)) w = newCollector(t) mustArg = func(c command, n int) { if len(c.args) != n { t.Fatalf("line %d: %q requires exactly %d argument (have %d: %q)", c.line, c.cmd, n, len(c.args), c.args) } } ) loop: for _, c := range cmds { c := c //fmt.Printf("line %d: %q %q\n", c.line, c.cmd, c.args) switch c.cmd { case "skip", "require": mustArg(c, 1) switch c.args[0] { case "op_all": if runtime.GOOS != "linux" { t.Skip("No op_all on this platform") } case "op_open": if runtime.GOOS != "linux" { t.Skip("No Open on this platform") } case "op_read": if runtime.GOOS != "linux" { t.Skip("No Read on this platform") } case "op_close_write": if runtime.GOOS != "linux" { t.Skip("No CloseWrite on this platform") } case "op_close_read": if runtime.GOOS != "linux" { t.Skip("No CloseRead on this platform") } case "always": t.Skip() case "symlink": if !internal.HasPrivilegesForSymlink() { t.Skipf("%s symlink: admin permissions required on Windows", c.cmd) } case "mkfifo": if runtime.GOOS == "windows" { t.Skip("No named pipes on Windows") } case "mknod": 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"`) } case "recurse": supportsRecurse(t) case "filter": supportsFilter(t) case "nofollow": supportsNofollow(t) case "windows": if runtime.GOOS == "windows" { t.Skip("Skipping on Windows") } case "netbsd": if runtime.GOOS == "netbsd" { t.Skip("Skipping on NetBSD") } case "openbsd": if runtime.GOOS == "openbsd" { t.Skip("Skipping on OpenBSD") } default: t.Fatalf("line %d: unknown %s reason: %q", c.line, c.cmd, c.args[0]) } //case "state": // mustArg(c, 0) // do = append(do, func() { eventSeparator(); fmt.Fprintln(os.Stderr); w.w.state(); fmt.Fprintln(os.Stderr) }) case "debug": mustArg(c, 1) switch c.args[0] { case "1", "on", "true", "yes": do = append(do, func() { debug = true }) case "0", "off", "false", "no": do = append(do, func() { debug = false }) default: t.Fatalf("line %d: unknown debug: %q", c.line, c.args[0]) } case "stop": mustArg(c, 0) break loop case "watch": if len(c.args) < 1 { t.Fatalf("line %d: %q requires at least %d arguments (have %d: %q)", c.line, c.cmd, 1, len(c.args), c.args) } if len(c.args) == 1 { do = append(do, func() { addWatch(t, w.w, tmppath(tmp, c.args[0])) }) continue } var follow addOpt for i := range c.args { if c.args[i] == "nofollow" || c.args[i] == "no-follow" { c.args = append(c.args[:i], c.args[i+1:]...) follow = withNoFollow() break } } var op Op for _, o := range c.args[1:] { switch strings.ToLower(o) { default: t.Fatalf("line %d: unknown: %q", c.line+1, o) case "default": op |= Create | Write | Remove | Rename | Chmod case "create": op |= Create case "write": op |= Write case "remove": op |= Remove case "rename": op |= Rename case "chmod": op |= Chmod case "open": op |= xUnportableOpen case "read": op |= xUnportableRead case "close_write": op |= xUnportableCloseWrite case "close_read": op |= xUnportableCloseRead } } do = append(do, func() { p := tmppath(tmp, c.args[0]) err := w.w.AddWith(p, withOps(op), follow) if err != nil { t.Fatalf("line %d: addWatch(%q): %s", c.line+1, p, err) } }) case "unwatch": mustArg(c, 1) do = append(do, func() { rmWatch(t, w.w, tmppath(tmp, c.args[0])) }) case "watchlist": mustArg(c, 1) n, err := strconv.ParseInt(c.args[0], 10, 0) if err != nil { t.Fatalf("line %d: %s", c.line, err) } do = append(do, func() { wl := w.w.WatchList() if l := int64(len(wl)); l != n { t.Errorf("line %d: watchlist has %d entries, not %d\n%q", c.line, l, n, wl) } }) case "touch": mustArg(c, 1) do = append(do, func() { touch(t, tmppath(tmp, c.args[0])) }) case "mkdir": recur := false if len(c.args) == 2 && c.args[0] == "-p" { recur, c.args = true, c.args[1:] } mustArg(c, 1) if recur { do = append(do, func() { mkdirAll(t, tmppath(tmp, c.args[0])) }) } else { do = append(do, func() { mkdir(t, tmppath(tmp, c.args[0])) }) } case "ln": mustArg(c, 3) if c.args[0] != "-s" { t.Fatalf("line %d: only ln -s is supported", c.line) } do = append(do, func() { symlink(t, tmppath(tmp, c.args[1]), tmppath(tmp, c.args[2])) }) case "mkfifo": mustArg(c, 1) do = append(do, func() { mkfifo(t, tmppath(tmp, c.args[0])) }) case "mknod": mustArg(c, 2) n, err := strconv.ParseInt(c.args[0], 10, 0) if err != nil { t.Fatalf("line %d: %s", c.line, err) } do = append(do, func() { mknod(t, int(n), tmppath(tmp, c.args[1])) }) case "mv": mustArg(c, 2) do = append(do, func() { mv(t, tmppath(tmp, c.args[0]), tmppath(tmp, c.args[1])) }) case "rm": recur := false if len(c.args) == 2 && c.args[0] == "-r" { recur, c.args = true, c.args[1:] } mustArg(c, 1) if recur { do = append(do, func() { rmAll(t, tmppath(tmp, c.args[0])) }) } else { do = append(do, func() { rm(t, tmppath(tmp, c.args[0])) }) } case "chmod": mustArg(c, 2) n, err := strconv.ParseUint(c.args[0], 8, 32) if err != nil { t.Fatalf("line %d: %s", c.line, err) } do = append(do, func() { chmod(t, fs.FileMode(n), tmppath(tmp, c.args[1])) }) case "cat": mustArg(c, 1) do = append(do, func() { cat(t, tmppath(tmp, c.args[0])) }) case "echo": if len(c.args) < 2 || len(c.args) > 3 { t.Fatalf("line %d: %q requires 2 or 3 arguments (have %d: %q)", c.line, c.cmd, len(c.args), c.args) } var data, op, dst string if len(c.args) == 2 { // echo foo >dst data, op, dst = c.args[0], c.args[1][:1], c.args[1][1:] if strings.HasPrefix(dst, ">") { op, dst = op+dst[:1], dst[1:] } } else { // echo foo > dst data, op, dst = c.args[0], c.args[1], c.args[2] } switch op { case ">": do = append(do, func() { echoTrunc(t, data, tmppath(tmp, dst)) }) case ">>": do = append(do, func() { echoAppend(t, data, tmppath(tmp, dst)) }) default: t.Fatalf("line %d: echo requires > (truncate) or >> (append): echo data >file", c.line) } case "sleep": mustArg(c, 1) n, err := strconv.ParseInt(strings.TrimRight(c.args[0], "ms"), 10, 0) if err != nil { t.Fatalf("line %d: %s", c.line, err) } do = append(do, func() { time.Sleep(time.Duration(n) * time.Millisecond) }) default: t.Errorf("line %d: unknown command %q", c.line, c.cmd) } } w.collect(t) for _, d := range do { d() } ev := w.stop(t) cmpEvents(t, tmp, ev, newEvents(t, want)) } fsnotify-1.8.0/internal/000077500000000000000000000000001471065432700152065ustar00rootroot00000000000000fsnotify-1.8.0/internal/darwin.go000066400000000000000000000017151471065432700170250ustar00rootroot00000000000000//go: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.8.0/internal/debug_darwin.go000066400000000000000000000040351471065432700201710ustar00rootroot00000000000000package 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.8.0/internal/debug_dragonfly.go000066400000000000000000000016761471065432700207020ustar00rootroot00000000000000package 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.8.0/internal/debug_freebsd.go000066400000000000000000000024361471065432700203220ustar00rootroot00000000000000package 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.8.0/internal/debug_kqueue.go000066400000000000000000000011171471065432700202020ustar00rootroot00000000000000//go: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 unknown = mask ) for _, n := range names { if mask&n.m == n.m { l = append(l, n.n) unknown ^= n.m } } if unknown > 0 { l = append(l, fmt.Sprintf("0x%x", unknown)) } fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s %10d:%-60s → %q\n", time.Now().Format("15:04:05.000000000"), mask, strings.Join(l, " | "), name) } fsnotify-1.8.0/internal/debug_linux.go000066400000000000000000000023531471065432700200450ustar00rootroot00000000000000package internal import ( "fmt" "os" "strings" "time" "golang.org/x/sys/unix" ) func Debug(name string, mask, cookie uint32) { names := []struct { n string m uint32 }{ {"IN_ACCESS", unix.IN_ACCESS}, {"IN_ATTRIB", unix.IN_ATTRIB}, {"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_IGNORED", unix.IN_IGNORED}, {"IN_ISDIR", unix.IN_ISDIR}, {"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_OPEN", unix.IN_OPEN}, {"IN_Q_OVERFLOW", unix.IN_Q_OVERFLOW}, {"IN_UNMOUNT", unix.IN_UNMOUNT}, } var ( l []string unknown = mask ) for _, n := range names { if mask&n.m == n.m { l = append(l, n.n) unknown ^= n.m } } if unknown > 0 { l = append(l, fmt.Sprintf("0x%x", unknown)) } var c string if cookie > 0 { c = fmt.Sprintf("(cookie: %d) ", cookie) } fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s %-30s → %s%q\n", time.Now().Format("15:04:05.000000000"), strings.Join(l, "|"), c, name) } fsnotify-1.8.0/internal/debug_netbsd.go000066400000000000000000000012321471065432700201600ustar00rootroot00000000000000package 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.8.0/internal/debug_openbsd.go000066400000000000000000000014261471065432700203400ustar00rootroot00000000000000package 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.8.0/internal/debug_solaris.go000066400000000000000000000017051471065432700203620ustar00rootroot00000000000000package 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 unknown = mask ) for _, n := range names { if mask&n.m == n.m { l = append(l, n.n) unknown ^= n.m } } if unknown > 0 { l = append(l, fmt.Sprintf("0x%x", unknown)) } fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s %10d:%-30s → %q\n", time.Now().Format("15:04:05.000000000"), mask, strings.Join(l, " | "), name) } fsnotify-1.8.0/internal/debug_windows.go000066400000000000000000000015551471065432700204030ustar00rootroot00000000000000package internal import ( "fmt" "os" "path/filepath" "strings" "time" "golang.org/x/sys/windows" ) func Debug(name string, mask uint32) { names := []struct { n string m uint32 }{ {"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 unknown = mask ) for _, n := range names { if mask&n.m == n.m { l = append(l, n.n) unknown ^= n.m } } if unknown > 0 { l = append(l, fmt.Sprintf("0x%x", unknown)) } fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s %-65s → %q\n", time.Now().Format("15:04:05.000000000"), strings.Join(l, " | "), filepath.ToSlash(name)) } fsnotify-1.8.0/internal/freebsd.go000066400000000000000000000013561471065432700171540ustar00rootroot00000000000000//go: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.8.0/internal/internal.go000066400000000000000000000000741471065432700173520ustar00rootroot00000000000000// Package internal contains some helpers. package internal fsnotify-1.8.0/internal/unix.go000066400000000000000000000013761471065432700165270ustar00rootroot00000000000000//go: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.8.0/internal/unix2.go000066400000000000000000000001341471065432700166000ustar00rootroot00000000000000//go:build !windows package internal func HasPrivilegesForSymlink() bool { return true } fsnotify-1.8.0/internal/windows.go000066400000000000000000000017021471065432700172270ustar00rootroot00000000000000//go: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.8.0/internal/ztest/000077500000000000000000000000001471065432700163575ustar00rootroot00000000000000fsnotify-1.8.0/internal/ztest/diff.go000066400000000000000000000411501471065432700176170ustar00rootroot00000000000000// Copy of https://github.com/arp242/zstd/tree/master/ztest – vendored here so // we don't add a dependency for just one file used in tests. DiffXML was // removed as it depends on zgo.at/zstd/zxml. // This code is based on https://github.com/pmezard/go-difflib // // Copyright (c) 2013, Patrick Mezard // 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. // The names of its contributors may not 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 // HOLDER 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. package ztest import ( "encoding/json" "fmt" "regexp" "strings" "sync" "time" ) type DiffOpt int const ( // Normalize whitespace: remove all whitespace at the start and end of every // line. DiffNormalizeWhitespace DiffOpt = iota + 1 // Treat arguments as JSON: format them before diffing. DiffJSON ) // Diff two strings and format as a unified diff. func Diff(have, want string, opt ...DiffOpt) string { have, want = applyOpt(have, want, opt...) d := makeUnifiedDiff(unifiedDiff{ A: splitLines(strings.TrimSpace(have)), B: splitLines(strings.TrimSpace(want)), Context: 3, }) if len(d) == 0 { return "" } return "\n" + d } // DiffMatch formats a unified diff, but accepts various patterns in the want // string: // // %(YEAR) current year in UTC // %(MONTH) current month in UTC // %(DAY) current day in UTC // %(UUID) UUID format (any version). // // %(ANY) any text: .+? // %(ANY 5) any text of exactly 5 characters: .{5}? // %(ANY 5,) any text of at least 5 characters: .{5,}? // %(ANY 5,10) any text between 5 and 10 characters: .{5,10}? // %(ANY 10) any text at most 10 characters: .{,10}? // %(NUMBER) any number; also allows length like ANY. // // %(..) any regular expression, but \ is not allowed. func DiffMatch(have, want string, opt ...DiffOpt) string { // TODO: %(..) syntax is somewhat unfortunate, as it conflicts with fmt // formatting strings. Would be better to use $(..), #(..), @(..), or // anything else really. have, want = applyOpt(have, want, opt...) now := time.Now().UTC() r := strings.NewReplacer( `%(YEAR)`, fmt.Sprintf("%d", now.Year()), `%(MONTH)`, fmt.Sprintf("%02d", now.Month()), `%(DAY)`, fmt.Sprintf("%02d", now.Day()), ) wantRe := regexp.MustCompile(`%\\\(.+?\\\)`).ReplaceAllStringFunc( regexp.QuoteMeta(r.Replace(want)), func(m string) string { switch { case m == `%\(UUID\)`: return `[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}` case m == `%\(ANY\)`: return `.+?` case m == `%\(NUMBER\)`: return `\d+?` case strings.HasPrefix(m, `%\(ANY `): return fmt.Sprintf(`.{%s}?`, m[7:len(m)-2]) case strings.HasPrefix(m, `%\(NUMBER `): return fmt.Sprintf(`\d{%s}?`, m[10:len(m)-2]) default: // TODO: we need to undo the \ from QuoteMeta() here, but this // means we won't be allowed to use \. Be a bit smarter about // this. TODO: doesn't quite seem to work? return strings.ReplaceAll(m[3:len(m)-2], `\`, ``) } }) // Quick check for exact match. if m := regexp.MustCompile(`^` + wantRe + `$`).MatchString(have); m { return "" } diff := unifiedDiff{ A: splitLines(strings.TrimSpace(have)), B: splitLines(strings.TrimSpace(wantRe)), Context: 3, } m := newMatcher(diff.A, diff.B) m.cmp = func(a, b string) bool { return regexp.MustCompile(b).MatchString(a) } diff.Matcher = m d := makeUnifiedDiff(diff) if len(d) == 0 { return "ztest.DiffMatch: strings didn't match but no diff?" // Should never happen. } return "\n" + d } var ( reNormalizeWhitespace *regexp.Regexp reNormalizeWhitespaceOnce sync.Once ) func applyOpt(have, want string, opt ...DiffOpt) (string, string) { for _, o := range opt { switch o { case DiffNormalizeWhitespace: reNormalizeWhitespaceOnce.Do(func() { reNormalizeWhitespace = regexp.MustCompile(`(?m)(^\s+|\s+$)`) }) have = reNormalizeWhitespace.ReplaceAllString(have, "") want = reNormalizeWhitespace.ReplaceAllString(want, "") case DiffJSON: if have == "" { have = "{}" } if want == "" { want = "{}" } var h interface{} haveJ, err := indentJSON([]byte(have), &h, "", " ") if err != nil { have = fmt.Sprintf("ztest.Diff: ERROR formatting have: %s\ntext: %s", err, have) } else { have = string(haveJ) } var w interface{} wantJ, err := indentJSON([]byte(want), &w, "", " ") if err != nil { want = fmt.Sprintf("ztest.Diff: ERROR formatting want: %s\ntext: %s", err, want) } else { want = string(wantJ) } } } return have, want } type match struct{ A, B, Size int } type opCode struct { Tag byte I1, I2, J1, J2 int } // sequenceMatcher compares sequence of strings. The basic // algorithm predates, and is a little fancier than, an algorithm // published in the late 1980's by Ratcliff and Obershelp under the // hyperbolic name "gestalt pattern matching". The basic idea is to find // the longest contiguous matching subsequence. // // Timing: Basic R-O is cubic time worst case and quadratic time expected // case. sequenceMatcher is quadratic time for the worst case and has // expected-case behavior dependent in a complicated way on how many // elements the sequences have in common; best case time is linear. type sequenceMatcher struct { a, b []string cmp func(a, b string) bool } func newMatcher(a, b []string) *sequenceMatcher { return &sequenceMatcher{ a: a, b: b, cmp: func(a, b string) bool { return a == b }, } } // Find longest matching block in a[alo:ahi] and b[blo:bhi]. // // Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where // // alo <= i <= i+k <= ahi // blo <= j <= j+k <= bhi // // and for all (i',j',k') meeting those conditions, // // k >= k' // i <= i' // and if i == i', j <= j' // // In other words, of all maximal matching blocks, return one that // starts earliest in a, and of all those maximal matching blocks that // start earliest in a, return the one that starts earliest in b. // // If no blocks match, return (alo, blo, 0). func (m *sequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) match { // Populate line -> index mapping b2j := make(map[string][]int) for i, s := range m.b { b2j[s] = append(b2j[s], i) } // CAUTION: stripping common prefix or suffix would be incorrect. // E.g., // ab // acab // Longest matching block is "ab", but if common prefix is // stripped, it's "a" (tied with "b"). UNIX(tm) diff does so // strip, so ends up claiming that ab is changed to acab by // inserting "ca" in the middle. That's minimal but unintuitive: // "it's obvious" that someone inserted "ac" at the front. // Windiff ends up at the same place as diff, but by pairing up // the unique 'b's and then matching the first two 'a's. besti, bestj, bestsize := alo, blo, 0 // find longest match. During an iteration of the loop, j2len[j] = length of // longest match ending with a[i-1] and b[j] j2len := map[int]int{} for i := alo; i != ahi; i++ { // look at all instances of a[i] in b. newj2len := map[int]int{} for _, j := range b2j[m.a[i]] { // a[i] matches b[j] if j < blo { continue } if j >= bhi { break } k := j2len[j-1] + 1 newj2len[j] = k if k > bestsize { besti, bestj, bestsize = i-k+1, j-k+1, k } } j2len = newj2len } // Extend the best by elements on each end. In particular, "popular" // elements aren't in b2j, which greatly speeds the inner loop above. for besti > alo && bestj > blo && m.cmp(m.a[besti-1], m.b[bestj-1]) { besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 } for besti+bestsize < ahi && bestj+bestsize < bhi && m.cmp(m.a[besti+bestsize], m.b[bestj+bestsize]) { bestsize += 1 } return match{A: besti, B: bestj, Size: bestsize} } // Return list of triples describing matching subsequences. // // Each triple is of the form (i, j, n), and means that // a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in // i and in j. It's also guaranteed that if (i, j, n) and (i', j', n') are // adjacent triples in the list, and the second is not the last triple in the // list, then i+n != i' or j+n != j'. IOW, adjacent triples never describe // adjacent equal blocks. // // The last triple is a dummy, (len(a), len(b), 0), and is the only // triple with n==0. func (m *sequenceMatcher) matchingBlocks() []match { var matchBlocks func(alo, ahi, blo, bhi int, matched []match) []match matchBlocks = func(alo, ahi, blo, bhi int, matched []match) []match { match := m.findLongestMatch(alo, ahi, blo, bhi) i, j, k := match.A, match.B, match.Size if match.Size > 0 { if alo < i && blo < j { matched = matchBlocks(alo, i, blo, j, matched) } matched = append(matched, match) if i+k < ahi && j+k < bhi { matched = matchBlocks(i+k, ahi, j+k, bhi, matched) } } return matched } matched := matchBlocks(0, len(m.a), 0, len(m.b), nil) // It's possible that we have adjacent equal blocks in the // matching_blocks list now. nonAdjacent := []match{} i1, j1, k1 := 0, 0, 0 for _, b := range matched { // Is this block adjacent to i1, j1, k1? i2, j2, k2 := b.A, b.B, b.Size if i1+k1 == i2 && j1+k1 == j2 { // Yes, so collapse them -- this just increases the length of // the first block by the length of the second, and the first // block so lengthened remains the block to compare against. k1 += k2 } else { // Not adjacent. Remember the first block (k1==0 means it's // the dummy we started with), and make the second block the // new block to compare against. if k1 > 0 { nonAdjacent = append(nonAdjacent, match{i1, j1, k1}) } i1, j1, k1 = i2, j2, k2 } } if k1 > 0 { nonAdjacent = append(nonAdjacent, match{i1, j1, k1}) } return append(nonAdjacent, match{len(m.a), len(m.b), 0}) } // Return list of 5-tuples describing how to turn a into b. // // Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple // has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the // tuple preceding it, and likewise for j1 == the previous j2. // // The tags are characters, with these meanings: // // 'r' (replace): a[i1:i2] should be replaced by b[j1:j2] // // 'd' (delete): a[i1:i2] should be deleted, j1==j2 in this case. // // 'i' (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case. // // 'e' (equal): a[i1:i2] == b[j1:j2] func (m *sequenceMatcher) GetOpCodes() []opCode { matching := m.matchingBlocks() opCodes := make([]opCode, 0, len(matching)) var i, j int for _, m := range matching { // invariant: we've pumped out correct diffs to change // a[:i] into b[:j], and the next matching block is // a[ai:ai+size] == b[bj:bj+size]. So we need to pump // out a diff to change a[i:ai] into b[j:bj], pump out // the matching block, and move (i,j) beyond the match ai, bj, size := m.A, m.B, m.Size tag := byte(0) if i < ai && j < bj { tag = 'r' } else if i < ai { tag = 'd' } else if j < bj { tag = 'i' } if tag > 0 { opCodes = append(opCodes, opCode{tag, i, ai, j, bj}) } i, j = ai+size, bj+size // the list of matching blocks is terminated by a // sentinel with size 0 if size > 0 { opCodes = append(opCodes, opCode{'e', ai, i, bj, j}) } } return opCodes } // Isolate change clusters by eliminating ranges with no changes. // // Return a generator of groups with up to n lines of context. // Each group is in the same format as returned by GetOpCodes(). func (m *sequenceMatcher) GetGroupedOpCodes(n int) [][]opCode { if n < 0 { n = 3 } codes := m.GetOpCodes() if len(codes) == 0 { codes = []opCode{opCode{'e', 0, 1, 0, 1}} } // Fixup leading and trailing groups if they show no changes. if codes[0].Tag == 'e' { c := codes[0] i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 codes[0] = opCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2} } if codes[len(codes)-1].Tag == 'e' { c := codes[len(codes)-1] i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 codes[len(codes)-1] = opCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)} } nn := n + n groups := [][]opCode{} group := []opCode{} for _, c := range codes { i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 // End the current group and start a new one whenever // there is a large range with no changes. if c.Tag == 'e' && i2-i1 > nn { group = append(group, opCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)}) groups = append(groups, group) group = []opCode{} i1, j1 = max(i1, i2-n), max(j1, j2-n) } group = append(group, opCode{c.Tag, i1, i2, j1, j2}) } if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') { groups = append(groups, group) } return groups } // Convert range to the "ed" format func formatRangeUnified(start, stop int) string { // Per the diff spec at http://www.unix.org/single_unix_specification/ beginning := start + 1 // lines start numbering with one length := stop - start if length == 1 { return fmt.Sprintf("%d", beginning) } if length == 0 { beginning -= 1 // empty ranges begin at line just before the range } return fmt.Sprintf("%d,%d", beginning, length) } // Unified diff parameters type unifiedDiff struct { A, B []string Context int Matcher *sequenceMatcher } // Compare two sequences of lines; generate the delta as a unified diff. // // Unified diffs are a compact way of showing line changes and a few // lines of context. The number of context lines is set by 'n' which // defaults to three. // // By default, the diff control lines (those with ---, +++, or @@) are // created with a trailing newline. This is helpful so that inputs // created from file.readlines() result in diffs that are suitable for // file.writelines() since both the inputs and outputs have trailing // newlines. // // For inputs that do not have trailing newlines, set the lineterm // argument to "" so that the output will be uniformly newline free. // // The unidiff format normally has a header for filenames and modification // times. Any or all of these may be specified using strings for // 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'. // The modification times are normally expressed in the ISO 8601 format. func makeUnifiedDiff(diff unifiedDiff) string { if diff.Matcher == nil { diff.Matcher = newMatcher(diff.A, diff.B) } var ( out strings.Builder started bool ) for _, g := range diff.Matcher.GetGroupedOpCodes(diff.Context) { if !started { started = true out.WriteString("--- have\n") out.WriteString("+++ want\n") } first, last := g[0], g[len(g)-1] out.WriteString(fmt.Sprintf("@@ -%s +%s @@\n", formatRangeUnified(first.I1, last.I2), formatRangeUnified(first.J1, last.J2))) for _, c := range g { i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 if c.Tag == 'e' { for _, line := range diff.A[i1:i2] { out.WriteString(" " + line) } continue } if c.Tag == 'r' || c.Tag == 'd' { for _, line := range diff.A[i1:i2] { out.WriteString("-have " + line) } } if c.Tag == 'r' || c.Tag == 'i' { for _, line := range diff.B[j1:j2] { out.WriteString("+want " + line) } } } } return out.String() } func min(a, b int) int { if a < b { return a } return b } func max(a, b int) int { if a > b { return a } return b } func splitLines(s string) []string { lines := strings.SplitAfter(s, "\n") lines[len(lines)-1] += "\n" return lines } func indentJSON(data []byte, v interface{}, prefix, indent string) ([]byte, error) { err := json.Unmarshal(data, v) if err != nil { return nil, err } return json.MarshalIndent(v, prefix, indent) } fsnotify-1.8.0/internal/ztest/diff_test.go000066400000000000000000000160611471065432700206610ustar00rootroot00000000000000package ztest import ( "bytes" "fmt" "reflect" "strings" "testing" "time" ) func assertEqual(t *testing.T, a, b interface{}) { if !reflect.DeepEqual(a, b) { t.Errorf("%v != %v", a, b) } } func splitChars(s string) []string { chars := make([]string, 0, len(s)) // Assume ASCII inputs for i := 0; i != len(s); i++ { chars = append(chars, string(s[i])) } return chars } func rep(s string, count int) string { return strings.Repeat(s, count) } func TestGetOptCodes(t *testing.T) { a := "qabxcd" b := "abycdf" s := newMatcher(splitChars(a), splitChars(b)) w := &bytes.Buffer{} for _, op := range s.GetOpCodes() { fmt.Fprintf(w, "%s a[%d:%d], (%s) b[%d:%d] (%s)\n", string(op.Tag), op.I1, op.I2, a[op.I1:op.I2], op.J1, op.J2, b[op.J1:op.J2]) } result := w.String() expected := `d a[0:1], (q) b[0:0] () e a[1:3], (ab) b[0:2] (ab) r a[3:4], (x) b[2:3] (y) e a[4:6], (cd) b[3:5] (cd) i a[6:6], () b[5:6] (f) ` if expected != result { t.Errorf("unexpected op codes: \n%s", result) } } func TestGroupedOpCodes(t *testing.T) { a := []string{} for i := 0; i != 39; i++ { a = append(a, fmt.Sprintf("%02d", i)) } b := []string{} b = append(b, a[:8]...) b = append(b, " i") b = append(b, a[8:19]...) b = append(b, " x") b = append(b, a[20:22]...) b = append(b, a[27:34]...) b = append(b, " y") b = append(b, a[35:]...) s := newMatcher(a, b) w := &bytes.Buffer{} for _, g := range s.GetGroupedOpCodes(-1) { fmt.Fprintf(w, "group\n") for _, op := range g { fmt.Fprintf(w, " %s, %d, %d, %d, %d\n", string(op.Tag), op.I1, op.I2, op.J1, op.J2) } } result := w.String() expected := `group e, 5, 8, 5, 8 i, 8, 8, 8, 9 e, 8, 11, 9, 12 group e, 16, 19, 17, 20 r, 19, 20, 20, 21 e, 20, 22, 21, 23 d, 22, 27, 23, 23 e, 27, 30, 23, 26 group e, 31, 34, 27, 30 r, 34, 35, 30, 31 e, 35, 38, 31, 34 ` if expected != result { t.Errorf("unexpected op codes: \n%s", result) } } func TestWithAsciiOneInsert(t *testing.T) { sm := newMatcher(splitChars(rep("b", 100)), splitChars("a"+rep("b", 100))) assertEqual(t, sm.GetOpCodes(), []opCode{{'i', 0, 0, 0, 1}, {'e', 0, 100, 1, 101}}) sm = newMatcher(splitChars(rep("b", 100)), splitChars(rep("b", 50)+"a"+rep("b", 50))) assertEqual(t, sm.GetOpCodes(), []opCode{{'e', 0, 50, 0, 50}, {'i', 50, 50, 50, 51}, {'e', 50, 100, 51, 101}}) } func TestWithAsciiOnDelete(t *testing.T) { sm := newMatcher(splitChars(rep("a", 40)+"c"+rep("b", 40)), splitChars(rep("a", 40)+rep("b", 40))) assertEqual(t, sm.GetOpCodes(), []opCode{{'e', 0, 40, 0, 40}, {'d', 40, 41, 40, 40}, {'e', 41, 81, 40, 80}}) } func TestSFBugsComparingEmptyLists(t *testing.T) { groups := newMatcher(nil, nil).GetGroupedOpCodes(-1) assertEqual(t, len(groups), 0) result := Diff("", "") assertEqual(t, result, "") } func TestOutputFormatRangeFormatUnified(t *testing.T) { // Per the diff spec at http://www.unix.org/single_unix_specification/ // // Each field shall be of the form: // %1d", if the range contains exactly one line, // and: // "%1d,%1d", , otherwise. // If a range is empty, its beginning line number shall be the number of // the line just before the range, or 0 if the empty range starts the file. fm := formatRangeUnified assertEqual(t, fm(3, 3), "3,0") assertEqual(t, fm(3, 4), "4") assertEqual(t, fm(3, 5), "4,2") assertEqual(t, fm(3, 6), "4,3") assertEqual(t, fm(0, 0), "0,0") } func TestDiffMatch(t *testing.T) { now := time.Now().UTC() year := fmt.Sprintf("%d", now.Year()) tests := []struct { inGot, inWant, want string }{ {"Hello", "Hello", ""}, {"Hello", "He%(ANY)", ""}, {"Hello " + year + "!", "Hello %(YEAR)!", ""}, {"Hello " + year + "!", "Hello %(YEAR)", "\n--- have\n+++ want\n@@ -1 +1 @@\n-have Hello 2024!\n+want Hello 2024\n"}, {"Hello xy", "Hello %(ANY 2)", ""}, {"Hello xy", "Hello %(ANY 2,)", ""}, {"Hello xy", "Hello %(ANY 2,4)", ""}, {"Hello xy", "Hello %(ANY 3)", "\n--- have\n+++ want\n@@ -1 +1 @@\n-have Hello xy\n+want Hello .{3}?\n"}, {"Hello xy", "Hello %(ANY ,1)", "\n--- have\n+++ want\n@@ -1 +1 @@\n-have Hello xy\n+want Hello .{,1}?\n"}, {"Hello xy", "Hello%([a-z ]+)", ""}, {"Hello 5xy", "Hello%([a-z ]+)", "\n--- have\n+++ want\n@@ -1 +1 @@\n-have Hello 5xy\n+want Hello[a-z ]+\n"}, { `{ "ID": 1, "SiteID": 1, "StartFromHitID": 0, "LastHitID": 3, "Path": "/tmp/goatcounter-export-test-20200630T00:25:05Z-0.csv.gz", "CreatedAt": "2020-06-30T00:25:05.855750823Z", "FinishedAt": null, "NumRows": 3, "Size": "0.0", "Hash": "sha256-7b756b6dd4d908eff7f7febad0fbdf59f2d7657d8fd09c8ff5133b45f86b1fbf", "Error": null }`, `{ "ID": 1, "SiteID": 1, "StartFromHitID": 0, "LastHitID": 3, "Path": "/tmp/goatcounter-export-test-%(ANY)Z-0.csv.gz", "CreatedAt": "%(ANY)Z", "FinishedAt": null, "NumRows": 3, "Size": "0.0", "Hash": "sha256-%(ANY)", "Error": null }`, "", }, { `{ "ID": 1, "SiteID": 1, "StartFromHitID": 0, "LastHitID": 3, "Path": "/tmp/goatcounter-export-test-20200630T00:25:05Z-0.csv.gz", "CreatedAt": "2020-06-30T00:25:05.855750823Z", "FinishedAt": null, "NumRows": 5, "Size": "0.0", "Hash": "sha256-7b756b6dd4d908eff7f7febad0fbdf59f2d7657d8fd09c8ff5133b45f86b1fbf", "Error": null }`, `{ "ID": 1, "SiteID": 1, "StartFromHitID": 0, "LastHitID": 3, "Path": "/tmp/goatcounter-export-test-%(ANY)Z-0.csv.gz", "CreatedAt": "%(ANY)T%(ANY)Z", "FinishedAt": null, "NumRows": 3, "Size": "0.0", "Hash": "sha256-%(ANY)", "Error": null }`, "\n--- have\n+++ want\n@@ -6,7 +6,7 @@\n \"Path\": \"/tmp/goatcounter-export-test-20200630T00:25:05Z-0.csv.gz\",\n \"CreatedAt\": \"2020-06-30T00:25:05.855750823Z\",\n \"FinishedAt\": null,\n-have \"NumRows\": 5,\n+want \"NumRows\": 3,\n \"Size\": \"0.0\",\n \"Hash\": \"sha256-7b756b6dd4d908eff7f7febad0fbdf59f2d7657d8fd09c8ff5133b45f86b1fbf\",\n \"Error\": null\n", }, } for i, tt := range tests { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { tt.inGot = strings.ReplaceAll(tt.inGot, "\t", "") tt.inWant = strings.ReplaceAll(tt.inWant, "\t", "") got := DiffMatch(tt.inGot, tt.inWant) if got != tt.want { t.Errorf("\ngot: %q\nwant: %q", got, tt.want) } }) } } func TestDiffJSON(t *testing.T) { tests := []struct { inHave, inWant, want string }{ {`{}`, ``, ``}, {``, `{}`, ``}, {``, `{"x": "x"}`, ` --- have +++ want @@ -1 +1,3 @@ -have {} +want { +want "x": "x" +want } `}, {`[1]`, `[1]`, ``}, {`"a"`, `"a"`, ``}, {`{"a": "x"}`, `{ "a": "x"}`, ``}, {`{"a": "x"}`, `{ "a": "y"}`, ` --- have +++ want @@ -1,3 +1,3 @@ { -have "a": "x" +want "a": "y" } `}, } for i, tt := range tests { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { tt.inHave = strings.ReplaceAll(tt.inHave, "\t", "") tt.inWant = strings.ReplaceAll(tt.inWant, "\t", "") have := Diff(tt.inHave, tt.inWant, DiffJSON) if have != tt.want { t.Errorf("\nhave: %q\nwant: %q", have, tt.want) } }) } } fsnotify-1.8.0/system_bsd.go000066400000000000000000000002521471065432700160740ustar00rootroot00000000000000//go: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.8.0/system_darwin.go000066400000000000000000000002431471065432700166100ustar00rootroot00000000000000//go: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.8.0/test/000077500000000000000000000000001471065432700143515ustar00rootroot00000000000000fsnotify-1.8.0/test/kqueue.c000066400000000000000000000053671471065432700160270ustar00rootroot00000000000000// 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 | O_PATH | O_NOFOLLOW); 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; } fsnotify-1.8.0/testdata/000077500000000000000000000000001471065432700152035ustar00rootroot00000000000000fsnotify-1.8.0/testdata/watch-dir/000077500000000000000000000000001471065432700170655ustar00rootroot00000000000000fsnotify-1.8.0/testdata/watch-dir/bug-277000066400000000000000000000010361471065432700201020ustar00rootroot00000000000000# https://github.com/fsnotify/fsnotify/issues/277 require symlink touch /file1 touch /file2 ln -s /file1 /link1 ln -s /file2 /link2 watch / touch /foo rm /foo mkdir /apple mv /apple /pear rm -r /pear Output: create /foo # touch /foo remove /foo # rm /foo create /apple # mkdir /apple rename /apple # mv /apple /pear create /pear ← /apple remove /pear # rm -r /pear dragonfly: create /foo remove /foo create /apple rename /apple create /pear # No event for rm -r pear? fsnotify-1.8.0/testdata/watch-dir/create-cyclic-symlink000066400000000000000000000003001471065432700231740ustar00rootroot00000000000000# Cyclic symlink require symlink ln -s ./ /link watch / rm /link echo foo >>/link Output: write /link create /link linux, windows, fen: remove /link create /link write /link fsnotify-1.8.0/testdata/watch-dir/create-dev000066400000000000000000000000731471065432700210270ustar00rootroot00000000000000require mknod watch / mknod 0 /dev Output: create /dev fsnotify-1.8.0/testdata/watch-dir/create-dir000066400000000000000000000001041471065432700210220ustar00rootroot00000000000000# Create a new directory. watch / mkdir /dir Output: create /dir fsnotify-1.8.0/testdata/watch-dir/create-empty-file000066400000000000000000000001071471065432700223220ustar00rootroot00000000000000# Create a new empty file. watch / touch /file Output: create /file fsnotify-1.8.0/testdata/watch-dir/create-fifo000066400000000000000000000001061471065432700211710ustar00rootroot00000000000000# FIFO require mkfifo watch / mkfifo /fifo Output: create /fifo fsnotify-1.8.0/testdata/watch-dir/create-file-with-data000066400000000000000000000001531471065432700230470ustar00rootroot00000000000000# Create a new empty file with some data. watch / echo data >>/file Output: create /file write /file fsnotify-1.8.0/testdata/watch-dir/create-unresolvable-symlink000066400000000000000000000002411471065432700244330ustar00rootroot00000000000000# Create unresolvable symlink require symlink watch / ln -s /target /link Output: create /link dragonfly: # TODO: should fix this if possible. no-events fsnotify-1.8.0/testdata/watch-dir/dir-only000066400000000000000000000002251471065432700205440ustar00rootroot00000000000000# touch /before-watch watch / echo data >>/file rm /file rm /before-watch Output: create /file write /file remove /file remove /before-watch fsnotify-1.8.0/testdata/watch-dir/make-file-unreadable000066400000000000000000000004451471065432700227450ustar00rootroot00000000000000# Make file unreadable after watch. skip windows # TODO: figure out how to make a file unreadable touch /unreadable touch /file watch / chmod 0 /unreadable echo hello >>/file rm /file rm /unreadable Output: chmod /unreadable write /file remove /file remove /unreadable fsnotify-1.8.0/testdata/watch-dir/multiple-creates000066400000000000000000000007401471065432700222700ustar00rootroot00000000000000# Create a file, delete it, and re-create the same file again. # TODO: this started failing in the CI, and I don't really know why? skip netbsd skip openbsd watch / echo data >>/file rm /file touch /file # Recreate the file echo data >>/file # Modify echo data >>/file # Modify Output: create /file # echo data >>/file write /file remove /file # rm /file create /file # touch /file write /file # echo data >>/file write /file # echo data >>/file fsnotify-1.8.0/testdata/watch-dir/only-chmod000066400000000000000000000006531471065432700210650ustar00rootroot00000000000000# Listen for chmod events only. require filter watch / chmod # Create touch /file mkdir /dir ln -s /file /link # Write echo data >>/file echo data >>/link touch /dir/file # Rename mv /file /rename mv /rename /file mv /dir /rename mv /rename /dir mv /link /rename mv /rename /link # Chmod chmod 644 /file chmod 644 /link chmod 755 /dir # Remove rm /file rm /link rm -r /dir Output: chmod /file chmod /file chmod /dir fsnotify-1.8.0/testdata/watch-dir/only-create000066400000000000000000000006631471065432700212370ustar00rootroot00000000000000# Listen for create events only. require filter watch / create # Create touch /file mkdir /dir ln -s /file /link # Write echo data >>/file echo data >>/link touch /dir/file # Rename mv /file /rename mv /rename /file mv /dir /rename mv /rename /dir mv /link /rename mv /rename /link # Chmod chmod 644 /file chmod 644 /link chmod 755 /dir # Remove rm /file rm /link rm -r /dir Output: create /file create /link create /dir fsnotify-1.8.0/testdata/watch-dir/only-remove000066400000000000000000000006631471065432700212710ustar00rootroot00000000000000# Listen for remove events only. require filter watch / remove # Create touch /file mkdir /dir ln -s /file /link # Write echo data >>/file echo data >>/link touch /dir/file # Rename mv /file /rename mv /rename /file mv /dir /rename mv /rename /dir mv /link /rename mv /rename /link # Chmod chmod 644 /file chmod 644 /link chmod 755 /dir # Remove rm /file rm /link rm -r /dir Output: remove /file remove /link remove /dir fsnotify-1.8.0/testdata/watch-dir/only-rename000066400000000000000000000012271471065432700212400ustar00rootroot00000000000000# Listen for rename events only. require filter watch / rename # Create touch /file mkdir /dir ln -s /file /link # Write echo data >>/file echo data >>/link touch /dir/file # Rename mv /file /rename mv /rename /file mv /dir /rename mv /rename /dir mv /link /rename mv /rename /link # Chmod chmod 644 /file chmod 644 /link chmod 755 /dir # Remove rm /file rm /link rm -r /dir Output: rename /file create /rename ← /file rename /rename create /file ← /rename rename /dir create /rename ← /dir rename /rename create /dir ← /rename rename /link create /rename ← /link rename /rename create /link ← /rename fsnotify-1.8.0/testdata/watch-dir/only-write000066400000000000000000000006371471065432700211270ustar00rootroot00000000000000# Listen for write events only. require filter watch / write # Create touch /file mkdir /dir ln -s /file /link # Write echo data >>/file echo data >>/link touch /dir/file # Rename mv /file /rename mv /rename /file mv /dir /rename mv /rename /dir mv /link /rename mv /rename /link # Chmod chmod 644 /file chmod 644 /link chmod 755 /dir # Remove rm /file rm /link rm -r /dir Output: write /file write /file fsnotify-1.8.0/testdata/watch-dir/op-all000066400000000000000000000007411471065432700201760ustar00rootroot00000000000000require op_all watch / default open read close_write close_read touch /file ln -s /file /link echo data >>/file2 cat /file cat /file2 cat /link mkdir /dir Output: create /file open /file close_write /file create /link create /file2 open /file2 write /file2 close_write /file2 open /file close_read /file open /file2 read /file2 close_read /file2 open /file close_read /file create /dir fsnotify-1.8.0/testdata/watch-dir/op-closeread000066400000000000000000000002161471065432700213640ustar00rootroot00000000000000# Basic close_read test require op_close_read echo "hello, world!" >>/file watch / default close_read cat /file Output: close_read /file fsnotify-1.8.0/testdata/watch-dir/op-closewrite000066400000000000000000000005331471065432700216050ustar00rootroot00000000000000# Basic close_write test require op_close_write watch / default close_write touch /file echo "hello, world!" >>/file cat /file echo "hello, world!" >/file Output: create /file # touch close_write /file write /file # echo >> close_write /file write /file # echo > write /file close_write /file fsnotify-1.8.0/testdata/watch-dir/op-open000066400000000000000000000001661471065432700203700ustar00rootroot00000000000000# Basic open test require op_open echo "hello, world!" >>/file watch / default open cat /file Output: open /file fsnotify-1.8.0/testdata/watch-dir/op-read000066400000000000000000000001741471065432700203410ustar00rootroot00000000000000# Basic close_read test require op_read echo "hello, world!" >>/file watch / default read cat /file Output: read /file fsnotify-1.8.0/testdata/watch-dir/remove-symlink000066400000000000000000000002141471065432700217660ustar00rootroot00000000000000# Remove a symlink. touch /file ln -s /file /link watch / rm /link Output: remove /link kqueue: # TODO: this is broken. no-events fsnotify-1.8.0/testdata/watch-dir/remove-while-watching-parent000066400000000000000000000007701471065432700245100ustar00rootroot00000000000000# Remove while also watching parent. # TODO: kind of a mess on Windows; sends something along the lines # of: # # create /abc # write /abc # write /abc/def # write /abc/def # remove /abc/def # write /abc # remove /abc # remove /abc # # But sometimes there's one or two more or less. skip windows watch / mkdir -p /abc/def/ghi watch /abc rm -r /abc Output: create /abc remove /abc/def remove /abc dragonfly: # TODO: no remove events? create /abc fsnotify-1.8.0/testdata/watch-dir/rename-file000066400000000000000000000001721471065432700211740ustar00rootroot00000000000000# Rename file in watched dir. echo asd >>/file watch / mv /file /rename Output: rename /file create /rename ← /file fsnotify-1.8.0/testdata/watch-dir/rename-from-other-watch000066400000000000000000000006631471065432700234500ustar00rootroot00000000000000# Rename from one watched directory to another watched directory. mkdir /dir1 mkdir /dir2 touch /dir1/file watch /dir1 watch /dir2 mv /dir1/file /dir2/rename Output: rename /dir1/file create /dir2/rename ← /dir1/file # Windows just doesn't send anything for this; seems like all watches are # independent. # # TODO: consider emulating this behaviour on other platforms. windows: remove /dir1/file create /dir2/rename fsnotify-1.8.0/testdata/watch-dir/rename-from-unwatched000066400000000000000000000002171471065432700232000ustar00rootroot00000000000000# Rename from unwatched dir. mkdir /unwatch touch /unwatch/file mkdir /dir watch /dir mv /unwatch/file /dir/file Output: create /dir/file fsnotify-1.8.0/testdata/watch-dir/rename-from-unwatched-overwrite000066400000000000000000000005211471065432700252220ustar00rootroot00000000000000# Rename overwriting existing file. mkdir /dir mkdir /unwatch touch /unwatch/file touch /dir/rename watch /dir mv /unwatch/file /dir/rename Output: remove /dir/rename create /dir/rename linux: # No remove event for inotify; inotify just sends MOVE_SELF. create /dir/rename dragonfly: # TODO: this is broken. remove /dir fsnotify-1.8.0/testdata/watch-dir/rename-overwrite000066400000000000000000000004771471065432700223130ustar00rootroot00000000000000# Rename file in watched dir, overwriting an existing file. touch /file touch /rename watch / mv /file /rename Output: remove /rename rename /file create /rename ← /file # Inotify just sends MOVED_FROM and MOVED_TO. linux: rename /file create /rename ← /file dragonfly: remove / rename /file fsnotify-1.8.0/testdata/watch-dir/rename-symlink000066400000000000000000000003221471065432700217400ustar00rootroot00000000000000# Rename a symlink. require symlink touch /file ln -s /file /link watch / mv /link /link-rename Output: rename /link create /link-rename ← /link kqueue: # TODO: this is broken. create /link-rename fsnotify-1.8.0/testdata/watch-dir/rename-to-unwatched000066400000000000000000000012101471065432700226510ustar00rootroot00000000000000# Rename to unwatched dir. # if runtime.GOOS == "netbsd" && isCI() { # t.Skip("fails in CI; see #488") // TODO # } mkdir /dir mkdir /unwatch watch /dir echo data >>/dir/file mv /dir/file /unwatch/rename echo data >>/unwatch/file # Modify the file outside of the watched dir touch /dir/file # Recreate the file that was moved Output: create /dir/file # cat data >/dir/file write /dir/file # ^ rename /dir/file # mv /dir/file /unwatch/rename create /dir/file # touch /dir/file # Windows has REMOVE /file, rather than CREATE /file windows: create /dir/file write /dir/file remove /dir/file create /dir/file fsnotify-1.8.0/testdata/watch-dir/rename-watched-dir000066400000000000000000000003241471065432700224470ustar00rootroot00000000000000# Rename watched directory. mkdir /dir watch /dir mv /dir /dir-rename touch /dir-rename/file Output: rename /dir windows: # TODO(v2): Windows should behave the same by default. See #518 create /dir/file fsnotify-1.8.0/testdata/watch-dir/subdir000066400000000000000000000012601471065432700202770ustar00rootroot00000000000000# watch / mkdir /sub # Create sub-directory touch /file # Create a file touch /sub/file2 # Create a file (Should not see this! we are not watching subdir) sleep 200ms rm -r /sub # Make sure receive deletes for both file and sub-directory rm /file Output: create /sub create /file remove /sub remove /file dragonfly: # TODO: not sure why the REMOVE /sub is dropped. create /sub create /file remove /file fen: create /sub create /file write /sub remove /sub remove /file windows: # Windows includes a write for the /sub dir too, two of them even(?) create /sub create /file write /sub write /sub remove /sub remove /file fsnotify-1.8.0/testdata/watch-dir/symlink-dir000066400000000000000000000002511471065432700212500ustar00rootroot00000000000000# Create a new symlink to a watched file. require symlink mkdir /dir watch / ln -s /dir /link Output: create /link dragonfly: # TODO: can we fix this? no-events fsnotify-1.8.0/testdata/watch-dir/symlink-file000066400000000000000000000002431471065432700214120ustar00rootroot00000000000000# Create a new symlink to a file. require symlink touch /file watch / ln -s /file /link Output: create /link dragonfly: # TODO: can we fix this? no-events fsnotify-1.8.0/testdata/watch-dir/symlink-nofollow000066400000000000000000000005621471065432700223360ustar00rootroot00000000000000# Create a new symlink to a watched file. require symlink require nofollow touch /file mkdir /dir watch / default nofollow ln -s /dir /link-file ln -s /dir /link-dir rm -r /dir echo asd >>/file rm /file rm /link-file rm /link-dir Output: create /link-dir create /link-file remove /dir write /file remove /file remove /link-file remove /link-dir fsnotify-1.8.0/testdata/watch-dir/truncate-file000066400000000000000000000004241471065432700215520ustar00rootroot00000000000000# Truncate file echo data >file watch / echo data >file Output: write /file # truncate write /file # write # Truncate is chmod on kqueue, except dragonfly where it seems a write. dragonfly: write /file write /file kqueue: chmod /file write /file fsnotify-1.8.0/testdata/watch-dir/unreadable-file000066400000000000000000000006431471065432700220320ustar00rootroot00000000000000# Create a watcher in a directory with an unreadable file. skip windows # TODO: figure out how to make a file unreadable touch /unreadable chmod 0 /unreadable touch /file watch / echo hello >>/file rm /file rm /unreadable Output: write /file remove /file remove /unreadable # We never set up a watcher on the unreadable file, so we don't get the # remove. kqueue: write /file remove /file fsnotify-1.8.0/testdata/watch-dir/watch-dir-inside-watched000066400000000000000000000011751471065432700235640ustar00rootroot00000000000000# Add a new watch for a directory we're already watching. # TODO: This consistently works fine on my NetBSD 9.2/Go 1.17 machine, but not # with NetBSD 10.0/Go 1.21 in the CI. I don't know if it's the version or # something else – need to look into that. # # Fails with: # # CREATE "/dir" # CREATE "/one" # WRITE "/one" # REMOVE "/one" skip netbsd watch / mkdir /dir watch /dir echo hello >>/one echo hello >>/dir/two rm /one rm /dir/two Output: create /dir create /one write /one create /dir/two write /dir/two remove /one remove /dir/two fsnotify-1.8.0/testdata/watch-dir/watch-file-inside-watched-dir000066400000000000000000000006521471065432700245000ustar00rootroot00000000000000# Add a new file watch for a directory we're already watching. watch / touch /file watch /file echo hello >>/file rm /file Output: create /file write /file remove /file linux: # TODO: double write. create /file write /file write /file chmod /file remove /file windows: # TODO: double write and remove. create /file write /file write /file remove /file remove /file fsnotify-1.8.0/testdata/watch-dir/watch-twice000066400000000000000000000002571471065432700212330ustar00rootroot00000000000000# Watch the same directory twice. watch / watch / touch /file echo hello >>/file rm /file mkdir /dir Output: create /file write /file remove /file create /dir fsnotify-1.8.0/testdata/watch-file/000077500000000000000000000000001471065432700172265ustar00rootroot00000000000000fsnotify-1.8.0/testdata/watch-file/chmod000066400000000000000000000001601471065432700202400ustar00rootroot00000000000000# chmod the watched file. touch /file watch /file chmod 700 /file Output: chmod /file windows: no-events fsnotify-1.8.0/testdata/watch-file/chmod-after-write000066400000000000000000000002641471065432700224740ustar00rootroot00000000000000# chmod after write. echo data >>/file watch /file chmod 700 /file echo more >>/file chmod 600 /file Output: chmod /file write /file chmod /file windows: write /file fsnotify-1.8.0/testdata/watch-file/op-all000066400000000000000000000005121471065432700203330ustar00rootroot00000000000000require op_all touch /file watch /file default open read close_write close_read cat /file echo data >>/file cat /file Output: open /file # cat /file close_read /file open /file # echo data >>/file write /file close_write /file open /file # cat /file read /file close_read /file fsnotify-1.8.0/testdata/watch-file/op-closeread000066400000000000000000000002221471065432700215220ustar00rootroot00000000000000# Basic close_read test require op_close_read echo "hello, world!" >>/file watch /file default close_read cat /file Output: close_read /file fsnotify-1.8.0/testdata/watch-file/op-closewrite000066400000000000000000000004411471065432700217440ustar00rootroot00000000000000# Basic close_write test require op_close_write touch /file watch /file default close_write echo "hello, world!" >>/file echo "hello, world!" >/file Output: write /file # echo >> close_write /file write /file # echo > write /file close_write /file fsnotify-1.8.0/testdata/watch-file/op-open000066400000000000000000000001721471065432700205260ustar00rootroot00000000000000# Basic open test require op_open echo "hello, world!" >>/file watch /file default open cat /file Output: open /file fsnotify-1.8.0/testdata/watch-file/op-read000066400000000000000000000002001471065432700204700ustar00rootroot00000000000000# Basic close_read test require op_read echo "hello, world!" >>/file watch /file default read cat /file Output: read /file fsnotify-1.8.0/testdata/watch-file/overwrite-watched-file000066400000000000000000000006431471065432700235340ustar00rootroot00000000000000# Overwrite watched file with non-watched file. touch /file touch /unwatch watch /file mv /unwatch /file echo asd >>/file rm /file # TODO: think what the best behaviour is here; do we want to keep the watch or # lose it? It's inconsistent now. Output: remove /file linux: chmod /file remove /file kqueue: remove /file create /file write /file remove /file dragonfly: no-events fsnotify-1.8.0/testdata/watch-file/overwrite-watched-file-with-watched-file000066400000000000000000000012251471065432700270340ustar00rootroot00000000000000# Overwrite watched file with non-watched file. touch /file touch /other watch /file watch /other mv /other /file echo asd >>/file rm /file # TODO: think what the best behaviour is here; do we want to keep the watch or # lose it? It's inconsistent now. Output: # On inotify we just get IN_MOVE_SELF and IN_DELETE_SELF without cookie; no # way to track this. chmod /file rename /other remove /file kqueue: rename /other remove /file create /file write /file remove /file windows: remove /file rename /other write /file remove /file fen: remove /file rename /other dragonfly: rename /other fsnotify-1.8.0/testdata/watch-file/re-add-renamed-filed000066400000000000000000000006131471065432700227770ustar00rootroot00000000000000# Re-add renamed file. touch /file watch /file mv /file /rename touch /file watch /file echo hello >>/rename echo hello >>/file Output: rename /file # mv file rename # Watcher gets removed on rename, so no write for /rename write /file # cat hello >file windows: # TODO(v2): Windows should behave the same by default. See #518 rename /file write /rename write /file fsnotify-1.8.0/testdata/watch-file/remove-watched-file000066400000000000000000000002401471065432700227740ustar00rootroot00000000000000# Remove watched file. touch /file watch /file rm /file Output: remove /file linux: # unlink always emits a chmod on linux. chmod /file remove /file fsnotify-1.8.0/testdata/watch-file/rename-watched-file000066400000000000000000000003561471065432700227560ustar00rootroot00000000000000# Rename watched file. touch /file watch /file mv /file /rename-one mv /rename-one /rename-two Output: rename /file windows: # TODO(v2): Windows should behave the same by default. See #518 rename /file rename /rename-one fsnotify-1.8.0/testdata/watch-file/watch-twice000066400000000000000000000001541471065432700213700ustar00rootroot00000000000000# watch same file twice. touch /file watch /file watch /file echo hello >>/file Output: write /file fsnotify-1.8.0/testdata/watch-recurse/000077500000000000000000000000001471065432700177575ustar00rootroot00000000000000fsnotify-1.8.0/testdata/watch-recurse/add-dir000066400000000000000000000015401471065432700212060ustar00rootroot00000000000000# Add directory to a recursively watched dir. require recurse mkdir -p /sub/dir watch /... mkdir -p /sub/dir/newdir touch /sub/dir/newdir/file rm /sub/dir/newdir/file # Re-create file. touch /sub/dir/newdir/file rm /sub/dir/newdir/file # Write to file. echo foo >/sub/dir/newdir/file Output: create /sub/dir/newdir create /sub/dir/newdir/file remove /sub/dir/newdir/file write /sub/dir/newdir create /sub/dir/newdir/file remove /sub/dir/newdir/file write /sub/dir/newdir create /sub/dir/newdir/file write /sub/dir/newdir/file linux: # Same as Windows, but without those stupid dir writes Windows sends. create /sub/dir/newdir create /sub/dir/newdir/file remove /sub/dir/newdir/file create /sub/dir/newdir/file remove /sub/dir/newdir/file create /sub/dir/newdir/file write /sub/dir/newdir/file fsnotify-1.8.0/testdata/watch-recurse/add-file000066400000000000000000000004541471065432700213520ustar00rootroot00000000000000# Make a nested directory tree, then write some files there. require recurse mkdir -p /sub/dir watch /... echo asd >/file echo asd >/sub/dir/file rm /file rm /sub/dir/file Output: create /file write /file create /sub/dir/file write /sub/dir/file remove /file remove /sub/dir/file fsnotify-1.8.0/testdata/watch-recurse/remove-dir000066400000000000000000000007201471065432700217520ustar00rootroot00000000000000# Remove nested directory require recurse mkdir -p /sub/dir watch /... echo asd >/sub/dir/file rm -r /sub Output: create /sub/dir/file write /sub/dir/file write /sub write /sub/dir remove /sub/dir/file write /sub/dir remove /sub/dir write /sub remove /sub linux: # Same as Windows, but without those stupid dir writes Windows sends. create /sub/dir/file write /sub/dir/file remove /sub/dir/file remove /sub/dir remove /sub fsnotify-1.8.0/testdata/watch-recurse/remove-recursive000066400000000000000000000010471471065432700232060ustar00rootroot00000000000000# Remove recursive. require recurse mkdir -p /dir1/subdir mkdir -p /dir2/subdir touch /dir1/subdir/file touch /dir2/subdir/file watch /dir1/... watch /dir2/... echo asd >>/dir1/subdir/file echo asd >>/dir2/subdir/file unwatch /dir1 unwatch /dir2 watchlist 0 echo asd >>/dir1/subdir/file echo asd >>/dir2/subdir/file Output: write /dir1/subdir write /dir1/subdir/file write /dir2/subdir write /dir2/subdir/file linux: # Same as Windows, but without those stupid dir writes Windows sends. write /dir1/subdir/file write /dir2/subdir/file fsnotify-1.8.0/testdata/watch-recurse/remove-watched-dir000066400000000000000000000021761471065432700233760ustar00rootroot00000000000000# Remove watched directory. require recurse mkdir /watch touch /watch/a touch /watch/b touch /watch/c touch /watch/d touch /watch/e touch /watch/f touch /watch/g mkdir /watch/h mkdir /watch/h/a mkdir /watch/i mkdir /watch/i/a mkdir /watch/j mkdir /watch/j/a watch /watch/... rm -r /watch Output: remove /watch/a remove /watch/b remove /watch/c remove /watch/d remove /watch/e remove /watch/f remove /watch/g write /watch/h remove /watch/h/a write /watch/h remove /watch/h write /watch/i remove /watch/i/a write /watch/i remove /watch/i write /watch/j remove /watch/j/a write /watch/j remove /watch/j remove /watch linux: # Same as Windows, but without those stupid dir writes Windows sends. remove /watch/a remove /watch/b remove /watch/c remove /watch/d remove /watch/e remove /watch/f remove /watch/g remove /watch/h/a remove /watch/h remove /watch/i/a remove /watch/i remove /watch/j/a remove /watch/j remove /watch fsnotify-1.8.0/testdata/watch-recurse/rename-dir000066400000000000000000000013311471065432700217230ustar00rootroot00000000000000# Rename nested directory. require recurse mkdir -p /sub/dir watch /... mv /sub /sub-rename touch /sub-rename/file touch /sub-rename/dir/file Output: rename /sub # mv /sub /sub-rename create /sub-rename ← /sub create /sub-rename/file # touch /sub-rename/file create /sub-rename/dir/file # touch /sub-rename/dir/file # Same as above, but with these stupid dir writes Windows sends. # # TODO: see if we can suppress that. windows: rename /sub # mv /sub /sub-rename create /sub-rename ← /sub write /sub-rename # touch /sub-rename/file create /sub-rename/file create /sub-rename/dir/file # touch /sub-rename/dir/file fsnotify-1.8.0/testdata/watch-symlink/000077500000000000000000000000001471065432700177755ustar00rootroot00000000000000fsnotify-1.8.0/testdata/watch-symlink/nofollow-dir000066400000000000000000000003111471065432700223260ustar00rootroot00000000000000# Watch a symlink. require symlink require nofollow mkdir /dir ln -s /dir /link watch /link default nofollow touch /dir/file chmod 777 /dir rm -r /dir rm /link Output: chmod /link remove /link fsnotify-1.8.0/testdata/watch-symlink/nofollow-file000066400000000000000000000003311471065432700224710ustar00rootroot00000000000000# Watch a symlink. require symlink require nofollow touch /file ln -s /file /link watch /link nofollow default chmod 777 /file echo asd >>/file rm /file rm /link touch /link Output: chmod /link remove /link fsnotify-1.8.0/testdata/watch-symlink/to-dir000066400000000000000000000002041471065432700211120ustar00rootroot00000000000000# Watch a symlink to a dir require symlink mkdir /dir ln -s /dir /link watch /link touch /dir/file Output: create /link/file fsnotify-1.8.0/testdata/watch-symlink/to-dir-relative000066400000000000000000000002531471065432700227270ustar00rootroot00000000000000# Watch a symlink to a dir require symlink mkdir /dir ln -s ./dir /link watch /link touch /dir/file Output: create /link/file kqueue: # TODO: broken no-events fsnotify-1.8.0/testdata/watch-symlink/to-file000066400000000000000000000002611471065432700212560ustar00rootroot00000000000000# Watch a symlink to a file. require symlink touch /file ln -s /file /link watch /link echo hello >>/file Output: write /link windows: # TODO: investigate. no-events fsnotify-1.8.0/testdata/watch-symlink/to-file-relative000066400000000000000000000003301471065432700230640ustar00rootroot00000000000000# Watch a symlink to a file. require symlink touch /file ln -s ./file /link watch /link echo hello >>/file Output: write /link kqueue: # TODO: broken no-events windows: # TODO: investigate. no-events