Skip to content

Commit bdba4e0

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 bdba4e0

File tree

2 files changed

+224
-37
lines changed

2 files changed

+224
-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: 223 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,22 @@ 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+
let module: HMODULE = GetModuleHandleW(windows_sys::w!("kernel32.dll"));
285+
if module.is_null() {
286+
return DirectoryReaderKind::Standard;
287+
}
288+
289+
let func_ptr = GetProcAddress(module, windows_sys::s!("ReadDirectoryChangesExW"));
290+
if func_ptr.is_some() {
291+
DirectoryReaderKind::Extended
292+
} else {
293+
DirectoryReaderKind::Standard
294+
}
295+
}
296+
}
297+
271298
fn start_read(
272299
rd: &ReadData,
273300
event_handler: Arc<Mutex<dyn EventHandler>>,
@@ -304,33 +331,61 @@ fn start_read(
304331
let request = Box::leak(request);
305332
(*overlapped).hEvent = request as *mut _ as _;
306333

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());
334+
match rd.directory_reader {
335+
DirectoryReaderKind::Extended => {
336+
// This is using an asynchronous call with a completion routine for receiving notifications
337+
// An I/O completion port would probably be more performant
338+
let ret = ReadDirectoryChangesExW(
339+
handle,
340+
request.buffer.as_mut_ptr() as *mut c_void,
341+
BUF_SIZE,
342+
monitor_subdir,
343+
flags,
344+
&mut 0u32 as *mut u32, // not used for async reqs
345+
overlapped,
346+
Some(handle_extended_event),
347+
ReadDirectoryNotifyExtendedInformation,
348+
);
349+
350+
if ret == 0 {
351+
// error reading. retransmute request memory to allow drop.
352+
// Because of the error, ownership of the `overlapped` alloc was not passed
353+
// over to `ReadDirectoryChangesExW`.
354+
// So we can claim ownership back.
355+
let _overlapped = Box::from_raw(overlapped);
356+
let request = Box::from_raw(request);
357+
ReleaseSemaphore(request.data.complete_sem, 1, ptr::null_mut());
358+
}
359+
}
360+
DirectoryReaderKind::Standard => {
361+
// This is using an asynchronous call with a completion routine for receiving notifications
362+
// An I/O completion port would probably be more performant
363+
let ret = ReadDirectoryChangesW(
364+
handle,
365+
request.buffer.as_mut_ptr() as *mut c_void,
366+
BUF_SIZE,
367+
monitor_subdir,
368+
flags,
369+
&mut 0u32 as *mut u32, // not used for async reqs
370+
overlapped,
371+
Some(handle_event),
372+
);
373+
374+
if ret == 0 {
375+
// error reading. retransmute request memory to allow drop.
376+
// Because of the error, ownership of the `overlapped` alloc was not passed
377+
// over to `ReadDirectoryChangesW`.
378+
// So we can claim ownership back.
379+
let _overlapped = Box::from_raw(overlapped);
380+
let request = Box::from_raw(request);
381+
ReleaseSemaphore(request.data.complete_sem, 1, ptr::null_mut());
382+
}
383+
}
329384
}
330385
}
331386
}
332387

333-
unsafe extern "system" fn handle_event(
388+
unsafe extern "system" fn handle_extended_event(
334389
error_code: u32,
335390
_bytes_written: u32,
336391
overlapped: *mut OVERLAPPED,
@@ -359,7 +414,7 @@ unsafe extern "system" fn handle_event(
359414
_ => {
360415
// Some unidentified error occurred, log and unwatch the directory, then return.
361416
log::error!(
362-
"unknown error in ReadDirectoryChangesW for directory {}: {}",
417+
"unknown error in ReadDirectoryChangesExW for directory {}: {}",
363418
request.data.dir.display(),
364419
error_code
365420
);
@@ -377,12 +432,12 @@ unsafe extern "system" fn handle_event(
377432
request.action_tx,
378433
);
379434

380-
// The FILE_NOTIFY_INFORMATION struct has a variable length due to the variable length
435+
// The FILE_NOTIFY_EXTENDED_INFORMATION struct has a variable length due to the variable length
381436
// string as its last member. Each struct contains an offset for getting the next entry in
382437
// the buffer.
383438
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.
439+
// In Wine, FILE_NOTIFY_EXTENDED_INFORMATION structs are packed placed in the buffer;
440+
// they are aligned to 16bit (WCHAR) boundary instead of 32bit required by FILE_NOTIFY_EXTENDED_INFORMATION.
386441
// Hence, we need to use `read_unaligned` here to avoid UB.
387442
let mut cur_entry = ptr::read_unaligned(cur_offset as *const FILE_NOTIFY_EXTENDED_INFORMATION);
388443
loop {
@@ -471,6 +526,138 @@ unsafe extern "system" fn handle_event(
471526
}
472527
}
473528

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

0 commit comments

Comments
 (0)