|
@@ -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")
|
|
|
|
|
-// os.Remove("file") // Triggers Chmod
|
|
|
|
|
-// fp.Close() // Triggers Remove
|
|
|
|
|
|
|
+// 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.
|
|
// 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
|
|
|
|
|
-// sysctl fs.inotify.max_user_watches=124983
|
|
|
|
|
-// sysctl fs.inotify.max_user_instances=128
|
|
|
|
|
|
|
+// # 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
|
|
// 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_instances=128
|
|
|
|
|
|
|
+// 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
|
|
// 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
|
|
|
|
|
+//
|
|
|
|
|
+// Paths can be added as "C:\path\to\dir", but forward slashes
|
|
|
|
|
+// ("C:/path/to/dir") will also work.
|
|
|
//
|
|
//
|
|
|
-// 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]).
|
|
|
|
|
|
|
+// 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.
|
|
|
//
|
|
//
|
|
|
-// [#11]: https://github.com/fsnotify/fsnotify/issues/11
|
|
|
|
|
-// [#15]: https://github.com/fsnotify/fsnotify/issues/15
|
|
|
|
|
|
|
+// 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
|
|
|
|
|
- // probably want to wait until you've stopped receiving
|
|
|
|
|
- // them (see the dedup example in cmd/fsnotify).
|
|
|
|
|
|
|
+ // 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
|
|
// 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
|
|
|
|
|
- // it's never sent.
|
|
|
|
|
|
|
+ // when a file is truncated. On Windows it's never
|
|
|
|
|
+ // 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
|
|
|
|
|
-// 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.
|
|
|
|
|
|
|
+// A path can only be watched once; watching it more than once is a no-op and will
|
|
|
|
|
+// not return an error. Paths that do not yet exist on the filesystem cannot be
|
|
|
|
|
+// watched.
|
|
|
//
|
|
//
|
|
|
-// A path will remain watched if it gets renamed to somewhere else on the same
|
|
|
|
|
-// filesystem, but the monitor will get removed if the path gets deleted and
|
|
|
|
|
-// re-created, or if it's moved to a different filesystem.
|
|
|
|
|
|
|
+// A watch will be automatically removed if the watched path is deleted or
|
|
|
|
|
+// renamed. The exception is the Windows backend, which doesn't remove the
|
|
|
|
|
+// watcher on renames.
|
|
|
//
|
|
//
|
|
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
|
// 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
|
|
|
|
|
-// to the file a temporary file will be written to first, and if successful the
|
|
|
|
|
-// temporary file is moved to to destination removing the original, or some
|
|
|
|
|
-// variant thereof. The watcher on the original file is now lost, as it no
|
|
|
|
|
-// longer exists.
|
|
|
|
|
-//
|
|
|
|
|
-// Instead, 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 {
|
|
|
|
|
|
|
+// 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.
|
|
|
|
|
+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
|
|
|
|
|
- // to the user, as that will just confuse them with an error about
|
|
|
|
|
- // a path they did not explicitly watch themselves.
|
|
|
|
|
|
|
+ // Since these are internal, not much sense in propagating error to
|
|
|
|
|
+ // the user, as that will just confuse them with an error about a
|
|
|
|
|
+ // path they did not explicitly watch themselves.
|
|
|
w.Remove(name)
|
|
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.
|
|
|
|
|
-// The flags are interpreted as 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.
|
|
|
|
|
|
|
+// addWatch adds name to the watched file set; the flags are interpreted as
|
|
|
|
|
+// described in kevent(2).
|
|
|
|
|
+//
|
|
|
|
|
+// Returns the real path to the file which was added, with symlinks resolved.
|
|
|
func (w *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
|
|
|
|
|
- //
|
|
|
|
|
- // 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.
|
|
|
|
|
|
|
+ // Follow Symlinks.
|
|
|
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,
|
|
|
|
|
- // or if it was watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles)
|
|
|
|
|
|
|
+ // Watch the directory if it has not been watched before, or if it was
|
|
|
|
|
+ // 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.
|
|
|
|
|
- // For example, mv f1 f2 will delete f2, then create f2.
|
|
|
|
|
|
|
+ // Look for a file that may have overwritten this; for example,
|
|
|
|
|
+ // 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
|
|
|
|
|
- // do a recursive watch and perform rm -fr, the parent directory might
|
|
|
|
|
- // have gone missing, ignore the missing directory and let the
|
|
|
|
|
- // upcoming delete event remove the watch from the parent directory.
|
|
|
|
|
- if _, err := os.Lstat(fileDir); err == nil {
|
|
|
|
|
- w.sendDirectoryChangeEvents(fileDir)
|
|
|
|
|
|
|
+ err := w.sendDirectoryChangeEvents(fileDir)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ if !w.sendError(err) {
|
|
|
|
|
+ closed = true
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
filePath := filepath.Clean(event.Name)
|
|
filePath := filepath.Clean(event.Name)
|
|
|
- if fileInfo, err := os.Lstat(filePath); err == nil {
|
|
|
|
|
- w.sendFileCreatedEventIfNew(filePath, fileInfo)
|
|
|
|
|
|
|
+ if fi, err := os.Lstat(filePath); err == nil {
|
|
|
|
|
+ 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 {
|
|
|
|
|
- path := filepath.Join(dirPath, fileInfo.Name())
|
|
|
|
|
|
|
+ for _, f := range files {
|
|
|
|
|
+ path := filepath.Join(dirPath, f.Name())
|
|
|
|
|
+
|
|
|
|
|
+ fi, err := f.Info()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return fmt.Errorf("%q: %w", path, err)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- cleanPath, err := w.internalWatch(path, fileInfo)
|
|
|
|
|
|
|
+ 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) {
|
|
|
|
|
- // Get all files
|
|
|
|
|
- files, err := ioutil.ReadDir(dir)
|
|
|
|
|
|
|
+func (w *Watcher) sendDirectoryChangeEvents(dir string) error {
|
|
|
|
|
+ files, err := os.ReadDir(dir)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
- if !w.sendError(fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)) {
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ // Directory no longer exists: we can ignore this safely. kqueue will
|
|
|
|
|
+ // still give us the correct events.
|
|
|
|
|
+ if errors.Is(err, os.ErrNotExist) {
|
|
|
|
|
+ return nil
|
|
|
}
|
|
}
|
|
|
|
|
+ return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Search for new files
|
|
|
|
|
- for _, fi := range files {
|
|
|
|
|
- err := w.sendFileCreatedEventIfNew(filepath.Join(dir, fi.Name()), fi)
|
|
|
|
|
|
|
+ for _, f := range files {
|
|
|
|
|
+ fi, err := f.Info()
|
|
|
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) {
|
|
|
|
|
- if fileInfo.IsDir() {
|
|
|
|
|
- // mimic Linux providing delete events for subdirectories
|
|
|
|
|
- // but preserve the flags used if currently watching subdirectory
|
|
|
|
|
|
|
+func (w *Watcher) internalWatch(name string, fi os.FileInfo) (string, error) {
|
|
|
|
|
+ if fi.IsDir() {
|
|
|
|
|
+ // mimic Linux providing delete events for subdirectories, but preserve
|
|
|
|
|
+ // the flags used if currently watching subdirectory
|
|
|
w.mu.Lock()
|
|
w.mu.Lock()
|
|
|
flags := w.dirFlags[name]
|
|
flags := w.dirFlags[name]
|
|
|
w.mu.Unlock()
|
|
w.mu.Unlock()
|