Avoid watchers for one-shot builds and preserve watcher errors (AI-assisted)

This commit is contained in:
CheckPickerUpper 2026-10-07 16:53:22 -04:00
parent 7e60406373
commit 691efad330
7 changed files with 160 additions and 14 deletions

View file

@ -2,6 +2,9 @@
## Unreleased Changes
* Added `StdBackend::new_without_watcher` and `Vfs::new_default_without_watcher` for filesystem access without allocating a watcher. Watching can be enabled later.
* Watcher I/O errors now preserve their original message and error kind.
# 0.4.0 (2026-07-01)
* Added `Vfs::canonicalize`. [#1201]
* **Breaking:** `StdBackend::new` and `Vfs::new_default` now return `io::Result`, so a failure to create the filesystem watcher is reported as an error instead of panicking. The `Default` implementation for `StdBackend` has been removed as a result. [#1267]

View file

@ -262,6 +262,16 @@ impl Vfs {
Ok(Self::new(StdBackend::new()?))
}
/// Creates a `Vfs` with `StdBackend` and automatic file watching disabled.
///
/// No filesystem watcher is allocated unless automatic watching is later
/// enabled and a file or directory is read.
pub fn new_default_without_watcher() -> Self {
let vfs = Self::new(StdBackend::new_without_watcher());
vfs.set_watch_enabled(false);
vfs
}
/// Creates a new `Vfs` with the given backend.
pub fn new<B: VfsBackend>(backend: B) -> Self {
let lock = VfsInner {
@ -588,6 +598,30 @@ mod test {
use std::io;
use std::path::PathBuf;
#[test]
fn watching_can_be_enabled_after_starting_without_watcher(
) -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let dir_path = dunce::canonicalize(dir.path())?;
let vfs = Vfs::new_default_without_watcher();
let receiver = vfs.event_receiver();
assert_eq!(vfs.read_dir(&dir_path)?.count(), 0);
vfs.set_watch_enabled(true);
assert_eq!(vfs.read_dir(&dir_path)?.count(), 0);
let file_path = dir_path.join("new.txt");
fs_err::write(&file_path, "created after watching was enabled")?;
let event = receiver.recv_timeout(std::time::Duration::from_secs(5))?;
assert!(
matches!(&event, crate::VfsEvent::Create(path) if path == &file_path),
"expected creation of {}, got {:?}",
file_path.display(),
event
);
Ok(())
}
/// https://github.com/rojo-rbx/rojo/issues/899
#[test]
fn read_to_string_lf_normalized_keeps_trailing_newline() {

View file

@ -4,24 +4,45 @@ use std::thread;
use std::time::Duration;
use std::{collections::HashSet, io};
use crossbeam_channel::Receiver;
use crossbeam_channel::{Receiver, Sender};
use notify::{watcher, DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent};
/// `VfsBackend` that uses `std::fs` and the `notify` crate.
pub struct StdBackend {
watcher: RecommendedWatcher,
watcher: Option<RecommendedWatcher>,
watcher_sender: Sender<VfsEvent>,
watcher_receiver: Receiver<VfsEvent>,
watches: HashSet<PathBuf>,
}
impl StdBackend {
/// Creates a filesystem backend with an initialized file watcher.
///
/// Returns an error if the platform's watcher cannot be created.
pub fn new() -> io::Result<StdBackend> {
let (notify_tx, notify_rx) = mpsc::channel();
let watcher = watcher(notify_tx, Duration::from_millis(50)).map_err(io::Error::other)?;
let mut backend = Self::new_without_watcher();
backend.watcher = Some(Self::create_watcher(backend.watcher_sender.clone())?);
Ok(backend)
}
/// Creates a filesystem backend without allocating a file watcher.
///
/// A watcher is initialized only if `watch` is subsequently called.
pub fn new_without_watcher() -> StdBackend {
let (tx, rx) = crossbeam_channel::unbounded();
Self {
watcher: None,
watcher_sender: tx,
watcher_receiver: rx,
watches: HashSet::new(),
}
}
fn create_watcher(tx: Sender<VfsEvent>) -> io::Result<RecommendedWatcher> {
let (notify_tx, notify_rx) = mpsc::channel();
let watcher = watcher(notify_tx, Duration::from_millis(50)).map_err(notify_error_to_io)?;
thread::spawn(move || {
for event in notify_rx {
@ -46,11 +67,7 @@ impl StdBackend {
Result::<(), crossbeam_channel::SendError<VfsEvent>>::Ok(())
});
Ok(Self {
watcher,
watcher_receiver: rx,
watches: HashSet::new(),
})
Ok(watcher)
}
}
@ -122,15 +139,47 @@ impl VfsBackend for StdBackend {
{
Ok(())
} else {
let watcher = match &mut self.watcher {
Some(watcher) => watcher,
slot @ None => slot.insert(Self::create_watcher(self.watcher_sender.clone())?),
};
self.watches.insert(path.to_path_buf());
self.watcher
watcher
.watch(path, RecursiveMode::Recursive)
.map_err(io::Error::other)
.map_err(notify_error_to_io)
}
}
fn unwatch(&mut self, path: &Path) -> io::Result<()> {
self.watches.remove(path);
self.watcher.unwatch(path).map_err(io::Error::other)
match &mut self.watcher {
Some(watcher) => watcher.unwatch(path).map_err(notify_error_to_io),
None => Ok(()),
}
}
}
fn notify_error_to_io(error: notify::Error) -> io::Error {
match error {
// notify 4 formats I/O errors using the deprecated Error::description,
// which can hide the actual operating system error.
notify::Error::Io(error) => error,
error => io::Error::other(error),
}
}
#[cfg(test)]
mod tests {
use super::notify_error_to_io;
use std::io;
#[test]
fn notify_io_error_preserves_message_and_kind() {
let message = "filesystem watcher permission denied";
let source = io::Error::new(io::ErrorKind::PermissionDenied, message);
let error = notify_error_to_io(notify::Error::Io(source));
assert_eq!(error.to_string(), message);
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
}
}