Skip to content

Commit 4476844

Browse files
riberkJohnTitor
andauthored
Fix: INotifyWatcher keeps watching deleted paths (#720)
Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
1 parent dcdadf3 commit 4476844

2 files changed

Lines changed: 59 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
## notify 9.0.0 (unreleased)
44
- CHANGE: raise MSRV to 1.85 **breaking**
55
- FIX: Fix the bug that `FsEventWatcher` crashes when dealing with empty path [#718]
6+
- FIX: Fix the bug that `INotifyWatcher` keeps watching deleted paths [#720]
67

78
[#718]: https://github.com/notify-rs/notify/pull/718
8-
9+
[#720]: https://github.com/notify-rs/notify/pull/720
910

1011
## notify 8.2.0 (2025-08-03)
1112
- FEATURE: notify user if inotify's `max_user_watches` has been reached [#698]

notify/src/inotify.rs

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,9 @@ impl EventLoop {
377377
}
378378

379379
for path in remove_watches {
380-
self.remove_watch(path, true).ok();
380+
if let Err(err) = self.remove_watch(path, true) {
381+
log::warn!("Unable to remove the path from the watches: {err:?}");
382+
}
381383
}
382384

383385
for path in add_watches {
@@ -476,20 +478,16 @@ impl EventLoop {
476478
Some((w, _, is_recursive, _)) => {
477479
if let Some(ref mut inotify) = self.inotify {
478480
let mut inotify_watches = inotify.watches();
479-
log::trace!("removing inotify watch: {}", path.display());
481+
log::trace!("removing inotify watch for {path:?}, remove_recursive: {remove_recursive:?}");
480482

481-
inotify_watches
482-
.remove(w.clone())
483-
.map_err(|e| Error::io(e).add_path(path.clone()))?;
483+
Self::remove_single_descriptor(&mut inotify_watches, w.clone());
484484
self.paths.remove(&w);
485485

486486
if is_recursive || remove_recursive {
487487
let mut remove_list = Vec::new();
488488
for (w, p) in &self.paths {
489489
if p.starts_with(&path) {
490-
inotify_watches
491-
.remove(w.clone())
492-
.map_err(|e| Error::io(e).add_path(p.into()))?;
490+
Self::remove_single_descriptor(&mut inotify_watches, w.clone());
493491
self.watches.remove(p);
494492
remove_list.push(w.clone());
495493
}
@@ -504,6 +502,34 @@ impl EventLoop {
504502
Ok(())
505503
}
506504

505+
/// As long as we use the `inotify` crate its behaviour is specified by the documentation of
506+
/// a [`inotify::Watches::remove`] method:
507+
/// ```text
508+
/// Directly returns the error from the call to [inotify_rm_watch].
509+
/// Returns an [io::Error] with [ErrorKind]::InvalidInput,
510+
/// if the given WatchDescriptor did not originate from this [Inotify] instance.
511+
/// ```
512+
///
513+
/// inotify documentation says, that `inotify_rm_watch` may fail with two specific errors:
514+
/// * EBADF - fd is not a valid file descriptor.
515+
/// * EINVAL - The watch descriptor wd is not valid or fd is not an inotify file descriptor.
516+
///
517+
/// Therefore, we can ignore this errors (and log it), because
518+
/// * in the case, when we are removing a watch because of an caught `DELETE` or `DELETE_SELF` event we want the
519+
/// path to be not watched, and in error cases it's already done (unknown file descriptor == it is not watched)
520+
/// * in the case, when user is trying to remove the watch, they can do nothing with that kind of an error,
521+
/// it's totally internal. BUT, if there are no "strange" states (like races between user call and internal call,
522+
/// when internal inotify file descriptor has already been invalidated, but the event still hasn't been handled)
523+
/// they will get an [`ErrorKind::WatchNotFound`] error and can deal with it
524+
///
525+
/// Log level is info, because it is not a "real" error. Expectedly, it may occurred only by race condition
526+
/// (like described above), in other cases it is a bug (but we aren't able to distinguish that states)
527+
fn remove_single_descriptor(watches: &mut inotify::Watches, wd: WatchDescriptor) {
528+
if let Err(err) = watches.remove(wd) {
529+
log::info!("unable to remove watch descriptor from inotify: {err:?}");
530+
}
531+
}
532+
507533
fn remove_all_watches(&mut self) -> Result<()> {
508534
if let Some(ref mut inotify) = self.inotify {
509535
let mut inotify_watches = inotify.watches();
@@ -771,6 +797,29 @@ mod tests {
771797
);
772798
}
773799

800+
/// https://github.com/notify-rs/notify/issues/709
801+
#[test]
802+
fn remove_a_subdir_in_a_recursively_watched_parent() {
803+
let tmpdir = tempfile::tempdir().expect("tmpdir");
804+
let subdirectory_path_1 = tmpdir.path().join("subdir");
805+
let subdirectory_path_2 = subdirectory_path_1.join("nested");
806+
std::fs::create_dir(&subdirectory_path_1).expect("unable to create a subdir");
807+
std::fs::create_dir(&subdirectory_path_2).expect("unable to create a nested dir");
808+
809+
let mut watcher =
810+
INotifyWatcher::new(|_| (), Config::default()).expect("unable to create watcher");
811+
watcher
812+
.watch(tmpdir.path(), RecursiveMode::Recursive)
813+
.expect("unable to watch");
814+
std::fs::remove_dir_all(&subdirectory_path_1).expect("unable to remove a subdir");
815+
let unwatch_result = watcher.unwatch(tmpdir.path());
816+
817+
assert!(
818+
matches!(unwatch_result, Ok(())),
819+
"error: {unwatch_result:#?}"
820+
);
821+
}
822+
774823
#[test]
775824
fn create_file() {
776825
let tmpdir = testdir();

0 commit comments

Comments
 (0)