core: sausage-vfs per process (/, /proc, /self, /run), activation, graph, watcher, upgrades

- /proc is a core layer: core/ (PROTOCOL.md, graph.json, graph.dot,
  locks.json, plugins/<name>/discover.json and client.rs) and one folder
  per process (info.json, log, and its /self files).
- Locks are released only after the task ended, the store dropped, and
  in-flight file operations returned.
- core.activate and core.revert with copies, discovery, compatibility
  checks of imports and consumers, groups, force. An admission gate
  makes the snapshot of old processes complete.
- Graph of discover documents by hash, refreshed by a file watcher.
- Upgrades: grace periods (x-sausage-grace), stop, service restarts,
  rpc.retired.
- SDK plugins in the real core (cli/tests/sdk.rs).
This commit is contained in:
2026-10-23 01:05:19 +01:00
parent 151550242b
commit edc2d1a63b
21 changed files with 2306 additions and 101 deletions
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "activation-guest"
version = "0.1.0"
edition = "2024"
[workspace]
[dependencies]
serde_json = "1"
+53
View File
@@ -0,0 +1,53 @@
//! Raw discovery and lifecycle guest for activation protocol tests.
use serde_json::{Value, json};
use std::io::{self, BufRead, Write};
/// Emits one flushed JSON-RPC response.
fn send(value: Value) {
println!("{value}");
io::stdout().flush().unwrap();
}
/// Reads a version-specific discovery document without the SDK.
fn main() {
let version = env!("FIXTURE_VERSION");
let plugin = std::env::var("SAUSAGE_PLUGIN").unwrap();
let mut lines = io::stdin().lock().lines();
let request: Value = serde_json::from_str(&lines.next().unwrap().unwrap()).unwrap();
let document: Value =
serde_json::from_slice(&std::fs::read(format!("/state/{plugin}/{version}.json")).unwrap())
.unwrap();
let result = match request["method"].as_str().unwrap() {
"rpc.discover" => {
if document["x-test-wait"] == true {
std::fs::write(format!("/state/{plugin}/discover-started"), "ready").unwrap();
while !std::path::Path::new(&format!("/state/{plugin}/discover-release")).exists() {
std::thread::sleep(std::time::Duration::from_millis(1));
}
}
document
}
"read" => {
let text =
std::fs::read_to_string(request["params"]["path"].as_str().unwrap()).unwrap();
serde_json::from_str(&text).unwrap_or(json!(text))
}
"rpc.retired" => {
std::fs::write(
format!("/state/{plugin}/retired"),
request["params"]["previous"].as_str().unwrap(),
)
.unwrap();
json!({"retired":version})
}
"serve" | "hold" => {
send(json!({"jsonrpc":"2.0","id":request["id"],"result":{"version":version}}));
if request["method"] == "serve" {
std::fs::write(format!("/state/{plugin}/service"), version).unwrap();
}
for _ in lines {}
std::fs::write(format!("/state/{plugin}/ended-{version}"), "ended").unwrap();
return;
}
_ => json!({"version":version}),
};
send(json!({"jsonrpc":"2.0","id":request["id"],"result":result}));
}
+1
View File
@@ -5,3 +5,4 @@ edition = "2024"
[workspace]
[dependencies]
serde_json = "1"
wasip2 = "1"
+124 -1
View File
@@ -14,6 +14,13 @@ fn receive(lines: &mut Lines) -> Value {
fn echo(params: &Value) -> Value {
params.clone()
}
fn discover(params: &Value) -> Value {
if let Ok(bytes) = std::fs::read("/state/probe/discover.json") {
serde_json::from_slice(&bytes).unwrap()
} else {
params.clone()
}
}
fn environment() -> Value {
json!({
"pid": std::env::var("SAUSAGE_PID").unwrap(),
@@ -155,10 +162,126 @@ fn outgoing_call(params: &Value, lines: &mut Lines) -> Value {
fn lock(params: &Value, lines: &mut Lines) -> Value {
outgoing_call(&json!({"method":"core.lock", "params":params}), lines)
}
fn filesystem(params: &Value) -> Value {
use std::fs;
let pid = std::env::var("SAUSAGE_PID").unwrap();
match params["topic"].as_str().unwrap() {
"root" => {
let preopens = wasip2::filesystem::preopens::get_directories();
assert_eq!(preopens.len(), 1);
assert_eq!(preopens[0].1, "/");
let names: Vec<_> = fs::read_dir("/")
.unwrap()
.map(|e| e.unwrap().file_name().into_string().unwrap())
.collect();
for name in ["proc", "self", "run"] {
assert!(names.iter().any(|n| n == name));
}
fs::write("/state/root", "world").unwrap();
fs::write("/run/shared", "shared").unwrap();
assert!(fs::rename("/run/shared", "/state/moved").is_err());
assert!(fs::rename("/state/root", "/self/moved").is_err());
assert_eq!(fs::read_to_string("/run/../state/root").unwrap(), "world");
}
"reserved" => {
for name in ["info.json", "log", "fs"] {
assert!(fs::write(format!("/self/{name}"), "bad").is_err());
assert!(fs::create_dir(format!("/self/{name}")).is_err());
fs::write("/self/temp", "ok").unwrap();
assert!(fs::rename("/self/temp", format!("/self/{name}")).is_err());
fs::remove_file("/self/temp").unwrap();
}
}
"self" => {
fs::write("/self/endpoint.tmp", "endpoint").unwrap();
fs::rename("/self/endpoint.tmp", "/self/endpoint").unwrap();
assert_eq!(
fs::read_to_string(format!("/proc/{pid}/endpoint")).unwrap(),
"endpoint"
);
assert!(fs::write(format!("/proc/{pid}/endpoint"), "bad").is_err());
assert!(fs::remove_file(format!("/proc/{pid}/endpoint")).is_err());
let info: Value =
serde_json::from_slice(&fs::read("/self/info.json").unwrap()).unwrap();
assert_eq!(info["plugin"], "probe");
assert_eq!(info["method"], "filesystem");
return json!(pid);
}
"isolated" => {
assert!(!fs::exists("/self/endpoint").unwrap());
}
"snapshot" => {
eprintln!("before snapshot");
let mut file = fs::File::open("/self/log").unwrap();
eprintln!("after snapshot");
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
assert!(contents.contains("start filesystem"));
assert!(!contents.contains("after snapshot"));
assert_eq!(file.read(&mut [0; 1]).unwrap(), 0);
}
"core" => {
assert!(
fs::read_to_string("/proc/core/PROTOCOL.md")
.unwrap()
.contains("## 4. The filesystem")
);
for name in ["graph.json", "locks.json"] {
let _: Value =
serde_json::from_slice(&fs::read(format!("/proc/core/{name}")).unwrap())
.unwrap();
}
assert!(
fs::read_to_string("/proc/core/graph.dot")
.unwrap()
.contains("digraph")
);
assert!(fs::read_dir("/proc/core/plugins").is_ok());
assert!(fs::write("/proc/core/graph.json", "bad").is_err());
}
_ => panic!("unknown filesystem topic"),
}
json!({})
}
fn write_loop(request: &Value, lines: &mut Lines) {
assert_eq!(
lock(&json!({"path":"/state/loop"}), lines),
json!({"held":true})
);
std::fs::write("/state/loop", "old").unwrap();
send(
json!({"jsonrpc":"2.0", "id":request["id"], "result":std::env::var("SAUSAGE_PID").unwrap()}),
);
loop {
std::fs::write("/state/loop", "old").unwrap();
}
}
fn locked_write(lines: &mut Lines) -> Value {
assert_eq!(
lock(&json!({"path":"/state/loop"}), lines),
json!({"held":true})
);
std::fs::write("/state/loop", "new").unwrap();
std::thread::sleep(std::time::Duration::from_millis(100));
assert_eq!(std::fs::read_to_string("/state/loop").unwrap(), "new");
json!({})
}
fn dispatch(request: &Value, lines: &mut Lines) -> Option<Value> {
let params = &request["params"];
let result = match request["method"].as_str().unwrap() {
"echo" | "rpc.discover" => echo(params),
"self_serve" => {
std::fs::write("/self/endpoint", "endpoint").unwrap();
serve(request, lines);
return None;
}
"filesystem" => filesystem(params),
"write_loop" => {
write_loop(request, lines);
return None;
}
"locked_write" => locked_write(lines),
"echo" => echo(params),
"rpc.discover" => discover(params),
"env" => environment(),
"multicall" => multicall(lines),
"notification" => notification(params),