use std::collections::HashMap;
use std::ffi::OsStr;
use std::os::fd::{AsFd, OwnedFd};
use std::path::{Path, PathBuf};
use inotify::{EventMask, Inotify, WatchDescriptor, WatchMask};
const CHANGES: WatchMask = WatchMask::CLOSE_WRITE
.union(WatchMask::CREATE)
.union(WatchMask::DELETE)
.union(WatchMask::MOVED_FROM)
.union(WatchMask::MOVED_TO);
pub(crate) struct Watcher {
inotify: Inotify,
config: PathBuf,
dirs: HashMap<WatchDescriptor, PathBuf>,
root: WatchDescriptor,
}
impl Watcher {
pub(crate) fn new(config: &Path) -> std::io::Result<Watcher> {
let inotify = Inotify::init()?;
let root = inotify.watches().add(config, CHANGES)?;
let mut watcher = Watcher {
inotify,
config: config.to_owned(),
dirs: HashMap::new(),
root,
};
watcher.watch_tree(&config.join("src"));
Ok(watcher)
}
pub(crate) fn fd(&self) -> std::io::Result<OwnedFd> {
self.inotify.as_fd().try_clone_to_owned()
}
pub(crate) fn changed(&mut self) -> bool {
let mut buffer = [0_u8; 4096];
let mut changed = false;
let mut new_dirs = Vec::new();
let mut gone = Vec::new();
loop {
let events = match self.inotify.read_events(&mut buffer) {
Ok(events) => events,
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => break,
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
Err(error) => {
crate::log(&format!("can't read file changes: {error}"));
break;
}
};
let mut any = false;
for event in events {
any = true;
if event.mask.contains(EventMask::Q_OVERFLOW) {
changed = true;
new_dirs.push(self.config.join("src"));
continue;
}
if event.mask.contains(EventMask::IGNORED) {
self.dirs.remove(&event.wd);
continue;
}
let name = event.name.unwrap_or_default();
let is_dir = event.mask.contains(EventMask::ISDIR);
let arrived = event
.mask
.intersects(EventMask::CREATE | EventMask::MOVED_TO);
let path = if event.wd == self.root {
if name == OsStr::new("Cargo.toml") {
changed = true;
continue;
}
if name != OsStr::new("src") || !is_dir {
continue;
}
changed = true;
self.config.join("src")
} else if let Some(dir) = self.dirs.get(&event.wd) {
changed |= relevant(name);
dir.join(name)
} else {
continue;
};
if arrived && (is_dir || path.is_symlink()) {
new_dirs.push(path);
} else if is_dir && event.mask.contains(EventMask::MOVED_FROM) {
gone.push(path);
}
}
if !any {
break;
}
}
for path in gone {
let watches: Vec<WatchDescriptor> = (self.dirs.iter())
.filter(|(_, dir)| dir.starts_with(&path))
.map(|(watch, _)| watch.clone())
.collect();
for watch in watches {
self.dirs.remove(&watch);
let _ = self.inotify.watches().remove(watch);
}
}
for dir in new_dirs {
self.watch_tree(&dir);
}
changed
}
fn watch_tree(&mut self, dir: &Path) {
match self.inotify.watches().add(dir, CHANGES) {
Ok(watch) if self.dirs.contains_key(&watch) => return,
Ok(watch) => {
self.dirs.insert(watch, dir.to_owned());
}
Err(error) => {
if error.kind() != std::io::ErrorKind::NotFound {
crate::log(&format!("can't watch {}: {error}", dir.display()));
}
return;
}
}
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
if entry
.file_type()
.is_ok_and(|kind| kind.is_dir() || kind.is_symlink())
{
self.watch_tree(&entry.path());
}
}
}
}
pub(crate) fn relevant(name: &OsStr) -> bool {
let name = name.to_string_lossy();
let auto_save = name.len() > 1 && name.starts_with('#') && name.ends_with('#');
!(name.starts_with('.') || name.ends_with('~') || name == "4913" || auto_save)
}