Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion notify/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
309 changes: 252 additions & 57 deletions notify/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,38 @@ 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,
};
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<PathBuf>, // if a file is being watched, this is its full path
directory_reader: DirectoryReaderKind,
complete_sem: HANDLE,
is_recursive: bool,
}
Expand Down Expand Up @@ -86,6 +95,7 @@ struct ReadDirectoryChangesServer {
meta_tx: Sender<MetaEvent>,
cmd_tx: Sender<Result<PathBuf>>,
watches: HashMap<PathBuf, WatchState>,
reader_kind: DirectoryReaderKind,
wakeup_sem: HANDLE,
}

Expand All @@ -112,6 +122,7 @@ impl ReadDirectoryChangesServer {
meta_tx,
cmd_tx,
watches: HashMap::new(),
reader_kind: available_directory_reader_kind(),
wakeup_sem,
};
server.run();
Expand Down Expand Up @@ -228,6 +239,7 @@ impl ReadDirectoryChangesServer {
let rd = ReadData {
dir: dir_target,
file: wf,
directory_reader: self.reader_kind,
complete_sem: semaphore,
is_recursive,
};
Expand Down Expand Up @@ -267,6 +279,22 @@ fn stop_watch(ws: &WatchState, meta_tx: &Sender<MetaEvent>) {
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<Mutex<dyn EventHandler>>,
Expand Down Expand Up @@ -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<OVERLAPPED> = Box::from_raw(overlapped);
let request: Box<ReadDirectoryRequest> = 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<dyn EventHandler>, res: Result<Event>) {
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 => {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also rolled the FILE_ACTION_RENAMED_OLD_NAME into the match... Because I can't see any reason it shouldn't be in it already. Was there a good reason that I'm unaware of? If so, I can revert this part.

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(
Expand Down Expand Up @@ -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 {
Expand Down