watchexec/crates/lib/src/fs.rs

345 lines
8.8 KiB
Rust
Raw Normal View History

//! Event source for changes to files and directories.
2021-08-16 15:15:17 +02:00
use std::{
collections::{HashMap, HashSet},
2021-10-12 17:06:39 +02:00
fs::metadata,
mem::take,
2021-10-14 14:38:21 +02:00
path::{Path, PathBuf},
2021-08-24 12:28:29 +02:00
time::Duration,
2021-08-16 15:15:17 +02:00
};
2021-08-16 11:49:12 +02:00
use async_priority_channel as priority;
2023-01-06 14:53:49 +01:00
use normalize_path::NormalizePath;
2022-09-02 11:12:47 +02:00
use notify::{Config, Watcher as _};
2021-08-16 15:15:17 +02:00
use tokio::sync::{mpsc, watch};
2023-01-06 14:53:49 +01:00
use tracing::{debug, error, trace};
2021-08-16 11:49:12 +02:00
2021-08-16 15:15:17 +02:00
use crate::{
error::{CriticalError, FsWatcherError, RuntimeError},
event::{Event, Priority, Source, Tag},
2021-08-16 15:15:17 +02:00
};
2021-08-16 11:49:12 +02:00
/// 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 {
2021-10-16 09:02:17 +02:00
/// 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.
2021-08-16 11:49:12 +02:00
Native,
2021-10-16 09:02:17 +02:00
/// Notifys [poll watcher][notify::PollWatcher] with a custom interval.
2021-08-24 12:28:29 +02:00
Poll(Duration),
2021-08-16 11:49:12 +02:00
}
impl Default for Watcher {
2021-08-16 15:15:17 +02:00
fn default() -> Self {
Self::Native
}
2021-08-16 11:49:12 +02:00
}
impl Watcher {
2021-08-18 15:12:50 +02:00
fn create(
self,
2021-08-19 16:59:39 +02:00
f: impl notify::EventHandler,
2021-08-18 15:12:50 +02:00
) -> Result<Box<dyn notify::Watcher + Send>, RuntimeError> {
2021-08-16 11:49:12 +02:00
match self {
2022-09-02 11:12:47 +02:00
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 _)
}
2021-08-16 15:15:17 +02:00
}
.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)
},
})
2021-08-16 11:49:12 +02:00
}
}
/// The configuration of the [fs][self] worker.
2021-08-16 11:49:12 +02:00
///
/// This is marked non-exhaustive so new configuration can be added without breaking.
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct WorkingData {
2021-10-16 09:02:17 +02:00
/// The set of paths to be watched.
2021-10-14 14:38:21 +02:00
pub pathset: Vec<WatchedPath>,
2021-10-16 09:02:17 +02:00
/// The kind of watcher to be used.
2021-08-16 11:49:12 +02:00
pub watcher: Watcher,
}
2021-10-14 14:38:21 +02:00
/// 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<PathBuf> for WatchedPath {
fn from(path: PathBuf) -> Self {
Self(path)
}
}
2021-10-15 14:13:39 +02:00
impl From<&str> for WatchedPath {
fn from(path: &str) -> Self {
Self(path.into())
}
}
2021-10-14 14:38:21 +02:00
impl From<&Path> for WatchedPath {
fn from(path: &Path) -> Self {
Self(path.into())
}
}
impl From<WatchedPath> for PathBuf {
fn from(path: WatchedPath) -> Self {
path.0
}
}
impl AsRef<Path> for WatchedPath {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}
2021-08-16 15:15:17 +02:00
/// Launch the filesystem event worker.
///
/// While you can run several, you should only have one.
2021-08-16 11:49:12 +02:00
///
2021-09-29 17:03:46 +02:00
/// 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.
///
2023-01-06 14:53:49 +01:00
/// 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).
2021-08-16 15:15:17 +02:00
///
/// # Examples
///
/// Direct usage:
///
/// ```no_run
/// use async_priority_channel as priority;
2021-08-16 15:15:17 +02:00
/// use tokio::sync::{mpsc, watch};
/// use watchexec::fs::{worker, WorkingData};
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let (ev_s, _) = priority::bounded(1024);
2021-08-16 15:15:17 +02:00
/// 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(())
/// }
/// ```
2021-08-16 11:49:12 +02:00
pub async fn worker(
mut working: watch::Receiver<WorkingData>,
errors: mpsc::Sender<RuntimeError>,
events: priority::Sender<Event, Priority>,
2021-08-16 11:49:12 +02:00
) -> Result<(), CriticalError> {
debug!("launching filesystem worker");
let mut watcher_type = Watcher::default();
2021-08-18 14:40:35 +02:00
let mut watcher = None;
let mut pathset = HashSet::new();
2021-08-16 11:49:12 +02:00
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());
2023-01-06 14:53:49 +01:00
for path in &data.pathset {
2021-08-16 11:49:12 +02:00
if !pathset.contains(path) {
to_watch.push(path.clone());
}
}
2023-01-06 14:53:49 +01:00
for path in &pathset {
2021-08-16 11:49:12 +02:00
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();
2021-08-16 15:15:17 +02:00
match kind.create(move |nev: Result<notify::Event, notify::Error>| {
2021-08-16 11:49:12 +02:00
trace!(event = ?nev, "receiving possible event from watcher");
2023-01-06 14:53:49 +01:00
if let Err(e) = process_event(nev, kind, &n_events) {
2021-08-16 15:15:17 +02:00
n_errors.try_send(e).ok();
2021-08-16 11:49:12 +02:00
}
}) {
Ok(w) => {
2021-09-29 17:03:46 +02:00
watcher = Some(w);
2021-08-16 11:49:12 +02:00
watcher_type = kind;
2021-08-16 15:15:17 +02:00
}
2021-08-16 11:49:12 +02:00
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");
2021-10-14 14:38:21 +02:00
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?;
}
2021-08-16 11:49:12 +02:00
} else {
pathset.remove(&path);
}
}
for path in to_watch {
trace!(?path, "adding path to the watcher");
2021-10-14 14:38:21 +02:00
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
2021-10-15 12:00:41 +02:00
// See https://github.com/watchexec/watchexec/issues/218
2021-08-16 11:49:12 +02:00
} else {
pathset.insert(path);
}
}
}
}
2021-08-16 15:37:01 +02:00
debug!("ending file watcher");
2021-08-16 11:49:12 +02:00
Ok(())
}
2021-08-18 14:40:35 +02:00
fn notify_multi_path_errors(
kind: Watcher,
2021-10-14 14:38:21 +02:00
path: WatchedPath,
mut err: notify::Error,
rm: bool,
) -> Vec<RuntimeError> {
let mut paths = take(&mut err.paths);
if paths.is_empty() {
2021-10-14 14:38:21 +02:00
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
}
2021-08-16 15:15:17 +02:00
fn process_event(
nev: Result<notify::Event, notify::Error>,
kind: Watcher,
2023-01-06 14:53:49 +01:00
n_events: &priority::Sender<Event, Priority>,
2021-08-16 15:15:17 +02:00
) -> Result<(), RuntimeError> {
let nev = nev.map_err(|err| RuntimeError::FsWatcher {
kind,
err: FsWatcherError::Event(err),
})?;
2021-08-16 15:15:17 +02:00
2021-09-13 09:34:40 +02:00
let mut tags = Vec::with_capacity(4);
tags.push(Tag::Source(Source::Filesystem));
tags.push(Tag::FileEventKind(nev.kind));
2021-08-16 15:15:17 +02:00
for path in nev.paths {
2021-10-12 17:06:39 +02:00
// 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()),
2023-01-06 14:53:49 +01:00
path: path.normalize(),
2021-10-12 17:06:39 +02:00
});
2021-08-16 15:15:17 +02:00
}
if let Some(pid) = nev.attrs.process_id() {
2021-09-13 09:34:40 +02:00
tags.push(Tag::Process(pid));
2021-08-16 15:15:17 +02:00
}
2021-09-02 22:14:04 +02:00
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()]);
}
2021-09-13 09:51:07 +02:00
let ev = Event { tags, metadata };
2021-08-16 15:15:17 +02:00
trace!(event = ?ev, "processed notify event into watchexec event");
n_events
.try_send(ev, Priority::Normal)
2021-08-17 11:41:13 +02:00
.map_err(|err| RuntimeError::EventChannelTrySend {
2021-08-16 15:15:17 +02:00
ctx: "fs watcher",
err,
})?;
Ok(())
}