#![allow(dead_code)]
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result};
use notify_debouncer_mini::notify::RecursiveMode;
use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer};
pub type ChangeHandler = Arc<dyn Fn(&[PathBuf]) + Send + Sync>;
pub const DEFAULT_DEBOUNCE: Duration = Duration::from_millis(500);
pub struct WatchHandle {
_debouncer: Debouncer<notify_debouncer_mini::notify::RecommendedWatcher>,
}
pub fn watch(
dir: &Path,
on_change: Option<ChangeHandler>,
debounce: Option<Duration>,
) -> Result<WatchHandle> {
if !dir.is_dir() {
anyhow::bail!("--watch path is not a directory: {}", dir.display());
}
let debounce = debounce.unwrap_or(DEFAULT_DEBOUNCE);
let dir_for_log = dir.to_path_buf();
let on_change = on_change.unwrap_or_else(|| {
Arc::new(|_| {
})
});
let mut debouncer = new_debouncer(debounce, move |result: DebounceEventResult| match result {
Ok(events) => {
let paths: Vec<PathBuf> = events.into_iter().map(|e| e.path).collect();
tracing::info!(
root = %dir_for_log.display(),
changed = paths.len(),
"watch: file change debounced"
);
on_change(&paths);
}
Err(e) => {
tracing::warn!(error = %e, "watch: error from notify");
}
})
.context("failed to construct file-system debouncer")?;
debouncer
.watcher()
.watch(dir, RecursiveMode::Recursive)
.with_context(|| format!("failed to watch {}", dir.display()))?;
tracing::info!(root = %dir.display(), debounce_ms = debounce.as_millis() as u64, "watch: active");
Ok(WatchHandle {
_debouncer: debouncer,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn watch_rejects_non_directory() {
let result = watch(Path::new("/this/does/not/exist"), None, None);
assert!(result.is_err());
}
#[test]
fn watch_starts_and_drops_clean() {
let dir = tempfile::tempdir().unwrap();
let _handle = watch(dir.path(), None, Some(Duration::from_millis(100))).unwrap();
}
#[test]
fn callback_fires_on_file_change() {
use std::thread::sleep;
let dir = tempfile::tempdir().unwrap();
let counter = Arc::new(AtomicUsize::new(0));
let counter_for_cb = counter.clone();
let cb: ChangeHandler = Arc::new(move |_paths: &[PathBuf]| {
counter_for_cb.fetch_add(1, Ordering::SeqCst);
});
let _handle = watch(dir.path(), Some(cb), Some(Duration::from_millis(100))).unwrap();
sleep(Duration::from_millis(50)); std::fs::write(dir.path().join("a.txt"), "hi").unwrap();
sleep(Duration::from_millis(400)); assert!(
counter.load(Ordering::SeqCst) >= 1,
"expected callback to fire at least once after file write"
);
}
}