Merge pull request #13343 from containerd/dependabot/go_modules/github.com/fsnotify/fsnotify-1.10.1

build(deps): bump github.com/fsnotify/fsnotify from 1.9.0 to 1.10.1
This commit is contained in:
Maksym Pavlenko
2026-07-23 22:15:51 +00:00
committed by GitHub
23 changed files with 327 additions and 288 deletions

2
go.mod
View File

@@ -38,7 +38,7 @@ require (
github.com/docker/go-metrics v0.0.1
github.com/docker/go-units v0.5.0
github.com/erofs/go-erofs v0.3.1
github.com/fsnotify/fsnotify v1.9.0
github.com/fsnotify/fsnotify v1.10.1
github.com/google/certtostore v1.0.6
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0

4
go.sum
View File

@@ -111,8 +111,8 @@ github.com/erofs/go-erofs v0.3.1 h1:Sux82Jq9yvyYhIoLgSHDp741p/+370HsOj9dAh1+VVs=
github.com/erofs/go-erofs v0.3.1/go.mod h1:XkSeN9MHszGd4+3gcEjadJLYHCQpWzJ7/8yznzMuzJs=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=

View File

@@ -1,14 +0,0 @@
freebsd_task:
name: 'FreeBSD'
freebsd_instance:
image_family: freebsd-14-2
install_script:
- pkg update -f
- pkg install -y go
test_script:
# run tests as user "cirrus" instead of root
- pw useradd cirrus -m
- chown -R cirrus:cirrus .
- FSNOTIFY_BUFFER=4096 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./...
- sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./...
- FSNOTIFY_DEBUG=1 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race -v ./...

View File

@@ -1,5 +1,54 @@
# Changelog
1.10.1 2026-05-04
-----------------
### Changes and fixes
- inotify: don't remove sibling watches sharing a path prefix ([#754])
- inotify, windows: don't rename sibling watches sharing a path prefix
([#755])
[#754]: https://github.com/fsnotify/fsnotify/pull/754
[#755]: https://github.com/fsnotify/fsnotify/pull/755
1.10.0 2026-04-30
-----------------
This version of fsnotify needs Go 1.23.
### Changes and fixes
- inotify: improve initialization error message ([#731])
- inotify: send Rename event if recursive watch is renamed ([#696])
- inotify: avoid copying event buffers when reading names ([#741])
- kqueue: skip dangling symlinks (ENOENT) in watchDirectoryFiles, so a
bad entry no longer aborts Watcher.Add for the whole directory ([#748])
- kqueue: drop watches directly in Close() to fix a file descriptor leak
when recycling watchers ([#740])
- windows: fix nil pointer dereference in remWatch ([#736])
- windows: lock watch field updates against concurrent WatchList to fix
a race introduced in v1.9.0 ([#709], [#749])
[#696]: https://github.com/fsnotify/fsnotify/pull/696
[#709]: https://github.com/fsnotify/fsnotify/pull/709
[#731]: https://github.com/fsnotify/fsnotify/pull/731
[#736]: https://github.com/fsnotify/fsnotify/pull/736
[#740]: https://github.com/fsnotify/fsnotify/pull/740
[#741]: https://github.com/fsnotify/fsnotify/pull/741
[#748]: https://github.com/fsnotify/fsnotify/pull/748
[#749]: https://github.com/fsnotify/fsnotify/pull/749
1.9.0 2024-04-04
----------------

View File

@@ -77,6 +77,8 @@ End-of-line escapes with `\` are not supported.
debug [yes/no] # Enable/disable FSNOTIFY_DEBUG (tests are run in
parallel by default, so -parallel=1 is probably a good
idea).
state # Print internal state to stderr (exact output differs
# per backend).
print [any strings] # Print text to stdout; for debugging.
touch path

View File

@@ -1,7 +1,7 @@
fsnotify is a Go library to provide cross-platform filesystem notifications on
Windows, Linux, macOS, BSD, and illumos.
Go 1.17 or newer is required; the full documentation is at
Go 1.23 or newer is required; the full documentation is at
https://pkg.go.dev/github.com/fsnotify/fsnotify
---
@@ -12,7 +12,7 @@ Platform support:
| :-------------------- | :--------- | :------------------------------------------------------------------------ |
| inotify | Linux | Supported |
| kqueue | BSD, macOS | Supported |
| ReadDirectoryChangesW | Windows | Supported |
| ReadDirectoryChangesW | Windows | Supported ([excluding `Chmod` operations][#487]) |
| FEN | illumos | Supported |
| fanotify | Linux 5.9+ | [Not yet](https://github.com/fsnotify/fsnotify/issues/114) |
| FSEvents | macOS | [Needs support in x/sys/unix][fsevents] |
@@ -22,6 +22,7 @@ Platform support:
Linux and illumos should include Android and Solaris, but these are currently
untested.
[#487]: https://github.com/fsnotify/fsnotify/issues/487
[fsevents]: https://github.com/fsnotify/fsnotify/issues/11#issuecomment-1279133120
[usn]: https://github.com/fsnotify/fsnotify/issues/53#issuecomment-1279829847
@@ -126,7 +127,7 @@ settings* until we have a native FSEvents implementation (see [#11]).
### 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
a temporary file which is then moved to a destination, overwriting the original
(or some variant thereof). The watcher on the original file is now lost, as that
no longer exists.
@@ -151,26 +152,57 @@ 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".
"instance", and every path you add is a "watch". Reaching the limit will result
in a "no space left on device" or "too many open files" error.
These are also exposed in `/proc` as `/proc/sys/fs/inotify/max_user_watches` and
`/proc/sys/fs/inotify/max_user_instances`
`/proc/sys/fs/inotify/max_user_instances`. The default values differ per distro
and available memory.
To increase them you can use `sysctl` or write the value to proc file:
# The default values on Linux 5.18
sysctl fs.inotify.max_user_watches=124983
sysctl fs.inotify.max_user_instances=128
sysctl fs.inotify.max_user_watches=200000
sysctl fs.inotify.max_user_instances=256
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
fs.inotify.max_user_watches=200000
fs.inotify.max_user_instances=256
### Windows
Recursive watching is not currently enabled through fsnotify's public API
(see the FAQ "Are subdirectories watched?" above). The notes below
describe Windows backend behavior observed when recursive watching is
enabled internally (for example, in fsnotify's own tests). They are kept
here as a reference for maintainers and contributors who encounter the
behavior, since the recursive code path still exists in the backend.
When recursive watching is enabled and you watch a directory, you may
receive a `Write` event for an intermediate directory whenever a child
entry inside it is created, renamed, or removed. For example, with a
recursive watch on `/a` and a new file `/a/b/c`, you will receive
`Create /a/b/c` and may also receive `Write /a/b`.
This happens because, on NTFS-backed volumes, modifying the entries of a
directory updates that directory's last-write time, and the Windows
backend requests `FILE_NOTIFY_CHANGE_LAST_WRITE` to support `Write` events
on files. The same `Write` filter therefore picks up the directory's
metadata update.
kqueue has the same "directory `Write` = directory contents changed"
semantics, so portable code that treats `Write` on a directory as
"something inside it changed" works on Windows and BSD/macOS, but not on
Linux (inotify uses `Write` only for file-content changes). If you only
care about file content, filter out `Write` events whose path refers to a
directory.
Whether the directory `Write` is actually delivered alongside the child
events is not guaranteed: it depends on `ReadDirectoryChangesW` buffering,
NTFS metadata update timing, and event coalescing, none of which fsnotify
controls.
Reaching the limit will result in a "no space left on device" or "too many open
files" error.
### kqueue (macOS, all BSD systems)
kqueue requires opening a file descriptor for every file that's being watched;

View File

@@ -158,7 +158,9 @@ func (w *fen) readEvents() {
pevents := make([]unix.PortEvent, 8)
for {
count, err := w.port.Get(pevents, 1, nil)
count, err := internal.IgnoringEINTR(func() (int, error) {
return 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 {

View File

@@ -55,10 +55,10 @@ type (
path map[string]uint32 // pathname → wd
}
watch struct {
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
path string // Watch path.
recurse bool // Recursion with ./...?
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.
watchFlags watchFlag
}
koekje struct {
cookie uint32
@@ -66,6 +66,9 @@ type (
}
)
func (w watch) byUser() bool { return w.watchFlags&flagByUser != 0 }
func (w watch) recurse() bool { return w.watchFlags&flagRecurse != 0 }
func newWatches() *watches {
return &watches{
wd: make(map[uint32]*watch),
@@ -79,6 +82,13 @@ func (w *watches) len() int { return len(w.wd) }
func (w *watches) add(ww *watch) { w.wd[ww.wd] = ww; w.path[ww.path] = ww.wd }
func (w *watches) remove(watch *watch) { delete(w.path, watch.path); delete(w.wd, watch.wd) }
func isSameOrDescendantPath(path, root string) bool {
if path == root {
return true
}
return strings.HasPrefix(path, root+string(os.PathSeparator))
}
func (w *watches) removePath(path string) ([]uint32, error) {
path, recurse := recursivePath(path)
wd, ok := w.path[path]
@@ -87,20 +97,20 @@ func (w *watches) removePath(path string) ([]uint32, error) {
}
watch := w.wd[wd]
if recurse && !watch.recurse {
if recurse && !watch.recurse() {
return nil, fmt.Errorf("can't use /... with non-recursive watch %q", path)
}
delete(w.path, path)
delete(w.wd, wd)
if !watch.recurse {
if !watch.recurse() {
return []uint32{wd}, nil
}
wds := make([]uint32, 0, 8)
wds = append(wds, wd)
for p, rwd := range w.path {
if strings.HasPrefix(p, path) {
if isSameOrDescendantPath(p, path) {
delete(w.path, p)
delete(w.wd, rwd)
wds = append(wds, rwd)
@@ -139,7 +149,7 @@ func newBackend(ev chan Event, errs chan error) (backend, error) {
// I/O operations won't terminate on close.
fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
if fd == -1 {
return nil, errno
return nil, fmt.Errorf("couldn't initialize inotify: %w", errno)
}
w := &inotify{
@@ -188,11 +198,8 @@ func (w *inotify) AddWith(path string, opts ...addOpt) error {
return fmt.Errorf("%w: %s", xErrUnsupported, with.op)
}
add := func(path string, with withOpts, recurse bool) error {
add := func(path string, with withOpts, wf watchFlag) error {
var flags uint32
if with.noFollow {
flags |= unix.IN_DONT_FOLLOW
}
if with.op.Has(Create) {
flags |= unix.IN_CREATE
}
@@ -220,7 +227,7 @@ func (w *inotify) AddWith(path string, opts ...addOpt) error {
if with.op.Has(xUnportableCloseRead) {
flags |= unix.IN_CLOSE_NOWRITE
}
return w.register(path, flags, recurse)
return w.register(path, flags, wf)
}
w.mu.Lock()
@@ -248,14 +255,18 @@ func (w *inotify) AddWith(path string, opts ...addOpt) error {
w.sendEvent(Event{Name: root, Op: Create})
}
return add(root, with, true)
wf := flagRecurse
if root == path {
wf |= flagByUser
}
return add(root, with, wf)
})
}
return add(path, with, false)
return add(path, with, 0)
}
func (w *inotify) register(path string, flags uint32, recurse bool) error {
func (w *inotify) register(path string, flags uint32, wf watchFlag) error {
return w.watches.updatePath(path, func(existing *watch) (*watch, error) {
if existing != nil {
flags |= existing.flags | unix.IN_MASK_ADD
@@ -272,10 +283,10 @@ func (w *inotify) register(path string, flags uint32, recurse bool) error {
if existing == nil {
return &watch{
wd: uint32(wd),
path: path,
flags: flags,
recurse: recurse,
wd: uint32(wd),
path: path,
flags: flags,
watchFlags: wf,
}, nil
}
@@ -425,11 +436,7 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs
nameLen = uint32(inEvent.Len)
)
if nameLen > 0 {
/// Point "bytes" at the first byte of the filename
bb := *buf
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&bb[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen]
/// The filename is padded with NULL bytes. TrimRight() gets rid of those.
name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\x00")
name += "/" + inotifyEventName(buf, offset, nameLen)
}
if debug {
@@ -450,7 +457,9 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs
// We can't really update the state when a watched path is moved; only
// IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove the watch.
if inEvent.Mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF {
if watch.recurse { // Do nothing
// Watch is set up as part of recurse: do nothing as the move gets
// registered from the parent directory.
if watch.recurse() && !watch.byUser() {
return Event{}, true
}
@@ -460,6 +469,10 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs
return Event{}, false
}
}
if watch.recurse() {
return Event{Name: watch.path, Op: Rename}, true
}
}
/// Skip if we're watching both this path and the parent; the parent will
@@ -473,11 +486,11 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs
ev := w.newEvent(name, inEvent.Mask, inEvent.Cookie)
// Need to update watch path for recurse.
if watch.recurse {
if watch.recurse() {
isDir := inEvent.Mask&unix.IN_ISDIR == unix.IN_ISDIR
/// New directory created: set up watch on it.
if isDir && ev.Has(Create) {
err := w.register(ev.Name, watch.flags, true)
err := w.register(ev.Name, watch.flags, flagRecurse)
if !w.sendError(err) {
return Event{}, false
}
@@ -495,7 +508,7 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs
if k == watch.wd || ww.path == ev.Name {
continue
}
if strings.HasPrefix(ww.path, ev.renamedFrom) {
if isSameOrDescendantPath(ww.path, ev.renamedFrom) {
ww.path = strings.Replace(ww.path, ev.renamedFrom, ev.Name, 1)
w.watches.wd[k] = ww
}
@@ -507,12 +520,13 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs
return ev, true
}
func (w *inotify) isRecursive(path string) bool {
ww := w.watches.byPath(path)
if ww == nil { // path could be a file, so also check the Dir.
ww = w.watches.byPath(filepath.Dir(path))
func inotifyEventName(buf *[65536]byte, offset, nameLen uint32) string {
start := int(offset + unix.SizeofInotifyEvent)
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[start]))[:nameLen:nameLen]
for nameLen > 0 && bytes[nameLen-1] == 0 {
nameLen--
}
return ww != nil && ww.recurse
return string(bytes[:nameLen])
}
func (w *inotify) newEvent(name string, mask, cookie uint32) Event {
@@ -578,6 +592,6 @@ func (w *inotify) state() {
w.mu.Lock()
defer w.mu.Unlock()
for wd, ww := range w.watches.wd {
fmt.Fprintf(os.Stderr, "%4d: recurse=%t %q\n", wd, ww.recurse, ww.path)
fmt.Fprintf(os.Stderr, "%4d: %q watchFlags=0x%x\n", wd, ww.path, ww.watchFlags)
}
}

View File

@@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"runtime"
"sort"
"sync"
"time"
@@ -245,9 +246,26 @@ func (w *kqueue) Close() error {
return nil
}
// Snapshot and drop all watches directly. w.Remove -> w.remove
// short-circuits on isClosed() (which is already true after
// w.shared.close() above), so calling Remove here in the happy path
// leaked every watched directory + file descriptor. On macOS a
// single directory watch opens an fd for every file in the dir, so
// long-running processes that recreate watchers (hot-reload dev
// servers, etc.) ran out of fds with EMFILE (#732).
pathsToRemove := w.watches.listPaths(false)
for _, name := range pathsToRemove {
w.Remove(name)
info, ok := w.watches.byPath(name)
if !ok {
// w.path has an entry for name but w.wd doesn't --
// drop the stale lookup entry so the map state is
// consistent after Close.
w.watches.remove(0, name)
continue
}
_ = w.register([]int{info.wd}, unix.EV_DELETE, 0)
unix.Close(info.wd)
w.watches.remove(info.wd, name)
}
unix.Close(w.closepipe[1]) // Send "quit" message to readEvents
@@ -376,19 +394,12 @@ func (w *kqueue) addWatch(name string, flags uint32, listDir bool) (string, erro
}
}
// Retry on EINTR; open() can return EINTR in practice on macOS.
// See #354, and Go issues 11180 and 39237.
for {
info.wd, err = unix.Open(name, openMode, 0)
if err == nil {
break
}
if errors.Is(err, unix.EINTR) {
continue
}
info.wd, err = internal.IgnoringEINTR(func() (int, error) {
return unix.Open(name, openMode, 0)
})
if err != nil {
return "", err
}
info.isDir = fi.IsDir()
}
@@ -436,9 +447,10 @@ func (w *kqueue) readEvents() {
eventBuffer := make([]unix.Kevent_t, 10)
for {
kevents, err := w.read(eventBuffer)
// EINTR is okay, the syscall was interrupted before timeout expired.
if err != nil && err != unix.EINTR {
kevents, err := internal.IgnoringEINTR(func() ([]unix.Kevent_t, error) {
return w.read(eventBuffer)
})
if err != nil {
if !w.sendError(fmt.Errorf("fsnotify.readEvents: %w", err)) {
return
}
@@ -583,12 +595,14 @@ func (w *kqueue) watchDirectoryFiles(dirPath string) error {
cleanPath, err := w.internalWatch(path, fi)
if err != nil {
// No permission to read the file; that's not a problem: just skip.
// But do add it to w.fileExists to prevent it from being picked up
// as a "new" file later (it still shows up in the directory
// No permission, or the entry resolved to a missing target
// (e.g. a dangling symlink): not a problem, just skip. But
// do mark it as seen to prevent it from being picked up as
// a "new" file later (it still shows up in the directory
// listing).
switch {
case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM):
case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) ||
errors.Is(err, os.ErrNotExist):
cleanPath = filepath.Clean(path)
default:
return fmt.Errorf("%q: %w", path, err)
@@ -703,3 +717,19 @@ func (w *kqueue) xSupports(op Op) bool {
}
return true
}
func (w *kqueue) state() {
w.watches.mu.Lock()
defer w.watches.mu.Unlock()
all := make([]int, 0, len(w.watches.wd))
for wd := range w.watches.wd {
all = append(all, wd)
}
sort.Ints(all)
for _, wd := range all {
ww := w.watches.wd[wd]
fmt.Fprintf(os.Stderr, "%4d %q linkname=%q\n", wd, ww.name, ww.linkName)
}
}

View File

@@ -11,7 +11,6 @@ import (
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"sync"
@@ -37,6 +36,13 @@ type readDirChangesW struct {
var defaultBufferSize = 50
func isSameOrDescendantPath(path, root string) bool {
if path == root {
return true
}
return strings.HasPrefix(path, root+string(os.PathSeparator))
}
func newBackend(ev chan Event, errs chan error) (backend, error) {
port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0)
if err != nil {
@@ -359,22 +365,26 @@ func (w *readDirChangesW) addWatch(pathname string, flags uint64, bufsize int) e
} else {
windows.CloseHandle(ino.handle)
}
w.mu.Lock()
if pathname == dir {
watchEntry.mask |= flags
} else {
watchEntry.names[filepath.Base(pathname)] |= flags
}
w.mu.Unlock()
err = w.startRead(watchEntry)
if err != nil {
return err
}
w.mu.Lock()
if pathname == dir {
watchEntry.mask &= ^provisional
} else {
watchEntry.names[filepath.Base(pathname)] &= ^provisional
}
w.mu.Unlock()
return nil
}
@@ -394,8 +404,13 @@ func (w *readDirChangesW) remWatch(pathname string) error {
w.mu.Lock()
watch := w.watches.get(ino)
w.mu.Unlock()
if watch == nil {
windows.CloseHandle(ino.handle)
return fmt.Errorf("%w: %s", ErrNonExistentWatch, pathname)
}
if recurse && !watch.recurse {
windows.CloseHandle(ino.handle)
return fmt.Errorf("can't use \\... with non-recursive watch %q", pathname)
}
@@ -403,16 +418,19 @@ func (w *readDirChangesW) remWatch(pathname string) error {
if err != nil {
w.sendError(os.NewSyscallError("CloseHandle", err))
}
if watch == nil {
return fmt.Errorf("%w: %s", ErrNonExistentWatch, pathname)
}
if pathname == dir {
w.sendEvent(watch.path, "", watch.mask&sysFSIGNORED)
w.mu.Lock()
mask := watch.mask
watch.mask = 0
w.mu.Unlock()
w.sendEvent(watch.path, "", mask&sysFSIGNORED)
} else {
name := filepath.Base(pathname)
w.sendEvent(filepath.Join(watch.path, name), "", watch.names[name]&sysFSIGNORED)
w.mu.Lock()
mask := watch.names[name]
delete(watch.names, name)
w.mu.Unlock()
w.sendEvent(filepath.Join(watch.path, name), "", mask&sysFSIGNORED)
}
return w.startRead(watch)
@@ -420,17 +438,23 @@ func (w *readDirChangesW) remWatch(pathname string) error {
// Must run within the I/O thread.
func (w *readDirChangesW) deleteWatch(watch *watch) {
for name, mask := range watch.names {
if mask&provisional == 0 {
w.sendEvent(filepath.Join(watch.path, name), "", mask&sysFSIGNORED)
// Snapshot+clear under the lock so concurrent WatchList() readers see a
// consistent state. sendEvent must run outside the lock since it can
// block on the user-facing Events channel.
w.mu.Lock()
names := watch.names
watch.names = make(map[string]uint64)
mask := watch.mask
watch.mask = 0
w.mu.Unlock()
for name, m := range names {
if m&provisional == 0 {
w.sendEvent(filepath.Join(watch.path, name), "", m&sysFSIGNORED)
}
delete(watch.names, name)
}
if watch.mask != 0 {
if watch.mask&provisional == 0 {
w.sendEvent(watch.path, "", watch.mask&sysFSIGNORED)
}
watch.mask = 0
if mask != 0 && mask&provisional == 0 {
w.sendEvent(watch.path, "", mask&sysFSIGNORED)
}
}
@@ -457,9 +481,8 @@ func (w *readDirChangesW) startRead(watch *watch) error {
}
// We need to pass the array, rather than the slice.
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&watch.buf))
rdErr := windows.ReadDirectoryChanges(watch.ino.handle,
(*byte)(unsafe.Pointer(hdr.Data)), uint32(hdr.Len),
unsafe.SliceData(watch.buf), uint32(len(watch.buf)),
watch.recurse, mask, nil, &watch.ov, 0)
if rdErr != nil {
err := os.NewSyscallError("ReadDirectoryChanges", rdErr)
@@ -565,12 +588,7 @@ func (w *readDirChangesW) readEvents() {
// Create a buf that is the size of the path name
size := int(raw.FileNameLength / 2)
var buf []uint16
// TODO: Use unsafe.Slice in Go 1.17; https://stackoverflow.com/questions/51187973
sh := (*reflect.SliceHeader)(unsafe.Pointer(&buf))
sh.Data = uintptr(unsafe.Pointer(&raw.FileName))
sh.Len = size
sh.Cap = size
buf := unsafe.Slice(&raw.FileName, size)
name := windows.UTF16ToString(buf)
fullname := filepath.Join(watch.path, name)
@@ -587,31 +605,35 @@ func (w *readDirChangesW) readEvents() {
case windows.FILE_ACTION_RENAMED_OLD_NAME:
watch.rename = name
case windows.FILE_ACTION_RENAMED_NEW_NAME:
// Update saved path of all sub-watches.
// Update saved path of all sub-watches and rename the
// names entry under the lock so WatchList() can't observe
// a torn state.
old := filepath.Join(watch.path, watch.rename)
w.mu.Lock()
for _, watchMap := range w.watches {
for _, ww := range watchMap {
if strings.HasPrefix(ww.path, old) {
if isSameOrDescendantPath(ww.path, old) {
ww.path = filepath.Join(fullname, strings.TrimPrefix(ww.path, old))
}
}
}
w.mu.Unlock()
if watch.names[watch.rename] != 0 {
watch.names[name] |= watch.names[watch.rename]
delete(watch.names, watch.rename)
mask = sysFSMOVESELF
}
w.mu.Unlock()
}
if raw.Action != windows.FILE_ACTION_RENAMED_NEW_NAME {
w.sendEvent(fullname, "", watch.names[name]&mask)
}
if raw.Action == windows.FILE_ACTION_REMOVED {
w.sendEvent(fullname, "", watch.names[name]&sysFSIGNORED)
w.mu.Lock()
ignored := watch.names[name] & sysFSIGNORED
delete(watch.names, name)
w.mu.Unlock()
w.sendEvent(fullname, "", ignored)
}
if watch.rename != "" && raw.Action == windows.FILE_ACTION_RENAMED_NEW_NAME {

View File

@@ -51,26 +51,25 @@ import (
// 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".
// create is an "instance", and every path you add is a "watch". Reaching the
// limit will result in a "no space left on device" or "too many open files"
// error.
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
// /proc/sys/fs/inotify/max_user_instances. The default values differ per distro
// and available memory.
//
// 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
// sysctl fs.inotify.max_user_watches=200000
// sysctl fs.inotify.max_user_instances=256
//
// 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.
// fs.inotify.max_user_watches=200000
// fs.inotify.max_user_instances=256
//
// # kqueue notes (macOS, BSD)
//
@@ -93,6 +92,28 @@ import (
// Sometimes it will send events for all files, sometimes it will send no
// events, and often only for some files.
//
// Recursive watching is not currently enabled through fsnotify's public
// API; the recursive code path is gated and only exercised by fsnotify's
// own tests. The note below describes backend behavior observed when
// recursive watching is enabled internally, and is kept here as a
// reference for maintainers and contributors who encounter it.
//
// When recursive watching is enabled and you watch a directory, you may
// receive a Write event for an intermediate directory whenever a child
// entry inside it is created, renamed, or removed. For example, with a
// recursive watch on /a and a new file /a/b/c, you will receive
// Create /a/b/c and may also receive Write /a/b.
//
// This happens because, on NTFS-backed volumes, modifying the entries of a
// directory updates that directory's last-write time, and the Windows
// backend requests FILE_NOTIFY_CHANGE_LAST_WRITE to support Write events
// on files. The same Write filter therefore picks up the directory's
// metadata update.
//
// Whether the directory Write is actually delivered alongside the child
// events is not guaranteed; it depends on ReadDirectoryChangesW buffering,
// NTFS metadata update timing, and event coalescing.
//
// 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
@@ -129,8 +150,12 @@ type Watcher struct {
// 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.
// Some systems also send Write events for directories
// when the directory contents change. This is the
// case for kqueue, and on Windows for the directory
// that contains a created, renamed, or removed child
// entry. It does not happen on inotify. See the
// per-platform notes on [Watcher].
//
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a
@@ -179,7 +204,9 @@ const (
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.
// and a write can be followed by more writes. On Windows and kqueue, a
// Write on a directory can also indicate that its contents changed; see
// the per-platform notes on [Watcher].
Write
// The path was removed; any watches on it will be removed. Some "remove"
@@ -220,7 +247,7 @@ const (
// File opened for reading was closed.
//
// Only works on Linux and FreeBSD.
// Only works on Linux.
xUnportableCloseRead
)
@@ -410,7 +437,6 @@ type (
withOpts struct {
bufsize int
op Op
noFollow bool
sendCreate bool
}
)
@@ -469,12 +495,6 @@ func withOps(op Op) addOpt {
return func(opt *withOpts) { opt.op = op }
}
// WithNoFollow disables following symlinks, so the symlinks themselves are
// watched.
func withNoFollow() addOpt {
return func(opt *withOpts) { opt.noFollow = true }
}
// "Internal" option for recursive watches on inotify.
func withCreate() addOpt {
return func(opt *withOpts) { opt.sendCreate = true }
@@ -494,3 +514,13 @@ func recursivePath(path string) (string, bool) {
}
return path, false
}
type watchFlag uint8
const (
// Added by user with Add(), rather than an internal watch.
flagByUser = watchFlag(0x01)
// Part of recursive watch; as the top-level path added by the user or an
// "internal" watch.
flagRecurse = watchFlag(0x02)
)

View File

@@ -15,25 +15,6 @@ var (
var maxfiles uint64
func SetRlimit() {
// Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/
var l syscall.Rlimit
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l)
if err == nil && l.Cur != l.Max {
l.Cur = l.Max
syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l)
}
maxfiles = l.Cur
if n, err := syscall.SysctlUint32("kern.maxfiles"); err == nil && uint64(n) < maxfiles {
maxfiles = uint64(n)
}
if n, err := syscall.SysctlUint32("kern.maxfilesperproc"); err == nil && uint64(n) < maxfiles {
maxfiles = uint64(n)
}
}
func Maxfiles() uint64 { return maxfiles }
func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) }
func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, dev) }

View File

@@ -6,52 +6,10 @@ var names = []struct {
n string
m uint32
}{
{"NOTE_ABSOLUTE", unix.NOTE_ABSOLUTE},
{"NOTE_ATTRIB", unix.NOTE_ATTRIB},
{"NOTE_BACKGROUND", unix.NOTE_BACKGROUND},
{"NOTE_CHILD", unix.NOTE_CHILD},
{"NOTE_CRITICAL", unix.NOTE_CRITICAL},
{"NOTE_DELETE", unix.NOTE_DELETE},
{"NOTE_EXEC", unix.NOTE_EXEC},
{"NOTE_EXIT", unix.NOTE_EXIT},
{"NOTE_EXITSTATUS", unix.NOTE_EXITSTATUS},
{"NOTE_EXIT_CSERROR", unix.NOTE_EXIT_CSERROR},
{"NOTE_EXIT_DECRYPTFAIL", unix.NOTE_EXIT_DECRYPTFAIL},
{"NOTE_EXIT_DETAIL", unix.NOTE_EXIT_DETAIL},
{"NOTE_EXIT_DETAIL_MASK", unix.NOTE_EXIT_DETAIL_MASK},
{"NOTE_EXIT_MEMORY", unix.NOTE_EXIT_MEMORY},
{"NOTE_EXIT_REPARENTED", unix.NOTE_EXIT_REPARENTED},
{"NOTE_EXTEND", unix.NOTE_EXTEND},
{"NOTE_FFAND", unix.NOTE_FFAND},
{"NOTE_FFCOPY", unix.NOTE_FFCOPY},
{"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK},
{"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK},
{"NOTE_FFNOP", unix.NOTE_FFNOP},
{"NOTE_FFOR", unix.NOTE_FFOR},
{"NOTE_FORK", unix.NOTE_FORK},
{"NOTE_FUNLOCK", unix.NOTE_FUNLOCK},
{"NOTE_LEEWAY", unix.NOTE_LEEWAY},
{"NOTE_LINK", unix.NOTE_LINK},
{"NOTE_LOWAT", unix.NOTE_LOWAT},
{"NOTE_MACHTIME", unix.NOTE_MACHTIME},
{"NOTE_MACH_CONTINUOUS_TIME", unix.NOTE_MACH_CONTINUOUS_TIME},
{"NOTE_NONE", unix.NOTE_NONE},
{"NOTE_NSECONDS", unix.NOTE_NSECONDS},
{"NOTE_OOB", unix.NOTE_OOB},
//{"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, -0x100000 (?!)
{"NOTE_PDATAMASK", unix.NOTE_PDATAMASK},
{"NOTE_REAP", unix.NOTE_REAP},
{"NOTE_RENAME", unix.NOTE_RENAME},
{"NOTE_REVOKE", unix.NOTE_REVOKE},
{"NOTE_SECONDS", unix.NOTE_SECONDS},
{"NOTE_SIGNAL", unix.NOTE_SIGNAL},
{"NOTE_TRACK", unix.NOTE_TRACK},
{"NOTE_TRACKERR", unix.NOTE_TRACKERR},
{"NOTE_TRIGGER", unix.NOTE_TRIGGER},
{"NOTE_USECONDS", unix.NOTE_USECONDS},
{"NOTE_VM_ERROR", unix.NOTE_VM_ERROR},
{"NOTE_VM_PRESSURE", unix.NOTE_VM_PRESSURE},
{"NOTE_VM_PRESSURE_SUDDEN_TERMINATE", unix.NOTE_VM_PRESSURE_SUDDEN_TERMINATE},
{"NOTE_VM_PRESSURE_TERMINATE", unix.NOTE_VM_PRESSURE_TERMINATE},
{"NOTE_WRITE", unix.NOTE_WRITE},
}

View File

@@ -7,27 +7,9 @@ var names = []struct {
m uint32
}{
{"NOTE_ATTRIB", unix.NOTE_ATTRIB},
{"NOTE_CHILD", unix.NOTE_CHILD},
{"NOTE_DELETE", unix.NOTE_DELETE},
{"NOTE_EXEC", unix.NOTE_EXEC},
{"NOTE_EXIT", unix.NOTE_EXIT},
{"NOTE_EXTEND", unix.NOTE_EXTEND},
{"NOTE_FFAND", unix.NOTE_FFAND},
{"NOTE_FFCOPY", unix.NOTE_FFCOPY},
{"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK},
{"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK},
{"NOTE_FFNOP", unix.NOTE_FFNOP},
{"NOTE_FFOR", unix.NOTE_FFOR},
{"NOTE_FORK", unix.NOTE_FORK},
{"NOTE_LINK", unix.NOTE_LINK},
{"NOTE_LOWAT", unix.NOTE_LOWAT},
{"NOTE_OOB", unix.NOTE_OOB},
{"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK},
{"NOTE_PDATAMASK", unix.NOTE_PDATAMASK},
{"NOTE_RENAME", unix.NOTE_RENAME},
{"NOTE_REVOKE", unix.NOTE_REVOKE},
{"NOTE_TRACK", unix.NOTE_TRACK},
{"NOTE_TRACKERR", unix.NOTE_TRACKERR},
{"NOTE_TRIGGER", unix.NOTE_TRIGGER},
{"NOTE_WRITE", unix.NOTE_WRITE},
}

View File

@@ -6,37 +6,15 @@ var names = []struct {
n string
m uint32
}{
{"NOTE_ABSTIME", unix.NOTE_ABSTIME},
{"NOTE_ATTRIB", unix.NOTE_ATTRIB},
{"NOTE_CHILD", unix.NOTE_CHILD},
{"NOTE_CLOSE", unix.NOTE_CLOSE},
{"NOTE_CLOSE_WRITE", unix.NOTE_CLOSE_WRITE},
{"NOTE_DELETE", unix.NOTE_DELETE},
{"NOTE_EXEC", unix.NOTE_EXEC},
{"NOTE_EXIT", unix.NOTE_EXIT},
{"NOTE_WRITE", unix.NOTE_WRITE},
{"NOTE_EXTEND", unix.NOTE_EXTEND},
{"NOTE_FFAND", unix.NOTE_FFAND},
{"NOTE_FFCOPY", unix.NOTE_FFCOPY},
{"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK},
{"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK},
{"NOTE_FFNOP", unix.NOTE_FFNOP},
{"NOTE_FFOR", unix.NOTE_FFOR},
{"NOTE_FILE_POLL", unix.NOTE_FILE_POLL},
{"NOTE_FORK", unix.NOTE_FORK},
{"NOTE_ATTRIB", unix.NOTE_ATTRIB},
{"NOTE_LINK", unix.NOTE_LINK},
{"NOTE_LOWAT", unix.NOTE_LOWAT},
{"NOTE_MSECONDS", unix.NOTE_MSECONDS},
{"NOTE_NSECONDS", unix.NOTE_NSECONDS},
{"NOTE_OPEN", unix.NOTE_OPEN},
{"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK},
{"NOTE_PDATAMASK", unix.NOTE_PDATAMASK},
{"NOTE_READ", unix.NOTE_READ},
{"NOTE_RENAME", unix.NOTE_RENAME},
{"NOTE_REVOKE", unix.NOTE_REVOKE},
{"NOTE_SECONDS", unix.NOTE_SECONDS},
{"NOTE_TRACK", unix.NOTE_TRACK},
{"NOTE_TRACKERR", unix.NOTE_TRACKERR},
{"NOTE_TRIGGER", unix.NOTE_TRIGGER},
{"NOTE_USECONDS", unix.NOTE_USECONDS},
{"NOTE_WRITE", unix.NOTE_WRITE},
{"NOTE_OPEN", unix.NOTE_OPEN},
{"NOTE_CLOSE", unix.NOTE_CLOSE},
{"NOTE_CLOSE_WRITE", unix.NOTE_CLOSE_WRITE},
{"NOTE_READ", unix.NOTE_READ},
}

View File

@@ -27,6 +27,6 @@ func Debug(name string, kevent *unix.Kevent_t) {
if unknown > 0 {
l = append(l, fmt.Sprintf("0x%x", unknown))
}
fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s %10d:%-60s → %q\n",
fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s %10d:%-20s → %q\n",
time.Now().Format("15:04:05.000000000"), mask, strings.Join(l, " | "), name)
}

View File

@@ -7,19 +7,9 @@ var names = []struct {
m uint32
}{
{"NOTE_ATTRIB", unix.NOTE_ATTRIB},
{"NOTE_CHILD", unix.NOTE_CHILD},
{"NOTE_DELETE", unix.NOTE_DELETE},
{"NOTE_EXEC", unix.NOTE_EXEC},
{"NOTE_EXIT", unix.NOTE_EXIT},
{"NOTE_EXTEND", unix.NOTE_EXTEND},
{"NOTE_FORK", unix.NOTE_FORK},
{"NOTE_LINK", unix.NOTE_LINK},
{"NOTE_LOWAT", unix.NOTE_LOWAT},
{"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK},
{"NOTE_PDATAMASK", unix.NOTE_PDATAMASK},
{"NOTE_RENAME", unix.NOTE_RENAME},
{"NOTE_REVOKE", unix.NOTE_REVOKE},
{"NOTE_TRACK", unix.NOTE_TRACK},
{"NOTE_TRACKERR", unix.NOTE_TRACKERR},
{"NOTE_WRITE", unix.NOTE_WRITE},
}

View File

@@ -7,22 +7,10 @@ var names = []struct {
m uint32
}{
{"NOTE_ATTRIB", unix.NOTE_ATTRIB},
// {"NOTE_CHANGE", unix.NOTE_CHANGE}, // Not on 386?
{"NOTE_CHILD", unix.NOTE_CHILD},
{"NOTE_DELETE", unix.NOTE_DELETE},
{"NOTE_EOF", unix.NOTE_EOF},
{"NOTE_EXEC", unix.NOTE_EXEC},
{"NOTE_EXIT", unix.NOTE_EXIT},
{"NOTE_EXTEND", unix.NOTE_EXTEND},
{"NOTE_FORK", unix.NOTE_FORK},
{"NOTE_LINK", unix.NOTE_LINK},
{"NOTE_LOWAT", unix.NOTE_LOWAT},
{"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK},
{"NOTE_PDATAMASK", unix.NOTE_PDATAMASK},
{"NOTE_RENAME", unix.NOTE_RENAME},
{"NOTE_REVOKE", unix.NOTE_REVOKE},
{"NOTE_TRACK", unix.NOTE_TRACK},
{"NOTE_TRACKERR", unix.NOTE_TRACKERR},
{"NOTE_TRUNCATE", unix.NOTE_TRUNCATE},
{"NOTE_WRITE", unix.NOTE_WRITE},
}

View File

@@ -15,17 +15,6 @@ var (
var maxfiles uint64
func SetRlimit() {
// Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/
var l syscall.Rlimit
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l)
if err == nil && l.Cur != l.Max {
l.Cur = l.Max
syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l)
}
maxfiles = uint64(l.Cur)
}
func Maxfiles() uint64 { return maxfiles }
func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) }
func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, uint64(dev)) }

View File

@@ -15,17 +15,6 @@ var (
var maxfiles uint64
func SetRlimit() {
// Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/
var l syscall.Rlimit
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l)
if err == nil && l.Cur != l.Max {
l.Cur = l.Max
syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l)
}
maxfiles = uint64(l.Cur)
}
func Maxfiles() uint64 { return maxfiles }
func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) }
func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, dev) }

View File

@@ -2,6 +2,24 @@
package internal
import "syscall"
func HasPrivilegesForSymlink() bool {
return true
}
// IgnoringEINTR makes a function call and repeats it if it returns an
// EINTR error. This appears to be required even though we install all
// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846.
// Also #20400 and #36644 are issues in which a signal handler is
// installed without setting SA_RESTART. None of these are the common case,
// but there are enough of them that it seems that we can't avoid
// an EINTR loop.
func IgnoringEINTR[T any](fn func() (T, error)) (T, error) {
for {
v, err := fn()
if err != syscall.EINTR {
return v, err
}
}
}

View File

@@ -14,7 +14,6 @@ var (
ErrUnixEACCES = errors.New("dummy")
)
func SetRlimit() {}
func Maxfiles() uint64 { return 1<<64 - 1 }
func Mkfifo(path string, mode uint32) error { return errors.New("no FIFOs on Windows") }
func Mknod(path string, mode uint32, dev int) error { return errors.New("no device nodes on Windows") }

4
vendor/modules.txt vendored
View File

@@ -307,8 +307,8 @@ github.com/erofs/go-erofs/internal/disk
# github.com/felixge/httpsnoop v1.0.4
## explicit; go 1.13
github.com/felixge/httpsnoop
# github.com/fsnotify/fsnotify v1.9.0
## explicit; go 1.17
# github.com/fsnotify/fsnotify v1.10.1
## explicit; go 1.23
github.com/fsnotify/fsnotify
github.com/fsnotify/fsnotify/internal
# github.com/fxamacker/cbor/v2 v2.9.0