Skip to content

Commit ad4f803

Browse files
committed
Use ReadDirectoryChangesW when ReadDirectoryChangesExW is not available (Windows versions earlier than 10).
Checked dynamically once at server creation and passed along to the ReadData.
1 parent 572dfbf commit ad4f803

File tree

2 files changed

+226
-37
lines changed

2 files changed

+226
-37
lines changed

notify/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ kqueue = { workspace = true, optional = true }
4343
mio = { workspace = true, optional = true }
4444

4545
[target.'cfg(windows)'.dependencies]
46-
windows-sys = { workspace = true, features = ["Win32_System_Threading", "Win32_Foundation", "Win32_Storage_FileSystem", "Win32_Security", "Win32_System_WindowsProgramming", "Win32_System_IO"] }
46+
windows-sys = { workspace = true, features = ["Win32_System_Threading", "Win32_Foundation", "Win32_Storage_FileSystem", "Win32_Security", "Win32_System_WindowsProgramming", "Win32_System_IO", "Win32_System_LibraryLoader"] }
4747

4848
[target.'cfg(any(target_os="freebsd", target_os="openbsd", target_os = "netbsd", target_os = "dragonflybsd", target_os = "ios"))'.dependencies]
4949
kqueue.workspace = true

notify/src/windows.rs

Lines changed: 225 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,38 @@ use std::slice;
2020
use std::sync::{Arc, Mutex};
2121
use std::thread;
2222
use windows_sys::Win32::Foundation::{
23-
CloseHandle, ERROR_ACCESS_DENIED, ERROR_OPERATION_ABORTED, ERROR_SUCCESS, HANDLE,
23+
CloseHandle, ERROR_ACCESS_DENIED, ERROR_OPERATION_ABORTED, ERROR_SUCCESS, HANDLE, HMODULE,
2424
INVALID_HANDLE_VALUE, WAIT_OBJECT_0,
2525
};
2626
use windows_sys::Win32::Storage::FileSystem::{
27-
CreateFileW, ReadDirectoryChangesExW, ReadDirectoryNotifyExtendedInformation,
28-
FILE_ACTION_ADDED, FILE_ACTION_MODIFIED, FILE_ACTION_REMOVED, FILE_ACTION_RENAMED_NEW_NAME,
29-
FILE_ACTION_RENAMED_OLD_NAME, FILE_ATTRIBUTE_DIRECTORY, FILE_FLAG_BACKUP_SEMANTICS,
30-
FILE_FLAG_OVERLAPPED, FILE_LIST_DIRECTORY, FILE_NOTIFY_CHANGE_ATTRIBUTES,
31-
FILE_NOTIFY_CHANGE_CREATION, FILE_NOTIFY_CHANGE_DIR_NAME, FILE_NOTIFY_CHANGE_FILE_NAME,
32-
FILE_NOTIFY_CHANGE_LAST_WRITE, FILE_NOTIFY_CHANGE_SECURITY, FILE_NOTIFY_CHANGE_SIZE,
33-
FILE_NOTIFY_EXTENDED_INFORMATION, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
34-
OPEN_EXISTING,
27+
CreateFileW, ReadDirectoryChangesExW, ReadDirectoryChangesW,
28+
ReadDirectoryNotifyExtendedInformation, FILE_ACTION_ADDED, FILE_ACTION_MODIFIED,
29+
FILE_ACTION_REMOVED, FILE_ACTION_RENAMED_NEW_NAME, FILE_ACTION_RENAMED_OLD_NAME,
30+
FILE_ATTRIBUTE_DIRECTORY, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OVERLAPPED,
31+
FILE_LIST_DIRECTORY, FILE_NOTIFY_CHANGE_ATTRIBUTES, FILE_NOTIFY_CHANGE_CREATION,
32+
FILE_NOTIFY_CHANGE_DIR_NAME, FILE_NOTIFY_CHANGE_FILE_NAME, FILE_NOTIFY_CHANGE_LAST_WRITE,
33+
FILE_NOTIFY_CHANGE_SECURITY, FILE_NOTIFY_CHANGE_SIZE, FILE_NOTIFY_EXTENDED_INFORMATION,
34+
FILE_NOTIFY_INFORMATION, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
3535
};
36+
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleW, GetProcAddress};
3637
use windows_sys::Win32::System::Threading::{
3738
CreateSemaphoreW, ReleaseSemaphore, WaitForSingleObjectEx, INFINITE,
3839
};
3940
use windows_sys::Win32::System::IO::{CancelIo, OVERLAPPED};
4041

