filesentry 0.1.0

robust (recursive) file watcher
Documentation
use core::panic;
use std::ffi::{OsStr, c_int};
use std::mem::{align_of, size_of};
use std::os::fd::AsRawFd;
use std::os::unix::ffi::OsStrExt;
use std::{io, slice};

use bitflags::bitflags;
use mio::unix::SourceFd;
use mio::{Events, Interest, Poll};
use rustix::fd::{AsFd, OwnedFd};
use rustix::fs::inotify::{self, CreateFlags, WatchFlags};
use rustix::io::Errno;

const INOTIFY: mio::Token = mio::Token(0);
pub const MESSAGE: mio::Token = mio::Token(1);

#[derive(Debug, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub(super) struct Watch(c_int);

impl Watch {
    pub fn is_invalid(&self) -> bool {
        self.0 < 0
    }
}

#[derive(Debug)]
pub(super) struct Inotify {
    fd: OwnedFd,
}

impl Inotify {
    pub(super) fn new() -> io::Result<Inotify> {
        let fd = inotify::init(CreateFlags::CLOEXEC | CreateFlags::NONBLOCK)?;
        Ok(Inotify { fd })
    }

    pub(super) fn add_directory_watch(&self, path: impl rustix::path::Arg) -> io::Result<Watch> {
        let watch = inotify::add_watch(
            self.as_fd(),
            path,
            WatchFlags::ATTRIB
                | WatchFlags::CREATE
                | WatchFlags::DELETE
                | WatchFlags::DELETE_SELF
                | WatchFlags::MODIFY
                | WatchFlags::MOVE_SELF
                | WatchFlags::MOVE
                | WatchFlags::DONT_FOLLOW
                | WatchFlags::EXCL_UNLINK
                | WatchFlags::ONLYDIR,
        )
        .map_err(|err| {
            if err == Errno::NOSPC {
                io::Error::new(io::ErrorKind::ResourceBusy, "exhaused inotify max_user_watches, try increasing the setting or adding stricter glob filter")
            } else {
                err.into()
            }
        })?;
        Ok(Watch(watch))
    }

    // pub(super) fn remove_watch(&self, watch: Watch) -> io::Result<()> {
    //     inotify::remove_watch(self.as_fd(), watch.0)?;
    //     Ok(())
    // }

    pub(super) fn event_loop(
        &self,
        poll: &mut Poll,
        mut handle_event: impl FnMut(&OsStr, &Event /* , SystemTime */),
        mut event_stream_done: impl FnMut(),
        mut handle_message: impl FnMut() -> bool,
        #[cfg(test)] slow: bool,
    ) -> io::Result<()> {
        let mut buf = vec![0; BUFFERSIZE].into_boxed_slice();
        let raw_fd = self.fd.as_raw_fd();
        let mut fd = SourceFd(&raw_fd);
        poll.registry()
            .register(&mut fd, INOTIFY, Interest::READABLE)?;
        let mut events = Events::with_capacity(16);
        loop {
            // Wait for something to happen.
            match poll.poll(&mut events, None) {
                Err(ref e) if matches!(e.kind(), std::io::ErrorKind::Interrupted) => {
                    // System call was interrupted, we will retry
                    // TODO: Not covered by tests (to reproduce likely need to setup signal handlers)
                }
                Err(e) => return Err(e),
                Ok(()) => {}
            }

            // let time = SystemTime::now();
            let mut message = false;
            let mut inotify = false;
            for event in &events {
                match event.token() {
                    INOTIFY => inotify = true,
                    MESSAGE => message = true,
                    _ => unreachable!(),
                }
            }
            events.clear();
            if inotify {
                // to reliably reproduce queue overflow we need to read events slowly
                // particularly this need to be done here where we read the FD and not in
                // the event handler callback, I think that is because our buffer size
                // is quite big
                #[cfg(test)]
                if slow {
                    use crate::tests::READ_DELAY;
                    std::thread::sleep(*READ_DELAY);
                }
                loop {
                    let buf_bytes = unsafe {
                        slice::from_raw_parts_mut(
                            buf.as_mut_ptr().cast::<u8>(),
                            buf.len() * size_of::<u32>(),
                        )
                    };
                    let size = match rustix::io::read(&self.fd, buf_bytes) {
                        Ok(0) | Err(Errno::AGAIN) => break,
                        Ok(read) => read,
                        Err(err) => return Err(err.into()),
                    };
                    let mut buf = &buf[..size.div_ceil(size_of::<u32>())];
                    while !buf.is_empty() {
                        let (event, name, rem) = Event::get(buf);
                        handle_event(name, event /* , time */);
                        buf = rem;
                    }
                }
                event_stream_done()
            }
            if message && handle_message() {
                break;
            }
        }
        Ok(())
    }
}

impl AsFd for Inotify {
    fn as_fd(&self) -> rustix::fd::BorrowedFd<'_> {
        self.fd.as_fd()
    }
}

#[repr(C)]
#[derive(Debug)]
pub(super) struct Event {
    pub wd: Watch,
    pub flags: EventFlags,
    pub cookie: u32,
    len: u32,
}

const ALIGNED_SIZED: usize = {
    if align_of::<u32>() != align_of::<Event>() {
        panic!("invalid align");
    }
    if size_of::<Event>() % align_of::<Event>() != 0 {
        panic!("invalid size");
    }
    size_of::<Event>() / align_of::<Event>()
};

const ALIGNED_MAX_EVENT_SIZE: usize = (size_of::<Event>() + 256) / align_of::<Event>();
const BUFFERSIZE: usize = 16 * 1024 * ALIGNED_MAX_EVENT_SIZE;

impl Event {
    fn get(buf: &[u32]) -> (&Event, &OsStr, &[u32]) {
        let (head, buf) = buf.split_at(ALIGNED_SIZED);
        let event: *const Event = head.as_ptr().cast();
        let event: &Event = unsafe { &*event };
        let len = event.len as usize;
        if len == 0 {
            (event, OsStr::new(""), buf)
        } else {
            let (name, tail) = buf.split_at(len.div_ceil(align_of::<Event>()));
            let name = unsafe { slice::from_raw_parts(name.as_ptr() as *const u8, len) };
            let name = std::ffi::CStr::from_bytes_until_nul(name).unwrap();
            let name = OsStr::from_bytes(name.to_bytes());
            (event, name, tail)
        }
    }
}

bitflags! {
    #[derive(Debug)]
    pub struct EventFlags: u32 {
        const ATTRIB = WatchFlags::ATTRIB.bits();
        const CREATE =  WatchFlags::CREATE.bits();
        const DELETE =  WatchFlags::DELETE.bits();
        const DELETE_SELF =  WatchFlags::DELETE_SELF.bits();
        const MODIFY =  WatchFlags::MODIFY.bits();
        const MOVE_SELF =  WatchFlags::MOVE_SELF.bits();
        const MOVE_FROM =  WatchFlags::MOVED_FROM.bits();
        const MOVE_TO =  WatchFlags::MOVED_TO.bits();
        const IGNORED =  0x00008000;
        const Q_OVERFLOW =  0x00004000;
        const UNMOUNT =  0x00002000;
    }
}