use std::path::{Path, PathBuf};
use std::sync::mpsc::{Receiver, Sender, channel};
use std::time::{Duration, Instant};
use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use crate::server::Event;
const DEBOUNCE: Duration = Duration::from_millis(80);
pub fn watch_file(path: &Path, events_tx: Sender<Event>) -> notify::Result<RecommendedWatcher> {
let target = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
let parent = target
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let (raw_tx, raw_rx) = channel::<notify::Result<notify::Event>>();
let mut watcher = notify::recommended_watcher(move |res| {
let _ = raw_tx.send(res);
})?;
watcher.watch(&parent, RecursiveMode::NonRecursive)?;
std::thread::spawn(move || debounce_loop(raw_rx, events_tx, target));
Ok(watcher)
}
fn debounce_loop(
raw_rx: Receiver<notify::Result<notify::Event>>,
events_tx: Sender<Event>,
target: PathBuf,
) {
loop {
let first = match raw_rx.recv() {
Ok(event) => event,
Err(_) => return,
};
let mut relevant = event_touches(&first, &target);
let deadline = Instant::now() + DEBOUNCE;
loop {
let now = Instant::now();
if now >= deadline {
break;
}
match raw_rx.recv_timeout(deadline - now) {
Ok(event) => relevant |= event_touches(&event, &target),
Err(_) => break,
}
}
if relevant && events_tx.send(Event::Reload).is_err() {
return;
}
}
}
fn event_touches(event: ¬ify::Result<notify::Event>, target: &Path) -> bool {
let Ok(event) = event else {
return false;
};
if matches!(event.kind, EventKind::Access(_)) {
return false;
}
event
.paths
.iter()
.any(|p| p == target || p.canonicalize().map(|c| c == target).unwrap_or(false))
}
#[cfg(test)]
mod tests {
use std::fs;
use std::sync::mpsc::{RecvTimeoutError, channel};
use super::*;
use crate::testutil::TestDir;
const EXPECT: Duration = Duration::from_secs(5);
#[test]
fn dropping_the_watcher_ends_the_watch() {
let dir = TestDir::new("watch-drop");
let file = dir.join("a.md");
fs::write(&file, "# A\n").unwrap();
let (events_tx, events_rx) = channel();
let watcher = watch_file(&file, events_tx).expect("watch the file");
fs::write(&file, "# A changed\n").unwrap();
assert_eq!(events_rx.recv_timeout(EXPECT), Ok(Event::Reload));
drop(watcher);
fs::write(&file, "# A changed again\n").unwrap();
loop {
match events_rx.recv_timeout(EXPECT) {
Ok(Event::Reload) => continue,
Err(RecvTimeoutError::Disconnected) => break,
Err(RecvTimeoutError::Timeout) => panic!("the watch outlived its watcher"),
Ok(other) => panic!("a file watch should only reload, but sent {other:?}"),
}
}
}
}