Skip to content

Commit

Permalink
Support watching the same inode through multiple paths.
Browse files Browse the repository at this point in the history
As a single change event can now refer to multiple paths, use "path
groups" for renames to differentiate sources and destinations.

Fixes notify-rs#572.
  • Loading branch information
Hugo van der Wijst committed Mar 8, 2024
1 parent 2511ebc commit 8a74886
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 52 deletions.
47 changes: 41 additions & 6 deletions notify-types/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,13 @@ pub struct Event {
/// creation, they should all go in this `Vec`. Otherwise, using the `Tracker` attr may be more
/// appropriate.
///
/// The order of the paths is likely to be significant! For example, renames where both ends of
/// the name change are known will have the "source" path first, and the "target" path last.
/// The order of the paths is likely to be significant! Furthermore, the paths might be part of
/// different "groups". For example, renames where both ends of the name change are known will
/// have the "source" paths first, followed by the "target" paths. These two sets of paths are
/// in distinct groups.
///
/// If there are different path groups, the `path_group_split_index` attribute indicates the
/// first index with paths in the second group.
pub paths: Vec<PathBuf>,

// "What should be in the struct" and "what can go in the attrs" is an interesting question.
Expand Down Expand Up @@ -400,6 +405,19 @@ struct EventAttributesInner {
)]
source: Option<String>,

/// Index of the first element in `paths` that is in the second path group.
///
/// In some cases there are two groups of paths. For instance, a renamed file can be accessed
/// through multiple source or destination paths (because of hard or symbolic links). Elements
/// in the `paths` vector located before this index are part of the first group (and thus refer
/// to the source file in the case of a rename), elements at or after the index are in the
/// secoud group (refering to the destination file).
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
path_group_split_index: Option<usize>,

/// The process ID of the originator of the event.
///
/// This attribute is experimental and, while included in Notify itself, is not considered
Expand Down Expand Up @@ -467,6 +485,10 @@ impl EventAttributes {
self.inner_mut().process_id = Some(process_id)
}

pub fn set_path_group_split_index(&mut self, path_group_split_index: usize) {
self.inner_mut().path_group_split_index = Some(path_group_split_index);
}

fn inner_mut(&mut self) -> &mut EventAttributesInner {
self.inner.get_or_insert_with(Box::default)
}
Expand Down Expand Up @@ -539,10 +561,16 @@ impl Event {
self
}

