Skip to content

Commit

Permalink
Add recursive watcher for Windows backend
Browse files Browse the repository at this point in the history
Recursive watches can be added by using a "/..." parameter, similar to
the Go command:

	w.Add("dir")         // Behaves as before.
	w.Add("dir/...")     // Watch dir and all paths underneath it.

	w.Remove("dir")      // Remove the watch for dir and, if
	                     // recursive, all paths underneath it too

	w.Remove("dir/...")  // Behaves like just "dir" if the path was
	                     // recursive, error otherwise (probably
	                     // want to add recursive remove too at some
	                     // point).

The advantage of using "/..." vs. an option is that it can be easily
specified in configuration files and the like; for example from a TOML
file:

	[watches]
	dirs = ["/tmp/one", "/tmp/two/..."]

Options for this were previously discussed at:
#339 (comment)

This should be expanded to other backends too; I started with Windows
because the implementation is the both the easiest and has the least
amount of control (just setting a boolean parameter), and we can focus
mostly on writing tests and documentation and the for it, and we can
then match the inotify and kqueue behaviour to the Windows one.

Fixes #21
  • Loading branch information
arp242 committed Nov 17, 2022
1 parent 1a76583 commit 3d1b830
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 57 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Expand Up @@ -10,6 +10,8 @@ Unreleased
- all: add `AddWith()`, which is identical to `Add()` but allows passing
options. ([#521])

- all: support recursively watching paths with `Add("path/...")`. ([#540])

- windows: allow setting the 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])
Expand Down Expand Up @@ -46,7 +48,7 @@ Unreleased

- 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 work there.
won't compile there.


[#371]: https://github.com/fsnotify/fsnotify/pull/371
Expand All @@ -58,6 +60,7 @@ Unreleased
[#526]: https://github.com/fsnotify/fsnotify/pull/526
[#528]: https://github.com/fsnotify/fsnotify/pull/528
[#537]: https://github.com/fsnotify/fsnotify/pull/537
[#540]: https://github.com/fsnotify/fsnotify/pull/540

1.6.0 - 2022-10-13
-------------------
Expand Down
18 changes: 13 additions & 5 deletions backend_fen.go
Expand Up @@ -184,7 +184,7 @@ func (w *Watcher) Close() error {
//
// 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.
// watched.
//
// A watch will be automatically removed if the watched path is deleted or
// renamed. The exception is the Windows backend, which doesn't remove the
Expand All @@ -200,8 +200,9 @@ func (w *Watcher) Close() error {
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
// after the watcher is started. By default subdirectories are not watched (i.e.
// it's non-recursive), but if the path ends with "/..." all files and
// subdirectories are watched too.
//
// # Watching files
//
Expand Down Expand Up @@ -266,8 +267,15 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error {

// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
// If the path was added as a recursive watch (e.g. as "/tmp/dir/...") then the
// entire recusrice watch will be removed. You can use both "/tmp/dir" and
// "/tmp/dir/..." (they behave identical).
//
// You cannot remove individual files or subdirectories from recursive watches;
// e.g. Add("/tmp/path/...") and then Remove("/tmp/path/sub") will fail.
//
// For other watches directories are removed non-recursively. For example, if
// you added "/tmp/dir" and "/tmp/dir/subdir" then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
Expand Down
18 changes: 13 additions & 5 deletions backend_inotify.go
Expand Up @@ -206,7 +206,7 @@ func (w *Watcher) Close() error {
//
// 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.
// watched.
//
// A watch will be automatically removed if the watched path is deleted or
// renamed. The exception is the Windows backend, which doesn't remove the
Expand All @@ -222,8 +222,9 @@ func (w *Watcher) Close() error {
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
// after the watcher is started. By default subdirectories are not watched (i.e.
// it's non-recursive), but if the path ends with "/..." all files and
// subdirectories are watched too.
//
// # Watching files
//
Expand Down Expand Up @@ -281,8 +282,15 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error {

// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
// If the path was added as a recursive watch (e.g. as "/tmp/dir/...") then the
// entire recusrice watch will be removed. You can use both "/tmp/dir" and
// "/tmp/dir/..." (they behave identical).
//
// You cannot remove individual files or subdirectories from recursive watches;
// e.g. Add("/tmp/path/...") and then Remove("/tmp/path/sub") will fail.
//
// For other watches directories are removed non-recursively. For example, if
// you added "/tmp/dir" and "/tmp/dir/subdir" then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
Expand Down
1 change: 1 addition & 0 deletions backend_inotify_test.go
Expand Up @@ -93,6 +93,7 @@ func TestInotifyDeleteOpenFile(t *testing.T) {
w.collect(t)

rm(t, file)
eventSeparator()
e := w.events(t)
cmpEvents(t, tmp, e, newEvents(t, `chmod /file`))

Expand Down
18 changes: 13 additions & 5 deletions backend_kqueue.go
Expand Up @@ -237,7 +237,7 @@ func (w *Watcher) Close() error {
//
// 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.
// watched.
//
// A watch will be automatically removed if the watched path is deleted or
// renamed. The exception is the Windows backend, which doesn't remove the
Expand All @@ -253,8 +253,9 @@ func (w *Watcher) Close() error {
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
// after the watcher is started. By default subdirectories are not watched (i.e.
// it's non-recursive), but if the path ends with "/..." all files and
// subdirectories are watched too.
//
// # Watching files
//
Expand Down Expand Up @@ -288,8 +289,15 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error {

// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
// If the path was added as a recursive watch (e.g. as "/tmp/dir/...") then the
// entire recusrice watch will be removed. You can use both "/tmp/dir" and
// "/tmp/dir/..." (they behave identical).
//
// You cannot remove individual files or subdirectories from recursive watches;
// e.g. Add("/tmp/path/...") and then Remove("/tmp/path/sub") will fail.
//
// For other watches directories are removed non-recursively. For example, if
// you added "/tmp/dir" and "/tmp/dir/subdir" then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
Expand Down
18 changes: 13 additions & 5 deletions backend_other.go
Expand Up @@ -119,7 +119,7 @@ func (w *Watcher) WatchList() []string { return nil }
//
// 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.
// watched.
//
// A watch will be automatically removed if the watched path is deleted or
// renamed. The exception is the Windows backend, which doesn't remove the
Expand All @@ -135,8 +135,9 @@ func (w *Watcher) WatchList() []string { return nil }
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
// after the watcher is started. By default subdirectories are not watched (i.e.
// it's non-recursive), but if the path ends with "/..." all files and
// subdirectories are watched too.
//
// # Watching files
//
Expand All @@ -162,8 +163,15 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error { return nil }

// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
// If the path was added as a recursive watch (e.g. as "/tmp/dir/...") then the
// entire recusrice watch will be removed. You can use both "/tmp/dir" and
// "/tmp/dir/..." (they behave identical).
//
// You cannot remove individual files or subdirectories from recursive watches;
// e.g. Add("/tmp/path/...") and then Remove("/tmp/path/sub") will fail.
//
// For other watches directories are removed non-recursively. For example, if
// you added "/tmp/dir" and "/tmp/dir/subdir" then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
Expand Down
51 changes: 34 additions & 17 deletions backend_windows.go
Expand Up @@ -193,7 +193,7 @@ func (w *Watcher) Close() error {
//
// 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.
// watched.
//
// A watch will be automatically removed if the watched path is deleted or
// renamed. The exception is the Windows backend, which doesn't remove the
Expand All @@ -209,8 +209,9 @@ func (w *Watcher) Close() error {
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
// after the watcher is started. By default subdirectories are not watched (i.e.
// it's non-recursive), but if the path ends with "/..." all files and
// subdirectories are watched too.
//
// # Watching files
//
Expand Down Expand Up @@ -258,8 +259,15 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error {

// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
// If the path was added as a recursive watch (e.g. as "/tmp/dir/...") then the
// entire recusrice watch will be removed. You can use both "/tmp/dir" and
// "/tmp/dir/..." (they behave identical).
//
// You cannot remove individual files or subdirectories from recursive watches;
// e.g. Add("/tmp/path/...") and then Remove("/tmp/path/sub") will fail.
//
// For other watches directories are removed non-recursively. For example, if
// you added "/tmp/dir" and "/tmp/dir/subdir" then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
Expand Down Expand Up @@ -362,13 +370,14 @@ type inode struct {
}

type watch struct {
ov windows.Overlapped
ino *inode // i-number
path string // Directory path
mask uint64 // Directory itself is being watched with these notify flags
names map[string]uint64 // Map of names being watched and their notify flags
rename string // Remembers the old name while renaming a file
buf []byte // buffer, allocated later
ov windows.Overlapped
ino *inode // i-number
recurse bool // Recursive watch?
path string // Directory path
mask uint64 // Directory itself is being watched with these notify flags
names map[string]uint64 // Map of names being watched and their notify flags
rename string // Remembers the old name while renaming a file
buf []byte // buffer, allocated later
}

type (
Expand Down Expand Up @@ -442,6 +451,7 @@ func (m watchMap) set(ino *inode, watch *watch) {

// Must run within the I/O thread.
func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error {
pathname, recurse := recursivePath(pathname)
dir, err := w.getDir(pathname)
if err != nil {
return err
Expand All @@ -461,10 +471,11 @@ func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error {
return os.NewSyscallError("CreateIoCompletionPort", err)
}
watchEntry = &watch{
ino: ino,
path: dir,
names: make(map[string]uint64),
buf: make([]byte, bufsize),
ino: ino,
path: dir,
names: make(map[string]uint64),
recurse: recurse,
buf: make([]byte, bufsize),
}
w.mu.Lock()
w.watches.set(ino, watchEntry)
Expand Down Expand Up @@ -494,6 +505,8 @@ func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error {

// Must run within the I/O thread.
func (w *Watcher) remWatch(pathname string) error {
pathname, recurse := recursivePath(pathname)

dir, err := w.getDir(pathname)
if err != nil {
return err
Expand All @@ -507,6 +520,10 @@ func (w *Watcher) remWatch(pathname string) error {
watch := w.watches.get(ino)
w.mu.Unlock()

if recurse && !watch.recurse {
return fmt.Errorf("can't use /... with non-recursive watch %q", pathname)
}

err = windows.CloseHandle(ino.handle)
if err != nil {
w.sendError(os.NewSyscallError("CloseHandle", err))
Expand Down Expand Up @@ -568,7 +585,7 @@ func (w *Watcher) startRead(watch *watch) error {
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&watch.buf))
rdErr := windows.ReadDirectoryChanges(watch.ino.handle,
(*byte)(unsafe.Pointer(hdr.Data)), uint32(hdr.Len),
false, mask, nil, &watch.ov, 0)
watch.recurse, mask, nil, &watch.ov, 0)
if rdErr != nil {
err := os.NewSyscallError("ReadDirectoryChanges", rdErr)
if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 {
Expand Down
10 changes: 10 additions & 0 deletions fsnotify.go
Expand Up @@ -12,6 +12,7 @@ package fsnotify
import (
"errors"
"fmt"
"path/filepath"
"strings"
)

Expand Down Expand Up @@ -131,3 +132,12 @@ func getOptions(opts ...addOpt) withOpts {
func WithBufferSize(bytes int) addOpt {
return func(opt *withOpts) { opt.bufsize = bytes }
}

// Check if this path is recursive (ends with "/..."), 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
}

0 comments on commit 3d1b830

Please sign in to comment.