325 lines
11 KiB
Rust
325 lines
11 KiB
Rust
//! 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<T>() -> Result<T> {
|
|
Err(io::ErrorKind::ReadOnlyFilesystem.into())
|
|
}
|
|
fn missing<T>() -> Result<T> {
|
|
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<Mounts> {
|
|
self.mounts(None, Memory::default(), Process::default())
|
|
.await
|
|
}
|
|
pub(crate) async fn mounts(
|
|
&self,
|
|
pid: Option<String>,
|
|
memory: Memory,
|
|
tracker: Process,
|
|
) -> anyhow::Result<Mounts> {
|
|
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();
|
|
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<Stat> {
|
|
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<Vec<Entry>> {
|
|
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<Arc<dyn File>> {
|
|
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<String>,
|
|
memory: Memory,
|
|
}
|
|
impl SelfFolder {
|
|
async fn generated(&self, path: &str) -> Result<Generated> {
|
|
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<Stat> {
|
|
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<Vec<Entry>> {
|
|
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<Arc<dyn File>> {
|
|
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
|
|
}
|
|
}
|
|
}
|