From 97a600395ecda9113e38326e4bb6d439cfa928b3 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 5 Oct 2026 16:18:28 +0200 Subject: [PATCH] fix(env): Windows watcher identity from file indexes --- packages/env/daemon/src/sys/unix.rs | 5 +++ packages/env/daemon/src/sys/windows.rs | 51 ++++++++++++++++++++++++-- packages/env/daemon/src/watch.rs | 25 ++++++++----- 3 files changed, 67 insertions(+), 14 deletions(-) diff --git a/packages/env/daemon/src/sys/unix.rs b/packages/env/daemon/src/sys/unix.rs index 1ec2ce046..764cad1d1 100644 --- a/packages/env/daemon/src/sys/unix.rs +++ b/packages/env/daemon/src/sys/unix.rs @@ -316,3 +316,8 @@ pub fn drive_cwds() -> Map { pub fn read_dir(path: &str) -> io::Result { std::fs::read_dir(path) } + +/// Device and inode of `path`; the metadata already has them. +pub fn path_identity(_path: &str, metadata: &Metadata, _follow: bool) -> (u64, u64) { + identity(metadata) +} diff --git a/packages/env/daemon/src/sys/windows.rs b/packages/env/daemon/src/sys/windows.rs index 7566108a3..f43a4bacf 100644 --- a/packages/env/daemon/src/sys/windows.rs +++ b/packages/env/daemon/src/sys/windows.rs @@ -17,11 +17,12 @@ use std::process::{Child, Command, ExitStatus, Stdio}; use std::sync::OnceLock; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::time::UNIX_EPOCH; -use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; +use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE}; use windows_sys::Win32::Storage::FileSystem::{ - CREATE_ALWAYS, CreateFileW, FILE_APPEND_DATA, FILE_ATTRIBUTE_NORMAL, - FILE_FLAG_BACKUP_SEMANTICS, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_DELETE, - FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_WRITE_DATA, MOVEFILE_REPLACE_EXISTING, MoveFileExW, + BY_HANDLE_FILE_INFORMATION, CREATE_ALWAYS, CreateFileW, FILE_APPEND_DATA, + FILE_ATTRIBUTE_NORMAL, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, + FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, + FILE_WRITE_DATA, GetFileInformationByHandle, MOVEFILE_REPLACE_EXISTING, MoveFileExW, OPEN_ALWAYS, OPEN_EXISTING, }; use windows_sys::Win32::System::IO::CancelSynchronousIo; @@ -602,6 +603,48 @@ pub fn read_dir(path: &str) -> io::Result { }) } +/// Volume serial number and file index of `path`, as libuv's `stat` reports `dev` and `ino`: the creation time is not +/// enough, because NTFS gives a file created under a recently removed name the old one's creation time ("tunneling"). +/// Opens a handle, like libuv; falls back to the metadata when the path cannot be opened. +pub fn path_identity(path: &str, metadata: &Metadata, follow: bool) -> (u64, u64) { + let Ok(name) = wide(path) else { + return identity(metadata); + }; + let flags = FILE_FLAG_BACKUP_SEMANTICS + | if follow { + 0 + } else { + FILE_FLAG_OPEN_REPARSE_POINT + }; + // SAFETY: a NUL-terminated wide path; the handle is closed below. + let handle = unsafe { + CreateFileW( + name.as_ptr(), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + std::ptr::null(), + OPEN_EXISTING, + flags, + std::ptr::null_mut(), + ) + }; + if handle == INVALID_HANDLE_VALUE { + return identity(metadata); + } + // SAFETY: a valid handle and a zeroed buffer for the call to fill. + let mut information: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() }; + let found = unsafe { GetFileInformationByHandle(handle, &mut information) } != 0; + // SAFETY: the handle opened above. + unsafe { CloseHandle(handle) }; + if !found { + return identity(metadata); + } + ( + u64::from(information.dwVolumeSerialNumber), + (u64::from(information.nFileIndexHigh) << 32) | u64::from(information.nFileIndexLow), + ) +} + #[cfg(test)] mod tests { use super::quote_argument; diff --git a/packages/env/daemon/src/watch.rs b/packages/env/daemon/src/watch.rs index 179f34448..18d202b82 100644 --- a/packages/env/daemon/src/watch.rs +++ b/packages/env/daemon/src/watch.rs @@ -119,9 +119,9 @@ fn denied(error: &io::Error) -> bool { error.kind() == io::ErrorKind::PermissionDenied } -fn entry_of(metadata: &Metadata, hash: Option) -> Entry { +fn kind_of(metadata: &Metadata) -> Kind { let file_type = metadata.file_type(); - let kind = if file_type.is_file() { + if file_type.is_file() { Kind::File } else if file_type.is_dir() { Kind::Directory @@ -129,8 +129,13 @@ fn entry_of(metadata: &Metadata, hash: Option) -> Entry { Kind::Symlink } else { Kind::Other - }; - let (dev, ino) = sys::identity(metadata); + } +} + +/// What a snapshot remembers of `path`, whose `metadata` came from `stat` (`follow`) or `lstat`. +fn entry_of(path: &str, metadata: &Metadata, follow: bool, hash: Option) -> Entry { + let kind = kind_of(metadata); + let (dev, ino) = sys::path_identity(path, metadata, follow); let directory = kind == Kind::Directory; Entry { kind, @@ -250,7 +255,7 @@ impl FileWatcher<'_> { self.control.aborted.load(Ordering::SeqCst) } - fn record(&self, scan: &mut Scan, path: &str, metadata: &Metadata) { + fn record(&self, scan: &mut Scan, path: &str, metadata: &Metadata, follow: bool) { let mut hash = None; let recent = metadata .modified() @@ -272,7 +277,7 @@ impl FileWatcher<'_> { }); } scan.snapshot - .insert(path.to_string(), entry_of(metadata, hash)); + .insert(path.to_string(), entry_of(path, metadata, follow, hash)); } fn count(&self, scan: &mut Scan, path: &str) -> Result<(), ScanError> { @@ -329,11 +334,11 @@ impl FileWatcher<'_> { let Ok(metadata) = fs::symlink_metadata(&path) else { continue; }; - let kind = entry_of(&metadata, None).kind; + let kind = kind_of(&metadata); scan.listed.insert(path.clone(), kind); // A target's own entry (following links) wins over its listing by another target. if !scan.snapshot.contains_key(&path) { - self.record(scan, &path, &metadata); + self.record(scan, &path, &metadata, false); } kind } @@ -364,7 +369,7 @@ impl FileWatcher<'_> { let entry = Entry { size: 0, mtime: (0, 0), - ..entry_of(&metadata, None) + ..entry_of(&ancestor, &metadata, false, None) }; scan.snapshot.insert(ancestor, entry); } @@ -378,7 +383,7 @@ impl FileWatcher<'_> { } Err(_) => continue, }; - self.record(&mut scan, &target.path, &metadata); + self.record(&mut scan, &target.path, &metadata, true); if metadata.is_file() && fs::symlink_metadata(&target.path) .is_ok_and(|link| link.file_type().is_symlink())