fix(env): Windows watcher identity from file indexes

This commit is contained in:
Mario Zechner 2026-10-05 16:18:28 +02:00
parent 956e81504c
commit 97a600395e
3 changed files with 67 additions and 14 deletions

View file

@ -316,3 +316,8 @@ pub fn drive_cwds() -> Map<String, Value> {
pub fn read_dir(path: &str) -> io::Result<std::fs::ReadDir> {
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)
}

View file

@ -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<std::fs::ReadDir> {
})
}
/// 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;

View file

@ -119,9 +119,9 @@ fn denied(error: &io::Error) -> bool {
error.kind() == io::ErrorKind::PermissionDenied
}
fn entry_of(metadata: &Metadata, hash: Option<u64>) -> 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<u64>) -> 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<u64>) -> 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())