From 403899022744c093db5c041369ff0b2f0457b482 Mon Sep 17 00:00:00 2001 From: Andrew Vu Date: Tue, 17 Feb 2026 01:40:22 -0500 Subject: [PATCH] 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 --- src/change_processor.rs | 55 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/change_processor.rs b/src/change_processor.rs index 6c5c8224..72d92850 100644 --- a/src/change_processor.rs +++ b/src/change_processor.rs @@ -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 { + 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 { let metadata = tree .get_metadata(id)