Register core.watch before its armed notification, coalesce later changes without losing post-notification writes, and clean up on caller death. Observe mounted layers and open file handles, including descriptor/stream writes, truncation, both rename paths and deletion. Track cancelled mutations through process idle and retain visible paths across renames. Document the protocol, expose native core discovery, and exercise watches through SDK callbacks and raw WASI filesystem operations. Card: SAU-359
329 lines
11 KiB
Rust
329 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();
|
|
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<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
|
|
}
|
|
}
|
|
}
|