use std::borrow::Cow;
use std::collections::HashMap;
use std::ffi::{CString, OsStr, OsString};
use std::mem::replace;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::task::{self, Poll};
use std::{fmt, io, ptr, slice};
use crate::io::Read;
use crate::{AsyncFd, SubmissionQueue, new_flag, syscall};
#[derive(Debug)]
pub struct Watcher {
fd: AsyncFd,
watching: HashMap<WatchFd, PathBufWithNull>,
}
type PathBufWithNull = CString;
type WatchFd = std::os::fd::RawFd;
const BUF_SIZE: usize = size_of::<libc::inotify_event>() + libc::NAME_MAX as usize + 1 ;
impl Watcher {
pub fn new(sq: SubmissionQueue) -> io::Result<Watcher> {
let ifd = syscall!(inotify_init1(libc::IN_CLOEXEC))?;
let fd = unsafe { AsyncFd::from_raw_fd(ifd, sq) };
Ok(Watcher {
fd,
watching: HashMap::new(),
})
}
pub fn watch_directory(
&mut self,
dir: PathBuf,
interest: Interest,
recursive: Recursive,
) -> io::Result<()> {
self.watch_path_recursive(dir, interest, recursive, true)
}
pub fn watch_file(&mut self, file: PathBuf, interest: Interest) -> io::Result<()> {
self.watch_path(file, interest.0)
}
pub fn watch(
&mut self,
dir: PathBuf,
interest: Interest,
recursive: Recursive,
) -> io::Result<()> {
self.watch_path_recursive(dir, interest, recursive, false)
}
fn watch_path_recursive(
&mut self,
dir: PathBuf,
interest: Interest,
recursive: Recursive,
dir_only: bool,
) -> io::Result<()> {
if let Recursive::All = recursive {
match std::fs::read_dir(&dir) {
Ok(read_dir) => {
for result in read_dir {
let entry = result?;
if entry.file_type()?.is_dir() {
let path = entry.path();
self.watch_directory(path, interest, Recursive::All)?;
}
}
}
Err(ref err) if !dir_only && err.kind() == io::ErrorKind::NotADirectory => {
}
Err(err) => return Err(err),
}
}
let mask = interest.0 | if dir_only { libc::IN_ONLYDIR } else { 0 };
self.watch_path(dir, mask)
}
fn watch_path(&mut self, path: PathBuf, mask: u32) -> io::Result<()> {
let path = unsafe {
PathBufWithNull::from_vec_unchecked(OsString::from(path).into_encoded_bytes())
};
let mask = mask
| libc::IN_DONT_FOLLOW
| libc::IN_EXCL_UNLINK
| libc::IN_MASK_ADD;
let fd = self.fd.fd();
let wd = syscall!(inotify_add_watch(fd, path.as_ptr(), mask))?;
_ = self.watching.insert(wd, path);
Ok(())
}
pub fn events<'w>(&'w mut self) -> Events<'w> {
Events {
watching: &mut self.watching,
state: EventsState::Reading(self.fd.read(Vec::with_capacity(BUF_SIZE))),
}
}
}
new_flag!(
pub struct Interest(u32) impl BitOr {
ALL = libc::IN_ALL_EVENTS,
ACCESS = libc::IN_ACCESS,
MODIFY = libc::IN_MODIFY,
METADATA = libc::IN_ATTRIB,
CLOSE_WRITE = libc::IN_CLOSE_WRITE,
CLOSE_NOWRITE = libc::IN_CLOSE_NOWRITE,
CLOSE = libc::IN_CLOSE,
OPEN = libc::IN_OPEN,
MOVE_FROM = libc::IN_MOVED_FROM,
MOVE_INTO = libc::IN_MOVED_TO,
MOVE = libc::IN_MOVE,
CREATE = libc::IN_CREATE,
DELETE = libc::IN_DELETE,
DELETE_SELF = libc::IN_DELETE_SELF,
MOVE_SELF = libc::IN_MOVE_SELF,
}
);
#[derive(Copy, Clone, Debug)]
#[non_exhaustive]
pub enum Recursive {
No,
All,
}
#[must_use = "`AsyncIterator`s do nothing unless polled"]
#[derive(Debug)]
pub struct Events<'w> {
watching: &'w mut HashMap<WatchFd, PathBufWithNull>,
state: EventsState<'w>,
}
#[derive(Debug)]
enum EventsState<'w> {
Reading(Read<'w, Vec<u8>>),
Processing {
buf: Vec<u8>,
processed: usize,
fd: &'w AsyncFd,
},
Done,
}
impl<'w> Events<'w> {
pub fn path_for<'a>(&'a self, event: &'a Event) -> Cow<'a, Path> {
let file_path = event.file_path();
match self.watched_path(&event.event.wd) {
Some(path) if file_path.as_os_str().is_empty() => Cow::Borrowed(path),
Some(path) => Cow::Owned(path.join(file_path)),
None => Cow::Borrowed(file_path),
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn watched_path<'a>(&'a self, wd: &WatchFd) -> Option<&'a Path> {
self.watching.get(wd).map(move |path| {
let path = unsafe { OsStr::from_encoded_bytes_unchecked(path.as_bytes()) };
Path::new(path)
})
}
pub fn poll_next(
mut self: Pin<&mut Self>,
ctx: &mut task::Context<'_>,
) -> Poll<Option<io::Result<&'w Event>>> {
let this = &mut *self;
loop {
match &mut this.state {
EventsState::Processing { buf, processed, .. } => {
if buf.len() > *processed {
debug_assert!(buf.len() >= *processed + size_of::<libc::inotify_event>());
#[allow(clippy::cast_ptr_alignment)]
let event_ptr = unsafe {
buf.as_ptr()
.byte_add(*processed)
.cast::<libc::inotify_event>()
};
let len = unsafe { (&*event_ptr).len as usize };
*processed += size_of::<libc::inotify_event>() + len;
debug_assert!(buf.len() >= *processed);
let mask = unsafe { (&*event_ptr).mask };
if mask & libc::IN_IGNORED != 0 {
let wd = unsafe { (&*event_ptr).wd };
_ = this.watching.remove(&wd);
continue; }
if mask & libc::IN_Q_OVERFLOW != 0 {
log::warn!("inotify event queue overflowed");
continue;
}
let path = unsafe {
slice::from_raw_parts(
event_ptr
.byte_add(size_of::<libc::inotify_event>())
.cast::<u8>(),
len,
)
};
let path_len = path.iter().rposition(|b| *b != 0).map_or(len, |n| n + 1);
#[allow(clippy::cast_ptr_alignment)]
let event: &'w Event = unsafe {
&*(ptr::slice_from_raw_parts(event_ptr.cast::<u8>(), path_len)
as *const Event)
};
return Poll::Ready(Some(Ok(event)));
}
let (mut buf, fd) = match replace(&mut this.state, EventsState::Done) {
EventsState::Processing {
buf,
processed: _,
fd,
} => (buf, fd),
EventsState::Reading(_) | EventsState::Done => unreachable!(),
};
buf.clear();
this.state = EventsState::Reading(fd.read(buf));
}
EventsState::Reading(read) => {
match Pin::new(&mut *read).poll(ctx) {
Poll::Ready(Ok(buf)) => {
if buf.is_empty() {
this.state = EventsState::Done;
return Poll::Ready(None);
}
this.state = EventsState::Processing {
buf,
processed: 0,
fd: read.fd(),
};
}
Poll::Ready(Err(err)) => {
this.state = EventsState::Done;
return Poll::Ready(Some(Err(err)));
}
Poll::Pending => return Poll::Pending,
}
}
EventsState::Done => return Poll::Ready(None),
}
}
}
}
#[cfg(feature = "nightly")]
impl<'w> std::async_iter::AsyncIterator for Events<'w> {
type Item = io::Result<&'w Event>;
fn poll_next(self: Pin<&mut Self>, ctx: &mut task::Context<'_>) -> Poll<Option<Self::Item>> {
self.poll_next(ctx)
}
}
pub struct Event {
event: libc::inotify_event,
path: [u8],
}
impl Event {
pub fn file_path(&self) -> &Path {
Path::new(unsafe { OsStr::from_encoded_bytes_unchecked(&self.path) })
}
bit_checks!(
is_dir, IN_ISDIR;
accessed, IN_ACCESS;
modified, IN_MODIFY;
metadata_changed, IN_ATTRIB;
closed_write, IN_CLOSE_WRITE;
closed_no_write, IN_CLOSE_NOWRITE;
closed, IN_CLOSE;
opened, IN_OPEN;
deleted, IN_DELETE_SELF;
moved, IN_MOVE_SELF;
unmounted, IN_UNMOUNT;
file_moved_from, IN_MOVED_FROM;
file_moved_into, IN_MOVED_TO;
file_moved, IN_MOVE;
file_created, IN_CREATE;
file_deleted, IN_DELETE;
);
const fn mask(&self) -> u32 {
self.event.mask
}
}
macro_rules! bit_checks {
( $( $(#[$meta: meta])* $fn_name: ident, $bit: ident ; )+ ) => {
$(
$( #[$meta] )*
pub fn $fn_name(&self) -> bool {
self.mask() & libc::$bit != 0
}
)+
fn events(&self) -> impl fmt::Debug {
struct Events<'a>(&'a Event);
impl<'a> fmt::Debug for Events<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut f = f.debug_list();
$(
if self.0.$fn_name() {
_ = f.entry(&stringify!($fn_name));
}
)+
f.finish()
}
}
Events(self)
}
};
}
use bit_checks;
#[allow(clippy::missing_fields_in_debug)]
impl fmt::Debug for Event {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut f = f.debug_struct("Event");
f.field("wd", &self.event.wd)
.field("mask", &self.event.mask)
.field("cookie", &self.event.cookie)
.field("file_path", &self.file_path())
.field("events", &self.events())
.finish()
}
}