mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-22 21:41:37 +00:00
- Updated the `prefetch_project_files` function to perform a shallow walk of the project folder, limiting the depth to enhance performance and avoid unnecessary recursion into specific directories. - Improved the logic for handling $path roots, ensuring correct canonicalization and efficient traversal based on the relationship to the project folder. - Adjusted the VFS watch setup to watch the project folder non-recursively while allowing recursive watching for $path roots, optimizing resource usage during file monitoring. These changes enhance the accuracy and performance of file prefetching and watching in the VFS.
730 lines
26 KiB
Rust
730 lines
26 KiB
Rust
use std::{
|
|
collections::HashSet,
|
|
io,
|
|
path::{Path, PathBuf},
|
|
sync::{Arc, Mutex, MutexGuard},
|
|
time::Instant,
|
|
};
|
|
|
|
use crossbeam_channel::Sender;
|
|
use memofs::{PrefetchCache, Vfs};
|
|
use thiserror::Error;
|
|
|
|
use crate::{
|
|
change_processor::ChangeProcessor,
|
|
message_queue::MessageQueue,
|
|
project::{Project, ProjectError},
|
|
session_id::SessionId,
|
|
snapshot::{
|
|
apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext, InstanceSnapshot,
|
|
PatchSet, RojoTree,
|
|
},
|
|
snapshot_middleware::{is_script_relevant_path, snapshot_from_vfs, INIT_FILE_PRIORITY},
|
|
};
|
|
|
|
/// Set to `true` to validate on plugin connect (useful for testing, do not enable on production).
|
|
const VALIDATE_TREE_ON_CONNECT: bool = false;
|
|
|
|
/// Result of a read-only tree freshness check. Reports how many instances
|
|
/// differ between the in-memory tree and the real filesystem.
|
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct TreeFreshnessReport {
|
|
pub is_fresh: bool,
|
|
pub added: usize,
|
|
pub removed: usize,
|
|
pub updated: usize,
|
|
pub elapsed_ms: f64,
|
|
}
|
|
|
|
/// Contains all of the state for a Rojo serve session. A serve session is used
|
|
/// when we need to build a Rojo tree and possibly rebuild it when input files
|
|
/// change.
|
|
///
|
|
/// Nothing here is specific to any Rojo interface. Though the primary way to
|
|
/// interact with a serve session is Rojo's HTTP right now, there's no reason
|
|
/// why Rojo couldn't expose an IPC or channels-based API for embedding in the
|
|
/// future. `ServeSession` would be roughly the right interface to expose for
|
|
/// those cases.
|
|
pub struct ServeSession {
|
|
/// The object responsible for listening to changes from the in-memory
|
|
/// filesystem, applying them, updating the Roblox instance tree, and
|
|
/// routing messages through the session's message queue to any connected
|
|
/// clients.
|
|
///
|
|
/// SHOULD BE DROPPED FIRST! ServeSession and ChangeProcessor communicate
|
|
/// with eachother via channels. If ServeSession hangs up those channels
|
|
/// before dropping the ChangeProcessor, its thread will panic with a
|
|
/// RecvError, causing the main thread to panic on drop.
|
|
///
|
|
/// `None` for oneshot sessions (syncback, upload, plugin install) that
|
|
/// don't need live filesystem monitoring.
|
|
///
|
|
/// Allowed to be unused because it has side effects when dropped.
|
|
#[allow(unused)]
|
|
change_processor: Option<ChangeProcessor>,
|
|
|
|
/// When the serve session was started. Used only for user-facing
|
|
/// diagnostics.
|
|
start_time: Instant,
|
|
|
|
/// The root project for the serve session.
|
|
///
|
|
/// This will be defined if a folder with a `default.project.json5` file was
|
|
/// used for starting the serve session, or if the user specified a full
|
|
/// path to a `.project.json5` file.
|
|
root_project: Project,
|
|
|
|
/// A randomly generated ID for this serve session. It's used to ensure that
|
|
/// a client doesn't begin connecting to a different server part way through
|
|
/// an operation that needs to be atomic.
|
|
session_id: SessionId,
|
|
|
|
/// The tree of Roblox instances associated with this session that will be
|
|
/// updated in real-time. This is derived from the session's VFS and will
|
|
/// eventually be mutable to connected clients.
|
|
tree: Arc<Mutex<RojoTree>>,
|
|
|
|
/// An in-memory filesystem containing all of the files relevant for this
|
|
/// live session.
|
|
///
|
|
/// The main use for accessing it from the session is for debugging issues
|
|
/// with Rojo's live-sync protocol.
|
|
vfs: Arc<Vfs>,
|
|
|
|
/// A queue of changes that have been applied to `tree` that affect clients.
|
|
///
|
|
/// Clients to the serve session will subscribe to this queue either
|
|
/// directly or through the HTTP API to be notified of mutations that need
|
|
/// to be applied.
|
|
message_queue: Arc<MessageQueue<AppliedPatchSet>>,
|
|
|
|
/// A channel to send mutation requests on. These will be handled by the
|
|
/// ChangeProcessor and trigger changes in the tree.
|
|
/// `None` for oneshot sessions.
|
|
tree_mutation_sender: Option<Sender<PatchSet>>,
|
|
|
|
/// Paths recently written by the API's syncback. The ChangeProcessor
|
|
/// checks this map and suppresses the file watcher echo for these paths
|
|
/// to avoid redundant re-snapshots and WebSocket messages.
|
|
/// Values are `(remove_count, create_write_count)` — each API write increments
|
|
/// the appropriate counter, each suppressed VFS event decrements it.
|
|
/// `None` for oneshot sessions.
|
|
#[allow(dead_code, clippy::type_complexity)]
|
|
suppressed_paths:
|
|
Option<Arc<Mutex<std::collections::HashMap<std::path::PathBuf, (usize, usize)>>>>,
|
|
|
|
/// Index of meta/model files that contain `Rojo_Ref_*` attributes.
|
|
/// Shared between ApiService (writes) and ChangeProcessor (rename updates).
|
|
/// `None` for oneshot sessions.
|
|
ref_path_index: Option<Arc<Mutex<crate::RefPathIndex>>>,
|
|
|
|
/// Root of the git repository, if the project is inside one.
|
|
/// Computed once at session start for use by auto-staging.
|
|
git_repo_root: Option<std::path::PathBuf>,
|
|
|
|
/// HEAD commit SHA at the time this serve session started.
|
|
/// Used as diff baseline so that files committed after session start
|
|
/// still appear in `changedIds` for auto-select defaults.
|
|
initial_head_commit: Option<String>,
|
|
}
|
|
|
|
/// Collect all filesystem paths reachable from the project tree's `$path`
|
|
/// entries, then read file contents in parallel.
|
|
fn prefetch_project_files(project: &Project, sync_scripts_only: bool) -> io::Result<PrefetchCache> {
|
|
use rayon::prelude::*;
|
|
use std::collections::HashMap;
|
|
use walkdir::WalkDir;
|
|
|
|
let folder = project.folder_location();
|
|
|
|
let mut roots: Vec<std::path::PathBuf> = Vec::new();
|
|
collect_path_roots(&project.tree, folder, &mut roots);
|
|
|
|
roots.sort();
|
|
let mut deduped: Vec<std::path::PathBuf> = Vec::new();
|
|
for root in roots {
|
|
if !deduped.iter().any(|existing| root.starts_with(existing)) {
|
|
deduped.push(root);
|
|
}
|
|
}
|
|
let roots = deduped;
|
|
|
|
if roots.is_empty() {
|
|
return Ok(PrefetchCache {
|
|
files: HashMap::new(),
|
|
is_file: HashMap::new(),
|
|
children: HashMap::new(),
|
|
dir_init: HashMap::new(),
|
|
});
|
|
}
|
|
|
|
let walk_start = Instant::now();
|
|
|
|
let mut entries: Vec<walkdir::DirEntry> = Vec::new();
|
|
|
|
// Shallow walk of project folder: just the folder entry + direct
|
|
// children. Covers the project file and top-level siblings (nested
|
|
// project files). One readdir syscall -- NOT recursive into
|
|
// target/, .git/, node_modules/, etc.
|
|
if folder.exists() {
|
|
for e in WalkDir::new(folder)
|
|
.follow_links(true)
|
|
.max_depth(1)
|
|
.into_iter()
|
|
.flatten()
|
|
{
|
|
entries.push(e);
|
|
}
|
|
}
|
|
|
|
// Recursive walk of each $path root. Canonicalize to resolve ".."
|
|
// before the equality/starts_with checks.
|
|
let canonical_folder = std::fs::canonicalize(folder).unwrap_or_else(|_| folder.to_path_buf());
|
|
for root in &roots {
|
|
if !root.exists() {
|
|
continue;
|
|
}
|
|
let canonical_root = std::fs::canonicalize(root).unwrap_or_else(|_| root.to_path_buf());
|
|
if canonical_root == canonical_folder {
|
|
// $path points to the project folder itself -- shallow walk
|
|
// already covers depth 0-1, fill in the subtree.
|
|
for e in WalkDir::new(root)
|
|
.follow_links(true)
|
|
.min_depth(2)
|
|
.into_iter()
|
|
.flatten()
|
|
{
|
|
entries.push(e);
|
|
}
|
|
} else {
|
|
for e in WalkDir::new(root).follow_links(true).into_iter().flatten() {
|
|
entries.push(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
let walk_elapsed = walk_start.elapsed();
|
|
let file_count = entries.iter().filter(|e| e.file_type().is_file()).count();
|
|
let dir_count = entries.len() - file_count;
|
|
|
|
log::debug!(
|
|
"Prefetch walk: {} files + {} dirs from {} root(s) in {:.1?}",
|
|
file_count,
|
|
dir_count,
|
|
roots.len(),
|
|
walk_elapsed,
|
|
);
|
|
|
|
let read_start = Instant::now();
|
|
|
|
let file_data: Vec<_> = entries
|
|
.par_iter()
|
|
.filter(|e| e.file_type().is_file())
|
|
.filter(|e| !sync_scripts_only || is_script_relevant_path(e.path()))
|
|
.filter_map(|e| {
|
|
let path = e.path().to_path_buf();
|
|
std::fs::read(&path).ok().map(|c| (path, c))
|
|
})
|
|
.collect();
|
|
|
|
let read_elapsed = read_start.elapsed();
|
|
|
|
log::debug!(
|
|
"Prefetch I/O: read {} files in {:.1?}",
|
|
file_data.len(),
|
|
read_elapsed,
|
|
);
|
|
|
|
let mut is_file_map: HashMap<std::path::PathBuf, bool> = HashMap::with_capacity(entries.len());
|
|
let mut children_map: HashMap<std::path::PathBuf, Vec<std::path::PathBuf>> =
|
|
HashMap::with_capacity(dir_count);
|
|
|
|
for entry in &entries {
|
|
let path = entry.path().to_path_buf();
|
|
is_file_map.insert(path.clone(), entry.file_type().is_file());
|
|
|
|
if entry.depth() > 0 {
|
|
if let Some(parent) = entry.path().parent() {
|
|
children_map
|
|
.entry(parent.to_path_buf())
|
|
.or_default()
|
|
.push(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
for children in children_map.values_mut() {
|
|
children.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
|
|
}
|
|
|
|
log::debug!(
|
|
"Prefetch maps: {} is_file + {} children dirs built",
|
|
is_file_map.len(),
|
|
children_map.len(),
|
|
);
|
|
|
|
let mut dir_init_map: HashMap<std::path::PathBuf, Option<(String, std::path::PathBuf)>> =
|
|
HashMap::with_capacity(children_map.len());
|
|
for (dir_path, child_paths) in &children_map {
|
|
let child_names: std::collections::HashSet<&str> = child_paths
|
|
.iter()
|
|
.filter_map(|p| p.file_name().and_then(|n| n.to_str()))
|
|
.collect();
|
|
let mut found = None;
|
|
for &(_, init_name) in INIT_FILE_PRIORITY {
|
|
if child_names.contains(init_name) {
|
|
found = Some((init_name.to_string(), dir_path.join(init_name)));
|
|
break;
|
|
}
|
|
}
|
|
dir_init_map.insert(dir_path.clone(), found);
|
|
}
|
|
|
|
Ok(PrefetchCache {
|
|
files: file_data.into_iter().collect::<HashMap<_, _>>(),
|
|
is_file: is_file_map,
|
|
children: children_map,
|
|
dir_init: dir_init_map,
|
|
})
|
|
}
|
|
|
|
/// Recursively collect all `$path` directories from the project tree.
|
|
pub fn collect_path_roots(node: &crate::project::ProjectNode, base: &Path, out: &mut Vec<PathBuf>) {
|
|
if let Some(path_node) = &node.path {
|
|
let resolved = base.join(path_node.path());
|
|
out.push(resolved);
|
|
}
|
|
for child_node in node.children.values() {
|
|
collect_path_roots(child_node, base, out);
|
|
}
|
|
}
|
|
|
|
impl ServeSession {
|
|
/// Shared initialization: loads the project and builds the initial
|
|
/// snapshot tree. Used by both `new()` and `new_oneshot()`.
|
|
fn init_tree(vfs: &Vfs, start_path: &Path) -> Result<(Project, RojoTree), ServeSessionError> {
|
|
log::trace!("Starting new ServeSession at path {}", start_path.display());
|
|
|
|
let root_project = Project::load_initial_project(vfs, start_path)?;
|
|
let sync_scripts_only = root_project.sync_scripts_only.unwrap_or(false);
|
|
|
|
if std::env::var("ATLAS_SEQUENTIAL").is_err() {
|
|
let prefetch_start = Instant::now();
|
|
match prefetch_project_files(&root_project, sync_scripts_only) {
|
|
Ok(cache) => {
|
|
let count = cache.files.len();
|
|
log::debug!(
|
|
"Prefetch total: {} files in {:.1?}",
|
|
count,
|
|
prefetch_start.elapsed()
|
|
);
|
|
if !cache.is_file.is_empty() {
|
|
vfs.set_prefetch_cache(cache);
|
|
}
|
|
}
|
|
Err(err) => {
|
|
log::warn!("Prefetch failed, falling back to sequential reads: {err}");
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut tree = RojoTree::new(InstanceSnapshot::new());
|
|
let root_id = tree.get_root_id();
|
|
let mut instance_context = InstanceContext::new();
|
|
instance_context.sync_scripts_only = sync_scripts_only;
|
|
|
|
let mut path_roots = Vec::new();
|
|
collect_path_roots(
|
|
&root_project.tree,
|
|
root_project.folder_location(),
|
|
&mut path_roots,
|
|
);
|
|
|
|
let snap_start = Instant::now();
|
|
log::trace!("Generating snapshot of instances from VFS");
|
|
vfs.set_watch_enabled(false);
|
|
let snapshot = snapshot_from_vfs(&instance_context, vfs, start_path)?;
|
|
vfs.set_watch_enabled(true);
|
|
log::debug!("Snapshot built in {:.1?}", snap_start.elapsed());
|
|
|
|
vfs.clear_prefetch_cache();
|
|
|
|
let patch_start = Instant::now();
|
|
let project_folder = root_project.folder_location().to_path_buf();
|
|
std::thread::scope(|s| {
|
|
s.spawn(|| {
|
|
let watch_start = Instant::now();
|
|
// Watch each $path root recursively (NOT the project folder --
|
|
// that would walk target/, .git/, etc. for repo-root projects).
|
|
for root in &path_roots {
|
|
let _ = vfs.watch(root);
|
|
}
|
|
// Watch project folder non-recursively for project file edits.
|
|
vfs.set_watch_recursive(false);
|
|
let _ = vfs.watch(&project_folder);
|
|
vfs.set_watch_recursive(true);
|
|
log::debug!(
|
|
"Set up {} watches in {:.1?}",
|
|
path_roots.len() + 1,
|
|
watch_start.elapsed(),
|
|
);
|
|
});
|
|
|
|
log::trace!("Computing initial patch set");
|
|
let patch_set = compute_patch_set(snapshot, &tree, root_id);
|
|
|
|
log::trace!("Applying initial patch set");
|
|
apply_patch_set(&mut tree, patch_set);
|
|
});
|
|
log::debug!("Watch + patch in {:.1?}", patch_start.elapsed());
|
|
|
|
Ok((root_project, tree))
|
|
}
|
|
|
|
/// Start a new serve session from the given in-memory filesystem and start
|
|
/// path.
|
|
///
|
|
/// The project file is expected to be loaded out-of-band since it's
|
|
/// currently loaded from the filesystem directly instead of through the
|
|
/// in-memory filesystem layer.
|
|
pub fn new<P: AsRef<Path>>(
|
|
vfs: Vfs,
|
|
start_path: P,
|
|
critical_error_receiver: Option<crossbeam_channel::Receiver<memofs::WatcherCriticalError>>,
|
|
) -> Result<Self, ServeSessionError> {
|
|
let start_path = start_path.as_ref();
|
|
let start_time = Instant::now();
|
|
|
|
let (root_project, tree) = Self::init_tree(&vfs, start_path)?;
|
|
|
|
let session_id = SessionId::new();
|
|
let message_queue = MessageQueue::new();
|
|
|
|
let tree = Arc::new(Mutex::new(tree));
|
|
let message_queue = Arc::new(message_queue);
|
|
let vfs = Arc::new(vfs);
|
|
|
|
let (tree_mutation_sender, tree_mutation_receiver) = crossbeam_channel::unbounded();
|
|
let suppressed_paths = Arc::new(Mutex::new(std::collections::HashMap::new()));
|
|
let ref_path_index = {
|
|
let mut index = crate::RefPathIndex::new();
|
|
let tree_guard = tree.lock().unwrap();
|
|
let mut ref_roots = Vec::new();
|
|
collect_path_roots(
|
|
&root_project.tree,
|
|
root_project.folder_location(),
|
|
&mut ref_roots,
|
|
);
|
|
for root in &ref_roots {
|
|
index.populate_from_dir(root, &tree_guard);
|
|
}
|
|
drop(tree_guard);
|
|
Arc::new(Mutex::new(index))
|
|
};
|
|
|
|
let git_repo_root = crate::git::git_repo_root(root_project.folder_location());
|
|
let initial_head_commit = git_repo_root
|
|
.as_deref()
|
|
.and_then(crate::git::git_head_commit);
|
|
|
|
log::trace!("Starting ChangeProcessor");
|
|
let change_processor = ChangeProcessor::start(
|
|
Arc::clone(&tree),
|
|
Arc::clone(&vfs),
|
|
Arc::clone(&message_queue),
|
|
tree_mutation_receiver,
|
|
Arc::clone(&suppressed_paths),
|
|
Arc::clone(&ref_path_index),
|
|
root_project.folder_location().to_path_buf(),
|
|
root_project.file_location.clone(),
|
|
critical_error_receiver,
|
|
git_repo_root.clone(),
|
|
root_project.sync_scripts_only.unwrap_or(false),
|
|
);
|
|
|
|
Ok(Self {
|
|
change_processor: Some(change_processor),
|
|
start_time,
|
|
session_id,
|
|
root_project,
|
|
tree,
|
|
message_queue,
|
|
tree_mutation_sender: Some(tree_mutation_sender),
|
|
vfs,
|
|
suppressed_paths: Some(suppressed_paths),
|
|
ref_path_index: Some(ref_path_index),
|
|
git_repo_root,
|
|
initial_head_commit,
|
|
})
|
|
}
|
|
|
|
/// Create a lightweight oneshot session that builds the project tree
|
|
/// but does NOT start a ChangeProcessor thread or filesystem watcher.
|
|
///
|
|
/// Use this for commands that only need a snapshot of the tree and
|
|
/// don't require live updates (syncback, upload, plugin install).
|
|
pub fn new_oneshot<P: AsRef<Path>>(vfs: Vfs, start_path: P) -> Result<Self, ServeSessionError> {
|
|
let start_path = start_path.as_ref();
|
|
let start_time = Instant::now();
|
|
|
|
let (root_project, tree) = Self::init_tree(&vfs, start_path)?;
|
|
|
|
Ok(Self {
|
|
change_processor: None,
|
|
start_time,
|
|
session_id: SessionId::new(),
|
|
root_project,
|
|
tree: Arc::new(Mutex::new(tree)),
|
|
message_queue: Arc::new(MessageQueue::new()),
|
|
tree_mutation_sender: None,
|
|
vfs: Arc::new(vfs),
|
|
suppressed_paths: None,
|
|
ref_path_index: None,
|
|
git_repo_root: None,
|
|
initial_head_commit: None,
|
|
})
|
|
}
|
|
|
|
pub fn tree_handle(&self) -> Arc<Mutex<RojoTree>> {
|
|
Arc::clone(&self.tree)
|
|
}
|
|
|
|
pub fn tree(&self) -> MutexGuard<'_, RojoTree> {
|
|
self.tree.lock().unwrap()
|
|
}
|
|
|
|
pub fn tree_mutation_sender(&self) -> Sender<PatchSet> {
|
|
self.tree_mutation_sender
|
|
.clone()
|
|
.expect("tree_mutation_sender is not available on oneshot sessions")
|
|
}
|
|
|
|
/// Returns a handle to the suppressed paths map, used to avoid
|
|
/// file watcher echo when the API writes files to disk.
|
|
#[allow(dead_code)]
|
|
pub fn suppressed_paths(
|
|
&self,
|
|
) -> Arc<Mutex<std::collections::HashMap<std::path::PathBuf, (usize, usize)>>> {
|
|
Arc::clone(
|
|
self.suppressed_paths
|
|
.as_ref()
|
|
.expect("suppressed_paths is not available on oneshot sessions"),
|
|
)
|
|
}
|
|
|
|
/// Returns a handle to the Ref path index, shared between ApiService
|
|
/// and ChangeProcessor for efficient rename path updates.
|
|
pub fn ref_path_index(&self) -> Arc<Mutex<crate::RefPathIndex>> {
|
|
Arc::clone(
|
|
self.ref_path_index
|
|
.as_ref()
|
|
.expect("ref_path_index is not available on oneshot sessions"),
|
|
)
|
|
}
|
|
|
|
#[allow(unused)]
|
|
pub fn vfs(&self) -> &Vfs {
|
|
&self.vfs
|
|
}
|
|
|
|
pub fn message_queue(&self) -> &MessageQueue<AppliedPatchSet> {
|
|
&self.message_queue
|
|
}
|
|
|
|
pub fn session_id(&self) -> SessionId {
|
|
self.session_id
|
|
}
|
|
|
|
pub fn project_name(&self) -> &str {
|
|
self.root_project
|
|
.name
|
|
.as_ref()
|
|
.expect("all top-level projects must have their name set")
|
|
}
|
|
|
|
pub fn place_id(&self) -> Option<u64> {
|
|
self.root_project.place_id
|
|
}
|
|
|
|
pub fn game_id(&self) -> Option<u64> {
|
|
self.root_project.game_id
|
|
}
|
|
|
|
pub fn start_time(&self) -> Instant {
|
|
self.start_time
|
|
}
|
|
|
|
pub fn serve_place_ids(&self) -> Option<&HashSet<u64>> {
|
|
self.root_project.serve_place_ids.as_ref()
|
|
}
|
|
|
|
pub fn blocked_place_ids(&self) -> Option<&HashSet<u64>> {
|
|
self.root_project.blocked_place_ids.as_ref()
|
|
}
|
|
|
|
pub fn root_dir(&self) -> &Path {
|
|
self.root_project.folder_location()
|
|
}
|
|
|
|
pub fn git_repo_root(&self) -> Option<&Path> {
|
|
self.git_repo_root.as_deref()
|
|
}
|
|
|
|
pub fn initial_head_commit(&self) -> Option<&str> {
|
|
self.initial_head_commit.as_deref()
|
|
}
|
|
|
|
pub fn root_project(&self) -> &Project {
|
|
&self.root_project
|
|
}
|
|
|
|
/// Returns whether sync should only include script instances.
|
|
/// When enabled, only Script, LocalScript, and ModuleScript are synced.
|
|
pub fn sync_scripts_only(&self) -> bool {
|
|
self.root_project.sync_scripts_only.unwrap_or(false)
|
|
}
|
|
|
|
/// Returns whether hidden/internal services should be ignored during sync.
|
|
/// When enabled, only "visible" services like Workspace, ReplicatedStorage, etc.
|
|
/// are included in sync operations.
|
|
///
|
|
/// Checks root-level `ignoreHiddenServices` first, then falls back to
|
|
/// `syncbackRules.ignoreHiddenServices` for backward compatibility.
|
|
/// Defaults to `true` if neither is specified.
|
|
pub fn ignore_hidden_services(&self) -> bool {
|
|
// Root-level setting takes precedence
|
|
if let Some(value) = self.root_project.ignore_hidden_services {
|
|
return value;
|
|
}
|
|
// Fall back to syncbackRules for backward compatibility
|
|
self.root_project
|
|
.syncback_rules
|
|
.as_ref()
|
|
.map(|rules| rules.ignore_hidden_services())
|
|
.unwrap_or(true)
|
|
}
|
|
|
|
/// Read-only check: re-snapshots from disk and returns how many
|
|
/// instances differ between the in-memory tree and the real filesystem.
|
|
/// Does NOT apply corrections — the tree is left unchanged.
|
|
pub fn check_tree_freshness(&self) -> TreeFreshnessReport {
|
|
let start = Instant::now();
|
|
let start_path: &Path = &self.root_project.file_location;
|
|
let mut instance_context = InstanceContext::new();
|
|
instance_context.sync_scripts_only = self.sync_scripts_only();
|
|
|
|
let snapshot = match snapshot_from_vfs(&instance_context, &self.vfs, start_path) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
log::error!("Tree freshness check snapshot error: {:?}", e);
|
|
return TreeFreshnessReport {
|
|
is_fresh: false,
|
|
added: 0,
|
|
removed: 0,
|
|
updated: 0,
|
|
elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
|
|
};
|
|
}
|
|
};
|
|
|
|
let tree = self.tree.lock().unwrap();
|
|
let root_id = tree.get_root_id();
|
|
let patch_set = compute_patch_set(snapshot, &tree, root_id);
|
|
|
|
let added = patch_set.added_instances.len();
|
|
let removed = patch_set.removed_instances.len();
|
|
let updated = patch_set.updated_instances.len();
|
|
|
|
TreeFreshnessReport {
|
|
is_fresh: added == 0 && removed == 0 && updated == 0,
|
|
added,
|
|
removed,
|
|
updated,
|
|
elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
|
|
}
|
|
}
|
|
|
|
/// Re-snapshots the project tree from the real filesystem and patches
|
|
/// the in-memory tree to correct any drift caused by missed VFS watcher
|
|
/// events. Called on plugin connect to guarantee the tree is fresh.
|
|
///
|
|
/// Skipped on freshly-started sessions (< 5 s) where the tree is
|
|
/// guaranteed correct and the VFS watcher should handle all changes.
|
|
///
|
|
/// Controlled by [`VALIDATE_TREE_ON_CONNECT`]; set that constant to
|
|
/// `false` to disable this entirely during testing.
|
|
pub fn validate_tree(&self) -> Vec<AppliedPatchSet> {
|
|
if !VALIDATE_TREE_ON_CONNECT {
|
|
log::debug!("Tree validation skipped (VALIDATE_TREE_ON_CONNECT = false)");
|
|
return Vec::new();
|
|
}
|
|
|
|
// On a freshly-started session the tree was just built from the
|
|
// filesystem and cannot be stale. Skip validation to avoid racing
|
|
// with the VFS watcher on early file changes.
|
|
const MIN_SESSION_AGE: std::time::Duration = std::time::Duration::from_secs(5);
|
|
if self.start_time.elapsed() < MIN_SESSION_AGE {
|
|
log::debug!(
|
|
"Tree validation skipped (session age {:.1?} < {:.1?})",
|
|
self.start_time.elapsed(),
|
|
MIN_SESSION_AGE
|
|
);
|
|
return Vec::new();
|
|
}
|
|
|
|
let start = Instant::now();
|
|
let start_path: &Path = &self.root_project.file_location;
|
|
let mut instance_context = InstanceContext::new();
|
|
instance_context.sync_scripts_only = self.sync_scripts_only();
|
|
|
|
let snapshot = match snapshot_from_vfs(&instance_context, &self.vfs, start_path) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
log::error!("Tree validation snapshot error: {:?}", e);
|
|
return Vec::new();
|
|
}
|
|
};
|
|
|
|
let mut tree = self.tree.lock().unwrap();
|
|
let root_id = tree.get_root_id();
|
|
let patch_set = compute_patch_set(snapshot, &tree, root_id);
|
|
|
|
if patch_set.removed_instances.is_empty()
|
|
&& patch_set.added_instances.is_empty()
|
|
&& patch_set.updated_instances.is_empty()
|
|
{
|
|
log::info!(
|
|
"Tree validation complete (no corrections needed) in {:.1?}",
|
|
start.elapsed()
|
|
);
|
|
return Vec::new();
|
|
}
|
|
|
|
log::info!("Tree validation found stale state, applying corrections");
|
|
let applied = apply_patch_set(&mut tree, patch_set);
|
|
log::info!("Tree validation complete in {:.1?}", start.elapsed());
|
|
vec![applied]
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum ServeSessionError {
|
|
#[error(transparent)]
|
|
Io {
|
|
#[from]
|
|
source: io::Error,
|
|
},
|
|
|
|
#[error(transparent)]
|
|
Project {
|
|
#[from]
|
|
source: ProjectError,
|
|
},
|
|
|
|
#[error(transparent)]
|
|
Other {
|
|
#[from]
|
|
source: anyhow::Error,
|
|
},
|
|
}
|