mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-20 12:45:05 +00:00
Compare commits
12 Commits
v0.5.0-alp
...
v0.5.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69d1accf3f | ||
|
|
785bdb8ecb | ||
|
|
78a1947cec | ||
|
|
0ff59ecb4e | ||
|
|
b58fed16b4 | ||
|
|
6719be02c3 | ||
|
|
8757834e07 | ||
|
|
aa243d1b8a | ||
|
|
aeb18eb124 | ||
|
|
6c3e118ee3 | ||
|
|
3c0fe4d684 | ||
|
|
12fd9aa1ef |
10
CHANGELOG.md
10
CHANGELOG.md
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.5.0 Alpha 3](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.3) (February 1, 2019)
|
||||||
|
* Changed default project file name from `roblox-project.json` to `default.project.json` ([#120](https://github.com/LPGhatguy/rojo/pull/120))
|
||||||
|
* The old file name will still be supported until 0.5.0 is fully released.
|
||||||
|
* Added warning when loading project files that don't end in `.project.json`
|
||||||
|
* This new extension enables Rojo to distinguish project files from random JSON files, which is necessary to support nested projects.
|
||||||
|
* Added new (empty) diagnostic page served from the server
|
||||||
|
* Added better error messages for when a file is missing that's referenced by a Rojo project
|
||||||
|
* Added support for visualization endpoints returning GraphViz source when Dot is not available
|
||||||
|
* Fixed an in-memory filesystem regression introduced recently ([#119](https://github.com/LPGhatguy/rojo/pull/119))
|
||||||
|
|
||||||
## [0.5.0 Alpha 2](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.2) (January 28, 2019)
|
## [0.5.0 Alpha 2](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.2) (January 28, 2019)
|
||||||
* Added support for `.model.json` files, compatible with 0.4.x
|
* Added support for `.model.json` files, compatible with 0.4.x
|
||||||
* Fixed in-memory filesystem not handling out-of-order filesystem change events
|
* Fixed in-memory filesystem not handling out-of-order filesystem change events
|
||||||
|
|||||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -345,7 +345,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_logger"
|
name = "env_logger"
|
||||||
version = "0.5.13"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
"atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@@ -1248,11 +1248,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rojo"
|
name = "rojo"
|
||||||
version = "0.5.0-alpha.1"
|
version = "0.5.0-alpha.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"csv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"csv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"env_logger 0.5.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
"env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@@ -1929,7 +1929,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
"checksum deflate 0.7.19 (registry+https://github.com/rust-lang/crates.io-index)" = "8a6abb26e16e8d419b5c78662aa9f82857c2386a073da266840e474d5055ec86"
|
"checksum deflate 0.7.19 (registry+https://github.com/rust-lang/crates.io-index)" = "8a6abb26e16e8d419b5c78662aa9f82857c2386a073da266840e474d5055ec86"
|
||||||
"checksum dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6d301140eb411af13d3115f9a562c85cc6b541ade9dfa314132244aaee7489dd"
|
"checksum dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6d301140eb411af13d3115f9a562c85cc6b541ade9dfa314132244aaee7489dd"
|
||||||
"checksum encoding_rs 0.8.14 (registry+https://github.com/rust-lang/crates.io-index)" = "a69d152eaa438a291636c1971b0a370212165ca8a75759eb66818c5ce9b538f7"
|
"checksum encoding_rs 0.8.14 (registry+https://github.com/rust-lang/crates.io-index)" = "a69d152eaa438a291636c1971b0a370212165ca8a75759eb66818c5ce9b538f7"
|
||||||
"checksum env_logger 0.5.13 (registry+https://github.com/rust-lang/crates.io-index)" = "15b0a4d2e39f8420210be8b27eeda28029729e2fd4291019455016c348240c38"
|
"checksum env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "afb070faf94c85d17d50ca44f6ad076bce18ae92f0037d350947240a36e9d42e"
|
||||||
"checksum error-chain 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07e791d3be96241c77c43846b665ef1384606da2cd2a48730abe606a12906e02"
|
"checksum error-chain 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07e791d3be96241c77c43846b665ef1384606da2cd2a48730abe606a12906e02"
|
||||||
"checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2"
|
"checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2"
|
||||||
"checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1"
|
"checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
return {
|
return {
|
||||||
codename = "Epiphany",
|
codename = "Epiphany",
|
||||||
version = {0, 5, 0, "-alpha.2"},
|
version = {0, 5, 0, "-alpha.3"},
|
||||||
expectedServerVersionString = "0.5.0 or newer",
|
expectedServerVersionString = "0.5.0 or newer",
|
||||||
protocolVersion = 2,
|
protocolVersion = 2,
|
||||||
defaultHost = "localhost",
|
defaultHost = "localhost",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rojo"
|
name = "rojo"
|
||||||
version = "0.5.0-alpha.2"
|
version = "0.5.0-alpha.3"
|
||||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||||
description = "A tool to create robust Roblox projects"
|
description = "A tool to create robust Roblox projects"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -22,7 +22,7 @@ bundle-plugin = []
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.27"
|
clap = "2.27"
|
||||||
csv = "1.0"
|
csv = "1.0"
|
||||||
env_logger = "0.5"
|
env_logger = "0.6"
|
||||||
failure = "0.1.3"
|
failure = "0.1.3"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
maplit = "1.0.1"
|
maplit = "1.0.1"
|
||||||
|
|||||||
54
server/assets/index.html
Normal file
54
server/assets/index.html
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Rojo</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: sans-serif;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 60rem;
|
||||||
|
background-color: #efefef;
|
||||||
|
border: 1px solid #666;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<h1 class="title">Rojo Live Sync is up and running!</h1>
|
||||||
|
<a class="docs" href="https://lpghatguy.github.io/rojo">Rojo Documentation</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -20,9 +20,14 @@ fn make_path_absolute(value: &Path) -> PathBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
env_logger::Builder::from_default_env()
|
{
|
||||||
.default_format_timestamp(false)
|
let log_env = env_logger::Env::default()
|
||||||
.init();
|
.default_filter_or("warn");
|
||||||
|
|
||||||
|
env_logger::Builder::from_env(log_env)
|
||||||
|
.default_format_timestamp(false)
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
|
||||||
let app = clap_app!(Rojo =>
|
let app = clap_app!(Rojo =>
|
||||||
(version: env!("CARGO_PKG_VERSION"))
|
(version: env!("CARGO_PKG_VERSION"))
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use failure::Fail;
|
|||||||
use crate::{
|
use crate::{
|
||||||
rbx_session::construct_oneoff_tree,
|
rbx_session::construct_oneoff_tree,
|
||||||
project::{Project, ProjectLoadFuzzyError},
|
project::{Project, ProjectLoadFuzzyError},
|
||||||
imfs::Imfs,
|
imfs::{Imfs, FsError},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -55,14 +55,18 @@ pub enum BuildError {
|
|||||||
XmlModelEncodeError(rbx_xml::EncodeError),
|
XmlModelEncodeError(rbx_xml::EncodeError),
|
||||||
|
|
||||||
#[fail(display = "Binary model file error")]
|
#[fail(display = "Binary model file error")]
|
||||||
BinaryModelEncodeError(rbx_binary::EncodeError)
|
BinaryModelEncodeError(rbx_binary::EncodeError),
|
||||||
|
|
||||||
|
#[fail(display = "{}", _0)]
|
||||||
|
FsError(#[fail(cause)] FsError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_from!(BuildError {
|
impl_from!(BuildError {
|
||||||
ProjectLoadFuzzyError => ProjectLoadError,
|
ProjectLoadFuzzyError => ProjectLoadError,
|
||||||
io::Error => IoError,
|
io::Error => IoError,
|
||||||
rbx_xml::EncodeError => XmlModelEncodeError,
|
rbx_xml::EncodeError => XmlModelEncodeError,
|
||||||
rbx_binary::EncodeError => BinaryModelEncodeError
|
rbx_binary::EncodeError => BinaryModelEncodeError,
|
||||||
|
FsError => FsError,
|
||||||
});
|
});
|
||||||
|
|
||||||
pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
|
pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
|
||||||
@@ -75,6 +79,7 @@ pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
|
|||||||
info!("Looking for project at {}", options.fuzzy_project_path.display());
|
info!("Looking for project at {}", options.fuzzy_project_path.display());
|
||||||
|
|
||||||
let project = Project::load_fuzzy(&options.fuzzy_project_path)?;
|
let project = Project::load_fuzzy(&options.fuzzy_project_path)?;
|
||||||
|
project.check_compatibility();
|
||||||
|
|
||||||
info!("Found project at {}", project.file_location.display());
|
info!("Found project at {}", project.file_location.display());
|
||||||
info!("Using project {:#?}", project);
|
info!("Using project {:#?}", project);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use failure::Fail;
|
|||||||
use crate::{
|
use crate::{
|
||||||
project::{Project, ProjectLoadFuzzyError},
|
project::{Project, ProjectLoadFuzzyError},
|
||||||
web::Server,
|
web::Server,
|
||||||
|
imfs::FsError,
|
||||||
live_session::LiveSession,
|
live_session::LiveSession,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,21 +25,26 @@ pub struct ServeOptions {
|
|||||||
pub enum ServeError {
|
pub enum ServeError {
|
||||||
#[fail(display = "Project load error: {}", _0)]
|
#[fail(display = "Project load error: {}", _0)]
|
||||||
ProjectLoadError(#[fail(cause)] ProjectLoadFuzzyError),
|
ProjectLoadError(#[fail(cause)] ProjectLoadFuzzyError),
|
||||||
|
|
||||||
|
#[fail(display = "{}", _0)]
|
||||||
|
FsError(#[fail(cause)] FsError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_from!(ServeError {
|
impl_from!(ServeError {
|
||||||
ProjectLoadFuzzyError => ProjectLoadError,
|
ProjectLoadFuzzyError => ProjectLoadError,
|
||||||
|
FsError => FsError,
|
||||||
});
|
});
|
||||||
|
|
||||||
pub fn serve(options: &ServeOptions) -> Result<(), ServeError> {
|
pub fn serve(options: &ServeOptions) -> Result<(), ServeError> {
|
||||||
info!("Looking for project at {}", options.fuzzy_project_path.display());
|
info!("Looking for project at {}", options.fuzzy_project_path.display());
|
||||||
|
|
||||||
let project = Arc::new(Project::load_fuzzy(&options.fuzzy_project_path)?);
|
let project = Arc::new(Project::load_fuzzy(&options.fuzzy_project_path)?);
|
||||||
|
project.check_compatibility();
|
||||||
|
|
||||||
info!("Found project at {}", project.file_location.display());
|
info!("Found project at {}", project.file_location.display());
|
||||||
info!("Using project {:#?}", project);
|
info!("Using project {:#?}", project);
|
||||||
|
|
||||||
let live_session = Arc::new(LiveSession::new(Arc::clone(&project)).unwrap());
|
let live_session = Arc::new(LiveSession::new(Arc::clone(&project))?);
|
||||||
let server = Server::new(Arc::clone(&live_session));
|
let server = Server::new(Arc::clone(&live_session));
|
||||||
|
|
||||||
let port = options.port
|
let port = options.port
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use reqwest::header::{ACCEPT, USER_AGENT, CONTENT_TYPE, COOKIE};
|
|||||||
use crate::{
|
use crate::{
|
||||||
rbx_session::construct_oneoff_tree,
|
rbx_session::construct_oneoff_tree,
|
||||||
project::{Project, ProjectLoadFuzzyError},
|
project::{Project, ProjectLoadFuzzyError},
|
||||||
imfs::Imfs,
|
imfs::{Imfs, FsError},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, Fail)]
|
||||||
@@ -33,6 +33,9 @@ pub enum UploadError {
|
|||||||
|
|
||||||
#[fail(display = "XML model file error")]
|
#[fail(display = "XML model file error")]
|
||||||
XmlModelEncodeError(rbx_xml::EncodeError),
|
XmlModelEncodeError(rbx_xml::EncodeError),
|
||||||
|
|
||||||
|
#[fail(display = "{}", _0)]
|
||||||
|
FsError(#[fail(cause)] FsError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_from!(UploadError {
|
impl_from!(UploadError {
|
||||||
@@ -40,6 +43,7 @@ impl_from!(UploadError {
|
|||||||
io::Error => IoError,
|
io::Error => IoError,
|
||||||
reqwest::Error => HttpError,
|
reqwest::Error => HttpError,
|
||||||
rbx_xml::EncodeError => XmlModelEncodeError,
|
rbx_xml::EncodeError => XmlModelEncodeError,
|
||||||
|
FsError => FsError,
|
||||||
});
|
});
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -56,6 +60,7 @@ pub fn upload(options: &UploadOptions) -> Result<(), UploadError> {
|
|||||||
info!("Looking for project at {}", options.fuzzy_project_path.display());
|
info!("Looking for project at {}", options.fuzzy_project_path.display());
|
||||||
|
|
||||||
let project = Project::load_fuzzy(&options.fuzzy_project_path)?;
|
let project = Project::load_fuzzy(&options.fuzzy_project_path)?;
|
||||||
|
project.check_compatibility();
|
||||||
|
|
||||||
info!("Found project at {}", project.file_location.display());
|
info!("Found project at {}", project.file_location.display());
|
||||||
info!("Using project {:#?}", project);
|
info!("Using project {:#?}", project);
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
use std::{
|
use std::{
|
||||||
sync::{mpsc, Arc, Mutex},
|
sync::{mpsc, Arc, Mutex},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
|
path::Path,
|
||||||
|
ops::Deref,
|
||||||
thread,
|
thread,
|
||||||
};
|
};
|
||||||
|
|
||||||
use log::trace;
|
use log::{warn, trace};
|
||||||
use notify::{
|
use notify::{
|
||||||
self,
|
self,
|
||||||
DebouncedEvent,
|
DebouncedEvent,
|
||||||
@@ -20,7 +22,66 @@ use crate::{
|
|||||||
|
|
||||||
const WATCH_TIMEOUT: Duration = Duration::from_millis(100);
|
const WATCH_TIMEOUT: Duration = Duration::from_millis(100);
|
||||||
|
|
||||||
fn handle_event(imfs: &Mutex<Imfs>, rbx_session: &Mutex<RbxSession>, event: DebouncedEvent) {
|
/// Watches for changes on the filesystem and links together the in-memory
|
||||||
|
/// filesystem and in-memory Roblox tree.
|
||||||
|
pub struct FsWatcher {
|
||||||
|
watcher: RecommendedWatcher,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FsWatcher {
|
||||||
|
/// Start a new FS watcher, watching all of the roots currently attached to
|
||||||
|
/// the given Imfs.
|
||||||
|
///
|
||||||
|
/// `rbx_session` is optional to make testing easier. If it isn't `None`,
|
||||||
|
/// events will be passed to it after they're given to the Imfs.
|
||||||
|
pub fn start(imfs: Arc<Mutex<Imfs>>, rbx_session: Option<Arc<Mutex<RbxSession>>>) -> FsWatcher {
|
||||||
|
let (watch_tx, watch_rx) = mpsc::channel();
|
||||||
|
|
||||||
|
let mut watcher = notify::watcher(watch_tx, WATCH_TIMEOUT)
|
||||||
|
.expect("Could not create filesystem watcher");
|
||||||
|
|
||||||
|
{
|
||||||
|
let imfs = imfs.lock().unwrap();
|
||||||
|
|
||||||
|
for root_path in imfs.get_roots() {
|
||||||
|
watcher.watch(root_path, RecursiveMode::Recursive)
|
||||||
|
.expect("Could not watch directory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let imfs = Arc::clone(&imfs);
|
||||||
|
let rbx_session = rbx_session.as_ref().map(Arc::clone);
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
trace!("Watcher thread started");
|
||||||
|
while let Ok(event) = watch_rx.recv() {
|
||||||
|
// handle_fs_event expects an Option<&Mutex<T>>, but we have
|
||||||
|
// an Option<Arc<Mutex<T>>>, so we coerce with Deref.
|
||||||
|
let session_ref = rbx_session.as_ref().map(Deref::deref);
|
||||||
|
|
||||||
|
handle_fs_event(&imfs, session_ref, event);
|
||||||
|
}
|
||||||
|
trace!("Watcher thread stopped");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
FsWatcher {
|
||||||
|
watcher,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop_watching_path(&mut self, path: &Path) {
|
||||||
|
match self.watcher.unwatch(path) {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Could not unwatch path {}: {}", path.display(), e);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_fs_event(imfs: &Mutex<Imfs>, rbx_session: Option<&Mutex<RbxSession>>, event: DebouncedEvent) {
|
||||||
match event {
|
match event {
|
||||||
DebouncedEvent::Create(path) => {
|
DebouncedEvent::Create(path) => {
|
||||||
trace!("Path created: {}", path.display());
|
trace!("Path created: {}", path.display());
|
||||||
@@ -30,7 +91,7 @@ fn handle_event(imfs: &Mutex<Imfs>, rbx_session: &Mutex<RbxSession>, event: Debo
|
|||||||
imfs.path_created(&path).unwrap();
|
imfs.path_created(&path).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
if let Some(rbx_session) = rbx_session {
|
||||||
let mut rbx_session = rbx_session.lock().unwrap();
|
let mut rbx_session = rbx_session.lock().unwrap();
|
||||||
rbx_session.path_created(&path);
|
rbx_session.path_created(&path);
|
||||||
}
|
}
|
||||||
@@ -43,7 +104,7 @@ fn handle_event(imfs: &Mutex<Imfs>, rbx_session: &Mutex<RbxSession>, event: Debo
|
|||||||
imfs.path_updated(&path).unwrap();
|
imfs.path_updated(&path).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
if let Some(rbx_session) = rbx_session {
|
||||||
let mut rbx_session = rbx_session.lock().unwrap();
|
let mut rbx_session = rbx_session.lock().unwrap();
|
||||||
rbx_session.path_updated(&path);
|
rbx_session.path_updated(&path);
|
||||||
}
|
}
|
||||||
@@ -56,20 +117,20 @@ fn handle_event(imfs: &Mutex<Imfs>, rbx_session: &Mutex<RbxSession>, event: Debo
|
|||||||
imfs.path_removed(&path).unwrap();
|
imfs.path_removed(&path).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
if let Some(rbx_session) = rbx_session {
|
||||||
let mut rbx_session = rbx_session.lock().unwrap();
|
let mut rbx_session = rbx_session.lock().unwrap();
|
||||||
rbx_session.path_removed(&path);
|
rbx_session.path_removed(&path);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DebouncedEvent::Rename(from_path, to_path) => {
|
DebouncedEvent::Rename(from_path, to_path) => {
|
||||||
trace!("Path rename: {} to {}", from_path.display(), to_path.display());
|
trace!("Path renamed: {} to {}", from_path.display(), to_path.display());
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut imfs = imfs.lock().unwrap();
|
let mut imfs = imfs.lock().unwrap();
|
||||||
imfs.path_moved(&from_path, &to_path).unwrap();
|
imfs.path_moved(&from_path, &to_path).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
if let Some(rbx_session) = rbx_session {
|
||||||
let mut rbx_session = rbx_session.lock().unwrap();
|
let mut rbx_session = rbx_session.lock().unwrap();
|
||||||
rbx_session.path_renamed(&from_path, &to_path);
|
rbx_session.path_renamed(&from_path, &to_path);
|
||||||
}
|
}
|
||||||
@@ -78,49 +139,4 @@ fn handle_event(imfs: &Mutex<Imfs>, rbx_session: &Mutex<RbxSession>, event: Debo
|
|||||||
trace!("Unhandled FS event: {:?}", other);
|
trace!("Unhandled FS event: {:?}", other);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Watches for changes on the filesystem and links together the in-memory
|
|
||||||
/// filesystem and in-memory Roblox tree.
|
|
||||||
pub struct FsWatcher {
|
|
||||||
#[allow(unused)]
|
|
||||||
watchers: Vec<RecommendedWatcher>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FsWatcher {
|
|
||||||
pub fn start(imfs: Arc<Mutex<Imfs>>, rbx_session: Arc<Mutex<RbxSession>>) -> FsWatcher {
|
|
||||||
let mut watchers = Vec::new();
|
|
||||||
|
|
||||||
{
|
|
||||||
let imfs_temp = imfs.lock().unwrap();
|
|
||||||
|
|
||||||
for root_path in imfs_temp.get_roots() {
|
|
||||||
let (watch_tx, watch_rx) = mpsc::channel();
|
|
||||||
|
|
||||||
let mut watcher = notify::watcher(watch_tx, WATCH_TIMEOUT)
|
|
||||||
.expect("Could not create `notify` watcher");
|
|
||||||
|
|
||||||
watcher.watch(root_path, RecursiveMode::Recursive)
|
|
||||||
.expect("Could not watch directory");
|
|
||||||
|
|
||||||
watchers.push(watcher);
|
|
||||||
|
|
||||||
let imfs = Arc::clone(&imfs);
|
|
||||||
let rbx_session = Arc::clone(&rbx_session);
|
|
||||||
let root_path = root_path.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
trace!("Watcher thread ({}) started", root_path.display());
|
|
||||||
while let Ok(event) = watch_rx.recv() {
|
|
||||||
handle_event(&imfs, &rbx_session, event);
|
|
||||||
}
|
|
||||||
trace!("Watcher thread ({}) stopped", root_path.display());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FsWatcher {
|
|
||||||
watchers,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,41 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
path::{self, Path, PathBuf},
|
path::{self, Path, PathBuf},
|
||||||
|
fmt,
|
||||||
fs,
|
fs,
|
||||||
io,
|
io,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use failure::Fail;
|
||||||
use serde_derive::{Serialize, Deserialize};
|
use serde_derive::{Serialize, Deserialize};
|
||||||
|
|
||||||
use crate::project::{Project, ProjectNode};
|
use crate::project::{Project, ProjectNode};
|
||||||
|
|
||||||
fn add_sync_points(imfs: &mut Imfs, project_node: &ProjectNode) -> io::Result<()> {
|
/// A wrapper around io::Error that also attaches the path associated with the
|
||||||
|
/// error.
|
||||||
|
#[derive(Debug, Fail)]
|
||||||
|
pub struct FsError {
|
||||||
|
#[fail(cause)]
|
||||||
|
inner: io::Error,
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FsError {
|
||||||
|
fn new<P: Into<PathBuf>>(inner: io::Error, path: P) -> FsError {
|
||||||
|
FsError {
|
||||||
|
inner,
|
||||||
|
path: path.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for FsError {
|
||||||
|
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(output, "{}: {}", self.path.display(), self.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_sync_points(imfs: &mut Imfs, project_node: &ProjectNode) -> Result<(), FsError> {
|
||||||
match project_node {
|
match project_node {
|
||||||
ProjectNode::Instance(node) => {
|
ProjectNode::Instance(node) => {
|
||||||
for child in node.children.values() {
|
for child in node.children.values() {
|
||||||
@@ -44,7 +70,7 @@ impl Imfs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_roots_from_project(&mut self, project: &Project) -> io::Result<()> {
|
pub fn add_roots_from_project(&mut self, project: &Project) -> Result<(), FsError> {
|
||||||
add_sync_points(self, &project.tree)
|
add_sync_points(self, &project.tree)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +89,7 @@ impl Imfs {
|
|||||||
self.items.get(path)
|
self.items.get(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_root(&mut self, path: &Path) -> io::Result<()> {
|
pub fn add_root(&mut self, path: &Path) -> Result<(), FsError> {
|
||||||
debug_assert!(path.is_absolute());
|
debug_assert!(path.is_absolute());
|
||||||
debug_assert!(!self.is_within_roots(path));
|
debug_assert!(!self.is_within_roots(path));
|
||||||
|
|
||||||
@@ -72,21 +98,33 @@ impl Imfs {
|
|||||||
self.descend_and_read_from_disk(path)
|
self.descend_and_read_from_disk(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path_created(&mut self, path: &Path) -> io::Result<()> {
|
pub fn remove_root(&mut self, path: &Path) {
|
||||||
|
debug_assert!(path.is_absolute());
|
||||||
|
|
||||||
|
if self.roots.get(path).is_some() {
|
||||||
|
self.remove_item(path);
|
||||||
|
|
||||||
|
if let Some(parent_path) = path.parent() {
|
||||||
|
self.unlink_child(parent_path, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path_created(&mut self, path: &Path) -> Result<(), FsError> {
|
||||||
debug_assert!(path.is_absolute());
|
debug_assert!(path.is_absolute());
|
||||||
debug_assert!(self.is_within_roots(path));
|
debug_assert!(self.is_within_roots(path));
|
||||||
|
|
||||||
self.descend_and_read_from_disk(path)
|
self.descend_and_read_from_disk(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path_updated(&mut self, path: &Path) -> io::Result<()> {
|
pub fn path_updated(&mut self, path: &Path) -> Result<(), FsError> {
|
||||||
debug_assert!(path.is_absolute());
|
debug_assert!(path.is_absolute());
|
||||||
debug_assert!(self.is_within_roots(path));
|
debug_assert!(self.is_within_roots(path));
|
||||||
|
|
||||||
self.descend_and_read_from_disk(path)
|
self.descend_and_read_from_disk(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path_removed(&mut self, path: &Path) -> io::Result<()> {
|
pub fn path_removed(&mut self, path: &Path) -> Result<(), FsError> {
|
||||||
debug_assert!(path.is_absolute());
|
debug_assert!(path.is_absolute());
|
||||||
debug_assert!(self.is_within_roots(path));
|
debug_assert!(self.is_within_roots(path));
|
||||||
|
|
||||||
@@ -99,7 +137,7 @@ impl Imfs {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path_moved(&mut self, from_path: &Path, to_path: &Path) -> io::Result<()> {
|
pub fn path_moved(&mut self, from_path: &Path, to_path: &Path) -> Result<(), FsError> {
|
||||||
self.path_removed(from_path)?;
|
self.path_removed(from_path)?;
|
||||||
self.path_created(to_path)?;
|
self.path_created(to_path)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -149,9 +187,9 @@ impl Imfs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn descend_and_read_from_disk(&mut self, path: &Path) -> io::Result<()> {
|
fn descend_and_read_from_disk(&mut self, path: &Path) -> Result<(), FsError> {
|
||||||
let root_path = self.get_root_path(path)
|
let root_path = self.get_root_path(path)
|
||||||
.expect("Tried to mkdirp for path that wasn't within roots!");
|
.expect("Tried to descent and read for path that wasn't within roots!");
|
||||||
|
|
||||||
// If this path is a root, we should read the entire thing.
|
// If this path is a root, we should read the entire thing.
|
||||||
if root_path == path {
|
if root_path == path {
|
||||||
@@ -170,7 +208,6 @@ impl Imfs {
|
|||||||
if self.items.contains_key(&next_path) {
|
if self.items.contains_key(&next_path) {
|
||||||
current_path = next_path;
|
current_path = next_path;
|
||||||
} else {
|
} else {
|
||||||
self.read_from_disk(¤t_path)?;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -178,14 +215,16 @@ impl Imfs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
self.read_from_disk(¤t_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_from_disk(&mut self, path: &Path) -> io::Result<()> {
|
fn read_from_disk(&mut self, path: &Path) -> Result<(), FsError> {
|
||||||
let metadata = fs::metadata(path)?;
|
let metadata = fs::metadata(path)
|
||||||
|
.map_err(|e| FsError::new(e, path))?;
|
||||||
|
|
||||||
if metadata.is_file() {
|
if metadata.is_file() {
|
||||||
let contents = fs::read(path)?;
|
let contents = fs::read(path)
|
||||||
|
.map_err(|e| FsError::new(e, path))?;
|
||||||
let item = ImfsItem::File(ImfsFile {
|
let item = ImfsItem::File(ImfsFile {
|
||||||
path: path.to_path_buf(),
|
path: path.to_path_buf(),
|
||||||
contents,
|
contents,
|
||||||
@@ -206,8 +245,13 @@ impl Imfs {
|
|||||||
|
|
||||||
self.items.insert(path.to_path_buf(), item);
|
self.items.insert(path.to_path_buf(), item);
|
||||||
|
|
||||||
for entry in fs::read_dir(path)? {
|
let dir_children = fs::read_dir(path)
|
||||||
let entry = entry?;
|
.map_err(|e| FsError::new(e, path))?;
|
||||||
|
|
||||||
|
for entry in dir_children {
|
||||||
|
let entry = entry
|
||||||
|
.map_err(|e| FsError::new(e, path))?;
|
||||||
|
|
||||||
let child_path = entry.path();
|
let child_path = entry.path();
|
||||||
|
|
||||||
self.read_from_disk(&child_path)?;
|
self.read_from_disk(&child_path)?;
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
use std::{
|
use std::{
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
io,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
fs_watcher::FsWatcher,
|
||||||
|
imfs::{Imfs, FsError},
|
||||||
message_queue::MessageQueue,
|
message_queue::MessageQueue,
|
||||||
project::Project,
|
project::Project,
|
||||||
imfs::Imfs,
|
|
||||||
session_id::SessionId,
|
|
||||||
rbx_session::RbxSession,
|
rbx_session::RbxSession,
|
||||||
|
session_id::SessionId,
|
||||||
snapshot_reconciler::InstanceChanges,
|
snapshot_reconciler::InstanceChanges,
|
||||||
fs_watcher::FsWatcher,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Contains all of the state for a Rojo live-sync session.
|
/// Contains all of the state for a Rojo live-sync session.
|
||||||
@@ -24,7 +23,7 @@ pub struct LiveSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LiveSession {
|
impl LiveSession {
|
||||||
pub fn new(project: Arc<Project>) -> io::Result<LiveSession> {
|
pub fn new(project: Arc<Project>) -> Result<LiveSession, FsError> {
|
||||||
let imfs = {
|
let imfs = {
|
||||||
let mut imfs = Imfs::new();
|
let mut imfs = Imfs::new();
|
||||||
imfs.add_roots_from_project(&project)?;
|
imfs.add_roots_from_project(&project)?;
|
||||||
@@ -41,7 +40,7 @@ impl LiveSession {
|
|||||||
|
|
||||||
let fs_watcher = FsWatcher::start(
|
let fs_watcher = FsWatcher::start(
|
||||||
Arc::clone(&imfs),
|
Arc::clone(&imfs),
|
||||||
Arc::clone(&rbx_session),
|
Some(Arc::clone(&rbx_session)),
|
||||||
);
|
);
|
||||||
|
|
||||||
let session_id = SessionId::new();
|
let session_id = SessionId::new();
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ use std::{
|
|||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use log::warn;
|
||||||
use failure::Fail;
|
use failure::Fail;
|
||||||
use maplit::hashmap;
|
use maplit::hashmap;
|
||||||
use rbx_tree::RbxValue;
|
use rbx_tree::RbxValue;
|
||||||
use serde_derive::{Serialize, Deserialize};
|
use serde_derive::{Serialize, Deserialize};
|
||||||
|
|
||||||
pub static PROJECT_FILENAME: &'static str = "roblox-project.json";
|
pub static PROJECT_FILENAME: &'static str = "default.project.json";
|
||||||
|
pub static COMPAT_PROJECT_FILENAME: &'static str = "roblox-project.json";
|
||||||
|
|
||||||
// Methods used for Serde's default value system, which doesn't support using
|
// Methods used for Serde's default value system, which doesn't support using
|
||||||
// value literals directly, only functions that return values.
|
// value literals directly, only functions that return values.
|
||||||
@@ -362,11 +364,17 @@ impl Project {
|
|||||||
} else if location_metadata.is_dir() {
|
} else if location_metadata.is_dir() {
|
||||||
let with_file = start_location.join(PROJECT_FILENAME);
|
let with_file = start_location.join(PROJECT_FILENAME);
|
||||||
|
|
||||||
if let Ok(with_file_metadata) = fs::metadata(&with_file) {
|
if let Ok(file_metadata) = fs::metadata(&with_file) {
|
||||||
if with_file_metadata.is_file() {
|
if file_metadata.is_file() {
|
||||||
return Some(with_file);
|
return Some(with_file);
|
||||||
} else {
|
}
|
||||||
return None;
|
}
|
||||||
|
|
||||||
|
let with_compat_file = start_location.join(COMPAT_PROJECT_FILENAME);
|
||||||
|
|
||||||
|
if let Ok(file_metadata) = fs::metadata(&with_compat_file) {
|
||||||
|
if file_metadata.is_file() {
|
||||||
|
return Some(with_compat_file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,6 +413,25 @@ impl Project {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if there are any compatibility issues with this project file and
|
||||||
|
/// warns the user if there are any.
|
||||||
|
pub fn check_compatibility(&self) {
|
||||||
|
let file_name = self.file_location
|
||||||
|
.file_name().unwrap()
|
||||||
|
.to_str().expect("Project file path was not valid Unicode!");
|
||||||
|
|
||||||
|
if file_name == COMPAT_PROJECT_FILENAME {
|
||||||
|
warn!("Rojo's default project file name changed in 0.5.0-alpha3.");
|
||||||
|
warn!("Support for the old project file name will be dropped before 0.5.0 releases.");
|
||||||
|
warn!("Your project file is named {}", COMPAT_PROJECT_FILENAME);
|
||||||
|
warn!("Rename your project file to {}", PROJECT_FILENAME);
|
||||||
|
} else if !file_name.ends_with(".project.json") {
|
||||||
|
warn!("Starting in Rojo 0.5.0-alpha3, it's recommended to give all project files the");
|
||||||
|
warn!(".project.json extension. This helps Rojo differentiate project files from");
|
||||||
|
warn!("other JSON files!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn to_source_project(&self) -> SourceProject {
|
fn to_source_project(&self) -> SourceProject {
|
||||||
SourceProject {
|
SourceProject {
|
||||||
name: self.name.clone(),
|
name: self.name.clone(),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use std::{
|
|||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use log::warn;
|
||||||
use rbx_tree::RbxId;
|
use rbx_tree::RbxId;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -27,13 +28,21 @@ digraph RojoTree {
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
/// Compiles DOT source to SVG by invoking dot on the command line.
|
/// Compiles DOT source to SVG by invoking dot on the command line.
|
||||||
pub fn graphviz_to_svg(source: &str) -> String {
|
pub fn graphviz_to_svg(source: &str) -> Option<String> {
|
||||||
let mut child = Command::new("dot")
|
let command = Command::new("dot")
|
||||||
.arg("-Tsvg")
|
.arg("-Tsvg")
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.spawn()
|
.spawn();
|
||||||
.expect("Failed to spawn GraphViz process -- make sure it's installed in order to use /api/visualize");
|
|
||||||
|
let mut child = match command {
|
||||||
|
Ok(child) => child,
|
||||||
|
Err(_) => {
|
||||||
|
warn!("Failed to spawn GraphViz process to visualize current state.");
|
||||||
|
warn!("If you want pretty graphs, install GraphViz and make sure 'dot' is on your PATH!");
|
||||||
|
return None;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
let stdin = child.stdin.as_mut().expect("Failed to open stdin");
|
let stdin = child.stdin.as_mut().expect("Failed to open stdin");
|
||||||
@@ -41,7 +50,7 @@ pub fn graphviz_to_svg(source: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let output = child.wait_with_output().expect("Failed to read stdout");
|
let output = child.wait_with_output().expect("Failed to read stdout");
|
||||||
String::from_utf8(output.stdout).expect("Failed to parse stdout as UTF-8")
|
Some(String::from_utf8(output.stdout).expect("Failed to parse stdout as UTF-8"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A Display wrapper struct to visualize an RbxSession as SVG.
|
/// A Display wrapper struct to visualize an RbxSession as SVG.
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ use crate::{
|
|||||||
rbx_session::{MetadataPerInstance},
|
rbx_session::{MetadataPerInstance},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static HOME_CONTENT: &str = include_str!("../assets/index.html");
|
||||||
|
|
||||||
/// Contains the instance metadata relevant to Rojo clients.
|
/// Contains the instance metadata relevant to Rojo clients.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -96,136 +98,31 @@ impl Server {
|
|||||||
|
|
||||||
router!(request,
|
router!(request,
|
||||||
(GET) (/) => {
|
(GET) (/) => {
|
||||||
Response::text("Rojo is up and running!")
|
self.handle_home()
|
||||||
},
|
},
|
||||||
|
|
||||||
(GET) (/api/rojo) => {
|
(GET) (/api/rojo) => {
|
||||||
// Get a summary of information about the server.
|
self.handle_api_rojo()
|
||||||
|
|
||||||
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
|
||||||
let tree = rbx_session.get_tree();
|
|
||||||
|
|
||||||
Response::json(&ServerInfoResponse {
|
|
||||||
server_version: self.server_version,
|
|
||||||
protocol_version: 2,
|
|
||||||
session_id: self.live_session.session_id,
|
|
||||||
expected_place_ids: self.live_session.project.serve_place_ids.clone(),
|
|
||||||
root_instance_id: tree.get_root_id(),
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
(GET) (/api/subscribe/{ cursor: u32 }) => {
|
(GET) (/api/subscribe/{ cursor: u32 }) => {
|
||||||
// Retrieve any messages past the given cursor index, and if
|
self.handle_api_subscribe(cursor)
|
||||||
// there weren't any, subscribe to receive any new messages.
|
|
||||||
|
|
||||||
let message_queue = Arc::clone(&self.live_session.message_queue);
|
|
||||||
|
|
||||||
// Did the client miss any messages since the last subscribe?
|
|
||||||
{
|
|
||||||
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
|
|
||||||
|
|
||||||
if !new_messages.is_empty() {
|
|
||||||
return Response::json(&SubscribeResponse {
|
|
||||||
session_id: self.live_session.session_id,
|
|
||||||
messages: Cow::Borrowed(&new_messages),
|
|
||||||
message_cursor: new_cursor,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel();
|
|
||||||
|
|
||||||
let sender_id = message_queue.subscribe(tx);
|
|
||||||
|
|
||||||
match rx.recv() {
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(_) => return Response::text("error!").with_status_code(500),
|
|
||||||
}
|
|
||||||
|
|
||||||
message_queue.unsubscribe(sender_id);
|
|
||||||
|
|
||||||
{
|
|
||||||
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
|
|
||||||
|
|
||||||
return Response::json(&SubscribeResponse {
|
|
||||||
session_id: self.live_session.session_id,
|
|
||||||
messages: Cow::Owned(new_messages),
|
|
||||||
message_cursor: new_cursor,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
(GET) (/api/read/{ id_list: String }) => {
|
(GET) (/api/read/{ id_list: String }) => {
|
||||||
let message_queue = Arc::clone(&self.live_session.message_queue);
|
|
||||||
|
|
||||||
let requested_ids: Option<Vec<RbxId>> = id_list
|
let requested_ids: Option<Vec<RbxId>> = id_list
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(RbxId::parse_str)
|
.map(RbxId::parse_str)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let requested_ids = match requested_ids {
|
self.handle_api_read(requested_ids)
|
||||||
Some(id) => id,
|
|
||||||
None => return rouille::Response::text("Malformed ID list").with_status_code(400),
|
|
||||||
};
|
|
||||||
|
|
||||||
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
|
||||||
let tree = rbx_session.get_tree();
|
|
||||||
|
|
||||||
let message_cursor = message_queue.get_message_cursor();
|
|
||||||
|
|
||||||
let mut instances = HashMap::new();
|
|
||||||
|
|
||||||
for &requested_id in &requested_ids {
|
|
||||||
if let Some(instance) = tree.get_instance(requested_id) {
|
|
||||||
let metadata = rbx_session.get_instance_metadata(requested_id)
|
|
||||||
.map(InstanceMetadata::from_session_metadata);
|
|
||||||
|
|
||||||
instances.insert(instance.get_id(), InstanceWithMetadata {
|
|
||||||
instance: Cow::Borrowed(instance),
|
|
||||||
metadata,
|
|
||||||
});
|
|
||||||
|
|
||||||
for descendant in tree.descendants(requested_id) {
|
|
||||||
let descendant_meta = rbx_session.get_instance_metadata(descendant.get_id())
|
|
||||||
.map(InstanceMetadata::from_session_metadata);
|
|
||||||
|
|
||||||
instances.insert(descendant.get_id(), InstanceWithMetadata {
|
|
||||||
instance: Cow::Borrowed(descendant),
|
|
||||||
metadata: descendant_meta,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Response::json(&ReadResponse {
|
|
||||||
session_id: self.live_session.session_id,
|
|
||||||
message_cursor,
|
|
||||||
instances,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
(GET) (/visualize/rbx) => {
|
(GET) (/visualize/rbx) => {
|
||||||
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
self.handle_visualize_rbx()
|
||||||
|
|
||||||
let dot_source = format!("{}", VisualizeRbxSession(&rbx_session));
|
|
||||||
|
|
||||||
Response::svg(graphviz_to_svg(&dot_source))
|
|
||||||
},
|
},
|
||||||
|
|
||||||
(GET) (/visualize/imfs) => {
|
(GET) (/visualize/imfs) => {
|
||||||
let imfs = self.live_session.imfs.lock().unwrap();
|
self.handle_visualize_imfs()
|
||||||
|
|
||||||
let dot_source = format!("{}", VisualizeImfs(&imfs));
|
|
||||||
|
|
||||||
Response::svg(graphviz_to_svg(&dot_source))
|
|
||||||
},
|
},
|
||||||
|
|
||||||
(GET) (/visualize/path_metadata) => {
|
(GET) (/visualize/path_metadata) => {
|
||||||
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
self.handle_visualize_path_metadata()
|
||||||
|
|
||||||
Response::json(&rbx_session.debug_get_metadata_per_path())
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_ => Response::empty_404()
|
_ => Response::empty_404()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -235,4 +132,131 @@ impl Server {
|
|||||||
|
|
||||||
rouille::start_server(address, move |request| self.handle_request(request));
|
rouille::start_server(address, move |request| self.handle_request(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_home(&self) -> Response {
|
||||||
|
Response::html(HOME_CONTENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a summary of information about the server
|
||||||
|
fn handle_api_rojo(&self) -> Response {
|
||||||
|
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
||||||
|
let tree = rbx_session.get_tree();
|
||||||
|
|
||||||
|
Response::json(&ServerInfoResponse {
|
||||||
|
server_version: self.server_version,
|
||||||
|
protocol_version: 2,
|
||||||
|
session_id: self.live_session.session_id,
|
||||||
|
expected_place_ids: self.live_session.project.serve_place_ids.clone(),
|
||||||
|
root_instance_id: tree.get_root_id(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve any messages past the given cursor index, and if
|
||||||
|
/// there weren't any, subscribe to receive any new messages.
|
||||||
|
fn handle_api_subscribe(&self, cursor: u32) -> Response {
|
||||||
|
let message_queue = Arc::clone(&self.live_session.message_queue);
|
||||||
|
|
||||||
|
// Did the client miss any messages since the last subscribe?
|
||||||
|
{
|
||||||
|
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
|
||||||
|
|
||||||
|
if !new_messages.is_empty() {
|
||||||
|
return Response::json(&SubscribeResponse {
|
||||||
|
session_id: self.live_session.session_id,
|
||||||
|
messages: Cow::Borrowed(&new_messages),
|
||||||
|
message_cursor: new_cursor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
|
let sender_id = message_queue.subscribe(tx);
|
||||||
|
|
||||||
|
match rx.recv() {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(_) => return Response::text("error!").with_status_code(500),
|
||||||
|
}
|
||||||
|
|
||||||
|
message_queue.unsubscribe(sender_id);
|
||||||
|
|
||||||
|
{
|
||||||
|
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
|
||||||
|
|
||||||
|
return Response::json(&SubscribeResponse {
|
||||||
|
session_id: self.live_session.session_id,
|
||||||
|
messages: Cow::Owned(new_messages),
|
||||||
|
message_cursor: new_cursor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_api_read(&self, requested_ids: Option<Vec<RbxId>>) -> Response {
|
||||||
|
let message_queue = Arc::clone(&self.live_session.message_queue);
|
||||||
|
|
||||||
|
let requested_ids = match requested_ids {
|
||||||
|
Some(id) => id,
|
||||||
|
None => return rouille::Response::text("Malformed ID list").with_status_code(400),
|
||||||
|
};
|
||||||
|
|
||||||
|
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
||||||
|
let tree = rbx_session.get_tree();
|
||||||
|
|
||||||
|
let message_cursor = message_queue.get_message_cursor();
|
||||||
|
|
||||||
|
let mut instances = HashMap::new();
|
||||||
|
|
||||||
|
for &requested_id in &requested_ids {
|
||||||
|
if let Some(instance) = tree.get_instance(requested_id) {
|
||||||
|
let metadata = rbx_session.get_instance_metadata(requested_id)
|
||||||
|
.map(InstanceMetadata::from_session_metadata);
|
||||||
|
|
||||||
|
instances.insert(instance.get_id(), InstanceWithMetadata {
|
||||||
|
instance: Cow::Borrowed(instance),
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
for descendant in tree.descendants(requested_id) {
|
||||||
|
let descendant_meta = rbx_session.get_instance_metadata(descendant.get_id())
|
||||||
|
.map(InstanceMetadata::from_session_metadata);
|
||||||
|
|
||||||
|
instances.insert(descendant.get_id(), InstanceWithMetadata {
|
||||||
|
instance: Cow::Borrowed(descendant),
|
||||||
|
metadata: descendant_meta,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::json(&ReadResponse {
|
||||||
|
session_id: self.live_session.session_id,
|
||||||
|
message_cursor,
|
||||||
|
instances,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_visualize_rbx(&self) -> Response {
|
||||||
|
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
||||||
|
let dot_source = format!("{}", VisualizeRbxSession(&rbx_session));
|
||||||
|
|
||||||
|
match graphviz_to_svg(&dot_source) {
|
||||||
|
Some(svg) => Response::svg(svg),
|
||||||
|
None => Response::text(dot_source),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_visualize_imfs(&self) -> Response {
|
||||||
|
let imfs = self.live_session.imfs.lock().unwrap();
|
||||||
|
let dot_source = format!("{}", VisualizeImfs(&imfs));
|
||||||
|
|
||||||
|
match graphviz_to_svg(&dot_source) {
|
||||||
|
Some(svg) => Response::svg(svg),
|
||||||
|
None => Response::text(dot_source),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_visualize_path_metadata(&self) -> Response {
|
||||||
|
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
||||||
|
Response::json(&rbx_session.debug_get_metadata_per_path())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
io,
|
|
||||||
fs,
|
fs,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use failure::Error;
|
||||||
use tempfile::{TempDir, tempdir};
|
use tempfile::{TempDir, tempdir};
|
||||||
|
|
||||||
use librojo::{
|
use librojo::{
|
||||||
@@ -19,7 +19,7 @@ enum FsEvent {
|
|||||||
Moved(PathBuf, PathBuf),
|
Moved(PathBuf, PathBuf),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_events(imfs: &mut Imfs, events: &[FsEvent]) -> io::Result<()> {
|
fn send_events(imfs: &mut Imfs, events: &[FsEvent]) -> Result<(), Error> {
|
||||||
for event in events {
|
for event in events {
|
||||||
match event {
|
match event {
|
||||||
FsEvent::Created(path) => imfs.path_created(path)?,
|
FsEvent::Created(path) => imfs.path_created(path)?,
|
||||||
@@ -56,7 +56,7 @@ fn check_expected(real: &Imfs, expected: &ExpectedImfs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn base_tree() -> io::Result<(TempDir, Imfs, ExpectedImfs, TestResources)> {
|
fn base_tree() -> Result<(TempDir, Imfs, ExpectedImfs, TestResources), Error> {
|
||||||
let root = tempdir()?;
|
let root = tempdir()?;
|
||||||
|
|
||||||
let foo_path = root.path().join("foo");
|
let foo_path = root.path().join("foo");
|
||||||
@@ -125,7 +125,7 @@ fn base_tree() -> io::Result<(TempDir, Imfs, ExpectedImfs, TestResources)> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn initial_read() -> io::Result<()> {
|
fn initial_read() -> Result<(), Error> {
|
||||||
let (_root, imfs, expected_imfs, _resources) = base_tree()?;
|
let (_root, imfs, expected_imfs, _resources) = base_tree()?;
|
||||||
|
|
||||||
check_expected(&imfs, &expected_imfs);
|
check_expected(&imfs, &expected_imfs);
|
||||||
@@ -134,7 +134,7 @@ fn initial_read() -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn adding_files() -> io::Result<()> {
|
fn adding_files() -> Result<(), Error> {
|
||||||
let (root, mut imfs, mut expected_imfs, resources) = base_tree()?;
|
let (root, mut imfs, mut expected_imfs, resources) = base_tree()?;
|
||||||
|
|
||||||
check_expected(&imfs, &expected_imfs);
|
check_expected(&imfs, &expected_imfs);
|
||||||
@@ -178,7 +178,7 @@ fn adding_files() -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn adding_folder() -> io::Result<()> {
|
fn adding_folder() -> Result<(), Error> {
|
||||||
let (root, imfs, mut expected_imfs, _resources) = base_tree()?;
|
let (root, imfs, mut expected_imfs, _resources) = base_tree()?;
|
||||||
|
|
||||||
check_expected(&imfs, &expected_imfs);
|
check_expected(&imfs, &expected_imfs);
|
||||||
@@ -255,7 +255,36 @@ fn adding_folder() -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn removing_file() -> io::Result<()> {
|
fn updating_files() -> Result<(), Error> {
|
||||||
|
let (_root, mut imfs, mut expected_imfs, resources) = base_tree()?;
|
||||||
|
|
||||||
|
check_expected(&imfs, &expected_imfs);
|
||||||
|
|
||||||
|
fs::write(&resources.bar_path, b"bar updated")?;
|
||||||
|
fs::write(&resources.baz_path, b"baz updated")?;
|
||||||
|
|
||||||
|
imfs.path_updated(&resources.bar_path)?;
|
||||||
|
imfs.path_updated(&resources.baz_path)?;
|
||||||
|
|
||||||
|
let bar_updated_item = ImfsItem::File(ImfsFile {
|
||||||
|
path: resources.bar_path.clone(),
|
||||||
|
contents: b"bar updated".to_vec(),
|
||||||
|
});
|
||||||
|
let baz_updated_item = ImfsItem::File(ImfsFile {
|
||||||
|
path: resources.baz_path.clone(),
|
||||||
|
contents: b"baz updated".to_vec(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expected_imfs.items.insert(resources.bar_path.clone(), bar_updated_item);
|
||||||
|
expected_imfs.items.insert(resources.baz_path.clone(), baz_updated_item);
|
||||||
|
|
||||||
|
check_expected(&imfs, &expected_imfs);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn removing_file() -> Result<(), Error> {
|
||||||
let (root, mut imfs, mut expected_imfs, resources) = base_tree()?;
|
let (root, mut imfs, mut expected_imfs, resources) = base_tree()?;
|
||||||
|
|
||||||
check_expected(&imfs, &expected_imfs);
|
check_expected(&imfs, &expected_imfs);
|
||||||
@@ -279,7 +308,7 @@ fn removing_file() -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn removing_folder() -> io::Result<()> {
|
fn removing_folder() -> Result<(), Error> {
|
||||||
let (root, imfs, mut expected_imfs, resources) = base_tree()?;
|
let (root, imfs, mut expected_imfs, resources) = base_tree()?;
|
||||||
|
|
||||||
check_expected(&imfs, &expected_imfs);
|
check_expected(&imfs, &expected_imfs);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ lazy_static! {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty() {
|
fn empty() {
|
||||||
let project_file_location = TEST_PROJECTS_ROOT.join("empty/roblox-project.json");
|
let project_file_location = TEST_PROJECTS_ROOT.join("empty/default.project.json");
|
||||||
let project = Project::load_exact(&project_file_location).unwrap();
|
let project = Project::load_exact(&project_file_location).unwrap();
|
||||||
|
|
||||||
assert_eq!(project.name, "empty");
|
assert_eq!(project.name, "empty");
|
||||||
@@ -29,7 +29,7 @@ fn empty() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_fuzzy_file() {
|
fn empty_fuzzy_file() {
|
||||||
let project_file_location = TEST_PROJECTS_ROOT.join("empty/roblox-project.json");
|
let project_file_location = TEST_PROJECTS_ROOT.join("empty/default.project.json");
|
||||||
let project = Project::load_fuzzy(&project_file_location).unwrap();
|
let project = Project::load_fuzzy(&project_file_location).unwrap();
|
||||||
|
|
||||||
assert_eq!(project.name, "empty");
|
assert_eq!(project.name, "empty");
|
||||||
@@ -45,7 +45,7 @@ fn empty_fuzzy_folder() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn single_sync_point() {
|
fn single_sync_point() {
|
||||||
let project_file_location = TEST_PROJECTS_ROOT.join("single-sync-point/roblox-project.json");
|
let project_file_location = TEST_PROJECTS_ROOT.join("single-sync-point/default.project.json");
|
||||||
let project = Project::load_exact(&project_file_location).unwrap();
|
let project = Project::load_exact(&project_file_location).unwrap();
|
||||||
|
|
||||||
let expected_project = {
|
let expected_project = {
|
||||||
@@ -100,7 +100,7 @@ fn single_sync_point() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_model() {
|
fn test_model() {
|
||||||
let project_file_location = TEST_PROJECTS_ROOT.join("test-model/roblox-project.json");
|
let project_file_location = TEST_PROJECTS_ROOT.join("test-model/default.project.json");
|
||||||
let project = Project::load_exact(&project_file_location).unwrap();
|
let project = Project::load_exact(&project_file_location).unwrap();
|
||||||
|
|
||||||
assert_eq!(project.name, "test-model");
|
assert_eq!(project.name, "test-model");
|
||||||
|
|||||||
6
test-projects/missing-files/default.project.json
Normal file
6
test-projects/missing-files/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "missing-files",
|
||||||
|
"tree": {
|
||||||
|
"$path": "does-not-exist"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user