diff --git a/Cargo.lock b/Cargo.lock index 4687b855..432311ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -272,15 +272,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" @@ -617,7 +608,6 @@ dependencies = [ "bitflags 2.10.0", "crossbeam-channel", "flume", - "fsevent-sys", "inotify", "insta", "kqueue", @@ -626,6 +616,8 @@ dependencies = [ "mio", "nix", "notify-types", + "objc2-core-foundation", + "objc2-core-services", "pretty_assertions", "serde_json", "tempfile", @@ -694,6 +686,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" diff --git a/Cargo.toml b/Cargo.toml index 7e7315ab..dbab314c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,8 @@ flume = "0.11.1" deser-hjson = "2.2.4" env_logger = "0.11.2" file-id = { version = "0.2.3", 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 4e5e035e..fcb1b5e6 100644 --- a/notify/Cargo.toml +++ b/notify/Cargo.toml @@ -10,7 +10,7 @@ categories = ["filesystem"] authors = [ "Félix Saparelli ", "Daniel Faust ", - "Aron Heinecke " + "Aron Heinecke ", ] rust-version.workspace = true edition.workspace = true @@ -21,7 +21,7 @@ repository.workspace = true default = ["macos_fsevent"] serde = ["notify-types/serde"] macos_kqueue = ["kqueue", "mio"] -macos_fsevent = ["fsevent-sys"] +macos_fsevent = ["objc2-core-foundation", "objc2-core-services"] serialization-compat-6 = ["notify-types/serialization-compat-6"] [dependencies] @@ -38,12 +38,32 @@ 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 } [target.'cfg(windows)'.dependencies] -windows-sys = { workspace = true, features = ["Win32_System_Threading", "Win32_Foundation", "Win32_Storage_FileSystem", "Win32_Security", "Win32_System_WindowsProgramming", "Win32_System_IO"] } +windows-sys = { workspace = true, features = [ + "Win32_System_Threading", + "Win32_Foundation", + "Win32_Storage_FileSystem", + "Win32_Security", + "Win32_System_WindowsProgramming", + "Win32_System_IO", +] } [target.'cfg(any(target_os="freebsd", target_os="openbsd", target_os = "netbsd", target_os = "dragonflybsd", target_os = "ios"))'.dependencies] kqueue.workspace = true diff --git a/notify/src/fsevent.rs b/notify/src/fsevent.rs index 5b79b2fa..186ff913 100644 --- a/notify/src/fsevent.rs +++ b/notify/src/fsevent.rs @@ -18,14 +18,13 @@ use crate::event::*; use crate::{ unbounded, Config, Error, EventHandler, PathsMut, RecursiveMode, Result, Sender, Watcher, }; -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; @@ -62,12 +61,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<()>)>, recursive_info: HashMap, } @@ -248,7 +247,7 @@ struct StreamContextInfo { } // Free the context when the stream created by `FSEventStreamCreate` is released. -extern "C" fn release_context(info: *const libc::c_void) { +unsafe 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 @@ -262,11 +261,6 @@ extern "C" fn release_context(info: *const libc::c_void) { } } -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 { @@ -293,9 +287,7 @@ impl PathsMut for FsEventPathsMut<'_> { impl FsEventWatcher { fn from_event_handler(event_handler: Arc>) -> Result { Ok(FsEventWatcher { - paths: unsafe { - cf::CFArrayCreateMutable(cf::kCFAllocatorDefault, 0, &cf::kCFTypeArrayCallBacks) - }, + paths: cf::CFMutableArray::empty(), since_when: fs::kFSEventStreamEventIdSinceNow, latency: 0.0, flags: fs::kFSEventStreamCreateFlagFileEvents | fs::kFSEventStreamCreateFlagNoDefer, @@ -332,49 +324,43 @@ impl FsEventWatcher { } if let Some((runloop, thread_handle)) = self.runloop.take() { - unsafe { - let runloop = runloop as *mut raw::c_void; - - 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) = (unsafe { path_to_cfstring_ref(path, &mut err) }) else { + if let Some(err) = NonNull::new(err) { + 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(idx as cf::CFIndex); } + } - cf::CFRelease(cf_path); - - for idx in to_remove.iter().rev() { - cf::CFArrayRemoveValueAtIndex(self.paths, *idx); - } + for idx in to_remove.iter().rev() { + 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 { @@ -392,26 +378,24 @@ impl FsEventWatcher { return Err(Error::path_not_found().add_path(path.into())); } let canonical_path = path.to_path_buf().canonicalize()?; - 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) = (unsafe { path_to_cfstring_ref(path, &mut err) }) else { + if let Some(err) = NonNull::new(err) { + 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.recursive_info .insert(canonical_path, recursive_mode.is_recursive()); Ok(()) } fn run(&mut self) -> Result<()> { - if unsafe { cf::CFArrayGetCount(self.paths) } == 0 { + if self.paths.is_empty() { // TODO: Reconstruct and add paths to error return Err(Error::path_not_found()); } @@ -430,31 +414,37 @@ impl FsEventWatcher { info: context as *mut libc::c_void, retain: None, release: Some(release_context), - copy_description: None, + copyDescription: None, }; let stream = unsafe { fs::FSEventStreamCreate( cf::kCFAllocatorDefault, - callback, - &stream_context, - self.paths, + Some(callback), + &stream_context as *const _ as *mut _, + 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(); @@ -466,21 +456,22 @@ impl FsEventWatcher { let stream = stream.0; unsafe { - let cur_runloop = cf::CFRunLoopGetCurrent(); + let cur_runloop = cf::CFRunLoop::current().unwrap(); + #[allow(deprecated)] fs::FSEventStreamScheduleWithRunLoop( stream, - cur_runloop, - cf::kCFRunLoopDefaultMode, + &cur_runloop, + cf::kCFRunLoopDefaultMode.unwrap(), ); fs::FSEventStreamStart(stream); // the calling to CFRunLoopRun will be terminated by CFRunLoopStop call in drop() rl_tx - .send(CFSendWrapper(cur_runloop)) + .send(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 @@ -505,13 +496,13 @@ impl FsEventWatcher { } } -extern "C" fn callback( - stream_ref: fs::FSEventStreamRef, +unsafe 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( @@ -526,14 +517,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 = &(*info).event_handler; @@ -543,7 +534,7 @@ unsafe fn callback_impl( .expect("Invalid UTF8 string."); let path = PathBuf::from(path); - let flag = *event_flags.add(p); + let flag = *event_flags.as_ptr().add(p); let flag = StreamFlags::from_bits(flag).unwrap_or_else(|| { panic!("Unable to decode StreamFlags: {}", flag); }); @@ -610,10 +601,44 @@ 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? +unsafe fn path_to_cfstring_ref( + source: &Path, + err: &mut *mut cf::CFError, +) -> Option> { + let url = cf::CFURL::from_file_path(source)?; + + let mut placeholder = url.absolute_url()?; + + let imaginary = cf::CFMutableArray::empty(); + + while !unsafe { placeholder.resource_is_reachable(err) } { + if let Some(child) = placeholder.last_path_component() { + imaginary.insert(0, &*child); } + + placeholder = cf::CFURL::new_copy_deleting_last_path_component(None, Some(&placeholder))?; + } + + let url = unsafe { cf::CFURL::new_file_reference_url(None, Some(&placeholder), err) }?; + + let mut placeholder = unsafe { cf::CFURL::new_file_path_url(None, Some(&url), err) }?; + + 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)]