diff --git a/Cargo.lock b/Cargo.lock index 8ca95b1f..c06c5667 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -312,15 +312,6 @@ dependencies = [ "spin", ] -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - [[package]] name = "futures" version = "0.3.31" @@ -670,6 +661,25 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "objc2-core-services" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583300ad934cba24ff5292aee751ecc070f7ca6b39a574cc21b7b5e588e06a0b" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -859,11 +869,12 @@ dependencies = [ "criterion", "crossbeam-channel", "flume", - "fsevent-sys", "inotify", "kqueue", "libc", "mio", + "objc2-core-foundation", + "objc2-core-services", "pretty_assertions", "rolldown-notify-types", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index d2add854..e65e718e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,7 +84,8 @@ flume = "0.11.1" deser-hjson = "2.2.4" tracing-subscriber = "0.3.20" rolldown-file-id = { version = "0.2.5", path = "file-id" } -fsevent-sys = "4.0.0" +objc2-core-foundation = { version = "0.3.2", default-features = false } +objc2-core-services = { version = "0.3.2", default-features = false } futures = "0.3.30" inotify = { version = "0.11.0", default-features = false } insta = "1.34.0" diff --git a/notify/Cargo.toml b/notify/Cargo.toml index b78e1aab..8fa0db8a 100644 --- a/notify/Cargo.toml +++ b/notify/Cargo.toml @@ -24,7 +24,7 @@ name = "notify" default = ["macos_fsevent"] serde = ["rolldown-notify-types/serde"] macos_kqueue = ["kqueue", "mio"] -macos_fsevent = ["fsevent-sys"] +macos_fsevent = ["objc2-core-foundation", "objc2-core-services"] serialization-compat-6 = ["rolldown-notify-types/serialization-compat-6"] [lints] @@ -44,7 +44,8 @@ mio.workspace = true [target.'cfg(target_os="macos")'.dependencies] bitflags.workspace = true -fsevent-sys = { workspace = true, optional = true } +objc2-core-foundation = { workspace = true, optional = true, features = ["std", "CFDate", "CFString", "CFArray", "CFRunLoop", "CFError", "CFURL"] } +objc2-core-services = { workspace = true, optional = true, features = ["std", "libc", "FSEvents"] } kqueue = { workspace = true, optional = true } mio = { workspace = true, optional = true } diff --git a/notify/src/fsevent.rs b/notify/src/fsevent.rs index 478da94b..cbbb8439 100644 --- a/notify/src/fsevent.rs +++ b/notify/src/fsevent.rs @@ -16,14 +16,13 @@ use crate::{Config, Error, EventHandler, PathsMut, Result, Sender, WatchMode, Watcher, unbounded}; use crate::{TargetMode, event::*}; -use fsevent_sys as fs; -use fsevent_sys::core_foundation as cf; +use objc2_core_foundation as cf; +use objc2_core_services as fs; use std::collections::HashMap; use std::ffi::CStr; use std::fmt; -use std::os::raw; use std::path::{Path, PathBuf}; -use std::ptr; +use std::ptr::{self, NonNull}; use std::sync::{Arc, Mutex}; use std::thread; @@ -60,12 +59,12 @@ bitflags::bitflags! { /// FSEvents-based `Watcher` implementation pub struct FsEventWatcher { - paths: cf::CFMutableArrayRef, + paths: cf::CFRetained>, since_when: fs::FSEventStreamEventId, latency: cf::CFTimeInterval, flags: fs::FSEventStreamCreateFlags, event_handler: Arc>, - runloop: Option<(cf::CFRunLoopRef, thread::JoinHandle<()>)>, + runloop: Option<(cf::CFRetained, thread::JoinHandle<()>)>, watches: HashMap, } @@ -247,7 +246,7 @@ struct StreamContextInfo { } // Free the context when the stream created by `FSEventStreamCreate` is released. -extern "C" fn release_context(info: *const libc::c_void) { +extern "C-unwind" fn release_context(info: *const libc::c_void) { // Safety: // - The [documentation] for `FSEventStreamContext` states that `release` is only // called when the stream is deallocated, so it is safe to convert `info` back into a @@ -259,11 +258,6 @@ extern "C" fn release_context(info: *const libc::c_void) { } } -unsafe extern "C" { - /// Indicates whether the run loop is waiting for an event. - fn CFRunLoopIsWaiting(runloop: cf::CFRunLoopRef) -> cf::Boolean; -} - struct FsEventPathsMut<'a>(&'a mut FsEventWatcher); impl<'a> FsEventPathsMut<'a> { fn new(watcher: &'a mut FsEventWatcher) -> Self { @@ -291,13 +285,7 @@ impl PathsMut for FsEventPathsMut<'_> { impl FsEventWatcher { fn from_event_handler(event_handler: Arc>) -> Self { FsEventWatcher { - paths: unsafe { - cf::CFArrayCreateMutable( - cf::kCFAllocatorDefault, - 0, - &raw const cf::kCFTypeArrayCallBacks, - ) - }, + paths: cf::CFMutableArray::empty(), since_when: fs::kFSEventStreamEventIdSinceNow, latency: 0.0, flags: fs::kFSEventStreamCreateFlagFileEvents | fs::kFSEventStreamCreateFlagNoDefer, @@ -332,49 +320,45 @@ impl FsEventWatcher { } if let Some((runloop, thread_handle)) = self.runloop.take() { - unsafe { - let runloop = runloop.cast::(); - - while CFRunLoopIsWaiting(runloop) == 0 { - thread::yield_now(); - } - - cf::CFRunLoopStop(runloop); + while !runloop.is_waiting() { + thread::yield_now(); } + runloop.stop(); + // Wait for the thread to shut down. thread_handle.join().expect("thread to shut down"); } } fn remove_path(&mut self, path: &Path) -> Result<()> { - let str_path = path.to_str().unwrap(); - unsafe { - let mut err: cf::CFErrorRef = ptr::null_mut(); - let cf_path = cf::str_path_to_cfstring_ref(str_path, &mut err); - if cf_path.is_null() { - if !err.is_null() { - cf::CFRelease(err as cf::CFRef); - } - return Err(Error::watch_not_found().add_path(path.into())); + let mut err: *mut cf::CFError = ptr::null_mut(); + let Some(cf_path) = path_to_cfstring_ref(path, &mut err) else { + if let Some(err) = NonNull::new(err) { + // SAFETY: ??? + let _ = unsafe { cf::CFRetained::from_raw(err) }; } + return Err(Error::watch_not_found().add_path(path.into())); + }; - let mut to_remove = Vec::new(); - for idx in 0..cf::CFArrayGetCount(self.paths) { - let item = cf::CFArrayGetValueAtIndex(self.paths, idx); - if cf::CFStringCompare(item, cf_path, cf::kCFCompareCaseInsensitive) - == cf::kCFCompareEqualTo - { - to_remove.push(idx); - } + let mut to_remove = Vec::new(); + for (idx, item) in self.paths.iter().enumerate() { + if item.compare( + Some(&cf_path), + cf::CFStringCompareFlags::CompareCaseInsensitive, + ) == cf::CFComparisonResult::CompareEqualTo + { + to_remove.push(cf::CFIndex::try_from(idx).unwrap()); } + } - cf::CFRelease(cf_path); - - for idx in to_remove.iter().rev() { - cf::CFArrayRemoveValueAtIndex(self.paths, *idx); - } + for idx in to_remove.iter().rev() { + // SAFETY: `the_array` is not `None` and the generic is correct, `idx` is in-bounds + unsafe { + cf::CFMutableArray::remove_value_at_index(Some(self.paths.as_opaque()), *idx); + }; } + let p = if let Ok(canonicalized_path) = path.canonicalize() { canonicalized_path } else { @@ -397,26 +381,26 @@ impl FsEventWatcher { .to_path_buf() .canonicalize() .unwrap_or(path.to_path_buf()); - let str_path = path.to_str().unwrap(); - unsafe { - let mut err: cf::CFErrorRef = ptr::null_mut(); - let cf_path = cf::str_path_to_cfstring_ref(str_path, &mut err); - if cf_path.is_null() { - // Most likely the directory was deleted, or permissions changed, - // while the above code was running. - cf::CFRelease(err as cf::CFRef); - return Err(Error::path_not_found().add_path(path.into())); + + let mut err: *mut cf::CFError = ptr::null_mut(); + let Some(cf_path) = path_to_cfstring_ref(path, &mut err) else { + if let Some(err) = NonNull::new(err) { + // SAFETY: ??? + let _ = unsafe { cf::CFRetained::from_raw(err) }; } - cf::CFArrayAppendValue(self.paths, cf_path); - cf::CFRelease(cf_path); - } + // Most likely the directory was deleted, or permissions changed, + // while the above code was running. + return Err(Error::path_not_found().add_path(path.into())); + }; + self.paths.append(&cf_path); + self.watches .insert(canonical_path, watch_mode.recursive_mode.is_recursive()); Ok(()) } fn run(&mut self) -> Result<()> { - if unsafe { cf::CFArrayGetCount(self.paths) } == 0 { + if self.paths.is_empty() { return Ok(()); } @@ -429,36 +413,40 @@ impl FsEventWatcher { recursive_info: self.watches.clone(), })); - let stream_context = fs::FSEventStreamContext { + let mut stream_context = fs::FSEventStreamContext { version: 0, info: context.cast::(), retain: None, release: Some(release_context), - copy_description: None, + copyDescription: None, }; let stream = unsafe { fs::FSEventStreamCreate( cf::kCFAllocatorDefault, - callback, - &raw const stream_context, - self.paths, + Some(callback), + &raw mut stream_context, + self.paths.as_opaque(), self.since_when, self.latency, self.flags, ) }; - // Wrapper to help send CFRef types across threads. - struct CFSendWrapper(cf::CFRef); - + // Wrapper to help send CFRunLoop types across threads. + struct CFRunLoopSendWrapper(cf::CFRetained); // Safety: - // - According to the Apple documentation, it's safe to move `CFRef`s across threads. + // - According to the Apple documentation, it's safe to move `CFRunLoop`s across threads. // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html - unsafe impl Send for CFSendWrapper {} + unsafe impl Send for CFRunLoopSendWrapper {} + + // Wrapper to help send FSEventStreamRef types across threads. + struct FSEventStreamSendWrapper(fs::FSEventStreamRef); + // SAFETY: Unclear? + unsafe impl Send for FSEventStreamSendWrapper {} // move into thread - let stream = CFSendWrapper(stream); + let stream = FSEventStreamSendWrapper(stream); // channel to pass runloop around let (rl_tx, rl_rx) = unbounded(); @@ -470,14 +458,16 @@ impl FsEventWatcher { let stream = stream.0; unsafe { - let cur_runloop = cf::CFRunLoopGetCurrent(); + // CFRunLoop::current() returns None only in OOM situations + let cur_runloop = cf::CFRunLoop::current().unwrap(); + #[expect(deprecated)] fs::FSEventStreamScheduleWithRunLoop( stream, - cur_runloop, - cf::kCFRunLoopDefaultMode, + &cur_runloop, + cf::kCFRunLoopDefaultMode.unwrap(), ); - if fs::FSEventStreamStart(stream) == 0 { + if !fs::FSEventStreamStart(stream) { fs::FSEventStreamInvalidate(stream); fs::FSEventStreamRelease(stream); rl_tx @@ -488,10 +478,10 @@ impl FsEventWatcher { // the calling to CFRunLoopRun will be terminated by CFRunLoopStop call in drop() rl_tx - .send(Ok(CFSendWrapper(cur_runloop))) + .send(Ok(CFRunLoopSendWrapper(cur_runloop))) .expect("Unable to send runloop to watcher"); - cf::CFRunLoopRun(); + cf::CFRunLoop::run(); fs::FSEventStreamStop(stream); // There are edge-cases, when many events are pending, // despite the stream being stopped, that the stream's @@ -499,7 +489,7 @@ impl FsEventWatcher { // is intended to prevent this. let event_id = fs::FSEventsGetCurrentEventId(); let device = fs::FSEventStreamGetDeviceBeingWatched(stream); - if fs::FSEventsPurgeEventsForDeviceUpToEventId(device, event_id) == 0 { + if !fs::FSEventsPurgeEventsForDeviceUpToEventId(device, event_id) { tracing::error!( "FSEventsPurgeEventsForDeviceUpToEventId failed for device {device}, event id {event_id}", ); @@ -521,13 +511,13 @@ impl FsEventWatcher { } } -extern "C" fn callback( - stream_ref: fs::FSEventStreamRef, +extern "C-unwind" fn callback( + stream_ref: fs::ConstFSEventStreamRef, info: *mut libc::c_void, - num_events: libc::size_t, // size_t numEvents - event_paths: *mut libc::c_void, // void *eventPaths - event_flags: *const fs::FSEventStreamEventFlags, // const FSEventStreamEventFlags eventFlags[] - event_ids: *const fs::FSEventStreamEventId, // const FSEventStreamEventId eventIds[] + num_events: libc::size_t, // size_t numEvents + event_paths: NonNull, // void *eventPaths + event_flags: NonNull, // const FSEventStreamEventFlags eventFlags[] + event_ids: NonNull, // const FSEventStreamEventId eventIds[] ) { unsafe { callback_impl( @@ -542,14 +532,14 @@ extern "C" fn callback( } unsafe fn callback_impl( - _stream_ref: fs::FSEventStreamRef, + _stream_ref: fs::ConstFSEventStreamRef, info: *mut libc::c_void, - num_events: libc::size_t, // size_t numEvents - event_paths: *mut libc::c_void, // void *eventPaths - event_flags: *const fs::FSEventStreamEventFlags, // const FSEventStreamEventFlags eventFlags[] - _event_ids: *const fs::FSEventStreamEventId, // const FSEventStreamEventId eventIds[] + num_events: libc::size_t, // size_t numEvents + event_paths: NonNull, // void *eventPaths + event_flags: NonNull, // const FSEventStreamEventFlags eventFlags[] + _event_ids: NonNull, // const FSEventStreamEventId eventIds[] ) { - let event_paths = event_paths as *const *const libc::c_char; + let event_paths = event_paths.as_ptr() as *const *const libc::c_char; let info = info as *const StreamContextInfo; let event_handler = unsafe { &(*info).event_handler }; @@ -559,7 +549,7 @@ unsafe fn callback_impl( .expect("Invalid UTF8 string."); let path = PathBuf::from(path); - let flag = unsafe { *event_flags.add(p) }; + let flag = unsafe { *event_flags.as_ptr().add(p) }; let flag = StreamFlags::from_bits(flag).unwrap_or_else(|| { panic!("Unable to decode StreamFlags: {flag}"); }); @@ -641,10 +631,50 @@ impl Watcher for FsEventWatcher { impl Drop for FsEventWatcher { fn drop(&mut self) { self.stop(); - unsafe { - cf::CFRelease(self.paths); + } +} + +/// Grabbed from . +/// +/// TODO: Could we simplify this? +fn path_to_cfstring_ref( + source: &Path, + err_ptr: &mut *mut cf::CFError, +) -> Option> { + let url = cf::CFURL::from_file_path(source)?; + let mut placeholder = url.absolute_url()?; + + let imaginary = cf::CFMutableArray::empty(); + // SAFETY: `err_ptr` is a valid pointer + while !unsafe { placeholder.resource_is_reachable(err_ptr) } { + if let Some(child) = placeholder.last_path_component() { + imaginary.insert(0, &*child); } + placeholder = cf::CFURL::new_copy_deleting_last_path_component(None, Some(&placeholder))?; + } + + // SAFETY: + // - `allocator` can be None (https://developer.apple.com/documentation/corefoundation/cfurlcreatefilereferenceurl(_:_:_:) says it can be NULL) + // - `url` is not None + // - `err_ptr` is a valid pointer + let url = unsafe { cf::CFURL::new_file_reference_url(None, Some(&placeholder), err_ptr) }?; + + // SAFETY: + // - `allocator` can be None (https://developer.apple.com/documentation/corefoundation/cfurlcreatefilepathurl(_:_:_:) says it can be NULL) + // - `url` is not None + // - `err_ptr` is a valid pointer + let mut placeholder = unsafe { cf::CFURL::new_file_path_url(None, Some(&url), err_ptr) }?; + + for component in imaginary { + placeholder = cf::CFURL::new_copy_appending_path_component( + None, + Some(&placeholder), + Some(&component), + false, + )?; } + + placeholder.file_system_path(cf::CFURLPathStyle::CFURLPOSIXPathStyle) } #[cfg(test)]