[chore]: Bump github.com/tdewolff/minify/v2 from 2.20.0 to 2.20.6 (#2337)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
parent
28f85db30a
commit
74b600655d
6
go.mod
6
go.mod
|
@ -46,7 +46,7 @@ require (
|
||||||
github.com/superseriousbusiness/activity v1.4.0-gts
|
github.com/superseriousbusiness/activity v1.4.0-gts
|
||||||
github.com/superseriousbusiness/exif-terminator v0.5.0
|
github.com/superseriousbusiness/exif-terminator v0.5.0
|
||||||
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8
|
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8
|
||||||
github.com/tdewolff/minify/v2 v2.20.0
|
github.com/tdewolff/minify/v2 v2.20.6
|
||||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
|
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
|
||||||
github.com/ulule/limiter/v3 v3.11.2
|
github.com/ulule/limiter/v3 v3.11.2
|
||||||
github.com/uptrace/bun v1.1.16
|
github.com/uptrace/bun v1.1.16
|
||||||
|
@ -97,7 +97,7 @@ require (
|
||||||
github.com/dsoprea/go-png-image-structure/v2 v2.0.0-20210512210324-29b889a6093d // indirect
|
github.com/dsoprea/go-png-image-structure/v2 v2.0.0-20210512210324-29b889a6093d // indirect
|
||||||
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e // indirect
|
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-errors/errors v1.4.1 // indirect
|
github.com/go-errors/errors v1.4.1 // indirect
|
||||||
|
@ -148,7 +148,7 @@ require (
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/subosito/gotenv v1.4.2 // indirect
|
github.com/subosito/gotenv v1.4.2 // indirect
|
||||||
github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe // indirect
|
github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe // indirect
|
||||||
github.com/tdewolff/parse/v2 v2.7.0 // indirect
|
github.com/tdewolff/parse/v2 v2.7.4 // indirect
|
||||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
|
|
17
go.sum
17
go.sum
|
@ -164,8 +164,8 @@ github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0X
|
||||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
github.com/fxamacker/cbor v1.5.1 h1:XjQWBgdmQyqimslUh5r4tUGmoqzHmBFQOImkWGi2awg=
|
github.com/fxamacker/cbor v1.5.1 h1:XjQWBgdmQyqimslUh5r4tUGmoqzHmBFQOImkWGi2awg=
|
||||||
github.com/fxamacker/cbor v1.5.1/go.mod h1:3aPGItF174ni7dDzd6JZ206H8cmr4GDNBGpPa971zsU=
|
github.com/fxamacker/cbor v1.5.1/go.mod h1:3aPGItF174ni7dDzd6JZ206H8cmr4GDNBGpPa971zsU=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
|
@ -491,12 +491,12 @@ github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430
|
||||||
github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe/go.mod h1:gH4P6gN1V+wmIw5o97KGaa1RgXB/tVpC2UNzijhg3E4=
|
github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe/go.mod h1:gH4P6gN1V+wmIw5o97KGaa1RgXB/tVpC2UNzijhg3E4=
|
||||||
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8 h1:nTIhuP157oOFcscuoK1kCme1xTeGIzztSw70lX9NrDQ=
|
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8 h1:nTIhuP157oOFcscuoK1kCme1xTeGIzztSw70lX9NrDQ=
|
||||||
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8/go.mod h1:uYC/W92oVRJ49Vh1GcvTqpeFqHi+Ovrl2sMllQWRAEo=
|
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8/go.mod h1:uYC/W92oVRJ49Vh1GcvTqpeFqHi+Ovrl2sMllQWRAEo=
|
||||||
github.com/tdewolff/minify/v2 v2.20.0 h1:JFoL/Jxnyebf/jw3woqpmwBjSNJYSeU+sTFl9dTMHQ8=
|
github.com/tdewolff/minify/v2 v2.20.6 h1:R4+Iw1ZqJxrqH52WWHtCpukMuhmO/EasY8YlDiSxphw=
|
||||||
github.com/tdewolff/minify/v2 v2.20.0/go.mod h1:TEE9CWftBwKQLUTZHuH9upjiqlt8zFpQOGxQ81rsG3c=
|
github.com/tdewolff/minify/v2 v2.20.6/go.mod h1:9t0EY9xySGt1vrP8iscmJfywQwDCQyQBYN6ge+9GwP0=
|
||||||
github.com/tdewolff/parse/v2 v2.7.0 h1:eVeKTV9nQ9BNS0LPlOgrhLXisiAjacaf60aRgSEtnic=
|
github.com/tdewolff/parse/v2 v2.7.4 h1:zrUn2CFg9+5llbUZcsycctFlNRyV1D5gFBZRxuGzdzk=
|
||||||
github.com/tdewolff/parse/v2 v2.7.0/go.mod h1:9p2qMIHpjRSTr1qnFxQr+igogyTUTlwvf9awHSm84h8=
|
github.com/tdewolff/parse/v2 v2.7.4/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
|
||||||
github.com/tdewolff/test v1.0.10 h1:uWiheaLgLcNFqHcdWveum7PQfMnIUTf9Kl3bFxrIoew=
|
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52 h1:gAQliwn+zJrkjAHVcBEYW/RFvd2St4yYimisvozAYlA=
|
||||||
github.com/tdewolff/test v1.0.10/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||||
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E=
|
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E=
|
||||||
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
|
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
|
||||||
github.com/tidwall/buntdb v1.1.2 h1:noCrqQXL9EKMtcdwJcmuVKSEjqu1ua99RHHgbLTEHRo=
|
github.com/tidwall/buntdb v1.1.2 h1:noCrqQXL9EKMtcdwJcmuVKSEjqu1ua99RHHgbLTEHRo=
|
||||||
|
@ -762,7 +762,6 @@ golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
freebsd_task:
|
||||||
|
name: 'FreeBSD'
|
||||||
|
freebsd_instance:
|
||||||
|
image_family: freebsd-13-2
|
||||||
|
install_script:
|
||||||
|
- pkg update -f
|
||||||
|
- pkg install -y go
|
||||||
|
test_script:
|
||||||
|
# run tests as user "cirrus" instead of root
|
||||||
|
- pw useradd cirrus -m
|
||||||
|
- chown -R cirrus:cirrus .
|
||||||
|
- FSNOTIFY_BUFFER=4096 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./...
|
||||||
|
- sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./...
|
|
@ -4,3 +4,4 @@
|
||||||
|
|
||||||
# Output of go build ./cmd/fsnotify
|
# Output of go build ./cmd/fsnotify
|
||||||
/fsnotify
|
/fsnotify
|
||||||
|
/fsnotify.exe
|
||||||
|
|
|
@ -1,16 +1,87 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
Unreleased
|
||||||
|
----------
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
Nothing yet.
|
Nothing yet.
|
||||||
|
|
||||||
## [1.6.0] - 2022-10-13
|
1.7.0 - 2023-10-22
|
||||||
|
------------------
|
||||||
|
This version of fsnotify needs Go 1.17.
|
||||||
|
|
||||||
|
### Additions
|
||||||
|
|
||||||
|
- illumos: add FEN backend to support illumos and Solaris. ([#371])
|
||||||
|
|
||||||
|
- all: add `NewBufferedWatcher()` to use a buffered channel, which can be useful
|
||||||
|
in cases where you can't control the kernel buffer and receive a large number
|
||||||
|
of events in bursts. ([#550], [#572])
|
||||||
|
|
||||||
|
- all: add `AddWith()`, which is identical to `Add()` but allows passing
|
||||||
|
options. ([#521])
|
||||||
|
|
||||||
|
- windows: allow setting the ReadDirectoryChangesW() buffer size with
|
||||||
|
`fsnotify.WithBufferSize()`; the default of 64K is the highest value that
|
||||||
|
works on all platforms and is enough for most purposes, but in some cases a
|
||||||
|
highest buffer is needed. ([#521])
|
||||||
|
|
||||||
|
### Changes and fixes
|
||||||
|
|
||||||
|
- inotify: remove watcher if a watched path is renamed ([#518])
|
||||||
|
|
||||||
|
After a rename the reported name wasn't updated, or even an empty string.
|
||||||
|
Inotify doesn't provide any good facilities to update it, so just remove the
|
||||||
|
watcher. This is already how it worked on kqueue and FEN.
|
||||||
|
|
||||||
|
On Windows this does work, and remains working.
|
||||||
|
|
||||||
|
- windows: don't listen for file attribute changes ([#520])
|
||||||
|
|
||||||
|
File attribute changes are sent as `FILE_ACTION_MODIFIED` by the Windows API,
|
||||||
|
with no way to see if they're a file write or attribute change, so would show
|
||||||
|
up as a fsnotify.Write event. This is never useful, and could result in many
|
||||||
|
spurious Write events.
|
||||||
|
|
||||||
|
- windows: return `ErrEventOverflow` if the buffer is full ([#525])
|
||||||
|
|
||||||
|
Before it would merely return "short read", making it hard to detect this
|
||||||
|
error.
|
||||||
|
|
||||||
|
- kqueue: make sure events for all files are delivered properly when removing a
|
||||||
|
watched directory ([#526])
|
||||||
|
|
||||||
|
Previously they would get sent with `""` (empty string) or `"."` as the path
|
||||||
|
name.
|
||||||
|
|
||||||
|
- kqueue: don't emit spurious Create events for symbolic links ([#524])
|
||||||
|
|
||||||
|
The link would get resolved but kqueue would "forget" it already saw the link
|
||||||
|
itself, resulting on a Create for every Write event for the directory.
|
||||||
|
|
||||||
|
- all: return `ErrClosed` on `Add()` when the watcher is closed ([#516])
|
||||||
|
|
||||||
|
- other: add `Watcher.Errors` and `Watcher.Events` to the no-op `Watcher` in
|
||||||
|
`backend_other.go`, making it easier to use on unsupported platforms such as
|
||||||
|
WASM, AIX, etc. ([#528])
|
||||||
|
|
||||||
|
- other: use the `backend_other.go` no-op if the `appengine` build tag is set;
|
||||||
|
Google AppEngine forbids usage of the unsafe package so the inotify backend
|
||||||
|
won't compile there.
|
||||||
|
|
||||||
|
[#371]: https://github.com/fsnotify/fsnotify/pull/371
|
||||||
|
[#516]: https://github.com/fsnotify/fsnotify/pull/516
|
||||||
|
[#518]: https://github.com/fsnotify/fsnotify/pull/518
|
||||||
|
[#520]: https://github.com/fsnotify/fsnotify/pull/520
|
||||||
|
[#521]: https://github.com/fsnotify/fsnotify/pull/521
|
||||||
|
[#524]: https://github.com/fsnotify/fsnotify/pull/524
|
||||||
|
[#525]: https://github.com/fsnotify/fsnotify/pull/525
|
||||||
|
[#526]: https://github.com/fsnotify/fsnotify/pull/526
|
||||||
|
[#528]: https://github.com/fsnotify/fsnotify/pull/528
|
||||||
|
[#537]: https://github.com/fsnotify/fsnotify/pull/537
|
||||||
|
[#550]: https://github.com/fsnotify/fsnotify/pull/550
|
||||||
|
[#572]: https://github.com/fsnotify/fsnotify/pull/572
|
||||||
|
|
||||||
|
1.6.0 - 2022-10-13
|
||||||
|
------------------
|
||||||
This version of fsnotify needs Go 1.16 (this was already the case since 1.5.1,
|
This version of fsnotify needs Go 1.16 (this was already the case since 1.5.1,
|
||||||
but not documented). It also increases the minimum Linux version to 2.6.32.
|
but not documented). It also increases the minimum Linux version to 2.6.32.
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,31 @@
|
||||||
fsnotify is a Go library to provide cross-platform filesystem notifications on
|
fsnotify is a Go library to provide cross-platform filesystem notifications on
|
||||||
Windows, Linux, macOS, and BSD systems.
|
Windows, Linux, macOS, BSD, and illumos.
|
||||||
|
|
||||||
Go 1.16 or newer is required; the full documentation is at
|
Go 1.17 or newer is required; the full documentation is at
|
||||||
https://pkg.go.dev/github.com/fsnotify/fsnotify
|
https://pkg.go.dev/github.com/fsnotify/fsnotify
|
||||||
|
|
||||||
**It's best to read the documentation at pkg.go.dev, as it's pinned to the last
|
|
||||||
released version, whereas this README is for the last development version which
|
|
||||||
may include additions/changes.**
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Platform support:
|
Platform support:
|
||||||
|
|
||||||
| Adapter | OS | Status |
|
| Backend | OS | Status |
|
||||||
| --------------------- | ---------------| -------------------------------------------------------------|
|
| :-------------------- | :--------- | :------------------------------------------------------------------------ |
|
||||||
| inotify | Linux 2.6.32+ | Supported |
|
| inotify | Linux | Supported |
|
||||||
| kqueue | BSD, macOS | Supported |
|
| kqueue | BSD, macOS | Supported |
|
||||||
| ReadDirectoryChangesW | Windows | Supported |
|
| ReadDirectoryChangesW | Windows | Supported |
|
||||||
| FSEvents | macOS | [Planned](https://github.com/fsnotify/fsnotify/issues/11) |
|
| FEN | illumos | Supported |
|
||||||
| FEN | Solaris 11 | [In Progress](https://github.com/fsnotify/fsnotify/pull/371) |
|
| fanotify | Linux 5.9+ | [Not yet](https://github.com/fsnotify/fsnotify/issues/114) |
|
||||||
| fanotify | Linux 5.9+ | [Maybe](https://github.com/fsnotify/fsnotify/issues/114) |
|
| AHAFS | AIX | [aix branch]; experimental due to lack of maintainer and test environment |
|
||||||
| USN Journals | Windows | [Maybe](https://github.com/fsnotify/fsnotify/issues/53) |
|
| FSEvents | macOS | [Needs support in x/sys/unix][fsevents] |
|
||||||
| Polling | *All* | [Maybe](https://github.com/fsnotify/fsnotify/issues/9) |
|
| USN Journals | Windows | [Needs support in x/sys/windows][usn] |
|
||||||
|
| Polling | *All* | [Not yet](https://github.com/fsnotify/fsnotify/issues/9) |
|
||||||
|
|
||||||
Linux and macOS should include Android and iOS, but these are currently untested.
|
Linux and illumos should include Android and Solaris, but these are currently
|
||||||
|
untested.
|
||||||
|
|
||||||
|
[fsevents]: https://github.com/fsnotify/fsnotify/issues/11#issuecomment-1279133120
|
||||||
|
[usn]: https://github.com/fsnotify/fsnotify/issues/53#issuecomment-1279829847
|
||||||
|
[aix branch]: https://github.com/fsnotify/fsnotify/issues/353#issuecomment-1284590129
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
-----
|
-----
|
||||||
|
@ -83,20 +85,23 @@ run with:
|
||||||
|
|
||||||
% go run ./cmd/fsnotify
|
% go run ./cmd/fsnotify
|
||||||
|
|
||||||
|
Further detailed documentation can be found in godoc:
|
||||||
|
https://pkg.go.dev/github.com/fsnotify/fsnotify
|
||||||
|
|
||||||
FAQ
|
FAQ
|
||||||
---
|
---
|
||||||
### Will a file still be watched when it's moved to another directory?
|
### Will a file still be watched when it's moved to another directory?
|
||||||
No, not unless you are watching the location it was moved to.
|
No, not unless you are watching the location it was moved to.
|
||||||
|
|
||||||
### Are subdirectories watched too?
|
### Are subdirectories watched?
|
||||||
No, you must add watches for any directory you want to watch (a recursive
|
No, you must add watches for any directory you want to watch (a recursive
|
||||||
watcher is on the roadmap: [#18]).
|
watcher is on the roadmap: [#18]).
|
||||||
|
|
||||||
[#18]: https://github.com/fsnotify/fsnotify/issues/18
|
[#18]: https://github.com/fsnotify/fsnotify/issues/18
|
||||||
|
|
||||||
### Do I have to watch the Error and Event channels in a goroutine?
|
### Do I have to watch the Error and Event channels in a goroutine?
|
||||||
As of now, yes (you can read both channels in the same goroutine using `select`,
|
Yes. You can read both channels in the same goroutine using `select` (you don't
|
||||||
you don't need a separate goroutine for both channels; see the example).
|
need a separate goroutine for both channels; see the example).
|
||||||
|
|
||||||
### Why don't notifications work with NFS, SMB, FUSE, /proc, or /sys?
|
### Why don't notifications work with NFS, SMB, FUSE, /proc, or /sys?
|
||||||
fsnotify requires support from underlying OS to work. The current NFS and SMB
|
fsnotify requires support from underlying OS to work. The current NFS and SMB
|
||||||
|
@ -107,6 +112,32 @@ This could be fixed with a polling watcher ([#9]), but it's not yet implemented.
|
||||||
|
|
||||||
[#9]: https://github.com/fsnotify/fsnotify/issues/9
|
[#9]: https://github.com/fsnotify/fsnotify/issues/9
|
||||||
|
|
||||||
|
### Why do I get many Chmod events?
|
||||||
|
Some programs may generate a lot of attribute changes; for example Spotlight on
|
||||||
|
macOS, anti-virus programs, backup applications, and some others are known to do
|
||||||
|
this. As a rule, it's typically best to ignore Chmod events. They're often not
|
||||||
|
useful, and tend to cause problems.
|
||||||
|
|
||||||
|
Spotlight indexing on macOS can result in multiple events (see [#15]). A
|
||||||
|
temporary workaround is to add your folder(s) to the *Spotlight Privacy
|
||||||
|
settings* until we have a native FSEvents implementation (see [#11]).
|
||||||
|
|
||||||
|
[#11]: https://github.com/fsnotify/fsnotify/issues/11
|
||||||
|
[#15]: https://github.com/fsnotify/fsnotify/issues/15
|
||||||
|
|
||||||
|
### Watching a file doesn't work well
|
||||||
|
Watching individual files (rather than directories) is generally not recommended
|
||||||
|
as many programs (especially editors) update files atomically: it will write to
|
||||||
|
a temporary file which is then moved to to destination, overwriting the original
|
||||||
|
(or some variant thereof). The watcher on the original file is now lost, as that
|
||||||
|
no longer exists.
|
||||||
|
|
||||||
|
The upshot of this is that a power failure or crash won't leave a half-written
|
||||||
|
file.
|
||||||
|
|
||||||
|
Watch the parent directory and use `Event.Name` to filter out files you're not
|
||||||
|
interested in. There is an example of this in `cmd/fsnotify/file.go`.
|
||||||
|
|
||||||
Platform-specific notes
|
Platform-specific notes
|
||||||
-----------------------
|
-----------------------
|
||||||
### Linux
|
### Linux
|
||||||
|
@ -151,11 +182,3 @@ these platforms.
|
||||||
|
|
||||||
The sysctl variables `kern.maxfiles` and `kern.maxfilesperproc` can be used to
|
The sysctl variables `kern.maxfiles` and `kern.maxfilesperproc` can be used to
|
||||||
control the maximum number of open files.
|
control the maximum number of open files.
|
||||||
|
|
||||||
### macOS
|
|
||||||
Spotlight indexing on macOS can result in multiple events (see [#15]). A temporary
|
|
||||||
workaround is to add your folder(s) to the *Spotlight Privacy settings* until we
|
|
||||||
have a native FSEvents implementation (see [#11]).
|
|
||||||
|
|
||||||
[#11]: https://github.com/fsnotify/fsnotify/issues/11
|
|
||||||
[#15]: https://github.com/fsnotify/fsnotify/issues/15
|
|
||||||
|
|
|
@ -1,10 +1,19 @@
|
||||||
//go:build solaris
|
//go:build solaris
|
||||||
// +build solaris
|
// +build solaris
|
||||||
|
|
||||||
|
// Note: the documentation on the Watcher type and methods is generated from
|
||||||
|
// mkdoc.zsh
|
||||||
|
|
||||||
package fsnotify
|
package fsnotify
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Watcher watches a set of paths, delivering events on a channel.
|
// Watcher watches a set of paths, delivering events on a channel.
|
||||||
|
@ -17,9 +26,9 @@ import (
|
||||||
// When a file is removed a Remove event won't be emitted until all file
|
// When a file is removed a Remove event won't be emitted until all file
|
||||||
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
||||||
//
|
//
|
||||||
// fp := os.Open("file")
|
// fp := os.Open("file")
|
||||||
// os.Remove("file") // Triggers Chmod
|
// os.Remove("file") // Triggers Chmod
|
||||||
// fp.Close() // Triggers Remove
|
// fp.Close() // Triggers Remove
|
||||||
//
|
//
|
||||||
// This is the event that inotify sends, so not much can be changed about this.
|
// This is the event that inotify sends, so not much can be changed about this.
|
||||||
//
|
//
|
||||||
|
@ -33,16 +42,16 @@ import (
|
||||||
//
|
//
|
||||||
// To increase them you can use sysctl or write the value to the /proc file:
|
// To increase them you can use sysctl or write the value to the /proc file:
|
||||||
//
|
//
|
||||||
// # Default values on Linux 5.18
|
// # Default values on Linux 5.18
|
||||||
// sysctl fs.inotify.max_user_watches=124983
|
// sysctl fs.inotify.max_user_watches=124983
|
||||||
// sysctl fs.inotify.max_user_instances=128
|
// sysctl fs.inotify.max_user_instances=128
|
||||||
//
|
//
|
||||||
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
||||||
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
||||||
// your distro's documentation):
|
// your distro's documentation):
|
||||||
//
|
//
|
||||||
// fs.inotify.max_user_watches=124983
|
// fs.inotify.max_user_watches=124983
|
||||||
// fs.inotify.max_user_instances=128
|
// fs.inotify.max_user_instances=128
|
||||||
//
|
//
|
||||||
// Reaching the limit will result in a "no space left on device" or "too many open
|
// Reaching the limit will result in a "no space left on device" or "too many open
|
||||||
// files" error.
|
// files" error.
|
||||||
|
@ -58,14 +67,20 @@ import (
|
||||||
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
||||||
// systems.
|
// systems.
|
||||||
//
|
//
|
||||||
// # macOS notes
|
// # Windows notes
|
||||||
//
|
//
|
||||||
// Spotlight indexing on macOS can result in multiple events (see [#15]). A
|
// Paths can be added as "C:\path\to\dir", but forward slashes
|
||||||
// temporary workaround is to add your folder(s) to the "Spotlight Privacy
|
// ("C:/path/to/dir") will also work.
|
||||||
// Settings" until we have a native FSEvents implementation (see [#11]).
|
|
||||||
//
|
//
|
||||||
// [#11]: https://github.com/fsnotify/fsnotify/issues/11
|
// When a watched directory is removed it will always send an event for the
|
||||||
// [#15]: https://github.com/fsnotify/fsnotify/issues/15
|
// directory itself, but may not send events for all files in that directory.
|
||||||
|
// Sometimes it will send events for all times, sometimes it will send no
|
||||||
|
// events, and often only for some files.
|
||||||
|
//
|
||||||
|
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
|
||||||
|
// value that is guaranteed to work with SMB filesystems. If you have many
|
||||||
|
// events in quick succession this may not be enough, and you will have to use
|
||||||
|
// [WithBufferSize] to increase the value.
|
||||||
type Watcher struct {
|
type Watcher struct {
|
||||||
// Events sends the filesystem change events.
|
// Events sends the filesystem change events.
|
||||||
//
|
//
|
||||||
|
@ -92,44 +107,129 @@ type Watcher struct {
|
||||||
// initiated by the user may show up as one or multiple
|
// initiated by the user may show up as one or multiple
|
||||||
// writes, depending on when the system syncs things to
|
// writes, depending on when the system syncs things to
|
||||||
// disk. For example when compiling a large Go program
|
// disk. For example when compiling a large Go program
|
||||||
// you may get hundreds of Write events, so you
|
// you may get hundreds of Write events, and you may
|
||||||
// probably want to wait until you've stopped receiving
|
// want to wait until you've stopped receiving them
|
||||||
// them (see the dedup example in cmd/fsnotify).
|
// (see the dedup example in cmd/fsnotify).
|
||||||
|
//
|
||||||
|
// Some systems may send Write event for directories
|
||||||
|
// when the directory content changes.
|
||||||
//
|
//
|
||||||
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
||||||
// when a file is removed (or more accurately, when a
|
// when a file is removed (or more accurately, when a
|
||||||
// link to an inode is removed). On kqueue it's sent
|
// link to an inode is removed). On kqueue it's sent
|
||||||
// and on kqueue when a file is truncated. On Windows
|
// when a file is truncated. On Windows it's never
|
||||||
// it's never sent.
|
// sent.
|
||||||
Events chan Event
|
Events chan Event
|
||||||
|
|
||||||
// Errors sends any errors.
|
// Errors sends any errors.
|
||||||
|
//
|
||||||
|
// ErrEventOverflow is used to indicate there are too many events:
|
||||||
|
//
|
||||||
|
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
|
||||||
|
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
|
||||||
|
// - kqueue, fen: Not used.
|
||||||
Errors chan error
|
Errors chan error
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
port *unix.EventPort
|
||||||
|
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
|
||||||
|
dirs map[string]struct{} // Explicitly watched directories
|
||||||
|
watches map[string]struct{} // Explicitly watched non-directories
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWatcher creates a new Watcher.
|
// NewWatcher creates a new Watcher.
|
||||||
func NewWatcher() (*Watcher, error) {
|
func NewWatcher() (*Watcher, error) {
|
||||||
return nil, errors.New("FEN based watcher not yet supported for fsnotify\n")
|
return NewBufferedWatcher(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close removes all watches and closes the events channel.
|
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
|
||||||
|
// channel.
|
||||||
|
//
|
||||||
|
// The main use case for this is situations with a very large number of events
|
||||||
|
// where the kernel buffer size can't be increased (e.g. due to lack of
|
||||||
|
// permissions). An unbuffered Watcher will perform better for almost all use
|
||||||
|
// cases, and whenever possible you will be better off increasing the kernel
|
||||||
|
// buffers instead of adding a large userspace buffer.
|
||||||
|
func NewBufferedWatcher(sz uint) (*Watcher, error) {
|
||||||
|
w := &Watcher{
|
||||||
|
Events: make(chan Event, sz),
|
||||||
|
Errors: make(chan error),
|
||||||
|
dirs: make(map[string]struct{}),
|
||||||
|
watches: make(map[string]struct{}),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
w.port, err = unix.NewEventPort()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fsnotify.NewWatcher: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go w.readEvents()
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendEvent attempts to send an event to the user, returning true if the event
|
||||||
|
// was put in the channel successfully and false if the watcher has been closed.
|
||||||
|
func (w *Watcher) sendEvent(name string, op Op) (sent bool) {
|
||||||
|
select {
|
||||||
|
case w.Events <- Event{Name: name, Op: op}:
|
||||||
|
return true
|
||||||
|
case <-w.done:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendError attempts to send an error to the user, returning true if the error
|
||||||
|
// was put in the channel successfully and false if the watcher has been closed.
|
||||||
|
func (w *Watcher) sendError(err error) (sent bool) {
|
||||||
|
select {
|
||||||
|
case w.Errors <- err:
|
||||||
|
return true
|
||||||
|
case <-w.done:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) isClosed() bool {
|
||||||
|
select {
|
||||||
|
case <-w.done:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close removes all watches and closes the Events channel.
|
||||||
func (w *Watcher) Close() error {
|
func (w *Watcher) Close() error {
|
||||||
return nil
|
// Take the lock used by associateFile to prevent lingering events from
|
||||||
|
// being processed after the close
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
if w.isClosed() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
close(w.done)
|
||||||
|
return w.port.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add starts monitoring the path for changes.
|
// Add starts monitoring the path for changes.
|
||||||
//
|
//
|
||||||
// A path can only be watched once; attempting to watch it more than once will
|
// A path can only be watched once; watching it more than once is a no-op and will
|
||||||
// return an error. Paths that do not yet exist on the filesystem cannot be
|
// not return an error. Paths that do not yet exist on the filesystem cannot be
|
||||||
// added. A watch will be automatically removed if the path is deleted.
|
// watched.
|
||||||
//
|
//
|
||||||
// A path will remain watched if it gets renamed to somewhere else on the same
|
// A watch will be automatically removed if the watched path is deleted or
|
||||||
// filesystem, but the monitor will get removed if the path gets deleted and
|
// renamed. The exception is the Windows backend, which doesn't remove the
|
||||||
// re-created, or if it's moved to a different filesystem.
|
// watcher on renames.
|
||||||
//
|
//
|
||||||
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
||||||
// filesystems (/proc, /sys, etc.) generally don't work.
|
// filesystems (/proc, /sys, etc.) generally don't work.
|
||||||
//
|
//
|
||||||
|
// Returns [ErrClosed] if [Watcher.Close] was called.
|
||||||
|
//
|
||||||
|
// See [Watcher.AddWith] for a version that allows adding options.
|
||||||
|
//
|
||||||
// # Watching directories
|
// # Watching directories
|
||||||
//
|
//
|
||||||
// All files in a directory are monitored, including new files that are created
|
// All files in a directory are monitored, including new files that are created
|
||||||
|
@ -139,15 +239,63 @@ func (w *Watcher) Close() error {
|
||||||
// # Watching files
|
// # Watching files
|
||||||
//
|
//
|
||||||
// Watching individual files (rather than directories) is generally not
|
// Watching individual files (rather than directories) is generally not
|
||||||
// recommended as many tools update files atomically. Instead of "just" writing
|
// recommended as many programs (especially editors) update files atomically: it
|
||||||
// to the file a temporary file will be written to first, and if successful the
|
// will write to a temporary file which is then moved to to destination,
|
||||||
// temporary file is moved to to destination removing the original, or some
|
// overwriting the original (or some variant thereof). The watcher on the
|
||||||
// variant thereof. The watcher on the original file is now lost, as it no
|
// original file is now lost, as that no longer exists.
|
||||||
// longer exists.
|
|
||||||
//
|
//
|
||||||
// Instead, watch the parent directory and use Event.Name to filter out files
|
// The upshot of this is that a power failure or crash won't leave a
|
||||||
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
|
// half-written file.
|
||||||
func (w *Watcher) Add(name string) error {
|
//
|
||||||
|
// Watch the parent directory and use Event.Name to filter out files you're not
|
||||||
|
// interested in. There is an example of this in cmd/fsnotify/file.go.
|
||||||
|
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
|
||||||
|
|
||||||
|
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
|
||||||
|
// the defaults described below are used.
|
||||||
|
//
|
||||||
|
// Possible options are:
|
||||||
|
//
|
||||||
|
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
|
||||||
|
// other platforms. The default is 64K (65536 bytes).
|
||||||
|
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
|
||||||
|
if w.isClosed() {
|
||||||
|
return ErrClosed
|
||||||
|
}
|
||||||
|
if w.port.PathIsWatched(name) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = getOptions(opts...)
|
||||||
|
|
||||||
|
// Currently we resolve symlinks that were explicitly requested to be
|
||||||
|
// watched. Otherwise we would use LStat here.
|
||||||
|
stat, err := os.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Associate all files in the directory.
|
||||||
|
if stat.IsDir() {
|
||||||
|
err := w.handleDirectory(name, stat, true, w.associateFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
w.dirs[name] = struct{}{}
|
||||||
|
w.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.associateFile(name, stat, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
w.watches[name] = struct{}{}
|
||||||
|
w.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,6 +305,336 @@ func (w *Watcher) Add(name string) error {
|
||||||
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
||||||
//
|
//
|
||||||
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
func (w *Watcher) Remove(name string) error {
|
func (w *Watcher) Remove(name string) error {
|
||||||
|
if w.isClosed() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !w.port.PathIsWatched(name) {
|
||||||
|
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user has expressed an intent. Immediately remove this name from
|
||||||
|
// whichever watch list it might be in. If it's not in there the delete
|
||||||
|
// doesn't cause harm.
|
||||||
|
w.mu.Lock()
|
||||||
|
delete(w.watches, name)
|
||||||
|
delete(w.dirs, name)
|
||||||
|
w.mu.Unlock()
|
||||||
|
|
||||||
|
stat, err := os.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove associations for every file in the directory.
|
||||||
|
if stat.IsDir() {
|
||||||
|
err := w.handleDirectory(name, stat, false, w.dissociateFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.port.DissociatePath(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readEvents contains the main loop that runs in a goroutine watching for events.
|
||||||
|
func (w *Watcher) readEvents() {
|
||||||
|
// If this function returns, the watcher has been closed and we can close
|
||||||
|
// these channels
|
||||||
|
defer func() {
|
||||||
|
close(w.Errors)
|
||||||
|
close(w.Events)
|
||||||
|
}()
|
||||||
|
|
||||||
|
pevents := make([]unix.PortEvent, 8)
|
||||||
|
for {
|
||||||
|
count, err := w.port.Get(pevents, 1, nil)
|
||||||
|
if err != nil && err != unix.ETIME {
|
||||||
|
// Interrupted system call (count should be 0) ignore and continue
|
||||||
|
if errors.Is(err, unix.EINTR) && count == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Get failed because we called w.Close()
|
||||||
|
if errors.Is(err, unix.EBADF) && w.isClosed() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// There was an error not caused by calling w.Close()
|
||||||
|
if !w.sendError(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p := pevents[:count]
|
||||||
|
for _, pevent := range p {
|
||||||
|
if pevent.Source != unix.PORT_SOURCE_FILE {
|
||||||
|
// Event from unexpected source received; should never happen.
|
||||||
|
if !w.sendError(errors.New("Event from unexpected source received")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.handleEvent(&pevent)
|
||||||
|
if err != nil {
|
||||||
|
if !w.sendError(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error {
|
||||||
|
files, err := os.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle all children of the directory.
|
||||||
|
for _, entry := range files {
|
||||||
|
finfo, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = handler(filepath.Join(path, finfo.Name()), finfo, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// And finally handle the directory itself.
|
||||||
|
return handler(path, stat, follow)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleEvent might need to emit more than one fsnotify event if the events
|
||||||
|
// bitmap matches more than one event type (e.g. the file was both modified and
|
||||||
|
// had the attributes changed between when the association was created and the
|
||||||
|
// when event was returned)
|
||||||
|
func (w *Watcher) handleEvent(event *unix.PortEvent) error {
|
||||||
|
var (
|
||||||
|
events = event.Events
|
||||||
|
path = event.Path
|
||||||
|
fmode = event.Cookie.(os.FileMode)
|
||||||
|
reRegister = true
|
||||||
|
)
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
_, watchedDir := w.dirs[path]
|
||||||
|
_, watchedPath := w.watches[path]
|
||||||
|
w.mu.Unlock()
|
||||||
|
isWatched := watchedDir || watchedPath
|
||||||
|
|
||||||
|
if events&unix.FILE_DELETE != 0 {
|
||||||
|
if !w.sendEvent(path, Remove) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
reRegister = false
|
||||||
|
}
|
||||||
|
if events&unix.FILE_RENAME_FROM != 0 {
|
||||||
|
if !w.sendEvent(path, Rename) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Don't keep watching the new file name
|
||||||
|
reRegister = false
|
||||||
|
}
|
||||||
|
if events&unix.FILE_RENAME_TO != 0 {
|
||||||
|
// We don't report a Rename event for this case, because Rename events
|
||||||
|
// are interpreted as referring to the _old_ name of the file, and in
|
||||||
|
// this case the event would refer to the new name of the file. This
|
||||||
|
// type of rename event is not supported by fsnotify.
|
||||||
|
|
||||||
|
// inotify reports a Remove event in this case, so we simulate this
|
||||||
|
// here.
|
||||||
|
if !w.sendEvent(path, Remove) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Don't keep watching the file that was removed
|
||||||
|
reRegister = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// The file is gone, nothing left to do.
|
||||||
|
if !reRegister {
|
||||||
|
if watchedDir {
|
||||||
|
w.mu.Lock()
|
||||||
|
delete(w.dirs, path)
|
||||||
|
w.mu.Unlock()
|
||||||
|
}
|
||||||
|
if watchedPath {
|
||||||
|
w.mu.Lock()
|
||||||
|
delete(w.watches, path)
|
||||||
|
w.mu.Unlock()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't get a deletion the file still exists and we're going to have
|
||||||
|
// to watch it again. Let's Stat it now so that we can compare permissions
|
||||||
|
// and have what we need to continue watching the file
|
||||||
|
|
||||||
|
stat, err := os.Lstat(path)
|
||||||
|
if err != nil {
|
||||||
|
// This is unexpected, but we should still emit an event. This happens
|
||||||
|
// most often on "rm -r" of a subdirectory inside a watched directory We
|
||||||
|
// get a modify event of something happening inside, but by the time we
|
||||||
|
// get here, the sudirectory is already gone. Clearly we were watching
|
||||||
|
// this path but now it is gone. Let's tell the user that it was
|
||||||
|
// removed.
|
||||||
|
if !w.sendEvent(path, Remove) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Suppress extra write events on removed directories; they are not
|
||||||
|
// informative and can be confusing.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve symlinks that were explicitly watched as we would have at Add()
|
||||||
|
// time. this helps suppress spurious Chmod events on watched symlinks
|
||||||
|
if isWatched {
|
||||||
|
stat, err = os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
// The symlink still exists, but the target is gone. Report the
|
||||||
|
// Remove similar to above.
|
||||||
|
if !w.sendEvent(path, Remove) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Don't return the error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if events&unix.FILE_MODIFIED != 0 {
|
||||||
|
if fmode.IsDir() {
|
||||||
|
if watchedDir {
|
||||||
|
if err := w.updateDirectory(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !w.sendEvent(path, Write) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !w.sendEvent(path, Write) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if events&unix.FILE_ATTRIB != 0 && stat != nil {
|
||||||
|
// Only send Chmod if perms changed
|
||||||
|
if stat.Mode().Perm() != fmode.Perm() {
|
||||||
|
if !w.sendEvent(path, Chmod) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat != nil {
|
||||||
|
// If we get here, it means we've hit an event above that requires us to
|
||||||
|
// continue watching the file or directory
|
||||||
|
return w.associateFile(path, stat, isWatched)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) updateDirectory(path string) error {
|
||||||
|
// The directory was modified, so we must find unwatched entities and watch
|
||||||
|
// them. If something was removed from the directory, nothing will happen,
|
||||||
|
// as everything else should still be watched.
|
||||||
|
files, err := os.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range files {
|
||||||
|
path := filepath.Join(path, entry.Name())
|
||||||
|
if w.port.PathIsWatched(path) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
finfo, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = w.associateFile(path, finfo, false)
|
||||||
|
if err != nil {
|
||||||
|
if !w.sendError(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !w.sendEvent(path, Create) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) associateFile(path string, stat os.FileInfo, follow bool) error {
|
||||||
|
if w.isClosed() {
|
||||||
|
return ErrClosed
|
||||||
|
}
|
||||||
|
// This is primarily protecting the call to AssociatePath but it is
|
||||||
|
// important and intentional that the call to PathIsWatched is also
|
||||||
|
// protected by this mutex. Without this mutex, AssociatePath has been seen
|
||||||
|
// to error out that the path is already associated.
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
if w.port.PathIsWatched(path) {
|
||||||
|
// Remove the old association in favor of this one If we get ENOENT,
|
||||||
|
// then while the x/sys/unix wrapper still thought that this path was
|
||||||
|
// associated, the underlying event port did not. This call will have
|
||||||
|
// cleared up that discrepancy. The most likely cause is that the event
|
||||||
|
// has fired but we haven't processed it yet.
|
||||||
|
err := w.port.DissociatePath(path)
|
||||||
|
if err != nil && err != unix.ENOENT {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// FILE_NOFOLLOW means we watch symlinks themselves rather than their
|
||||||
|
// targets.
|
||||||
|
events := unix.FILE_MODIFIED | unix.FILE_ATTRIB | unix.FILE_NOFOLLOW
|
||||||
|
if follow {
|
||||||
|
// We *DO* follow symlinks for explicitly watched entries.
|
||||||
|
events = unix.FILE_MODIFIED | unix.FILE_ATTRIB
|
||||||
|
}
|
||||||
|
return w.port.AssociatePath(path, stat,
|
||||||
|
events,
|
||||||
|
stat.Mode())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) dissociateFile(path string, stat os.FileInfo, unused bool) error {
|
||||||
|
if !w.port.PathIsWatched(path) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return w.port.DissociatePath(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
|
||||||
|
// yet removed).
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
|
func (w *Watcher) WatchList() []string {
|
||||||
|
if w.isClosed() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
entries := make([]string, 0, len(w.watches)+len(w.dirs))
|
||||||
|
for pathname := range w.dirs {
|
||||||
|
entries = append(entries, pathname)
|
||||||
|
}
|
||||||
|
for pathname := range w.watches {
|
||||||
|
entries = append(entries, pathname)
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
//go:build linux
|
//go:build linux && !appengine
|
||||||
// +build linux
|
// +build linux,!appengine
|
||||||
|
|
||||||
|
// Note: the documentation on the Watcher type and methods is generated from
|
||||||
|
// mkdoc.zsh
|
||||||
|
|
||||||
package fsnotify
|
package fsnotify
|
||||||
|
|
||||||
|
@ -26,9 +29,9 @@ import (
|
||||||
// When a file is removed a Remove event won't be emitted until all file
|
// When a file is removed a Remove event won't be emitted until all file
|
||||||
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
||||||
//
|
//
|
||||||
// fp := os.Open("file")
|
// fp := os.Open("file")
|
||||||
// os.Remove("file") // Triggers Chmod
|
// os.Remove("file") // Triggers Chmod
|
||||||
// fp.Close() // Triggers Remove
|
// fp.Close() // Triggers Remove
|
||||||
//
|
//
|
||||||
// This is the event that inotify sends, so not much can be changed about this.
|
// This is the event that inotify sends, so not much can be changed about this.
|
||||||
//
|
//
|
||||||
|
@ -42,16 +45,16 @@ import (
|
||||||
//
|
//
|
||||||
// To increase them you can use sysctl or write the value to the /proc file:
|
// To increase them you can use sysctl or write the value to the /proc file:
|
||||||
//
|
//
|
||||||
// # Default values on Linux 5.18
|
// # Default values on Linux 5.18
|
||||||
// sysctl fs.inotify.max_user_watches=124983
|
// sysctl fs.inotify.max_user_watches=124983
|
||||||
// sysctl fs.inotify.max_user_instances=128
|
// sysctl fs.inotify.max_user_instances=128
|
||||||
//
|
//
|
||||||
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
||||||
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
||||||
// your distro's documentation):
|
// your distro's documentation):
|
||||||
//
|
//
|
||||||
// fs.inotify.max_user_watches=124983
|
// fs.inotify.max_user_watches=124983
|
||||||
// fs.inotify.max_user_instances=128
|
// fs.inotify.max_user_instances=128
|
||||||
//
|
//
|
||||||
// Reaching the limit will result in a "no space left on device" or "too many open
|
// Reaching the limit will result in a "no space left on device" or "too many open
|
||||||
// files" error.
|
// files" error.
|
||||||
|
@ -67,14 +70,20 @@ import (
|
||||||
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
||||||
// systems.
|
// systems.
|
||||||
//
|
//
|
||||||
// # macOS notes
|
// # Windows notes
|
||||||
//
|
//
|
||||||
// Spotlight indexing on macOS can result in multiple events (see [#15]). A
|
// Paths can be added as "C:\path\to\dir", but forward slashes
|
||||||
// temporary workaround is to add your folder(s) to the "Spotlight Privacy
|
// ("C:/path/to/dir") will also work.
|
||||||
// Settings" until we have a native FSEvents implementation (see [#11]).
|
|
||||||
//
|
//
|
||||||
// [#11]: https://github.com/fsnotify/fsnotify/issues/11
|
// When a watched directory is removed it will always send an event for the
|
||||||
// [#15]: https://github.com/fsnotify/fsnotify/issues/15
|
// directory itself, but may not send events for all files in that directory.
|
||||||
|
// Sometimes it will send events for all times, sometimes it will send no
|
||||||
|
// events, and often only for some files.
|
||||||
|
//
|
||||||
|
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
|
||||||
|
// value that is guaranteed to work with SMB filesystems. If you have many
|
||||||
|
// events in quick succession this may not be enough, and you will have to use
|
||||||
|
// [WithBufferSize] to increase the value.
|
||||||
type Watcher struct {
|
type Watcher struct {
|
||||||
// Events sends the filesystem change events.
|
// Events sends the filesystem change events.
|
||||||
//
|
//
|
||||||
|
@ -101,36 +110,148 @@ type Watcher struct {
|
||||||
// initiated by the user may show up as one or multiple
|
// initiated by the user may show up as one or multiple
|
||||||
// writes, depending on when the system syncs things to
|
// writes, depending on when the system syncs things to
|
||||||
// disk. For example when compiling a large Go program
|
// disk. For example when compiling a large Go program
|
||||||
// you may get hundreds of Write events, so you
|
// you may get hundreds of Write events, and you may
|
||||||
// probably want to wait until you've stopped receiving
|
// want to wait until you've stopped receiving them
|
||||||
// them (see the dedup example in cmd/fsnotify).
|
// (see the dedup example in cmd/fsnotify).
|
||||||
|
//
|
||||||
|
// Some systems may send Write event for directories
|
||||||
|
// when the directory content changes.
|
||||||
//
|
//
|
||||||
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
||||||
// when a file is removed (or more accurately, when a
|
// when a file is removed (or more accurately, when a
|
||||||
// link to an inode is removed). On kqueue it's sent
|
// link to an inode is removed). On kqueue it's sent
|
||||||
// and on kqueue when a file is truncated. On Windows
|
// when a file is truncated. On Windows it's never
|
||||||
// it's never sent.
|
// sent.
|
||||||
Events chan Event
|
Events chan Event
|
||||||
|
|
||||||
// Errors sends any errors.
|
// Errors sends any errors.
|
||||||
|
//
|
||||||
|
// ErrEventOverflow is used to indicate there are too many events:
|
||||||
|
//
|
||||||
|
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
|
||||||
|
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
|
||||||
|
// - kqueue, fen: Not used.
|
||||||
Errors chan error
|
Errors chan error
|
||||||
|
|
||||||
// Store fd here as os.File.Read() will no longer return on close after
|
// Store fd here as os.File.Read() will no longer return on close after
|
||||||
// calling Fd(). See: https://github.com/golang/go/issues/26439
|
// calling Fd(). See: https://github.com/golang/go/issues/26439
|
||||||
fd int
|
fd int
|
||||||
mu sync.Mutex // Map access
|
|
||||||
inotifyFile *os.File
|
inotifyFile *os.File
|
||||||
watches map[string]*watch // Map of inotify watches (key: path)
|
watches *watches
|
||||||
paths map[int]string // Map of watched paths (key: watch descriptor)
|
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
|
||||||
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
|
closeMu sync.Mutex
|
||||||
doneResp chan struct{} // Channel to respond to Close
|
doneResp chan struct{} // Channel to respond to Close
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
watches struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
wd map[uint32]*watch // wd → watch
|
||||||
|
path map[string]uint32 // pathname → wd
|
||||||
|
}
|
||||||
|
watch struct {
|
||||||
|
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
|
||||||
|
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
|
||||||
|
path string // Watch path.
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func newWatches() *watches {
|
||||||
|
return &watches{
|
||||||
|
wd: make(map[uint32]*watch),
|
||||||
|
path: make(map[string]uint32),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watches) len() int {
|
||||||
|
w.mu.RLock()
|
||||||
|
defer w.mu.RUnlock()
|
||||||
|
return len(w.wd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watches) add(ww *watch) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
w.wd[ww.wd] = ww
|
||||||
|
w.path[ww.path] = ww.wd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watches) remove(wd uint32) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
delete(w.path, w.wd[wd].path)
|
||||||
|
delete(w.wd, wd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watches) removePath(path string) (uint32, bool) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
wd, ok := w.path[path]
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(w.path, path)
|
||||||
|
delete(w.wd, wd)
|
||||||
|
|
||||||
|
return wd, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watches) byPath(path string) *watch {
|
||||||
|
w.mu.RLock()
|
||||||
|
defer w.mu.RUnlock()
|
||||||
|
return w.wd[w.path[path]]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watches) byWd(wd uint32) *watch {
|
||||||
|
w.mu.RLock()
|
||||||
|
defer w.mu.RUnlock()
|
||||||
|
return w.wd[wd]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watches) updatePath(path string, f func(*watch) (*watch, error)) error {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
var existing *watch
|
||||||
|
wd, ok := w.path[path]
|
||||||
|
if ok {
|
||||||
|
existing = w.wd[wd]
|
||||||
|
}
|
||||||
|
|
||||||
|
upd, err := f(existing)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if upd != nil {
|
||||||
|
w.wd[upd.wd] = upd
|
||||||
|
w.path[upd.path] = upd.wd
|
||||||
|
|
||||||
|
if upd.wd != wd {
|
||||||
|
delete(w.wd, wd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWatcher creates a new Watcher.
|
// NewWatcher creates a new Watcher.
|
||||||
func NewWatcher() (*Watcher, error) {
|
func NewWatcher() (*Watcher, error) {
|
||||||
// Create inotify fd
|
return NewBufferedWatcher(0)
|
||||||
// Need to set the FD to nonblocking mode in order for SetDeadline methods to work
|
}
|
||||||
// Otherwise, blocking i/o operations won't terminate on close
|
|
||||||
|
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
|
||||||
|
// channel.
|
||||||
|
//
|
||||||
|
// The main use case for this is situations with a very large number of events
|
||||||
|
// where the kernel buffer size can't be increased (e.g. due to lack of
|
||||||
|
// permissions). An unbuffered Watcher will perform better for almost all use
|
||||||
|
// cases, and whenever possible you will be better off increasing the kernel
|
||||||
|
// buffers instead of adding a large userspace buffer.
|
||||||
|
func NewBufferedWatcher(sz uint) (*Watcher, error) {
|
||||||
|
// Need to set nonblocking mode for SetDeadline to work, otherwise blocking
|
||||||
|
// I/O operations won't terminate on close.
|
||||||
fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
|
fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
|
||||||
if fd == -1 {
|
if fd == -1 {
|
||||||
return nil, errno
|
return nil, errno
|
||||||
|
@ -139,9 +260,8 @@ func NewWatcher() (*Watcher, error) {
|
||||||
w := &Watcher{
|
w := &Watcher{
|
||||||
fd: fd,
|
fd: fd,
|
||||||
inotifyFile: os.NewFile(uintptr(fd), ""),
|
inotifyFile: os.NewFile(uintptr(fd), ""),
|
||||||
watches: make(map[string]*watch),
|
watches: newWatches(),
|
||||||
paths: make(map[int]string),
|
Events: make(chan Event, sz),
|
||||||
Events: make(chan Event),
|
|
||||||
Errors: make(chan error),
|
Errors: make(chan error),
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
doneResp: make(chan struct{}),
|
doneResp: make(chan struct{}),
|
||||||
|
@ -157,8 +277,8 @@ func (w *Watcher) sendEvent(e Event) bool {
|
||||||
case w.Events <- e:
|
case w.Events <- e:
|
||||||
return true
|
return true
|
||||||
case <-w.done:
|
case <-w.done:
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns true if the error was sent, or false if watcher is closed.
|
// Returns true if the error was sent, or false if watcher is closed.
|
||||||
|
@ -180,17 +300,15 @@ func (w *Watcher) isClosed() bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close removes all watches and closes the events channel.
|
// Close removes all watches and closes the Events channel.
|
||||||
func (w *Watcher) Close() error {
|
func (w *Watcher) Close() error {
|
||||||
w.mu.Lock()
|
w.closeMu.Lock()
|
||||||
if w.isClosed() {
|
if w.isClosed() {
|
||||||
w.mu.Unlock()
|
w.closeMu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send 'close' signal to goroutine, and set the Watcher to closed.
|
|
||||||
close(w.done)
|
close(w.done)
|
||||||
w.mu.Unlock()
|
w.closeMu.Unlock()
|
||||||
|
|
||||||
// Causes any blocking reads to return with an error, provided the file
|
// Causes any blocking reads to return with an error, provided the file
|
||||||
// still supports deadline operations.
|
// still supports deadline operations.
|
||||||
|
@ -207,17 +325,21 @@ func (w *Watcher) Close() error {
|
||||||
|
|
||||||
// Add starts monitoring the path for changes.
|
// Add starts monitoring the path for changes.
|
||||||
//
|
//
|
||||||
// A path can only be watched once; attempting to watch it more than once will
|
// A path can only be watched once; watching it more than once is a no-op and will
|
||||||
// return an error. Paths that do not yet exist on the filesystem cannot be
|
// not return an error. Paths that do not yet exist on the filesystem cannot be
|
||||||
// added. A watch will be automatically removed if the path is deleted.
|
// watched.
|
||||||
//
|
//
|
||||||
// A path will remain watched if it gets renamed to somewhere else on the same
|
// A watch will be automatically removed if the watched path is deleted or
|
||||||
// filesystem, but the monitor will get removed if the path gets deleted and
|
// renamed. The exception is the Windows backend, which doesn't remove the
|
||||||
// re-created, or if it's moved to a different filesystem.
|
// watcher on renames.
|
||||||
//
|
//
|
||||||
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
||||||
// filesystems (/proc, /sys, etc.) generally don't work.
|
// filesystems (/proc, /sys, etc.) generally don't work.
|
||||||
//
|
//
|
||||||
|
// Returns [ErrClosed] if [Watcher.Close] was called.
|
||||||
|
//
|
||||||
|
// See [Watcher.AddWith] for a version that allows adding options.
|
||||||
|
//
|
||||||
// # Watching directories
|
// # Watching directories
|
||||||
//
|
//
|
||||||
// All files in a directory are monitored, including new files that are created
|
// All files in a directory are monitored, including new files that are created
|
||||||
|
@ -227,44 +349,59 @@ func (w *Watcher) Close() error {
|
||||||
// # Watching files
|
// # Watching files
|
||||||
//
|
//
|
||||||
// Watching individual files (rather than directories) is generally not
|
// Watching individual files (rather than directories) is generally not
|
||||||
// recommended as many tools update files atomically. Instead of "just" writing
|
// recommended as many programs (especially editors) update files atomically: it
|
||||||
// to the file a temporary file will be written to first, and if successful the
|
// will write to a temporary file which is then moved to to destination,
|
||||||
// temporary file is moved to to destination removing the original, or some
|
// overwriting the original (or some variant thereof). The watcher on the
|
||||||
// variant thereof. The watcher on the original file is now lost, as it no
|
// original file is now lost, as that no longer exists.
|
||||||
// longer exists.
|
|
||||||
//
|
//
|
||||||
// Instead, watch the parent directory and use Event.Name to filter out files
|
// The upshot of this is that a power failure or crash won't leave a
|
||||||
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
|
// half-written file.
|
||||||
func (w *Watcher) Add(name string) error {
|
//
|
||||||
name = filepath.Clean(name)
|
// Watch the parent directory and use Event.Name to filter out files you're not
|
||||||
|
// interested in. There is an example of this in cmd/fsnotify/file.go.
|
||||||
|
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
|
||||||
|
|
||||||
|
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
|
||||||
|
// the defaults described below are used.
|
||||||
|
//
|
||||||
|
// Possible options are:
|
||||||
|
//
|
||||||
|
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
|
||||||
|
// other platforms. The default is 64K (65536 bytes).
|
||||||
|
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
|
||||||
if w.isClosed() {
|
if w.isClosed() {
|
||||||
return errors.New("inotify instance already closed")
|
return ErrClosed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
name = filepath.Clean(name)
|
||||||
|
_ = getOptions(opts...)
|
||||||
|
|
||||||
var flags uint32 = unix.IN_MOVED_TO | unix.IN_MOVED_FROM |
|
var flags uint32 = unix.IN_MOVED_TO | unix.IN_MOVED_FROM |
|
||||||
unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY |
|
unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY |
|
||||||
unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF
|
unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF
|
||||||
|
|
||||||
w.mu.Lock()
|
return w.watches.updatePath(name, func(existing *watch) (*watch, error) {
|
||||||
defer w.mu.Unlock()
|
if existing != nil {
|
||||||
watchEntry := w.watches[name]
|
flags |= existing.flags | unix.IN_MASK_ADD
|
||||||
if watchEntry != nil {
|
}
|
||||||
flags |= watchEntry.flags | unix.IN_MASK_ADD
|
|
||||||
}
|
|
||||||
wd, errno := unix.InotifyAddWatch(w.fd, name, flags)
|
|
||||||
if wd == -1 {
|
|
||||||
return errno
|
|
||||||
}
|
|
||||||
|
|
||||||
if watchEntry == nil {
|
wd, err := unix.InotifyAddWatch(w.fd, name, flags)
|
||||||
w.watches[name] = &watch{wd: uint32(wd), flags: flags}
|
if wd == -1 {
|
||||||
w.paths[wd] = name
|
return nil, err
|
||||||
} else {
|
}
|
||||||
watchEntry.wd = uint32(wd)
|
|
||||||
watchEntry.flags = flags
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
if existing == nil {
|
||||||
|
return &watch{
|
||||||
|
wd: uint32(wd),
|
||||||
|
path: name,
|
||||||
|
flags: flags,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.wd = uint32(wd)
|
||||||
|
existing.flags = flags
|
||||||
|
return existing, nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove stops monitoring the path for changes.
|
// Remove stops monitoring the path for changes.
|
||||||
|
@ -273,32 +410,22 @@ func (w *Watcher) Add(name string) error {
|
||||||
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
||||||
//
|
//
|
||||||
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
func (w *Watcher) Remove(name string) error {
|
func (w *Watcher) Remove(name string) error {
|
||||||
name = filepath.Clean(name)
|
if w.isClosed() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return w.remove(filepath.Clean(name))
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch the watch.
|
func (w *Watcher) remove(name string) error {
|
||||||
w.mu.Lock()
|
wd, ok := w.watches.removePath(name)
|
||||||
defer w.mu.Unlock()
|
|
||||||
watch, ok := w.watches[name]
|
|
||||||
|
|
||||||
// Remove it from inotify.
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
|
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We successfully removed the watch if InotifyRmWatch doesn't return an
|
success, errno := unix.InotifyRmWatch(w.fd, wd)
|
||||||
// error, we need to clean up our internal state to ensure it matches
|
|
||||||
// inotify's kernel state.
|
|
||||||
delete(w.paths, int(watch.wd))
|
|
||||||
delete(w.watches, name)
|
|
||||||
|
|
||||||
// inotify_rm_watch will return EINVAL if the file has been deleted;
|
|
||||||
// the inotify will already have been removed.
|
|
||||||
// watches and pathes are deleted in ignoreLinux() implicitly and asynchronously
|
|
||||||
// by calling inotify_rm_watch() below. e.g. readEvents() goroutine receives IN_IGNORE
|
|
||||||
// so that EINVAL means that the wd is being rm_watch()ed or its file removed
|
|
||||||
// by another thread and we have not received IN_IGNORE event.
|
|
||||||
success, errno := unix.InotifyRmWatch(w.fd, watch.wd)
|
|
||||||
if success == -1 {
|
if success == -1 {
|
||||||
// TODO: Perhaps it's not helpful to return an error here in every case;
|
// TODO: Perhaps it's not helpful to return an error here in every case;
|
||||||
// The only two possible errors are:
|
// The only two possible errors are:
|
||||||
|
@ -312,26 +439,26 @@ func (w *Watcher) Remove(name string) error {
|
||||||
// are watching is deleted.
|
// are watching is deleted.
|
||||||
return errno
|
return errno
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WatchList returns all paths added with [Add] (and are not yet removed).
|
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
|
||||||
|
// yet removed).
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
func (w *Watcher) WatchList() []string {
|
func (w *Watcher) WatchList() []string {
|
||||||
w.mu.Lock()
|
if w.isClosed() {
|
||||||
defer w.mu.Unlock()
|
return nil
|
||||||
|
|
||||||
entries := make([]string, 0, len(w.watches))
|
|
||||||
for pathname := range w.watches {
|
|
||||||
entries = append(entries, pathname)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries
|
entries := make([]string, 0, w.watches.len())
|
||||||
}
|
w.watches.mu.RLock()
|
||||||
|
for pathname := range w.watches.path {
|
||||||
|
entries = append(entries, pathname)
|
||||||
|
}
|
||||||
|
w.watches.mu.RUnlock()
|
||||||
|
|
||||||
type watch struct {
|
return entries
|
||||||
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
|
|
||||||
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// readEvents reads from the inotify file descriptor, converts the
|
// readEvents reads from the inotify file descriptor, converts the
|
||||||
|
@ -367,14 +494,11 @@ func (w *Watcher) readEvents() {
|
||||||
if n < unix.SizeofInotifyEvent {
|
if n < unix.SizeofInotifyEvent {
|
||||||
var err error
|
var err error
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
// If EOF is received. This should really never happen.
|
err = io.EOF // If EOF is received. This should really never happen.
|
||||||
err = io.EOF
|
|
||||||
} else if n < 0 {
|
} else if n < 0 {
|
||||||
// If an error occurred while reading.
|
err = errno // If an error occurred while reading.
|
||||||
err = errno
|
|
||||||
} else {
|
} else {
|
||||||
// Read was too short.
|
err = errors.New("notify: short read in readEvents()") // Read was too short.
|
||||||
err = errors.New("notify: short read in readEvents()")
|
|
||||||
}
|
}
|
||||||
if !w.sendError(err) {
|
if !w.sendError(err) {
|
||||||
return
|
return
|
||||||
|
@ -403,18 +527,29 @@ func (w *Watcher) readEvents() {
|
||||||
// doesn't append the filename to the event, but we would like to always fill the
|
// doesn't append the filename to the event, but we would like to always fill the
|
||||||
// the "Name" field with a valid filename. We retrieve the path of the watch from
|
// the "Name" field with a valid filename. We retrieve the path of the watch from
|
||||||
// the "paths" map.
|
// the "paths" map.
|
||||||
w.mu.Lock()
|
watch := w.watches.byWd(uint32(raw.Wd))
|
||||||
name, ok := w.paths[int(raw.Wd)]
|
|
||||||
// IN_DELETE_SELF occurs when the file/directory being watched is removed.
|
|
||||||
// This is a sign to clean up the maps, otherwise we are no longer in sync
|
|
||||||
// with the inotify kernel state which has already deleted the watch
|
|
||||||
// automatically.
|
|
||||||
if ok && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
|
|
||||||
delete(w.paths, int(raw.Wd))
|
|
||||||
delete(w.watches, name)
|
|
||||||
}
|
|
||||||
w.mu.Unlock()
|
|
||||||
|
|
||||||
|
// inotify will automatically remove the watch on deletes; just need
|
||||||
|
// to clean our state here.
|
||||||
|
if watch != nil && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
|
||||||
|
w.watches.remove(watch.wd)
|
||||||
|
}
|
||||||
|
// We can't really update the state when a watched path is moved;
|
||||||
|
// only IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove
|
||||||
|
// the watch.
|
||||||
|
if watch != nil && mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF {
|
||||||
|
err := w.remove(watch.path)
|
||||||
|
if err != nil && !errors.Is(err, ErrNonExistentWatch) {
|
||||||
|
if !w.sendError(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var name string
|
||||||
|
if watch != nil {
|
||||||
|
name = watch.path
|
||||||
|
}
|
||||||
if nameLen > 0 {
|
if nameLen > 0 {
|
||||||
// Point "bytes" at the first byte of the filename
|
// Point "bytes" at the first byte of the filename
|
||||||
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen]
|
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen]
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
//go:build freebsd || openbsd || netbsd || dragonfly || darwin
|
//go:build freebsd || openbsd || netbsd || dragonfly || darwin
|
||||||
// +build freebsd openbsd netbsd dragonfly darwin
|
// +build freebsd openbsd netbsd dragonfly darwin
|
||||||
|
|
||||||
|
// Note: the documentation on the Watcher type and methods is generated from
|
||||||
|
// mkdoc.zsh
|
||||||
|
|
||||||
package fsnotify
|
package fsnotify
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -24,9 +26,9 @@ import (
|
||||||
// When a file is removed a Remove event won't be emitted until all file
|
// When a file is removed a Remove event won't be emitted until all file
|
||||||
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
||||||
//
|
//
|
||||||
// fp := os.Open("file")
|
// fp := os.Open("file")
|
||||||
// os.Remove("file") // Triggers Chmod
|
// os.Remove("file") // Triggers Chmod
|
||||||
// fp.Close() // Triggers Remove
|
// fp.Close() // Triggers Remove
|
||||||
//
|
//
|
||||||
// This is the event that inotify sends, so not much can be changed about this.
|
// This is the event that inotify sends, so not much can be changed about this.
|
||||||
//
|
//
|
||||||
|
@ -40,16 +42,16 @@ import (
|
||||||
//
|
//
|
||||||
// To increase them you can use sysctl or write the value to the /proc file:
|
// To increase them you can use sysctl or write the value to the /proc file:
|
||||||
//
|
//
|
||||||
// # Default values on Linux 5.18
|
// # Default values on Linux 5.18
|
||||||
// sysctl fs.inotify.max_user_watches=124983
|
// sysctl fs.inotify.max_user_watches=124983
|
||||||
// sysctl fs.inotify.max_user_instances=128
|
// sysctl fs.inotify.max_user_instances=128
|
||||||
//
|
//
|
||||||
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
||||||
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
||||||
// your distro's documentation):
|
// your distro's documentation):
|
||||||
//
|
//
|
||||||
// fs.inotify.max_user_watches=124983
|
// fs.inotify.max_user_watches=124983
|
||||||
// fs.inotify.max_user_instances=128
|
// fs.inotify.max_user_instances=128
|
||||||
//
|
//
|
||||||
// Reaching the limit will result in a "no space left on device" or "too many open
|
// Reaching the limit will result in a "no space left on device" or "too many open
|
||||||
// files" error.
|
// files" error.
|
||||||
|
@ -65,14 +67,20 @@ import (
|
||||||
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
||||||
// systems.
|
// systems.
|
||||||
//
|
//
|
||||||
// # macOS notes
|
// # Windows notes
|
||||||
//
|
//
|
||||||
// Spotlight indexing on macOS can result in multiple events (see [#15]). A
|
// Paths can be added as "C:\path\to\dir", but forward slashes
|
||||||
// temporary workaround is to add your folder(s) to the "Spotlight Privacy
|
// ("C:/path/to/dir") will also work.
|
||||||
// Settings" until we have a native FSEvents implementation (see [#11]).
|
|
||||||
//
|
//
|
||||||
// [#11]: https://github.com/fsnotify/fsnotify/issues/11
|
// When a watched directory is removed it will always send an event for the
|
||||||
// [#15]: https://github.com/fsnotify/fsnotify/issues/15
|
// directory itself, but may not send events for all files in that directory.
|
||||||
|
// Sometimes it will send events for all times, sometimes it will send no
|
||||||
|
// events, and often only for some files.
|
||||||
|
//
|
||||||
|
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
|
||||||
|
// value that is guaranteed to work with SMB filesystems. If you have many
|
||||||
|
// events in quick succession this may not be enough, and you will have to use
|
||||||
|
// [WithBufferSize] to increase the value.
|
||||||
type Watcher struct {
|
type Watcher struct {
|
||||||
// Events sends the filesystem change events.
|
// Events sends the filesystem change events.
|
||||||
//
|
//
|
||||||
|
@ -99,18 +107,27 @@ type Watcher struct {
|
||||||
// initiated by the user may show up as one or multiple
|
// initiated by the user may show up as one or multiple
|
||||||
// writes, depending on when the system syncs things to
|
// writes, depending on when the system syncs things to
|
||||||
// disk. For example when compiling a large Go program
|
// disk. For example when compiling a large Go program
|
||||||
// you may get hundreds of Write events, so you
|
// you may get hundreds of Write events, and you may
|
||||||
// probably want to wait until you've stopped receiving
|
// want to wait until you've stopped receiving them
|
||||||
// them (see the dedup example in cmd/fsnotify).
|
// (see the dedup example in cmd/fsnotify).
|
||||||
|
//
|
||||||
|
// Some systems may send Write event for directories
|
||||||
|
// when the directory content changes.
|
||||||
//
|
//
|
||||||
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
||||||
// when a file is removed (or more accurately, when a
|
// when a file is removed (or more accurately, when a
|
||||||
// link to an inode is removed). On kqueue it's sent
|
// link to an inode is removed). On kqueue it's sent
|
||||||
// and on kqueue when a file is truncated. On Windows
|
// when a file is truncated. On Windows it's never
|
||||||
// it's never sent.
|
// sent.
|
||||||
Events chan Event
|
Events chan Event
|
||||||
|
|
||||||
// Errors sends any errors.
|
// Errors sends any errors.
|
||||||
|
//
|
||||||
|
// ErrEventOverflow is used to indicate there are too many events:
|
||||||
|
//
|
||||||
|
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
|
||||||
|
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
|
||||||
|
// - kqueue, fen: Not used.
|
||||||
Errors chan error
|
Errors chan error
|
||||||
|
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
|
@ -133,6 +150,18 @@ type pathInfo struct {
|
||||||
|
|
||||||
// NewWatcher creates a new Watcher.
|
// NewWatcher creates a new Watcher.
|
||||||
func NewWatcher() (*Watcher, error) {
|
func NewWatcher() (*Watcher, error) {
|
||||||
|
return NewBufferedWatcher(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
|
||||||
|
// channel.
|
||||||
|
//
|
||||||
|
// The main use case for this is situations with a very large number of events
|
||||||
|
// where the kernel buffer size can't be increased (e.g. due to lack of
|
||||||
|
// permissions). An unbuffered Watcher will perform better for almost all use
|
||||||
|
// cases, and whenever possible you will be better off increasing the kernel
|
||||||
|
// buffers instead of adding a large userspace buffer.
|
||||||
|
func NewBufferedWatcher(sz uint) (*Watcher, error) {
|
||||||
kq, closepipe, err := newKqueue()
|
kq, closepipe, err := newKqueue()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -147,7 +176,7 @@ func NewWatcher() (*Watcher, error) {
|
||||||
paths: make(map[int]pathInfo),
|
paths: make(map[int]pathInfo),
|
||||||
fileExists: make(map[string]struct{}),
|
fileExists: make(map[string]struct{}),
|
||||||
userWatches: make(map[string]struct{}),
|
userWatches: make(map[string]struct{}),
|
||||||
Events: make(chan Event),
|
Events: make(chan Event, sz),
|
||||||
Errors: make(chan error),
|
Errors: make(chan error),
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
@ -197,8 +226,8 @@ func (w *Watcher) sendEvent(e Event) bool {
|
||||||
case w.Events <- e:
|
case w.Events <- e:
|
||||||
return true
|
return true
|
||||||
case <-w.done:
|
case <-w.done:
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns true if the error was sent, or false if watcher is closed.
|
// Returns true if the error was sent, or false if watcher is closed.
|
||||||
|
@ -207,11 +236,11 @@ func (w *Watcher) sendError(err error) bool {
|
||||||
case w.Errors <- err:
|
case w.Errors <- err:
|
||||||
return true
|
return true
|
||||||
case <-w.done:
|
case <-w.done:
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close removes all watches and closes the events channel.
|
// Close removes all watches and closes the Events channel.
|
||||||
func (w *Watcher) Close() error {
|
func (w *Watcher) Close() error {
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
if w.isClosed {
|
if w.isClosed {
|
||||||
|
@ -239,17 +268,21 @@ func (w *Watcher) Close() error {
|
||||||
|
|
||||||
// Add starts monitoring the path for changes.
|
// Add starts monitoring the path for changes.
|
||||||
//
|
//
|
||||||
// A path can only be watched once; attempting to watch it more than once will
|
// A path can only be watched once; watching it more than once is a no-op and will
|
||||||
// return an error. Paths that do not yet exist on the filesystem cannot be
|
// not return an error. Paths that do not yet exist on the filesystem cannot be
|
||||||
// added. A watch will be automatically removed if the path is deleted.
|
// watched.
|
||||||
//
|
//
|
||||||
// A path will remain watched if it gets renamed to somewhere else on the same
|
// A watch will be automatically removed if the watched path is deleted or
|
||||||
// filesystem, but the monitor will get removed if the path gets deleted and
|
// renamed. The exception is the Windows backend, which doesn't remove the
|
||||||
// re-created, or if it's moved to a different filesystem.
|
// watcher on renames.
|
||||||
//
|
//
|
||||||
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
||||||
// filesystems (/proc, /sys, etc.) generally don't work.
|
// filesystems (/proc, /sys, etc.) generally don't work.
|
||||||
//
|
//
|
||||||
|
// Returns [ErrClosed] if [Watcher.Close] was called.
|
||||||
|
//
|
||||||
|
// See [Watcher.AddWith] for a version that allows adding options.
|
||||||
|
//
|
||||||
// # Watching directories
|
// # Watching directories
|
||||||
//
|
//
|
||||||
// All files in a directory are monitored, including new files that are created
|
// All files in a directory are monitored, including new files that are created
|
||||||
|
@ -259,15 +292,28 @@ func (w *Watcher) Close() error {
|
||||||
// # Watching files
|
// # Watching files
|
||||||
//
|
//
|
||||||
// Watching individual files (rather than directories) is generally not
|
// Watching individual files (rather than directories) is generally not
|
||||||
// recommended as many tools update files atomically. Instead of "just" writing
|
// recommended as many programs (especially editors) update files atomically: it
|
||||||
// to the file a temporary file will be written to first, and if successful the
|
// will write to a temporary file which is then moved to to destination,
|
||||||
// temporary file is moved to to destination removing the original, or some
|
// overwriting the original (or some variant thereof). The watcher on the
|
||||||
// variant thereof. The watcher on the original file is now lost, as it no
|
// original file is now lost, as that no longer exists.
|
||||||
// longer exists.
|
|
||||||
//
|
//
|
||||||
// Instead, watch the parent directory and use Event.Name to filter out files
|
// The upshot of this is that a power failure or crash won't leave a
|
||||||
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
|
// half-written file.
|
||||||
func (w *Watcher) Add(name string) error {
|
//
|
||||||
|
// Watch the parent directory and use Event.Name to filter out files you're not
|
||||||
|
// interested in. There is an example of this in cmd/fsnotify/file.go.
|
||||||
|
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
|
||||||
|
|
||||||
|
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
|
||||||
|
// the defaults described below are used.
|
||||||
|
//
|
||||||
|
// Possible options are:
|
||||||
|
//
|
||||||
|
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
|
||||||
|
// other platforms. The default is 64K (65536 bytes).
|
||||||
|
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
|
||||||
|
_ = getOptions(opts...)
|
||||||
|
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
w.userWatches[name] = struct{}{}
|
w.userWatches[name] = struct{}{}
|
||||||
w.mu.Unlock()
|
w.mu.Unlock()
|
||||||
|
@ -281,9 +327,19 @@ func (w *Watcher) Add(name string) error {
|
||||||
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
||||||
//
|
//
|
||||||
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
func (w *Watcher) Remove(name string) error {
|
func (w *Watcher) Remove(name string) error {
|
||||||
|
return w.remove(name, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) remove(name string, unwatchFiles bool) error {
|
||||||
name = filepath.Clean(name)
|
name = filepath.Clean(name)
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
|
if w.isClosed {
|
||||||
|
w.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
watchfd, ok := w.watches[name]
|
watchfd, ok := w.watches[name]
|
||||||
w.mu.Unlock()
|
w.mu.Unlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -315,7 +371,7 @@ func (w *Watcher) Remove(name string) error {
|
||||||
w.mu.Unlock()
|
w.mu.Unlock()
|
||||||
|
|
||||||
// Find all watched paths that are in this directory that are not external.
|
// Find all watched paths that are in this directory that are not external.
|
||||||
if isDir {
|
if unwatchFiles && isDir {
|
||||||
var pathsToRemove []string
|
var pathsToRemove []string
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
for fd := range w.watchesByDir[name] {
|
for fd := range w.watchesByDir[name] {
|
||||||
|
@ -326,20 +382,25 @@ func (w *Watcher) Remove(name string) error {
|
||||||
}
|
}
|
||||||
w.mu.Unlock()
|
w.mu.Unlock()
|
||||||
for _, name := range pathsToRemove {
|
for _, name := range pathsToRemove {
|
||||||
// Since these are internal, not much sense in propagating error
|
// Since these are internal, not much sense in propagating error to
|
||||||
// to the user, as that will just confuse them with an error about
|
// the user, as that will just confuse them with an error about a
|
||||||
// a path they did not explicitly watch themselves.
|
// path they did not explicitly watch themselves.
|
||||||
w.Remove(name)
|
w.Remove(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WatchList returns all paths added with [Add] (and are not yet removed).
|
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
|
||||||
|
// yet removed).
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
func (w *Watcher) WatchList() []string {
|
func (w *Watcher) WatchList() []string {
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
defer w.mu.Unlock()
|
defer w.mu.Unlock()
|
||||||
|
if w.isClosed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
entries := make([]string, 0, len(w.userWatches))
|
entries := make([]string, 0, len(w.userWatches))
|
||||||
for pathname := range w.userWatches {
|
for pathname := range w.userWatches {
|
||||||
|
@ -352,18 +413,18 @@ func (w *Watcher) WatchList() []string {
|
||||||
// Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE)
|
// Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE)
|
||||||
const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME
|
const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME
|
||||||
|
|
||||||
// addWatch adds name to the watched file set.
|
// addWatch adds name to the watched file set; the flags are interpreted as
|
||||||
// The flags are interpreted as described in kevent(2).
|
// described in kevent(2).
|
||||||
// Returns the real path to the file which was added, if any, which may be different from the one passed in the case of symlinks.
|
//
|
||||||
|
// Returns the real path to the file which was added, with symlinks resolved.
|
||||||
func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
|
func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
|
||||||
var isDir bool
|
var isDir bool
|
||||||
// Make ./name and name equivalent
|
|
||||||
name = filepath.Clean(name)
|
name = filepath.Clean(name)
|
||||||
|
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
if w.isClosed {
|
if w.isClosed {
|
||||||
w.mu.Unlock()
|
w.mu.Unlock()
|
||||||
return "", errors.New("kevent instance already closed")
|
return "", ErrClosed
|
||||||
}
|
}
|
||||||
watchfd, alreadyWatching := w.watches[name]
|
watchfd, alreadyWatching := w.watches[name]
|
||||||
// We already have a watch, but we can still override flags.
|
// We already have a watch, but we can still override flags.
|
||||||
|
@ -383,27 +444,30 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Follow Symlinks
|
// Follow Symlinks.
|
||||||
//
|
|
||||||
// Linux can add unresolvable symlinks to the watch list without issue,
|
|
||||||
// and Windows can't do symlinks period. To maintain consistency, we
|
|
||||||
// will act like everything is fine if the link can't be resolved.
|
|
||||||
// There will simply be no file events for broken symlinks. Hence the
|
|
||||||
// returns of nil on errors.
|
|
||||||
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||||
name, err = filepath.EvalSymlinks(name)
|
link, err := os.Readlink(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Return nil because Linux can add unresolvable symlinks to the
|
||||||
|
// watch list without problems, so maintain consistency with
|
||||||
|
// that. There will be no file events for broken symlinks.
|
||||||
|
// TODO: more specific check; returns os.PathError; ENOENT?
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
_, alreadyWatching = w.watches[name]
|
_, alreadyWatching = w.watches[link]
|
||||||
w.mu.Unlock()
|
w.mu.Unlock()
|
||||||
|
|
||||||
if alreadyWatching {
|
if alreadyWatching {
|
||||||
return name, nil
|
// Add to watches so we don't get spurious Create events later
|
||||||
|
// on when we diff the directories.
|
||||||
|
w.watches[name] = 0
|
||||||
|
w.fileExists[name] = struct{}{}
|
||||||
|
return link, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
name = link
|
||||||
fi, err = os.Lstat(name)
|
fi, err = os.Lstat(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil
|
return "", nil
|
||||||
|
@ -411,7 +475,7 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry on EINTR; open() can return EINTR in practice on macOS.
|
// Retry on EINTR; open() can return EINTR in practice on macOS.
|
||||||
// See #354, and go issues 11180 and 39237.
|
// See #354, and Go issues 11180 and 39237.
|
||||||
for {
|
for {
|
||||||
watchfd, err = unix.Open(name, openMode, 0)
|
watchfd, err = unix.Open(name, openMode, 0)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -444,14 +508,13 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
|
||||||
w.watchesByDir[parentName] = watchesByDir
|
w.watchesByDir[parentName] = watchesByDir
|
||||||
}
|
}
|
||||||
watchesByDir[watchfd] = struct{}{}
|
watchesByDir[watchfd] = struct{}{}
|
||||||
|
|
||||||
w.paths[watchfd] = pathInfo{name: name, isDir: isDir}
|
w.paths[watchfd] = pathInfo{name: name, isDir: isDir}
|
||||||
w.mu.Unlock()
|
w.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
if isDir {
|
if isDir {
|
||||||
// Watch the directory if it has not been watched before,
|
// Watch the directory if it has not been watched before, or if it was
|
||||||
// or if it was watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles)
|
// watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles)
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
|
|
||||||
watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE &&
|
watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE &&
|
||||||
|
@ -473,13 +536,10 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
|
||||||
// Event values that it sends down the Events channel.
|
// Event values that it sends down the Events channel.
|
||||||
func (w *Watcher) readEvents() {
|
func (w *Watcher) readEvents() {
|
||||||
defer func() {
|
defer func() {
|
||||||
err := unix.Close(w.kq)
|
|
||||||
if err != nil {
|
|
||||||
w.Errors <- err
|
|
||||||
}
|
|
||||||
unix.Close(w.closepipe[0])
|
|
||||||
close(w.Events)
|
close(w.Events)
|
||||||
close(w.Errors)
|
close(w.Errors)
|
||||||
|
_ = unix.Close(w.kq)
|
||||||
|
unix.Close(w.closepipe[0])
|
||||||
}()
|
}()
|
||||||
|
|
||||||
eventBuffer := make([]unix.Kevent_t, 10)
|
eventBuffer := make([]unix.Kevent_t, 10)
|
||||||
|
@ -513,18 +573,8 @@ func (w *Watcher) readEvents() {
|
||||||
|
|
||||||
event := w.newEvent(path.name, mask)
|
event := w.newEvent(path.name, mask)
|
||||||
|
|
||||||
if path.isDir && !event.Has(Remove) {
|
|
||||||
// Double check to make sure the directory exists. This can
|
|
||||||
// happen when we do a rm -fr on a recursively watched folders
|
|
||||||
// and we receive a modification event first but the folder has
|
|
||||||
// been deleted and later receive the delete event.
|
|
||||||
if _, err := os.Lstat(event.Name); os.IsNotExist(err) {
|
|
||||||
event.Op |= Remove
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.Has(Rename) || event.Has(Remove) {
|
if event.Has(Rename) || event.Has(Remove) {
|
||||||
w.Remove(event.Name)
|
w.remove(event.Name, false)
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
delete(w.fileExists, event.Name)
|
delete(w.fileExists, event.Name)
|
||||||
w.mu.Unlock()
|
w.mu.Unlock()
|
||||||
|
@ -540,26 +590,30 @@ func (w *Watcher) readEvents() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.Has(Remove) {
|
if event.Has(Remove) {
|
||||||
// Look for a file that may have overwritten this.
|
// Look for a file that may have overwritten this; for example,
|
||||||
// For example, mv f1 f2 will delete f2, then create f2.
|
// mv f1 f2 will delete f2, then create f2.
|
||||||
if path.isDir {
|
if path.isDir {
|
||||||
fileDir := filepath.Clean(event.Name)
|
fileDir := filepath.Clean(event.Name)
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
_, found := w.watches[fileDir]
|
_, found := w.watches[fileDir]
|
||||||
w.mu.Unlock()
|
w.mu.Unlock()
|
||||||
if found {
|
if found {
|
||||||
// make sure the directory exists before we watch for changes. When we
|
err := w.sendDirectoryChangeEvents(fileDir)
|
||||||
// do a recursive watch and perform rm -fr, the parent directory might
|
if err != nil {
|
||||||
// have gone missing, ignore the missing directory and let the
|
if !w.sendError(err) {
|
||||||
// upcoming delete event remove the watch from the parent directory.
|
closed = true
|
||||||
if _, err := os.Lstat(fileDir); err == nil {
|
}
|
||||||
w.sendDirectoryChangeEvents(fileDir)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
filePath := filepath.Clean(event.Name)
|
filePath := filepath.Clean(event.Name)
|
||||||
if fileInfo, err := os.Lstat(filePath); err == nil {
|
if fi, err := os.Lstat(filePath); err == nil {
|
||||||
w.sendFileCreatedEventIfNew(filePath, fileInfo)
|
err := w.sendFileCreatedEventIfNew(filePath, fi)
|
||||||
|
if err != nil {
|
||||||
|
if !w.sendError(err) {
|
||||||
|
closed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -582,21 +636,31 @@ func (w *Watcher) newEvent(name string, mask uint32) Event {
|
||||||
if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB {
|
if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB {
|
||||||
e.Op |= Chmod
|
e.Op |= Chmod
|
||||||
}
|
}
|
||||||
|
// No point sending a write and delete event at the same time: if it's gone,
|
||||||
|
// then it's gone.
|
||||||
|
if e.Op.Has(Write) && e.Op.Has(Remove) {
|
||||||
|
e.Op &^= Write
|
||||||
|
}
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
// watchDirectoryFiles to mimic inotify when adding a watch on a directory
|
// watchDirectoryFiles to mimic inotify when adding a watch on a directory
|
||||||
func (w *Watcher) watchDirectoryFiles(dirPath string) error {
|
func (w *Watcher) watchDirectoryFiles(dirPath string) error {
|
||||||
// Get all files
|
// Get all files
|
||||||
files, err := ioutil.ReadDir(dirPath)
|
files, err := os.ReadDir(dirPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fileInfo := range files {
|
for _, f := range files {
|
||||||
path := filepath.Join(dirPath, fileInfo.Name())
|
path := filepath.Join(dirPath, f.Name())
|
||||||
|
|
||||||
cleanPath, err := w.internalWatch(path, fileInfo)
|
fi, err := f.Info()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%q: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanPath, err := w.internalWatch(path, fi)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// No permission to read the file; that's not a problem: just skip.
|
// No permission to read the file; that's not a problem: just skip.
|
||||||
// But do add it to w.fileExists to prevent it from being picked up
|
// But do add it to w.fileExists to prevent it from being picked up
|
||||||
|
@ -606,7 +670,7 @@ func (w *Watcher) watchDirectoryFiles(dirPath string) error {
|
||||||
case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM):
|
case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM):
|
||||||
cleanPath = filepath.Clean(path)
|
cleanPath = filepath.Clean(path)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("%q: %w", filepath.Join(dirPath, fileInfo.Name()), err)
|
return fmt.Errorf("%q: %w", path, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -622,26 +686,37 @@ func (w *Watcher) watchDirectoryFiles(dirPath string) error {
|
||||||
//
|
//
|
||||||
// This functionality is to have the BSD watcher match the inotify, which sends
|
// This functionality is to have the BSD watcher match the inotify, which sends
|
||||||
// a create event for files created in a watched directory.
|
// a create event for files created in a watched directory.
|
||||||
func (w *Watcher) sendDirectoryChangeEvents(dir string) {
|
func (w *Watcher) sendDirectoryChangeEvents(dir string) error {
|
||||||
// Get all files
|
files, err := os.ReadDir(dir)
|
||||||
files, err := ioutil.ReadDir(dir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !w.sendError(fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)) {
|
// Directory no longer exists: we can ignore this safely. kqueue will
|
||||||
return
|
// still give us the correct events.
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search for new files
|
for _, f := range files {
|
||||||
for _, fi := range files {
|
fi, err := f.Info()
|
||||||
err := w.sendFileCreatedEventIfNew(filepath.Join(dir, fi.Name()), fi)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.sendFileCreatedEventIfNew(filepath.Join(dir, fi.Name()), fi)
|
||||||
|
if err != nil {
|
||||||
|
// Don't need to send an error if this file isn't readable.
|
||||||
|
if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendFileCreatedEvent sends a create event if the file isn't already being tracked.
|
// sendFileCreatedEvent sends a create event if the file isn't already being tracked.
|
||||||
func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInfo) (err error) {
|
func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fi os.FileInfo) (err error) {
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
_, doesExist := w.fileExists[filePath]
|
_, doesExist := w.fileExists[filePath]
|
||||||
w.mu.Unlock()
|
w.mu.Unlock()
|
||||||
|
@ -652,7 +727,7 @@ func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInf
|
||||||
}
|
}
|
||||||
|
|
||||||
// like watchDirectoryFiles (but without doing another ReadDir)
|
// like watchDirectoryFiles (but without doing another ReadDir)
|
||||||
filePath, err = w.internalWatch(filePath, fileInfo)
|
filePath, err = w.internalWatch(filePath, fi)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -664,10 +739,10 @@ func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInf
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Watcher) internalWatch(name string, fileInfo os.FileInfo) (string, error) {
|
func (w *Watcher) internalWatch(name string, fi os.FileInfo) (string, error) {
|
||||||
if fileInfo.IsDir() {
|
if fi.IsDir() {
|
||||||
// mimic Linux providing delete events for subdirectories
|
// mimic Linux providing delete events for subdirectories, but preserve
|
||||||
// but preserve the flags used if currently watching subdirectory
|
// the flags used if currently watching subdirectory
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
flags := w.dirFlags[name]
|
flags := w.dirFlags[name]
|
||||||
w.mu.Unlock()
|
w.mu.Unlock()
|
||||||
|
|
|
@ -1,39 +1,169 @@
|
||||||
//go:build !darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows
|
//go:build appengine || (!darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows)
|
||||||
// +build !darwin,!dragonfly,!freebsd,!openbsd,!linux,!netbsd,!solaris,!windows
|
// +build appengine !darwin,!dragonfly,!freebsd,!openbsd,!linux,!netbsd,!solaris,!windows
|
||||||
|
|
||||||
|
// Note: the documentation on the Watcher type and methods is generated from
|
||||||
|
// mkdoc.zsh
|
||||||
|
|
||||||
package fsnotify
|
package fsnotify
|
||||||
|
|
||||||
import (
|
import "errors"
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Watcher watches a set of files, delivering events to a channel.
|
// Watcher watches a set of paths, delivering events on a channel.
|
||||||
type Watcher struct{}
|
//
|
||||||
|
// A watcher should not be copied (e.g. pass it by pointer, rather than by
|
||||||
|
// value).
|
||||||
|
//
|
||||||
|
// # Linux notes
|
||||||
|
//
|
||||||
|
// When a file is removed a Remove event won't be emitted until all file
|
||||||
|
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
||||||
|
//
|
||||||
|
// fp := os.Open("file")
|
||||||
|
// os.Remove("file") // Triggers Chmod
|
||||||
|
// fp.Close() // Triggers Remove
|
||||||
|
//
|
||||||
|
// This is the event that inotify sends, so not much can be changed about this.
|
||||||
|
//
|
||||||
|
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
|
||||||
|
// for the number of watches per user, and fs.inotify.max_user_instances
|
||||||
|
// specifies the maximum number of inotify instances per user. Every Watcher you
|
||||||
|
// create is an "instance", and every path you add is a "watch".
|
||||||
|
//
|
||||||
|
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
|
||||||
|
// /proc/sys/fs/inotify/max_user_instances
|
||||||
|
//
|
||||||
|
// To increase them you can use sysctl or write the value to the /proc file:
|
||||||
|
//
|
||||||
|
// # Default values on Linux 5.18
|
||||||
|
// sysctl fs.inotify.max_user_watches=124983
|
||||||
|
// sysctl fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
||||||
|
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
||||||
|
// your distro's documentation):
|
||||||
|
//
|
||||||
|
// fs.inotify.max_user_watches=124983
|
||||||
|
// fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// Reaching the limit will result in a "no space left on device" or "too many open
|
||||||
|
// files" error.
|
||||||
|
//
|
||||||
|
// # kqueue notes (macOS, BSD)
|
||||||
|
//
|
||||||
|
// kqueue requires opening a file descriptor for every file that's being watched;
|
||||||
|
// so if you're watching a directory with five files then that's six file
|
||||||
|
// descriptors. You will run in to your system's "max open files" limit faster on
|
||||||
|
// these platforms.
|
||||||
|
//
|
||||||
|
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
|
||||||
|
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
||||||
|
// systems.
|
||||||
|
//
|
||||||
|
// # Windows notes
|
||||||
|
//
|
||||||
|
// Paths can be added as "C:\path\to\dir", but forward slashes
|
||||||
|
// ("C:/path/to/dir") will also work.
|
||||||
|
//
|
||||||
|
// When a watched directory is removed it will always send an event for the
|
||||||
|
// directory itself, but may not send events for all files in that directory.
|
||||||
|
// Sometimes it will send events for all times, sometimes it will send no
|
||||||
|
// events, and often only for some files.
|
||||||
|
//
|
||||||
|
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
|
||||||
|
// value that is guaranteed to work with SMB filesystems. If you have many
|
||||||
|
// events in quick succession this may not be enough, and you will have to use
|
||||||
|
// [WithBufferSize] to increase the value.
|
||||||
|
type Watcher struct {
|
||||||
|
// Events sends the filesystem change events.
|
||||||
|
//
|
||||||
|
// fsnotify can send the following events; a "path" here can refer to a
|
||||||
|
// file, directory, symbolic link, or special file like a FIFO.
|
||||||
|
//
|
||||||
|
// fsnotify.Create A new path was created; this may be followed by one
|
||||||
|
// or more Write events if data also gets written to a
|
||||||
|
// file.
|
||||||
|
//
|
||||||
|
// fsnotify.Remove A path was removed.
|
||||||
|
//
|
||||||
|
// fsnotify.Rename A path was renamed. A rename is always sent with the
|
||||||
|
// old path as Event.Name, and a Create event will be
|
||||||
|
// sent with the new name. Renames are only sent for
|
||||||
|
// paths that are currently watched; e.g. moving an
|
||||||
|
// unmonitored file into a monitored directory will
|
||||||
|
// show up as just a Create. Similarly, renaming a file
|
||||||
|
// to outside a monitored directory will show up as
|
||||||
|
// only a Rename.
|
||||||
|
//
|
||||||
|
// fsnotify.Write A file or named pipe was written to. A Truncate will
|
||||||
|
// also trigger a Write. A single "write action"
|
||||||
|
// initiated by the user may show up as one or multiple
|
||||||
|
// writes, depending on when the system syncs things to
|
||||||
|
// disk. For example when compiling a large Go program
|
||||||
|
// you may get hundreds of Write events, and you may
|
||||||
|
// want to wait until you've stopped receiving them
|
||||||
|
// (see the dedup example in cmd/fsnotify).
|
||||||
|
//
|
||||||
|
// Some systems may send Write event for directories
|
||||||
|
// when the directory content changes.
|
||||||
|
//
|
||||||
|
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
||||||
|
// when a file is removed (or more accurately, when a
|
||||||
|
// link to an inode is removed). On kqueue it's sent
|
||||||
|
// when a file is truncated. On Windows it's never
|
||||||
|
// sent.
|
||||||
|
Events chan Event
|
||||||
|
|
||||||
|
// Errors sends any errors.
|
||||||
|
//
|
||||||
|
// ErrEventOverflow is used to indicate there are too many events:
|
||||||
|
//
|
||||||
|
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
|
||||||
|
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
|
||||||
|
// - kqueue, fen: Not used.
|
||||||
|
Errors chan error
|
||||||
|
}
|
||||||
|
|
||||||
// NewWatcher creates a new Watcher.
|
// NewWatcher creates a new Watcher.
|
||||||
func NewWatcher() (*Watcher, error) {
|
func NewWatcher() (*Watcher, error) {
|
||||||
return nil, fmt.Errorf("fsnotify not supported on %s", runtime.GOOS)
|
return nil, errors.New("fsnotify not supported on the current platform")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close removes all watches and closes the events channel.
|
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
|
||||||
func (w *Watcher) Close() error {
|
// channel.
|
||||||
return nil
|
//
|
||||||
}
|
// The main use case for this is situations with a very large number of events
|
||||||
|
// where the kernel buffer size can't be increased (e.g. due to lack of
|
||||||
|
// permissions). An unbuffered Watcher will perform better for almost all use
|
||||||
|
// cases, and whenever possible you will be better off increasing the kernel
|
||||||
|
// buffers instead of adding a large userspace buffer.
|
||||||
|
func NewBufferedWatcher(sz uint) (*Watcher, error) { return NewWatcher() }
|
||||||
|
|
||||||
|
// Close removes all watches and closes the Events channel.
|
||||||
|
func (w *Watcher) Close() error { return nil }
|
||||||
|
|
||||||
|
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
|
||||||
|
// yet removed).
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
|
func (w *Watcher) WatchList() []string { return nil }
|
||||||
|
|
||||||
// Add starts monitoring the path for changes.
|
// Add starts monitoring the path for changes.
|
||||||
//
|
//
|
||||||
// A path can only be watched once; attempting to watch it more than once will
|
// A path can only be watched once; watching it more than once is a no-op and will
|
||||||
// return an error. Paths that do not yet exist on the filesystem cannot be
|
// not return an error. Paths that do not yet exist on the filesystem cannot be
|
||||||
// added. A watch will be automatically removed if the path is deleted.
|
// watched.
|
||||||
//
|
//
|
||||||
// A path will remain watched if it gets renamed to somewhere else on the same
|
// A watch will be automatically removed if the watched path is deleted or
|
||||||
// filesystem, but the monitor will get removed if the path gets deleted and
|
// renamed. The exception is the Windows backend, which doesn't remove the
|
||||||
// re-created, or if it's moved to a different filesystem.
|
// watcher on renames.
|
||||||
//
|
//
|
||||||
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
||||||
// filesystems (/proc, /sys, etc.) generally don't work.
|
// filesystems (/proc, /sys, etc.) generally don't work.
|
||||||
//
|
//
|
||||||
|
// Returns [ErrClosed] if [Watcher.Close] was called.
|
||||||
|
//
|
||||||
|
// See [Watcher.AddWith] for a version that allows adding options.
|
||||||
|
//
|
||||||
// # Watching directories
|
// # Watching directories
|
||||||
//
|
//
|
||||||
// All files in a directory are monitored, including new files that are created
|
// All files in a directory are monitored, including new files that are created
|
||||||
|
@ -43,17 +173,26 @@ func (w *Watcher) Close() error {
|
||||||
// # Watching files
|
// # Watching files
|
||||||
//
|
//
|
||||||
// Watching individual files (rather than directories) is generally not
|
// Watching individual files (rather than directories) is generally not
|
||||||
// recommended as many tools update files atomically. Instead of "just" writing
|
// recommended as many programs (especially editors) update files atomically: it
|
||||||
// to the file a temporary file will be written to first, and if successful the
|
// will write to a temporary file which is then moved to to destination,
|
||||||
// temporary file is moved to to destination removing the original, or some
|
// overwriting the original (or some variant thereof). The watcher on the
|
||||||
// variant thereof. The watcher on the original file is now lost, as it no
|
// original file is now lost, as that no longer exists.
|
||||||
// longer exists.
|
|
||||||
//
|
//
|
||||||
// Instead, watch the parent directory and use Event.Name to filter out files
|
// The upshot of this is that a power failure or crash won't leave a
|
||||||
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
|
// half-written file.
|
||||||
func (w *Watcher) Add(name string) error {
|
//
|
||||||
return nil
|
// Watch the parent directory and use Event.Name to filter out files you're not
|
||||||
}
|
// interested in. There is an example of this in cmd/fsnotify/file.go.
|
||||||
|
func (w *Watcher) Add(name string) error { return nil }
|
||||||
|
|
||||||
|
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
|
||||||
|
// the defaults described below are used.
|
||||||
|
//
|
||||||
|
// Possible options are:
|
||||||
|
//
|
||||||
|
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
|
||||||
|
// other platforms. The default is 64K (65536 bytes).
|
||||||
|
func (w *Watcher) AddWith(name string, opts ...addOpt) error { return nil }
|
||||||
|
|
||||||
// Remove stops monitoring the path for changes.
|
// Remove stops monitoring the path for changes.
|
||||||
//
|
//
|
||||||
|
@ -61,6 +200,6 @@ func (w *Watcher) Add(name string) error {
|
||||||
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
||||||
//
|
//
|
||||||
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
||||||
func (w *Watcher) Remove(name string) error {
|
//
|
||||||
return nil
|
// Returns nil if [Watcher.Close] was called.
|
||||||
}
|
func (w *Watcher) Remove(name string) error { return nil }
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
//go:build windows
|
//go:build windows
|
||||||
// +build windows
|
// +build windows
|
||||||
|
|
||||||
|
// Windows backend based on ReadDirectoryChangesW()
|
||||||
|
//
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw
|
||||||
|
//
|
||||||
|
// Note: the documentation on the Watcher type and methods is generated from
|
||||||
|
// mkdoc.zsh
|
||||||
|
|
||||||
package fsnotify
|
package fsnotify
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -27,9 +34,9 @@ import (
|
||||||
// When a file is removed a Remove event won't be emitted until all file
|
// When a file is removed a Remove event won't be emitted until all file
|
||||||
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
||||||
//
|
//
|
||||||
// fp := os.Open("file")
|
// fp := os.Open("file")
|
||||||
// os.Remove("file") // Triggers Chmod
|
// os.Remove("file") // Triggers Chmod
|
||||||
// fp.Close() // Triggers Remove
|
// fp.Close() // Triggers Remove
|
||||||
//
|
//
|
||||||
// This is the event that inotify sends, so not much can be changed about this.
|
// This is the event that inotify sends, so not much can be changed about this.
|
||||||
//
|
//
|
||||||
|
@ -43,16 +50,16 @@ import (
|
||||||
//
|
//
|
||||||
// To increase them you can use sysctl or write the value to the /proc file:
|
// To increase them you can use sysctl or write the value to the /proc file:
|
||||||
//
|
//
|
||||||
// # Default values on Linux 5.18
|
// # Default values on Linux 5.18
|
||||||
// sysctl fs.inotify.max_user_watches=124983
|
// sysctl fs.inotify.max_user_watches=124983
|
||||||
// sysctl fs.inotify.max_user_instances=128
|
// sysctl fs.inotify.max_user_instances=128
|
||||||
//
|
//
|
||||||
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
||||||
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
||||||
// your distro's documentation):
|
// your distro's documentation):
|
||||||
//
|
//
|
||||||
// fs.inotify.max_user_watches=124983
|
// fs.inotify.max_user_watches=124983
|
||||||
// fs.inotify.max_user_instances=128
|
// fs.inotify.max_user_instances=128
|
||||||
//
|
//
|
||||||
// Reaching the limit will result in a "no space left on device" or "too many open
|
// Reaching the limit will result in a "no space left on device" or "too many open
|
||||||
// files" error.
|
// files" error.
|
||||||
|
@ -68,14 +75,20 @@ import (
|
||||||
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
||||||
// systems.
|
// systems.
|
||||||
//
|
//
|
||||||
// # macOS notes
|
// # Windows notes
|
||||||
//
|
//
|
||||||
// Spotlight indexing on macOS can result in multiple events (see [#15]). A
|
// Paths can be added as "C:\path\to\dir", but forward slashes
|
||||||
// temporary workaround is to add your folder(s) to the "Spotlight Privacy
|
// ("C:/path/to/dir") will also work.
|
||||||
// Settings" until we have a native FSEvents implementation (see [#11]).
|
|
||||||
//
|
//
|
||||||
// [#11]: https://github.com/fsnotify/fsnotify/issues/11
|
// When a watched directory is removed it will always send an event for the
|
||||||
// [#15]: https://github.com/fsnotify/fsnotify/issues/15
|
// directory itself, but may not send events for all files in that directory.
|
||||||
|
// Sometimes it will send events for all times, sometimes it will send no
|
||||||
|
// events, and often only for some files.
|
||||||
|
//
|
||||||
|
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
|
||||||
|
// value that is guaranteed to work with SMB filesystems. If you have many
|
||||||
|
// events in quick succession this may not be enough, and you will have to use
|
||||||
|
// [WithBufferSize] to increase the value.
|
||||||
type Watcher struct {
|
type Watcher struct {
|
||||||
// Events sends the filesystem change events.
|
// Events sends the filesystem change events.
|
||||||
//
|
//
|
||||||
|
@ -102,31 +115,52 @@ type Watcher struct {
|
||||||
// initiated by the user may show up as one or multiple
|
// initiated by the user may show up as one or multiple
|
||||||
// writes, depending on when the system syncs things to
|
// writes, depending on when the system syncs things to
|
||||||
// disk. For example when compiling a large Go program
|
// disk. For example when compiling a large Go program
|
||||||
// you may get hundreds of Write events, so you
|
// you may get hundreds of Write events, and you may
|
||||||
// probably want to wait until you've stopped receiving
|
// want to wait until you've stopped receiving them
|
||||||
// them (see the dedup example in cmd/fsnotify).
|
// (see the dedup example in cmd/fsnotify).
|
||||||
|
//
|
||||||
|
// Some systems may send Write event for directories
|
||||||
|
// when the directory content changes.
|
||||||
//
|
//
|
||||||
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
||||||
// when a file is removed (or more accurately, when a
|
// when a file is removed (or more accurately, when a
|
||||||
// link to an inode is removed). On kqueue it's sent
|
// link to an inode is removed). On kqueue it's sent
|
||||||
// and on kqueue when a file is truncated. On Windows
|
// when a file is truncated. On Windows it's never
|
||||||
// it's never sent.
|
// sent.
|
||||||
Events chan Event
|
Events chan Event
|
||||||
|
|
||||||
// Errors sends any errors.
|
// Errors sends any errors.
|
||||||
|
//
|
||||||
|
// ErrEventOverflow is used to indicate there are too many events:
|
||||||
|
//
|
||||||
|
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
|
||||||
|
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
|
||||||
|
// - kqueue, fen: Not used.
|
||||||
Errors chan error
|
Errors chan error
|
||||||
|
|
||||||
port windows.Handle // Handle to completion port
|
port windows.Handle // Handle to completion port
|
||||||
input chan *input // Inputs to the reader are sent on this channel
|
input chan *input // Inputs to the reader are sent on this channel
|
||||||
quit chan chan<- error
|
quit chan chan<- error
|
||||||
|
|
||||||
mu sync.Mutex // Protects access to watches, isClosed
|
mu sync.Mutex // Protects access to watches, closed
|
||||||
watches watchMap // Map of watches (key: i-number)
|
watches watchMap // Map of watches (key: i-number)
|
||||||
isClosed bool // Set to true when Close() is first called
|
closed bool // Set to true when Close() is first called
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWatcher creates a new Watcher.
|
// NewWatcher creates a new Watcher.
|
||||||
func NewWatcher() (*Watcher, error) {
|
func NewWatcher() (*Watcher, error) {
|
||||||
|
return NewBufferedWatcher(50)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
|
||||||
|
// channel.
|
||||||
|
//
|
||||||
|
// The main use case for this is situations with a very large number of events
|
||||||
|
// where the kernel buffer size can't be increased (e.g. due to lack of
|
||||||
|
// permissions). An unbuffered Watcher will perform better for almost all use
|
||||||
|
// cases, and whenever possible you will be better off increasing the kernel
|
||||||
|
// buffers instead of adding a large userspace buffer.
|
||||||
|
func NewBufferedWatcher(sz uint) (*Watcher, error) {
|
||||||
port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0)
|
port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, os.NewSyscallError("CreateIoCompletionPort", err)
|
return nil, os.NewSyscallError("CreateIoCompletionPort", err)
|
||||||
|
@ -135,7 +169,7 @@ func NewWatcher() (*Watcher, error) {
|
||||||
port: port,
|
port: port,
|
||||||
watches: make(watchMap),
|
watches: make(watchMap),
|
||||||
input: make(chan *input, 1),
|
input: make(chan *input, 1),
|
||||||
Events: make(chan Event, 50),
|
Events: make(chan Event, sz),
|
||||||
Errors: make(chan error),
|
Errors: make(chan error),
|
||||||
quit: make(chan chan<- error, 1),
|
quit: make(chan chan<- error, 1),
|
||||||
}
|
}
|
||||||
|
@ -143,6 +177,12 @@ func NewWatcher() (*Watcher, error) {
|
||||||
return w, nil
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) isClosed() bool {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
return w.closed
|
||||||
|
}
|
||||||
|
|
||||||
func (w *Watcher) sendEvent(name string, mask uint64) bool {
|
func (w *Watcher) sendEvent(name string, mask uint64) bool {
|
||||||
if mask == 0 {
|
if mask == 0 {
|
||||||
return false
|
return false
|
||||||
|
@ -167,14 +207,14 @@ func (w *Watcher) sendError(err error) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close removes all watches and closes the events channel.
|
// Close removes all watches and closes the Events channel.
|
||||||
func (w *Watcher) Close() error {
|
func (w *Watcher) Close() error {
|
||||||
w.mu.Lock()
|
if w.isClosed() {
|
||||||
if w.isClosed {
|
|
||||||
w.mu.Unlock()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
w.isClosed = true
|
|
||||||
|
w.mu.Lock()
|
||||||
|
w.closed = true
|
||||||
w.mu.Unlock()
|
w.mu.Unlock()
|
||||||
|
|
||||||
// Send "quit" message to the reader goroutine
|
// Send "quit" message to the reader goroutine
|
||||||
|
@ -188,17 +228,21 @@ func (w *Watcher) Close() error {
|
||||||
|
|
||||||
// Add starts monitoring the path for changes.
|
// Add starts monitoring the path for changes.
|
||||||
//
|
//
|
||||||
// A path can only be watched once; attempting to watch it more than once will
|
// A path can only be watched once; watching it more than once is a no-op and will
|
||||||
// return an error. Paths that do not yet exist on the filesystem cannot be
|
// not return an error. Paths that do not yet exist on the filesystem cannot be
|
||||||
// added. A watch will be automatically removed if the path is deleted.
|
// watched.
|
||||||
//
|
//
|
||||||
// A path will remain watched if it gets renamed to somewhere else on the same
|
// A watch will be automatically removed if the watched path is deleted or
|
||||||
// filesystem, but the monitor will get removed if the path gets deleted and
|
// renamed. The exception is the Windows backend, which doesn't remove the
|
||||||
// re-created, or if it's moved to a different filesystem.
|
// watcher on renames.
|
||||||
//
|
//
|
||||||
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
||||||
// filesystems (/proc, /sys, etc.) generally don't work.
|
// filesystems (/proc, /sys, etc.) generally don't work.
|
||||||
//
|
//
|
||||||
|
// Returns [ErrClosed] if [Watcher.Close] was called.
|
||||||
|
//
|
||||||
|
// See [Watcher.AddWith] for a version that allows adding options.
|
||||||
|
//
|
||||||
// # Watching directories
|
// # Watching directories
|
||||||
//
|
//
|
||||||
// All files in a directory are monitored, including new files that are created
|
// All files in a directory are monitored, including new files that are created
|
||||||
|
@ -208,27 +252,41 @@ func (w *Watcher) Close() error {
|
||||||
// # Watching files
|
// # Watching files
|
||||||
//
|
//
|
||||||
// Watching individual files (rather than directories) is generally not
|
// Watching individual files (rather than directories) is generally not
|
||||||
// recommended as many tools update files atomically. Instead of "just" writing
|
// recommended as many programs (especially editors) update files atomically: it
|
||||||
// to the file a temporary file will be written to first, and if successful the
|
// will write to a temporary file which is then moved to to destination,
|
||||||
// temporary file is moved to to destination removing the original, or some
|
// overwriting the original (or some variant thereof). The watcher on the
|
||||||
// variant thereof. The watcher on the original file is now lost, as it no
|
// original file is now lost, as that no longer exists.
|
||||||
// longer exists.
|
|
||||||
//
|
//
|
||||||
// Instead, watch the parent directory and use Event.Name to filter out files
|
// The upshot of this is that a power failure or crash won't leave a
|
||||||
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
|
// half-written file.
|
||||||
func (w *Watcher) Add(name string) error {
|
//
|
||||||
w.mu.Lock()
|
// Watch the parent directory and use Event.Name to filter out files you're not
|
||||||
if w.isClosed {
|
// interested in. There is an example of this in cmd/fsnotify/file.go.
|
||||||
w.mu.Unlock()
|
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
|
||||||
return errors.New("watcher already closed")
|
|
||||||
|
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
|
||||||
|
// the defaults described below are used.
|
||||||
|
//
|
||||||
|
// Possible options are:
|
||||||
|
//
|
||||||
|
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
|
||||||
|
// other platforms. The default is 64K (65536 bytes).
|
||||||
|
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
|
||||||
|
if w.isClosed() {
|
||||||
|
return ErrClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
with := getOptions(opts...)
|
||||||
|
if with.bufsize < 4096 {
|
||||||
|
return fmt.Errorf("fsnotify.WithBufferSize: buffer size cannot be smaller than 4096 bytes")
|
||||||
}
|
}
|
||||||
w.mu.Unlock()
|
|
||||||
|
|
||||||
in := &input{
|
in := &input{
|
||||||
op: opAddWatch,
|
op: opAddWatch,
|
||||||
path: filepath.Clean(name),
|
path: filepath.Clean(name),
|
||||||
flags: sysFSALLEVENTS,
|
flags: sysFSALLEVENTS,
|
||||||
reply: make(chan error),
|
reply: make(chan error),
|
||||||
|
bufsize: with.bufsize,
|
||||||
}
|
}
|
||||||
w.input <- in
|
w.input <- in
|
||||||
if err := w.wakeupReader(); err != nil {
|
if err := w.wakeupReader(); err != nil {
|
||||||
|
@ -243,7 +301,13 @@ func (w *Watcher) Add(name string) error {
|
||||||
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
||||||
//
|
//
|
||||||
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
func (w *Watcher) Remove(name string) error {
|
func (w *Watcher) Remove(name string) error {
|
||||||
|
if w.isClosed() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
in := &input{
|
in := &input{
|
||||||
op: opRemoveWatch,
|
op: opRemoveWatch,
|
||||||
path: filepath.Clean(name),
|
path: filepath.Clean(name),
|
||||||
|
@ -256,8 +320,15 @@ func (w *Watcher) Remove(name string) error {
|
||||||
return <-in.reply
|
return <-in.reply
|
||||||
}
|
}
|
||||||
|
|
||||||
// WatchList returns all paths added with [Add] (and are not yet removed).
|
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
|
||||||
|
// yet removed).
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
func (w *Watcher) WatchList() []string {
|
func (w *Watcher) WatchList() []string {
|
||||||
|
if w.isClosed() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
defer w.mu.Unlock()
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
@ -279,7 +350,6 @@ func (w *Watcher) WatchList() []string {
|
||||||
// This should all be removed at some point, and just use windows.FILE_NOTIFY_*
|
// This should all be removed at some point, and just use windows.FILE_NOTIFY_*
|
||||||
const (
|
const (
|
||||||
sysFSALLEVENTS = 0xfff
|
sysFSALLEVENTS = 0xfff
|
||||||
sysFSATTRIB = 0x4
|
|
||||||
sysFSCREATE = 0x100
|
sysFSCREATE = 0x100
|
||||||
sysFSDELETE = 0x200
|
sysFSDELETE = 0x200
|
||||||
sysFSDELETESELF = 0x400
|
sysFSDELETESELF = 0x400
|
||||||
|
@ -305,9 +375,6 @@ func (w *Watcher) newEvent(name string, mask uint32) Event {
|
||||||
if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM {
|
if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM {
|
||||||
e.Op |= Rename
|
e.Op |= Rename
|
||||||
}
|
}
|
||||||
if mask&sysFSATTRIB == sysFSATTRIB {
|
|
||||||
e.Op |= Chmod
|
|
||||||
}
|
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,10 +388,11 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type input struct {
|
type input struct {
|
||||||
op int
|
op int
|
||||||
path string
|
path string
|
||||||
flags uint32
|
flags uint32
|
||||||
reply chan error
|
bufsize int
|
||||||
|
reply chan error
|
||||||
}
|
}
|
||||||
|
|
||||||
type inode struct {
|
type inode struct {
|
||||||
|
@ -334,13 +402,14 @@ type inode struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type watch struct {
|
type watch struct {
|
||||||
ov windows.Overlapped
|
ov windows.Overlapped
|
||||||
ino *inode // i-number
|
ino *inode // i-number
|
||||||
path string // Directory path
|
recurse bool // Recursive watch?
|
||||||
mask uint64 // Directory itself is being watched with these notify flags
|
path string // Directory path
|
||||||
names map[string]uint64 // Map of names being watched and their notify flags
|
mask uint64 // Directory itself is being watched with these notify flags
|
||||||
rename string // Remembers the old name while renaming a file
|
names map[string]uint64 // Map of names being watched and their notify flags
|
||||||
buf [65536]byte // 64K buffer
|
rename string // Remembers the old name while renaming a file
|
||||||
|
buf []byte // buffer, allocated later
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -413,7 +482,10 @@ func (m watchMap) set(ino *inode, watch *watch) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must run within the I/O thread.
|
// Must run within the I/O thread.
|
||||||
func (w *Watcher) addWatch(pathname string, flags uint64) error {
|
func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error {
|
||||||
|
//pathname, recurse := recursivePath(pathname)
|
||||||
|
recurse := false
|
||||||
|
|
||||||
dir, err := w.getDir(pathname)
|
dir, err := w.getDir(pathname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -433,9 +505,11 @@ func (w *Watcher) addWatch(pathname string, flags uint64) error {
|
||||||
return os.NewSyscallError("CreateIoCompletionPort", err)
|
return os.NewSyscallError("CreateIoCompletionPort", err)
|
||||||
}
|
}
|
||||||
watchEntry = &watch{
|
watchEntry = &watch{
|
||||||
ino: ino,
|
ino: ino,
|
||||||
path: dir,
|
path: dir,
|
||||||
names: make(map[string]uint64),
|
names: make(map[string]uint64),
|
||||||
|
recurse: recurse,
|
||||||
|
buf: make([]byte, bufsize),
|
||||||
}
|
}
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
w.watches.set(ino, watchEntry)
|
w.watches.set(ino, watchEntry)
|
||||||
|
@ -465,6 +539,8 @@ func (w *Watcher) addWatch(pathname string, flags uint64) error {
|
||||||
|
|
||||||
// Must run within the I/O thread.
|
// Must run within the I/O thread.
|
||||||
func (w *Watcher) remWatch(pathname string) error {
|
func (w *Watcher) remWatch(pathname string) error {
|
||||||
|
pathname, recurse := recursivePath(pathname)
|
||||||
|
|
||||||
dir, err := w.getDir(pathname)
|
dir, err := w.getDir(pathname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -478,6 +554,10 @@ func (w *Watcher) remWatch(pathname string) error {
|
||||||
watch := w.watches.get(ino)
|
watch := w.watches.get(ino)
|
||||||
w.mu.Unlock()
|
w.mu.Unlock()
|
||||||
|
|
||||||
|
if recurse && !watch.recurse {
|
||||||
|
return fmt.Errorf("can't use \\... with non-recursive watch %q", pathname)
|
||||||
|
}
|
||||||
|
|
||||||
err = windows.CloseHandle(ino.handle)
|
err = windows.CloseHandle(ino.handle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.sendError(os.NewSyscallError("CloseHandle", err))
|
w.sendError(os.NewSyscallError("CloseHandle", err))
|
||||||
|
@ -535,8 +615,11 @@ func (w *Watcher) startRead(watch *watch) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rdErr := windows.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0],
|
// We need to pass the array, rather than the slice.
|
||||||
uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0)
|
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&watch.buf))
|
||||||
|
rdErr := windows.ReadDirectoryChanges(watch.ino.handle,
|
||||||
|
(*byte)(unsafe.Pointer(hdr.Data)), uint32(hdr.Len),
|
||||||
|
watch.recurse, mask, nil, &watch.ov, 0)
|
||||||
if rdErr != nil {
|
if rdErr != nil {
|
||||||
err := os.NewSyscallError("ReadDirectoryChanges", rdErr)
|
err := os.NewSyscallError("ReadDirectoryChanges", rdErr)
|
||||||
if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 {
|
if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 {
|
||||||
|
@ -563,9 +646,8 @@ func (w *Watcher) readEvents() {
|
||||||
runtime.LockOSThread()
|
runtime.LockOSThread()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
// This error is handled after the watch == nil check below.
|
||||||
qErr := windows.GetQueuedCompletionStatus(w.port, &n, &key, &ov, windows.INFINITE)
|
qErr := windows.GetQueuedCompletionStatus(w.port, &n, &key, &ov, windows.INFINITE)
|
||||||
// This error is handled after the watch == nil check below. NOTE: this
|
|
||||||
// seems odd, note sure if it's correct.
|
|
||||||
|
|
||||||
watch := (*watch)(unsafe.Pointer(ov))
|
watch := (*watch)(unsafe.Pointer(ov))
|
||||||
if watch == nil {
|
if watch == nil {
|
||||||
|
@ -595,7 +677,7 @@ func (w *Watcher) readEvents() {
|
||||||
case in := <-w.input:
|
case in := <-w.input:
|
||||||
switch in.op {
|
switch in.op {
|
||||||
case opAddWatch:
|
case opAddWatch:
|
||||||
in.reply <- w.addWatch(in.path, uint64(in.flags))
|
in.reply <- w.addWatch(in.path, uint64(in.flags), in.bufsize)
|
||||||
case opRemoveWatch:
|
case opRemoveWatch:
|
||||||
in.reply <- w.remWatch(in.path)
|
in.reply <- w.remWatch(in.path)
|
||||||
}
|
}
|
||||||
|
@ -605,6 +687,8 @@ func (w *Watcher) readEvents() {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch qErr {
|
switch qErr {
|
||||||
|
case nil:
|
||||||
|
// No error
|
||||||
case windows.ERROR_MORE_DATA:
|
case windows.ERROR_MORE_DATA:
|
||||||
if watch == nil {
|
if watch == nil {
|
||||||
w.sendError(errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer"))
|
w.sendError(errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer"))
|
||||||
|
@ -626,13 +710,12 @@ func (w *Watcher) readEvents() {
|
||||||
default:
|
default:
|
||||||
w.sendError(os.NewSyscallError("GetQueuedCompletionPort", qErr))
|
w.sendError(os.NewSyscallError("GetQueuedCompletionPort", qErr))
|
||||||
continue
|
continue
|
||||||
case nil:
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var offset uint32
|
var offset uint32
|
||||||
for {
|
for {
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
w.sendError(errors.New("short read in readEvents()"))
|
w.sendError(ErrEventOverflow)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -703,8 +786,9 @@ func (w *Watcher) readEvents() {
|
||||||
|
|
||||||
// Error!
|
// Error!
|
||||||
if offset >= n {
|
if offset >= n {
|
||||||
|
//lint:ignore ST1005 Windows should be capitalized
|
||||||
w.sendError(errors.New(
|
w.sendError(errors.New(
|
||||||
"Windows system assumed buffer larger than it is, events have likely been missed."))
|
"Windows system assumed buffer larger than it is, events have likely been missed"))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -720,9 +804,6 @@ func (w *Watcher) toWindowsFlags(mask uint64) uint32 {
|
||||||
if mask&sysFSMODIFY != 0 {
|
if mask&sysFSMODIFY != 0 {
|
||||||
m |= windows.FILE_NOTIFY_CHANGE_LAST_WRITE
|
m |= windows.FILE_NOTIFY_CHANGE_LAST_WRITE
|
||||||
}
|
}
|
||||||
if mask&sysFSATTRIB != 0 {
|
|
||||||
m |= windows.FILE_NOTIFY_CHANGE_ATTRIBUTES
|
|
||||||
}
|
|
||||||
if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 {
|
if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 {
|
||||||
m |= windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME
|
m |= windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
//go:build !plan9
|
|
||||||
// +build !plan9
|
|
||||||
|
|
||||||
// Package fsnotify provides a cross-platform interface for file system
|
// Package fsnotify provides a cross-platform interface for file system
|
||||||
// notifications.
|
// notifications.
|
||||||
|
//
|
||||||
|
// Currently supported systems:
|
||||||
|
//
|
||||||
|
// Linux 2.6.32+ via inotify
|
||||||
|
// BSD, macOS via kqueue
|
||||||
|
// Windows via ReadDirectoryChangesW
|
||||||
|
// illumos via FEN
|
||||||
package fsnotify
|
package fsnotify
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,34 +38,52 @@ type Op uint32
|
||||||
// The operations fsnotify can trigger; see the documentation on [Watcher] for a
|
// The operations fsnotify can trigger; see the documentation on [Watcher] for a
|
||||||
// full description, and check them with [Event.Has].
|
// full description, and check them with [Event.Has].
|
||||||
const (
|
const (
|
||||||
|
// A new pathname was created.
|
||||||
Create Op = 1 << iota
|
Create Op = 1 << iota
|
||||||
|
|
||||||
|
// The pathname was written to; this does *not* mean the write has finished,
|
||||||
|
// and a write can be followed by more writes.
|
||||||
Write
|
Write
|
||||||
|
|
||||||
|
// The path was removed; any watches on it will be removed. Some "remove"
|
||||||
|
// operations may trigger a Rename if the file is actually moved (for
|
||||||
|
// example "remove to trash" is often a rename).
|
||||||
Remove
|
Remove
|
||||||
|
|
||||||
|
// The path was renamed to something else; any watched on it will be
|
||||||
|
// removed.
|
||||||
Rename
|
Rename
|
||||||
|
|
||||||
|
// File attributes were changed.
|
||||||
|
//
|
||||||
|
// It's generally not recommended to take action on this event, as it may
|
||||||
|
// get triggered very frequently by some software. For example, Spotlight
|
||||||
|
// indexing on macOS, anti-virus software, backup software, etc.
|
||||||
Chmod
|
Chmod
|
||||||
)
|
)
|
||||||
|
|
||||||
// Common errors that can be reported by a watcher
|
// Common errors that can be reported.
|
||||||
var (
|
var (
|
||||||
ErrNonExistentWatch = errors.New("can't remove non-existent watcher")
|
ErrNonExistentWatch = errors.New("fsnotify: can't remove non-existent watch")
|
||||||
ErrEventOverflow = errors.New("fsnotify queue overflow")
|
ErrEventOverflow = errors.New("fsnotify: queue or buffer overflow")
|
||||||
|
ErrClosed = errors.New("fsnotify: watcher already closed")
|
||||||
)
|
)
|
||||||
|
|
||||||
func (op Op) String() string {
|
func (o Op) String() string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
if op.Has(Create) {
|
if o.Has(Create) {
|
||||||
b.WriteString("|CREATE")
|
b.WriteString("|CREATE")
|
||||||
}
|
}
|
||||||
if op.Has(Remove) {
|
if o.Has(Remove) {
|
||||||
b.WriteString("|REMOVE")
|
b.WriteString("|REMOVE")
|
||||||
}
|
}
|
||||||
if op.Has(Write) {
|
if o.Has(Write) {
|
||||||
b.WriteString("|WRITE")
|
b.WriteString("|WRITE")
|
||||||
}
|
}
|
||||||
if op.Has(Rename) {
|
if o.Has(Rename) {
|
||||||
b.WriteString("|RENAME")
|
b.WriteString("|RENAME")
|
||||||
}
|
}
|
||||||
if op.Has(Chmod) {
|
if o.Has(Chmod) {
|
||||||
b.WriteString("|CHMOD")
|
b.WriteString("|CHMOD")
|
||||||
}
|
}
|
||||||
if b.Len() == 0 {
|
if b.Len() == 0 {
|
||||||
|
@ -70,7 +93,7 @@ func (op Op) String() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Has reports if this operation has the given operation.
|
// Has reports if this operation has the given operation.
|
||||||
func (o Op) Has(h Op) bool { return o&h == h }
|
func (o Op) Has(h Op) bool { return o&h != 0 }
|
||||||
|
|
||||||
// Has reports if this event has the given operation.
|
// Has reports if this event has the given operation.
|
||||||
func (e Event) Has(op Op) bool { return e.Op.Has(op) }
|
func (e Event) Has(op Op) bool { return e.Op.Has(op) }
|
||||||
|
@ -79,3 +102,45 @@ func (e Event) Has(op Op) bool { return e.Op.Has(op) }
|
||||||
func (e Event) String() string {
|
func (e Event) String() string {
|
||||||
return fmt.Sprintf("%-13s %q", e.Op.String(), e.Name)
|
return fmt.Sprintf("%-13s %q", e.Op.String(), e.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
addOpt func(opt *withOpts)
|
||||||
|
withOpts struct {
|
||||||
|
bufsize int
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultOpts = withOpts{
|
||||||
|
bufsize: 65536, // 64K
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOptions(opts ...addOpt) withOpts {
|
||||||
|
with := defaultOpts
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&with)
|
||||||
|
}
|
||||||
|
return with
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBufferSize sets the [ReadDirectoryChangesW] buffer size.
|
||||||
|
//
|
||||||
|
// This only has effect on Windows systems, and is a no-op for other backends.
|
||||||
|
//
|
||||||
|
// The default value is 64K (65536 bytes) which is the highest value that works
|
||||||
|
// on all filesystems and should be enough for most applications, but if you
|
||||||
|
// have a large burst of events it may not be enough. You can increase it if
|
||||||
|
// you're hitting "queue or buffer overflow" errors ([ErrEventOverflow]).
|
||||||
|
//
|
||||||
|
// [ReadDirectoryChangesW]: https://learn.microsoft.com/en-gb/windows/win32/api/winbase/nf-winbase-readdirectorychangesw
|
||||||
|
func WithBufferSize(bytes int) addOpt {
|
||||||
|
return func(opt *withOpts) { opt.bufsize = bytes }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this path is recursive (ends with "/..." or "\..."), and return the
|
||||||
|
// path with the /... stripped.
|
||||||
|
func recursivePath(path string) (string, bool) {
|
||||||
|
if filepath.Base(path) == "..." {
|
||||||
|
return filepath.Dir(path), true
|
||||||
|
}
|
||||||
|
return path, false
|
||||||
|
}
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
[ "${ZSH_VERSION:-}" = "" ] && echo >&2 "Only works with zsh" && exit 1
|
[ "${ZSH_VERSION:-}" = "" ] && echo >&2 "Only works with zsh" && exit 1
|
||||||
setopt err_exit no_unset pipefail extended_glob
|
setopt err_exit no_unset pipefail extended_glob
|
||||||
|
|
||||||
# Simple script to update the godoc comments on all watchers. Probably took me
|
# Simple script to update the godoc comments on all watchers so you don't need
|
||||||
# more time to write this than doing it manually, but ah well 🙃
|
# to update the same comment 5 times.
|
||||||
|
|
||||||
watcher=$(<<EOF
|
watcher=$(<<EOF
|
||||||
// Watcher watches a set of paths, delivering events on a channel.
|
// Watcher watches a set of paths, delivering events on a channel.
|
||||||
|
@ -16,9 +16,9 @@ watcher=$(<<EOF
|
||||||
// When a file is removed a Remove event won't be emitted until all file
|
// When a file is removed a Remove event won't be emitted until all file
|
||||||
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
||||||
//
|
//
|
||||||
// fp := os.Open("file")
|
// fp := os.Open("file")
|
||||||
// os.Remove("file") // Triggers Chmod
|
// os.Remove("file") // Triggers Chmod
|
||||||
// fp.Close() // Triggers Remove
|
// fp.Close() // Triggers Remove
|
||||||
//
|
//
|
||||||
// This is the event that inotify sends, so not much can be changed about this.
|
// This is the event that inotify sends, so not much can be changed about this.
|
||||||
//
|
//
|
||||||
|
@ -32,16 +32,16 @@ watcher=$(<<EOF
|
||||||
//
|
//
|
||||||
// To increase them you can use sysctl or write the value to the /proc file:
|
// To increase them you can use sysctl or write the value to the /proc file:
|
||||||
//
|
//
|
||||||
// # Default values on Linux 5.18
|
// # Default values on Linux 5.18
|
||||||
// sysctl fs.inotify.max_user_watches=124983
|
// sysctl fs.inotify.max_user_watches=124983
|
||||||
// sysctl fs.inotify.max_user_instances=128
|
// sysctl fs.inotify.max_user_instances=128
|
||||||
//
|
//
|
||||||
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
||||||
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
||||||
// your distro's documentation):
|
// your distro's documentation):
|
||||||
//
|
//
|
||||||
// fs.inotify.max_user_watches=124983
|
// fs.inotify.max_user_watches=124983
|
||||||
// fs.inotify.max_user_instances=128
|
// fs.inotify.max_user_instances=128
|
||||||
//
|
//
|
||||||
// Reaching the limit will result in a "no space left on device" or "too many open
|
// Reaching the limit will result in a "no space left on device" or "too many open
|
||||||
// files" error.
|
// files" error.
|
||||||
|
@ -57,14 +57,20 @@ watcher=$(<<EOF
|
||||||
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
||||||
// systems.
|
// systems.
|
||||||
//
|
//
|
||||||
// # macOS notes
|
// # Windows notes
|
||||||
//
|
//
|
||||||
// Spotlight indexing on macOS can result in multiple events (see [#15]). A
|
// Paths can be added as "C:\\path\\to\\dir", but forward slashes
|
||||||
// temporary workaround is to add your folder(s) to the "Spotlight Privacy
|
// ("C:/path/to/dir") will also work.
|
||||||
// Settings" until we have a native FSEvents implementation (see [#11]).
|
|
||||||
//
|
//
|
||||||
// [#11]: https://github.com/fsnotify/fsnotify/issues/11
|
// When a watched directory is removed it will always send an event for the
|
||||||
// [#15]: https://github.com/fsnotify/fsnotify/issues/15
|
// directory itself, but may not send events for all files in that directory.
|
||||||
|
// Sometimes it will send events for all times, sometimes it will send no
|
||||||
|
// events, and often only for some files.
|
||||||
|
//
|
||||||
|
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
|
||||||
|
// value that is guaranteed to work with SMB filesystems. If you have many
|
||||||
|
// events in quick succession this may not be enough, and you will have to use
|
||||||
|
// [WithBufferSize] to increase the value.
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -73,20 +79,36 @@ new=$(<<EOF
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
|
newbuffered=$(<<EOF
|
||||||
|
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
|
||||||
|
// channel.
|
||||||
|
//
|
||||||
|
// The main use case for this is situations with a very large number of events
|
||||||
|
// where the kernel buffer size can't be increased (e.g. due to lack of
|
||||||
|
// permissions). An unbuffered Watcher will perform better for almost all use
|
||||||
|
// cases, and whenever possible you will be better off increasing the kernel
|
||||||
|
// buffers instead of adding a large userspace buffer.
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
add=$(<<EOF
|
add=$(<<EOF
|
||||||
// Add starts monitoring the path for changes.
|
// Add starts monitoring the path for changes.
|
||||||
//
|
//
|
||||||
// A path can only be watched once; attempting to watch it more than once will
|
// A path can only be watched once; watching it more than once is a no-op and will
|
||||||
// return an error. Paths that do not yet exist on the filesystem cannot be
|
// not return an error. Paths that do not yet exist on the filesystem cannot be
|
||||||
// added. A watch will be automatically removed if the path is deleted.
|
// watched.
|
||||||
//
|
//
|
||||||
// A path will remain watched if it gets renamed to somewhere else on the same
|
// A watch will be automatically removed if the watched path is deleted or
|
||||||
// filesystem, but the monitor will get removed if the path gets deleted and
|
// renamed. The exception is the Windows backend, which doesn't remove the
|
||||||
// re-created, or if it's moved to a different filesystem.
|
// watcher on renames.
|
||||||
//
|
//
|
||||||
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
||||||
// filesystems (/proc, /sys, etc.) generally don't work.
|
// filesystems (/proc, /sys, etc.) generally don't work.
|
||||||
//
|
//
|
||||||
|
// Returns [ErrClosed] if [Watcher.Close] was called.
|
||||||
|
//
|
||||||
|
// See [Watcher.AddWith] for a version that allows adding options.
|
||||||
|
//
|
||||||
// # Watching directories
|
// # Watching directories
|
||||||
//
|
//
|
||||||
// All files in a directory are monitored, including new files that are created
|
// All files in a directory are monitored, including new files that are created
|
||||||
|
@ -96,14 +118,27 @@ add=$(<<EOF
|
||||||
// # Watching files
|
// # Watching files
|
||||||
//
|
//
|
||||||
// Watching individual files (rather than directories) is generally not
|
// Watching individual files (rather than directories) is generally not
|
||||||
// recommended as many tools update files atomically. Instead of "just" writing
|
// recommended as many programs (especially editors) update files atomically: it
|
||||||
// to the file a temporary file will be written to first, and if successful the
|
// will write to a temporary file which is then moved to to destination,
|
||||||
// temporary file is moved to to destination removing the original, or some
|
// overwriting the original (or some variant thereof). The watcher on the
|
||||||
// variant thereof. The watcher on the original file is now lost, as it no
|
// original file is now lost, as that no longer exists.
|
||||||
// longer exists.
|
|
||||||
//
|
//
|
||||||
// Instead, watch the parent directory and use Event.Name to filter out files
|
// The upshot of this is that a power failure or crash won't leave a
|
||||||
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
|
// half-written file.
|
||||||
|
//
|
||||||
|
// Watch the parent directory and use Event.Name to filter out files you're not
|
||||||
|
// interested in. There is an example of this in cmd/fsnotify/file.go.
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
addwith=$(<<EOF
|
||||||
|
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
|
||||||
|
// the defaults described below are used.
|
||||||
|
//
|
||||||
|
// Possible options are:
|
||||||
|
//
|
||||||
|
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
|
||||||
|
// other platforms. The default is 64K (65536 bytes).
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -114,16 +149,21 @@ remove=$(<<EOF
|
||||||
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
||||||
//
|
//
|
||||||
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
close=$(<<EOF
|
close=$(<<EOF
|
||||||
// Close removes all watches and closes the events channel.
|
// Close removes all watches and closes the Events channel.
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
watchlist=$(<<EOF
|
watchlist=$(<<EOF
|
||||||
// WatchList returns all paths added with [Add] (and are not yet removed).
|
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
|
||||||
|
// yet removed).
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -153,20 +193,29 @@ events=$(<<EOF
|
||||||
// initiated by the user may show up as one or multiple
|
// initiated by the user may show up as one or multiple
|
||||||
// writes, depending on when the system syncs things to
|
// writes, depending on when the system syncs things to
|
||||||
// disk. For example when compiling a large Go program
|
// disk. For example when compiling a large Go program
|
||||||
// you may get hundreds of Write events, so you
|
// you may get hundreds of Write events, and you may
|
||||||
// probably want to wait until you've stopped receiving
|
// want to wait until you've stopped receiving them
|
||||||
// them (see the dedup example in cmd/fsnotify).
|
// (see the dedup example in cmd/fsnotify).
|
||||||
|
//
|
||||||
|
// Some systems may send Write event for directories
|
||||||
|
// when the directory content changes.
|
||||||
//
|
//
|
||||||
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
||||||
// when a file is removed (or more accurately, when a
|
// when a file is removed (or more accurately, when a
|
||||||
// link to an inode is removed). On kqueue it's sent
|
// link to an inode is removed). On kqueue it's sent
|
||||||
// and on kqueue when a file is truncated. On Windows
|
// when a file is truncated. On Windows it's never
|
||||||
// it's never sent.
|
// sent.
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
errors=$(<<EOF
|
errors=$(<<EOF
|
||||||
// Errors sends any errors.
|
// Errors sends any errors.
|
||||||
|
//
|
||||||
|
// ErrEventOverflow is used to indicate there are too many events:
|
||||||
|
//
|
||||||
|
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
|
||||||
|
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
|
||||||
|
// - kqueue, fen: Not used.
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -200,7 +249,9 @@ set-cmt() {
|
||||||
|
|
||||||
set-cmt '^type Watcher struct ' $watcher
|
set-cmt '^type Watcher struct ' $watcher
|
||||||
set-cmt '^func NewWatcher(' $new
|
set-cmt '^func NewWatcher(' $new
|
||||||
|
set-cmt '^func NewBufferedWatcher(' $newbuffered
|
||||||
set-cmt '^func (w \*Watcher) Add(' $add
|
set-cmt '^func (w \*Watcher) Add(' $add
|
||||||
|
set-cmt '^func (w \*Watcher) AddWith(' $addwith
|
||||||
set-cmt '^func (w \*Watcher) Remove(' $remove
|
set-cmt '^func (w \*Watcher) Remove(' $remove
|
||||||
set-cmt '^func (w \*Watcher) Close(' $close
|
set-cmt '^func (w \*Watcher) Close(' $close
|
||||||
set-cmt '^func (w \*Watcher) WatchList(' $watchlist
|
set-cmt '^func (w \*Watcher) WatchList(' $watchlist
|
||||||
|
|
|
@ -8,12 +8,13 @@ import (
|
||||||
// Token is a single token unit with an attribute value (if given) and hash of the data.
|
// Token is a single token unit with an attribute value (if given) and hash of the data.
|
||||||
type Token struct {
|
type Token struct {
|
||||||
html.TokenType
|
html.TokenType
|
||||||
Hash Hash
|
Hash Hash
|
||||||
Data []byte
|
Data []byte
|
||||||
Text []byte
|
Text []byte
|
||||||
AttrVal []byte
|
AttrVal []byte
|
||||||
Traits traits
|
Traits traits
|
||||||
Offset int
|
Offset int
|
||||||
|
HasTemplate bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenBuffer is a buffer that allows for token look-ahead.
|
// TokenBuffer is a buffer that allows for token look-ahead.
|
||||||
|
@ -40,10 +41,11 @@ func (z *TokenBuffer) read(t *Token) {
|
||||||
t.Offset = z.r.Offset()
|
t.Offset = z.r.Offset()
|
||||||
t.TokenType, t.Data = z.l.Next()
|
t.TokenType, t.Data = z.l.Next()
|
||||||
t.Text = z.l.Text()
|
t.Text = z.l.Text()
|
||||||
|
t.HasTemplate = z.l.HasTemplate()
|
||||||
if t.TokenType == html.AttributeToken {
|
if t.TokenType == html.AttributeToken {
|
||||||
t.Offset += 1 + len(t.Text) + 1
|
t.Offset += 1 + len(t.Text) + 1
|
||||||
t.AttrVal = z.l.AttrVal()
|
t.AttrVal = z.l.AttrVal()
|
||||||
if len(t.AttrVal) > 1 && (t.AttrVal[0] == '"' || t.AttrVal[0] == '\'') {
|
if 1 < len(t.AttrVal) && (t.AttrVal[0] == '"' || t.AttrVal[0] == '\'') {
|
||||||
t.Offset++
|
t.Offset++
|
||||||
t.AttrVal = t.AttrVal[1 : len(t.AttrVal)-1] // quotes will be readded in attribute loop if necessary
|
t.AttrVal = t.AttrVal[1 : len(t.AttrVal)-1] // quotes will be readded in attribute loop if necessary
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -41,6 +41,13 @@ var (
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
var GoTemplateDelims = [2]string{"{{", "}}"}
|
||||||
|
var HandlebarsTemplateDelims = [2]string{"{{", "}}"}
|
||||||
|
var MustacheTemplateDelims = [2]string{"{{", "}}"}
|
||||||
|
var EJSTemplateDelims = [2]string{"<%", "%>"}
|
||||||
|
var ASPTemplateDelims = [2]string{"<%", "%>"}
|
||||||
|
var PHPTemplateDelims = [2]string{"<?", "?>"}
|
||||||
|
|
||||||
// Minifier is an HTML minifier.
|
// Minifier is an HTML minifier.
|
||||||
type Minifier struct {
|
type Minifier struct {
|
||||||
KeepComments bool
|
KeepComments bool
|
||||||
|
@ -50,6 +57,7 @@ type Minifier struct {
|
||||||
KeepEndTags bool
|
KeepEndTags bool
|
||||||
KeepQuotes bool
|
KeepQuotes bool
|
||||||
KeepWhitespace bool
|
KeepWhitespace bool
|
||||||
|
TemplateDelims [2]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Minify minifies HTML data, it reads from r and writes to w.
|
// Minify minifies HTML data, it reads from r and writes to w.
|
||||||
|
@ -71,7 +79,7 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
|
||||||
z := parse.NewInput(r)
|
z := parse.NewInput(r)
|
||||||
defer z.Restore()
|
defer z.Restore()
|
||||||
|
|
||||||
l := html.NewLexer(z)
|
l := html.NewTemplateLexer(z, o.TemplateDelims)
|
||||||
tb := NewTokenBuffer(z, l)
|
tb := NewTokenBuffer(z, l)
|
||||||
for {
|
for {
|
||||||
t := *tb.Shift()
|
t := *tb.Shift()
|
||||||
|
@ -126,8 +134,9 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
|
||||||
w.Write(t.Data)
|
w.Write(t.Data)
|
||||||
}
|
}
|
||||||
case html.TextToken:
|
case html.TextToken:
|
||||||
// CSS and JS minifiers for inline code
|
if t.HasTemplate {
|
||||||
if rawTagHash != 0 {
|
w.Write(t.Data)
|
||||||
|
} else if rawTagHash != 0 {
|
||||||
if rawTagHash == Style || rawTagHash == Script || rawTagHash == Iframe {
|
if rawTagHash == Style || rawTagHash == Script || rawTagHash == Iframe {
|
||||||
var mimetype []byte
|
var mimetype []byte
|
||||||
var params map[string]string
|
var params map[string]string
|
||||||
|
@ -372,6 +381,9 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
|
||||||
break
|
break
|
||||||
} else if attr.Text == nil {
|
} else if attr.Text == nil {
|
||||||
continue // removed attribute
|
continue // removed attribute
|
||||||
|
} else if attr.HasTemplate {
|
||||||
|
w.Write(attr.Data)
|
||||||
|
continue // don't minify attributes that contain templates
|
||||||
}
|
}
|
||||||
|
|
||||||
val := attr.AttrVal
|
val := attr.AttrVal
|
||||||
|
@ -389,35 +401,30 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
|
||||||
attr.Hash == Action && t.Hash == Form) {
|
attr.Hash == Action && t.Hash == Form) {
|
||||||
continue // omit empty attribute values
|
continue // omit empty attribute values
|
||||||
}
|
}
|
||||||
if attr.Traits&caselessAttr != 0 {
|
|
||||||
val = parse.ToLower(val)
|
|
||||||
}
|
|
||||||
if rawTagHash != 0 && attr.Hash == Type {
|
if rawTagHash != 0 && attr.Hash == Type {
|
||||||
rawTagMediatype = parse.Copy(val)
|
rawTagMediatype = parse.Copy(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
if attr.Hash == Enctype || attr.Hash == Codetype || attr.Hash == Accept || attr.Hash == Type && (t.Hash == A || t.Hash == Link || t.Hash == Embed || t.Hash == Object || t.Hash == Source || t.Hash == Script || t.Hash == Style) {
|
if attr.Hash == Enctype ||
|
||||||
|
attr.Hash == Formenctype ||
|
||||||
|
attr.Hash == Accept ||
|
||||||
|
attr.Hash == Type && (t.Hash == A || t.Hash == Link || t.Hash == Embed || t.Hash == Object || t.Hash == Source || t.Hash == Script) {
|
||||||
val = minify.Mediatype(val)
|
val = minify.Mediatype(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
// default attribute values can be omitted
|
// default attribute values can be omitted
|
||||||
if !o.KeepDefaultAttrVals && (attr.Hash == Type && (t.Hash == Script && jsMimetypes[string(val)] ||
|
if !o.KeepDefaultAttrVals && (attr.Hash == Type && (t.Hash == Script && jsMimetypes[string(parse.ToLower(parse.Copy(val)))] ||
|
||||||
t.Hash == Style && bytes.Equal(val, cssMimeBytes) ||
|
t.Hash == Style && parse.EqualFold(val, cssMimeBytes) ||
|
||||||
t.Hash == Link && bytes.Equal(val, cssMimeBytes) ||
|
t.Hash == Link && parse.EqualFold(val, cssMimeBytes) ||
|
||||||
t.Hash == Input && bytes.Equal(val, textBytes) ||
|
t.Hash == Input && parse.EqualFold(val, textBytes) ||
|
||||||
t.Hash == Button && bytes.Equal(val, submitBytes)) ||
|
t.Hash == Button && parse.EqualFold(val, submitBytes)) ||
|
||||||
attr.Hash == Language && t.Hash == Script ||
|
attr.Hash == Method && parse.EqualFold(val, getBytes) ||
|
||||||
attr.Hash == Method && bytes.Equal(val, getBytes) ||
|
attr.Hash == Enctype && parse.EqualFold(val, formMimeBytes) ||
|
||||||
attr.Hash == Enctype && bytes.Equal(val, formMimeBytes) ||
|
|
||||||
attr.Hash == Colspan && bytes.Equal(val, oneBytes) ||
|
attr.Hash == Colspan && bytes.Equal(val, oneBytes) ||
|
||||||
attr.Hash == Rowspan && bytes.Equal(val, oneBytes) ||
|
attr.Hash == Rowspan && bytes.Equal(val, oneBytes) ||
|
||||||
attr.Hash == Shape && bytes.Equal(val, rectBytes) ||
|
attr.Hash == Shape && parse.EqualFold(val, rectBytes) ||
|
||||||
attr.Hash == Span && bytes.Equal(val, oneBytes) ||
|
attr.Hash == Span && bytes.Equal(val, oneBytes) ||
|
||||||
attr.Hash == Clear && bytes.Equal(val, noneBytes) ||
|
attr.Hash == Media && t.Hash == Style && parse.EqualFold(val, allBytes)) {
|
||||||
attr.Hash == Frameborder && bytes.Equal(val, oneBytes) ||
|
|
||||||
attr.Hash == Scrolling && bytes.Equal(val, autoBytes) ||
|
|
||||||
attr.Hash == Valuetype && bytes.Equal(val, dataBytes) ||
|
|
||||||
attr.Hash == Media && t.Hash == Style && bytes.Equal(val, allBytes)) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -440,7 +447,7 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
|
||||||
val = val[11:]
|
val = val[11:]
|
||||||
}
|
}
|
||||||
attrMinifyBuffer.Reset()
|
attrMinifyBuffer.Reset()
|
||||||
if err := m.MinifyMimetype(jsMimeBytes, attrMinifyBuffer, buffer.NewReader(val), nil); err == nil {
|
if err := m.MinifyMimetype(jsMimeBytes, attrMinifyBuffer, buffer.NewReader(val), inlineParams); err == nil {
|
||||||
val = attrMinifyBuffer.Bytes()
|
val = attrMinifyBuffer.Bytes()
|
||||||
} else if err != minify.ErrNotExist {
|
} else if err != minify.ErrNotExist {
|
||||||
return minify.UpdateErrorPosition(err, z, attr.Offset)
|
return minify.UpdateErrorPosition(err, z, attr.Offset)
|
||||||
|
|
|
@ -13,7 +13,6 @@ const (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
booleanAttr traits = 1 << iota
|
booleanAttr traits = 1 << iota
|
||||||
caselessAttr
|
|
||||||
urlAttr
|
urlAttr
|
||||||
trimAttr
|
trimAttr
|
||||||
)
|
)
|
||||||
|
@ -163,106 +162,124 @@ var tagMap = map[Hash]traits{
|
||||||
}
|
}
|
||||||
|
|
||||||
var attrMap = map[Hash]traits{
|
var attrMap = map[Hash]traits{
|
||||||
Accept: trimAttr,
|
Accept: trimAttr, // list of mimetypes
|
||||||
Accept_Charset: caselessAttr,
|
Accept_Charset: trimAttr,
|
||||||
Action: urlAttr,
|
Accesskey: trimAttr,
|
||||||
Align: caselessAttr,
|
Action: urlAttr,
|
||||||
Alink: caselessAttr,
|
Allow: trimAttr,
|
||||||
Allowfullscreen: booleanAttr,
|
Allowfullscreen: booleanAttr,
|
||||||
Async: booleanAttr,
|
As: trimAttr,
|
||||||
Autofocus: booleanAttr,
|
Async: booleanAttr,
|
||||||
Autoplay: booleanAttr,
|
Autocapitalize: trimAttr,
|
||||||
Axis: caselessAttr,
|
Autocomplete: trimAttr,
|
||||||
Background: urlAttr,
|
Autofocus: booleanAttr,
|
||||||
Bgcolor: caselessAttr,
|
Autoplay: booleanAttr,
|
||||||
Charset: caselessAttr,
|
Blocking: trimAttr,
|
||||||
Checked: booleanAttr,
|
Capture: trimAttr,
|
||||||
Cite: urlAttr,
|
Charset: trimAttr,
|
||||||
Class: trimAttr,
|
Checked: booleanAttr,
|
||||||
Classid: urlAttr,
|
Cite: urlAttr,
|
||||||
Clear: caselessAttr,
|
Class: trimAttr,
|
||||||
Codebase: urlAttr,
|
Color: trimAttr,
|
||||||
Codetype: trimAttr,
|
Cols: trimAttr, // uint bigger than 0
|
||||||
Color: caselessAttr,
|
Colspan: trimAttr, // uint bigger than 0
|
||||||
Cols: trimAttr,
|
Contenteditable: trimAttr,
|
||||||
Colspan: trimAttr,
|
Controls: booleanAttr,
|
||||||
Compact: booleanAttr,
|
Coords: trimAttr, // list of floats
|
||||||
Controls: booleanAttr,
|
Crossorigin: trimAttr,
|
||||||
Data: urlAttr,
|
Data: urlAttr,
|
||||||
Declare: booleanAttr,
|
Datetime: trimAttr,
|
||||||
Default: booleanAttr,
|
Decoding: trimAttr,
|
||||||
DefaultChecked: booleanAttr,
|
Default: booleanAttr,
|
||||||
DefaultMuted: booleanAttr,
|
Defer: booleanAttr,
|
||||||
DefaultSelected: booleanAttr,
|
Dir: trimAttr,
|
||||||
Defer: booleanAttr,
|
Disabled: booleanAttr,
|
||||||
Dir: caselessAttr,
|
Draggable: trimAttr,
|
||||||
Disabled: booleanAttr,
|
Enctype: trimAttr, // mimetype
|
||||||
Enabled: booleanAttr,
|
Enterkeyhint: trimAttr,
|
||||||
Enctype: trimAttr,
|
Fetchpriority: trimAttr,
|
||||||
Face: caselessAttr,
|
For: trimAttr,
|
||||||
Formaction: urlAttr,
|
Form: trimAttr,
|
||||||
Formnovalidate: booleanAttr,
|
Formaction: urlAttr,
|
||||||
Frame: caselessAttr,
|
Formenctype: trimAttr, // mimetype
|
||||||
Hidden: booleanAttr,
|
Formmethod: trimAttr,
|
||||||
Href: urlAttr,
|
Formnovalidate: booleanAttr,
|
||||||
Hreflang: caselessAttr,
|
Formtarget: trimAttr,
|
||||||
Http_Equiv: caselessAttr,
|
Headers: trimAttr,
|
||||||
Icon: urlAttr,
|
Height: trimAttr, // uint
|
||||||
Inert: booleanAttr,
|
Hidden: trimAttr, // TODO: boolean
|
||||||
Ismap: booleanAttr,
|
High: trimAttr, // float
|
||||||
Itemscope: booleanAttr,
|
Href: urlAttr,
|
||||||
Lang: trimAttr,
|
Hreflang: trimAttr, // BCP 47
|
||||||
Language: caselessAttr,
|
Http_Equiv: trimAttr,
|
||||||
Link: caselessAttr,
|
Imagesizes: trimAttr,
|
||||||
Longdesc: urlAttr,
|
Imagesrcset: trimAttr,
|
||||||
Manifest: urlAttr,
|
Inert: booleanAttr,
|
||||||
Maxlength: trimAttr,
|
Inputmode: trimAttr,
|
||||||
Media: caselessAttr | trimAttr,
|
Is: trimAttr,
|
||||||
Method: caselessAttr,
|
Ismap: booleanAttr,
|
||||||
Multiple: booleanAttr,
|
Itemid: urlAttr,
|
||||||
Muted: booleanAttr,
|
Itemprop: trimAttr,
|
||||||
Nohref: booleanAttr,
|
Itemref: trimAttr,
|
||||||
Noresize: booleanAttr,
|
Itemscope: booleanAttr,
|
||||||
Noshade: booleanAttr,
|
Itemtype: trimAttr, // list of urls
|
||||||
Novalidate: booleanAttr,
|
Kind: trimAttr,
|
||||||
Nowrap: booleanAttr,
|
Lang: trimAttr, // BCP 47
|
||||||
Open: booleanAttr,
|
List: trimAttr,
|
||||||
Pauseonexit: booleanAttr,
|
Loading: trimAttr,
|
||||||
Poster: urlAttr,
|
Loop: booleanAttr,
|
||||||
Profile: urlAttr,
|
Low: trimAttr, // float
|
||||||
Readonly: booleanAttr,
|
Max: trimAttr, // float or varies
|
||||||
Rel: caselessAttr | trimAttr,
|
Maxlength: trimAttr, // uint
|
||||||
Required: booleanAttr,
|
Media: trimAttr,
|
||||||
Rev: caselessAttr,
|
Method: trimAttr,
|
||||||
Reversed: booleanAttr,
|
Min: trimAttr, // float or varies
|
||||||
Rows: trimAttr,
|
Minlength: trimAttr, // uint
|
||||||
Rowspan: trimAttr,
|
Multiple: booleanAttr,
|
||||||
Rules: caselessAttr,
|
Muted: booleanAttr,
|
||||||
Scope: caselessAttr,
|
Nomodule: booleanAttr,
|
||||||
Scoped: booleanAttr,
|
Novalidate: booleanAttr,
|
||||||
Scrolling: caselessAttr,
|
Open: booleanAttr,
|
||||||
Seamless: booleanAttr,
|
Optimum: trimAttr, // float
|
||||||
Selected: booleanAttr,
|
Pattern: trimAttr, // regex
|
||||||
Shape: caselessAttr,
|
Ping: trimAttr, // list of urls
|
||||||
Size: trimAttr,
|
Playsinline: booleanAttr,
|
||||||
Sortable: booleanAttr,
|
Popover: trimAttr,
|
||||||
Span: trimAttr,
|
Popovertarget: trimAttr,
|
||||||
Src: urlAttr,
|
Popovertargetaction: trimAttr,
|
||||||
Srcset: trimAttr,
|
Poster: urlAttr,
|
||||||
Tabindex: trimAttr,
|
Preload: trimAttr,
|
||||||
Target: caselessAttr,
|
Profile: urlAttr,
|
||||||
Text: caselessAttr,
|
Readonly: booleanAttr,
|
||||||
Translate: caselessAttr,
|
Referrerpolicy: trimAttr,
|
||||||
Truespeed: booleanAttr,
|
Rel: trimAttr,
|
||||||
Type: trimAttr,
|
Required: booleanAttr,
|
||||||
Typemustmatch: booleanAttr,
|
Reversed: booleanAttr,
|
||||||
Undeterminate: booleanAttr,
|
Rows: trimAttr, // uint bigger than 0
|
||||||
Usemap: urlAttr,
|
Rowspan: trimAttr, // uint
|
||||||
Valign: caselessAttr,
|
Sandbox: trimAttr,
|
||||||
Valuetype: caselessAttr,
|
Scope: trimAttr,
|
||||||
Vlink: caselessAttr,
|
Selected: booleanAttr,
|
||||||
Visible: booleanAttr,
|
Shadowrootmode: trimAttr,
|
||||||
Xmlns: urlAttr,
|
Shadowrootdelegatesfocus: booleanAttr,
|
||||||
|
Shape: trimAttr,
|
||||||
|
Size: trimAttr, // uint bigger than 0
|
||||||
|
Sizes: trimAttr,
|
||||||
|
Span: trimAttr, // uint bigger than 0
|
||||||
|
Spellcheck: trimAttr,
|
||||||
|
Src: urlAttr,
|
||||||
|
Srclang: trimAttr, // BCP 47
|
||||||
|
Srcset: trimAttr,
|
||||||
|
Start: trimAttr, // int
|
||||||
|
Step: trimAttr, // float or "any"
|
||||||
|
Tabindex: trimAttr, // int
|
||||||
|
Target: trimAttr,
|
||||||
|
Translate: trimAttr,
|
||||||
|
Type: trimAttr,
|
||||||
|
Usemap: trimAttr,
|
||||||
|
Width: trimAttr, // uint
|
||||||
|
Wrap: trimAttr,
|
||||||
|
Xmlns: urlAttr,
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsMimetypes = map[string]bool{
|
var jsMimetypes = map[string]bool{
|
||||||
|
|
|
@ -56,16 +56,26 @@ func (tt TokenType) String() string {
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
var GoTemplate = [2]string{"{{", "}}"}
|
||||||
|
var HandlebarsTemplate = [2]string{"{{", "}}"}
|
||||||
|
var MustacheTemplate = [2]string{"{{", "}}"}
|
||||||
|
var EJSTemplate = [2]string{"<%", "%>"}
|
||||||
|
var ASPTemplate = [2]string{"<%", "%>"}
|
||||||
|
var PHPTemplate = [2]string{"<?", "?>"}
|
||||||
|
|
||||||
// Lexer is the state for the lexer.
|
// Lexer is the state for the lexer.
|
||||||
type Lexer struct {
|
type Lexer struct {
|
||||||
r *parse.Input
|
r *parse.Input
|
||||||
err error
|
tmplBegin []byte
|
||||||
|
tmplEnd []byte
|
||||||
|
err error
|
||||||
|
|
||||||
rawTag Hash
|
rawTag Hash
|
||||||
inTag bool
|
inTag bool
|
||||||
|
|
||||||
text []byte
|
text []byte
|
||||||
attrVal []byte
|
attrVal []byte
|
||||||
|
hasTmpl bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLexer returns a new Lexer for a given io.Reader.
|
// NewLexer returns a new Lexer for a given io.Reader.
|
||||||
|
@ -75,6 +85,14 @@ func NewLexer(r *parse.Input) *Lexer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewTemplateLexer(r *parse.Input, tmpl [2]string) *Lexer {
|
||||||
|
return &Lexer{
|
||||||
|
r: r,
|
||||||
|
tmplBegin: []byte(tmpl[0]),
|
||||||
|
tmplEnd: []byte(tmpl[1]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Err returns the error encountered during lexing, this is often io.EOF but also other errors can be returned.
|
// Err returns the error encountered during lexing, this is often io.EOF but also other errors can be returned.
|
||||||
func (l *Lexer) Err() error {
|
func (l *Lexer) Err() error {
|
||||||
if l.err != nil {
|
if l.err != nil {
|
||||||
|
@ -88,14 +106,25 @@ func (l *Lexer) Text() []byte {
|
||||||
return l.text
|
return l.text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AttrKey returns the attribute key when an AttributeToken was returned from Next.
|
||||||
|
func (l *Lexer) AttrKey() []byte {
|
||||||
|
return l.text
|
||||||
|
}
|
||||||
|
|
||||||
// AttrVal returns the attribute value when an AttributeToken was returned from Next.
|
// AttrVal returns the attribute value when an AttributeToken was returned from Next.
|
||||||
func (l *Lexer) AttrVal() []byte {
|
func (l *Lexer) AttrVal() []byte {
|
||||||
return l.attrVal
|
return l.attrVal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasTemplate returns the true if the token value contains a template.
|
||||||
|
func (l *Lexer) HasTemplate() bool {
|
||||||
|
return l.hasTmpl
|
||||||
|
}
|
||||||
|
|
||||||
// Next returns the next Token. It returns ErrorToken when an error was encountered. Using Err() one can retrieve the error message.
|
// Next returns the next Token. It returns ErrorToken when an error was encountered. Using Err() one can retrieve the error message.
|
||||||
func (l *Lexer) Next() (TokenType, []byte) {
|
func (l *Lexer) Next() (TokenType, []byte) {
|
||||||
l.text = nil
|
l.text = nil
|
||||||
|
l.hasTmpl = false
|
||||||
var c byte
|
var c byte
|
||||||
if l.inTag {
|
if l.inTag {
|
||||||
l.attrVal = nil
|
l.attrVal = nil
|
||||||
|
@ -122,7 +151,7 @@ func (l *Lexer) Next() (TokenType, []byte) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.rawTag != 0 {
|
if l.rawTag != 0 {
|
||||||
if rawText := l.shiftRawText(); len(rawText) > 0 {
|
if rawText := l.shiftRawText(); 0 < len(rawText) {
|
||||||
l.text = rawText
|
l.text = rawText
|
||||||
l.rawTag = 0
|
l.rawTag = 0
|
||||||
return TextToken, rawText
|
return TextToken, rawText
|
||||||
|
@ -135,12 +164,12 @@ func (l *Lexer) Next() (TokenType, []byte) {
|
||||||
if c == '<' {
|
if c == '<' {
|
||||||
c = l.r.Peek(1)
|
c = l.r.Peek(1)
|
||||||
isEndTag := c == '/' && l.r.Peek(2) != '>' && (l.r.Peek(2) != 0 || l.r.PeekErr(2) == nil)
|
isEndTag := c == '/' && l.r.Peek(2) != '>' && (l.r.Peek(2) != 0 || l.r.PeekErr(2) == nil)
|
||||||
if l.r.Pos() > 0 {
|
if !isEndTag && (c < 'a' || 'z' < c) && (c < 'A' || 'Z' < c) && c != '!' && c != '?' {
|
||||||
if isEndTag || 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '!' || c == '?' {
|
// not a tag
|
||||||
// return currently buffered texttoken so that we can return tag next iteration
|
} else if 0 < l.r.Pos() {
|
||||||
l.text = l.r.Shift()
|
// return currently buffered texttoken so that we can return tag next iteration
|
||||||
return TextToken, l.text
|
l.text = l.r.Shift()
|
||||||
}
|
return TextToken, l.text
|
||||||
} else if isEndTag {
|
} else if isEndTag {
|
||||||
l.r.Move(2)
|
l.r.Move(2)
|
||||||
// only endtags that are not followed by > or EOF arrive here
|
// only endtags that are not followed by > or EOF arrive here
|
||||||
|
@ -159,8 +188,12 @@ func (l *Lexer) Next() (TokenType, []byte) {
|
||||||
l.r.Move(1)
|
l.r.Move(1)
|
||||||
return CommentToken, l.shiftBogusComment()
|
return CommentToken, l.shiftBogusComment()
|
||||||
}
|
}
|
||||||
|
} else if 0 < len(l.tmplBegin) && l.at(l.tmplBegin...) {
|
||||||
|
l.r.Move(len(l.tmplBegin))
|
||||||
|
l.moveTemplate()
|
||||||
|
l.hasTmpl = true
|
||||||
} else if c == 0 && l.r.Err() != nil {
|
} else if c == 0 && l.r.Err() != nil {
|
||||||
if l.r.Pos() > 0 {
|
if 0 < l.r.Pos() {
|
||||||
l.text = l.r.Shift()
|
l.text = l.r.Shift()
|
||||||
return TextToken, l.text
|
return TextToken, l.text
|
||||||
}
|
}
|
||||||
|
@ -241,6 +274,10 @@ func (l *Lexer) shiftRawText() []byte {
|
||||||
} else {
|
} else {
|
||||||
l.r.Move(1)
|
l.r.Move(1)
|
||||||
}
|
}
|
||||||
|
} else if 0 < len(l.tmplBegin) && l.at(l.tmplBegin...) {
|
||||||
|
l.r.Move(len(l.tmplBegin))
|
||||||
|
l.moveTemplate()
|
||||||
|
l.hasTmpl = true
|
||||||
} else if c == 0 && l.r.Err() != nil {
|
} else if c == 0 && l.r.Err() != nil {
|
||||||
return l.r.Shift()
|
return l.r.Shift()
|
||||||
} else {
|
} else {
|
||||||
|
@ -346,6 +383,11 @@ func (l *Lexer) shiftStartTag() (TokenType, []byte) {
|
||||||
func (l *Lexer) shiftAttribute() []byte {
|
func (l *Lexer) shiftAttribute() []byte {
|
||||||
nameStart := l.r.Pos()
|
nameStart := l.r.Pos()
|
||||||
var c byte
|
var c byte
|
||||||
|
if 0 < len(l.tmplBegin) && l.at(l.tmplBegin...) {
|
||||||
|
l.r.Move(len(l.tmplBegin))
|
||||||
|
l.moveTemplate()
|
||||||
|
l.hasTmpl = true
|
||||||
|
}
|
||||||
for { // attribute name state
|
for { // attribute name state
|
||||||
if c = l.r.Peek(0); c == ' ' || c == '=' || c == '>' || c == '/' && l.r.Peek(1) == '>' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == 0 && l.r.Err() != nil {
|
if c = l.r.Peek(0); c == ' ' || c == '=' || c == '>' || c == '/' && l.r.Peek(1) == '>' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == 0 && l.r.Err() != nil {
|
||||||
break
|
break
|
||||||
|
@ -360,6 +402,7 @@ func (l *Lexer) shiftAttribute() []byte {
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
nameHasTmpl := l.hasTmpl
|
||||||
if c == '=' {
|
if c == '=' {
|
||||||
l.r.Move(1)
|
l.r.Move(1)
|
||||||
for { // before attribute value state
|
for { // before attribute value state
|
||||||
|
@ -378,11 +421,20 @@ func (l *Lexer) shiftAttribute() []byte {
|
||||||
if c == delim {
|
if c == delim {
|
||||||
l.r.Move(1)
|
l.r.Move(1)
|
||||||
break
|
break
|
||||||
|
} else if 0 < len(l.tmplBegin) && l.at(l.tmplBegin...) {
|
||||||
|
l.r.Move(len(l.tmplBegin))
|
||||||
|
l.moveTemplate()
|
||||||
|
l.hasTmpl = true
|
||||||
} else if c == 0 && l.r.Err() != nil {
|
} else if c == 0 && l.r.Err() != nil {
|
||||||
break
|
break
|
||||||
|
} else {
|
||||||
|
l.r.Move(1)
|
||||||
}
|
}
|
||||||
l.r.Move(1)
|
|
||||||
}
|
}
|
||||||
|
} else if 0 < len(l.tmplBegin) && l.at(l.tmplBegin...) {
|
||||||
|
l.r.Move(len(l.tmplBegin))
|
||||||
|
l.moveTemplate()
|
||||||
|
l.hasTmpl = true
|
||||||
} else { // attribute value unquoted state
|
} else { // attribute value unquoted state
|
||||||
for {
|
for {
|
||||||
if c := l.r.Peek(0); c == ' ' || c == '>' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == 0 && l.r.Err() != nil {
|
if c := l.r.Peek(0); c == ' ' || c == '>' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == 0 && l.r.Err() != nil {
|
||||||
|
@ -396,7 +448,15 @@ func (l *Lexer) shiftAttribute() []byte {
|
||||||
l.r.Rewind(nameEnd)
|
l.r.Rewind(nameEnd)
|
||||||
l.attrVal = nil
|
l.attrVal = nil
|
||||||
}
|
}
|
||||||
l.text = parse.ToLower(l.r.Lexeme()[nameStart:nameEnd])
|
if 0 < len(l.tmplBegin) && l.at(l.tmplBegin...) {
|
||||||
|
l.r.Move(len(l.tmplBegin))
|
||||||
|
l.moveTemplate()
|
||||||
|
l.hasTmpl = true
|
||||||
|
}
|
||||||
|
l.text = l.r.Lexeme()[nameStart:nameEnd]
|
||||||
|
if !nameHasTmpl {
|
||||||
|
l.text = parse.ToLower(l.text)
|
||||||
|
}
|
||||||
return l.r.Shift()
|
return l.r.Shift()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -473,6 +533,35 @@ func (l *Lexer) shiftXML(rawTag Hash) []byte {
|
||||||
return l.r.Shift()
|
return l.r.Shift()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) moveTemplate() {
|
||||||
|
for {
|
||||||
|
if c := l.r.Peek(0); l.at(l.tmplEnd...) || c == 0 && l.r.Err() != nil {
|
||||||
|
if c != 0 {
|
||||||
|
l.r.Move(len(l.tmplEnd))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} else if c == '"' || c == '\'' {
|
||||||
|
l.r.Move(1)
|
||||||
|
escape := false
|
||||||
|
for {
|
||||||
|
if c2 := l.r.Peek(0); !escape && c2 == c || c2 == 0 && l.r.Err() != nil {
|
||||||
|
if c2 != 0 {
|
||||||
|
l.r.Move(1)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} else if c2 == '\\' {
|
||||||
|
escape = !escape
|
||||||
|
} else {
|
||||||
|
escape = false
|
||||||
|
}
|
||||||
|
l.r.Move(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
l.r.Move(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
func (l *Lexer) at(b ...byte) bool {
|
func (l *Lexer) at(b ...byte) bool {
|
||||||
|
|
|
@ -167,8 +167,8 @@ github.com/dsoprea/go-utility/v2/image
|
||||||
# github.com/dustin/go-humanize v1.0.1
|
# github.com/dustin/go-humanize v1.0.1
|
||||||
## explicit; go 1.16
|
## explicit; go 1.16
|
||||||
github.com/dustin/go-humanize
|
github.com/dustin/go-humanize
|
||||||
# github.com/fsnotify/fsnotify v1.6.0
|
# github.com/fsnotify/fsnotify v1.7.0
|
||||||
## explicit; go 1.16
|
## explicit; go 1.17
|
||||||
github.com/fsnotify/fsnotify
|
github.com/fsnotify/fsnotify
|
||||||
# github.com/gabriel-vasile/mimetype v1.4.2
|
# github.com/gabriel-vasile/mimetype v1.4.2
|
||||||
## explicit; go 1.20
|
## explicit; go 1.20
|
||||||
|
@ -636,11 +636,11 @@ github.com/superseriousbusiness/oauth2/v4/generates
|
||||||
github.com/superseriousbusiness/oauth2/v4/manage
|
github.com/superseriousbusiness/oauth2/v4/manage
|
||||||
github.com/superseriousbusiness/oauth2/v4/models
|
github.com/superseriousbusiness/oauth2/v4/models
|
||||||
github.com/superseriousbusiness/oauth2/v4/server
|
github.com/superseriousbusiness/oauth2/v4/server
|
||||||
# github.com/tdewolff/minify/v2 v2.20.0
|
# github.com/tdewolff/minify/v2 v2.20.6
|
||||||
## explicit; go 1.18
|
## explicit; go 1.18
|
||||||
github.com/tdewolff/minify/v2
|
github.com/tdewolff/minify/v2
|
||||||
github.com/tdewolff/minify/v2/html
|
github.com/tdewolff/minify/v2/html
|
||||||
# github.com/tdewolff/parse/v2 v2.7.0
|
# github.com/tdewolff/parse/v2 v2.7.4
|
||||||
## explicit; go 1.13
|
## explicit; go 1.13
|
||||||
github.com/tdewolff/parse/v2
|
github.com/tdewolff/parse/v2
|
||||||
github.com/tdewolff/parse/v2/buffer
|
github.com/tdewolff/parse/v2/buffer
|
||||||
|
|
Loading…
Reference in New Issue