├── .editorconfig ├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── fsevent-async-demo.rs └── fsevent-demo.rs ├── fsevent-sys ├── .gitignore ├── Cargo.toml ├── LICENSE └── src │ ├── core_foundation.rs │ ├── fsevent.rs │ └── lib.rs ├── src └── lib.rs └── tests └── fsevent.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | [*.rs] 10 | charset = utf-8 11 | indent_style = space 12 | indent_size = 4 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | # Compiled files 3 | *.o 4 | *.so 5 | *.rlib 6 | *.dll 7 | 8 | # Executables 9 | *.exe 10 | 11 | # Generated by Cargo 12 | /target/ 13 | 14 | Cargo.lock 15 | 16 | # IntelliJ Files 17 | .idea/ 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | global: 3 | - secure: k+j9L/43VUG2xRCe1rcoch/WpGqIqx+egV5cHtHJo3pjujLkTRQJVe6Jmbeg8mSxt8HZVwli4sDatTcxvBUyhZ1JFrnXVnc70Q7LvzfcMpMTUepT1Mx5A7d3Qg7l96459xRhgNcPbx4JnHMWT+tX+R1Bip2Z4otEgfUpun93NQ0= 4 | language: rust 5 | script: 6 | - cargo build -v 7 | - cargo doc --no-deps -v 8 | - mv target/doc doc 9 | after_script: 10 | - curl http://www.rust-ci.org/artifacts/put?t=$RUSTCI_TOKEN | sh 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "fsevent" 4 | version = "2.1.1" 5 | authors = [ "Pierre Baillet " ] 6 | description = "Rust bindings to the fsevent-sys macOS API for file changes notifications" 7 | license = "MIT" 8 | repository = "https://github.com/octplane/fsevent-rust" 9 | edition = "2018" 10 | 11 | [dependencies] 12 | bitflags = "1" 13 | fsevent-sys = "4.0.0" 14 | # fsevent-sys = { path = "fsevent-sys" } 15 | 16 | [dev-dependencies] 17 | tempfile = "3" 18 | time = "0.2.9" 19 | 20 | [package.metadata.docs.rs] 21 | targets = ["x86_64-apple-darwin"] 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Pierre Baillet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FSEvent API for Rust 2 | 3 | Original Author: 4 | - Pierre Baillet [octplane](https://github.com/octplane) 5 | 6 | # Installation 7 | 8 | In `cargo.toml` 9 | 10 | ``` 11 | fsevent = "*" 12 | ``` 13 | 14 | # Usage 15 | 16 | cf examples/ folder. 17 | 18 | # Contributors 19 | 20 | - Mathieu Poumeyrol [kali](https://github.com/kali) 21 | - ShuYu Wang [andelf](https://github.com/andelf) 22 | - Jake Kerr [jakerr](https://github.com/jakerr) 23 | - Jorge Aparicio [japaric](https://github.com/japaric) 24 | - Markus Westerlind [Marwes](https://github.com/Marwes) 25 | - Bruce Mitchener [waywardmonkeys](https://github.com/waywardmonkeys) 26 | - Zac Brown [zacbrown](https://github.com/zacbrown) 27 | - mtak- [mtak-](https://github.com/mtak-) 28 | - Yuki Okushi [JohnTitor](https://github.com/JohnTitor) 29 | - Alexander Kjäll [alexanderkjall](https://github.com/alexanderkjall ) 30 | - Alphyr [a1phy](https://github.com/a1phyr) 31 | - Simon [simonchatts](https://github.com/simonchatts) 32 | - Erick Tryzelaar [erickt](https://github.com/erickt) 33 | -------------------------------------------------------------------------------- /examples/fsevent-async-demo.rs: -------------------------------------------------------------------------------- 1 | extern crate fsevent; 2 | 3 | use std::sync::mpsc::channel; 4 | use std::thread; 5 | 6 | #[cfg(not(target_os="macos"))] 7 | fn main() {} 8 | 9 | #[cfg(target_os="macos")] 10 | fn main() { 11 | let (sender, receiver) = channel(); 12 | 13 | let t = thread::spawn(move || { 14 | let mut fsevent = fsevent::FsEvent::new(vec![".".to_string()]); 15 | fsevent.observe_async(sender).unwrap(); 16 | std::thread::sleep(std::time::Duration::from_secs(5)); // sleep five seconds 17 | fsevent.shutdown_observe(); 18 | }); 19 | 20 | loop { 21 | let duration = std::time::Duration::from_secs(1); 22 | match receiver.recv_timeout(duration) { 23 | Ok(val) => println!("{:?}", val), 24 | Err(e) => match e { 25 | std::sync::mpsc::RecvTimeoutError::Disconnected => break, 26 | _ => {} // This is the case where nothing entered the channel buffer (no file mods). 27 | } 28 | } 29 | } 30 | 31 | t.join().unwrap(); 32 | } 33 | -------------------------------------------------------------------------------- /examples/fsevent-demo.rs: -------------------------------------------------------------------------------- 1 | extern crate fsevent; 2 | use std::sync::mpsc::channel; 3 | use std::thread; 4 | 5 | #[cfg(not(target_os="macos"))] 6 | fn main() {} 7 | 8 | #[cfg(target_os="macos")] 9 | fn main() { 10 | let (sender, receiver) = channel(); 11 | 12 | let _t = thread::spawn(move || { 13 | let fsevent = fsevent::FsEvent::new(vec![".".to_string()]); 14 | fsevent.observe(sender); 15 | }); 16 | 17 | loop { 18 | let val = receiver.recv(); 19 | println!("{:?}", val.unwrap()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /fsevent-sys/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | /target/ 3 | 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /fsevent-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fsevent-sys" 3 | version = "4.1.0" 4 | authors = ["Pierre Baillet "] 5 | description = "Rust bindings to the fsevent macOS API for file changes notifications" 6 | license = "MIT" 7 | repository = "https://github.com/octplane/fsevent-rust/tree/master/fsevent-sys" 8 | edition = "2018" 9 | 10 | [dependencies] 11 | libc = "0.2.68" 12 | 13 | [package.metadata.docs.rs] 14 | targets = ["x86_64-apple-darwin"] 15 | -------------------------------------------------------------------------------- /fsevent-sys/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Pierre Baillet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /fsevent-sys/src/core_foundation.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals, non_camel_case_types)] 2 | 3 | extern crate libc; 4 | 5 | use std::ffi::CString; 6 | use std::ptr; 7 | use std::str; 8 | 9 | pub type Boolean = ::std::os::raw::c_uchar; 10 | 11 | pub type CFRef = *mut ::std::os::raw::c_void; 12 | 13 | pub type CFIndex = ::std::os::raw::c_long; 14 | pub type CFTimeInterval = f64; 15 | pub type CFAbsoluteTime = CFTimeInterval; 16 | 17 | #[doc(hidden)] 18 | pub enum CFError {} 19 | 20 | pub type CFAllocatorRef = CFRef; 21 | pub type CFArrayRef = CFRef; 22 | pub type CFMutableArrayRef = CFRef; 23 | pub type CFURLRef = CFRef; 24 | pub type CFErrorRef = *mut CFError; 25 | pub type CFStringRef = CFRef; 26 | pub type CFRunLoopRef = CFRef; 27 | 28 | pub const NULL: CFRef = 0 as CFRef; 29 | pub const NULL_REF_PTR: *mut CFRef = 0 as *mut CFRef; 30 | 31 | pub type CFAllocatorRetainCallBack = 32 | extern "C" fn(*const ::std::os::raw::c_void) -> *const ::std::os::raw::c_void; 33 | pub type CFAllocatorReleaseCallBack = extern "C" fn(*const ::std::os::raw::c_void); 34 | pub type CFAllocatorCopyDescriptionCallBack = 35 | extern "C" fn(*const ::std::os::raw::c_void) -> *const CFStringRef; 36 | 37 | pub type CFURLPathStyle = CFIndex; 38 | 39 | pub const kCFAllocatorDefault: CFAllocatorRef = NULL; 40 | pub const kCFURLPOSIXPathStyle: CFURLPathStyle = 0; 41 | pub const kCFURLHFSPathStyle: CFURLPathStyle = 1; 42 | pub const kCFURLWindowsPathStyle: CFURLPathStyle = 2; 43 | 44 | pub const kCFStringEncodingUTF8: CFStringEncoding = 0x08000100; 45 | pub type CFStringEncoding = u32; 46 | 47 | pub const kCFCompareEqualTo: CFIndex = 0; 48 | pub type CFComparisonResult = CFIndex; 49 | 50 | // MacOS uses Case Insensitive path 51 | pub const kCFCompareCaseInsensitive: CFStringCompareFlags = 1; 52 | pub type CFStringCompareFlags = ::std::os::raw::c_ulong; 53 | 54 | pub type CFArrayRetainCallBack = 55 | extern "C" fn(CFAllocatorRef, *const ::std::os::raw::c_void) -> *const ::std::os::raw::c_void; 56 | pub type CFArrayReleaseCallBack = extern "C" fn(CFAllocatorRef, *const ::std::os::raw::c_void); 57 | pub type CFArrayCopyDescriptionCallBack = 58 | extern "C" fn(*const ::std::os::raw::c_void) -> CFStringRef; 59 | pub type CFArrayEqualCallBack = 60 | extern "C" fn(*const ::std::os::raw::c_void, *const ::std::os::raw::c_void) -> Boolean; 61 | 62 | #[repr(C)] 63 | pub struct CFArrayCallBacks { 64 | version: CFIndex, 65 | retain: Option, 66 | release: Option, 67 | cp: Option, 68 | equal: Option, 69 | } 70 | //impl Clone for CFArrayCallBacks { } 71 | 72 | #[link(name = "CoreFoundation", kind = "framework")] 73 | extern "C" { 74 | pub static kCFTypeArrayCallBacks: CFArrayCallBacks; 75 | pub static kCFRunLoopDefaultMode: CFStringRef; 76 | 77 | pub fn CFRelease(res: CFRef); 78 | pub fn CFShow(res: CFRef); 79 | pub fn CFCopyDescription(cf: CFRef) -> CFStringRef; 80 | 81 | pub fn CFRunLoopRun(); 82 | pub fn CFRunLoopStop(run_loop: CFRunLoopRef); 83 | pub fn CFRunLoopGetCurrent() -> CFRunLoopRef; 84 | 85 | pub fn CFArrayCreateMutable( 86 | allocator: CFRef, 87 | capacity: CFIndex, 88 | callbacks: *const CFArrayCallBacks, 89 | ) -> CFMutableArrayRef; 90 | pub fn CFArrayInsertValueAtIndex(arr: CFMutableArrayRef, position: CFIndex, element: CFRef); 91 | pub fn CFArrayAppendValue(aff: CFMutableArrayRef, element: CFRef); 92 | pub fn CFArrayGetCount(arr: CFMutableArrayRef) -> CFIndex; 93 | pub fn CFArrayGetValueAtIndex(arr: CFMutableArrayRef, index: CFIndex) -> CFRef; 94 | 95 | pub fn CFURLCreateFileReferenceURL( 96 | allocator: CFRef, 97 | url: CFURLRef, 98 | err: *mut CFErrorRef, 99 | ) -> CFURLRef; 100 | pub fn CFURLCreateFilePathURL( 101 | allocator: CFRef, 102 | url: CFURLRef, 103 | err: *mut CFErrorRef, 104 | ) -> CFURLRef; 105 | pub fn CFURLCreateFromFileSystemRepresentation( 106 | allocator: CFRef, 107 | path: *const ::std::os::raw::c_char, 108 | len: CFIndex, 109 | is_directory: bool, 110 | ) -> CFURLRef; 111 | pub fn CFURLCopyAbsoluteURL(res: CFURLRef) -> CFURLRef; 112 | pub fn CFURLCopyLastPathComponent(res: CFURLRef) -> CFStringRef; 113 | pub fn CFURLCreateCopyDeletingLastPathComponent(allocator: CFRef, url: CFURLRef) -> CFURLRef; 114 | pub fn CFURLCreateCopyAppendingPathComponent( 115 | allocation: CFRef, 116 | url: CFURLRef, 117 | path: CFStringRef, 118 | is_directory: bool, 119 | ) -> CFURLRef; 120 | pub fn CFURLCopyFileSystemPath(anUrl: CFURLRef, path_style: CFURLPathStyle) -> CFStringRef; 121 | 122 | pub fn CFURLResourceIsReachable(res: CFURLRef, err: *mut CFErrorRef) -> bool; 123 | 124 | pub fn CFShowStr(str: CFStringRef); 125 | pub fn CFStringGetCString( 126 | theString: CFStringRef, 127 | buffer: *mut ::std::os::raw::c_char, 128 | buffer_size: CFIndex, 129 | encoding: CFStringEncoding, 130 | ) -> bool; 131 | pub fn CFStringGetCStringPtr( 132 | theString: CFStringRef, 133 | encoding: CFStringEncoding, 134 | ) -> *const ::std::os::raw::c_char; 135 | pub fn CFStringCreateWithCString( 136 | alloc: CFRef, 137 | source: *const ::std::os::raw::c_char, 138 | encoding: CFStringEncoding, 139 | ) -> CFStringRef; 140 | 141 | pub fn CFStringCompare( 142 | theString1: CFStringRef, 143 | theString2: CFStringRef, 144 | compareOptions: CFStringCompareFlags, 145 | ) -> CFComparisonResult; 146 | pub fn CFArrayRemoveValueAtIndex(theArray: CFMutableArrayRef, idx: CFIndex); 147 | } 148 | 149 | pub unsafe fn str_path_to_cfstring_ref(source: &str, err: &mut CFErrorRef) -> CFStringRef { 150 | let c_path = CString::new(source).unwrap(); 151 | let c_len = libc::strlen(c_path.as_ptr()); 152 | let mut url = CFURLCreateFromFileSystemRepresentation( 153 | kCFAllocatorDefault, 154 | c_path.as_ptr(), 155 | c_len as CFIndex, 156 | false, 157 | ); 158 | if url.is_null() { 159 | return ptr::null_mut(); 160 | } 161 | 162 | let mut placeholder = CFURLCopyAbsoluteURL(url); 163 | CFRelease(url); 164 | if placeholder.is_null() { 165 | return ptr::null_mut(); 166 | } 167 | 168 | let mut imaginary: CFRef = ptr::null_mut(); 169 | 170 | while !CFURLResourceIsReachable(placeholder, ptr::null_mut()) { 171 | if imaginary.is_null() { 172 | imaginary = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks); 173 | if imaginary.is_null() { 174 | CFRelease(placeholder); 175 | return ptr::null_mut(); 176 | } 177 | } 178 | 179 | let child = CFURLCopyLastPathComponent(placeholder); 180 | CFArrayInsertValueAtIndex(imaginary, 0, child); 181 | CFRelease(child); 182 | 183 | url = CFURLCreateCopyDeletingLastPathComponent(kCFAllocatorDefault, placeholder); 184 | CFRelease(placeholder); 185 | placeholder = url; 186 | } 187 | 188 | url = CFURLCreateFileReferenceURL(kCFAllocatorDefault, placeholder, err); 189 | CFRelease(placeholder); 190 | if url.is_null() { 191 | if !imaginary.is_null() { 192 | CFRelease(imaginary); 193 | } 194 | return ptr::null_mut(); 195 | } 196 | 197 | placeholder = CFURLCreateFilePathURL(kCFAllocatorDefault, url, err); 198 | CFRelease(url); 199 | if placeholder.is_null() { 200 | if !imaginary.is_null() { 201 | CFRelease(imaginary); 202 | } 203 | return ptr::null_mut(); 204 | } 205 | 206 | if !imaginary.is_null() { 207 | let mut count = 0; 208 | while count < CFArrayGetCount(imaginary) { 209 | let component = CFArrayGetValueAtIndex(imaginary, count); 210 | url = CFURLCreateCopyAppendingPathComponent( 211 | kCFAllocatorDefault, 212 | placeholder, 213 | component, 214 | false, 215 | ); 216 | CFRelease(placeholder); 217 | if url.is_null() { 218 | CFRelease(imaginary); 219 | return ptr::null_mut(); 220 | } 221 | placeholder = url; 222 | count += 1; 223 | } 224 | CFRelease(imaginary); 225 | } 226 | 227 | let cf_path = CFURLCopyFileSystemPath(placeholder, kCFURLPOSIXPathStyle); 228 | CFRelease(placeholder); 229 | cf_path 230 | } 231 | -------------------------------------------------------------------------------- /fsevent-sys/src/fsevent.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals, non_camel_case_types)] 2 | 3 | use crate::core_foundation::{ 4 | Boolean, CFAbsoluteTime, CFAllocatorCopyDescriptionCallBack, CFAllocatorRef, 5 | CFAllocatorReleaseCallBack, CFAllocatorRetainCallBack, CFArrayRef, CFIndex, CFRunLoopRef, 6 | CFStringRef, CFTimeInterval, 7 | }; 8 | use libc::dev_t; 9 | use std::os::raw::{c_uint, c_void}; 10 | 11 | pub type FSEventStreamRef = *mut c_void; 12 | pub type ConstFSEventStreamRef = *const c_void; 13 | 14 | pub type FSEventStreamCallback = extern "C" fn( 15 | FSEventStreamRef, // ConstFSEventStreamRef streamRef 16 | *mut c_void, // void *clientCallBackInfo 17 | usize, // size_t numEvents 18 | *mut c_void, // void *eventPaths 19 | *const FSEventStreamEventFlags, // const FSEventStreamEventFlags eventFlags[] 20 | *const FSEventStreamEventId, // const FSEventStreamEventId eventIds[] 21 | ); 22 | 23 | pub type FSEventStreamEventId = u64; 24 | 25 | pub type FSEventStreamCreateFlags = c_uint; 26 | 27 | pub type FSEventStreamEventFlags = c_uint; 28 | 29 | pub const kFSEventStreamEventIdSinceNow: FSEventStreamEventId = 0xFFFFFFFFFFFFFFFF; 30 | 31 | pub const kFSEventStreamCreateFlagNone: FSEventStreamCreateFlags = 0x00000000; 32 | pub const kFSEventStreamCreateFlagUseCFTypes: FSEventStreamCreateFlags = 0x00000001; 33 | pub const kFSEventStreamCreateFlagNoDefer: FSEventStreamCreateFlags = 0x00000002; 34 | pub const kFSEventStreamCreateFlagWatchRoot: FSEventStreamCreateFlags = 0x00000004; 35 | pub const kFSEventStreamCreateFlagIgnoreSelf: FSEventStreamCreateFlags = 0x00000008; 36 | pub const kFSEventStreamCreateFlagFileEvents: FSEventStreamCreateFlags = 0x00000010; 37 | pub const kFSEventStreamCreateFlagMarkSelf: FSEventStreamCreateFlags = 0x00000020; 38 | pub const kFSEventStreamCreateFlagUseExtendedData: FSEventStreamCreateFlags = 0x00000040; 39 | pub const kFSEventStreamCreateFlagFullHistory: FSEventStreamCreateFlags = 0x00000080; 40 | 41 | pub const kFSEventStreamEventFlagNone: FSEventStreamEventFlags = 0x00000000; 42 | pub const kFSEventStreamEventFlagMustScanSubDirs: FSEventStreamEventFlags = 0x00000001; 43 | pub const kFSEventStreamEventFlagUserDropped: FSEventStreamEventFlags = 0x00000002; 44 | pub const kFSEventStreamEventFlagKernelDropped: FSEventStreamEventFlags = 0x00000004; 45 | pub const kFSEventStreamEventFlagEventIdsWrapped: FSEventStreamEventFlags = 0x00000008; 46 | pub const kFSEventStreamEventFlagHistoryDone: FSEventStreamEventFlags = 0x00000010; 47 | pub const kFSEventStreamEventFlagRootChanged: FSEventStreamEventFlags = 0x00000020; 48 | pub const kFSEventStreamEventFlagMount: FSEventStreamEventFlags = 0x00000040; 49 | pub const kFSEventStreamEventFlagUnmount: FSEventStreamEventFlags = 0x00000080; 50 | pub const kFSEventStreamEventFlagItemCreated: FSEventStreamEventFlags = 0x00000100; 51 | pub const kFSEventStreamEventFlagItemRemoved: FSEventStreamEventFlags = 0x00000200; 52 | pub const kFSEventStreamEventFlagItemInodeMetaMod: FSEventStreamEventFlags = 0x00000400; 53 | pub const kFSEventStreamEventFlagItemRenamed: FSEventStreamEventFlags = 0x00000800; 54 | pub const kFSEventStreamEventFlagItemModified: FSEventStreamEventFlags = 0x00001000; 55 | pub const kFSEventStreamEventFlagItemFinderInfoMod: FSEventStreamEventFlags = 0x00002000; 56 | pub const kFSEventStreamEventFlagItemChangeOwner: FSEventStreamEventFlags = 0x00004000; 57 | pub const kFSEventStreamEventFlagItemXattrMod: FSEventStreamEventFlags = 0x00008000; 58 | pub const kFSEventStreamEventFlagItemIsFile: FSEventStreamEventFlags = 0x00010000; 59 | pub const kFSEventStreamEventFlagItemIsDir: FSEventStreamEventFlags = 0x00020000; 60 | pub const kFSEventStreamEventFlagItemIsSymlink: FSEventStreamEventFlags = 0x00040000; 61 | pub const kFSEventStreamEventFlagOwnEvent: FSEventStreamEventFlags = 0x00080000; 62 | pub const kFSEventStreamEventFlagItemIsHardlink: FSEventStreamEventFlags = 0x00100000; 63 | pub const kFSEventStreamEventFlagItemIsLastHardlink: FSEventStreamEventFlags = 0x00200000; 64 | pub const kFSEventStreamEventFlagItemCloned: FSEventStreamEventFlags = 0x00400000; 65 | 66 | #[repr(C)] 67 | pub struct FSEventStreamContext { 68 | pub version: CFIndex, 69 | pub info: *mut c_void, 70 | pub retain: Option, 71 | pub release: Option, 72 | pub copy_description: Option, 73 | } 74 | 75 | // https://developer.apple.com/documentation/coreservices/file_system_events 76 | #[link(name = "CoreServices", kind = "framework")] 77 | extern "C" { 78 | pub fn FSEventStreamCopyDescription(stream_ref: ConstFSEventStreamRef) -> CFStringRef; 79 | pub fn FSEventStreamCopyPathsBeingWatched(streamRef: ConstFSEventStreamRef) -> CFArrayRef; 80 | pub fn FSEventStreamCreate( 81 | allocator: CFAllocatorRef, 82 | callback: FSEventStreamCallback, 83 | context: *const FSEventStreamContext, 84 | pathsToWatch: CFArrayRef, 85 | sinceWhen: FSEventStreamEventId, 86 | latency: CFTimeInterval, 87 | flags: FSEventStreamCreateFlags, 88 | ) -> FSEventStreamRef; 89 | pub fn FSEventStreamCreateRelativeToDevice( 90 | allocator: CFAllocatorRef, 91 | callback: FSEventStreamCallback, 92 | context: *const FSEventStreamContext, 93 | deviceToWatch: dev_t, 94 | pathsToWatchRelativeToDevice: CFArrayRef, 95 | sinceWhen: FSEventStreamEventId, 96 | latency: CFTimeInterval, 97 | flags: FSEventStreamCreateFlags, 98 | ) -> FSEventStreamRef; 99 | pub fn FSEventStreamFlushAsync(stream_ref: FSEventStreamRef) -> FSEventStreamEventId; 100 | pub fn FSEventStreamFlushSync(streamRef: FSEventStreamRef); 101 | pub fn FSEventStreamGetDeviceBeingWatched(stream_ref: ConstFSEventStreamRef) -> dev_t; 102 | pub fn FSEventStreamGetLatestEventId(stream_ref: ConstFSEventStreamRef) 103 | -> FSEventStreamEventId; 104 | pub fn FSEventStreamInvalidate(stream_ref: FSEventStreamRef); 105 | pub fn FSEventStreamRelease(stream_ref: FSEventStreamRef); 106 | pub fn FSEventStreamRetain(stream_ref: FSEventStreamRef); 107 | pub fn FSEventStreamScheduleWithRunLoop( 108 | stream_ref: FSEventStreamRef, 109 | run_loop: CFRunLoopRef, 110 | run_loop_mode: CFStringRef, 111 | ); 112 | // pub fn FSEventStreamSetDispatchQueue(streamRef: FSEventStreamRef, q: DispatchQueue); 113 | pub fn FSEventStreamSetExclusionPaths( 114 | stream_ref: FSEventStreamRef, 115 | paths_to_exclude: CFArrayRef, 116 | ) -> Boolean; 117 | pub fn FSEventStreamShow(stream_ref: FSEventStreamRef); 118 | pub fn FSEventStreamStart(stream_ref: FSEventStreamRef) -> Boolean; 119 | pub fn FSEventStreamStop(stream_ref: FSEventStreamRef); 120 | pub fn FSEventStreamUnscheduleFromRunLoop( 121 | stream_ref: FSEventStreamRef, 122 | run_loop: CFRunLoopRef, 123 | run_loop_mode: CFStringRef, 124 | ); 125 | // pub fn FSEventsCopyUUIDForDevice(dev: dev_t) -> CFUUID; 126 | pub fn FSEventsGetCurrentEventId() -> FSEventStreamEventId; 127 | pub fn FSEventsGetLastEventIdForDeviceBeforeTime( 128 | dev: dev_t, 129 | time: CFAbsoluteTime, 130 | ) -> FSEventStreamEventId; 131 | pub fn FSEventsPurgeEventsForDeviceUpToEventId( 132 | dev: dev_t, 133 | eventId: FSEventStreamEventId, 134 | ) -> Boolean; 135 | } 136 | -------------------------------------------------------------------------------- /fsevent-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_os = "macos")] 2 | #![cfg_attr(feature = "cargo-clippy", allow(unreadable_literal))] 3 | 4 | pub mod core_foundation; 5 | mod fsevent; 6 | 7 | pub use fsevent::*; 8 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_os = "macos")] 2 | #![deny( 3 | trivial_numeric_casts, 4 | unstable_features, 5 | unused_import_braces, 6 | unused_qualifications 7 | )] 8 | #![cfg_attr(feature = "cargo-clippy", allow(unreadable_literal))] 9 | 10 | #[macro_use] 11 | extern crate bitflags; 12 | 13 | extern crate fsevent_sys as fsevent; 14 | 15 | use fsevent as fs; 16 | use fsevent::core_foundation as cf; 17 | 18 | use std::ffi::CStr; 19 | use std::fmt::{Display, Formatter}; 20 | use std::os::raw::{c_char, c_void}; 21 | use std::ptr; 22 | 23 | use std::sync::mpsc::Sender; 24 | 25 | // Helper to send the runloop from an observer thread. 26 | struct CFRunLoopSendWrapper(cf::CFRunLoopRef); 27 | 28 | // Safety: According to the Apple documentation, it is safe to send CFRef types across threads. 29 | // 30 | // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html 31 | unsafe impl Send for CFRunLoopSendWrapper {} 32 | 33 | pub struct FsEvent { 34 | paths: Vec, 35 | since_when: fs::FSEventStreamEventId, 36 | latency: cf::CFTimeInterval, 37 | flags: fs::FSEventStreamCreateFlags, 38 | runloop: Option, 39 | } 40 | 41 | #[derive(Debug)] 42 | pub struct Event { 43 | pub event_id: u64, 44 | pub flag: StreamFlags, 45 | pub path: String, 46 | } 47 | 48 | // Synchronize with 49 | // /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/Headers/FSEvents.h 50 | bitflags! { 51 | #[repr(C)] 52 | pub struct StreamFlags: u32 { 53 | const NONE = 0x00000000; 54 | const MUST_SCAN_SUBDIRS = 0x00000001; 55 | const USER_DROPPED = 0x00000002; 56 | const KERNEL_DROPPED = 0x00000004; 57 | const IDS_WRAPPED = 0x00000008; 58 | const HISTORY_DONE = 0x00000010; 59 | const ROOT_CHANGED = 0x00000020; 60 | const MOUNT = 0x00000040; 61 | const UNMOUNT = 0x00000080; 62 | const ITEM_CREATED = 0x00000100; 63 | const ITEM_REMOVED = 0x00000200; 64 | const INODE_META_MOD = 0x00000400; 65 | const ITEM_RENAMED = 0x00000800; 66 | const ITEM_MODIFIED = 0x00001000; 67 | const FINDER_INFO_MOD = 0x00002000; 68 | const ITEM_CHANGE_OWNER = 0x00004000; 69 | const ITEM_XATTR_MOD = 0x00008000; 70 | const IS_FILE = 0x00010000; 71 | const IS_DIR = 0x00020000; 72 | const IS_SYMLINK = 0x00040000; 73 | const OWN_EVENT = 0x00080000; 74 | const IS_HARDLINK = 0x00100000; 75 | const IS_LAST_HARDLINK = 0x00200000; 76 | const ITEM_CLONED = 0x400000; 77 | } 78 | } 79 | 80 | impl Display for StreamFlags { 81 | fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { 82 | if self.contains(StreamFlags::MUST_SCAN_SUBDIRS) { 83 | let _d = write!(f, "MUST_SCAN_SUBDIRS "); 84 | } 85 | if self.contains(StreamFlags::USER_DROPPED) { 86 | let _d = write!(f, "USER_DROPPED "); 87 | } 88 | if self.contains(StreamFlags::KERNEL_DROPPED) { 89 | let _d = write!(f, "KERNEL_DROPPED "); 90 | } 91 | if self.contains(StreamFlags::IDS_WRAPPED) { 92 | let _d = write!(f, "IDS_WRAPPED "); 93 | } 94 | if self.contains(StreamFlags::HISTORY_DONE) { 95 | let _d = write!(f, "HISTORY_DONE "); 96 | } 97 | if self.contains(StreamFlags::ROOT_CHANGED) { 98 | let _d = write!(f, "ROOT_CHANGED "); 99 | } 100 | if self.contains(StreamFlags::MOUNT) { 101 | let _d = write!(f, "MOUNT "); 102 | } 103 | if self.contains(StreamFlags::UNMOUNT) { 104 | let _d = write!(f, "UNMOUNT "); 105 | } 106 | if self.contains(StreamFlags::ITEM_CREATED) { 107 | let _d = write!(f, "ITEM_CREATED "); 108 | } 109 | if self.contains(StreamFlags::ITEM_REMOVED) { 110 | let _d = write!(f, "ITEM_REMOVED "); 111 | } 112 | if self.contains(StreamFlags::INODE_META_MOD) { 113 | let _d = write!(f, "INODE_META_MOD "); 114 | } 115 | if self.contains(StreamFlags::ITEM_RENAMED) { 116 | let _d = write!(f, "ITEM_RENAMED "); 117 | } 118 | if self.contains(StreamFlags::ITEM_MODIFIED) { 119 | let _d = write!(f, "ITEM_MODIFIED "); 120 | } 121 | if self.contains(StreamFlags::FINDER_INFO_MOD) { 122 | let _d = write!(f, "FINDER_INFO_MOD "); 123 | } 124 | if self.contains(StreamFlags::ITEM_CHANGE_OWNER) { 125 | let _d = write!(f, "ITEM_CHANGE_OWNER "); 126 | } 127 | if self.contains(StreamFlags::ITEM_XATTR_MOD) { 128 | let _d = write!(f, "ITEM_XATTR_MOD "); 129 | } 130 | if self.contains(StreamFlags::IS_FILE) { 131 | let _d = write!(f, "IS_FILE "); 132 | } 133 | if self.contains(StreamFlags::IS_DIR) { 134 | let _d = write!(f, "IS_DIR "); 135 | } 136 | if self.contains(StreamFlags::IS_SYMLINK) { 137 | let _d = write!(f, "IS_SYMLINK "); 138 | } 139 | if self.contains(StreamFlags::OWN_EVENT) { 140 | let _d = write!(f, "OWN_EVENT "); 141 | } 142 | if self.contains(StreamFlags::IS_LAST_HARDLINK) { 143 | let _d = write!(f, "IS_LAST_HARDLINK "); 144 | } 145 | if self.contains(StreamFlags::IS_HARDLINK) { 146 | let _d = write!(f, "IS_HARDLINK "); 147 | } 148 | if self.contains(StreamFlags::ITEM_CLONED) { 149 | let _d = write!(f, "ITEM_CLONED "); 150 | } 151 | write!(f, "") 152 | } 153 | } 154 | 155 | fn default_stream_context(event_sender: *const Sender) -> fs::FSEventStreamContext { 156 | let ptr = event_sender as *mut c_void; 157 | fs::FSEventStreamContext { 158 | version: 0, 159 | info: ptr, 160 | retain: None, 161 | release: None, 162 | copy_description: None, 163 | } 164 | } 165 | 166 | pub type Result = std::result::Result; 167 | 168 | #[derive(Debug)] 169 | pub struct Error { 170 | msg: String, 171 | } 172 | 173 | impl std::error::Error for Error { 174 | fn description(&self) -> &str { 175 | &self.msg 176 | } 177 | } 178 | 179 | impl Display for Error { 180 | fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { 181 | self.msg.fmt(f) 182 | } 183 | } 184 | 185 | impl From for Error { 186 | fn from(err: std::sync::mpsc::RecvTimeoutError) -> Error { 187 | Self { 188 | msg: err.to_string(), 189 | } 190 | } 191 | } 192 | 193 | impl FsEvent { 194 | pub fn new(paths: Vec) -> Self { 195 | Self { 196 | paths, 197 | since_when: fs::kFSEventStreamEventIdSinceNow, 198 | latency: 0.0, 199 | flags: fs::kFSEventStreamCreateFlagFileEvents | fs::kFSEventStreamCreateFlagNoDefer, 200 | runloop: None, 201 | } 202 | } 203 | 204 | // https://github.com/thibaudgg/rb-fsevent/blob/master/ext/fsevent_watch/main.c 205 | pub fn append_path(&mut self, source: &str) -> Result<()> { 206 | self.paths.push(source.to_string()); 207 | Ok(()) 208 | } 209 | 210 | fn build_native_paths(&self) -> Result { 211 | let native_paths = unsafe { 212 | cf::CFArrayCreateMutable(cf::kCFAllocatorDefault, 0, &cf::kCFTypeArrayCallBacks) 213 | }; 214 | 215 | if native_paths == std::ptr::null_mut() { 216 | Err(Error { 217 | msg: "Unable to allocate CFMutableArrayRef".to_string(), 218 | }) 219 | } else { 220 | for path in &self.paths { 221 | unsafe { 222 | let mut err = ptr::null_mut(); 223 | let cf_path = cf::str_path_to_cfstring_ref(path, &mut err); 224 | if !err.is_null() { 225 | let cf_str = cf::CFCopyDescription(err as cf::CFRef); 226 | let mut buf = [0; 1024]; 227 | cf::CFStringGetCString( 228 | cf_str, 229 | buf.as_mut_ptr(), 230 | buf.len() as cf::CFIndex, 231 | cf::kCFStringEncodingUTF8, 232 | ); 233 | return Err(Error { 234 | msg: CStr::from_ptr(buf.as_ptr()) 235 | .to_str() 236 | .unwrap_or("Unknown error") 237 | .to_string(), 238 | }); 239 | } else { 240 | cf::CFArrayAppendValue(native_paths, cf_path); 241 | cf::CFRelease(cf_path); 242 | } 243 | } 244 | } 245 | 246 | Ok(native_paths) 247 | } 248 | } 249 | 250 | fn internal_observe( 251 | since_when: fs::FSEventStreamEventId, 252 | latency: cf::CFTimeInterval, 253 | flags: fs::FSEventStreamCreateFlags, 254 | paths: cf::CFMutableArrayRef, 255 | event_sender: Sender, 256 | runloop_sender: Option>, 257 | ) -> Result<()> { 258 | let stream_context = default_stream_context(&event_sender); 259 | let paths = paths.into(); 260 | 261 | unsafe { 262 | let stream = fs::FSEventStreamCreate( 263 | cf::kCFAllocatorDefault, 264 | callback, 265 | &stream_context, 266 | paths, 267 | since_when, 268 | latency, 269 | flags, 270 | ); 271 | 272 | if let Some(ret_tx) = runloop_sender { 273 | let runloop = CFRunLoopSendWrapper(cf::CFRunLoopGetCurrent()); 274 | ret_tx 275 | .send(runloop) 276 | .expect("unabe to send CFRunLoopRef"); 277 | } 278 | 279 | fs::FSEventStreamScheduleWithRunLoop( 280 | stream, 281 | cf::CFRunLoopGetCurrent(), 282 | cf::kCFRunLoopDefaultMode, 283 | ); 284 | 285 | fs::FSEventStreamStart(stream); 286 | cf::CFRunLoopRun(); 287 | 288 | fs::FSEventStreamFlushSync(stream); 289 | fs::FSEventStreamStop(stream); 290 | } 291 | 292 | Ok(()) 293 | } 294 | 295 | pub fn observe(&self, event_sender: Sender) { 296 | let native_paths = self 297 | .build_native_paths() 298 | .expect("Unable to build CFMutableArrayRef of watched paths."); 299 | Self::internal_observe( 300 | self.since_when, 301 | self.latency, 302 | self.flags, 303 | native_paths, 304 | event_sender, 305 | None, 306 | ) 307 | .unwrap(); 308 | } 309 | 310 | pub fn observe_async(&mut self, event_sender: Sender) -> Result<()> { 311 | let (ret_tx, ret_rx) = std::sync::mpsc::channel(); 312 | let native_paths = self.build_native_paths()?; 313 | 314 | struct CFMutableArraySendWrapper(cf::CFMutableArrayRef); 315 | 316 | // Safety 317 | // - See comment on `CFRunLoopSendWrapper 318 | unsafe impl Send for CFMutableArraySendWrapper {} 319 | 320 | let safe_native_paths = CFMutableArraySendWrapper(native_paths); 321 | 322 | let since_when = self.since_when; 323 | let latency = self.latency; 324 | let flags = self.flags; 325 | 326 | std::thread::spawn(move || { 327 | Self::internal_observe( 328 | since_when, 329 | latency, 330 | flags, 331 | safe_native_paths.0, 332 | event_sender, 333 | Some(ret_tx), 334 | ) 335 | }); 336 | 337 | self.runloop = Some(ret_rx.recv().unwrap().0); 338 | 339 | Ok(()) 340 | } 341 | 342 | // Shut down the event stream. 343 | pub fn shutdown_observe(&mut self) { 344 | if let Some(runloop) = self.runloop.take() { 345 | unsafe { cf::CFRunLoopStop(runloop); } 346 | } 347 | } 348 | } 349 | 350 | extern "C" fn callback( 351 | _stream_ref: fs::FSEventStreamRef, 352 | info: *mut c_void, 353 | num_events: usize, // size_t numEvents 354 | event_paths: *mut c_void, // void *eventPaths 355 | event_flags: *const fs::FSEventStreamEventFlags, // const FSEventStreamEventFlags eventFlags[] 356 | event_ids: *const fs::FSEventStreamEventId, // const FSEventStreamEventId eventIds[] 357 | ) { 358 | unsafe { 359 | let event_paths = event_paths as *const *const c_char; 360 | let sender = info as *mut Sender; 361 | 362 | for pos in 0..num_events { 363 | let path = CStr::from_ptr(*event_paths.add(pos)) 364 | .to_str() 365 | .expect("Invalid UTF8 string."); 366 | let flag = *event_flags.add(pos); 367 | let event_id = *event_ids.add(pos); 368 | 369 | let event = Event { 370 | event_id: event_id, 371 | flag: StreamFlags::from_bits(flag).unwrap_or_else(|| { 372 | panic!("Unable to decode StreamFlags: {} for {}", flag, path) 373 | }), 374 | path: path.to_string(), 375 | }; 376 | let _s = (*sender).send(event); 377 | } 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /tests/fsevent.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_os="macos")] 2 | 3 | use fsevent_sys::core_foundation as cf; 4 | use fsevent::*; 5 | use std::fs; 6 | use std::fs::read_link; 7 | use std::fs::OpenOptions; 8 | use std::io::Write; 9 | use std::path::{Component, PathBuf}; 10 | use std::thread; 11 | use std::time::{Duration, SystemTime}; 12 | 13 | use std::sync::mpsc::{channel, Receiver}; 14 | 15 | // Helper to send the runloop from an observer thread. 16 | struct CFRunLoopSendWrapper(cf::CFRunLoopRef); 17 | 18 | // Safety: According to the Apple documentation, it is safe to send CFRef types across threads. 19 | // 20 | // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html 21 | unsafe impl Send for CFRunLoopSendWrapper {} 22 | 23 | fn validate_recv(rx: Receiver, evs: Vec<(String, StreamFlags)>) { 24 | let timeout: Duration = Duration::new(5, 0); 25 | let deadline = SystemTime::now() + timeout; 26 | let mut evs = evs.clone(); 27 | 28 | while SystemTime::now() < deadline { 29 | if let Ok(actual) = rx.try_recv() { 30 | let mut found: Option = None; 31 | for i in 0..evs.len() { 32 | let expected = evs.get(i).unwrap(); 33 | if actual.path == expected.0 && actual.flag == expected.1 { 34 | found = Some(i); 35 | break; 36 | } 37 | } 38 | if let Some(i) = found { 39 | evs.remove(i); 40 | } else { 41 | panic!("actual: {:?} not found in expected: {:?}", actual, evs); 42 | } 43 | } 44 | if evs.is_empty() { 45 | break; 46 | } 47 | } 48 | assert!( 49 | evs.is_empty(), 50 | "Some expected events did not occur before the test timedout:\n\t\t{:?}", 51 | evs 52 | ); 53 | } 54 | 55 | // TODO: replace with std::fs::canonicalize rust-lang/rust#27706. 56 | fn resolve_path(path: &str) -> PathBuf { 57 | let mut out = PathBuf::new(); 58 | let buf = PathBuf::from(path); 59 | for p in buf.components() { 60 | match p { 61 | Component::RootDir => out.push("/"), 62 | Component::Normal(osstr) => { 63 | out.push(osstr); 64 | if let Ok(real) = read_link(&out) { 65 | if real.is_relative() { 66 | out.pop(); 67 | out.push(real); 68 | } else { 69 | out = real; 70 | } 71 | } 72 | } 73 | _ => (), 74 | } 75 | } 76 | out 77 | } 78 | 79 | #[test] 80 | fn observe_folder_sync() { 81 | internal_observe_folder(false); 82 | } 83 | 84 | #[test] 85 | fn observe_folder_async() { 86 | internal_observe_folder(true); 87 | } 88 | 89 | fn internal_observe_folder(run_async: bool) { 90 | let dir = tempfile::Builder::new().prefix("dur").tempdir().unwrap(); 91 | // Resolve path so we don't have to worry about affect of symlinks on the test. 92 | let dst = resolve_path(dir.path().to_str().unwrap()); 93 | 94 | let mut dst1 = dst.clone(); 95 | dst1.push("dest1"); 96 | 97 | let ddst1 = dst1.clone(); 98 | fs::create_dir(dst1.as_path().to_str().unwrap()).unwrap(); 99 | 100 | let mut dst2 = dst.clone(); 101 | 102 | dst2.push("dest2"); 103 | let ddst2 = dst2.clone(); 104 | fs::create_dir(dst2.as_path().to_str().unwrap()).unwrap(); 105 | 106 | let mut dst3 = dst.clone(); 107 | 108 | dst3.push("dest3"); 109 | let ddst3 = dst3.clone(); 110 | fs::create_dir(dst3.as_path().to_str().unwrap()).unwrap(); 111 | 112 | let (sender, receiver) = channel(); 113 | 114 | let mut async_fsevent = fsevent::FsEvent::new(vec![]); 115 | let runloop_and_thread = if run_async { 116 | async_fsevent 117 | .append_path(dst1.as_path().to_str().unwrap()) 118 | .unwrap(); 119 | async_fsevent 120 | .append_path(dst2.as_path().to_str().unwrap()) 121 | .unwrap(); 122 | async_fsevent 123 | .append_path(dst3.as_path().to_str().unwrap()) 124 | .unwrap(); 125 | async_fsevent.observe_async(sender).unwrap(); 126 | 127 | None 128 | } else { 129 | let (tx, rx) = std::sync::mpsc::channel(); 130 | let observe_thread = thread::spawn(move || { 131 | let runloop = unsafe { cf::CFRunLoopGetCurrent() }; 132 | tx.send(CFRunLoopSendWrapper(runloop)).unwrap(); 133 | 134 | let mut fsevent = fsevent::FsEvent::new(vec![]); 135 | fsevent 136 | .append_path(dst1.as_path().to_str().unwrap()) 137 | .unwrap(); 138 | fsevent 139 | .append_path(dst2.as_path().to_str().unwrap()) 140 | .unwrap(); 141 | fsevent 142 | .append_path(dst3.as_path().to_str().unwrap()) 143 | .unwrap(); 144 | fsevent.observe(sender); 145 | }); 146 | 147 | let runloop = rx.recv().unwrap(); 148 | 149 | Some((runloop.0, observe_thread)) 150 | }; 151 | 152 | validate_recv( 153 | receiver, 154 | vec![ 155 | ( 156 | ddst1.to_str().unwrap().to_string(), 157 | StreamFlags::ITEM_CREATED | StreamFlags::IS_DIR, 158 | ), 159 | ( 160 | ddst2.to_str().unwrap().to_string(), 161 | StreamFlags::ITEM_CREATED | StreamFlags::IS_DIR, 162 | ), 163 | ( 164 | ddst3.to_str().unwrap().to_string(), 165 | StreamFlags::ITEM_CREATED | StreamFlags::IS_DIR, 166 | ), 167 | ], 168 | ); 169 | 170 | if let Some((runloop, thread)) = runloop_and_thread { 171 | unsafe { cf::CFRunLoopStop(runloop); } 172 | 173 | thread.join().unwrap(); 174 | } else { 175 | async_fsevent.shutdown_observe(); 176 | } 177 | } 178 | 179 | #[test] 180 | fn validate_watch_single_file_sync() { 181 | internal_validate_watch_single_file(false); 182 | } 183 | 184 | #[test] 185 | fn validate_watch_single_file_async() { 186 | internal_validate_watch_single_file(true); 187 | } 188 | 189 | fn internal_validate_watch_single_file(run_async: bool) { 190 | let dir = tempfile::Builder::new().prefix("dur").tempdir().unwrap(); 191 | // Resolve path so we don't have to worry about affect of symlinks on the test. 192 | let mut dst = resolve_path(dir.path().to_str().unwrap()); 193 | dst.push("out.txt"); 194 | let (sender, receiver) = channel(); 195 | 196 | let mut file = OpenOptions::new() 197 | .write(true) 198 | .create(true) 199 | .open(dst.clone().as_path()) 200 | .unwrap(); 201 | file.write_all(b"create").unwrap(); 202 | file.flush().unwrap(); 203 | drop(file); 204 | 205 | let mut async_fsevent = fsevent::FsEvent::new(vec![]); 206 | let runloop_and_thread = if run_async { 207 | let dst = dst.clone(); 208 | async_fsevent 209 | .append_path(dst.as_path().to_str().unwrap()) 210 | .unwrap(); 211 | async_fsevent.observe_async(sender).unwrap(); 212 | 213 | None 214 | } else { 215 | let (tx, rx) = std::sync::mpsc::channel(); 216 | let dst = dst.clone(); 217 | 218 | // Leak the thread. 219 | let observe_thread = thread::spawn(move || { 220 | let runloop = unsafe { cf::CFRunLoopGetCurrent() }; 221 | tx.send(CFRunLoopSendWrapper(runloop)).unwrap(); 222 | 223 | let mut fsevent = fsevent::FsEvent::new(vec![]); 224 | fsevent 225 | .append_path(dst.as_path().to_str().unwrap()) 226 | .unwrap(); 227 | fsevent.observe(sender); 228 | }); 229 | 230 | let runloop = rx.recv().unwrap(); 231 | 232 | Some((runloop.0, observe_thread)) 233 | }; 234 | 235 | { 236 | let dst = dst.clone(); 237 | let t3 = thread::spawn(move || { 238 | thread::sleep(Duration::new(15, 0)); // Wait another 500ms after observe. 239 | let mut file = OpenOptions::new() 240 | .write(true) 241 | .append(true) 242 | .open(dst.as_path()) 243 | .unwrap(); 244 | file.write_all(b"foo").unwrap(); 245 | file.flush().unwrap(); 246 | }); 247 | t3.join().unwrap(); 248 | } 249 | 250 | validate_recv( 251 | receiver, 252 | vec![( 253 | dst.to_str().unwrap().to_string(), 254 | StreamFlags::ITEM_MODIFIED | StreamFlags::ITEM_CREATED | StreamFlags::IS_FILE, 255 | )], 256 | ); 257 | 258 | if let Some((runloop, observe_thread)) = runloop_and_thread { 259 | unsafe { cf::CFRunLoopStop(runloop); } 260 | 261 | observe_thread.join().unwrap(); 262 | } else { 263 | async_fsevent.shutdown_observe(); 264 | } 265 | } 266 | --------------------------------------------------------------------------------