sausage core: modules per topic, journal writer thread with WAL and batches, hash cache by metadata, 49 tests per rule

Blank stdout lines give no warning. core.exit requests the exit in its
own handler.
This commit is contained in:
2026-10-23 00:32:33 +01:00
parent 218a718a3d
commit 8542b9fcb8
17 changed files with 2016 additions and 814 deletions
+134
View File
@@ -0,0 +1,134 @@
//! Execution of native futures and WASI processes, including transport drainage.
use crate::{
Host, NativeContext, NativePlugin,
process::{ProcessGuard, 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<wasmtime::component::Component>,
native: Option<NativePlugin>,
incoming: mpsc::UnboundedReceiver<Value>,
has_id: bool,
) {
let _guard = ProcessGuard {
host: self.clone(),
pid: pid.clone(),
};
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);
self.finish(&pid, "exit 0");
return;
}
let result = self
.run_guest(&pid, &info, &component.unwrap(), incoming, has_id)
.await;
self.finish(&pid, &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: &wasmtime::component::Component,
mut incoming: mpsc::UnboundedReceiver<Value>,
has_id: bool,
) -> anyhow::Result<String> {
let config = InstanceConfig {
world: self.0.world.root.clone(),
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(component, config).await?;
let mut initial = json!({
"jsonrpc": "2.0",
"method": info.method,
"params": info.params,
});
if has_id {
initial["id"] = json!(pid);
}
self.write_input(pid, &mut instance, initial).await?;
let mut stdin_open = true;
let mut stderr_open = true;
loop {
tokio::select! {
line = instance.stderr.recv(), if stderr_open => {
stderr_open = self.stderr_line(pid,&info.plugin,line);
}
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?;
}
}
}
while let Some(line) = instance.stderr.recv().await {
self.0.journal.write(pid, &info.plugin, "stderr", &line);
}
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>,
) -> std::io::Result<bool> {
if let Some(value) = value {
self.write_input(pid, instance, value).await?;
Ok(true)
} else {
instance.stdin.take();
Ok(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
}
}
fn stderr_line(&self, pid: &str, plugin: &str, line: Option<String>) -> bool {
if let Some(line) = line {
self.0.journal.write(pid, plugin, "stderr", &line);
true
} else {
false
}
}
}