use crate::{Error, ErrorKind, FsPath, Handle, Notifier, Result, WatchKind, fs::watch::Ring};
use std::{
collections::VecDeque,
ffi::{CStr, CString, c_char, c_void},
os::unix::ffi::OsStrExt,
sync::{
Arc, Mutex, MutexGuard, OnceLock,
atomic::{AtomicBool, Ordering},
},
};
type Ref = *const c_void;
type Callback = unsafe extern "C" fn(Ref, *mut c_void, usize, *mut c_void, *const u32, *const u64);
type StringFn = unsafe extern "C" fn(Ref, *const c_char) -> Ref;
type ArrayFn = unsafe extern "C" fn(Ref, *const Ref, isize, Ref) -> Ref;
type ReleaseFn = unsafe extern "C" fn(Ref);
type CreateFn =
unsafe extern "C" fn(Ref, Callback, *const StreamContext, Ref, u64, f64, u32) -> Ref;
type QueueFn = unsafe extern "C" fn(Ref, Ref);
type StartFn = unsafe extern "C" fn(Ref) -> u8;
#[repr(C)]
struct StreamContext {
version: isize,
info: *mut c_void,
retain: Option<unsafe extern "C" fn(Ref) -> Ref>,
release: Option<unsafe extern "C" fn(Ref)>,
description: Option<unsafe extern "C" fn(Ref) -> Ref>,
}
struct Api {
string: StringFn,
array: ArrayFn,
cf_release: ReleaseFn,
create: CreateFn,
set_queue: QueueFn,
start: StartFn,
stop: ReleaseFn,
invalidate: ReleaseFn,
release: ReleaseFn,
}
unsafe impl Send for Api {}
unsafe impl Sync for Api {}
unsafe extern "C" {
fn dispatch_queue_create(label: *const c_char, attr: Ref) -> Ref;
fn dispatch_sync_f(queue: Ref, context: *mut c_void, work: unsafe extern "C" fn(*mut c_void));
fn dispatch_release(object: Ref);
fn dispatch_retain(object: Ref);
}
const SINCE_NOW: u64 = u64::MAX;
const NO_DEFER: u32 = 0x02;
const WATCH_ROOT: u32 = 0x04;
const FILE_EVENTS: u32 = 0x10;
const MUST_SCAN_SUBDIRS: u32 = 0x01;
const USER_DROPPED: u32 = 0x02;
const KERNEL_DROPPED: u32 = 0x04;
const ROOT_CHANGED: u32 = 0x20;
const RENAMED: u32 = 0x100 | 0x200 | 0x800; const MODIFIED: u32 = 0x400 | 0x1000 | 0x2000 | 0x4000 | 0x8000;
const IS_DIRECTORY: u32 = 0x20000;
fn api() -> Result<&'static Api> {
static API: OnceLock<Option<Api>> = OnceLock::new();
API.get_or_init(|| {
let open = |path: &CStr| {
let image = unsafe { libc::dlopen(path.as_ptr(), libc::RTLD_LAZY | libc::RTLD_LOCAL) };
(!image.is_null()).then_some(image)
};
let core = open(c"/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")?;
let services = open(c"/System/Library/Frameworks/CoreServices.framework/CoreServices")?;
macro_rules! symbol {
($image:expr, $name:literal, $type:ty) => {{
let pointer = unsafe { libc::dlsym($image, $name.as_ptr()) };
if pointer.is_null() {
return None;
}
unsafe { std::mem::transmute::<*mut c_void, $type>(pointer) }
}};
}
Some(Api {
string: symbol!(
core,
c"CFStringCreateWithFileSystemRepresentation",
StringFn
),
array: symbol!(core, c"CFArrayCreate", ArrayFn),
cf_release: symbol!(core, c"CFRelease", ReleaseFn),
create: symbol!(services, c"FSEventStreamCreate", CreateFn),
set_queue: symbol!(services, c"FSEventStreamSetDispatchQueue", QueueFn),
start: symbol!(services, c"FSEventStreamStart", StartFn),
stop: symbol!(services, c"FSEventStreamStop", ReleaseFn),
invalidate: symbol!(services, c"FSEventStreamInvalidate", ReleaseFn),
release: symbol!(services, c"FSEventStreamRelease", ReleaseFn),
})
})
.as_ref()
.ok_or(Error::new(ErrorKind::Unsupported))
}
fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
m.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
}
struct Inbox {
pending: AtomicBool,
ready: Mutex<VecDeque<Handle>>,
notifier: Mutex<Option<Notifier>>,
}
struct Context {
inbox: Arc<Inbox>,
ring: Mutex<Ring>,
queued: AtomicBool,
handle: Handle,
root: CString,
name: Box<[u8]>,
recursive: bool,
}
unsafe extern "C" fn receive(
_stream: Ref,
info: *mut c_void,
count: usize,
paths: *mut c_void,
flags: *const u32,
_ids: *const u64,
) {
let (context, paths, flags) = unsafe {
(
&*info.cast::<Context>(),
std::slice::from_raw_parts(paths.cast::<*const c_char>(), count),
std::slice::from_raw_parts(flags, count),
)
};
let root = context.root.as_bytes();
let mut ring = lock(&context.ring);
let mut published = false;
for (&path, &flags) in paths.iter().zip(flags) {
if flags & (MUST_SCAN_SUBDIRS | USER_DROPPED | KERNEL_DROPPED) != 0 {
ring.lost();
published = true;
}
if flags & ROOT_CHANGED != 0 {
ring.push(WatchKind::Rename, &context.name);
published = true;
continue;
}
let path = unsafe { CStr::from_ptr(path) }.to_bytes();
let Some(rest) = path.strip_prefix(root) else {
continue;
};
let mut flags = flags;
let name = if rest.is_empty() {
flags &= !RENAMED;
&context.name[..]
} else if root == b"/" {
rest
} else if let Some(child) = rest.strip_prefix(b"/") {
child
} else {
continue;
};
if !context.recursive && !rest.is_empty() && name.contains(&b'/') {
continue;
}
let kind = if flags & RENAMED == 0 && (flags & MODIFIED != 0 || flags & IS_DIRECTORY == 0) {
WatchKind::Change
} else {
WatchKind::Rename
};
ring.push(kind, name);
published = true;
}
drop(ring);
if published && !context.queued.swap(true, Ordering::AcqRel) {
let mut ready = lock(&context.inbox.ready);
ready.push_back(context.handle);
context.inbox.pending.store(true, Ordering::Release);
drop(ready);
if let Some(notifier) = lock(&context.inbox.notifier).as_ref() {
let _ = notifier.notify();
}
}
}
unsafe extern "C" fn barrier(_: *mut c_void) {}
pub(super) struct Stream {
stream: Ref,
paths: Ref,
string: Ref,
queue: Ref,
started: bool,
context: Box<Context>,
}
impl Stream {
pub fn rearm(&self) {
self.context.queued.store(false, Ordering::Release);
}
pub fn with_ring<R>(&self, f: impl FnOnce(&mut Ring) -> R) -> R {
f(&mut lock(&self.context.ring))
}
}
impl Drop for Stream {
fn drop(&mut self) {
let Ok(api) = api() else {
return;
};
unsafe {
if self.started {
(api.stop)(self.stream);
(api.invalidate)(self.stream);
}
if !self.queue.is_null() {
dispatch_sync_f(self.queue, std::ptr::null_mut(), barrier);
dispatch_release(self.queue);
}
if !self.stream.is_null() {
(api.release)(self.stream);
}
if !self.paths.is_null() {
(api.cf_release)(self.paths);
}
if !self.string.is_null() {
(api.cf_release)(self.string);
}
}
}
}
pub(super) struct Streams {
inbox: Arc<Inbox>,
queue: Ref,
}
impl Streams {
pub fn new() -> Self {
let inbox = Arc::new(Inbox {
pending: AtomicBool::new(false),
ready: Mutex::new(VecDeque::with_capacity(crate::slots::PAGE)),
notifier: Mutex::new(None),
});
drop(lock(&inbox.ready));
drop(lock(&inbox.notifier));
Self {
inbox,
queue: std::ptr::null(),
}
}
pub fn set_notifier(&mut self, notifier: Notifier) {
*lock(&self.inbox.notifier) = Some(notifier);
}
pub fn has_work(&self) -> bool {
self.inbox.pending.load(Ordering::Acquire)
}
pub fn drain(&mut self, mut ready: impl FnMut(Handle)) {
if !self.has_work() {
return;
}
let mut inbox = lock(&self.inbox.ready);
self.inbox.pending.store(false, Ordering::Release);
while let Some(h) = inbox.pop_front() {
ready(h);
}
}
pub fn start(&mut self, h: Handle, path: &FsPath, recursive: bool) -> Result<Stream> {
let api = api()?;
let root = std::fs::canonicalize(path.as_path()).map_err(Error::from)?;
let root = CString::new(root.as_os_str().as_bytes())
.map_err(|_| Error::new(ErrorKind::InvalidInput))?;
let name = root
.as_bytes()
.rsplit(|&b| b == b'/')
.find(|part| !part.is_empty())
.unwrap_or(b"/")
.into();
if self.queue.is_null() {
self.queue =
unsafe { dispatch_queue_create(c"turnloop.fsevents".as_ptr(), std::ptr::null()) };
if self.queue.is_null() {
return Err(Error::new(ErrorKind::ResourceLimit));
}
}
let context = Box::new(Context {
inbox: self.inbox.clone(),
ring: Mutex::new(Ring::new()),
queued: AtomicBool::new(false),
handle: h,
root,
name,
recursive,
});
drop(lock(&context.ring));
unsafe { dispatch_retain(self.queue) };
let mut stream = Stream {
stream: std::ptr::null(),
paths: std::ptr::null(),
string: std::ptr::null(),
queue: self.queue,
started: false,
context,
};
stream.string = unsafe { (api.string)(std::ptr::null(), stream.context.root.as_ptr()) };
if stream.string.is_null() {
return Err(Error::new(ErrorKind::InvalidInput));
}
stream.paths =
unsafe { (api.array)(std::ptr::null(), &stream.string, 1, std::ptr::null()) };
if stream.paths.is_null() {
return Err(Error::new(ErrorKind::ResourceLimit));
}
let info = StreamContext {
version: 0,
info: (&*stream.context as *const Context).cast_mut().cast(),
retain: None,
release: None,
description: None,
};
stream.stream = unsafe {
(api.create)(
std::ptr::null(),
receive,
&info,
stream.paths,
SINCE_NOW,
0.0,
NO_DEFER | WATCH_ROOT | FILE_EVENTS,
)
};
if stream.stream.is_null() {
return Err(Error::new(ErrorKind::Other));
}
unsafe { (api.set_queue)(stream.stream, self.queue) };
stream.started = true;
if unsafe { (api.start)(stream.stream) } == 0 {
return Err(Error::new(ErrorKind::Other));
}
Ok(stream)
}
pub fn stop(&mut self, h: Handle, stream: Stream) {
drop(stream);
lock(&self.inbox.ready).retain(|&queued| queued != h);
}
}
impl Drop for Streams {
fn drop(&mut self) {
if !self.queue.is_null() {
unsafe { dispatch_release(self.queue) };
}
}
}