Files
rojo/src/serve_session.rs
Lucien Greathouse da05078ff3 Load project file from VFS instead of through fs.
Fixes #320.

Previously, the root project file was loaded via methods on Project
(which do not know about the VFS) instead of through the VFS like
all other disk access.

This meant that Rojo was unable to build its own plugin because
there is no project file on the real disk, only in the VFS.
2020-06-17 23:13:29 -07:00

242 lines
7.6 KiB
Rust

use std::{
borrow::Cow,
collections::HashSet,
io,
path::{Path, PathBuf},
sync::{Arc, Mutex, MutexGuard},
time::Instant,
};
use crossbeam_channel::Sender;
use memofs::IoResultExt;
use memofs::Vfs;
use rbx_dom_weak::RbxInstanceProperties;
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,
InstancePropertiesWithMeta, PatchSet, RojoTree,
},
snapshot_middleware::{snapshot_from_vfs, SnapshotError},
};
/// Contains all of the state for a Rojo serve session.
///
/// 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.json` file was
/// used for starting the serve session, or if the user specified a full
/// path to a `.project.json` 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>,
}
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 project_path;
if Project::is_project_file(start_path) {
project_path = Cow::Borrowed(start_path);
} else {
project_path = Cow::Owned(start_path.join("default.project.json"));
}
log::debug!("Loading project file from {}", project_path.display());
let root_project = match vfs.read(&project_path).with_not_found()? {
Some(contents) => Project::load_from_slice(&contents, &project_path)?,
None => {
return Err(ServeSessionError::NoProjectFound {
path: project_path.to_path_buf(),
});
}
};
let mut tree = RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "ROOT".to_owned(),
class_name: "Folder".to_owned(),
properties: Default::default(),
},
metadata: Default::default(),
});
let root_id = tree.get_root_id();
let instance_context = InstanceContext::default();
log::trace!("Generating snapshot of instances from VFS");
let snapshot = snapshot_from_vfs(&instance_context, &vfs, &start_path)?
.expect("snapshot did not return an instance");
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();
log::trace!("Starting ChangeProcessor");
let change_processor = ChangeProcessor::start(
Arc::clone(&tree),
Arc::clone(&vfs),
Arc::clone(&message_queue),
tree_mutation_receiver,
);
Ok(Self {
change_processor,
start_time,
session_id,
root_project,
tree,
message_queue,
tree_mutation_sender,
vfs,
})
}
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()
}
#[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
}
pub fn project_port(&self) -> Option<u16> {
self.root_project.serve_port
}
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()
}
}
#[derive(Debug, Error)]
pub enum ServeSessionError {
#[error(
"Rojo requires a project file, but no project file was found in path {}\n\
See https://rojo.space/docs/ for guides and documentation.",
.path.display()
)]
NoProjectFound { path: PathBuf },
#[error(transparent)]
Io {
#[from]
source: io::Error,
},
#[error(transparent)]
Project {
#[from]
source: ProjectError,
},
#[error(transparent)]
Snapshot {
#[from]
source: SnapshotError,
},
}