//! Per-process mount tables, reserved self files, and the live process namespace. use crate::Host; use sausage_vfs::{ Entry, File, Generated, Layer, Memory, Mounts, Open, Process, Result, Stat, async_trait, }; use std::{io, sync::Arc}; fn denied() -> Result { Err(io::ErrorKind::ReadOnlyFilesystem.into()) } fn missing() -> Result { Err(io::ErrorKind::NotFound.into()) } fn directory() -> Stat { Stat { directory: true, symlink: false, size: 0, accessed: None, modified: None, } } fn entry(name: String, directory: bool) -> Entry { Entry { name, directory, symlink: false, } } fn reserved(path: &str) -> bool { matches!( path.trim_start_matches('/').split('/').next(), Some("info.json" | "log" | "fs") ) } impl Host { /// Create a console filesystem view without adding a process. Its `/self` is private and ephemeral. pub async fn filesystem(&self) -> anyhow::Result { self.mounts(None, Memory::default(), Process::default()) .await } pub(crate) async fn mounts( &self, pid: Option, memory: Memory, tracker: Process, ) -> anyhow::Result { let root = self.0.world.root.clone(); let operations = tracker.clone(); let host = tracker .clone() .blocking(move || sausage_vfs::Host::new(root, tracker)) .await?; let mut mounts = Mounts::default(); mounts.mount( "/", self.0 .observer .layer(Arc::new(host), "/", operations.clone()), false, ); mounts.mount("/proc", Arc::new(Proc(self.clone())), true); mounts.mount( "/self", self.0.observer.layer( Arc::new(SelfFolder { host: self.clone(), pid: pid.clone(), memory, }), &format!("/proc/{}", pid.as_deref().unwrap_or("console")), operations.clone(), ), false, ); mounts.mount( "/run", self.0 .observer .layer(Arc::new(self.0.run.clone()), "/run", operations), false, ); Ok(mounts) } fn core_files(&self) -> Generated { let mut files = Generated::default(); files.insert( "/discover.json", || Ok(include_bytes!("core.json").to_vec()), ); let protocol = include_bytes!("../../PROTOCOL.md"); files.insert("/PROTOCOL.md", move || Ok(protocol.to_vec())); let graph = self.graph_json(); files.insert("/graph.json", move || Ok(graph.clone())); let dot = self.graph_dot(); files.insert("/graph.dot", move || Ok(dot.clone())); let locks = { let state = self.0.state.lock().unwrap(); let locks: std::collections::BTreeMap<_, _> = state .locks .iter() .map(|(path, lock)| { let waiting: Vec<_> = lock.queue.iter().map(|(pid, _)| pid).collect(); let info = serde_json::json!({ "owner": lock.owner, "waiting": waiting, }); (path.clone(), info) }) .collect(); serde_json::to_vec(&locks).unwrap() }; files.insert("/locks.json", move || Ok(locks.clone())); for name in self.discovered_plugins() { if let Some(doc) = self.discover_document(&name) { let bytes = serde_json::to_vec(&doc).unwrap(); files.insert(&format!("/plugins/{name}/discover.json"), move || { Ok(bytes.clone()) }); files.insert(&format!("/plugins/{name}/client.rs"), move || { sausage_interface::client_rs(&name, &doc) .map(String::into_bytes) .map_err(io::Error::other) }); } } files } } struct Proc(Host); impl Proc { fn folder(&self, path: &str) -> Result<(SelfFolder, String)> { let (pid, rest) = path .trim_start_matches('/') .split_once('/') .unwrap_or((path.trim_start_matches('/'), "")); let memory = self .0 .0 .state .lock() .unwrap() .processes .get(pid) .map(|p| p.memory.clone()) .ok_or(io::ErrorKind::NotFound)?; Ok(( SelfFolder { host: self.0.clone(), pid: Some(pid.into()), memory, }, format!("/{rest}"), )) } } #[async_trait] impl Layer for Proc { async fn stat(&self, path: &str, follow: bool) -> Result { let path = path.trim_end_matches('/'); let path = if path.is_empty() { "/" } else { path }; if matches!(path.trim_end_matches('/'), "" | "/core" | "/core/plugins") { return Ok(directory()); } if let Some(path) = path.strip_prefix("/core/") { return self.0.core_files().stat(&format!("/{path}"), follow).await; } let (folder, path) = self.folder(path)?; folder.stat(&path, follow).await } async fn list(&self, path: &str) -> Result> { let path = path.trim_end_matches('/'); let path = if path.is_empty() { "/" } else { path }; if path == "/" { let mut entries = vec![entry("core".into(), true)]; entries.extend(self.0.processes().into_keys().map(|pid| entry(pid, true))); entries.sort_by(|a, b| a.name.cmp(&b.name)); return Ok(entries); } if path.trim_end_matches('/') == "/core/plugins" { return Ok(self .0 .discovered_plugins() .into_iter() .filter(|name| self.0.discover_document(name).is_some()) .map(|name| entry(name, true)) .collect()); } if path.trim_end_matches('/') == "/core" { let mut entries = self.0.core_files().list("/").await?; if !entries.iter().any(|e| e.name == "plugins") { entries.push(entry("plugins".into(), true)); } return Ok(entries); } if let Some(path) = path.strip_prefix("/core/") { return self.0.core_files().list(&format!("/{path}")).await; } let (folder, path) = self.folder(path)?; folder.list(&path).await } async fn open(&self, path: &str, options: Open) -> Result> { let path = path.trim_end_matches('/'); let path = if path.is_empty() { "/" } else { path }; if options.write || options.create || options.truncate { return denied(); } if matches!(path.trim_end_matches('/'), "" | "/core" | "/core/plugins") { return Memory::default().open("/", options).await; } if let Some(path) = path.strip_prefix("/core/") { return self.0.core_files().open(&format!("/{path}"), options).await; } let (folder, path) = self.folder(path)?; folder.open(&path, options).await } async fn mkdir(&self, _: &str) -> Result<()> { denied() } async fn remove_file(&self, _: &str) -> Result<()> { denied() } async fn remove_dir(&self, _: &str) -> Result<()> { denied() } async fn rename(&self, _: &str, _: &str) -> Result<()> { denied() } } struct SelfFolder { host: Host, pid: Option, memory: Memory, } impl SelfFolder { async fn generated(&self, path: &str) -> Result { let pid = self.pid.as_ref().ok_or(io::ErrorKind::NotFound)?; let bytes = match path { "/info.json" => { let info = self .host .0 .state .lock() .unwrap() .processes .get(pid) .map(|p| p.info.clone()) .ok_or(io::ErrorKind::NotFound)?; serde_json::to_vec(&info).unwrap() } "/log" => { let lines = self .host .0 .journal .query(serde_json::json!({"pid":pid,"limit":i64::MAX})) .await; let mut bytes = Vec::new(); for line in lines.as_array().unwrap() { serde_json::to_writer(&mut bytes, line).unwrap(); bytes.push(b'\n'); } bytes } _ => return missing(), }; let mut files = Generated::default(); files.insert(path, move || Ok(bytes.clone())); Ok(files) } } #[async_trait] impl Layer for SelfFolder { async fn stat(&self, path: &str, follow: bool) -> Result { if reserved(path) { self.generated(path).await?.stat(path, follow).await } else { self.memory.stat(path, follow).await } } async fn list(&self, path: &str) -> Result> { let mut entries = self.memory.list(path).await?; if path == "/" && self.pid.is_some() { entries.extend([entry("info.json".into(), false), entry("log".into(), false)]); } Ok(entries) } async fn open(&self, path: &str, options: Open) -> Result> { if reserved(path) { if options.write || options.create || options.truncate { return denied(); } self.generated(path).await?.open(path, options).await } else { self.memory.open(path, options).await } } async fn mkdir(&self, path: &str) -> Result<()> { if reserved(path) { denied() } else { self.memory.mkdir(path).await } } async fn remove_file(&self, path: &str) -> Result<()> { if reserved(path) { denied() } else { self.memory.remove_file(path).await } } async fn remove_dir(&self, path: &str) -> Result<()> { if reserved(path) || path == "/" { denied() } else { self.memory.remove_dir(path).await } } async fn rename(&self, from: &str, to: &str) -> Result<()> { if reserved(from) || reserved(to) || from == "/" || to == "/" { denied() } else { self.memory.rename(from, to).await } } }