makepad/libs/filesystem_watcher/src/macos.rs
2026-04-27 14:51:24 +02:00

415 lines
13 KiB
Rust

use crate::{FileSystemEvent, FileSystemEventKind, WatchCallback, WatchRoot};
use std::collections::HashMap;
use std::ffi::{c_void, CStr, CString};
use std::os::raw::{c_char, c_double};
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::{Duration, SystemTime};
type CFAllocatorRef = *const c_void;
type CFStringRef = *const c_void;
type CFArrayRef = *const c_void;
type CFRunLoopRef = *mut c_void;
type CFIndex = isize;
type Boolean = u8;
type FSEventStreamRef = *mut c_void;
type FSEventStreamEventId = u64;
type FSEventStreamEventFlags = u32;
type FSEventStreamCreateFlags = u32;
type CFTimeInterval = c_double;
const K_CF_STRING_ENCODING_UTF8: u32 = 0x0800_0100;
const K_FS_EVENT_STREAM_EVENT_ID_SINCE_NOW: FSEventStreamEventId = 0xFFFF_FFFF_FFFF_FFFF;
const K_FS_EVENT_STREAM_CREATE_FLAG_FILE_EVENTS: FSEventStreamCreateFlags = 0x0000_0010;
const K_FS_EVENT_STREAM_CREATE_FLAG_USE_CF_TYPES: FSEventStreamCreateFlags = 0x0000_0001;
const K_FS_EVENT_STREAM_CREATE_FLAG_NO_DEFER: FSEventStreamCreateFlags = 0x0000_0002;
#[repr(C)]
struct FSEventStreamContext {
version: CFIndex,
info: *mut c_void,
retain: Option<extern "C" fn(*const c_void) -> *const c_void>,
release: Option<extern "C" fn(*const c_void)>,
copy_description: Option<extern "C" fn(*const c_void) -> CFStringRef>,
}
type FSEventStreamCallback = extern "C" fn(
stream_ref: FSEventStreamRef,
client_callback_info: *mut c_void,
num_events: usize,
event_paths: *mut c_void,
event_flags: *const FSEventStreamEventFlags,
event_ids: *const FSEventStreamEventId,
);
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
static kCFRunLoopDefaultMode: CFStringRef;
fn CFStringCreateWithCString(
alloc: CFAllocatorRef,
c_str: *const c_char,
encoding: u32,
) -> CFStringRef;
fn CFArrayCreate(
allocator: CFAllocatorRef,
values: *const *const c_void,
num_values: CFIndex,
callbacks: *const c_void,
) -> CFArrayRef;
fn CFArrayGetCount(the_array: CFArrayRef) -> CFIndex;
fn CFArrayGetValueAtIndex(the_array: CFArrayRef, idx: CFIndex) -> *const c_void;
fn CFStringGetCString(
the_string: CFStringRef,
buffer: *mut c_char,
buffer_size: CFIndex,
encoding: u32,
) -> Boolean;
fn CFRelease(cf: *const c_void);
fn CFRunLoopGetCurrent() -> CFRunLoopRef;
fn CFRunLoopRun();
fn CFRunLoopStop(rl: CFRunLoopRef);
}
#[link(name = "CoreServices", kind = "framework")]
unsafe extern "C" {
fn FSEventStreamCreate(
allocator: CFAllocatorRef,
callback: FSEventStreamCallback,
context: *mut FSEventStreamContext,
paths_to_watch: CFArrayRef,
since_when: FSEventStreamEventId,
latency: CFTimeInterval,
flags: FSEventStreamCreateFlags,
) -> FSEventStreamRef;
fn FSEventStreamScheduleWithRunLoop(
stream_ref: FSEventStreamRef,
run_loop: CFRunLoopRef,
run_loop_mode: CFStringRef,
);
fn FSEventStreamStart(stream_ref: FSEventStreamRef) -> Boolean;
fn FSEventStreamStop(stream_ref: FSEventStreamRef);
fn FSEventStreamInvalidate(stream_ref: FSEventStreamRef);
fn FSEventStreamRelease(stream_ref: FSEventStreamRef);
}
struct CallbackInfo {
mount: String,
root: PathBuf,
on_event: WatchCallback,
}
extern "C" fn context_retain(info: *const c_void) -> *const c_void {
info
}
extern "C" fn context_release(info: *const c_void) {
if info.is_null() {
return;
}
unsafe {
drop(Box::from_raw(info as *mut CallbackInfo));
}
}
extern "C" fn fsevent_callback(
_stream_ref: FSEventStreamRef,
client_callback_info: *mut c_void,
num_events: usize,
event_paths: *mut c_void,
_event_flags: *const FSEventStreamEventFlags,
_event_ids: *const FSEventStreamEventId,
) {
if client_callback_info.is_null() || num_events == 0 {
return;
}
let info = unsafe { &*(client_callback_info as *const CallbackInfo) };
let paths_array = event_paths as CFArrayRef;
let mut emitted = 0usize;
if !paths_array.is_null() {
let count = unsafe { CFArrayGetCount(paths_array) }.max(0) as usize;
let total = num_events.min(count);
for i in 0..total {
let cf_path =
unsafe { CFArrayGetValueAtIndex(paths_array, i as CFIndex) } as CFStringRef;
if cf_path.is_null() {
continue;
}
let mut buf = vec![0 as c_char; 8192];
let ok = unsafe {
CFStringGetCString(
cf_path,
buf.as_mut_ptr(),
buf.len() as CFIndex,
K_CF_STRING_ENCODING_UTF8,
)
};
if ok == 0 {
continue;
}
let path = unsafe { CStr::from_ptr(buf.as_ptr()) }
.to_string_lossy()
.into_owned();
(info.on_event)(FileSystemEvent {
mount: info.mount.clone(),
path: PathBuf::from(path),
kind: FileSystemEventKind::Changed,
});
emitted += 1;
}
}
if emitted == 0 {
(info.on_event)(FileSystemEvent {
mount: info.mount.clone(),
path: info.root.clone(),
kind: FileSystemEventKind::Changed,
});
}
}
pub struct PlatformWatcher {
run_loop: Arc<Mutex<usize>>,
stop: Arc<AtomicBool>,
thread: Option<JoinHandle<()>>,
}
impl PlatformWatcher {
pub fn start(roots: Vec<WatchRoot>, on_event: WatchCallback) -> Result<Self, String> {
let run_loop = Arc::new(Mutex::new(0usize));
let stop = Arc::new(AtomicBool::new(false));
let run_loop_thread = Arc::clone(&run_loop);
let stop_thread = Arc::clone(&stop);
let (ready_tx, ready_rx) = mpsc::channel();
let thread = thread::Builder::new()
.name("fswatch-macos".to_string())
.spawn(move || {
run_loop_thread_main(roots, on_event, run_loop_thread, stop_thread, ready_tx)
})
.map_err(|err| format!("failed to spawn macos watcher thread: {}", err))?;
ready_rx
.recv()
.map_err(|_| "macos watcher thread exited before initialization".to_string())?;
Ok(Self {
run_loop,
stop,
thread: Some(thread),
})
}
pub fn stop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
let run_loop = self.run_loop.lock().ok().map(|guard| *guard).unwrap_or(0);
if run_loop != 0 {
unsafe {
CFRunLoopStop(run_loop as CFRunLoopRef);
}
}
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
fn run_loop_thread_main(
roots: Vec<WatchRoot>,
on_event: WatchCallback,
run_loop_slot: Arc<Mutex<usize>>,
stop: Arc<AtomicBool>,
ready_tx: mpsc::Sender<()>,
) {
let run_loop = unsafe { CFRunLoopGetCurrent() };
if let Ok(mut slot) = run_loop_slot.lock() {
*slot = run_loop as usize;
}
let roots_for_poll = roots.clone();
let mut streams = Vec::<FSEventStreamRef>::new();
let mut arrays = Vec::<CFArrayRef>::new();
let mut strings = Vec::<CFStringRef>::new();
for root in roots {
let Ok(c_root) = CString::new(root.path.as_os_str().as_bytes()) else {
continue;
};
let cf_root = unsafe {
CFStringCreateWithCString(std::ptr::null(), c_root.as_ptr(), K_CF_STRING_ENCODING_UTF8)
};
if cf_root.is_null() {
continue;
}
let values = [cf_root as *const c_void];
let cf_array =
unsafe { CFArrayCreate(std::ptr::null(), values.as_ptr(), 1, std::ptr::null()) };
if cf_array.is_null() {
unsafe {
CFRelease(cf_root);
}
continue;
}
let callback_info = Box::new(CallbackInfo {
mount: root.mount,
root: root.path,
on_event: Arc::clone(&on_event),
});
let callback_info_ptr = Box::into_raw(callback_info) as *mut c_void;
let mut context = FSEventStreamContext {
version: 0,
info: callback_info_ptr,
retain: Some(context_retain),
release: Some(context_release),
copy_description: None,
};
let stream = unsafe {
FSEventStreamCreate(
std::ptr::null(),
fsevent_callback,
&mut context,
cf_array,
K_FS_EVENT_STREAM_EVENT_ID_SINCE_NOW,
0.1,
K_FS_EVENT_STREAM_CREATE_FLAG_FILE_EVENTS
| K_FS_EVENT_STREAM_CREATE_FLAG_USE_CF_TYPES
| K_FS_EVENT_STREAM_CREATE_FLAG_NO_DEFER,
)
};
if stream.is_null() {
unsafe {
context_release(callback_info_ptr);
CFRelease(cf_array);
CFRelease(cf_root);
}
continue;
}
unsafe {
FSEventStreamScheduleWithRunLoop(stream, run_loop, kCFRunLoopDefaultMode);
}
let started = unsafe { FSEventStreamStart(stream) };
if started == 0 {
unsafe {
FSEventStreamInvalidate(stream);
FSEventStreamRelease(stream);
CFRelease(cf_array);
CFRelease(cf_root);
}
continue;
}
streams.push(stream);
arrays.push(cf_array);
strings.push(cf_root);
}
if !streams.is_empty() {
let _ = ready_tx.send(());
unsafe {
CFRunLoopRun();
}
} else {
let _ = ready_tx.send(());
poll_loop(roots_for_poll, on_event, stop);
}
for stream in streams {
unsafe {
FSEventStreamStop(stream);
FSEventStreamInvalidate(stream);
FSEventStreamRelease(stream);
}
}
for array in arrays {
unsafe { CFRelease(array) };
}
for string in strings {
unsafe { CFRelease(string) };
}
if let Ok(mut slot) = run_loop_slot.lock() {
*slot = 0;
}
}
fn poll_loop(roots: Vec<WatchRoot>, on_event: WatchCallback, stop: Arc<AtomicBool>) {
const FORCE_EMIT_INTERVAL: Duration = Duration::from_secs(1);
let mut fingerprints: HashMap<String, u64> = HashMap::new();
let mut last_emit: HashMap<String, std::time::Instant> = HashMap::new();
for root in &roots {
fingerprints.insert(root.mount.clone(), fingerprint_tree(&root.path));
last_emit.insert(root.mount.clone(), std::time::Instant::now());
}
while !stop.load(Ordering::Relaxed) {
std::thread::sleep(Duration::from_millis(220));
for root in &roots {
let next = fingerprint_tree(&root.path);
let prev = fingerprints.entry(root.mount.clone()).or_insert(next);
let changed = *prev != next;
let now = std::time::Instant::now();
let should_force_emit = last_emit
.get(&root.mount)
.is_some_and(|ts| now.saturating_duration_since(*ts) >= FORCE_EMIT_INTERVAL);
if changed {
*prev = next;
}
if changed || should_force_emit {
last_emit.insert(root.mount.clone(), now);
(on_event)(FileSystemEvent {
mount: root.mount.clone(),
path: root.path.clone(),
kind: FileSystemEventKind::Changed,
});
}
}
}
}
fn fingerprint_tree(root: &Path) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
let mut stack = vec![root.to_path_buf()];
while let Some(path) = stack.pop() {
let Ok(meta) = std::fs::metadata(&path) else {
continue;
};
let rel = path.strip_prefix(root).unwrap_or(&path);
rel.to_string_lossy().hash(&mut hasher);
meta.is_dir().hash(&mut hasher);
meta.len().hash(&mut hasher);
if let Ok(modified) = meta.modified() {
if let Ok(delta) = modified.duration_since(SystemTime::UNIX_EPOCH) {
delta.as_nanos().hash(&mut hasher);
}
}
if meta.is_dir() {
let Ok(entries) = std::fs::read_dir(&path) else {
continue;
};
let mut children = entries
.filter_map(|entry| entry.ok().map(|v| v.path()))
.collect::<Vec<_>>();
children.sort();
for child in children.into_iter().rev() {
stack.push(child);
}
}
}
hasher.finish()
}