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:
+20
-13
@@ -1,7 +1,7 @@
|
||||
//! Serialized candidate-copy activation and revert under PROTOCOL.md section 10.3.
|
||||
use crate::{
|
||||
Host, RpcResult, error,
|
||||
graph::{Interface, io_error, problems},
|
||||
graph::{Interface, io_error, problems, read_hashed},
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use std::{collections::BTreeMap, path::PathBuf};
|
||||
@@ -14,6 +14,13 @@ struct Candidate {
|
||||
impl Host {
|
||||
/// Copies first, discovers that copy, checks the combined graph, then installs it.
|
||||
pub(crate) async fn activation(&self, _caller: &str, method: &str, params: Value) -> RpcResult {
|
||||
let host = self.clone();
|
||||
let method = method.to_owned();
|
||||
tokio::spawn(async move { host.activation_body(&method, params).await })
|
||||
.await
|
||||
.map_err(|failure| error(-32603, &failure.to_string()))?
|
||||
}
|
||||
async fn activation_body(&self, method: &str, params: Value) -> RpcResult {
|
||||
let _activation = self.0.activations.lock().await;
|
||||
let mut candidates = Vec::new();
|
||||
let result = self
|
||||
@@ -79,12 +86,12 @@ impl Host {
|
||||
};
|
||||
let copy = directory.join(format!("activate-{}.wasm", uuid::Uuid::now_v7()));
|
||||
// Read once. Discovery and installation both use the same private bytes.
|
||||
let bytes = tokio::fs::read(source).await.map_err(io_error)?;
|
||||
let (bytes, hash) = read_hashed(source).await.map_err(io_error)?;
|
||||
tokio::fs::write(©, &bytes).await.map_err(io_error)?;
|
||||
self.0
|
||||
.journal
|
||||
.write("core", plugin, "core", "activation copied candidate");
|
||||
let discovery = self.discover_copy(plugin, ©, &bytes).await;
|
||||
let discovery = self.discover_copy(plugin, ©, hash).await;
|
||||
let interface = match discovery {
|
||||
Ok(interface) => interface,
|
||||
Err(error) => {
|
||||
@@ -101,19 +108,13 @@ impl Host {
|
||||
interface,
|
||||
});
|
||||
}
|
||||
self.refresh_graph().await?;
|
||||
let previous = self.0.graph.lock().unwrap().plugins.clone();
|
||||
if previous.values().any(|entry| entry.error.is_some()) {
|
||||
return Err(error(
|
||||
-32003,
|
||||
"activation waits for successful discovery of every plugin",
|
||||
));
|
||||
}
|
||||
let mut proposed = previous.clone();
|
||||
let names: Vec<_> = candidates
|
||||
.iter()
|
||||
.map(|candidate| candidate.plugin.clone())
|
||||
.collect();
|
||||
self.refresh_graph_except(&names).await?;
|
||||
let previous = self.0.graph.lock().unwrap().plugins.clone();
|
||||
let mut proposed = previous.clone();
|
||||
for candidate in candidates.iter() {
|
||||
proposed.insert(candidate.plugin.clone(), candidate.interface.clone());
|
||||
}
|
||||
@@ -160,6 +161,12 @@ impl Host {
|
||||
let admission = self.0.admission.write().await;
|
||||
// Every candidate already has a temporary name before any installed file changes.
|
||||
for candidate in candidates {
|
||||
if previous
|
||||
.get(&candidate.plugin)
|
||||
.is_some_and(|old| old.hash == candidate.interface.hash)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let directory = self.0.world.plugin_dir(&candidate.plugin);
|
||||
let current = directory.join("plugin.wasm");
|
||||
if tokio::fs::try_exists(¤t).await.map_err(io_error)? {
|
||||
@@ -180,7 +187,7 @@ impl Host {
|
||||
);
|
||||
if let Some(old) = previous
|
||||
.get(&candidate.plugin)
|
||||
.filter(|old| old.hash != candidate.interface.hash)
|
||||
.filter(|old| !old.hash.is_empty() && old.hash != candidate.interface.hash)
|
||||
{
|
||||
let grace = old.document["x-sausage-grace"]
|
||||
.as_f64()
|
||||
|
||||
Reference in New Issue
Block a user