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