diff --git a/server/src/commands/serve.rs b/server/src/commands/serve.rs index d11d8b74..e8faae95 100644 --- a/server/src/commands/serve.rs +++ b/server/src/commands/serve.rs @@ -25,8 +25,13 @@ pub fn serve(fuzzy_project_location: &Path) { roblox_studio::install_bundled_plugin().unwrap(); - let session = Arc::new(Session::new(project)); - let server = Server::new(Arc::from(session)); + let session = Arc::new({ + let mut session = Session::new(project); + session.start().unwrap(); + session + }); + + let server = Server::new(Arc::clone(&session)); println!("Server listening on port 8000."); diff --git a/server/src/lib.rs b/server/src/lib.rs index eba116c5..315d266c 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -18,5 +18,6 @@ pub mod project; pub mod rbx; pub mod roblox_studio; pub mod session; +pub mod vfs; pub mod web; pub mod web_util; \ No newline at end of file diff --git a/server/src/session.rs b/server/src/session.rs index 37239bcb..483162be 100644 --- a/server/src/session.rs +++ b/server/src/session.rs @@ -1,20 +1,36 @@ use std::{ - sync::{Arc, RwLock}, + sync::{Arc, RwLock, Mutex, mpsc}, + thread, + io, + time::Duration, }; use rand; +use notify::{ + self, + DebouncedEvent, + RecommendedWatcher, + RecursiveMode, + Watcher, +}; + use ::{ message_queue::MessageQueue, rbx::RbxTree, - project::Project, + project::{Project, ProjectNode}, + vfs::Vfs, }; +const WATCH_TIMEOUT_MS: u64 = 100; + pub struct Session { project: Project, pub session_id: String, pub message_queue: Arc, pub tree: Arc>, + vfs: Arc>, + watchers: Vec, } impl Session { @@ -26,9 +42,81 @@ impl Session { project, message_queue: Arc::new(MessageQueue::new()), tree: Arc::new(RwLock::new(RbxTree::new())), + vfs: Arc::new(Mutex::new(Vfs::new())), + watchers: Vec::new(), } } + pub fn start(&mut self) -> io::Result<()> { + fn add_sync_points(vfs: &mut Vfs, project_node: &ProjectNode) -> io::Result<()> { + match project_node { + ProjectNode::Regular { children, .. } => { + for child in children.values() { + add_sync_points(vfs, child)?; + } + }, + ProjectNode::SyncPoint { path } => { + vfs.add_root(path)?; + }, + } + + Ok(()) + } + + { + let mut vfs = self.vfs.lock().unwrap(); + + for child in self.project.tree.values() { + add_sync_points(&mut vfs, child)?; + } + + for root in vfs.get_roots() { + println!("Watching {}", root.display()); + + let (watch_tx, watch_rx) = mpsc::channel(); + + let mut watcher = notify::watcher(watch_tx, Duration::from_millis(WATCH_TIMEOUT_MS)).unwrap(); + + watcher.watch(root, RecursiveMode::Recursive).unwrap(); + self.watchers.push(watcher); + + let vfs = Arc::clone(&self.vfs); + + thread::spawn(move || { + println!("Thread started"); + loop { + match watch_rx.recv() { + Ok(event) => { + println!("Event: {:?}", event); + + match event { + DebouncedEvent::Create(path) | DebouncedEvent::Write(path) => { + let mut vfs = vfs.lock().unwrap(); + vfs.add_or_update(&path).unwrap(); + }, + DebouncedEvent::Remove(path) => { + let mut vfs = vfs.lock().unwrap(); + vfs.remove(&path); + }, + DebouncedEvent::Rename(from_path, to_path) => { + let mut vfs = vfs.lock().unwrap(); + vfs.remove(&from_path); + vfs.add_or_update(&to_path).unwrap(); + }, + _ => continue, + }; + }, + Err(_) => break, + }; + } + println!("Thread stopped"); + }); + } + } + + Ok(()) + } + pub fn get_project(&self) -> &Project { &self.project } diff --git a/server/src/vfs.rs b/server/src/vfs.rs new file mode 100644 index 00000000..70e02c04 --- /dev/null +++ b/server/src/vfs.rs @@ -0,0 +1,146 @@ +use std::{ + collections::{HashMap, HashSet}, + path::{Path, PathBuf}, + fs, + io, +}; + +pub struct Vfs { + contents: HashMap>, + items: HashMap, + roots: HashSet, +} + +impl Vfs { + pub fn new() -> Vfs { + Vfs { + contents: HashMap::new(), + items: HashMap::new(), + roots: HashSet::new(), + } + } + + pub fn add_root<'a, 'b>(&'a mut self, root_path: &'b Path) -> io::Result<&'a VfsItem> { + debug_assert!(root_path.is_absolute()); + + self.roots.insert(root_path.to_path_buf()); + + VfsItem::get(self, root_path) + } + + pub fn get_roots(&self) -> &HashSet { + &self.roots + } + + pub fn get(&mut self, path: &Path) -> Option<&VfsItem> { + debug_assert!(path.is_absolute()); + debug_assert!(self.is_valid_path(path)); + + self.items.get(path) + } + + pub fn remove(&mut self, path: &Path) { + debug_assert!(path.is_absolute()); + debug_assert!(self.is_valid_path(path)); + + match self.items.remove(path) { + Some(item) => match item { + VfsItem::File(_) => { + self.contents.remove(path); + }, + VfsItem::Directory(VfsDirectory { children, .. }) => { + for child_path in &children { + self.remove(child_path); + } + }, + }, + None => {}, + } + } + + pub fn add_or_update<'a, 'b>(&'a mut self, path: &'b Path) -> io::Result<&'a VfsItem> { + debug_assert!(path.is_absolute()); + debug_assert!(self.is_valid_path(path)); + + VfsItem::get(self, path) + } + + fn is_valid_path(&self, path: &Path) -> bool { + let mut is_valid_path = false; + + for root_path in &self.roots { + if path.starts_with(root_path) { + is_valid_path = true; + break; + } + } + + is_valid_path + } +} + +pub struct VfsFile { + path: PathBuf, +} + +impl VfsFile { + pub fn read_contents<'file, 'vfs>(&'file self, vfs: &'vfs mut Vfs) -> io::Result<&'vfs [u8]> { + if !vfs.contents.contains_key(&self.path) { + let contents = fs::read(&self.path)?; + vfs.contents.insert(self.path.clone(), contents); + } + + Ok(vfs.contents.get(&self.path).unwrap()) + } +} + +pub struct VfsDirectory { + path: PathBuf, + children: HashSet, +} + +pub enum VfsItem { + File(VfsFile), + Directory(VfsDirectory), +} + +impl VfsItem { + fn get<'a, 'b>(vfs: &'a mut Vfs, root_path: &'b Path) -> io::Result<&'a VfsItem> { + let metadata = fs::metadata(root_path)?; + + if metadata.is_file() { + let item = VfsItem::File(VfsFile { + path: root_path.to_path_buf(), + }); + + vfs.items.insert(root_path.to_path_buf(), item); + + let contents = fs::read(root_path)?; + vfs.contents.insert(root_path.to_path_buf(), contents); + + Ok(vfs.items.get(root_path).unwrap()) + } else if metadata.is_dir() { + let mut children = HashSet::new(); + + for entry in fs::read_dir(root_path)? { + let entry = entry?; + let path = entry.path(); + + VfsItem::get(vfs, &path)?; + + children.insert(path); + } + + let item = VfsItem::Directory(VfsDirectory { + path: root_path.to_path_buf(), + children, + }); + + vfs.items.insert(root_path.to_path_buf(), item); + + Ok(vfs.items.get(root_path).unwrap()) + } else { + unimplemented!(); + } + } +} \ No newline at end of file diff --git a/test-projects/single-sync-point/lib/main.lua b/test-projects/single-sync-point/lib/main.lua new file mode 100644 index 00000000..e69de29b diff --git a/test-projects/single-sync-point/roblox-project.json b/test-projects/single-sync-point/roblox-project.json new file mode 100644 index 00000000..3681fb2b --- /dev/null +++ b/test-projects/single-sync-point/roblox-project.json @@ -0,0 +1,11 @@ +{ + "name": "empty", + "tree": { + "ReplicatedStorage": { + "$className": "ReplicatedStorage", + "Foo": { + "$path": "lib" + } + } + } +} \ No newline at end of file