/// Adds a path to the event if the argument is Some.
pub fn add_some_path(self, path: Option<PathBuf>) -> Self {
if let Some(path) = path {
self.add_path(path)
/// Adds paths to the event.
pub fn add_paths(mut self, paths: &[PathBuf]) -> Self {
self.paths.extend_from_slice(paths);
self
}

/// Adds paths to the event if the argument is Some.
pub fn add_some_paths(self, paths: Option<&Vec<PathBuf>>) -> Self {
if let Some(paths) = paths {
self.add_paths(paths)
} else {
self
}
Expand Down Expand Up @@ -571,6 +599,13 @@ impl Event {
self.attrs.set_process_id(process_id);
self
}

/// Sets the tracker.
pub fn set_path_group_split_index(mut self, path_group_split_index: usize) -> Self {
self.attrs
.set_path_group_split_index(path_group_split_index);
self
}
}

impl fmt::Debug for Event {
Expand Down
124 changes: 78 additions & 46 deletions notify/src/inotify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ struct EventLoop {
event_handler: Box<dyn EventHandler>,
/// PathBuf -> (WatchDescriptor, WatchMask, is_recursive, is_dir)
watches: HashMap<PathBuf, (WatchDescriptor, WatchMask, bool, bool)>,
paths: HashMap<WatchDescriptor, PathBuf>,
paths: HashMap<WatchDescriptor, Vec<PathBuf>>,
rename_event: Option<Event>,
}

Expand All @@ -58,17 +58,19 @@ enum EventLoopMsg {

#[inline]
fn add_watch_by_event(
path: &Option<PathBuf>,
paths: &Option<Vec<PathBuf>>,
event: &inotify_sys::Event<&OsStr>,
watches: &HashMap<PathBuf, (WatchDescriptor, WatchMask, bool, bool)>,
add_watches: &mut Vec<PathBuf>,
) {
if let Some(ref path) = *path {
if let Some(ref paths) = *paths {
if event.mask.contains(EventMask::ISDIR) {
if let Some(parent_path) = path.parent() {
if let Some(&(_, _, is_recursive, _)) = watches.get(parent_path) {
if is_recursive {
add_watches.push(path.to_owned());
for path in paths {
if let Some(parent_path) = path.parent() {
if let Some(&(_, _, is_recursive, _)) = watches.get(parent_path) {
if is_recursive {
add_watches.push(path.to_owned());
}
}
}
}
Expand All @@ -78,13 +80,15 @@ fn add_watch_by_event(

#[inline]
fn remove_watch_by_event(
path: &Option<PathBuf>,
paths: &Option<Vec<PathBuf>>,
watches: &HashMap<PathBuf, (WatchDescriptor, WatchMask, bool, bool)>,
remove_watches: &mut Vec<PathBuf>,
) {
if let Some(ref path) = *path {
if watches.contains_key(path) {
remove_watches.push(path.to_owned());
if let Some(ref paths) = *paths {
for path in paths {
if watches.contains_key(path) {
remove_watches.push(path.to_owned());
}
}
}
}
Expand Down Expand Up @@ -212,20 +216,22 @@ impl EventLoop {
self.event_handler.handle_event(ev);
}

let path = match event.name {
Some(name) => self.paths.get(&event.wd).map(|root| root.join(name)),
let paths = match event.name {
Some(name) => self.paths.get(&event.wd).map(|roots| {
roots.iter().map(|root| root.join(name)).collect()
}),
None => self.paths.get(&event.wd).cloned(),
};

let mut evs = Vec::new();

if event.mask.contains(EventMask::MOVED_FROM) {
remove_watch_by_event(&path, &self.watches, &mut remove_watches);
remove_watch_by_event(&paths, &self.watches, &mut remove_watches);

let event = Event::new(EventKind::Modify(ModifyKind::Name(
RenameMode::From,
)))
.add_some_path(path.clone())
.add_some_paths(paths.as_ref())
.set_tracker(event.cookie as usize);

self.rename_event = Some(event.clone());
Expand All @@ -235,7 +241,7 @@ impl EventLoop {
evs.push(
Event::new(EventKind::Modify(ModifyKind::Name(RenameMode::To)))
.set_tracker(event.cookie as usize)
.add_some_path(path.clone()),
.add_some_paths(paths.as_ref()),
);

let trackers_match = self
Expand All @@ -253,18 +259,19 @@ impl EventLoop {
RenameMode::Both,
)))
.set_tracker(event.cookie as usize)
.add_some_path(rename_event.paths.first().cloned())
.add_some_path(path.clone()),
.add_paths(&rename_event.paths)
.set_path_group_split_index(rename_event.paths.len())
.add_some_paths(paths.as_ref()),
);
}
add_watch_by_event(&path, &event, &self.watches, &mut add_watches);
add_watch_by_event(&paths, &event, &self.watches, &mut add_watches);
}
if event.mask.contains(EventMask::MOVE_SELF) {
evs.push(
Event::new(EventKind::Modify(ModifyKind::Name(
RenameMode::From,
)))
.add_some_path(path.clone()),
.add_some_paths(paths.as_ref()),
);
// TODO stat the path and get to new path
// - emit To and Both events
Expand All @@ -279,9 +286,9 @@ impl EventLoop {
CreateKind::File
},
))
.add_some_path(path.clone()),
.add_some_paths(paths.as_ref()),
);
add_watch_by_event(&path, &event, &self.watches, &mut add_watches);
add_watch_by_event(&paths, &event, &self.watches, &mut add_watches);
}
if event.mask.contains(EventMask::DELETE) {
evs.push(
Expand All @@ -292,12 +299,12 @@ impl EventLoop {
RemoveKind::File
},
))
.add_some_path(path.clone()),
.add_some_paths(paths.as_ref()),
);
remove_watch_by_event(&path, &self.watches, &mut remove_watches);
remove_watch_by_event(&paths, &self.watches, &mut remove_watches);
}
if event.mask.contains(EventMask::DELETE_SELF) {
let remove_kind = match &path {
let remove_kind = match paths.as_ref().and_then(|ps| ps.first()) {
Some(watched_path) => {
let current_watch = self.watches.get(watched_path);
match current_watch {
Expand All @@ -315,48 +322,48 @@ impl EventLoop {
};
evs.push(
Event::new(EventKind::Remove(remove_kind))
.add_some_path(path.clone()),
.add_some_paths(paths.as_ref()),
);
remove_watch_by_event(&path, &self.watches, &mut remove_watches);
remove_watch_by_event(&paths, &self.watches, &mut remove_watches);
}
if event.mask.contains(EventMask::MODIFY) {
evs.push(
Event::new(EventKind::Modify(ModifyKind::Data(
DataChange::Any,
)))
.add_some_path(path.clone()),
.add_some_paths(paths.as_ref()),
);
}
if event.mask.contains(EventMask::CLOSE_WRITE) {
evs.push(
Event::new(EventKind::Access(AccessKind::Close(
AccessMode::Write,
)))
.add_some_path(path.clone()),
.add_some_paths(paths.as_ref()),
);
}
if event.mask.contains(EventMask::CLOSE_NOWRITE) {
evs.push(
Event::new(EventKind::Access(AccessKind::Close(
AccessMode::Read,
)))
.add_some_path(path.clone()),
.add_some_paths(paths.as_ref()),
);
}
if event.mask.contains(EventMask::ATTRIB) {
evs.push(
Event::new(EventKind::Modify(ModifyKind::Metadata(
MetadataKind::Any,
)))
.add_some_path(path.clone()),
.add_some_paths(paths.as_ref()),
);
}
if event.mask.contains(EventMask::OPEN) {
evs.push(
Event::new(EventKind::Access(AccessKind::Open(
AccessMode::Any,
)))
.add_some_path(path.clone()),
.add_some_paths(paths.as_ref()),
);
}

Expand Down Expand Up @@ -451,7 +458,7 @@ impl EventLoop {
let is_dir = metadata(&path).map_err(Error::io)?.is_dir();
self.watches
.insert(path.clone(), (w.clone(), watchmask, is_recursive, is_dir));
self.paths.insert(w, path);
self.paths.entry(w).or_default().push(path);
Ok(())
}
}
Expand All @@ -468,19 +475,40 @@ impl EventLoop {
let mut inotify_watches = inotify.watches();
log::trace!("removing inotify watch: {}", path.display());

inotify_watches
.remove(w.clone())
.map_err(|e| Error::io(e).add_path(path.clone()))?;
self.paths.remove(&w);
let remove_watch = self.paths.get_mut(&w).map_or(true, |paths| {
paths.retain(|p| p != &path);
paths.is_empty()
});
if remove_watch {
inotify_watches
.remove(w.clone())
.map_err(|e| Error::io(e).add_path(path.clone()))?;
self.paths.remove(&w);
}

if is_recursive || remove_recursive {
let mut remove_list = Vec::new();
for (w, p) in &self.paths {
if p.starts_with(&path) {
inotify_watches
.remove(w.clone())
.map_err(|e| Error::io(e).add_path(p.into()))?;
self.watches.remove(p);
for (w, paths) in &mut self.paths {
let mut removed_path = None;
let remove_watch = {
paths.retain(|p| {
let remove = p.starts_with(&path);
if remove {
removed_path = self.watches.remove_entry(p).map(|(k, _)| k);
}
!remove
});
paths.is_empty()
};

if remove_watch {
inotify_watches.remove(w.clone()).map_err(|e| {
let mut err = Error::io(e);
if let Some(p) = removed_path {
err = err.add_path(p);
}
err
})?;
remove_list.push(w.clone());
}
}
Expand All @@ -498,9 +526,13 @@ impl EventLoop {
if let Some(ref mut inotify) = self.inotify {
let mut inotify_watches = inotify.watches();
for (w, p) in &self.paths {
inotify_watches
.remove(w.clone())
.map_err(|e| Error::io(e).add_path(p.into()))?;
inotify_watches.remove(w.clone()).map_err(|e| {
let mut err = Error::io(e);
if let Some(path) = p.first() {
err = err.add_path(path.into());
}
err
})?;
}
self.watches.clear();
self.paths.clear();
Expand Down

0 comments on commit 8a74886

Please sign in to comment.