Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

inotify: remove watch when renaming a watched path #518

Merged
merged 1 commit into from Oct 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Expand Up @@ -13,10 +13,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changes and fixes

- inotify: remove watcher if a watched path is renamed ([#518])

After a rename the reported name wasn't updated, or even an empty string.
Inotify doesn't provide any good facilities to update it, so just remove the
watcher. This is already how it worked on kqueue and FEN.

On Windows this does work, and remains working.

- all: return ErrClosed on Add() when the watcher is closed ([#516])

[#371]: https://github.com/fsnotify/fsnotify/pull/371
[#516]: https://github.com/fsnotify/fsnotify/pull/516
[#518]: https://github.com/fsnotify/fsnotify/pull/518

## [1.6.0] - 2022-10-13

Expand Down
8 changes: 4 additions & 4 deletions backend_fen.go
Expand Up @@ -187,11 +187,11 @@ 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. A watch will be automatically removed if the path is deleted.
// added.
//
// 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
// filesystems (/proc, /sys, etc.) generally don't work.
Expand Down
47 changes: 25 additions & 22 deletions backend_inotify.go
Expand Up @@ -120,8 +120,8 @@ type Watcher struct {
fd int
mu sync.Mutex // Map access
inotifyFile *os.File
watches map[string]*watch // Map of inotify watches (key: path)
paths map[int]string // Map of watched paths (key: watch descriptor)
watches map[string]*watch // Map of inotify watches (path → watch)
paths map[int]string // Map of watched paths (watch descriptor → path)
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
doneResp chan struct{} // Channel to respond to Close
}
Expand Down Expand Up @@ -209,11 +209,11 @@ 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. A watch will be automatically removed if the path is deleted.
// added.
//
// 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
// filesystems (/proc, /sys, etc.) generally don't work.
Expand Down Expand Up @@ -285,25 +285,20 @@ func (w *Watcher) Remove(name string) error {
// Fetch the watch.
w.mu.Lock()
defer w.mu.Unlock()
watch, ok := w.watches[name]

// Remove it from inotify.
watch, ok := w.watches[name]
if !ok {
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
}

// We successfully removed the watch if InotifyRmWatch doesn't return an
// error, we need to clean up our internal state to ensure it matches
// inotify's kernel state.
return w.remove(name, watch)
}

// Unlocked!
func (w *Watcher) remove(name string, watch *watch) error {
delete(w.paths, int(watch.wd))
delete(w.watches, name)

// inotify_rm_watch will return EINVAL if the file has been deleted;
// the inotify will already have been removed.
// watches and pathes are deleted in ignoreLinux() implicitly and asynchronously
// by calling inotify_rm_watch() below. e.g. readEvents() goroutine receives IN_IGNORE
// so that EINVAL means that the wd is being rm_watch()ed or its file removed
// by another thread and we have not received IN_IGNORE event.
success, errno := unix.InotifyRmWatch(w.fd, watch.wd)
if success == -1 {
// TODO: Perhaps it's not helpful to return an error here in every case;
Expand All @@ -318,7 +313,6 @@ func (w *Watcher) Remove(name string) error {
// are watching is deleted.
return errno
}

return nil
}

Expand Down Expand Up @@ -417,14 +411,23 @@ func (w *Watcher) readEvents() {
// the "paths" map.
w.mu.Lock()
name, ok := w.paths[int(raw.Wd)]
// IN_DELETE_SELF occurs when the file/directory being watched is removed.
// This is a sign to clean up the maps, otherwise we are no longer in sync
// with the inotify kernel state which has already deleted the watch
// automatically.
// inotify will automatically remove the watch on deletes; just need
// to clean our state here.
if ok && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
delete(w.paths, int(raw.Wd))
delete(w.watches, name)
}
// 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 ok && mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF {
err := w.remove(name, w.watches[name])
if err != nil {
if !w.sendError(err) {
return
}
}
}
w.mu.Unlock()

if nameLen > 0 {
Expand Down
12 changes: 6 additions & 6 deletions backend_kqueue.go
Expand Up @@ -241,11 +241,11 @@ 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. A watch will be automatically removed if the path is deleted.
// added.
//
// 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
// filesystems (/proc, /sys, etc.) generally don't work.
Expand Down Expand Up @@ -677,8 +677,8 @@ func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInf

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
// mimic Linux providing delete events for subdirectories, but preserve
// the flags used if currently watching subdirectory
w.mu.Lock()
flags := w.dirFlags[name]
w.mu.Unlock()
Expand Down
8 changes: 4 additions & 4 deletions backend_other.go
Expand Up @@ -25,11 +25,11 @@ 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. A watch will be automatically removed if the path is deleted.
// added.
//
// 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
// filesystems (/proc, /sys, etc.) generally don't work.
Expand Down
8 changes: 4 additions & 4 deletions backend_windows.go
Expand Up @@ -196,11 +196,11 @@ 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. A watch will be automatically removed if the path is deleted.
// added.
//
// 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
// filesystems (/proc, /sys, etc.) generally don't work.
Expand Down
61 changes: 16 additions & 45 deletions fsnotify_test.go
Expand Up @@ -444,38 +444,21 @@ func TestWatchRename(t *testing.T) {
`},

{"rename watched directory", func(t *testing.T, w *Watcher, tmp string) {
addWatch(t, w, tmp)

dir := filepath.Join(tmp, "dir")
mkdir(t, dir)
addWatch(t, w, dir)

mv(t, dir, tmp, "dir-renamed")
touch(t, tmp, "dir-renamed/file")
}, `
CREATE "/dir" # mkdir
RENAME "/dir" # mv
CREATE "/dir-renamed"
RENAME "/dir"
CREATE "/dir/file" # touch

windows:
CREATE "/dir" # mkdir
RENAME "/dir" # mv
CREATE "/dir-renamed"
CREATE "/dir-renamed/file" # touch
rename /dir

# TODO: no results for the touch; this is probably a bug; windows
# was fixed in #370.
kqueue:
CREATE "/dir" # mkdir
CREATE "/dir-renamed" # mv
REMOVE|RENAME "/dir"
fen:
CREATE "/dir" # mkdir
RENAME "/dir" # mv
CREATE "/dir-renamed"
WRITE "/dir-renamed" # touch
remove|rename /dir

# TODO(v2): Windows should behave the same by default. See #518
windows:
create /dir/file
`},

{"rename watched file", func(t *testing.T, w *Watcher, tmp string) {
Expand All @@ -488,19 +471,12 @@ func TestWatchRename(t *testing.T) {
mv(t, file, rename)
mv(t, rename, tmp, "rename-two")
}, `
# TODO: this should update the path. And even then, not clear what
# go renamed to what.
rename /file # mv file rename
rename /file # mv rename rename-two

# TODO: seems to lose the watch?
kqueue, fen:
rename /file
rename /file

# It's actually more correct on Windows.
# TODO(v2): Windows should behave the same by default. See #518
windows:
rename /file
rename /rename-one
rename /file
rename /rename-one
`},

{"re-add renamed file", func(t *testing.T, w *Watcher, tmp string) {
Expand All @@ -517,19 +493,14 @@ func TestWatchRename(t *testing.T) {
cat(t, "hello", file)
}, `
rename /file # mv file rename
write /rename # cat hello >rename
# Watcher gets removed on rename, so no write for /rename
write /file # cat hello >file

# TODO: wrong.
linux:
RENAME "/file"
WRITE "/file"
WRITE ""

# TODO: wrong.
kqueue, fen:
RENAME "/file"
WRITE "/file"
# TODO(v2): Windows should behave the same by default. See #518
windows:
rename /file
write /rename
write /file
`},
}

Expand Down
8 changes: 4 additions & 4 deletions mkdoc.zsh
Expand Up @@ -78,11 +78,11 @@ add=$(<<EOF
//
// 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.
// added.
//
// 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
// filesystems (/proc, /sys, etc.) generally don't work.
Expand Down