Fix stale filesystem event path handling (AI-assisted)

This commit is contained in:
Mitchell 2026-08-30 13:25:24 -05:00
parent a30a8ecd0f
commit 480277fc1f
3 changed files with 81 additions and 12 deletions

1
AI-canary.txt Normal file
View file

@ -0,0 +1 @@
This pull request is AI generated

View file

@ -35,9 +35,11 @@ Making a new release? Simply add the new header with the version and date undern
* Fixed `rojo serve` silently stopping syncing file changes on Windows when the served project path was a verbatim (`\\?\`) path, because tree paths and file-watcher event paths were canonicalized to different forms. ([#1290])
* Fixed `rojo sourcemap --absolute` emitting verbatim (`\\?\`) paths on Windows, which broke require types in luau-lsp. ([#1290])
* The plugin now disables the `Check for Updates` setting if you block access to `api.github.com`. ([#1297])
* Fixed `rojo serve` crashing when queued filesystem events refer to paths whose ancestors have already been removed. ([#1314])
[#1290]: https://github.com/rojo-rbx/rojo/pull/1290
[#1297]: https://github.com/rojo-rbx/rojo/pull/1297
[#1314]: https://github.com/rojo-rbx/rojo/issues/1314
## [7.7.0] (July 1st, 2026)

View file

@ -2,9 +2,9 @@ use crossbeam_channel::{select, Receiver, RecvError, Sender};
use jod_thread::JoinHandle;
use memofs::{IoResultExt, Vfs, VfsEvent};
use rbx_dom_weak::types::{Ref, Variant};
use std::path::PathBuf;
use std::{
fs,
fs, io,
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
@ -113,6 +113,31 @@ struct JobThreadContext {
message_queue: Arc<MessageQueue<AppliedPatchSet>>,
}
/// Canonicalizes as much of an event path as still exists.
///
/// Filesystem events can become stale while they wait to be processed. For
/// example, recursively removing a directory can remove a child's parent
/// before the child's event is handled. Canonicalizing the nearest existing
/// ancestor preserves the normalized prefix used by the tree while allowing
/// the missing suffix to still drive reconciliation.
fn canonicalize_event_path(vfs: &Vfs, path: &Path) -> io::Result<PathBuf> {
let mut ancestor = path;
loop {
match vfs.canonicalize(ancestor) {
Ok(canonical_ancestor) => {
let missing_suffix = path.strip_prefix(ancestor).map_err(io::Error::other)?;
return Ok(canonical_ancestor.join(missing_suffix));
}
Err(err) if err.kind() == io::ErrorKind::NotFound => match ancestor.parent() {
Some(parent) => ancestor = parent,
None => return Err(err),
},
Err(err) => return Err(err),
}
}
}
impl JobThreadContext {
/// Computes and applies patches to the DOM for a given file path.
///
@ -168,16 +193,18 @@ impl JobThreadContext {
// For a given VFS event, we might have many changes to different parts
// of the tree. Calculate and apply all of these changes.
let applied_patches = match event {
VfsEvent::Create(path) | VfsEvent::Write(path) => {
self.apply_patches(self.vfs.canonicalize(&path).unwrap())
}
VfsEvent::Remove(path) => {
// MemoFS does not track parent removals yet, so we can canonicalize
// the parent path safely and then append the removed path's file name.
let parent = path.parent().unwrap();
let file_name = path.file_name().unwrap();
let parent_normalized = self.vfs.canonicalize(parent).unwrap();
self.apply_patches(parent_normalized.join(file_name))
VfsEvent::Create(path) | VfsEvent::Write(path) | VfsEvent::Remove(path) => {
match canonicalize_event_path(&self.vfs, &path) {
Ok(path) => self.apply_patches(path),
Err(err) => {
log::error!(
"Could not canonicalize filesystem event path {}: {}",
path.display(),
err
);
Vec::new()
}
}
}
_ => {
log::warn!("Unhandled VFS event: {:?}", event);
@ -383,3 +410,42 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<
Some(applied_patch_set)
}
#[cfg(test)]
mod test {
use super::*;
use memofs::{InMemoryFs, VfsSnapshot};
use crate::snapshot::InstanceSnapshot;
fn stale_event_context() -> JobThreadContext {
let mut backend = InMemoryFs::new();
backend
.load_snapshot("/project", VfsSnapshot::empty_dir())
.unwrap();
JobThreadContext {
tree: Arc::new(Mutex::new(RojoTree::new(InstanceSnapshot::new()))),
vfs: Arc::new(Vfs::new(backend)),
message_queue: Arc::new(MessageQueue::new()),
}
}
#[test]
fn stale_remove_event_does_not_panic_when_parent_was_removed() {
let context = stale_event_context();
let path = PathBuf::from("/project/out/server/deep/file.luau");
assert_eq!(canonicalize_event_path(&context.vfs, &path).unwrap(), path);
context.handle_vfs_event(VfsEvent::Remove(path));
}
#[test]
fn stale_write_event_does_not_panic_when_path_was_removed() {
let context = stale_event_context();
context.handle_vfs_event(VfsEvent::Write("/project/out/server/deep/file.luau".into()));
}
}