diff --git a/notify/Cargo.toml b/notify/Cargo.toml index 0c50132d..677523aa 100644 --- a/notify/Cargo.toml +++ b/notify/Cargo.toml @@ -43,7 +43,7 @@ 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", "Win32_System_LibraryLoader"] } [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/windows.rs b/notify/src/windows.rs index 797052ed..008bf605 100644 --- a/notify/src/windows.rs +++ b/notify/src/windows.rs @@ -20,18 +20,20 @@ use std::slice; use std::sync::{Arc, Mutex}; use std::thread; use windows_sys::Win32::Foundation::{ - CloseHandle, ERROR_ACCESS_DENIED, ERROR_OPERATION_ABORTED, ERROR_SUCCESS, HANDLE, + CloseHandle, ERROR_ACCESS_DENIED, ERROR_OPERATION_ABORTED, ERROR_SUCCESS, HANDLE, HMODULE, INVALID_HANDLE_VALUE, WAIT_OBJECT_0, }; use windows_sys::Win32::Storage::FileSystem::{ - CreateFileW, ReadDirectoryChangesW, FILE_ACTION_ADDED, FILE_ACTION_MODIFIED, + CreateFileW, ReadDirectoryChangesExW, ReadDirectoryChangesW, + ReadDirectoryNotifyExtendedInformation, FILE_ACTION_ADDED, FILE_ACTION_MODIFIED, FILE_ACTION_REMOVED, FILE_ACTION_RENAMED_NEW_NAME, FILE_ACTION_RENAMED_OLD_NAME, - FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OVERLAPPED, FILE_LIST_DIRECTORY, - FILE_NOTIFY_CHANGE_ATTRIBUTES, FILE_NOTIFY_CHANGE_CREATION, FILE_NOTIFY_CHANGE_DIR_NAME, - FILE_NOTIFY_CHANGE_FILE_NAME, FILE_NOTIFY_CHANGE_LAST_WRITE, FILE_NOTIFY_CHANGE_SECURITY, - FILE_NOTIFY_CHANGE_SIZE, FILE_NOTIFY_INFORMATION, FILE_SHARE_DELETE, FILE_SHARE_READ, - FILE_SHARE_WRITE, OPEN_EXISTING, + FILE_ATTRIBUTE_DIRECTORY, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OVERLAPPED, + FILE_LIST_DIRECTORY, FILE_NOTIFY_CHANGE_ATTRIBUTES, FILE_NOTIFY_CHANGE_CREATION, + FILE_NOTIFY_CHANGE_DIR_NAME, FILE_NOTIFY_CHANGE_FILE_NAME, FILE_NOTIFY_CHANGE_LAST_WRITE, + FILE_NOTIFY_CHANGE_SECURITY, FILE_NOTIFY_CHANGE_SIZE, FILE_NOTIFY_EXTENDED_INFORMATION, + FILE_NOTIFY_INFORMATION, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING, }; +use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleW, GetProcAddress}; use windows_sys::Win32::System::Threading::{ CreateSemaphoreW, ReleaseSemaphore, WaitForSingleObjectEx, INFINITE, }; @@ -39,10 +41,17 @@ use windows_sys::Win32::System::IO::{CancelIo, OVERLAPPED}; const BUF_SIZE: u32 = 16384; +#[derive(Clone, Copy)] +enum DirectoryReaderKind { + Standard, + Extended, +} + #[derive(Clone)] struct ReadData { dir: PathBuf, // directory that is being watched file: Option, // if a file is being watched, this is its full path + directory_reader: DirectoryReaderKind, complete_sem: HANDLE, is_recursive: bool, } @@ -86,6 +95,7 @@ struct ReadDirectoryChangesServer { meta_tx: Sender, cmd_tx: Sender>, watches: HashMap, + reader_kind: DirectoryReaderKind, wakeup_sem: HANDLE, } @@ -112,6 +122,7 @@ impl ReadDirectoryChangesServer { meta_tx, cmd_tx, watches: HashMap::new(), + reader_kind: available_directory_reader_kind(), wakeup_sem, }; server.run(); @@ -228,6 +239,7 @@ impl ReadDirectoryChangesServer { let rd = ReadData { dir: dir_target, file: wf, + directory_reader: self.reader_kind, complete_sem: semaphore, is_recursive, }; @@ -267,6 +279,22 @@ fn stop_watch(ws: &WatchState, meta_tx: &Sender) { let _ = meta_tx.send(MetaEvent::SingleWatchComplete); } +fn available_directory_reader_kind() -> DirectoryReaderKind { + unsafe { + let module: HMODULE = GetModuleHandleW(windows_sys::w!("kernel32.dll")); + if module.is_null() { + return DirectoryReaderKind::Standard; + } + + let func_ptr = GetProcAddress(module, windows_sys::s!("ReadDirectoryChangesExW")); + if func_ptr.is_some() { + DirectoryReaderKind::Extended + } else { + DirectoryReaderKind::Standard + } + } +} + fn start_read( rd: &ReadData, event_handler: Arc>, @@ -303,29 +331,199 @@ fn start_read( let request = Box::leak(request); (*overlapped).hEvent = request as *mut _ as _; - // This is using an asynchronous call with a completion routine for receiving notifications - // An I/O completion port would probably be more performant - let ret = ReadDirectoryChangesW( - handle, - request.buffer.as_mut_ptr() as *mut c_void, - BUF_SIZE, - monitor_subdir, - flags, - &mut 0u32 as *mut u32, // not used for async reqs - overlapped, - Some(handle_event), - ); + match rd.directory_reader { + DirectoryReaderKind::Extended => { + // This is using an asynchronous call with a completion routine for receiving notifications + // An I/O completion port would probably be more performant + let ret = ReadDirectoryChangesExW( + handle, + request.buffer.as_mut_ptr() as *mut c_void, + BUF_SIZE, + monitor_subdir, + flags, + &mut 0u32 as *mut u32, // not used for async reqs + overlapped, + Some(handle_extended_event), + ReadDirectoryNotifyExtendedInformation, + ); + + if ret == 0 { + // error reading. retransmute request memory to allow drop. + // Because of the error, ownership of the `overlapped` alloc was not passed + // over to `ReadDirectoryChangesExW`. + // So we can claim ownership back. + let _overlapped = Box::from_raw(overlapped); + let request = Box::from_raw(request); + ReleaseSemaphore(request.data.complete_sem, 1, ptr::null_mut()); + } + } + DirectoryReaderKind::Standard => { + // This is using an asynchronous call with a completion routine for receiving notifications + // An I/O completion port would probably be more performant + let ret = ReadDirectoryChangesW( + handle, + request.buffer.as_mut_ptr() as *mut c_void, + BUF_SIZE, + monitor_subdir, + flags, + &mut 0u32 as *mut u32, // not used for async reqs + overlapped, + Some(handle_event), + ); + + if ret == 0 { + // error reading. retransmute request memory to allow drop. + // Because of the error, ownership of the `overlapped` alloc was not passed + // over to `ReadDirectoryChangesW`. + // So we can claim ownership back. + let _overlapped = Box::from_raw(overlapped); + let request = Box::from_raw(request); + ReleaseSemaphore(request.data.complete_sem, 1, ptr::null_mut()); + } + } + } + } +} + +unsafe extern "system" fn handle_extended_event( + error_code: u32, + _bytes_written: u32, + overlapped: *mut OVERLAPPED, +) { + let overlapped: Box = Box::from_raw(overlapped); + let request: Box = Box::from_raw(overlapped.hEvent as *mut _); - if ret == 0 { - // error reading. retransmute request memory to allow drop. - // Because of the error, ownership of the `overlapped` alloc was not passed - // over to `ReadDirectoryChangesW`. - // So we can claim ownership back. - let _overlapped = Box::from_raw(overlapped); - let request = Box::from_raw(request); + match error_code { + ERROR_OPERATION_ABORTED => { + // received when dir is unwatched or watcher is shutdown; return and let overlapped/request get drop-cleaned + ReleaseSemaphore(request.data.complete_sem, 1, ptr::null_mut()); + return; + } + ERROR_ACCESS_DENIED => { + // This could happen when the watched directory is deleted or trashed, first check if it's the case. + // If so, unwatch the directory and return, otherwise, continue to handle the event. + if !request.data.dir.exists() { + request.unwatch(); + ReleaseSemaphore(request.data.complete_sem, 1, ptr::null_mut()); + return; + } + } + ERROR_SUCCESS => { + // Success, continue to handle the event + } + _ => { + // Some unidentified error occurred, log and unwatch the directory, then return. + log::error!( + "unknown error in ReadDirectoryChangesExW for directory {}: {}", + request.data.dir.display(), + error_code + ); + request.unwatch(); ReleaseSemaphore(request.data.complete_sem, 1, ptr::null_mut()); + return; } } + + // Get the next request queued up as soon as possible + start_read( + &request.data, + request.event_handler.clone(), + request.handle, + request.action_tx, + ); + + // The FILE_NOTIFY_EXTENDED_INFORMATION struct has a variable length due to the variable length + // string as its last member. Each struct contains an offset for getting the next entry in + // the buffer. + let mut cur_offset: *const u8 = request.buffer.as_ptr(); + // In Wine, FILE_NOTIFY_EXTENDED_INFORMATION structs are packed placed in the buffer; + // they are aligned to 16bit (WCHAR) boundary instead of 32bit required by FILE_NOTIFY_EXTENDED_INFORMATION. + // Hence, we need to use `read_unaligned` here to avoid UB. + let mut cur_entry = ptr::read_unaligned(cur_offset as *const FILE_NOTIFY_EXTENDED_INFORMATION); + loop { + // filename length is size in bytes, so / 2 + let len = cur_entry.FileNameLength as usize / 2; + let encoded_path: &[u16] = slice::from_raw_parts( + cur_offset + .offset(std::mem::offset_of!(FILE_NOTIFY_EXTENDED_INFORMATION, FileName) as isize) + as _, + len, + ); + // prepend root to get a full path + let path = request + .data + .dir + .join(PathBuf::from(OsString::from_wide(encoded_path))); + + // if we are watching a single file, ignore the event unless the path is exactly + // the watched file + let skip = match request.data.file { + None => false, + Some(ref watch_path) => *watch_path != path, + }; + + if !skip { + log::trace!( + "Event: path = `{}`, action = {:?}", + path.display(), + cur_entry.Action + ); + + let newe = Event::new(EventKind::Any).add_path(path); + + fn emit_event(event_handler: &Mutex, res: Result) { + if let Ok(mut guard) = event_handler.lock() { + let f: &mut dyn EventHandler = &mut *guard; + f.handle_event(res); + } + } + + let event_handler = |res| emit_event(&request.event_handler, res); + + match cur_entry.Action { + FILE_ACTION_RENAMED_OLD_NAME => { + let kind = EventKind::Modify(ModifyKind::Name(RenameMode::From)); + let ev = newe.set_kind(kind); + event_handler(Ok(ev)) + } + FILE_ACTION_RENAMED_NEW_NAME => { + let kind = EventKind::Modify(ModifyKind::Name(RenameMode::To)); + let ev = newe.set_kind(kind); + event_handler(Ok(ev)); + } + FILE_ACTION_ADDED => { + let kind = if (cur_entry.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 { + EventKind::Create(CreateKind::Folder) + } else { + EventKind::Create(CreateKind::File) + }; + let ev = newe.set_kind(kind); + event_handler(Ok(ev)); + } + FILE_ACTION_REMOVED => { + let kind = if (cur_entry.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 { + EventKind::Remove(RemoveKind::Folder) + } else { + EventKind::Remove(RemoveKind::File) + }; + let ev = newe.set_kind(kind); + event_handler(Ok(ev)); + } + FILE_ACTION_MODIFIED => { + let kind = EventKind::Modify(ModifyKind::Any); + let ev = newe.set_kind(kind); + event_handler(Ok(ev)); + } + _ => (), + }; + } + + if cur_entry.NextEntryOffset == 0 { + break; + } + cur_offset = cur_offset.offset(cur_entry.NextEntryOffset as isize); + cur_entry = ptr::read_unaligned(cur_offset as *const FILE_NOTIFY_EXTENDED_INFORMATION); + } } unsafe extern "system" fn handle_event( @@ -422,37 +620,34 @@ unsafe extern "system" fn handle_event( let event_handler = |res| emit_event(&request.event_handler, res); - if cur_entry.Action == FILE_ACTION_RENAMED_OLD_NAME { - let mode = RenameMode::From; - let kind = ModifyKind::Name(mode); - let kind = EventKind::Modify(kind); - let ev = newe.set_kind(kind); - event_handler(Ok(ev)) - } else { - match cur_entry.Action { - FILE_ACTION_RENAMED_NEW_NAME => { - let kind = EventKind::Modify(ModifyKind::Name(RenameMode::To)); - let ev = newe.set_kind(kind); - event_handler(Ok(ev)); - } - FILE_ACTION_ADDED => { - let kind = EventKind::Create(CreateKind::Any); - let ev = newe.set_kind(kind); - event_handler(Ok(ev)); - } - FILE_ACTION_REMOVED => { - let kind = EventKind::Remove(RemoveKind::Any); - let ev = newe.set_kind(kind); - event_handler(Ok(ev)); - } - FILE_ACTION_MODIFIED => { - let kind = EventKind::Modify(ModifyKind::Any); - let ev = newe.set_kind(kind); - event_handler(Ok(ev)); - } - _ => (), - }; - } + match cur_entry.Action { + FILE_ACTION_RENAMED_OLD_NAME => { + let kind = EventKind::Modify(ModifyKind::Name(RenameMode::From)); + let ev = newe.set_kind(kind); + event_handler(Ok(ev)) + } + FILE_ACTION_RENAMED_NEW_NAME => { + let kind = EventKind::Modify(ModifyKind::Name(RenameMode::To)); + let ev = newe.set_kind(kind); + event_handler(Ok(ev)); + } + FILE_ACTION_ADDED => { + let kind = EventKind::Create(CreateKind::Any); + let ev = newe.set_kind(kind); + event_handler(Ok(ev)); + } + FILE_ACTION_REMOVED => { + let kind = EventKind::Remove(RemoveKind::Any); + let ev = newe.set_kind(kind); + event_handler(Ok(ev)); + } + FILE_ACTION_MODIFIED => { + let kind = EventKind::Modify(ModifyKind::Any); + let ev = newe.set_kind(kind); + event_handler(Ok(ev)); + } + _ => (), + }; } if cur_entry.NextEntryOffset == 0 {