watchexec/lib/src/fs/recursor.rs

135 lines
3.1 KiB
Rust

use std::{
collections::{HashMap, HashSet},
mem::take,
path::PathBuf,
};
use notify::{Error as NotifyError, RecursiveMode, Watcher};
use tracing::error;
use crate::error::RuntimeError;
use super::{Recurse, WatchedPath, Watcher as WatcherKind};
// TODO: for Recurse::Native, on error return a Recurse::Filtered with a filter
// that ignores the erroring paths.
// See https://github.com/watchexec/watchexec/issues/218
#[derive(Debug, Default)]
pub struct PathSet {
/// Just the recurse:no and recurse:native paths.
plain: HashSet<WatchedPath>,
/// The recurse:filtered paths, mapped to the subpaths they're currently watching.
filtered: HashMap<WatchedPath, HashSet<PathBuf>>,
}
impl PathSet {
pub(super) fn drain(&mut self) {
take(&mut self.plain);
take(&mut self.filtered);
}
pub(super) fn len(&self) -> usize {
self.plain.len().saturating_add(self.filtered.len())
}
pub(super) fn contains(&self, path: &WatchedPath) -> bool {
self.plain.contains(path) || self.filtered.contains_key(path)
}
pub(super) fn iter(&self) -> impl Iterator<Item = &WatchedPath> {
self.plain.iter().chain(self.filtered.keys())
}
fn add_plain(&mut self, path: &WatchedPath) {
self.plain.insert(path.clone());
}
fn rm_plain(&mut self, path: &WatchedPath) {
self.plain.remove(path);
}
}
impl WatchedPath {
pub(super) fn watch(
&self,
kind: WatcherKind,
watcher: &mut Box<dyn Watcher + Send>,
pathset: &mut PathSet,
) -> Result<(), RuntimeError> {
match &self.recursive {
Recurse::No => {
watcher
.watch(&self.dirpath, RecursiveMode::NonRecursive)
.map_err(|err| self.multi_path_error(kind, err, false))?;
pathset.add_plain(self);
}
Recurse::Native => {
watcher
.watch(&self.dirpath, RecursiveMode::Recursive)
.map_err(|err| self.multi_path_error(kind, err, false))?;
pathset.add_plain(self);
}
Recurse::Filtered(_f) => {
todo!()
}
}
Ok(())
}
pub(super) fn unwatch(
&self,
kind: WatcherKind,
watcher: &mut Box<dyn Watcher + Send>,
pathset: &mut PathSet,
) -> Result<(), RuntimeError> {
match &self.recursive {
Recurse::No | Recurse::Native => {
watcher
.unwatch(&self.dirpath)
.map_err(|err| self.multi_path_error(kind, err, false))?;
pathset.rm_plain(self);
}
Recurse::Filtered(_f) => {
todo!()
}
}
Ok(())
}
fn multi_path_error(&self, kind: WatcherKind, mut err: NotifyError, rm: bool) -> RuntimeError {
error!(?err, "notify {}watch() error", if rm { "un" } else { "" });
let mut paths = take(&mut err.paths);
if paths.is_empty() {
paths.push(self.dirpath.clone());
}
let generic = err.to_string();
let mut err = Some(err);
let mut errs = Vec::with_capacity(paths.len());
for path in paths {
let e = err
.take()
.unwrap_or_else(|| notify::Error::generic(&generic))
.add_path(path.clone());
errs.push(if rm {
RuntimeError::FsWatcherPathRemove { path, kind, err: e }
} else {
RuntimeError::FsWatcherPathAdd { path, kind, err: e }
});
}
if errs.len() == 1 {
errs.pop().expect("corrupt state (just checked)")
} else {
RuntimeError::Set(errs)
}
}
}