Skip to content

Commit

Permalink
Add recursive watcher for Windows backends
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 watches 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.

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 for it, and we can then match the inotify and
kqueue behaviour to the Windows one.

Updates #18
  • Loading branch information
arp242 committed Nov 17, 2022
1 parent 1a76583 commit a46dc0c
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 56 deletions.
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
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 a46dc0c

Please sign in to comment.