mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 14:01:37 +00:00
- Updated the `suppressed_paths` structure in `ChangeProcessor` and `ApiService` to store counts for both remove and create/write events, enhancing the accuracy of event suppression. - Refactored methods for suppressing and unsuppressing paths to accommodate the new structure, ensuring that the system correctly manages VFS events based on the type of operation. - Improved documentation and comments to clarify the purpose and behavior of the suppression logic, contributing to better maintainability and understanding of the code.
290 lines
9.5 KiB
Rust
290 lines
9.5 KiB
Rust
use std::{
|
|
collections::HashSet,
|
|
io,
|
|
net::IpAddr,
|
|
path::Path,
|
|
sync::{Arc, Mutex, MutexGuard},
|
|
time::Instant,
|
|
};
|
|
|
|
use crossbeam_channel::Sender;
|
|
use memofs::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::snapshot_from_vfs,
|
|
};
|
|
|
|
/// 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.
|
|
///
|
|
/// Allowed to be unused because it has side effects when dropped.
|
|
#[allow(unused)]
|
|
change_processor: 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.
|
|
tree_mutation_sender: 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.
|
|
#[allow(dead_code)]
|
|
suppressed_paths: Arc<Mutex<std::collections::HashMap<std::path::PathBuf, (usize, usize)>>>,
|
|
}
|
|
|
|
impl ServeSession {
|
|
/// 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) -> Result<Self, ServeSessionError> {
|
|
let start_path = start_path.as_ref();
|
|
let start_time = Instant::now();
|
|
|
|
log::trace!("Starting new ServeSession at path {}", start_path.display());
|
|
|
|
let root_project = Project::load_initial_project(&vfs, start_path)?;
|
|
|
|
let mut tree = RojoTree::new(InstanceSnapshot::new());
|
|
|
|
let root_id = tree.get_root_id();
|
|
|
|
let decode_windows_invalid_chars = root_project
|
|
.syncback_rules
|
|
.as_ref()
|
|
.map(|rules| rules.encode_windows_invalid_chars())
|
|
.unwrap_or(true);
|
|
|
|
let mut instance_context = InstanceContext::new();
|
|
instance_context.set_decode_windows_invalid_chars(decode_windows_invalid_chars);
|
|
|
|
log::trace!("Generating snapshot of instances from VFS");
|
|
let snapshot = snapshot_from_vfs(&instance_context, &vfs, start_path)?;
|
|
|
|
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);
|
|
|
|
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()));
|
|
|
|
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),
|
|
);
|
|
|
|
Ok(Self {
|
|
change_processor,
|
|
start_time,
|
|
session_id,
|
|
root_project,
|
|
tree,
|
|
message_queue,
|
|
tree_mutation_sender,
|
|
vfs,
|
|
suppressed_paths,
|
|
})
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
|
|
#[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 project_port(&self) -> Option<u16> {
|
|
self.root_project.serve_port
|
|
}
|
|
|
|
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 serve_address(&self) -> Option<IpAddr> {
|
|
self.root_project.serve_address
|
|
}
|
|
|
|
pub fn root_dir(&self) -> &Path {
|
|
self.root_project.folder_location()
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
#[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,
|
|
},
|
|
}
|