Files
sausage/cli/src/execution.rs
T
surma d7b78ed0fd 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.
2026-10-23 02:25:03 +01:00

130 lines
4.0 KiB
Rust

//! Execution of native futures and WASI processes, including transport drainage.
use crate::{
Host, NativeContext, NativePlugin,
process::ProcessInfo,
runtime::{Instance, InstanceConfig},
};
use serde_json::{Value, json};
use tokio::sync::mpsc;
impl Host {
/// Run one process and retain its cleanup guard until all transport reads finish.
pub(crate) async fn run_process(
&self,
pid: String,
info: ProcessInfo,
component: Option<std::sync::Arc<crate::runtime::Prepared>>,
native: Option<NativePlugin>,
incoming: mpsc::UnboundedReceiver<Value>,
has_id: bool,
) -> String {
if let Some(native) = native {
let context = NativeContext {
host: self.clone(),
pid: pid.clone(),
};
let result = native(context, info.method, info.params).await;
self.answer(&pid, result);
return "exit 0".into();
}
let result = self
.run_guest(&pid, &info, &component.unwrap(), incoming, has_id)
.await;
result.unwrap_or_else(|error| error.to_string())
}
/// Drain stdout before deciding whether the guest returned an answer.
async fn run_guest(
&self,
pid: &str,
info: &ProcessInfo,
component: &crate::runtime::Prepared,
mut incoming: mpsc::UnboundedReceiver<Value>,
has_id: bool,
) -> anyhow::Result<String> {
let (memory, tracker, store_ended) = {
let state = self.0.state.lock().unwrap();
let p = state.processes.get(pid).unwrap();
(p.memory.clone(), p.files.clone(), p.store_ended.clone())
};
let config = InstanceConfig {
filesystem: self.mounts(Some(pid.into()), memory, tracker).await?,
store_ended,
network: true,
env: vec![
("SAUSAGE_PID".into(), pid.into()),
("SAUSAGE_PLUGIN".into(), info.plugin.clone()),
(
"SAUSAGE_PARENT".into(),
info.parent.clone().unwrap_or_else(|| "core".into()),
),
],
};
let mut instance = self
.0
.runtime
.start_logged(
component,
config,
self.0.journal.clone(),
pid.into(),
info.plugin.clone(),
)
.await?;
let mut initial = json!({
"jsonrpc": "2.0",
"method": info.method,
"params": info.params,
});
if has_id {
initial["id"] = json!(pid);
}
let mut stdin_open = self.forward_input(pid, &mut instance, Some(initial)).await;
loop {
tokio::select! {
line = instance.stdout.recv() => {
if !self.stdout_line(pid,line) { break; }
}
value = incoming.recv(), if stdin_open => {
stdin_open = self.forward_input(pid,&mut instance,value).await;
}
}
}
Ok(instance.wait().await.to_string())
}
/// Close stdin only after the queued responses reach the guest.
async fn forward_input(
&self,
pid: &str,
instance: &mut Instance,
value: Option<Value>,
) -> bool {
if let Some(value) = value
&& self.write_input(pid, instance, value).await.is_ok()
{
return true;
}
instance.stdin.take();
false
}
async fn write_input(
&self,
pid: &str,
instance: &mut Instance,
value: Value,
) -> std::io::Result<()> {
instance
.send_line(&value.to_string(), |line| self.output(pid, line))
.await
}
fn stdout_line(&self, pid: &str, line: Option<String>) -> bool {
if let Some(line) = line {
self.output(pid, line);
true
} else {
false
}
}
}