use crate::error::{IncludeError, Result};
use crate::file::LoaderFile;
use notify::{Config as NotifyConfig, RecursiveMode, Watcher};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, mpsc};
use std::time::Duration;
#[derive(Debug)]
pub struct FileWatcher {
stop: Arc<AtomicBool>,
thread: Option<std::thread::JoinHandle<()>>,
}
impl LoaderFile {
pub fn watch<F>(&self, on_change: F) -> Result<FileWatcher>
where
F: Fn() + Send + Sync + 'static,
{
let (tx, rx) = mpsc::channel();
let mut watcher =
notify::RecommendedWatcher::new(tx, NotifyConfig::default()).map_err(|error| {
IncludeError::Watch {
source: Box::new(error),
}
})?;
let watched = match self.path().parent() {
Some(parent) if !parent.as_os_str().is_empty() => parent.to_path_buf(),
_ => self.path().to_path_buf(),
};
watcher
.watch(&watched, RecursiveMode::NonRecursive)
.map_err(|error| IncludeError::Watch {
source: Box::new(error),
})?;
let stop = Arc::new(AtomicBool::new(false));
let stop_flag = Arc::clone(&stop);
let file = self.clone();
let relevant_paths = relevant_paths(self.path());
let thread = std::thread::Builder::new()
.name(format!("cordis-watch-{}", self.path().display()))
.spawn(move || {
let _watcher = watcher; let mut pending = false;
loop {
if stop_flag.load(Ordering::Relaxed) {
break;
}
match rx.recv_timeout(Duration::from_millis(100)) {
Ok(Ok(event)) => {
let relevant = event
.paths
.iter()
.any(|event_path| relevant_paths.contains(event_path));
if relevant {
pending = true;
}
}
Ok(Err(_)) => {}
Err(mpsc::RecvTimeoutError::Timeout) => {
if pending {
pending = false;
if !file.is_suspended() {
on_change();
}
}
}
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
}
})?;
Ok(FileWatcher {
stop,
thread: Some(thread),
})
}
}
impl Drop for FileWatcher {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
fn relevant_paths(file: &std::path::Path) -> Vec<std::path::PathBuf> {
let mut candidates = vec![file.to_path_buf()];
if let Ok(canonical) = std::fs::canonicalize(file) {
candidates.push(canonical);
} else if let (Some(name), Some(parent)) = (file.file_name(), file.parent()) {
if let Ok(canonical_parent) = std::fs::canonicalize(parent) {
candidates.push(canonical_parent.join(name));
}
}
candidates
}