From 8213aa12c3540a4105b0ae916f15ba754f4dc934 Mon Sep 17 00:00:00 2001 From: Hugo van der Wijst Date: Wed, 31 Jul 2024 11:22:06 -0700 Subject: [PATCH 1/2] Add `watch_filtered`, filtering paths to watch. --- notify/src/inotify.rs | 49 ++++++++++++++++++++++++++++++++++--------- notify/src/lib.rs | 28 ++++++++++++++++++++++++- notify/src/null.rs | 9 ++++++-- notify/src/poll.rs | 11 ++++++++-- 4 files changed, 82 insertions(+), 15 deletions(-) diff --git a/notify/src/inotify.rs b/notify/src/inotify.rs index 3c35787c..2d5bae5a 100644 --- a/notify/src/inotify.rs +++ b/notify/src/inotify.rs @@ -5,7 +5,9 @@ //! will return events for the directory itself, and for files inside the directory. use super::event::*; -use super::{Config, Error, ErrorKind, EventHandler, RecursiveMode, Result, Watcher}; +use super::{ + Config, Error, ErrorKind, EventHandler, RecursiveMode, Result, WatchFilterFn, Watcher, +}; use crate::{bounded, unbounded, BoundSender, Receiver, Sender}; use inotify as inotify_sys; use inotify_sys::{EventMask, Inotify, WatchDescriptor, WatchMask}; @@ -50,7 +52,12 @@ pub struct INotifyWatcher { } enum EventLoopMsg { - AddWatch(PathBuf, RecursiveMode, Sender>), + AddWatch( + PathBuf, + RecursiveMode, + Box, + Sender>, + ), RemoveWatch(PathBuf, Sender>), Shutdown, Configure(Config, BoundSender>), @@ -166,8 +173,13 @@ impl EventLoop { fn handle_messages(&mut self) { while let Ok(msg) = self.event_loop_rx.try_recv() { match msg { - EventLoopMsg::AddWatch(path, recursive_mode, tx) => { - let _ = tx.send(self.add_watch(path, recursive_mode.is_recursive(), true)); + EventLoopMsg::AddWatch(path, recursive_mode, watch_filter, tx) => { + let _ = tx.send(self.add_watch( + path, + recursive_mode.is_recursive(), + true, + &watch_filter, + )); } EventLoopMsg::RemoveWatch(path, tx) => { let _ = tx.send(self.remove_watch(path, false)); @@ -386,11 +398,17 @@ impl EventLoop { } for path in add_watches { - self.add_watch(path, true, false).ok(); + self.add_watch(path, true, false, &|_| true).ok(); } } - fn add_watch(&mut self, path: PathBuf, is_recursive: bool, mut watch_self: bool) -> Result<()> { + fn add_watch( + &mut self, + path: PathBuf, + is_recursive: bool, + mut watch_self: bool, + watch_filter: &dyn Fn(&Path) -> bool, + ) -> Result<()> { // If the watch is not recursive, or if we determine (by stat'ing the path to get its // metadata) that the watched path is not a directory, add a single path watch. if !is_recursive || !metadata(&path).map_err(Error::io)?.is_dir() { @@ -401,6 +419,7 @@ impl EventLoop { .follow_links(true) .into_iter() .filter_map(filter_dir) + .filter(|e| watch_filter(e.path())) { self.add_single_watch(entry.path().to_path_buf(), is_recursive, watch_self)?; watch_self = false; @@ -531,7 +550,12 @@ impl INotifyWatcher { Ok(INotifyWatcher { channel, waker }) } - fn watch_inner(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { + fn watch_inner( + &mut self, + path: &Path, + recursive_mode: RecursiveMode, + watch_filter: Box, + ) -> Result<()> { let pb = if path.is_absolute() { path.to_owned() } else { @@ -539,7 +563,7 @@ impl INotifyWatcher { p.join(path) }; let (tx, rx) = unbounded(); - let msg = EventLoopMsg::AddWatch(pb, recursive_mode, tx); + let msg = EventLoopMsg::AddWatch(pb, recursive_mode, watch_filter, tx); // we expect the event loop to live and reply => unwraps must not panic self.channel.send(msg).unwrap(); @@ -570,8 +594,13 @@ impl Watcher for INotifyWatcher { Self::from_event_handler(Box::new(event_handler)) } - fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { - self.watch_inner(path, recursive_mode) + fn watch_filtered( + &mut self, + path: &Path, + recursive_mode: RecursiveMode, + watch_filter: Box, + ) -> Result<()> { + self.watch_inner(path, recursive_mode, watch_filter) } fn unwatch(&mut self, path: &Path) -> Result<()> { diff --git a/notify/src/lib.rs b/notify/src/lib.rs index 40d7fa03..cdaf18bc 100644 --- a/notify/src/lib.rs +++ b/notify/src/lib.rs @@ -324,6 +324,8 @@ pub enum WatcherKind { NullWatcher, } +type WatchFilterFn = dyn Fn(&Path) -> bool + Send; + /// Type that can deliver file activity notifications /// /// Watcher is implemented per platform using the best implementation available on that platform. @@ -349,7 +351,31 @@ pub trait Watcher { /// /// [#165]: https://github.com/notify-rs/notify/issues/165 /// [#166]: https://github.com/notify-rs/notify/issues/166 - fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()>; + fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { + self.watch_filtered(path, recursive_mode, Box::new(|_| true)) + } + + /// Begin watching a new path, filtering out sub-paths by name. + /// + /// If the `path` is a directory, `recursive_mode` will be evaluated. If `recursive_mode` is + /// `RecursiveMode::Recursive` events will be delivered for all files in that tree. Otherwise + /// only the directory and its immediate children will be watched. + /// + /// If the `path` is a file, `recursive_mode` will be ignored and events will be delivered only + /// for the file. + /// + /// On some platforms, if the `path` is renamed or removed while being watched, behaviour may + /// be unexpected. See discussions in [#165] and [#166]. If less surprising behaviour is wanted + /// one may non-recursively watch the _parent_ directory as well and manage related events. + /// + /// [#165]: https://github.com/notify-rs/notify/issues/165 + /// [#166]: https://github.com/notify-rs/notify/issues/166 + fn watch_filtered( + &mut self, + path: &Path, + recursive_mode: RecursiveMode, + watch_filter: Box, + ) -> Result<()>; /// Stop watching a path. /// diff --git a/notify/src/null.rs b/notify/src/null.rs index bbcd80d9..c8a99b1c 100644 --- a/notify/src/null.rs +++ b/notify/src/null.rs @@ -4,7 +4,7 @@ use crate::Config; -use super::{RecursiveMode, Result, Watcher}; +use super::{RecursiveMode, Result, WatchFilterFn, Watcher}; use std::path::Path; /// Stub `Watcher` implementation @@ -14,7 +14,12 @@ use std::path::Path; pub struct NullWatcher; impl Watcher for NullWatcher { - fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { + fn watch_filtered( + &mut self, + path: &Path, + recursive_mode: RecursiveMode, + watch_filter: Box, + ) -> Result<()> { Ok(()) } diff --git a/notify/src/poll.rs b/notify/src/poll.rs index 9a9a9e17..ba4be0e1 100644 --- a/notify/src/poll.rs +++ b/notify/src/poll.rs @@ -3,7 +3,9 @@ //! Checks the `watch`ed paths periodically to detect changes. This implementation only uses //! Rust stdlib APIs and should work on all of the platforms it supports. -use crate::{unbounded, Config, Error, EventHandler, Receiver, RecursiveMode, Sender, Watcher}; +use crate::{ + unbounded, Config, Error, EventHandler, Receiver, RecursiveMode, Sender, WatchFilterFn, Watcher, +}; use std::{ collections::HashMap, path::{Path, PathBuf}, @@ -612,7 +614,12 @@ impl Watcher for PollWatcher { Self::new(event_handler, config) } - fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> crate::Result<()> { + fn watch_filtered( + &mut self, + path: &Path, + recursive_mode: RecursiveMode, + _watch_filter: Box, + ) -> crate::Result<()> { self.watch_inner(path, recursive_mode); Ok(()) From 561c4bc5529648674a098d3c24748d9efd8e0a8c Mon Sep 17 00:00:00 2001 From: Hugo van der Wijst Date: Wed, 6 Nov 2024 16:32:38 -0800 Subject: [PATCH 2/2] Add fsevent support and filter newly added paths too. --- notify/src/fsevent.rs | 43 ++++++++++++++++++-------- notify/src/inotify.rs | 71 +++++++++++++++++++++++++------------------ notify/src/lib.rs | 37 +++++++++++++++++++--- notify/src/null.rs | 4 +-- notify/src/poll.rs | 4 +-- 5 files changed, 109 insertions(+), 50 deletions(-) diff --git a/notify/src/fsevent.rs b/notify/src/fsevent.rs index 56bdcd9d..3d77aeb7 100644 --- a/notify/src/fsevent.rs +++ b/notify/src/fsevent.rs @@ -15,7 +15,9 @@ #![allow(non_upper_case_globals, dead_code)] use crate::event::*; -use crate::{unbounded, Config, Error, EventHandler, RecursiveMode, Result, Sender, Watcher}; +use crate::{ + unbounded, Config, Error, EventHandler, RecursiveMode, Result, Sender, WatchFilter, Watcher, +}; use fsevent_sys as fs; use fsevent_sys::core_foundation as cf; use std::collections::HashMap; @@ -66,7 +68,7 @@ pub struct FsEventWatcher { flags: fs::FSEventStreamCreateFlags, event_handler: Arc>, runloop: Option<(cf::CFRunLoopRef, thread::JoinHandle<()>)>, - recursive_info: HashMap, + recursive_info: HashMap, } impl fmt::Debug for FsEventWatcher { @@ -242,7 +244,7 @@ fn translate_flags(flags: StreamFlags, precise: bool) -> Vec { struct StreamContextInfo { event_handler: Arc>, - recursive_info: HashMap, + recursive_info: HashMap, } // Free the context when the stream created by `FSEventStreamCreate` is released. @@ -280,9 +282,14 @@ impl FsEventWatcher { }) } - fn watch_inner(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { + fn watch_inner( + &mut self, + path: &Path, + recursive_mode: RecursiveMode, + watch_filter: WatchFilter, + ) -> Result<()> { self.stop(); - let result = self.append_path(path, recursive_mode); + let result = self.append_path(path, recursive_mode, watch_filter); // ignore return error: may be empty path list let _ = self.run(); result @@ -360,7 +367,12 @@ impl FsEventWatcher { } // https://github.com/thibaudgg/rb-fsevent/blob/master/ext/fsevent_watch/main.c - fn append_path(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { + fn append_path( + &mut self, + path: &Path, + recursive_mode: RecursiveMode, + watch_filter: WatchFilter, + ) -> Result<()> { if !path.exists() { return Err(Error::path_not_found().add_path(path.into())); } @@ -378,8 +390,10 @@ impl FsEventWatcher { cf::CFArrayAppendValue(self.paths, cf_path); cf::CFRelease(cf_path); } - self.recursive_info - .insert(canonical_path, recursive_mode.is_recursive()); + self.recursive_info.insert( + canonical_path, + (recursive_mode.is_recursive(), watch_filter), + ); Ok(()) } @@ -522,8 +536,8 @@ unsafe fn callback_impl( }); let mut handle_event = false; - for (p, r) in &(*info).recursive_info { - if path.starts_with(p) { + for (p, (r, filt)) in &(*info).recursive_info { + if path.starts_with(p) && filt.should_watch(p) { if *r || &path == p { handle_event = true; break; @@ -557,8 +571,13 @@ impl Watcher for FsEventWatcher { Self::from_event_handler(Arc::new(Mutex::new(event_handler))) } - fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { - self.watch_inner(path, recursive_mode) + fn watch_filtered( + &mut self, + path: &Path, + recursive_mode: RecursiveMode, + watch_filter: WatchFilter, + ) -> Result<()> { + self.watch_inner(path, recursive_mode, watch_filter) } fn unwatch(&mut self, path: &Path) -> Result<()> { diff --git a/notify/src/inotify.rs b/notify/src/inotify.rs index 2d5bae5a..801e408c 100644 --- a/notify/src/inotify.rs +++ b/notify/src/inotify.rs @@ -5,9 +5,7 @@ //! will return events for the directory itself, and for files inside the directory. use super::event::*; -use super::{ - Config, Error, ErrorKind, EventHandler, RecursiveMode, Result, WatchFilterFn, Watcher, -}; +use super::{Config, Error, ErrorKind, EventHandler, RecursiveMode, Result, WatchFilter, Watcher}; use crate::{bounded, unbounded, BoundSender, Receiver, Sender}; use inotify as inotify_sys; use inotify_sys::{EventMask, Inotify, WatchDescriptor, WatchMask}; @@ -39,7 +37,7 @@ struct EventLoop { inotify: Option, event_handler: Box, /// PathBuf -> (WatchDescriptor, WatchMask, is_recursive, is_dir) - watches: HashMap, + watches: HashMap, paths: HashMap, rename_event: Option, } @@ -52,12 +50,7 @@ pub struct INotifyWatcher { } enum EventLoopMsg { - AddWatch( - PathBuf, - RecursiveMode, - Box, - Sender>, - ), + AddWatch(PathBuf, RecursiveMode, WatchFilter, Sender>), RemoveWatch(PathBuf, Sender>), Shutdown, Configure(Config, BoundSender>), @@ -67,15 +60,15 @@ enum EventLoopMsg { fn add_watch_by_event( path: &Option, event: &inotify_sys::Event<&OsStr>, - watches: &HashMap, - add_watches: &mut Vec, + watches: &HashMap, + add_watches: &mut Vec<(PathBuf, WatchFilter)>, ) { if let Some(ref path) = *path { if event.mask.contains(EventMask::ISDIR) { if let Some(parent_path) = path.parent() { - if let Some(&(_, _, is_recursive, _)) = watches.get(parent_path) { + if let Some(&(_, _, is_recursive, _, ref filter)) = watches.get(parent_path) { if is_recursive { - add_watches.push(path.to_owned()); + add_watches.push((path.to_owned(), filter.clone())); } } } @@ -86,7 +79,7 @@ fn add_watch_by_event( #[inline] fn remove_watch_by_event( path: &Option, - watches: &HashMap, + watches: &HashMap, remove_watches: &mut Vec, ) { if let Some(ref path) = *path { @@ -178,7 +171,7 @@ impl EventLoop { path, recursive_mode.is_recursive(), true, - &watch_filter, + watch_filter, )); } EventLoopMsg::RemoveWatch(path, tx) => { @@ -313,8 +306,8 @@ impl EventLoop { Some(watched_path) => { let current_watch = self.watches.get(watched_path); match current_watch { - Some(&(_, _, _, true)) => RemoveKind::Folder, - Some(&(_, _, _, false)) => RemoveKind::File, + Some(&(_, _, _, true, _)) => RemoveKind::Folder, + Some(&(_, _, _, false, _)) => RemoveKind::File, None => RemoveKind::Other, } } @@ -397,8 +390,8 @@ impl EventLoop { self.remove_watch(path, true).ok(); } - for path in add_watches { - self.add_watch(path, true, false, &|_| true).ok(); + for (path, filter) in add_watches { + self.add_watch(path, true, false, filter).ok(); } } @@ -407,21 +400,30 @@ impl EventLoop { path: PathBuf, is_recursive: bool, mut watch_self: bool, - watch_filter: &dyn Fn(&Path) -> bool, + watch_filter: WatchFilter, ) -> Result<()> { + if !watch_filter.should_watch(&path) { + return Ok(()); + } + // If the watch is not recursive, or if we determine (by stat'ing the path to get its // metadata) that the watched path is not a directory, add a single path watch. if !is_recursive || !metadata(&path).map_err(Error::io)?.is_dir() { - return self.add_single_watch(path, false, true); + return self.add_single_watch(path, false, true, WatchFilter::accept_all()); } for entry in WalkDir::new(path) .follow_links(true) .into_iter() .filter_map(filter_dir) - .filter(|e| watch_filter(e.path())) + .filter(|e| watch_filter.should_watch(e.path())) { - self.add_single_watch(entry.path().to_path_buf(), is_recursive, watch_self)?; + self.add_single_watch( + entry.path().to_path_buf(), + is_recursive, + watch_self, + watch_filter.clone(), + )?; watch_self = false; } @@ -433,6 +435,7 @@ impl EventLoop { path: PathBuf, is_recursive: bool, watch_self: bool, + watch_filter: WatchFilter, ) -> Result<()> { let mut watchmask = WatchMask::ATTRIB | WatchMask::CREATE @@ -447,7 +450,7 @@ impl EventLoop { watchmask.insert(WatchMask::MOVE_SELF); } - if let Some(&(_, old_watchmask, _, _)) = self.watches.get(&path) { + if let Some(&(_, old_watchmask, _, _, _)) = self.watches.get(&path) { watchmask.insert(old_watchmask); watchmask.insert(WatchMask::MASK_ADD); } @@ -468,8 +471,16 @@ impl EventLoop { Ok(w) => { watchmask.remove(WatchMask::MASK_ADD); let is_dir = metadata(&path).map_err(Error::io)?.is_dir(); - self.watches - .insert(path.clone(), (w.clone(), watchmask, is_recursive, is_dir)); + self.watches.insert( + path.clone(), + ( + w.clone(), + watchmask, + is_recursive, + is_dir, + watch_filter.clone(), + ), + ); self.paths.insert(w, path); Ok(()) } @@ -482,7 +493,7 @@ impl EventLoop { fn remove_watch(&mut self, path: PathBuf, remove_recursive: bool) -> Result<()> { match self.watches.remove(&path) { None => return Err(Error::watch_not_found().add_path(path)), - Some((w, _, is_recursive, _)) => { + Some((w, _, is_recursive, _, _)) => { if let Some(ref mut inotify) = self.inotify { let mut inotify_watches = inotify.watches(); log::trace!("removing inotify watch: {}", path.display()); @@ -554,7 +565,7 @@ impl INotifyWatcher { &mut self, path: &Path, recursive_mode: RecursiveMode, - watch_filter: Box, + watch_filter: WatchFilter, ) -> Result<()> { let pb = if path.is_absolute() { path.to_owned() @@ -598,7 +609,7 @@ impl Watcher for INotifyWatcher { &mut self, path: &Path, recursive_mode: RecursiveMode, - watch_filter: Box, + watch_filter: WatchFilter, ) -> Result<()> { self.watch_inner(path, recursive_mode, watch_filter) } diff --git a/notify/src/lib.rs b/notify/src/lib.rs index cdaf18bc..90d83344 100644 --- a/notify/src/lib.rs +++ b/notify/src/lib.rs @@ -179,7 +179,7 @@ pub use config::{Config, RecursiveMode}; pub use error::{Error, ErrorKind, Result}; pub use notify_types::event::{self, Event, EventKind}; -use std::path::Path; +use std::{path::Path, sync::Arc}; #[allow(dead_code)] #[cfg(feature = "crossbeam-channel")] @@ -324,7 +324,36 @@ pub enum WatcherKind { NullWatcher, } -type WatchFilterFn = dyn Fn(&Path) -> bool + Send; +type FilterFn = dyn Fn(&Path) -> bool + Send + Sync; +/// Path filter to limit what gets watched. +#[derive(Clone)] +pub struct WatchFilter(Option>); + +impl std::fmt::Debug for WatchFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("WatchFilterFn") + .field(&self.0.as_ref().map_or("no filter", |_| "filter fn")) + .finish() + } +} + +impl WatchFilter { + /// A filter that accepts any path, use to watch all paths. + pub fn accept_all() -> WatchFilter { + WatchFilter(None) + } + + /// A fitler to limit the paths that get watched. + /// + /// Only paths for which `filter` returns `true` will be watched. + pub fn with_filter(filter: Arc) -> WatchFilter { + WatchFilter(Some(filter)) + } + + fn should_watch(&self, path: &Path) -> bool { + self.0.as_ref().map_or(true, |f| f(path)) + } +} /// Type that can deliver file activity notifications /// @@ -352,7 +381,7 @@ pub trait Watcher { /// [#165]: https://github.com/notify-rs/notify/issues/165 /// [#166]: https://github.com/notify-rs/notify/issues/166 fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { - self.watch_filtered(path, recursive_mode, Box::new(|_| true)) + self.watch_filtered(path, recursive_mode, WatchFilter::accept_all()) } /// Begin watching a new path, filtering out sub-paths by name. @@ -374,7 +403,7 @@ pub trait Watcher { &mut self, path: &Path, recursive_mode: RecursiveMode, - watch_filter: Box, + watch_filter: WatchFilter, ) -> Result<()>; /// Stop watching a path. diff --git a/notify/src/null.rs b/notify/src/null.rs index c8a99b1c..be227005 100644 --- a/notify/src/null.rs +++ b/notify/src/null.rs @@ -4,7 +4,7 @@ use crate::Config; -use super::{RecursiveMode, Result, WatchFilterFn, Watcher}; +use super::{RecursiveMode, Result, WatchFilter, Watcher}; use std::path::Path; /// Stub `Watcher` implementation @@ -18,7 +18,7 @@ impl Watcher for NullWatcher { &mut self, path: &Path, recursive_mode: RecursiveMode, - watch_filter: Box, + watch_filter: WatchFilter, ) -> Result<()> { Ok(()) } diff --git a/notify/src/poll.rs b/notify/src/poll.rs index ba4be0e1..98072d18 100644 --- a/notify/src/poll.rs +++ b/notify/src/poll.rs @@ -4,7 +4,7 @@ //! Rust stdlib APIs and should work on all of the platforms it supports. use crate::{ - unbounded, Config, Error, EventHandler, Receiver, RecursiveMode, Sender, WatchFilterFn, Watcher, + unbounded, Config, Error, EventHandler, Receiver, RecursiveMode, Sender, WatchFilter, Watcher, }; use std::{ collections::HashMap, @@ -618,7 +618,7 @@ impl Watcher for PollWatcher { &mut self, path: &Path, recursive_mode: RecursiveMode, - _watch_filter: Box, + _watch_filter: WatchFilter, ) -> crate::Result<()> { self.watch_inner(path, recursive_mode);