use std::path::Path;
use std::time::{Duration, Instant};
pub const POLL_PAUSE: Duration = Duration::from_millis(50);
#[cfg(test)]
thread_local! {
static LIVE_ON_THIS_THREAD: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
#[cfg(test)]
pub fn live_watches_on_this_thread() -> usize {
return LIVE_ON_THIS_THREAD.with(std::cell::Cell::get);
}
pub struct Watch {
#[cfg(target_os = "linux")]
descriptor: i32,
}
#[cfg(target_os = "linux")]
fn parse_events(buffer: &[u8]) -> Vec<(u32, &[u8])> {
let header: usize = std::mem::size_of::<libc::inotify_event>();
let mut events: Vec<(u32, &[u8])> = Vec::new();
let mut at: usize = 0;
while at + header <= buffer.len() {
let field = |offset: usize| -> u32 {
let mut bytes: [u8; 4] = [0; 4];
bytes.copy_from_slice(&buffer[at + offset..at + offset + 4]);
return u32::from_ne_bytes(bytes);
};
let length: usize = usize::try_from(field(12)).unwrap_or(usize::MAX);
let start: usize = at + header;
let end: usize = start.saturating_add(length).min(buffer.len());
let name: &[u8] = buffer[start..end].split(|byte| { return *byte == 0; }).next().unwrap_or(&[]);
events.push((field(4), name));
at = start.saturating_add(length);
}
return events;
}
#[cfg(target_os = "linux")]
pub fn events_end_watch(buffer: &[u8]) -> bool {
return parse_events(buffer).iter().any(|(mask, _)| { return mask & (libc::IN_UNMOUNT | libc::IN_IGNORED | libc::IN_MOVE_SELF) != 0; });
}
#[cfg(target_os = "linux")]
pub fn events_name(buffer: &[u8], wanted: &[u8]) -> bool {
return events_end_watch(buffer)
|| parse_events(buffer).iter().any(|(mask, name)| { return mask & libc::IN_Q_OVERFLOW != 0 || *name == wanted; });
}
#[cfg(target_os = "linux")]
impl Watch {
pub fn on(directory: &Path) -> Option<Watch> {
use std::os::unix::ffi::OsStrExt;
let path = std::ffi::CString::new(directory.as_os_str().as_bytes()).ok()?;
let descriptor: i32 = unsafe { libc::inotify_init1(libc::IN_CLOEXEC) };
if descriptor < 0 {
return None;
}
let added: i32 = unsafe { libc::inotify_add_watch(descriptor, path.as_ptr(), libc::IN_DELETE | libc::IN_MOVED_FROM | libc::IN_MOVE_SELF) };
if added < 0 {
unsafe { libc::close(descriptor) };
return None;
}
#[cfg(test)]
LIVE_ON_THIS_THREAD.with(|count| { count.set(count.get() + 1); });
return Some(Watch { descriptor });
}
pub fn wait(&self, timeout: Duration, name: &std::ffi::OsStr) -> bool {
use std::os::unix::ffi::OsStrExt;
let deadline: Instant = Instant::now() + timeout;
loop {
let left: Duration = deadline.saturating_duration_since(Instant::now());
if left.is_zero() {
return true;
}
let mut entry = libc::pollfd { fd: self.descriptor, events: libc::POLLIN, revents: 0 };
let milliseconds: i32 = i32::try_from(left.as_millis()).unwrap_or(i32::MAX).max(1);
let ready: i32 = unsafe { libc::poll(&raw mut entry, 1, milliseconds) };
if ready <= 0 {
return true;
}
let mut buffer: [u8; 4096] = [0; 4096];
let read: isize = unsafe { libc::read(self.descriptor, buffer.as_mut_ptr().cast(), buffer.len()) };
let Ok(count) = usize::try_from(read) else { return true };
if count == 0 {
return true;
}
if events_name(&buffer[..count], name.as_bytes()) {
return !events_end_watch(&buffer[..count]);
}
}
}
}
#[cfg(target_os = "linux")]
impl Drop for Watch {
fn drop(&mut self) {
#[cfg(test)]
LIVE_ON_THIS_THREAD.with(|count| { count.set(count.get().saturating_sub(1)); });
unsafe { libc::close(self.descriptor) };
}
}
#[cfg(not(target_os = "linux"))]
impl Watch {
pub fn on(_directory: &Path) -> Option<Watch> {
return None;
}
pub fn wait(&self, _timeout: Duration, _name: &std::ffi::OsStr) -> bool {
return true;
}
}