use std::borrow::{Borrow, Cow};
use std::collections::HashMap;
use std::ffi::{CStr, CString, OsStr, OsString};
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::task::{self, Poll};
use std::{fmt, io, mem, ptr};
use crate::fs::Metadata;
use crate::fs::notify::{self, Events, Interest, Recursive, Watcher};
use crate::kqueue::fd::OpKind;
use crate::kqueue::op::{FdIter, Next};
use crate::kqueue::{self, kqueue};
use crate::op::{FdIter as _, OpState};
use crate::{AsyncFd, SubmissionQueue, syscall};
pub(crate) type Watching = HashMap<WatchedFd, PathBufWithNull>;
type PathBufWithNull = CString;
pub(crate) struct WatchedFd(OwnedFd);
impl WatchedFd {
fn open(path: &CStr) -> io::Result<WatchedFd> {
let flags = libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC;
#[cfg(any(
target_os = "dragonfly",
target_os = "ios",
target_os = "macos",
target_os = "netbsd",
target_os = "tvos",
target_os = "visionos",
target_os = "watchos",
))]
let flags = flags | libc::O_EVTONLY;
let fd = syscall!(openat(libc::AT_FDCWD, path.as_ptr(), flags))?;
Ok(WatchedFd(unsafe { OwnedFd::from_raw_fd(fd) }))
}
}
impl Eq for WatchedFd {}
impl PartialEq for WatchedFd {
fn eq(&self, other: &Self) -> bool {
self.0.as_raw_fd() == other.0.as_raw_fd()
}
}
impl Hash for WatchedFd {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.as_raw_fd().hash(state);
}
}
impl Borrow<RawFd> for WatchedFd {
fn borrow(&self) -> &RawFd {
unsafe { &*ptr::from_ref(self).cast::<RawFd>() }
}
}
impl fmt::Debug for WatchedFd {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.as_raw_fd().fmt(f)
}
}
impl Watcher {
pub(crate) fn new_sys(sq: SubmissionQueue) -> io::Result<Watcher> {
let kq = kqueue()?;
let fd = AsyncFd::new(kq, sq);
Ok(Watcher {
fd,
watching: HashMap::new(),
})
}
}
pub(crate) fn watch_recursive(
kq: &AsyncFd,
watching: &mut Watching,
dir: PathBuf,
interest: Interest,
recursive: Recursive,
dir_only: bool,
) -> io::Result<()> {
watch_path(kq, watching, dir, interest, recursive, dir_only, None, None)
}
pub(crate) fn watch(
kq: &AsyncFd,
watching: &mut Watching,
path: PathBuf,
interest: Interest,
) -> io::Result<()> {
let recursive = Recursive::No;
watch_path(kq, watching, path, interest, recursive, false, None, None)
}
#[allow(clippy::too_many_arguments)]
fn watch_path(
kq: &AsyncFd,
watching: &mut Watching,
path: PathBuf,
interest: Interest,
recursive: Recursive,
dir_only: bool,
is_dir: Option<bool>,
parent: Option<RawFd>,
) -> io::Result<()> {
let path =
unsafe { PathBufWithNull::from_vec_unchecked(OsString::from(path).into_encoded_bytes()) };
let fd = WatchedFd::open(&path)?;
let is_dir = if let Some(false) = is_dir {
false
} else if parent.is_some()
&& let Recursive::No = recursive
{
let mut metadata: Metadata = unsafe { mem::zeroed() };
syscall!(fstat(fd.0.as_raw_fd(), &raw mut metadata.0))?;
metadata.is_dir()
} else {
let path = unsafe { Path::new(OsStr::from_encoded_bytes_unchecked(path.as_bytes())) };
let parent = fd.0.as_raw_fd();
match std::fs::read_dir(path) {
Ok(read_dir) => {
for result in read_dir {
let entry = result?;
let path = entry.path();
let is_dir = entry.file_type()?.is_dir();
watch_path(
kq,
watching,
path,
interest,
recursive,
false, Some(is_dir),
Some(parent),
)?;
}
true }
Err(ref err) if !dir_only && err.kind() == io::ErrorKind::NotADirectory => {
false }
Err(err) => return Err(err),
}
};
watch_fd(kq, watching, fd, path, interest, is_dir, parent)
}
fn watch_fd(
kq: &AsyncFd,
watching: &mut Watching,
fd: WatchedFd,
path: PathBufWithNull,
interest: Interest,
is_dir: bool,
parent: Option<RawFd>,
) -> io::Result<()> {
let mut udata: usize = if is_dir {
EVENT_EXTRA_IS_DIR as usize
} else {
0
};
#[allow(clippy::cast_sign_loss)] if let Some(fd) = parent {
udata |= (fd as usize) << 32;
}
let mut flags = 0;
#[cfg(any(target_os = "freebsd", target_os = "netbsd"))]
if interest.0 & INTEREST_ACCESS != 0 {
flags |= libc::NOTE_READ;
}
if interest.0 & INTEREST_MODIFY != 0 {
flags |= libc::NOTE_WRITE | libc::NOTE_EXTEND;
#[cfg(target_os = "openbsd")]
{
flags |= libc::NOTE_TRUNCATE;
}
}
if interest.0 & INTEREST_METADATA != 0 {
flags |= libc::NOTE_ATTRIB;
}
#[cfg(any(target_os = "freebsd", target_os = "netbsd"))]
if interest.0 & INTEREST_CLOSE_WRITE != 0 {
flags |= libc::NOTE_CLOSE_WRITE;
}
#[cfg(any(target_os = "freebsd", target_os = "netbsd"))]
if interest.0 & INTEREST_CLOSE_NOWRITE != 0 {
flags |= libc::NOTE_CLOSE;
}
#[cfg(any(target_os = "freebsd", target_os = "netbsd"))]
if interest.0 & INTEREST_OPEN != 0 {
flags |= libc::NOTE_OPEN;
}
if interest.0 & INTEREST_MOVE_INTO != 0 {
flags |= libc::NOTE_WRITE;
}
if interest.0 & INTEREST_MOVE_FROM != 0 {
if is_dir {
flags |= libc::NOTE_WRITE;
}
if parent.is_some() {
flags |= libc::NOTE_RENAME;
}
}
if interest.0 & INTEREST_CREATE != 0 && is_dir {
flags |= libc::NOTE_WRITE;
}
if interest.0 & INTEREST_DELETE != 0 && parent.is_some() {
flags |= libc::NOTE_DELETE;
}
if parent.is_none() {
if interest.0 & INTEREST_DELETE_SELF != 0 {
flags |= libc::NOTE_DELETE;
}
if interest.0 & INTEREST_MOVE_SELF != 0 {
flags |= libc::NOTE_RENAME;
}
}
let change = Event(libc::kevent {
ident: fd.0.as_raw_fd().cast_unsigned() as _,
filter: libc::EVFILT_VNODE,
flags: libc::EV_ADD | libc::EV_CLEAR,
fflags: flags,
udata: udata as _,
..unsafe { mem::zeroed() }
});
let lpath =
unsafe { Path::new(OsStr::from_encoded_bytes_unchecked(path.as_bytes())).display() };
log::trace!(change:?, fd:?, path:% = lpath; "watching path");
syscall!(kevent(
kq.fd(),
&raw const change.0,
1,
ptr::null_mut(),
0,
ptr::null(),
))?;
_ = watching.insert(fd, path);
Ok(())
}
pub(crate) const INTEREST_ALL: u32 = INTEREST_ACCESS
| INTEREST_MODIFY
| INTEREST_METADATA
| INTEREST_CLOSE
| INTEREST_OPEN
| INTEREST_MOVE
| INTEREST_CREATE
| INTEREST_DELETE
| INTEREST_DELETE_SELF
| INTEREST_MOVE_SELF;
pub(crate) const INTEREST_ACCESS: u32 = 1 << 0;
pub(crate) const INTEREST_MODIFY: u32 = 1 << 1;
pub(crate) const INTEREST_METADATA: u32 = 1 << 2;
pub(crate) const INTEREST_CLOSE_WRITE: u32 = 1 << 3;
pub(crate) const INTEREST_CLOSE_NOWRITE: u32 = 1 << 4;
pub(crate) const INTEREST_CLOSE: u32 = INTEREST_CLOSE_WRITE | INTEREST_CLOSE_NOWRITE;
pub(crate) const INTEREST_OPEN: u32 = 1 << 5;
pub(crate) const INTEREST_MOVE_INTO: u32 = 1 << 6;
pub(crate) const INTEREST_MOVE_FROM: u32 = 1 << 7;
pub(crate) const INTEREST_MOVE: u32 = INTEREST_MOVE_INTO | INTEREST_MOVE_FROM;
pub(crate) const INTEREST_CREATE: u32 = 1 << 8;
pub(crate) const INTEREST_DELETE: u32 = 1 << 9;
pub(crate) const INTEREST_DELETE_SELF: u32 = 1 << 10;
pub(crate) const INTEREST_MOVE_SELF: u32 = 1 << 11;
#[derive(Debug)]
pub(crate) struct EventsState<'w>(<NotifyOp<'w> as crate::op::FdIter>::State);
impl<'w> EventsState<'w> {
pub(crate) fn new(_: &'w AsyncFd) -> EventsState<'w> {
EventsState(kqueue::op::State::new((Vec::with_capacity(8), 0), ()))
}
}
impl<'w> Events<'w> {
pub(crate) fn path_for_sys<'a>(&'a self, event: &'a Event) -> Cow<'a, Path> {
#[allow(clippy::cast_possible_wrap)]
let fd = event.0.ident as RawFd;
if let Some(path) = self.watching.get(&fd) {
let path = unsafe { OsStr::from_encoded_bytes_unchecked(path.as_bytes()) };
return Cow::Borrowed(Path::new(path));
}
panic!("unknown fd in kevent")
}
pub(crate) fn poll_sys(
mut self: Pin<&mut Self>,
ctx: &mut task::Context<'_>,
) -> Poll<Option<io::Result<&'w notify::Event>>> {
let Events {
fd: kq,
state: EventsState(state),
..
} = &mut *self;
NotifyOp::poll_next(state, ctx, kq)
}
}
pub(crate) struct NotifyOp<'a>(PhantomData<&'a ()>);
impl<'a> FdIter for NotifyOp<'a> {
type Output = &'a notify::Event;
type Resources = (Vec<Event>, usize);
type Args = ();
type OperationOutput = ();
const OP_KIND: OpKind = OpKind::Read;
#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
fn try_run(
kq: &AsyncFd,
(events, processed): &mut Self::Resources,
(): &mut Self::Args,
) -> io::Result<Self::OperationOutput> {
*processed += 1;
if events.len() > *processed {
return Ok(());
}
let timeout = libc::timespec {
tv_sec: 0,
tv_nsec: 0,
};
let n = syscall!(kevent(
kq.fd(),
ptr::null(),
0,
events.as_mut_ptr().cast::<libc::kevent>(),
events.capacity() as _,
&raw const timeout,
))?;
if n == 0 {
Err(io::ErrorKind::WouldBlock.into())
} else {
unsafe { events.set_len(n as _) };
*processed = 0;
log::trace!(events:?; "got fs events");
let mut i = 0;
while let Some((head, tail)) = events.split_at_mut_checked(i + 1) {
let event = &head[i];
if event.is_dir() && (event.0.fflags & libc::NOTE_WRITE != 0) {
if let Some(idx) = tail.iter().position(|e| {
e.parent_fd() == Some(event.0.ident as _)
&& e.0.fflags & libc::NOTE_RENAME != 0
}) {
_ = events.remove(i); let event = &mut events[i + idx];
event.0.udata =
(event.0.udata as u64 | u64::from(EVENT_EXTRA_FILE_MOVED_FROM)) as _;
event.0.fflags &= !libc::NOTE_RENAME; continue; } else if head.iter().any(|e| {
e.parent_fd() == Some(event.0.ident as _)
&& e.0.fflags & libc::NOTE_DELETE != 0
}) {
_ = events.remove(i); } else {
let event = &mut head[i];
event.0.fflags &= !libc::NOTE_WRITE; event.0.udata =
(event.0.udata as u64 | u64::from(EVENT_EXTRA_FILE_CREATED)) as _;
}
}
i += 1;
}
Ok(())
}
}
fn next(_: &Self::Resources, (): &Self::OperationOutput) -> Next {
Next::TryRun
}
fn map_next(
_: &AsyncFd,
(events, processed): &Self::Resources,
(): Self::OperationOutput,
) -> Self::Output {
let event = &events[*processed];
debug_assert_eq!(event.0.filter, libc::EVFILT_VNODE);
unsafe { &*ptr::from_ref(event).cast::<notify::Event>() }
}
}
pub(crate) use crate::kqueue::Event;
impl Event {
#[allow(clippy::unused_self)]
pub(crate) fn file_path(&self) -> &Path {
panic!(
"a10::fs::notify::Event::file_path doesn't work with kqueue, use Events::path_for instead",
)
}
pub(crate) const fn mask(&self) -> u32 {
self.0.fflags
}
fn parent_fd(&self) -> Option<RawFd> {
let fd = (self.0.udata as isize) >> 32;
if fd == 0 { None } else { Some(fd as RawFd) }
}
fn mask_extra(&self) -> u32 {
((self.0.udata as usize) & !((u32::MAX as usize) << 32)) as u32
}
pub(crate) fn is_dir(&self) -> bool {
self.mask_extra() & EVENT_EXTRA_IS_DIR != 0
}
pub(crate) fn modified(&self) -> bool {
if self.is_dir() {
false } else {
self.mask() & EVENT_MODIFIED != 0
}
}
pub(crate) fn deleted(&self) -> bool {
if self.parent_fd().is_some() {
false
} else {
self.mask() & libc::NOTE_DELETE != 0
}
}
pub(crate) fn file_moved_from(&self) -> bool {
self.mask_extra() & EVENT_EXTRA_FILE_MOVED_FROM != 0
}
#[allow(clippy::unused_self)]
pub(crate) fn file_moved_into(&self) -> bool {
false }
pub(crate) fn file_moved(&self) -> bool {
self.mask_extra() & EVENT_EXTRA_FILE_MOVED != 0
}
pub(crate) fn file_created(&self) -> bool {
self.mask_extra() & EVENT_EXTRA_FILE_CREATED != 0
}
pub(crate) fn file_deleted(&self) -> bool {
if self.parent_fd().is_some() {
self.mask() & libc::NOTE_DELETE != 0
} else {
false
}
}
}
#[cfg(any(target_os = "freebsd", target_os = "netbsd"))]
pub(crate) const EVENT_ACCESSED: u32 = libc::NOTE_READ;
#[cfg(target_os = "openbsd")]
pub(crate) const EVENT_MODIFIED: u32 = libc::NOTE_WRITE | libc::NOTE_EXTEND | libc::NOTE_TRUNCATE;
#[cfg(not(target_os = "openbsd"))]
pub(crate) const EVENT_MODIFIED: u32 = libc::NOTE_WRITE | libc::NOTE_EXTEND;
pub(crate) const EVENT_METADATA_CHANGED: u32 = libc::NOTE_ATTRIB;
#[cfg(any(target_os = "freebsd", target_os = "netbsd"))]
pub(crate) const EVENT_CLOSED_WRITE: u32 = libc::NOTE_CLOSE_WRITE;
#[cfg(any(target_os = "freebsd", target_os = "netbsd"))]
pub(crate) const EVENT_CLOSED_NO_WRITE: u32 = libc::NOTE_CLOSE;
#[cfg(any(target_os = "freebsd", target_os = "netbsd"))]
pub(crate) const EVENT_CLOSED: u32 = libc::NOTE_CLOSE_WRITE | libc::NOTE_CLOSE;
#[cfg(any(target_os = "freebsd", target_os = "netbsd"))]
pub(crate) const EVENT_OPENED: u32 = libc::NOTE_OPEN;
pub(crate) const EVENT_MOVED: u32 = libc::NOTE_RENAME;
pub(crate) const EVENT_UNMOUNTED: u32 = libc::NOTE_REVOKE;
const EVENT_EXTRA_IS_DIR: u32 = 1 << 0; const EVENT_EXTRA_FILE_MOVED_FROM: u32 = 1 << 1;
const EVENT_EXTRA_FILE_MOVED: u32 = EVENT_EXTRA_FILE_MOVED_FROM;
const EVENT_EXTRA_FILE_CREATED: u32 = 1 << 3;