//! Event source for changes to files and directories. use std::{ collections::{HashMap, HashSet}, fs::metadata, mem::take, path::{Path, PathBuf}, time::Duration, }; use async_priority_channel as priority; use normalize_path::NormalizePath; use notify::{Config, Watcher as _}; use tokio::sync::{mpsc, watch}; use tracing::{debug, error, trace}; use crate::{ error::{CriticalError, FsWatcherError, RuntimeError}, event::{Event, Priority, Source, Tag}, }; /// What kind of filesystem watcher to use. /// /// For now only native and poll watchers are supported. In the future there may be additional /// watchers available on some platforms. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum Watcher { /// The Notify-recommended watcher on the platform. /// /// For platforms Notify supports, that's a [native implementation][notify::RecommendedWatcher], /// for others it's polling with a default interval. Native, /// Notify’s [poll watcher][notify::PollWatcher] with a custom interval. Poll(Duration), } impl Default for Watcher { fn default() -> Self { Self::Native } } impl Watcher { fn create( self, f: impl notify::EventHandler, ) -> Result, RuntimeError> { match self { Self::Native => { notify::RecommendedWatcher::new(f, Config::default()).map(|w| Box::new(w) as _) } Self::Poll(delay) => { notify::PollWatcher::new(f, Config::default().with_poll_interval(delay)) .map(|w| Box::new(w) as _) } } .map_err(|err| RuntimeError::FsWatcher { kind: self, err: if cfg!(target_os = "linux") && (matches!(err.kind, notify::ErrorKind::MaxFilesWatch) || matches!(err.kind, notify::ErrorKind::Io(ref ioerr) if ioerr.raw_os_error() == Some(28))) { FsWatcherError::TooManyWatches(err) } else if cfg!(target_os = "linux") && matches!(err.kind, notify::ErrorKind::Io(ref ioerr) if ioerr.raw_os_error() == Some(24)) { FsWatcherError::TooManyHandles(err) } else { FsWatcherError::Create(err) }, }) } } /// The configuration of the [fs][self] worker. /// /// This is marked non-exhaustive so new configuration can be added without breaking. #[derive(Clone, Debug, Default)] #[non_exhaustive] pub struct WorkingData { /// The set of paths to be watched. pub pathset: Vec, /// The kind of watcher to be used. pub watcher: Watcher, } /// A path to watch. /// /// This is currently only a wrapper around a [`PathBuf`], but may be augmented in the future. #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct WatchedPath(PathBuf); impl From for WatchedPath { fn from(path: PathBuf) -> Self { Self(path) } } impl From<&str> for WatchedPath { fn from(path: &str) -> Self { Self(path.into()) } } impl From<&Path> for WatchedPath { fn from(path: &Path) -> Self { Self(path.into()) } } impl From for PathBuf { fn from(path: WatchedPath) -> Self { path.0 } } impl AsRef for WatchedPath { fn as_ref(&self) -> &Path { self.0.as_ref() } } /// Launch the filesystem event worker. /// /// While you can run several, you should only have one. /// /// This only does a bare minimum of setup; to actually start the work, you need to set a non-empty /// pathset on the [`WorkingData`] with the [`watch`] channel, and send a notification. Take care /// _not_ to drop the watch sender: this will cause the worker to stop gracefully, which may not be /// what was expected. /// /// Note that the paths emitted by the watcher are normalised. No guarantee is made about the /// implementation or output of that normalisation (it may change without notice). /// /// # Examples /// /// Direct usage: /// /// ```no_run /// use async_priority_channel as priority; /// use tokio::sync::{mpsc, watch}; /// use watchexec::fs::{worker, WorkingData}; /// /// #[tokio::main] /// async fn main() -> Result<(), Box> { /// let (ev_s, _) = priority::bounded(1024); /// let (er_s, _) = mpsc::channel(64); /// let (wd_s, wd_r) = watch::channel(WorkingData::default()); /// /// let mut wkd = WorkingData::default(); /// wkd.pathset = vec![".".into()]; /// wd_s.send(wkd)?; /// /// worker(wd_r, er_s, ev_s).await?; /// Ok(()) /// } /// ``` pub async fn worker( mut working: watch::Receiver, errors: mpsc::Sender, events: priority::Sender, ) -> Result<(), CriticalError> { debug!("launching filesystem worker"); let mut watcher_type = Watcher::default(); let mut watcher = None; let mut pathset = HashSet::new(); while working.changed().await.is_ok() { // In separate scope so we drop the working read lock as early as we can let (new_watcher, to_watch, to_drop) = { let data = working.borrow(); trace!(?data, "filesystem worker got a working data change"); if data.pathset.is_empty() { trace!("no more watched paths, dropping watcher"); watcher.take(); pathset.drain(); continue; } if watcher.is_none() || watcher_type != data.watcher { pathset.drain(); (Some(data.watcher), data.pathset.clone(), Vec::new()) } else { let mut to_watch = Vec::with_capacity(data.pathset.len()); let mut to_drop = Vec::with_capacity(pathset.len()); for path in &data.pathset { if !pathset.contains(path) { to_watch.push(path.clone()); } } for path in &pathset { if !data.pathset.contains(path) { to_drop.push(path.clone()); } } (None, to_watch, to_drop) } }; if let Some(kind) = new_watcher { debug!(?kind, "creating new watcher"); let n_errors = errors.clone(); let n_events = events.clone(); match kind.create(move |nev: Result| { trace!(event = ?nev, "receiving possible event from watcher"); if let Err(e) = process_event(nev, kind, &n_events) { n_errors.try_send(e).ok(); } }) { Ok(w) => { watcher = Some(w); watcher_type = kind; } Err(e) => { errors.send(e).await?; } } } if let Some(w) = watcher.as_mut() { debug!(?to_watch, ?to_drop, "applying changes to the watcher"); for path in to_drop { trace!(?path, "removing path from the watcher"); if let Err(err) = w.unwatch(path.as_ref()) { error!(?err, "notify unwatch() error"); for e in notify_multi_path_errors(watcher_type, path, err, true) { errors.send(e).await?; } } else { pathset.remove(&path); } } for path in to_watch { trace!(?path, "adding path to the watcher"); if let Err(err) = w.watch(path.as_ref(), notify::RecursiveMode::Recursive) { error!(?err, "notify watch() error"); for e in notify_multi_path_errors(watcher_type, path, err, false) { errors.send(e).await?; } // TODO: unwatch and re-watch manually while ignoring all the erroring paths // See https://github.com/watchexec/watchexec/issues/218 } else { pathset.insert(path); } } } } debug!("ending file watcher"); Ok(()) } fn notify_multi_path_errors( kind: Watcher, path: WatchedPath, mut err: notify::Error, rm: bool, ) -> Vec { let mut paths = take(&mut err.paths); if paths.is_empty() { paths.push(path.into()); } 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(RuntimeError::FsWatcher { kind, err: if rm { FsWatcherError::PathRemove { path, err: e } } else { FsWatcherError::PathAdd { path, err: e } }, }); } errs } fn process_event( nev: Result, kind: Watcher, n_events: &priority::Sender, ) -> Result<(), RuntimeError> { let nev = nev.map_err(|err| RuntimeError::FsWatcher { kind, err: FsWatcherError::Event(err), })?; let mut tags = Vec::with_capacity(4); tags.push(Tag::Source(Source::Filesystem)); tags.push(Tag::FileEventKind(nev.kind)); for path in nev.paths { // possibly pull file_type from whatever notify (or the native driver) returns? tags.push(Tag::Path { file_type: metadata(&path).ok().map(|m| m.file_type().into()), path: path.normalize(), }); } if let Some(pid) = nev.attrs.process_id() { tags.push(Tag::Process(pid)); } let mut metadata = HashMap::new(); if let Some(uid) = nev.attrs.info() { metadata.insert("file-event-info".to_string(), vec![uid.to_string()]); } if let Some(src) = nev.attrs.source() { metadata.insert("notify-backend".to_string(), vec![src.to_string()]); } let ev = Event { tags, metadata }; trace!(event = ?ev, "processed notify event into watchexec event"); n_events .try_send(ev, Priority::Normal) .map_err(|err| RuntimeError::EventChannelTrySend { ctx: "fs watcher", err, })?; Ok(()) }