pax_global_header 0000666 0000000 0000000 00000000064 15063112431 0014506 g ustar 00root root 0000000 0000000 52 comment=1ffa131bc064059b451a9990f68036751496ab5a
golang-fsnotify-1.9.0/ 0000775 0000000 0000000 00000000000 15063112431 0014623 5 ustar 00root root 0000000 0000000 golang-fsnotify-1.9.0/.cirrus.yml 0000664 0000000 0000000 00000001121 15063112431 0016726 0 ustar 00root root 0000000 0000000 freebsd_task:
name: 'FreeBSD'
freebsd_instance:
image_family: freebsd-14-2
install_script:
- pkg update -f
- pkg install -y go
test_script:
# run tests as user "cirrus" instead of root
- pw useradd cirrus -m
- chown -R cirrus:cirrus .
- FSNOTIFY_BUFFER=4096 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./...
- sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./...
- FSNOTIFY_DEBUG=1 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race -v ./...
golang-fsnotify-1.9.0/.github/ 0000775 0000000 0000000 00000000000 15063112431 0016163 5 ustar 00root root 0000000 0000000 golang-fsnotify-1.9.0/.github/FUNDING.yml 0000664 0000000 0000000 00000000017 15063112431 0017776 0 ustar 00root root 0000000 0000000 github: arp242
golang-fsnotify-1.9.0/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15063112431 0020346 5 ustar 00root root 0000000 0000000 golang-fsnotify-1.9.0/.github/ISSUE_TEMPLATE/bug.yml 0000664 0000000 0000000 00000003000 15063112431 0021637 0 ustar 00root root 0000000 0000000 ---
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']
golang-fsnotify-1.9.0/.github/ISSUE_TEMPLATE/other.md 0000664 0000000 0000000 00000000210 15063112431 0022002 0 ustar 00root root 0000000 0000000 ---
name: 'Other'
about: "Anything that's not a bug such as feature requests, questions, etc."
title: ''
labels: ''
assignees: ''
---
golang-fsnotify-1.9.0/.github/workflows/ 0000775 0000000 0000000 00000000000 15063112431 0020220 5 ustar 00root root 0000000 0000000 golang-fsnotify-1.9.0/.github/workflows/staticcheck.yml 0000664 0000000 0000000 00000004213 15063112431 0023230 0 ustar 00root root 0000000 0000000 name: 'staticcheck'
on:
pull_request:
paths: ['**.go', 'go.mod', '.github/workflows/*']
push:
branches: ['main']
jobs:
staticcheck:
name: 'staticcheck'
runs-on: 'ubuntu-latest'
env: {cache: 'staticcheck-${{ github.ref }}'}
steps:
# Setup
- uses: 'actions/checkout@v4'
- id: 'cache-restore'
uses: 'actions/cache/restore@v4'
with:
key: '${{ env.cache }}'
path: |
${{ runner.temp }}/staticcheck
/home/runner/.cache/go-build
restore-keys: |
staticcheck-${{ github.ref }}
staticcheck-refs/heads/main
- uses: 'actions/setup-go@v5'
with: {go-version: '1.24'}
- uses: 'actions/cache@v4'
with:
key: '${{ runner.os }}-staticcheck'
path: |
${{ runner.temp }}/staticcheck
${{ steps.install_go.outputs.GOCACHE || '' }}
# Run
- run: |
export STATICCHECK_CACHE="${{ runner.temp }}/staticcheck"
go install honnef.co/go/tools/cmd/staticcheck@latest
fail=0
for a in $(go tool dist list); do
export GOOS=${a%%/*}
export GOARCH=${a#*/}
case "$GOOS" in
(android|ios) continue ;; # Requires cgo to link.
(js|wasip1) continue ;; # No build tags in internal/ TODO: should maybe fix?
(plan9) continue ;; # Errors out on some missing definitions like syscall.Errno.
esac
echo $a
go vet ./... || fail=1
staticcheck ./... || fail=1
done
exit $fail
# Store cache
- name: 'delete existing cache'
if: '${{ steps.cache-restore.outputs.cache-hit }}'
env: {GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'}
continue-on-error: true
run: |
gh extension install actions/gh-actions-cache
gh actions-cache delete "${{ env.cache }}" --confirm
- uses: 'actions/cache/save@v4'
with:
key: '${{ env.cache }}'
path: |
${{ runner.temp }}/staticcheck
/home/runner/.cache/go-build
golang-fsnotify-1.9.0/.github/workflows/test.yml 0000664 0000000 0000000 00000014140 15063112431 0021722 0 ustar 00root root 0000000 0000000 name: 'test'
on:
pull_request:
paths: ['**.go', 'go.mod', '.github/workflows/*']
push:
branches: ['main', 'aix']
jobs:
linux:
strategy:
fail-fast: false
matrix:
os: ['ubuntu-20.04', 'ubuntu-latest', 'ubuntu-24.04-arm']
go: ['1.17', '1.24']
runs-on: '${{ matrix.os }}'
timeout-minutes: 10
steps:
- uses: 'actions/checkout@v4'
- uses: 'actions/setup-go@v5'
with: {go-version: '${{ matrix.go }}'}
- name: test
run: |
FSNOTIFY_BUFFER=4096 go test -parallel 1 -race ./...
go test -parallel 1 -race ./...
FSNOTIFY_DEBUG=1 go test -parallel 1 -race -v ./...
windows:
strategy:
fail-fast: false
matrix:
os: ['windows-latest']
go: ['1.17', '1.24']
runs-on: '${{ matrix.os }}'
timeout-minutes: 10
steps:
- uses: 'actions/checkout@v4'
- uses: 'actions/setup-go@v5'
with: {go-version: '${{ matrix.go }}'}
- name: test
run: |
go test -parallel 1 -race ./...
$Env:FSNOTIFY_BUFFER = 4096
go test -parallel 1 -race ./...
$Env:FSNOTIFY_DEBUG = 1
go test -parallel 1 -race -v ./...
# Test gccgo
gcc:
runs-on: 'ubuntu-24.04'
name: 'test (ubuntu-24.04, gccgo 13.2)'
timeout-minutes: 10
steps:
- uses: 'actions/checkout@v4'
- name: test
run: |
sudo apt-get -y install gccgo-13
FSNOTIFY_BUFFER=4096 go-13 test -parallel 1 ./...
go-13 test -parallel 1 ./...
FSNOTIFY_DEBUG=1 go-13 test -parallel 1 -v ./...
macos:
name: 'test'
strategy:
fail-fast: false
matrix:
os: ['macos-13', 'macos-latest']
go: ['1.17', '1.24']
runs-on: '${{ matrix.os }}'
timeout-minutes: 10
steps:
- uses: 'actions/checkout@v4'
- uses: 'actions/setup-go@v5'
with: {go-version: '${{ matrix.go }}'}
- name: test
run: |
FSNOTIFY_BUFFER=4096 go test -parallel 1 -race ./...
go test -parallel 1 -race ./...
FSNOTIFY_DEBUG=1 go test -parallel 1 -race -v ./...
# OpenBSD; no -race as the VM doesn't include the comp set.
#
# TODO: should probably add this, but on my local machine the tests time out
# with -race as the waits aren't long enough (OpenBSD is kind of slow),
# so should probably look into that first. Go 1.19 is supposed to have a
# much faster race detector, so maybe waiting until we have that is
# enough.
openbsd:
runs-on: 'ubuntu-latest'
name: 'test (openbsd, 1.17)'
timeout-minutes: 10
steps:
- uses: 'actions/checkout@v4'
- name: 'test (openbsd, 1.17)'
id: 'openbsd'
uses: 'vmactions/openbsd-vm@v1'
with:
prepare: pkg_add go
run: |
useradd -mG wheel action
FSNOTIFY_BUFFER=4096 su action -c 'go test -parallel 1 ./...'
su action -c 'go test -parallel 1 ./...'
FSNOTIFY_DEBUG=1 su action -c 'go test -parallel 1 -v ./...'
# NetBSD
netbsd:
runs-on: 'ubuntu-latest'
name: 'test (netbsd, 1.21)'
timeout-minutes: 10
steps:
- uses: 'actions/checkout@v4'
- name: 'test (netbsd, 1.21)'
id: 'netbsd'
uses: 'vmactions/netbsd-vm@v1'
with:
prepare: pkg_add go
# TODO: no -race for the same reason as OpenBSD (the timing; it does run).
run: |
useradd -mG wheel action
FSNOTIFY_BUFFER=4096 su action -c '/usr/pkg/bin/go??? test -parallel 1 ./...'
su action -c '/usr/pkg/bin/go??? test -parallel 1 ./...'
FSNOTIFY_DEBUG=1 su action -c '/usr/pkg/bin/go??? test -parallel 1 -v ./...'
# DragonFlyBSD
dragonflybsd:
runs-on: 'ubuntu-latest'
name: 'test (dragonflybsd, 1.20)'
timeout-minutes: 10
steps:
- uses: 'actions/checkout@v4'
- name: 'test (dragonflybsd, 1.20)'
id: 'dragonflybsd'
uses: 'vmactions/dragonflybsd-vm@v1'
with:
prepare: pkg install -y go
# TODO: no -race for the same reason as OpenBSD (the timing; it does run).
run: |
pw user add -mG wheel -n action
env FSNOTIFY_BUFFER=4096 su action -c 'go test -parallel 1 ./...'
su action -c 'go test -parallel 1 ./...'
env FSNOTIFY_DEBUG=1 su action -c 'go test -parallel 1 -v ./...'
# illumos
illumos:
runs-on: 'ubuntu-latest'
name: 'test (illumos, 1.22)'
timeout-minutes: 10
steps:
- uses: 'actions/checkout@v4'
- name: 'test (illumos, 1.22)'
id: 'illumos'
uses: 'vmactions/omnios-vm@v1'
with:
prepare: pkg install go-122
run: |
useradd action
export GOCACHE=/tmp/go-cache
export GOPATH=/tmp/go-path
FSNOTIFY_BUFFER=4096 su action -c '/opt/ooce/go-1.22/bin/go test -parallel 1 ./...'
su action -c '/opt/ooce/go-1.22/bin/go test -parallel 1 ./...'
FSNOTIFY_DEBUG=1 su action -c '/opt/ooce/go-1.22/bin/go test -parallel 1 -v ./...'
# Solaris
# TODO: latest version is go 1.7(!) Need to find a good way to install a more
# recent version; the go.dev doesn't have binaries for Solaris.
# solaris:
# runs-on: 'ubuntu-latest'
# name: 'test (solaris, 1.17)'
# timeout-minutes: 10
# steps:
# - uses: 'actions/checkout@v4'
# - name: 'test (solaris, 1.17)'
# id: 'solaris'
# uses: 'vmactions/solaris-vm@v1'
# with:
# prepare: pkg install pkg://solaris/developer/golang-17
# run: |
# useradd action
# export GOCACHE=/tmp/go-cache
# export GOPATH=/tmp/go-path
# FSNOTIFY_BUFFER=4096 su action -c 'go test -parallel 1 ./...'
# su action -c 'go test -parallel 1 ./...'
# FSNOTIFY_DEBUG=1 su action -c 'go test -parallel 1 -v ./...'
golang-fsnotify-1.9.0/.gitignore 0000664 0000000 0000000 00000000175 15063112431 0016616 0 ustar 00root root 0000000 0000000 # go test -c output
*.test
*.test.exe
# Output of go build ./cmd/fsnotify
/fsnotify
/fsnotify.exe
/test/kqueue
/test/a.out
golang-fsnotify-1.9.0/.mailmap 0000664 0000000 0000000 00000000172 15063112431 0016244 0 ustar 00root root 0000000 0000000 Chris Howey
Nathan Youngman <4566+nathany@users.noreply.github.com>
golang-fsnotify-1.9.0/CHANGELOG.md 0000664 0000000 0000000 00000055576 15063112431 0016456 0 ustar 00root root 0000000 0000000 # Changelog
1.9.0 2024-04-04
----------------
### Changes and fixes
- all: make BufferedWatcher buffered again ([#657])
- inotify: fix race when adding/removing watches while a watched path is being
deleted ([#678], [#686])
- inotify: don't send empty event if a watched path is unmounted ([#655])
- inotify: don't register duplicate watches when watching both a symlink and its
target; previously that would get "half-added" and removing the second would
panic ([#679])
- kqueue: fix watching relative symlinks ([#681])
- kqueue: correctly mark pre-existing entries when watching a link to a dir on
kqueue ([#682])
- illumos: don't send error if changed file is deleted while processing the
event ([#678])
[#657]: https://github.com/fsnotify/fsnotify/pull/657
[#678]: https://github.com/fsnotify/fsnotify/pull/678
[#686]: https://github.com/fsnotify/fsnotify/pull/686
[#655]: https://github.com/fsnotify/fsnotify/pull/655
[#681]: https://github.com/fsnotify/fsnotify/pull/681
[#679]: https://github.com/fsnotify/fsnotify/pull/679
[#682]: https://github.com/fsnotify/fsnotify/pull/682
1.8.0 2024-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
golang-fsnotify-1.9.0/CONTRIBUTING.md 0000664 0000000 0000000 00000010501 15063112431 0017051 0 ustar 00root root 0000000 0000000 Thank you for your interest in contributing to fsnotify! We try to review and
merge PRs in a reasonable timeframe, but please be aware that:
- To avoid "wasted" work, please 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).
print [any strings] # Print text to stdout; for debugging.
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
golang-fsnotify-1.9.0/LICENSE 0000664 0000000 0000000 00000002773 15063112431 0015641 0 ustar 00root root 0000000 0000000 Copyright © 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.
golang-fsnotify-1.9.0/README.md 0000664 0000000 0000000 00000015264 15063112431 0016112 0 ustar 00root root 0000000 0000000 fsnotify 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) |
| 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
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.
golang-fsnotify-1.9.0/backend_fen.go 0000664 0000000 0000000 00000026350 15063112431 0017377 0 ustar 00root root 0000000 0000000 //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"
"io/fs"
"os"
"path/filepath"
"sync"
"time"
"github.com/fsnotify/fsnotify/internal"
"golang.org/x/sys/unix"
)
type fen struct {
*shared
Events chan Event
Errors chan error
mu sync.Mutex
port *unix.EventPort
dirs map[string]Op // Explicitly watched directories
watches map[string]Op // Explicitly watched non-directories
}
var defaultBufferSize = 0
func newBackend(ev chan Event, errs chan error) (backend, error) {
w := &fen{
shared: newShared(ev, errs),
Events: ev,
Errors: errs,
dirs: make(map[string]Op),
watches: make(map[string]Op),
}
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
}
func (w *fen) Close() error {
if w.shared.close() {
return nil
}
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(fmt.Errorf("port.Get: %w", 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(Event{Name: path, Op: Remove}) {
return nil
}
reRegister = false
}
if events&unix.FILE_RENAME_FROM != 0 {
if !w.sendEvent(Event{Name: path, Op: 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(Event{Name: path, Op: 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(Event{Name: path, Op: 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(Event{Name: path, Op: 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(Event{Name: path, Op: 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(Event{Name: path, Op: 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
err := w.associateFile(path, stat, isWatched)
if errors.Is(err, fs.ErrNotExist) {
// Path may have been removed since the stat.
err = nil
}
return err
}
return nil
}
// 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.
func (w *fen) updateDirectory(path string) error {
files, err := os.ReadDir(path)
if err != nil {
// Directory no longer exists: probably just deleted since we got the
// event.
if errors.Is(err, fs.ErrNotExist) {
return 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 errors.Is(err, fs.ErrNotExist) {
// File may have disappeared between getting the dir listing and
// adding the port: that's okay to ignore.
continue
}
if !w.sendError(err) {
return nil
}
if !w.sendEvent(Event{Name: path, Op: 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 fmt.Errorf("port.DissociatePath(%q): %w", path, 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
}
err := w.port.AssociatePath(path, stat, events, stat.Mode())
if err != nil {
return fmt.Errorf("port.AssociatePath(%q): %w", path, err)
}
return nil
}
func (w *fen) dissociateFile(path string, stat os.FileInfo, unused bool) error {
if !w.port.PathIsWatched(path) {
return nil
}
err := w.port.DissociatePath(path)
if err != nil {
return fmt.Errorf("port.DissociatePath(%q): %w", path, err)
}
return nil
}
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
}
golang-fsnotify-1.9.0/backend_fen_test.go 0000664 0000000 0000000 00000002330 15063112431 0020426 0 ustar 00root root 0000000 0000000 //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)
}
golang-fsnotify-1.9.0/backend_inotify.go 0000664 0000000 0000000 00000035332 15063112431 0020310 0 ustar 00root root 0000000 0000000 //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 {
*shared
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
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 {
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) byPath(path string) *watch { return w.wd[w.path[path]] }
func (w *watches) byWd(wd uint32) *watch { return w.wd[wd] }
func (w *watches) len() int { return len(w.wd) }
func (w *watches) add(ww *watch) { w.wd[ww.wd] = ww; w.path[ww.path] = ww.wd }
func (w *watches) remove(watch *watch) { delete(w.path, watch.path); delete(w.wd, watch.wd) }
func (w *watches) removePath(path string) ([]uint32, error) {
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 strings.HasPrefix(p, path) {
delete(w.path, p)
delete(w.wd, rwd)
wds = append(wds, rwd)
}
}
return wds, nil
}
func (w *watches) updatePath(path string, f func(*watch) (*watch, error)) error {
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
}
var defaultBufferSize = 0
func newBackend(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{
shared: newShared(ev, errs),
Events: ev,
Errors: errs,
fd: fd,
inotifyFile: os.NewFile(uintptr(fd), ""),
watches: newWatches(),
doneResp: make(chan struct{}),
}
go w.readEvents()
return w, nil
}
func (w *inotify) Close() error {
if w.shared.close() {
return nil
}
// 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
}
<-w.doneResp // Wait for readEvents() to finish.
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)
}
add := func(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)
}
w.mu.Lock()
defer w.mu.Unlock()
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 add(root, with, true)
})
}
return add(path, with, false)
}
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 e, ok := w.watches.wd[uint32(wd)]; ok {
return e, nil
}
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)
}
w.mu.Lock()
defer w.mu.Unlock()
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
}
w.mu.Lock()
defer w.mu.Unlock()
entries := make([]string, 0, w.watches.len())
for pathname := range w.watches.path {
entries = append(entries, pathname)
}
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
for {
if w.isClosed() {
return
}
n, err := w.inotifyFile.Read(buf[:])
if err != nil {
if errors.Is(err, os.ErrClosed) {
return
}
if !w.sendError(err) {
return
}
continue
}
if n < unix.SizeofInotifyEvent {
err := errors.New("notify: short read in readEvents()") // Read was too short.
if n == 0 {
err = io.EOF // If EOF is received. This should really never happen.
}
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) {
// Point to the event in the buffer.
inEvent := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset]))
if inEvent.Mask&unix.IN_Q_OVERFLOW != 0 {
if !w.sendError(ErrEventOverflow) {
return
}
}
ev, ok := w.handleEvent(inEvent, &buf, offset)
if !ok {
return
}
if !w.sendEvent(ev) {
return
}
// Move to the next event in the buffer
offset += unix.SizeofInotifyEvent + inEvent.Len
}
}
}
func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offset uint32) (Event, bool) {
w.mu.Lock()
defer w.mu.Unlock()
/// 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.
///
/// 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.
watch := w.watches.byWd(uint32(inEvent.Wd))
if watch == nil {
return Event{}, true
}
var (
name = watch.path
nameLen = uint32(inEvent.Len)
)
if nameLen > 0 {
/// Point "bytes" at the first byte of the filename
bb := *buf
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&bb[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen]
/// The filename is padded with NULL bytes. TrimRight() gets rid of those.
name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\x00")
}
if debug {
internal.Debug(name, inEvent.Mask, inEvent.Cookie)
}
if inEvent.Mask&unix.IN_IGNORED != 0 || inEvent.Mask&unix.IN_UNMOUNT != 0 {
w.watches.remove(watch)
return Event{}, true
}
// inotify will automatically remove the watch on deletes; just need
// to clean our state here.
if inEvent.Mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
w.watches.remove(watch)
}
// 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 inEvent.Mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF {
if watch.recurse { // Do nothing
return Event{}, true
}
err := w.remove(watch.path)
if err != nil && !errors.Is(err, ErrNonExistentWatch) {
if !w.sendError(err) {
return Event{}, false
}
}
}
/// 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 inEvent.Mask&unix.IN_DELETE_SELF != 0 {
_, ok := w.watches.path[filepath.Dir(watch.path)]
if ok {
return Event{}, true
}
}
ev := w.newEvent(name, inEvent.Mask, inEvent.Cookie)
// Need to update watch path for recurse.
if watch.recurse {
isDir := inEvent.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 Event{}, false
}
// 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 != "" {
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
}
}
}
}
}
return ev, true
}
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.mu.Lock()
defer w.mu.Unlock()
for wd, ww := range w.watches.wd {
fmt.Fprintf(os.Stderr, "%4d: recurse=%t %q\n", wd, ww.recurse, ww.path)
}
}
golang-fsnotify-1.9.0/backend_inotify_test.go 0000664 0000000 0000000 00000005125 15063112431 0021344 0 ustar 00root root 0000000 0000000 //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`))
}
golang-fsnotify-1.9.0/backend_kqueue.go 0000664 0000000 0000000 00000042405 15063112431 0020125 0 ustar 00root root 0000000 0000000 //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 {
*shared
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
}
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) bool {
w.mu.Lock()
defer w.mu.Unlock()
fd, ok := w.path[path]
if !ok { // Already deleted: don't re-set it here.
return false
}
info := w.wd[fd]
info.dirFlags = flags
w.wd[fd] = info
return true
}
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
}
var defaultBufferSize = 0
func newBackend(ev chan Event, errs chan error) (backend, error) {
kq, closepipe, err := newKqueue()
if err != nil {
return nil, err
}
w := &kqueue{
shared: newShared(ev, errs),
Events: ev,
Errors: errs,
kq: kq,
closepipe: closepipe,
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 err != nil {
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
}
func (w *kqueue) Close() error {
if w.shared.close() {
return nil
}
pathsToRemove := w.watches.listPaths(false)
for _, name := range pathsToRemove {
w.Remove(name)
}
unix.Close(w.closepipe[1]) // Send "quit" message to readEvents
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, false)
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, listDir bool) (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, but only for paths added with Add(), and not paths
// we're adding from internalWatch from a listdir.
if !listDir && fi.Mode()&os.ModeSymlink == os.ModeSymlink {
link, err := os.Readlink(name)
if err != nil {
return "", err
}
if !filepath.IsAbs(link) {
link = filepath.Join(filepath.Dir(name), link)
}
_, 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 "", err
}
}
// 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)
if !w.watches.updateDirFlags(name, flags) {
return "", nil
}
if watchDir {
d := name
if info.linkName != "" {
d = info.linkName
}
if err := w.watchDirectoryFiles(d); 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 %q: %w", dir, err)
}
for _, f := range files {
fi, err := f.Info()
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return 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) || errors.Is(err, os.ErrNotExist) {
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, true)
}
// Watch file to mimic Linux inotify.
return w.addWatch(name, noteAllEvents, true)
}
// 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
}
golang-fsnotify-1.9.0/backend_kqueue_test.go 0000664 0000000 0000000 00000004523 15063112431 0021163 0 ustar 00root root 0000000 0000000 //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
}
}
}
golang-fsnotify-1.9.0/backend_other.go 0000664 0000000 0000000 00000001476 15063112431 0017752 0 ustar 00root root 0000000 0000000 //go:build appengine || (!darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows)
package fsnotify
import "errors"
type other struct {
Events chan Event
Errors chan error
}
var defaultBufferSize = 0
func newBackend(ev chan Event, errs chan error) (backend, error) {
return nil, errors.New("fsnotify not supported on the current platform")
}
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 }
golang-fsnotify-1.9.0/backend_windows.go 0000664 0000000 0000000 00000040547 15063112431 0020325 0 ustar 00root root 0000000 0000000 //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
done 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
}
var defaultBufferSize = 50
func newBackend(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),
done: 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.done:
w.done <- 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.done:
return false
case w.Errors <- err:
return true
}
}
func (w *readDirChangesW) Close() error {
if w.isClosed() {
return nil
}
w.mu.Lock()
w.closed = true
w.mu.Unlock()
// Send "done" message to the reader goroutine
ch := make(chan error)
w.done <- 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.done:
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
}
golang-fsnotify-1.9.0/backend_windows_test.go 0000664 0000000 0000000 00000002574 15063112431 0021362 0 ustar 00root root 0000000 0000000 //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.
t.Skip("broken test")
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")
}
}
golang-fsnotify-1.9.0/cmd/ 0000775 0000000 0000000 00000000000 15063112431 0015366 5 ustar 00root root 0000000 0000000 golang-fsnotify-1.9.0/cmd/fsnotify/ 0000775 0000000 0000000 00000000000 15063112431 0017227 5 ustar 00root root 0000000 0000000 golang-fsnotify-1.9.0/cmd/fsnotify/closewrite.go 0000664 0000000 0000000 00000002767 15063112431 0021752 0 ustar 00root root 0000000 0000000 package 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)
}
}
}
*/
golang-fsnotify-1.9.0/cmd/fsnotify/dedup.go 0000664 0000000 0000000 00000004315 15063112431 0020662 0 ustar 00root root 0000000 0000000 package 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)
}
}
}
golang-fsnotify-1.9.0/cmd/fsnotify/file.go 0000664 0000000 0000000 00000003441 15063112431 0020477 0 ustar 00root root 0000000 0000000 package 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)
}
}
}
golang-fsnotify-1.9.0/cmd/fsnotify/main.go 0000664 0000000 0000000 00000002712 15063112431 0020504 0 ustar 00root root 0000000 0000000 // 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...)
}
}
golang-fsnotify-1.9.0/cmd/fsnotify/watch.go 0000664 0000000 0000000 00000002212 15063112431 0020661 0 ustar 00root root 0000000 0000000 package 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)
}
}
}
golang-fsnotify-1.9.0/fsnotify.go 0000664 0000000 0000000 00000042041 15063112431 0017014 0 ustar 00root root 0000000 0000000 // 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.
//lint:ignore ST1012 not relevant
xErrUnsupported = errors.New("fsnotify: not supported with this backend")
)
// NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) {
ev, errs := make(chan Event, defaultBufferSize), 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, sz), make(chan error)
b, err := newBackend(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).
//
// The order is undefined, and may differ per call. 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
}
golang-fsnotify-1.9.0/fsnotify_test.go 0000664 0000000 0000000 00000061307 15063112431 0020061 0 ustar 00root root 0000000 0000000 package 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) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("chmod doesn't work on Windows") // TODO: see if we can make a file unreadable
}
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.ErrUnixEACCES) {
t.Errorf("not unix.EACCESS: %T %#[1]v", err)
}
if !errors.Is(err, internal.ErrSyscallEACCES) {
t.Errorf("not syscall.EACCESS: %T %#[1]v", err)
}
})
// The second Add() should be a no-op
t.Run("add same dir 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
`))
})
t.Run("add same dir twice through symlink", func(t *testing.T) {
t.Parallel()
if isSolaris() {
t.Skip("broken: links are resolved and added twice") // TODO: should fix
}
if !internal.HasPrivilegesForSymlink() {
t.Skip("admin permissions required on Windows")
}
tmp := t.TempDir()
mkdir(t, tmp, "dir")
symlink(t, join(tmp, "dir"), tmp, "link")
w := newCollector(t)
if err := w.w.Add(join(tmp, "dir")); err != nil {
t.Fatal(err)
}
if err := w.w.Add(join(tmp, "link")); err != nil {
t.Fatal(err)
}
w.collect(t)
touch(t, tmp, "dir/file")
rm(t, tmp, "dir/file")
cmpEvents(t, tmp, w.events(t), newEvents(t, `
create /dir/file
remove /dir/file
`))
})
t.Run("add same file twice", func(t *testing.T) {
t.Parallel()
tmp := t.TempDir()
touch(t, tmp, "file")
w := newCollector(t)
if err := w.w.Add(join(tmp, "file")); err != nil {
t.Fatal(err)
}
if err := w.w.Add(join(tmp, "file")); err != nil {
t.Fatal(err)
}
w.collect(t)
echoAppend(t, "aaa", tmp, "file")
rm(t, tmp, "file")
cmpEvents(t, tmp, w.events(t), newEvents(t, `
write /file
remove /file
linux:
write /file
chmod /file
remove /file
`))
})
t.Run("add same file twice through symlink", func(t *testing.T) {
t.Parallel()
if isSolaris() {
t.Skip("broken: links are resolved and added twice") // TODO: should fix
}
if !internal.HasPrivilegesForSymlink() {
t.Skip("admin permissions required on Windows")
}
tmp := t.TempDir()
touch(t, tmp, "file")
symlink(t, join(tmp, "file"), tmp, "link")
w := newCollector(t)
if err := w.w.Add(join(tmp, "file")); err != nil {
t.Fatal(err)
}
if err := w.w.Add(join(tmp, "link")); err != nil {
t.Fatal(err)
}
w.collect(t)
echoAppend(t, "aaa", tmp, "file")
rm(t, tmp, "file")
cmpEvents(t, tmp, w.events(t), newEvents(t, `
write /file
remove /file
linux:
write /file
chmod /file
remove /file
`))
})
t.Run("not reading events", func(t *testing.T) {
t.Parallel()
w := newWatcher(t)
defer w.Close()
tmp := t.TempDir()
mkdir(t, tmp, "/dir1")
mkdir(t, tmp, "/dir2")
addWatch(t, w, tmp, "/dir1")
addWatch(t, w, tmp, "/dir2")
{
have, want := w.WatchList(), []string{join(tmp, "/dir1"), join(tmp, "/dir2")}
sort.Strings(have)
if !reflect.DeepEqual(have, want) {
t.Errorf("\nhave: %s\nwant: %s", have, want)
}
}
if err := w.Remove(join(tmp, "/dir1")); err != nil {
t.Fatal(err)
}
{
have, want := w.WatchList(), []string{join(tmp, "/dir2")}
sort.Strings(have)
if !reflect.DeepEqual(have, want) {
t.Errorf("\nhave: %s\nwant: %s", have, want)
}
}
})
}
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)
}
})
t.Run("remove same dir twice through symlink", func(t *testing.T) {
t.Parallel()
if isSolaris() {
t.Skip("broken: links are resolved and added twice") // TODO: should fix
}
if !internal.HasPrivilegesForSymlink() {
t.Skip("admin permissions required on Windows")
}
tmp := t.TempDir()
mkdir(t, tmp, "dir")
symlink(t, join(tmp, "dir"), tmp, "link")
w := newWatcher(t)
defer w.Close()
addWatch(t, w, tmp, "dir")
addWatch(t, w, tmp, "link")
if err := w.Remove(join(tmp, "dir")); err != nil {
t.Fatal(err)
}
err := w.Remove(join(tmp, "link"))
if err == nil {
t.Fatal("no error")
}
if !errors.Is(err, ErrNonExistentWatch) {
t.Fatalf("wrong error: %T", err)
}
})
t.Run("remove same file twice", func(t *testing.T) {
t.Parallel()
tmp := t.TempDir()
touch(t, tmp, "file")
w := newWatcher(t)
defer w.Close()
addWatch(t, w, tmp, "file")
if err := w.Remove(join(tmp, "file")); err != nil {
t.Fatal(err)
}
err := w.Remove(join(tmp, "file"))
if err == nil {
t.Fatal("no error")
}
if !errors.Is(err, ErrNonExistentWatch) {
t.Fatalf("wrong error: %T", err)
}
})
t.Run("remove same file twice through symlink", func(t *testing.T) {
t.Parallel()
if isSolaris() {
t.Skip("broken: links are resolved and added twice") // TODO: should fix
}
if !internal.HasPrivilegesForSymlink() {
t.Skip("admin permissions required on Windows")
}
if runtime.GOOS == "windows" {
// TODO: I'm not sure if this is due to a problem in our code, or
// just a limitation of Windows, but very few people are using links
// in Windows and even fewer links to files, so whatever.
t.Skip("fails on Windows")
}
tmp := t.TempDir()
touch(t, tmp, "file")
symlink(t, join(tmp, "file"), tmp, "link")
w := newWatcher(t)
defer w.Close()
addWatch(t, w, tmp, "file")
addWatch(t, w, tmp, "link")
if err := w.Remove(join(tmp, "file")); err != nil {
t.Fatal(err)
}
err := w.Remove(join(tmp, "link"))
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("release file handles", 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)
})
}
func TestRace(t *testing.T) {
// Would panic on inotify: https://github.com/fsnotify/fsnotify/issues/616
t.Run("add and remove watches", func(t *testing.T) {
t.Parallel()
tmp := t.TempDir()
w := newCollector(t, tmp)
w.collect(t)
var (
dir = join(tmp, "/dir")
wg sync.WaitGroup
)
wg.Add(400)
for i := 0; i < 100; i++ {
go func() { defer wg.Done(); os.MkdirAll(dir, 0o0755) }()
go func() { defer wg.Done(); os.RemoveAll(dir) }()
go func() { defer wg.Done(); w.w.Add(dir) }()
go func() { defer wg.Done(); w.w.Remove(dir) }()
}
wg.Wait()
w.stop(t)
})
// Race when deleting watched directory, creating it again, and re-adding
// it.
t.Run("remove self", func(t *testing.T) {
t.Parallel()
// TODO: seems to hang forever on Windows; possibly related to:
// https://github.com/fsnotify/fsnotify/issues/656
//
// Although it seems to be on different points:
//
// goroutine 8 [chan receive]:
// github.com/fsnotify/fsnotify.(*readDirChangesW).AddWith(0xc00002ea40, {0xc0000940f0, 0x48}, {0x0, 0x0, 0xaf2e7f?})
// C:/Users/martin/fsnotify/backend_windows.go:141 +0x38d
// github.com/fsnotify/fsnotify.(*readDirChangesW).Add(0xc000092000?, {0xc0000940f0?, 0x1?})
// C:/Users/martin/fsnotify/backend_windows.go:111 +0x1f
// github.com/fsnotify/fsnotify.(*Watcher).Add(...)
// C:/Users/martin/fsnotify/fsnotify.go:313
// github.com/fsnotify/fsnotify.TestRace.func2(0xc000003a40)
// C:/Users/martin/fsnotify/fsnotify_test.go:865 +0x1ee
// testing.tRunner(0xc000003a40, 0xb71930)
// C:/Program Files/Go/src/testing/testing.go:1792 +0xcb
// created by testing.(*T).Run in goroutine 7
// C:/Program Files/Go/src/testing/testing.go:1851 +0x3f6
//
// goroutine 9 [select, locked to thread]:
// github.com/fsnotify/fsnotify.(*readDirChangesW).sendError(...)
// C:/Users/martin/fsnotify/backend_windows.go:85
// github.com/fsnotify/fsnotify.(*readDirChangesW).startRead(0xc00002ea40, 0xc0001a4100)
// C:/Users/martin/fsnotify/backend_windows.go:453 +0x319
// github.com/fsnotify/fsnotify.(*readDirChangesW).readEvents(0xc00002ea40)
// C:/Users/martin/fsnotify/backend_windows.go:548 +0x290
// created by github.com/fsnotify/fsnotify.newBufferedBackend in goroutine 8
// C:/Users/martin/fsnotify/backend_windows.go:55 +0x198
//
// goroutine 18 [chan send]:
// github.com/fsnotify/fsnotify.(*eventCollector).collect.func1()
// C:/Users/martin/fsnotify/helpers_test.go:436 +0x2cd
// created by github.com/fsnotify/fsnotify.(*eventCollector).collect in goroutine 8
// C:/Users/martin/fsnotify/helpers_test.go:427 +0x67
//
// The Windows backend hasn't really changed in a long time, so old
// problem, and not something we need to fix right now.
if runtime.GOOS == "windows" {
t.Skip("hangs on windows")
}
// TODO: sometimes hands on "unix.Close(info.wd)" in kqueue.remove().
// Only seems to happen on macOS and not the other kqueue platforms.
//
// Just skip for now; want to rewrite kqueue backend anyway...
if runtime.GOOS == "darwin" {
t.Skip("hangs on macOS")
}
tmp := t.TempDir()
w := newCollector(t, tmp)
w.collect(t)
var (
dir = join(tmp, "/dir")
wg sync.WaitGroup
)
w.w.Add(dir)
wg.Add(2000)
for i := 0; i < 1000; i++ {
go func() { defer wg.Done(); os.RemoveAll(dir) }()
go func() { defer wg.Done(); os.MkdirAll(dir, 0o0755) }()
w.w.Add(dir)
}
wg.Wait()
w.stop(t)
})
}
func TestNewWatcher(t *testing.T) {
w, err := NewWatcher()
if err != nil {
t.Fatal(err)
}
defaultSz := 0
if runtime.GOOS == "windows" {
defaultSz = 50
}
if c := cap(w.Events); c != defaultSz {
t.Errorf("cap of NewWatcher() is not %d but %d", defaultSz, c)
}
w, err = NewBufferedWatcher(0)
if err != nil {
t.Fatal(err)
}
if c := cap(w.Events); c != 0 {
t.Errorf("cap of NewWatcher() is not %d but %d", 0, c)
}
w, err = NewBufferedWatcher(42)
if err != nil {
t.Fatal(err)
}
if c := cap(w.Events); c != 42 {
t.Errorf("cap of NewWatcher() is not %d but %d", 42, c)
}
}
golang-fsnotify-1.9.0/go.mod 0000664 0000000 0000000 00000000427 15063112431 0015734 0 ustar 00root root 0000000 0000000 module 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
)
golang-fsnotify-1.9.0/go.sum 0000664 0000000 0000000 00000000231 15063112431 0015752 0 ustar 00root root 0000000 0000000 golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang-fsnotify-1.9.0/helpers_test.go 0000664 0000000 0000000 00000055674 15063112431 0017674 0 ustar 00root root 0000000 0000000 package 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.Errorf("eventCollector: unexpected error on Errors chan: %s", 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 "print":
do = append(do, func() { fmt.Println(strings.Join(c.args, " ")) })
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))
}
golang-fsnotify-1.9.0/internal/ 0000775 0000000 0000000 00000000000 15063112431 0016437 5 ustar 00root root 0000000 0000000 golang-fsnotify-1.9.0/internal/darwin.go 0000664 0000000 0000000 00000001724 15063112431 0020256 0 ustar 00root root 0000000 0000000 //go:build darwin
package internal
import (
"syscall"
"golang.org/x/sys/unix"
)
var (
ErrSyscallEACCES = syscall.EACCES
ErrUnixEACCES = 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 = 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) }
golang-fsnotify-1.9.0/internal/debug_darwin.go 0000664 0000000 0000000 00000004035 15063112431 0021422 0 ustar 00root root 0000000 0000000 package 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},
}
golang-fsnotify-1.9.0/internal/debug_dragonfly.go 0000664 0000000 0000000 00000001676 15063112431 0022133 0 ustar 00root root 0000000 0000000 package 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},
}
golang-fsnotify-1.9.0/internal/debug_freebsd.go 0000664 0000000 0000000 00000002436 15063112431 0021553 0 ustar 00root root 0000000 0000000 package 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},
}
golang-fsnotify-1.9.0/internal/debug_kqueue.go 0000664 0000000 0000000 00000001117 15063112431 0021433 0 ustar 00root root 0000000 0000000 //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)
}
golang-fsnotify-1.9.0/internal/debug_linux.go 0000664 0000000 0000000 00000002353 15063112431 0021276 0 ustar 00root root 0000000 0000000 package 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)
}
golang-fsnotify-1.9.0/internal/debug_netbsd.go 0000664 0000000 0000000 00000001232 15063112431 0021411 0 ustar 00root root 0000000 0000000 package 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},
}
golang-fsnotify-1.9.0/internal/debug_openbsd.go 0000664 0000000 0000000 00000001426 15063112431 0021571 0 ustar 00root root 0000000 0000000 package 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},
}
golang-fsnotify-1.9.0/internal/debug_solaris.go 0000664 0000000 0000000 00000001705 15063112431 0021613 0 ustar 00root root 0000000 0000000 package 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)
}
golang-fsnotify-1.9.0/internal/debug_windows.go 0000664 0000000 0000000 00000001555 15063112431 0021634 0 ustar 00root root 0000000 0000000 package 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))
}
golang-fsnotify-1.9.0/internal/freebsd.go 0000664 0000000 0000000 00000001364 15063112431 0020404 0 ustar 00root root 0000000 0000000 //go:build freebsd
package internal
import (
"syscall"
"golang.org/x/sys/unix"
)
var (
ErrSyscallEACCES = syscall.EACCES
ErrUnixEACCES = 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)) }
golang-fsnotify-1.9.0/internal/internal.go 0000664 0000000 0000000 00000000074 15063112431 0020603 0 ustar 00root root 0000000 0000000 // Package internal contains some helpers.
package internal
golang-fsnotify-1.9.0/internal/unix.go 0000664 0000000 0000000 00000001416 15063112431 0017753 0 ustar 00root root 0000000 0000000 //go:build !windows && !darwin && !freebsd && !plan9
package internal
import (
"syscall"
"golang.org/x/sys/unix"
)
var (
ErrSyscallEACCES = syscall.EACCES
ErrUnixEACCES = 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) }
golang-fsnotify-1.9.0/internal/unix2.go 0000664 0000000 0000000 00000000134 15063112431 0020031 0 ustar 00root root 0000000 0000000 //go:build !windows
package internal
func HasPrivilegesForSymlink() bool {
return true
}
golang-fsnotify-1.9.0/internal/windows.go 0000664 0000000 0000000 00000001710 15063112431 0020457 0 ustar 00root root 0000000 0000000 //go:build windows
package internal
import (
"errors"
"golang.org/x/sys/windows"
)
// Just a dummy.
var (
ErrSyscallEACCES = errors.New("dummy")
ErrUnixEACCES = 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()
}
golang-fsnotify-1.9.0/internal/ztest/ 0000775 0000000 0000000 00000000000 15063112431 0017610 5 ustar 00root root 0000000 0000000 golang-fsnotify-1.9.0/internal/ztest/diff.go 0000664 0000000 0000000 00000041175 15063112431 0021057 0 ustar 00root root 0000000 0000000 // 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 is a 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.
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)
}
golang-fsnotify-1.9.0/internal/ztest/diff_test.go 0000664 0000000 0000000 00000016101 15063112431 0022105 0 ustar 00root root 0000000 0000000 package 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 " + year + "!\n+want Hello " + year + "\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)
}
})
}
}
golang-fsnotify-1.9.0/shared.go 0000664 0000000 0000000 00000002013 15063112431 0016414 0 ustar 00root root 0000000 0000000 package fsnotify
import "sync"
type shared struct {
Events chan Event
Errors chan error
done chan struct{}
mu sync.Mutex
}
func newShared(ev chan Event, errs chan error) *shared {
return &shared{
Events: ev,
Errors: errs,
done: make(chan struct{}),
}
}
// Returns true if the event was sent, or false if watcher is closed.
func (w *shared) sendEvent(e Event) bool {
if e.Op == 0 {
return true
}
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 *shared) sendError(err error) bool {
if err == nil {
return true
}
select {
case <-w.done:
return false
case w.Errors <- err:
return true
}
}
func (w *shared) isClosed() bool {
select {
case <-w.done:
return true
default:
return false
}
}
// Mark as closed; returns true if it was already closed.
func (w *shared) close() bool {
w.mu.Lock()
defer w.mu.Unlock()
if w.isClosed() {
return true
}
close(w.done)
return false
}
golang-fsnotify-1.9.0/staticcheck.conf 0000664 0000000 0000000 00000000110 15063112431 0017747 0 ustar 00root root 0000000 0000000 checks = ['all',
'-U1000', # Don't complain about unused functions.
]
golang-fsnotify-1.9.0/system_bsd.go 0000664 0000000 0000000 00000000252 15063112431 0017325 0 ustar 00root root 0000000 0000000 //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
golang-fsnotify-1.9.0/system_darwin.go 0000664 0000000 0000000 00000000243 15063112431 0020041 0 ustar 00root root 0000000 0000000 //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
golang-fsnotify-1.9.0/test/ 0000775 0000000 0000000 00000000000 15063112431 0015602 5 ustar 00root root 0000000 0000000 golang-fsnotify-1.9.0/test/kqueue.c 0000664 0000000 0000000 00000005367 15063112431 0017260 0 ustar 00root root 0000000 0000000 // 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;
}
golang-fsnotify-1.9.0/testdata/ 0000775 0000000 0000000 00000000000 15063112431 0016434 5 ustar 00root root 0000000 0000000 golang-fsnotify-1.9.0/testdata/watch-dir/ 0000775 0000000 0000000 00000000000 15063112431 0020316 5 ustar 00root root 0000000 0000000 golang-fsnotify-1.9.0/testdata/watch-dir/bug-277 0000664 0000000 0000000 00000001036 15063112431 0021333 0 ustar 00root root 0000000 0000000 # 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?
golang-fsnotify-1.9.0/testdata/watch-dir/create-cyclic-symlink 0000664 0000000 0000000 00000000300 15063112431 0024425 0 ustar 00root root 0000000 0000000 # Cyclic symlink
require symlink
ln -s ./ /link
watch /
rm /link
echo foo >>/link
Output:
write /link
write /link
linux, windows, fen:
remove /link
create /link
write /link
golang-fsnotify-1.9.0/testdata/watch-dir/create-dev 0000664 0000000 0000000 00000000073 15063112431 0022260 0 ustar 00root root 0000000 0000000 require mknod
watch /
mknod 0 /dev
Output:
create /dev
golang-fsnotify-1.9.0/testdata/watch-dir/create-dir 0000664 0000000 0000000 00000000104 15063112431 0022253 0 ustar 00root root 0000000 0000000 # Create a new directory.
watch /
mkdir /dir
Output:
create /dir
golang-fsnotify-1.9.0/testdata/watch-dir/create-empty-file 0000664 0000000 0000000 00000000107 15063112431 0023553 0 ustar 00root root 0000000 0000000 # Create a new empty file.
watch /
touch /file
Output:
create /file
golang-fsnotify-1.9.0/testdata/watch-dir/create-fifo 0000664 0000000 0000000 00000000106 15063112431 0022422 0 ustar 00root root 0000000 0000000 # FIFO
require mkfifo
watch /
mkfifo /fifo
Output:
create /fifo
golang-fsnotify-1.9.0/testdata/watch-dir/create-file-in-subdir 0000664 0000000 0000000 00000000146 15063112431 0024314 0 ustar 00root root 0000000 0000000 watch /
mkdir /dir
touch /dir/file
Output:
create /dir
fen: # TODO
create /dir
write /dir
golang-fsnotify-1.9.0/testdata/watch-dir/create-file-with-data 0000664 0000000 0000000 00000000153 15063112431 0024300 0 ustar 00root root 0000000 0000000 # Create a new empty file with some data.
watch /
echo data >>/file
Output:
create /file
write /file
golang-fsnotify-1.9.0/testdata/watch-dir/create-unresolvable-symlink 0000664 0000000 0000000 00000000241 15063112431 0025664 0 ustar 00root root 0000000 0000000 # Create unresolvable symlink
require symlink
watch /
ln -s /target /link
Output:
create /link
dragonfly: # TODO: should fix this if possible.
no-events
golang-fsnotify-1.9.0/testdata/watch-dir/dir-only 0000664 0000000 0000000 00000000225 15063112431 0021775 0 ustar 00root root 0000000 0000000 #
touch /before-watch
watch /
echo data >>/file
rm /file
rm /before-watch
Output:
create /file
write /file
remove /file
remove /before-watch
golang-fsnotify-1.9.0/testdata/watch-dir/make-file-unreadable 0000664 0000000 0000000 00000000445 15063112431 0024176 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/multiple-creates 0000664 0000000 0000000 00000000740 15063112431 0023521 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/only-chmod 0000664 0000000 0000000 00000000653 15063112431 0022316 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/only-create 0000664 0000000 0000000 00000000663 15063112431 0022470 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/only-remove 0000664 0000000 0000000 00000000663 15063112431 0022522 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/only-rename 0000664 0000000 0000000 00000001227 15063112431 0022471 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/only-write 0000664 0000000 0000000 00000000637 15063112431 0022360 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/op-all 0000664 0000000 0000000 00000000741 15063112431 0021427 0 ustar 00root root 0000000 0000000 require 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
golang-fsnotify-1.9.0/testdata/watch-dir/op-closeread 0000664 0000000 0000000 00000000216 15063112431 0022615 0 ustar 00root root 0000000 0000000 # Basic close_read test
require op_close_read
echo "hello, world!" >>/file
watch / default close_read
cat /file
Output:
close_read /file
golang-fsnotify-1.9.0/testdata/watch-dir/op-closewrite 0000664 0000000 0000000 00000000533 15063112431 0023036 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/op-open 0000664 0000000 0000000 00000000166 15063112431 0021621 0 ustar 00root root 0000000 0000000 # Basic open test
require op_open
echo "hello, world!" >>/file
watch / default open
cat /file
Output:
open /file
golang-fsnotify-1.9.0/testdata/watch-dir/op-read 0000664 0000000 0000000 00000000174 15063112431 0021572 0 ustar 00root root 0000000 0000000 # Basic close_read test
require op_read
echo "hello, world!" >>/file
watch / default read
cat /file
Output:
read /file
golang-fsnotify-1.9.0/testdata/watch-dir/remove-symlink 0000664 0000000 0000000 00000000234 15063112431 0023221 0 ustar 00root root 0000000 0000000 # Remove a symlink.
require symlink
touch /file
ln -s /file /link
watch /
rm /link
Output:
remove /link
kqueue: # TODO: this is broken.
no-events
golang-fsnotify-1.9.0/testdata/watch-dir/remove-while-watching-parent 0000664 0000000 0000000 00000000770 15063112431 0025741 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/rename-file 0000664 0000000 0000000 00000000172 15063112431 0022425 0 ustar 00root root 0000000 0000000 # Rename file in watched dir.
echo asd >>/file
watch /
mv /file /rename
Output:
rename /file
create /rename ← /file
golang-fsnotify-1.9.0/testdata/watch-dir/rename-from-other-watch 0000664 0000000 0000000 00000000663 15063112431 0024701 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/rename-from-unwatched 0000664 0000000 0000000 00000000217 15063112431 0024431 0 ustar 00root root 0000000 0000000 # Rename from unwatched dir.
mkdir /unwatch
touch /unwatch/file
mkdir /dir
watch /dir
mv /unwatch/file /dir/file
Output:
create /dir/file
golang-fsnotify-1.9.0/testdata/watch-dir/rename-from-unwatched-overwrite 0000664 0000000 0000000 00000000521 15063112431 0026453 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/rename-overwrite 0000664 0000000 0000000 00000000477 15063112431 0023544 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/rename-symlink 0000664 0000000 0000000 00000000322 15063112431 0023171 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/rename-to-unwatched 0000664 0000000 0000000 00000001210 15063112431 0024102 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/rename-watched-dir 0000664 0000000 0000000 00000000324 15063112431 0023700 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/subdir 0000664 0000000 0000000 00000001260 15063112431 0021530 0 ustar 00root root 0000000 0000000 #
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
golang-fsnotify-1.9.0/testdata/watch-dir/symlink-dir 0000664 0000000 0000000 00000001343 15063112431 0022504 0 ustar 00root root 0000000 0000000 # Create a new symlink to a watched file.
require symlink
mkdir /dir
watch /
ln -s /dir /link
# Shouldn't give any events.
touch link/file
Output:
create /link
# kqueue sends an event because it sends an event every time a directory
# changes, but fsnotify doesn't detect the symlink as a "directory". It's
# been like this for over a decade. When it was changed before people
# complained so I'm hesitant to fix it now as part of a bugfix.
#
# TODO: it shouldn't do this; it doesn't work like this on inotify.
kqueue:
create /link
write /link
# TODO: should also fix FEN, which seems to send a write for regular dirs as
# well.
fen:
create /link
write /dir
dragonfly: # TODO: can we fix this?
no-events
golang-fsnotify-1.9.0/testdata/watch-dir/symlink-file 0000664 0000000 0000000 00000000243 15063112431 0022643 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/symlink-nofollow 0000664 0000000 0000000 00000000562 15063112431 0023567 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/truncate-file 0000664 0000000 0000000 00000000424 15063112431 0023003 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/unreadable-file 0000664 0000000 0000000 00000000643 15063112431 0023263 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/watch-dir-inside-watched 0000664 0000000 0000000 00000001175 15063112431 0025015 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/watch-file-inside-watched-dir 0000664 0000000 0000000 00000000652 15063112431 0025731 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-dir/watch-twice 0000664 0000000 0000000 00000000257 15063112431 0022464 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-file/ 0000775 0000000 0000000 00000000000 15063112431 0020457 5 ustar 00root root 0000000 0000000 golang-fsnotify-1.9.0/testdata/watch-file/chmod 0000664 0000000 0000000 00000000160 15063112431 0021471 0 ustar 00root root 0000000 0000000 # chmod the watched file.
touch /file
watch /file
chmod 700 /file
Output:
chmod /file
windows:
no-events
golang-fsnotify-1.9.0/testdata/watch-file/chmod-after-write 0000664 0000000 0000000 00000000264 15063112431 0023725 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-file/op-all 0000664 0000000 0000000 00000000512 15063112431 0021564 0 ustar 00root root 0000000 0000000 require 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
golang-fsnotify-1.9.0/testdata/watch-file/op-closeread 0000664 0000000 0000000 00000000222 15063112431 0022753 0 ustar 00root root 0000000 0000000 # Basic close_read test
require op_close_read
echo "hello, world!" >>/file
watch /file default close_read
cat /file
Output:
close_read /file
golang-fsnotify-1.9.0/testdata/watch-file/op-closewrite 0000664 0000000 0000000 00000000441 15063112431 0023175 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-file/op-open 0000664 0000000 0000000 00000000172 15063112431 0021757 0 ustar 00root root 0000000 0000000 # Basic open test
require op_open
echo "hello, world!" >>/file
watch /file default open
cat /file
Output:
open /file
golang-fsnotify-1.9.0/testdata/watch-file/op-read 0000664 0000000 0000000 00000000200 15063112431 0021721 0 ustar 00root root 0000000 0000000 # Basic close_read test
require op_read
echo "hello, world!" >>/file
watch /file default read
cat /file
Output:
read /file
golang-fsnotify-1.9.0/testdata/watch-file/overwrite-watched-file 0000664 0000000 0000000 00000000643 15063112431 0024765 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-file/overwrite-watched-file-with-watched-file 0000664 0000000 0000000 00000001225 15063112431 0030265 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-file/re-add-renamed-filed 0000664 0000000 0000000 00000000613 15063112431 0024230 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-file/remove-watched-file 0000664 0000000 0000000 00000000240 15063112431 0024225 0 ustar 00root root 0000000 0000000 # Remove watched file.
touch /file
watch /file
rm /file
Output:
remove /file
linux: # unlink always emits a chmod on linux.
chmod /file
remove /file
golang-fsnotify-1.9.0/testdata/watch-file/rename-watched-file 0000664 0000000 0000000 00000000356 15063112431 0024207 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-file/watch-twice 0000664 0000000 0000000 00000000154 15063112431 0022621 0 ustar 00root root 0000000 0000000 # watch same file twice.
touch /file
watch /file
watch /file
echo hello >>/file
Output:
write /file
golang-fsnotify-1.9.0/testdata/watch-recurse/ 0000775 0000000 0000000 00000000000 15063112431 0021210 5 ustar 00root root 0000000 0000000 golang-fsnotify-1.9.0/testdata/watch-recurse/add-dir 0000664 0000000 0000000 00000001540 15063112431 0022437 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-recurse/add-file 0000664 0000000 0000000 00000000454 15063112431 0022603 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-recurse/remove-dir 0000664 0000000 0000000 00000000720 15063112431 0023203 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-recurse/remove-recursive 0000664 0000000 0000000 00000001047 15063112431 0024437 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-recurse/remove-watched-dir 0000664 0000000 0000000 00000002176 15063112431 0024627 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-recurse/rename-dir 0000664 0000000 0000000 00000001331 15063112431 0023154 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-symlink/ 0000775 0000000 0000000 00000000000 15063112431 0021226 5 ustar 00root root 0000000 0000000 golang-fsnotify-1.9.0/testdata/watch-symlink/nofollow-dir 0000664 0000000 0000000 00000000311 15063112431 0023557 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-symlink/nofollow-file 0000664 0000000 0000000 00000000331 15063112431 0023722 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-symlink/to-dir 0000664 0000000 0000000 00000000230 15063112431 0022342 0 ustar 00root root 0000000 0000000 # Watch a symlink to a dir
require symlink
mkdir /dir
touch /dir/existing
ln -s /dir /link
watch /link
touch /dir/file
Output:
create /link/file
golang-fsnotify-1.9.0/testdata/watch-symlink/to-dir-relative 0000664 0000000 0000000 00000000205 15063112431 0024155 0 ustar 00root root 0000000 0000000 # Watch a symlink to a dir
require symlink
mkdir /dir
ln -s ./dir /link
watch /link
touch /dir/file
Output:
create /link/file
golang-fsnotify-1.9.0/testdata/watch-symlink/to-file 0000664 0000000 0000000 00000000261 15063112431 0022507 0 ustar 00root root 0000000 0000000 # 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
golang-fsnotify-1.9.0/testdata/watch-symlink/to-file-relative 0000664 0000000 0000000 00000000263 15063112431 0024322 0 ustar 00root root 0000000 0000000 # 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