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:
+72
-9
@@ -15,7 +15,10 @@ use wasmtime::component::{Component, Linker, ResourceTable};
|
||||
use wasmtime::{Config, Engine, Store};
|
||||
use wasmtime_wasi::cli::{AsyncStdinStream, AsyncStdoutStream};
|
||||
|
||||
use wasmtime_wasi::p2::bindings::Command;
|
||||
use wasmtime_wasi::p2::bindings::CommandPre;
|
||||
|
||||
/// A component with its imports resolved once for this runtime.
|
||||
pub(crate) struct Prepared(Result<CommandPre<ProcessState>, String>);
|
||||
|
||||
use wasmtime_wasi::{I32Exit, WasiCtx, WasiCtxView, WasiView};
|
||||
use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpCtxView, WasiHttpHooks, WasiHttpView};
|
||||
@@ -130,17 +133,27 @@ pub struct Instance {
|
||||
pub stdout: mpsc::Receiver<String>,
|
||||
/// The stderr lines that the process writes.
|
||||
pub stderr: mpsc::Receiver<String>,
|
||||
stderr_task: Option<JoinHandle<()>>,
|
||||
task: Option<JoinHandle<Exit>>,
|
||||
store_ended: tokio::sync::watch::Sender<bool>,
|
||||
}
|
||||
|
||||
impl Drop for Instance {
|
||||
fn drop(&mut self) {
|
||||
if let Some(task) = self.task.take() {
|
||||
let task = self.task.take();
|
||||
let stderr = self.stderr_task.take();
|
||||
if let Some(task) = &task {
|
||||
task.abort();
|
||||
}
|
||||
if task.is_some() || stderr.is_some() {
|
||||
let ended = self.store_ended.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = task.await;
|
||||
if let Some(task) = task {
|
||||
let _ = task.await;
|
||||
}
|
||||
if let Some(stderr) = stderr {
|
||||
let _ = stderr.await;
|
||||
}
|
||||
ended.send_replace(true);
|
||||
});
|
||||
}
|
||||
@@ -179,6 +192,10 @@ impl Instance {
|
||||
pub async fn wait(&mut self) -> Exit {
|
||||
let result = self.task.as_mut().unwrap().await;
|
||||
self.task.take();
|
||||
if let Some(stderr) = self.stderr_task.as_mut() {
|
||||
let _ = stderr.await;
|
||||
}
|
||||
self.stderr_task.take();
|
||||
self.store_ended.send_replace(true);
|
||||
match result {
|
||||
Ok(exit) => exit,
|
||||
@@ -238,6 +255,28 @@ impl Runtime {
|
||||
Ok(Component::new(&self.engine, bytes)?)
|
||||
}
|
||||
|
||||
pub(crate) fn prepare(&self, component: &Component) -> Prepared {
|
||||
// Preserve link failures as process exits, rather than load errors.
|
||||
Prepared(
|
||||
self.linker
|
||||
.instantiate_pre(component)
|
||||
.and_then(CommandPre::new)
|
||||
.map_err(|error| error.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) async fn start_logged(
|
||||
self: &Arc<Self>,
|
||||
prepared: &Prepared,
|
||||
cfg: InstanceConfig,
|
||||
journal: Arc<crate::journal::Journal>,
|
||||
pid: String,
|
||||
plugin: String,
|
||||
) -> Result<Instance> {
|
||||
self.start_inner(prepared.0.clone(), cfg, &[], Some((journal, pid, plugin)))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Start a WASI task with the supplied virtual namespace.
|
||||
pub async fn start(
|
||||
self: &Arc<Self>,
|
||||
@@ -253,6 +292,17 @@ impl Runtime {
|
||||
component: &Component,
|
||||
cfg: InstanceConfig,
|
||||
args: &[String],
|
||||
) -> Result<Instance> {
|
||||
self.start_inner(self.prepare(component).0, cfg, args, None)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn start_inner(
|
||||
self: &Arc<Self>,
|
||||
prepared: Result<CommandPre<ProcessState>, String>,
|
||||
cfg: InstanceConfig,
|
||||
args: &[String],
|
||||
log: Option<(Arc<crate::journal::Journal>, String, String)>,
|
||||
) -> Result<Instance> {
|
||||
let (host_in, guest_in) = tokio::io::duplex(64 * 1024);
|
||||
let (guest_out, host_out) = tokio::io::duplex(64 * 1024);
|
||||
@@ -299,22 +349,34 @@ impl Runtime {
|
||||
});
|
||||
|
||||
let (err_tx, err_rx) = mpsc::channel(1024);
|
||||
tokio::spawn(async move {
|
||||
let mut lines = BufReader::new(host_err).lines();
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
if err_tx.send(line).await.is_err() {
|
||||
let stderr_task = tokio::spawn(async move {
|
||||
let mut reader = BufReader::new(host_err);
|
||||
let mut bytes = Vec::new();
|
||||
while let Ok(n) = reader.read_until(b'\n', &mut bytes).await {
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
let line = String::from_utf8_lossy(&bytes)
|
||||
.trim_end_matches(['\r', '\n'])
|
||||
.to_string();
|
||||
bytes.clear();
|
||||
if let Some((journal, pid, plugin)) = &log {
|
||||
journal.write(pid, plugin, "stderr", &line);
|
||||
} else if err_tx.send(line).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
let rt = self.clone();
|
||||
let component = component.clone();
|
||||
cfg.store_ended.send_replace(false);
|
||||
let task = tokio::spawn(async move {
|
||||
let mut store = Store::new(&rt.engine, state);
|
||||
store.epoch_deadline_async_yield_and_update(1);
|
||||
let result = async {
|
||||
let cmd = Command::instantiate_async(&mut store, &component, &rt.linker).await?;
|
||||
let cmd = prepared
|
||||
.map_err(wasmtime::Error::msg)?
|
||||
.instantiate_async(&mut store)
|
||||
.await?;
|
||||
cmd.wasi_cli_run().call_run(&mut store).await
|
||||
}
|
||||
.await;
|
||||
@@ -336,6 +398,7 @@ impl Runtime {
|
||||
stdin: Some(host_in),
|
||||
stdout: line_rx,
|
||||
stderr: err_rx,
|
||||
stderr_task: Some(stderr_task),
|
||||
task: Some(task),
|
||||
store_ended: cfg.store_ended,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user