Fix change_processor crash when path is a directory during two-way sync

The change processor was crashing because fs::write and fs::remove_file
were being called on directory paths instead of file paths. This happens
with init scripts where the instigating source points to the directory
instead of the init file.

Added resolve_init_path to find the actual init file in a directory,
and used remove_dir_all for directory removal.

Fixes #1206
This commit is contained in:
Andrew Vu 2026-02-17 01:40:22 -05:00
parent 9bbb1edd79
commit 4038990227

View file

@ -2,7 +2,7 @@ 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::path::{Path, PathBuf};
use std::{
fs,
sync::{Arc, Mutex},
@ -200,7 +200,19 @@ impl JobThreadContext {
if let Some(instance) = tree.get_instance(id) {
if let Some(instigating_source) = &instance.metadata().instigating_source {
match instigating_source {
InstigatingSource::Path(path) => fs::remove_file(path).unwrap(),
InstigatingSource::Path(path) => {
if path.is_dir() {
if let Err(err) = fs::remove_dir_all(path) {
log::warn!(
"Failed to remove directory {}: {}",
path.display(),
err
);
}
} else {
fs::remove_file(path).unwrap();
}
}
InstigatingSource::ProjectNode { .. } => {
log::warn!(
"Cannot remove instance {:?}, it's from a project file",
@ -244,7 +256,17 @@ impl JobThreadContext {
match instigating_source {
InstigatingSource::Path(path) => {
if let Some(Variant::String(value)) = changed_value {
fs::write(path, value).unwrap();
match resolve_init_path(path) {
Some(resolved) => {
fs::write(&resolved, value).unwrap();
}
None => {
log::warn!(
"Could not resolve init file for directory {}, skipping write",
path.display()
);
}
}
} else {
log::warn!("Cannot change Source to non-string value.");
}
@ -280,6 +302,33 @@ impl JobThreadContext {
}
}
// If the path is a directory, try to find the init file inside it.
// Returns None if it's a directory but no init file exists.
fn resolve_init_path(path: &Path) -> Option<PathBuf> {
if !path.is_dir() {
return Some(path.to_path_buf());
}
const INIT_FILENAMES: &[&str] = &[
"init.luau",
"init.lua",
"init.server.luau",
"init.server.lua",
"init.client.luau",
"init.client.lua",
"init.csv",
];
for filename in INIT_FILENAMES {
let candidate = path.join(filename);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<AppliedPatchSet> {
let metadata = tree
.get_metadata(id)