use super::{Cover, Happened, Kind};
use crate::hashing::{HashMap, HashSet};
use rustix::fd::OwnedFd;
use rustix::fs::inotify::{self, CreateFlags, ReadFlags, WatchFlags};
use rustix::io::Errno;
use std::mem::MaybeUninit;
use std::path::{Path, PathBuf};
pub(super) struct Backend {
root: PathBuf,
queue: OwnedFd,
watches: HashMap<i32, Vec<PathBuf>>,
added: HashSet<PathBuf>,
}
const EVENTS: WatchFlags = WatchFlags::CREATE
.union(WatchFlags::DELETE)
.union(WatchFlags::MODIFY)
.union(WatchFlags::CLOSE_WRITE)
.union(WatchFlags::MOVED_FROM)
.union(WatchFlags::MOVED_TO)
.union(WatchFlags::ATTRIB)
.union(WatchFlags::DELETE_SELF)
.union(WatchFlags::MOVE_SELF)
.union(WatchFlags::EXCL_UNLINK)
.union(WatchFlags::ONLYDIR);
impl Backend {
pub(super) const FOLLOWS_DIRECTORY: bool = true;
pub(super) const FOLLOWS_LINKS: bool = true;
pub(super) const ECHOES_ENTRIES: bool = false;
pub(super) fn new(root: &Path) -> Result<Self, String> {
if let Some(system) = remote(root) {
return Err(format!(
"the sources are on {system}, which does not report changes made elsewhere"
));
}
let queue = inotify::init(CreateFlags::NONBLOCK | CreateFlags::CLOEXEC)
.map_err(|error| format!("inotify: {error}"))?;
Ok(Self {
root: root.to_path_buf(),
queue,
watches: HashMap::default(),
added: HashSet::default(),
})
}
pub(super) fn discover(&mut self, directory: &Path, recursive: bool) -> Result<(), String> {
let mut pending = vec![directory.to_path_buf()];
while let Some(directory) = pending.pop() {
match self.add(&directory) {
Ok(_) => {}
Err(error) => {
limit(error)?;
continue;
}
}
if !recursive {
continue;
}
let Ok(entries) = std::fs::read_dir(&directory) else {
continue;
};
for entry in entries.flatten() {
if entry.file_type().is_ok_and(|kind| kind.is_dir()) {
pending.push(entry.path());
}
}
}
Ok(())
}
pub(super) fn cover(&mut self, directory: &Path) -> Result<Cover, String> {
let outside = crate::refresh::outside(&super::key(&self.root, directory));
if outside && !self.added.contains(directory) && remote(directory).is_some() {
return Ok(Cover::Unwatched);
}
let mut at = directory;
let mut above = false;
loop {
match self.add(at) {
Ok(anew) if above => return Ok(Cover::Above(anew)),
Ok(anew) => return Ok(Cover::Watched(anew)),
Err(Errno::NOENT | Errno::NOTDIR) => match at.parent() {
Some(parent) if !parent.as_os_str().is_empty() => {
at = parent;
above = true;
}
_ => return Ok(Cover::Unwatched),
},
Err(error) => {
limit(error)?;
return Ok(Cover::Unwatched);
}
}
}
}
fn add(&mut self, directory: &Path) -> Result<bool, Errno> {
if self.added.contains(directory) {
return Ok(false);
}
let watch = inotify::add_watch(&self.queue, directory, EVENTS)?;
self.watches
.entry(watch)
.or_default()
.push(directory.to_path_buf());
self.added.insert(directory.to_path_buf());
Ok(true)
}
pub(super) fn tidy(&mut self) {}
pub(super) fn events(&mut self) -> Vec<Happened> {
let mut happened = Vec::new();
let mut buffer = [MaybeUninit::<u8>::uninit(); 16 * 1024];
let mut reader = inotify::Reader::new(&self.queue, &mut buffer);
loop {
let event = match reader.next() {
Ok(event) => event,
Err(Errno::AGAIN) => return happened,
Err(Errno::INTR) => continue,
Err(_) => {
happened.push(Happened::Lost);
return happened;
}
};
let flags = event.events();
if flags.contains(ReadFlags::QUEUE_OVERFLOW) {
happened.push(Happened::Lost);
return happened;
}
if flags.intersects(ReadFlags::IGNORED | ReadFlags::MOVE_SELF | ReadFlags::UNMOUNT) {
if self.watches.contains_key(&event.wd()) {
happened.push(Happened::Lost);
return happened;
}
continue;
}
let Some(spellings) = self.watches.get(&event.wd()) else {
continue;
};
let Some(name) = event.file_name() else {
happened.push(Happened::Directory);
continue;
};
let kind = if flags.intersects(ReadFlags::CREATE | ReadFlags::MOVED_TO) {
Kind::Added
} else if flags.intersects(ReadFlags::DELETE | ReadFlags::MOVED_FROM) {
Kind::Removed
} else {
Kind::Changed
};
let name =
<std::ffi::OsStr as std::os::unix::ffi::OsStrExt>::from_bytes(name.to_bytes());
for spelling in spellings {
happened.push(Happened::Entry {
path: spelling.join(name),
kind,
directory: Some(flags.contains(ReadFlags::ISDIR)),
});
}
}
}
}
fn remote(directory: &Path) -> Option<&'static str> {
const REMOTE: [(u32, &str); 14] = [
(0x6969, "NFS"),
(0x517b, "SMB"),
(0xff53_4d42, "CIFS"),
(0xfe53_4d42, "SMB"),
(0x0102_1997, "9P"),
(0x6573_5546, "a FUSE file system"),
(0x00c3_6400, "Ceph"),
(0x5346_414f, "AFS"),
(0x6b41_4653, "AFS"),
(0x7375_7245, "Coda"),
(0x786f_4256, "a VirtualBox shared folder"),
(0x0bd0_0bd0, "Lustre"),
(0x4750_4653, "GPFS"),
(0x0116_1970, "GFS2"),
];
let system = rustix::fs::statfs(directory).ok()?.f_type as u32;
REMOTE
.iter()
.find(|(magic, _)| *magic == system)
.map(|(_, name)| *name)
}
fn limit(error: Errno) -> Result<(), String> {
match error {
Errno::NOSPC => {
Err("the inotify watch limit is reached; raise fs.inotify.max_user_watches".into())
}
Errno::NOMEM => Err("inotify is out of memory".into()),
_ => Ok(()),
}
}