mod backend;
mod recursor;
use std::{
collections::{HashMap, HashSet, VecDeque},
fs::metadata,
mem::take,
path::PathBuf,
sync::{
atomic::{AtomicU64, Ordering},
Arc, Mutex,
},
time::Duration,
};
use async_priority_channel as priority;
use normalize_path::NormalizePath;
use notify::{
event::{ModifyKind, RenameMode},
EventKind,
};
use tokio::sync::mpsc;
use tracing::{debug, trace};
use watchexec_events::{Event, Priority, Source, Tag};
use crate::{
error::{CriticalError, FsWatcherError, RuntimeError},
filter::Filterer,
Config,
};
use self::{
backend::{classify_resource_error, create_notify, Backend},
recursor::{FsScanner, Recursor},
};
pub use crate::WatchedPath;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum Watcher {
#[default]
Native,
Poll(Duration),
}
impl Watcher {
fn create(
self,
follow_symlinks: bool,
f: impl notify::EventHandler,
) -> Result<Box<dyn Backend>, CriticalError> {
create_notify(self, follow_symlinks, f).map_err(|error| CriticalError::FsWatcherInit {
kind: self,
err: match classify_resource_error(&error) {
Some(resource) => resource.into_fs_error(error),
None => FsWatcherError::Create(error),
},
})
}
fn backend_kind(self) -> notify::WatcherKind {
self.backend_kind_for(<notify::RecommendedWatcher as notify::Watcher>::kind())
}
const fn backend_kind_for(self, recommended: notify::WatcherKind) -> notify::WatcherKind {
match self {
Self::Native => match recommended {
notify::WatcherKind::Kqueue => notify::WatcherKind::PollWatcher,
_ => recommended,
},
Self::Poll(_) => notify::WatcherKind::PollWatcher,
}
}
fn recursion_strategy(self) -> RecursionStrategy {
recursion_strategy(self.backend_kind())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RecursionStrategy {
PerDirectory,
RecursiveRoots,
NotifyOwned,
}
impl RecursionStrategy {
const fn owns_logical_tree(self) -> bool {
matches!(self, Self::PerDirectory | Self::RecursiveRoots)
}
}
const fn recursion_strategy(kind: notify::WatcherKind) -> RecursionStrategy {
match kind {
notify::WatcherKind::Inotify
| notify::WatcherKind::ReadDirectoryChangesWatcher
| notify::WatcherKind::PollWatcher => RecursionStrategy::PerDirectory,
notify::WatcherKind::Fsevent => RecursionStrategy::RecursiveRoots,
_ => RecursionStrategy::NotifyOwned,
}
}
const fn strategy_requires_recreation(
strategy: RecursionStrategy,
roots_changed: bool,
needs_retry: bool,
) -> bool {
match strategy {
RecursionStrategy::PerDirectory => false,
RecursionStrategy::RecursiveRoots | RecursionStrategy::NotifyOwned => {
roots_changed || needs_retry
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum Topology {
Create(PathBuf),
Remove(PathBuf),
Rename { from: PathBuf, to: PathBuf },
Ambiguous(PathBuf),
Rescan,
}
#[derive(Debug)]
enum ControlMessage {
Topology {
generation: u64,
sequence: u64,
changes: Vec<Topology>,
},
Error {
watcher: Watcher,
error: notify::Error,
},
}
#[derive(Debug)]
struct OrdinaryMessage {
generation: u64,
topology_sequence: u64,
event: notify::Event,
}
#[derive(Default)]
struct CallbackFence {
generation: AtomicU64,
topology_enqueued: AtomicU64,
serial: Mutex<()>,
}
#[derive(Clone, Copy, Debug)]
struct PendingReady {
topology_fence: Option<u64>,
}
fn dispatch_callback_event(
generation: u64,
watcher: Watcher,
control: &mpsc::UnboundedSender<ControlMessage>,
ordinary: &mpsc::Sender<OrdinaryMessage>,
callback_fence: &CallbackFence,
event: Result<notify::Event, notify::Error>,
) {
if callback_fence.generation.load(Ordering::Acquire) != generation {
return;
}
trace!(?event, "receiving possible event from watcher");
let changes = if watcher.recursion_strategy().owns_logical_tree() {
event.as_ref().ok().map(topology_messages)
} else {
None
};
let Ok(_serial) = callback_fence.serial.lock() else {
return;
};
if callback_fence.generation.load(Ordering::Acquire) != generation {
return;
}
match event {
Ok(event) => {
let changes = changes.unwrap_or_default();
let topology_sequence = if changes.is_empty() {
callback_fence.topology_enqueued.load(Ordering::Acquire)
} else {
let sequence = callback_fence
.topology_enqueued
.fetch_add(1, Ordering::AcqRel)
.wrapping_add(1);
if control
.send(ControlMessage::Topology {
generation,
sequence,
changes,
})
.is_err()
{
return;
}
sequence
};
match ordinary.try_send(OrdinaryMessage {
generation,
topology_sequence,
event,
}) {
Err(mpsc::error::TrySendError::Full(_)) => {
debug!("filesystem callback event lane is full; dropping ordinary event");
}
Ok(()) | Err(mpsc::error::TrySendError::Closed(_)) => {}
}
}
Err(error) => {
let _ = control.send(ControlMessage::Error { watcher, error });
}
}
}
struct AppliedConfig {
pathset: HashSet<WatchedPath>,
watcher: Watcher,
follow_symlinks: bool,
filter: Arc<dyn Filterer>,
}
struct WorkerState {
recursor: Option<Recursor>,
applied: Option<AppliedConfig>,
generation: u64,
cwd: PathBuf,
control: mpsc::UnboundedSender<ControlMessage>,
ordinary: mpsc::Sender<OrdinaryMessage>,
callback_fence: Arc<CallbackFence>,
events: priority::Sender<Event, Priority>,
processed_topology: u64,
retained_generations: HashSet<u64>,
pending_ready: Option<PendingReady>,
}
impl WorkerState {
fn new(
cwd: PathBuf,
control: mpsc::UnboundedSender<ControlMessage>,
ordinary: mpsc::Sender<OrdinaryMessage>,
callback_fence: Arc<CallbackFence>,
events: priority::Sender<Event, Priority>,
) -> Self {
Self {
recursor: None,
applied: None,
generation: 0,
cwd,
control,
ordinary,
callback_fence,
events,
processed_topology: 0,
retained_generations: HashSet::new(),
pending_ready: None,
}
}
fn apply_config(&mut self, config: &Config) -> Result<(), CriticalError> {
let pathset = config.pathset.get();
let normalized_pathset: HashSet<_> = pathset
.iter()
.map(|path| WatchedPath {
path: if path.path.is_absolute() {
path.path.normalize()
} else {
self.cwd.join(&path.path).normalize()
},
recursive: path.recursive,
})
.collect();
let watcher = config.file_watcher.get();
let strategy = watcher.recursion_strategy();
let follow_symlinks = config.follow_symlinks.get();
let filter = config.filterer.snapshot();
let roots_changed = self
.applied
.as_ref()
.map_or(true, |applied| applied.pathset != normalized_pathset);
let filter_changed = self
.applied
.as_ref()
.map_or(true, |applied| !Arc::ptr_eq(&applied.filter, &filter));
let watcher_changed = self
.applied
.as_ref()
.map_or(true, |applied| applied.watcher != watcher);
let follow_symlinks_changed = self
.applied
.as_ref()
.map_or(true, |applied| applied.follow_symlinks != follow_symlinks);
let configuration_changed =
roots_changed || filter_changed || watcher_changed || follow_symlinks_changed;
if configuration_changed {
self.retained_generations.clear();
}
let needs_retry = self.recursor.as_ref().map_or(false, Recursor::needs_retry);
let recreate = self.applied.is_none()
|| watcher_changed
|| follow_symlinks_changed
|| strategy_requires_recreation(strategy, roots_changed, needs_retry);
self.applied = Some(AppliedConfig {
pathset: normalized_pathset,
watcher,
follow_symlinks,
filter: filter.clone(),
});
if pathset.is_empty() {
let was_active = self.recursor.take().is_some();
if was_active {
trace!("no more watched paths, dropping watcher");
} else {
trace!("no watched paths, no watcher needed");
}
self.bump_generation();
return Ok(());
}
if recreate || self.recursor.is_none() {
debug!(kind=?watcher, follow_symlinks, "creating new watcher");
if !configuration_changed && self.recursor.is_some() {
self.retained_generations.insert(self.generation);
}
self.recursor.take();
let backend = self.create_backend(watcher, follow_symlinks)?;
let mut recursor = Recursor::new(
backend,
Box::new(FsScanner),
watcher,
watcher.backend_kind(),
follow_symlinks,
self.cwd.clone(),
filter.clone(),
);
recursor.reconcile(&pathset, filter);
self.recursor = Some(recursor);
} else if strategy.owns_logical_tree() && (roots_changed || filter_changed || needs_retry) {
trace!(
roots_changed,
filter_changed,
"reconciling filesystem sources"
);
if let Some(recursor) = self.recursor.as_mut() {
recursor.reconcile(&pathset, filter);
}
}
Ok(())
}
fn request_ready(&mut self, revision: u64) {
if revision > 0 {
self.pending_ready = Some(PendingReady {
topology_fence: None,
});
}
}
fn capture_ready_fence(&mut self) {
if !matches!(
self.pending_ready,
Some(PendingReady {
topology_fence: None,
..
})
) {
return;
}
let fence = {
let _serial = self
.callback_fence
.serial
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
self.callback_fence
.topology_enqueued
.load(Ordering::Acquire)
};
if let Some(ready) = self.pending_ready.as_mut() {
ready.topology_fence = Some(fence);
}
}
fn bump_generation(&mut self) -> u64 {
self.generation = self.generation.wrapping_add(1);
self.callback_fence
.generation
.store(self.generation, Ordering::Release);
self.generation
}
fn create_backend(
&mut self,
watcher: Watcher,
follow_symlinks: bool,
) -> Result<Box<dyn Backend>, CriticalError> {
let generation = self.bump_generation();
let control = self.control.clone();
let ordinary = self.ordinary.clone();
let callback_fence = self.callback_fence.clone();
watcher.create(follow_symlinks, move |event| {
dispatch_callback_event(
generation,
watcher,
&control,
&ordinary,
&callback_fence,
event,
);
})
}
fn rebuild_backend(
&mut self,
runtime_errors: &mut VecDeque<RuntimeError>,
) -> Result<(), CriticalError> {
let Some((watcher, follow_symlinks)) = self
.applied
.as_ref()
.map(|applied| (applied.watcher, applied.follow_symlinks))
else {
return Ok(());
};
let replay_limit = self
.recursor
.as_ref()
.map_or(1, |recursor| recursor.replay_count().saturating_add(2));
for _ in 0..replay_limit {
if let Some(recursor) = self.recursor.as_mut() {
recursor.prepare_backend_rebuild();
}
self.retained_generations.insert(self.generation);
let backend = self.create_backend(watcher, follow_symlinks)?;
let Some(recursor) = self.recursor.as_mut() else {
return Ok(());
};
recursor.install_backend(backend);
let mut replay = recursor.replay_backend_snapshot();
runtime_errors.extend(replay.errors.drain(..));
if !replay.rebuild_backend {
return Ok(());
}
}
if let Some(recursor) = self.recursor.as_mut() {
recursor.defer_replay();
}
Ok(())
}
fn handle_topology(&mut self, change: Topology) {
let Some(recursor) = self.recursor.as_mut() else {
return;
};
if !recursor.is_managed() {
return;
}
match change {
Topology::Create(path) => recursor.topology_create(path),
Topology::Remove(path) => recursor.topology_remove(path),
Topology::Rename { from, to } => recursor.topology_rename(from, to),
Topology::Ambiguous(path) => recursor.topology_ambiguous(path),
Topology::Rescan => recursor.rescan(),
}
}
fn handle_control(&mut self, message: ControlMessage, errors: &mut VecDeque<RuntimeError>) {
match message {
ControlMessage::Topology {
generation,
sequence,
changes,
} => {
self.processed_topology = self.processed_topology.max(sequence);
if generation != self.generation {
trace!("ignoring topology from obsolete filesystem watcher");
return;
}
for change in changes {
self.handle_topology(change);
}
}
ControlMessage::Error { watcher, error } => {
if let Err(error) = process_event(Err(error), watcher, &self.events) {
errors.push_back(error);
}
}
}
}
fn recursor_has_work(&self) -> bool {
self.recursor.as_ref().map_or(false, Recursor::has_work)
}
fn control_allowed(&self) -> bool {
self.pending_ready.map_or(true, |ready| {
ready
.topology_fence
.map_or(true, |fence| self.processed_topology < fence)
})
}
fn ordinary_ready(&self, message: &OrdinaryMessage) -> bool {
message.topology_sequence <= self.processed_topology && !self.recursor_has_work()
}
fn handle_ordinary(&mut self, message: OrdinaryMessage, errors: &mut VecDeque<RuntimeError>) {
self.retained_generations
.retain(|generation| *generation >= message.generation);
if message.generation != self.generation
&& !self.retained_generations.contains(&message.generation)
{
trace!("ignoring event from obsolete filesystem watcher");
return;
}
let public = self.recursor.as_ref().map_or(true, |recursor| {
!recursor.is_managed() || recursor.event_is_public(&message.event)
});
if !public {
return;
}
let watcher = self
.applied
.as_ref()
.map_or_else(Watcher::default, |applied| applied.watcher);
if let Err(error) = process_event(Ok(message.event), watcher, &self.events) {
errors.push_back(error);
}
}
}
fn deliver_errors_before_critical(
errors: &mpsc::Sender<RuntimeError>,
pending: &mut VecDeque<RuntimeError>,
) {
while let Some(error) = pending.pop_front() {
match errors.try_send(error) {
Ok(()) => {}
Err(
mpsc::error::TrySendError::Full(error) | mpsc::error::TrySendError::Closed(error),
) => {
tracing::error!(
?error,
"runtime error preceding critical filesystem failure"
);
for error in pending.drain(..) {
tracing::error!(
?error,
"runtime error preceding critical filesystem failure"
);
}
break;
}
}
}
}
pub async fn worker(
config: Arc<Config>,
errors: mpsc::Sender<RuntimeError>,
events: priority::Sender<Event, Priority>,
) -> Result<(), CriticalError> {
debug!("launching filesystem worker");
let cwd = std::env::current_dir().map_err(|err| CriticalError::IoError {
about: "obtaining current directory for filesystem watching",
err,
})?;
let (control_tx, mut control_rx) = mpsc::unbounded_channel();
let (ordinary_tx, mut ordinary_rx) = mpsc::channel(config.event_channel_size.max(1));
let callback_fence = Arc::new(CallbackFence::default());
let mut state = WorkerState::new(cwd, control_tx, ordinary_tx, callback_fence, events);
let mut config_watch = config.watch();
let mut runtime_errors = VecDeque::new();
let mut pending_ordinary = None;
let revision = config_watch.next().await;
state.apply_config(&config)?;
state.request_ready(revision);
loop {
if config_watch.pending() {
let revision = config_watch.next().await;
state.apply_config(&config)?;
state.request_ready(revision);
continue;
}
if state
.pending_ready
.as_ref()
.map_or(false, |ready| ready.topology_fence.is_none())
&& !state.recursor_has_work()
{
state.capture_ready_fence();
}
let mut progressed = false;
let control_allowed = state.control_allowed();
if control_allowed {
match control_rx.try_recv() {
Ok(message) => {
state.handle_control(message, &mut runtime_errors);
progressed = true;
}
Err(mpsc::error::TryRecvError::Empty | mpsc::error::TryRecvError::Disconnected) => {
}
}
}
if state.recursor_has_work() {
let step = state
.recursor
.as_mut()
.expect("recursor disappeared")
.step();
progressed = true;
runtime_errors.extend(step.errors);
if step.rebuild_backend {
if let Err(critical) = state.rebuild_backend(&mut runtime_errors) {
deliver_errors_before_critical(&errors, &mut runtime_errors);
return Err(critical);
}
}
}
if config_watch.pending() {
continue;
}
if let Some(ready) = state.pending_ready {
if ready
.topology_fence
.map_or(false, |fence| state.processed_topology >= fence)
&& !state.recursor_has_work()
{
state.pending_ready = None;
let _ = config.fs_ready.send(());
progressed = true;
}
}
if state.pending_ready.is_none() {
if let Some(message) = pending_ordinary.take() {
if state.ordinary_ready(&message) {
state.handle_ordinary(message, &mut runtime_errors);
progressed = true;
} else {
pending_ordinary = Some(message);
}
}
if pending_ordinary.is_none() {
match ordinary_rx.try_recv() {
Ok(message) => {
pending_ordinary = Some(message);
progressed = true;
}
Err(
mpsc::error::TryRecvError::Empty | mpsc::error::TryRecvError::Disconnected,
) => {}
}
}
}
if let Some(error) = runtime_errors.pop_front() {
match errors.try_send(error) {
Ok(()) => progressed = true,
Err(mpsc::error::TrySendError::Full(error)) => {
runtime_errors.push_front(error);
}
Err(mpsc::error::TrySendError::Closed(error)) => {
return Err(mpsc::error::SendError(error).into());
}
}
}
if progressed {
tokio::task::yield_now().await;
continue;
}
let control_allowed = state.control_allowed();
tokio::select! {
biased;
revision = config_watch.next() => {
state.apply_config(&config)?;
state.request_ready(revision);
}
Some(message) = control_rx.recv(), if control_allowed => {
state.handle_control(message, &mut runtime_errors);
}
Some(message) = ordinary_rx.recv(), if pending_ordinary.is_none() && state.pending_ready.is_none() => {
pending_ordinary = Some(message);
}
permit = errors.reserve(), if !runtime_errors.is_empty() => {
if let Ok(permit) = permit {
permit.send(runtime_errors.pop_front().expect("error queue emptied"));
} else {
let error = runtime_errors.pop_front().expect("error queue emptied");
return Err(mpsc::error::SendError(error).into());
}
}
}
}
}
fn topology_messages(event: ¬ify::Event) -> Vec<Topology> {
let mut changes = Vec::new();
if event.need_rescan() {
changes.push(Topology::Rescan);
}
match event.kind {
EventKind::Create(_) => changes.extend(event.paths.iter().cloned().map(Topology::Create)),
EventKind::Remove(_) => changes.extend(event.paths.iter().cloned().map(Topology::Remove)),
EventKind::Modify(ModifyKind::Name(RenameMode::Both)) if event.paths.len() >= 2 => {
changes.push(Topology::Rename {
from: event.paths[0].clone(),
to: event.paths[1].clone(),
});
changes.extend(event.paths.iter().skip(2).cloned().map(Topology::Ambiguous));
}
EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {
changes.extend(event.paths.iter().cloned().map(Topology::Remove));
}
EventKind::Modify(ModifyKind::Name(_)) | EventKind::Any | EventKind::Other => {
changes.extend(event.paths.iter().cloned().map(Topology::Ambiguous));
}
_ => {}
}
changes
}
fn notify_multi_path_errors(
kind: Watcher,
watched_path: WatchedPath,
mut err: notify::Error,
rm: bool,
) -> Vec<RuntimeError> {
let mut paths = take(&mut err.paths);
if paths.is_empty() {
paths.push(watched_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 error = 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: error }
} else {
FsWatcherError::PathAdd { path, err: error }
},
});
}
errs
}
fn process_event(
nev: Result<notify::Event, notify::Error>,
kind: Watcher,
n_events: &priority::Sender<Event, Priority>,
) -> 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 {
tags.push(Tag::Path {
file_type: metadata(&path)
.ok()
.map(|metadata| metadata.file_type().into()),
path: path.normalize(),
});
}
if let Some(pid) = nev.attrs.process_id() {
tags.push(Tag::Process(pid));
}
let mut event_metadata = HashMap::new();
if let Some(uid) = nev.attrs.info() {
event_metadata.insert("file-event-info".to_string(), vec![uid.to_string()]);
}
if let Some(src) = nev.attrs.source() {
event_metadata.insert("notify-backend".to_string(), vec![src.to_string()]);
}
let event = Event {
tags,
metadata: event_metadata,
};
trace!(?event, "processed notify event into watchexec event");
match n_events.try_send(event, Priority::Normal) {
Ok(()) => {}
Err(priority::TrySendError::Full(_)) => {
debug!(
"fs watcher event channel is full; dropping event \
(tune Config::event_channel_size if this happens often)"
);
}
Err(priority::TrySendError::Closed(event)) => {
return Err(RuntimeError::EventChannelSend {
ctx: "fs watcher",
err: priority::SendError(event),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
dispatch_callback_event, process_event, recursion_strategy, strategy_requires_recreation,
topology_messages, worker, CallbackFence, ControlMessage, OrdinaryMessage,
RecursionStrategy, Topology, Watcher, WorkerState,
};
use crate::{
error::{FsWatcherError, RuntimeError},
Config,
};
use async_priority_channel as priority;
use futures::FutureExt as _;
use notify::{
event::{Flag, ModifyKind, RenameMode},
EventKind,
};
use std::{
collections::VecDeque,
path::PathBuf,
sync::{atomic::Ordering, Arc},
};
use tokio::sync::mpsc;
use watchexec_events::Priority;
#[test]
fn backend_strategy_matches_notify_capabilities() {
assert_eq!(
Watcher::Poll(std::time::Duration::from_secs(1)).recursion_strategy(),
RecursionStrategy::PerDirectory
);
assert_eq!(
recursion_strategy(notify::WatcherKind::Inotify),
RecursionStrategy::PerDirectory
);
assert_eq!(
recursion_strategy(notify::WatcherKind::ReadDirectoryChangesWatcher),
RecursionStrategy::PerDirectory
);
assert_eq!(
recursion_strategy(notify::WatcherKind::PollWatcher),
RecursionStrategy::PerDirectory
);
assert_eq!(
recursion_strategy(notify::WatcherKind::Fsevent),
RecursionStrategy::RecursiveRoots
);
assert_eq!(
recursion_strategy(notify::WatcherKind::Kqueue),
RecursionStrategy::NotifyOwned
);
assert_eq!(
recursion_strategy(notify::WatcherKind::NullWatcher),
RecursionStrategy::NotifyOwned
);
let recommended = <notify::RecommendedWatcher as notify::Watcher>::kind();
let native_kind = Watcher::Native.backend_kind_for(recommended);
assert_eq!(Watcher::Native.backend_kind(), native_kind);
assert_eq!(
Watcher::Native.recursion_strategy(),
recursion_strategy(native_kind)
);
}
#[test]
fn recursive_root_strategy_recreates_for_root_changes_or_retry() {
assert!(!strategy_requires_recreation(
RecursionStrategy::RecursiveRoots,
false,
false
));
assert!(strategy_requires_recreation(
RecursionStrategy::RecursiveRoots,
false,
true
));
assert!(strategy_requires_recreation(
RecursionStrategy::RecursiveRoots,
true,
false
));
}
#[test]
fn native_backend_falls_back_from_kqueue_to_poll() {
assert_eq!(
Watcher::Native.backend_kind_for(notify::WatcherKind::Kqueue),
notify::WatcherKind::PollWatcher
);
}
#[test]
fn native_backend_preserves_other_recommendations() {
for kind in [
notify::WatcherKind::Inotify,
notify::WatcherKind::Fsevent,
notify::WatcherKind::PollWatcher,
notify::WatcherKind::ReadDirectoryChangesWatcher,
notify::WatcherKind::NullWatcher,
] {
assert_eq!(Watcher::Native.backend_kind_for(kind), kind);
}
}
#[test]
fn explicit_poll_ignores_native_recommendation() {
let poll = Watcher::Poll(std::time::Duration::from_secs(1));
for recommended in [
notify::WatcherKind::Inotify,
notify::WatcherKind::Fsevent,
notify::WatcherKind::Kqueue,
notify::WatcherKind::ReadDirectoryChangesWatcher,
notify::WatcherKind::NullWatcher,
] {
assert_eq!(
poll.backend_kind_for(recommended),
notify::WatcherKind::PollWatcher
);
}
}
#[test]
fn process_event_drops_when_channel_full() {
let (ev_s, _ev_r) = priority::bounded::<watchexec_events::Event, Priority>(1);
let nev = Ok(notify::Event::new(EventKind::Any));
assert!(process_event(nev, super::Watcher::default(), &ev_s).is_ok());
let nev = Ok(notify::Event::new(EventKind::Any));
let res = process_event(nev, super::Watcher::default(), &ev_s);
assert!(
res.is_ok(),
"full channel should drop the event silently, not return a RuntimeError (got {res:?})",
);
}
#[test]
fn process_event_propagates_when_channel_closed() {
let (ev_s, ev_r) = priority::bounded::<watchexec_events::Event, Priority>(1);
drop(ev_r);
let nev = Ok(notify::Event::new(EventKind::Any));
let res = process_event(nev, super::Watcher::default(), &ev_s);
assert!(
matches!(res, Err(RuntimeError::EventChannelSend { .. })),
"closed channel should propagate as EventChannelSend, got {res:?}",
);
}
#[test]
fn topology_lane_remains_lossless_when_ordinary_lane_is_full() {
let (control, mut control_rx) = mpsc::unbounded_channel();
let (ordinary, mut ordinary_rx) = mpsc::channel(1);
let fence = CallbackFence::default();
fence.generation.store(1, Ordering::Release);
dispatch_callback_event(
1,
Watcher::Poll(std::time::Duration::from_secs(1)),
&control,
&ordinary,
&fence,
Ok(notify::Event::new(EventKind::Access(
notify::event::AccessKind::Any,
))),
);
dispatch_callback_event(
1,
Watcher::Poll(std::time::Duration::from_secs(1)),
&control,
&ordinary,
&fence,
Ok(
notify::Event::new(EventKind::Create(notify::event::CreateKind::Folder))
.add_path("/root/new".into()),
),
);
assert!(ordinary_rx.try_recv().is_ok());
assert!(matches!(
ordinary_rx.try_recv(),
Err(mpsc::error::TryRecvError::Empty)
));
assert!(matches!(
control_rx.try_recv(),
Ok(ControlMessage::Topology {
generation: 1,
sequence: 1,
changes,
}) if changes == vec![Topology::Create(PathBuf::from("/root/new"))]
));
}
#[test]
fn callback_rejects_obsolete_generation_before_enqueue() {
let (control, mut control_rx) = mpsc::unbounded_channel();
let (ordinary, mut ordinary_rx) = mpsc::channel(1);
let fence = CallbackFence::default();
fence.generation.store(2, Ordering::Release);
dispatch_callback_event(
1,
Watcher::Native,
&control,
&ordinary,
&fence,
Ok(
notify::Event::new(EventKind::Create(notify::event::CreateKind::Folder))
.add_path("/obsolete".into()),
),
);
assert!(matches!(
control_rx.try_recv(),
Err(mpsc::error::TryRecvError::Empty)
));
assert!(matches!(
ordinary_rx.try_recv(),
Err(mpsc::error::TryRecvError::Empty)
));
}
#[test]
fn accepted_callback_error_survives_reconfiguration_race() {
let (control, mut control_rx) = mpsc::unbounded_channel();
let (ordinary, _ordinary_rx) = mpsc::channel(1);
let (events, _event_rx) = priority::bounded(1);
let callback_fence = Arc::new(CallbackFence::default());
callback_fence.generation.store(1, Ordering::Release);
let original = Watcher::Poll(std::time::Duration::from_secs(7));
dispatch_callback_event(
1,
original,
&control,
&ordinary,
&callback_fence,
Err(notify::Error::generic("accepted callback error")),
);
let mut state = WorkerState::new(
PathBuf::from("/work"),
control,
ordinary,
callback_fence,
events,
);
state.generation = 2;
let mut errors = VecDeque::new();
state.handle_control(control_rx.try_recv().unwrap(), &mut errors);
assert!(matches!(
errors.pop_front(),
Some(RuntimeError::FsWatcher {
kind,
err: FsWatcherError::Event(_),
}) if kind == original
));
}
#[test]
fn readiness_captures_topology_only_at_first_quiescence() {
let (control, _control_rx) = mpsc::unbounded_channel();
let (ordinary, _ordinary_rx) = mpsc::channel(1);
let (events, _event_rx) = priority::bounded(1);
let callback_fence = Arc::new(CallbackFence::default());
callback_fence.topology_enqueued.store(3, Ordering::Release);
let mut state = WorkerState::new(
PathBuf::from("/work"),
control,
ordinary,
callback_fence.clone(),
events,
);
state.request_ready(1);
assert_eq!(state.pending_ready.unwrap().topology_fence, None);
callback_fence.topology_enqueued.store(4, Ordering::Release);
state.capture_ready_fence();
assert_eq!(state.pending_ready.unwrap().topology_fence, Some(4));
}
#[test]
fn readiness_fence_waits_for_callback_holding_enqueue_gate() {
let (control, _control_rx) = mpsc::unbounded_channel();
let (ordinary, _ordinary_rx) = mpsc::channel(1);
let (events, _event_rx) = priority::bounded(1);
let callback_fence = Arc::new(CallbackFence::default());
let mut state = WorkerState::new(
PathBuf::from("/work"),
control.clone(),
ordinary,
callback_fence.clone(),
events,
);
state.request_ready(1);
let (acquired_tx, acquired_rx) = std::sync::mpsc::channel();
let (release_tx, release_rx) = std::sync::mpsc::channel();
std::thread::scope(|scope| {
let gate = callback_fence.clone();
scope.spawn(move || {
let _serial = gate.serial.lock().unwrap();
acquired_tx.send(()).unwrap();
release_rx.recv().unwrap();
let sequence = gate.topology_enqueued.fetch_add(1, Ordering::AcqRel) + 1;
control
.send(ControlMessage::Topology {
generation: 0,
sequence,
changes: vec![Topology::Create("/during-reconcile".into())],
})
.unwrap();
});
acquired_rx.recv().unwrap();
release_tx.send(()).unwrap();
state.capture_ready_fence();
});
assert_eq!(state.pending_ready.unwrap().topology_fence, Some(1));
}
#[test]
fn paired_rename_preserves_from_to_order() {
let event = notify::Event::new(EventKind::Modify(ModifyKind::Name(RenameMode::Both)))
.add_path(PathBuf::from("from"))
.add_path(PathBuf::from("to"));
assert_eq!(
topology_messages(&event),
vec![Topology::Rename {
from: PathBuf::from("from"),
to: PathBuf::from("to"),
}]
);
}
#[test]
fn obsolete_generation_does_not_emit_ordinary_event() {
let (control, _control_rx) = mpsc::unbounded_channel();
let (ordinary, _ordinary_rx) = mpsc::channel(4);
let (events, event_rx) = priority::bounded(4);
let mut state = WorkerState::new(
PathBuf::from("/work"),
control,
ordinary,
Arc::new(CallbackFence::default()),
events,
);
state.generation = 2;
let mut errors = VecDeque::new();
state.handle_ordinary(
OrdinaryMessage {
generation: 1,
topology_sequence: 0,
event: notify::Event::new(EventKind::Any),
},
&mut errors,
);
assert!(errors.is_empty());
assert!(matches!(
event_rx.try_recv(),
Err(priority::TryRecvError::Empty)
));
}
#[test]
fn internally_rebuilt_generation_emits_accepted_ordinary_event() {
let (control, _control_rx) = mpsc::unbounded_channel();
let (ordinary, _ordinary_rx) = mpsc::channel(4);
let (events, event_rx) = priority::bounded(4);
let mut state = WorkerState::new(
PathBuf::from("/work"),
control,
ordinary,
Arc::new(CallbackFence::default()),
events,
);
state.generation = 2;
state.retained_generations.insert(1);
let mut errors = VecDeque::new();
state.handle_ordinary(
OrdinaryMessage {
generation: 1,
topology_sequence: 1,
event: notify::Event::new(EventKind::Any).add_path("/retargeted".into()),
},
&mut errors,
);
assert!(errors.is_empty());
assert!(event_rx.try_recv().is_ok());
}
#[test]
fn current_generation_closed_event_error_remains_reliable() {
let (control, _control_rx) = mpsc::unbounded_channel();
let (ordinary, _ordinary_rx) = mpsc::channel(4);
let (events, event_rx) = priority::bounded(4);
drop(event_rx);
let mut state = WorkerState::new(
PathBuf::from("/work"),
control,
ordinary,
Arc::new(CallbackFence::default()),
events,
);
state.generation = 1;
let mut errors = VecDeque::new();
state.handle_control(
ControlMessage::Topology {
generation: 1,
sequence: 1,
changes: vec![Topology::Rescan],
},
&mut errors,
);
state.handle_ordinary(
OrdinaryMessage {
generation: 1,
topology_sequence: 1,
event: notify::Event::new(EventKind::Any),
},
&mut errors,
);
assert!(matches!(
errors.pop_front(),
Some(RuntimeError::EventChannelSend { .. })
));
}
#[test]
fn worker_skips_revision_zero_ready_but_signals_same_value_revision() {
let runtime = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
runtime.block_on(async {
let config = Arc::new(Config::default());
let mut ready = config.fs_ready();
let (events, _event_rx) = priority::bounded(4);
let (errors, _error_rx) = mpsc::channel(4);
let task = tokio::spawn(worker(config.clone(), errors, events));
for _ in 0..4 {
tokio::task::yield_now().await;
}
assert!(ready.changed().now_or_never().is_none());
config.pathset(Vec::<super::WatchedPath>::new());
ready.changed().await.unwrap();
task.abort();
});
}
#[test]
fn split_rename_and_rescan_produce_reliable_topology() {
let from = notify::Event::new(EventKind::Modify(ModifyKind::Name(RenameMode::From)))
.add_path(PathBuf::from("old"));
let to = notify::Event::new(EventKind::Modify(ModifyKind::Name(RenameMode::To)))
.add_path(PathBuf::from("new"));
let rescan = notify::Event::new(EventKind::Any).set_flag(Flag::Rescan);
assert_eq!(
topology_messages(&from),
vec![Topology::Remove(PathBuf::from("old"))]
);
assert_eq!(
topology_messages(&to),
vec![Topology::Ambiguous(PathBuf::from("new"))]
);
assert_eq!(topology_messages(&rescan), vec![Topology::Rescan]);
}
}