4142
const BUF_SIZE: u32 = 16384;
4243

44+
#[derive(Clone, Copy)]
45+
enum DirectoryReaderKind {
46+
Standard,
47+
Extended,
48+
}
49+
4350
#[derive(Clone)]
4451
struct ReadData {
4552
dir: PathBuf, // directory that is being watched
4653
file: Option<PathBuf>, // if a file is being watched, this is its full path
54+
directory_reader: DirectoryReaderKind,
4755
complete_sem: HANDLE,
4856
is_recursive: bool,
4957
}
@@ -87,6 +95,7 @@ struct ReadDirectoryChangesServer {
8795
meta_tx: Sender<MetaEvent>,
8896
cmd_tx: Sender<Result<PathBuf>>,
8997
watches: HashMap<PathBuf, WatchState>,
98+
reader_kind: DirectoryReaderKind,
9099
wakeup_sem: HANDLE,
91100
}
92101

@@ -113,6 +122,7 @@ impl ReadDirectoryChangesServer {
113122
meta_tx,
114123
cmd_tx,
115124
watches: HashMap::new(),
125+
reader_kind: available_directory_reader_kind(),
116126
wakeup_sem,
117127
};
118128
server.run();
@@ -229,6 +239,7 @@ impl ReadDirectoryChangesServer {
229239
let rd = ReadData {
230240
dir: dir_target,
231241
file: wf,
242+
directory_reader: self.reader_kind,
232243
complete_sem: semaphore,
233244
is_recursive,
234245
};
@@ -268,6 +279,24 @@ fn stop_watch(ws: &WatchState, meta_tx: &Sender<MetaEvent>) {
268279
let _ = meta_tx.send(MetaEvent::SingleWatchComplete);
269280
}
270281

282+
fn available_directory_reader_kind() -> DirectoryReaderKind {
283+
unsafe {
284+
// Get handle to kernel32.dll
285+
let module: HMODULE = GetModuleHandleW(windows_sys::w!("kernel32.dll"));
286+
if module.is_null() {
287+
return DirectoryReaderKind::Standard;
288+
}
289+
290+
// Check if function exists
291+
let func_ptr = GetProcAddress(module, windows_sys::s!("ReadDirectoryChangesExW"));
292+
if func_ptr.is_some() {
293+
DirectoryReaderKind::Extended
294+
} else {
295+
DirectoryReaderKind::Standard
296+
}
297+
}
298+
}
299+
271300
fn start_read(
272301
rd: &ReadData,
273302
event_handler: Arc<Mutex<dyn EventHandler>>,
@@ -304,33 +333,61 @@ fn start_read(
304333
let request = Box::leak(request);
305334
(*overlapped).hEvent = request as *mut _ as _;
306335

307-
// This is using an asynchronous call with a completion routine for receiving notifications
308-
// An I/O completion port would probably be more performant
309-
let ret = ReadDirectoryChangesExW(
310-
handle,
311-
request.buffer.as_mut_ptr() as *mut c_void,
312-
BUF_SIZE,
313-
monitor_subdir,
314-
flags,
315-
&mut 0u32 as *mut u32, // not used for async reqs
316-
overlapped,
317-
Some(handle_event),
318-
ReadDirectoryNotifyExtendedInformation,
319-
);
320-
321-
if ret == 0 {
322-
// error reading. retransmute request memory to allow drop.
323-
// Because of the error, ownership of the `overlapped` alloc was not passed
324-
// over to `ReadDirectoryChangesW`.
325-
// So we can claim ownership back.
326-
let _overlapped = Box::from_raw(overlapped);
327-
let request = Box::from_raw(request);
328-
ReleaseSemaphore(request.data.complete_sem, 1, ptr::null_mut());
336+
match rd.directory_reader {
337+
DirectoryReaderKind::Extended => {
338+
// This is using an asynchronous call with a completion routine for receiving notifications
339+
// An I/O completion port would probably be more performant
340+
let ret = ReadDirectoryChangesExW(
341+
handle,
342+
request.buffer.as_mut_ptr() as *mut c_void,
343+
BUF_SIZE,
344+
monitor_subdir,
345+
flags,
346+
&mut 0u32 as *mut u32, // not used for async reqs
347+
overlapped,
348+
Some(handle_extended_event),
349+
ReadDirectoryNotifyExtendedInformation,
350+
);
351+
352+
if ret == 0 {
353+
// error reading. retransmute request memory to allow drop.
354+
// Because of the error, ownership of the `overlapped` alloc was not passed
355+
// over to `ReadDirectoryChangesExW`.
356+
// So we can claim ownership back.
357+
let _overlapped = Box::from_raw(overlapped);
358+
let request = Box::from_raw(request);
359+
ReleaseSemaphore(request.data.complete_sem, 1, ptr::null_mut());
360+
}
361+
}
362+
DirectoryReaderKind::Standard => {
363+
// This is using an asynchronous call with a completion routine for receiving notifications
364+
// An I/O completion port would probably be more performant
365+
let ret = ReadDirectoryChangesW(
366+
handle,
367+
request.buffer.as_mut_ptr() as *mut c_void,
368+
BUF_SIZE,
369+
monitor_subdir,
370+
flags,
371+
&mut 0u32 as *mut u32, // not used for async reqs
372+
overlapped,
373+
Some(handle_event),
374+
);
375+
376+
if ret == 0 {
377+
// error reading. retransmute request memory to allow drop.
378+
// Because of the error, ownership of the `overlapped` alloc was not passed
379+
// over to `ReadDirectoryChangesW`.
380+
// So we can claim ownership back.
381+
let _overlapped = Box::from_raw(overlapped);
382+
let request = Box::from_raw(request);
383+
ReleaseSemaphore(request.data.complete_sem, 1, ptr::null_mut());
384+
}
385+
}
329386
}
330387
}
331388
}
332389

333-
unsafe extern "system" fn handle_event(
390+
unsafe extern "system" fn handle_extended_event(
334391
error_code: u32,
335392
_bytes_written: u32,
336393
overlapped: *mut OVERLAPPED,
@@ -359,7 +416,7 @@ unsafe extern "system" fn handle_event(
359416
_ => {
360417
// Some unidentified error occurred, log and unwatch the directory, then return.
361418
log::error!(
362-
"unknown error in ReadDirectoryChangesW for directory {}: {}",
419+
"unknown error in ReadDirectoryChangesExW for directory {}: {}",
363420
request.data.dir.display(),
364421
error_code
365422
);
@@ -377,12 +434,12 @@ unsafe extern "system" fn handle_event(
377434
request.action_tx,
378435
);
379436

380-
// The FILE_NOTIFY_INFORMATION struct has a variable length due to the variable length
437+
// The FILE_NOTIFY_EXTENDED_INFORMATION struct has a variable length due to the variable length
381438
// string as its last member. Each struct contains an offset for getting the next entry in
382439
// the buffer.
383440
let mut cur_offset: *const u8 = request.buffer.as_ptr();
384-
// In Wine, FILE_NOTIFY_INFORMATION structs are packed placed in the buffer;
385-
// they are aligned to 16bit (WCHAR) boundary instead of 32bit required by FILE_NOTIFY_INFORMATION.
441+
// In Wine, FILE_NOTIFY_EXTENDED_INFORMATION structs are packed placed in the buffer;
442+
// they are aligned to 16bit (WCHAR) boundary instead of 32bit required by FILE_NOTIFY_EXTENDED_INFORMATION.
386443
// Hence, we need to use `read_unaligned` here to avoid UB.
387444
let mut cur_entry = ptr::read_unaligned(cur_offset as *const FILE_NOTIFY_EXTENDED_INFORMATION);
388445
loop {
@@ -471,6 +528,138 @@ unsafe extern "system" fn handle_event(
471528
}
472529
}
473530

531+
unsafe extern "system" fn handle_event(
532+
error_code: u32,
533+
_bytes_written: u32,
534+
overlapped: *mut OVERLAPPED,
535+
) {
536+
let overlapped: Box<OVERLAPPED> = Box::from_raw(overlapped);
537+
let request: Box<ReadDirectoryRequest> = Box::from_raw(overlapped.hEvent as *mut _);
538+
539+
match error_code {
540+
ERROR_OPERATION_ABORTED => {
541+
// received when dir is unwatched or watcher is shutdown; return and let overlapped/request get drop-cleaned
542+
ReleaseSemaphore(request.data.complete_sem, 1, ptr::null_mut());
543+
return;
544+
}
545+
ERROR_ACCESS_DENIED => {
546+
// This could happen when the watched directory is deleted or trashed, first check if it's the case.
547+
// If so, unwatch the directory and return, otherwise, continue to handle the event.
548+
if !request.data.dir.exists() {
549+
request.unwatch();
550+
ReleaseSemaphore(request.data.complete_sem, 1, ptr::null_mut());
551+
return;
552+
}
553+
}
554+
ERROR_SUCCESS => {
555+
// Success, continue to handle the event
556+
}
557+
_ => {
558+
// Some unidentified error occurred, log and unwatch the directory, then return.
559+
log::error!(
560+
"unknown error in ReadDirectoryChangesW for directory {}: {}",
561+
request.data.dir.display(),
562+
error_code
563+
);
564+
request.unwatch();
565+
ReleaseSemaphore(request.data.complete_sem, 1, ptr::null_mut());
566+
return;
567+
}
568+
}
569+
570+
// Get the next request queued up as soon as possible
571+
start_read(
572+
&request.data,
573+
request.event_handler.clone(),
574+
request.handle,
575+
request.action_tx,
576+
);
577+
578+
// The FILE_NOTIFY_INFORMATION struct has a variable length due to the variable length
579+
// string as its last member. Each struct contains an offset for getting the next entry in
580+
// the buffer.
581+
let mut cur_offset: *const u8 = request.buffer.as_ptr();
582+
// In Wine, FILE_NOTIFY_INFORMATION structs are packed placed in the buffer;
583+
// they are aligned to 16bit (WCHAR) boundary instead of 32bit required by FILE_NOTIFY_INFORMATION.
584+
// Hence, we need to use `read_unaligned` here to avoid UB.
585+
let mut cur_entry = ptr::read_unaligned(cur_offset as *const FILE_NOTIFY_INFORMATION);
586+
loop {
587+
// filename length is size in bytes, so / 2
588+
let len = cur_entry.FileNameLength as usize / 2;
589+
let encoded_path: &[u16] = slice::from_raw_parts(
590+
cur_offset.offset(std::mem::offset_of!(FILE_NOTIFY_INFORMATION, FileName) as isize)
591+
as _,
592+
len,
593+
);
594+
// prepend root to get a full path
595+
let path = request
596+
.data
597+
.dir
598+
.join(PathBuf::from(OsString::from_wide(encoded_path)));
599+
600+
// if we are watching a single file, ignore the event unless the path is exactly
601+
// the watched file
602+
let skip = match request.data.file {
603+
None => false,
604+
Some(ref watch_path) => *watch_path != path,
605+
};
606+
607+
if !skip {
608+
log::trace!(
609+
"Event: path = `{}`, action = {:?}",
610+
path.display(),
611+
cur_entry.Action
612+
);
613+
614+
let newe = Event::new(EventKind::Any).add_path(path);
615+
616+
fn emit_event(event_handler: &Mutex<dyn EventHandler>, res: Result<Event>) {
617+
if let Ok(mut guard) = event_handler.lock() {
618+
let f: &mut dyn EventHandler = &mut *guard;
619+
f.handle_event(res);
620+
}
621+
}
622+
623+
let event_handler = |res| emit_event(&request.event_handler, res);
624+
625+
match cur_entry.Action {
626+
FILE_ACTION_RENAMED_OLD_NAME => {
627+
let kind = EventKind::Modify(ModifyKind::Name(RenameMode::From));
628+
let ev = newe.set_kind(kind);
629+
event_handler(Ok(ev))
630+
}
631+
FILE_ACTION_RENAMED_NEW_NAME => {
632+
let kind = EventKind::Modify(ModifyKind::Name(RenameMode::To));
633+
let ev = newe.set_kind(kind);
634+
event_handler(Ok(ev));
635+
}
636+
FILE_ACTION_ADDED => {
637+
let kind = EventKind::Create(CreateKind::Any);
638+
let ev = newe.set_kind(kind);
639+
event_handler(Ok(ev));
640+
}
641+
FILE_ACTION_REMOVED => {
642+
let kind = EventKind::Remove(RemoveKind::Any);
643+
let ev = newe.set_kind(kind);
644+
event_handler(Ok(ev));
645+
}
646+
FILE_ACTION_MODIFIED => {
647+
let kind = EventKind::Modify(ModifyKind::Any);
648+
let ev = newe.set_kind(kind);
649+
event_handler(Ok(ev));
650+
}
651+
_ => (),
652+
};
653+
}
654+
655+
if cur_entry.NextEntryOffset == 0 {
656+
break;
657+
}
658+
cur_offset = cur_offset.offset(cur_entry.NextEntryOffset as isize);
659+
cur_entry = ptr::read_unaligned(cur_offset as *const FILE_NOTIFY_INFORMATION);
660+
}
661+
}
662+
474663
/// Watcher implementation based on ReadDirectoryChanges
475664
#[derive(Debug)]
476665
pub struct ReadDirectoryChangesWatcher {

0 commit comments

Comments
 (0)