core: review fixes, init builds the agent, console without a terminal
Activation path (review T6R): a broken plugin or a stray file in /plugins blocks no activation; candidates replace their own entries; discovery has a 10 s deadline and its process is stopped after the answer; an unchanged hash keeps plugin.prev.wasm; activations run in their own task; the watcher is not recursive; no duplicate services; rpc.retired waits for live processes of its version; reads and hashes on blocking threads. Guest I/O and per-call paths: a stdin write error is EOF, stdout and stderr are drained before the status; stderr is lossy UTF-8 and goes to the journal from its own reader; compiled components are freed; shutdown starts no new processes; lock queues drop stopping processes; compile before the admission gate; CommandPre per component; core.unlock in order; null errors are successes; the journal thread survives SQLite errors. sausage init builds the agent with cargo.build; sausage without a command creates ./world. The console reads plain lines without a terminal. PROTOCOL.md and DECISIONS.md describe the decided rules. The flake tests the whole workspace and checks the packaged cargo.build.
This commit is contained in:
+70
-18
@@ -93,20 +93,31 @@ impl Host {
|
||||
{
|
||||
return Err(error(-32601, "invalid plugin name"));
|
||||
}
|
||||
let admission = host.0.admission.read().await;
|
||||
let native = host.0.state.lock().unwrap().native.get(plugin).cloned();
|
||||
let (component, version) = if native.is_some() {
|
||||
(None, "native".into())
|
||||
} else {
|
||||
let (component, hash) = host
|
||||
.0
|
||||
.components
|
||||
.load(
|
||||
wasm.unwrap_or_else(|| host.0.world.plugin_dir(plugin).join("plugin.wasm")),
|
||||
host.0.runtime.clone(),
|
||||
let path = wasm.unwrap_or_else(|| host.0.world.plugin_dir(plugin).join("plugin.wasm"));
|
||||
let (native, component, version, admission) = loop {
|
||||
if host.0.shutdown.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
return Err(error(-32600, "host is stopping"));
|
||||
}
|
||||
let native = host.0.state.lock().unwrap().native.get(plugin).cloned();
|
||||
let loaded = if native.is_none() {
|
||||
Some(
|
||||
host.0
|
||||
.components
|
||||
.load(path.clone(), host.0.runtime.clone())
|
||||
.await?,
|
||||
)
|
||||
.await?;
|
||||
(Some(component), hash)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let admission = host.0.admission.read().await;
|
||||
if let Some(loaded) = loaded {
|
||||
if crate::components::Fingerprint::read(&path).await? != loaded.fingerprint {
|
||||
drop(admission);
|
||||
continue;
|
||||
}
|
||||
break (native, Some(loaded.prepared), loaded.hash, admission);
|
||||
}
|
||||
break (native, None, "native".into(), admission);
|
||||
};
|
||||
let pid = format!("{plugin}-{}", Uuid::now_v7());
|
||||
let (tx, rx) = oneshot::channel();
|
||||
@@ -126,6 +137,9 @@ impl Host {
|
||||
};
|
||||
{
|
||||
let mut state = host.0.state.lock().unwrap();
|
||||
if host.0.shutdown.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
return Err(error(-32600, "host is stopping"));
|
||||
}
|
||||
if caller != "console"
|
||||
&& caller != "core"
|
||||
&& !state
|
||||
@@ -149,7 +163,7 @@ impl Host {
|
||||
abort: None,
|
||||
caller,
|
||||
id: id.clone(),
|
||||
answer: Some(tx),
|
||||
answer: if id.is_some() { Some(tx) } else { None },
|
||||
ended: watch::channel(false).0,
|
||||
},
|
||||
);
|
||||
@@ -220,11 +234,18 @@ impl Host {
|
||||
return;
|
||||
}
|
||||
if let Some(method) = v["method"].as_str() {
|
||||
if method == "core.notify" {
|
||||
if method == "core.notify" || method == "core.unlock" {
|
||||
// Deliver progress before a following response removes its destination.
|
||||
let params = v.get("params").cloned().unwrap_or(json!({}));
|
||||
let result = if !params.is_object() {
|
||||
Err(error(-32602, "params must be an object"))
|
||||
} else if method == "core.unlock" {
|
||||
if let Some(path) = params["path"].as_str() {
|
||||
self.unlock(pid, path);
|
||||
Ok(Value::Null)
|
||||
} else {
|
||||
Err(error(-32602, "path must be a string"))
|
||||
}
|
||||
} else if let Some(name) = params["name"].as_str() {
|
||||
self.notify(
|
||||
pid,
|
||||
@@ -277,7 +298,7 @@ impl Host {
|
||||
} else if v["id"] == pid {
|
||||
self.answer(
|
||||
pid,
|
||||
if let Some(e) = v.get("error") {
|
||||
if let Some(e) = v.get("error").filter(|e| !e.is_null()) {
|
||||
Err(e.clone())
|
||||
} else {
|
||||
Ok(v["result"].clone())
|
||||
@@ -287,8 +308,39 @@ impl Host {
|
||||
}
|
||||
pub(crate) fn discard(&self, pid: &str) {
|
||||
// The journal deduplicates this warning for each process.
|
||||
if let Some(info) = self.processes().get(pid) {
|
||||
self.0.journal.warning(pid, &info.plugin);
|
||||
if let Some(plugin) = self
|
||||
.0
|
||||
.state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.processes
|
||||
.get(pid)
|
||||
.map(|p| p.info.plugin.clone())
|
||||
{
|
||||
self.0.journal.warning(pid, &plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
/// PROTOCOL 6: an unlock from stdout completes before output returns.
|
||||
#[tokio::test]
|
||||
async fn stdout_unlock_is_synchronous() {
|
||||
let folder = std::env::temp_dir().join(format!("sausage-unlock-{}", Uuid::now_v7()));
|
||||
crate::world::World::create(&folder).unwrap();
|
||||
let host = Host::start_with_cache(&folder, &folder.join("cache"))
|
||||
.await
|
||||
.unwrap();
|
||||
host.lock("core", "ordered", true).await.unwrap();
|
||||
host.output(
|
||||
"core",
|
||||
r#"{"jsonrpc":"2.0","method":"core.unlock","params":{"path":"ordered"}}"#.into(),
|
||||
);
|
||||
let held = host.lock("console", "ordered", false).await.unwrap();
|
||||
host.stop().await;
|
||||
std::fs::remove_dir_all(folder).unwrap();
|
||||
assert_eq!(held, json!({"held":true}));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user