Files
sausage/userland/spawn/src/lib.rs
T
2026-11-06 22:20:05 +00:00

69 lines
3.7 KiB
Rust

//! Launch world programs through the process interface. No native subprocesses.
//! Commands inherit the environment, cwd, and streams unless the caller changes them.
//! Pipe callers must drain stdout and stderr before wait to avoid bounded-pipe deadlocks.
use std::{collections::BTreeMap, io, path::{Path, PathBuf}};
mod bindings {
wit_bindgen::generate!({ path: "../../host/wit", world: "program", generate_all, with: { "wasi:io/poll@0.2.12": ::wasi::io::poll } });
}
use bindings::sausage::process::host;
pub use bindings::wasi::io::streams::{InputStream, OutputStream};
pub use host::{Stdio, Watcher, Change, ChangeKind};
pub use wasi::io::poll::{Pollable, poll};
/// Hard-stop a process group by a live process ID.
pub fn kill(pid: u32) -> io::Result<()> { host::kill(pid).map_err(io::Error::other) }
/// A prepared child process.
pub struct Command {
program: String,
args: Vec<String>,
env: BTreeMap<String, String>,
cwd: PathBuf,
group: Option<u32>,
stdin: Stdio, stdout: Stdio, stderr: Stdio,
}
impl Command {
/// Use the parent's cwd, environment, and streams.
pub fn new(program: impl AsRef<str>) -> Self {
Self { program: program.as_ref().into(), args: Vec::new(), env: std::env::vars().collect(),
cwd: std::env::current_dir().unwrap_or_else(|_| "/".into()), group: None,
stdin: Stdio::Inherit, stdout: Stdio::Inherit, stderr: Stdio::Inherit }
}
pub fn arg(&mut self, arg: impl AsRef<str>) -> &mut Self { self.args.push(arg.as_ref().into()); self }
pub fn args(&mut self, args: impl IntoIterator<Item = impl AsRef<str>>) -> &mut Self {
for arg in args { self.arg(arg); } self
}
pub fn env(&mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> &mut Self {
self.env.insert(key.as_ref().into(), value.as_ref().into()); self
}
pub fn env_clear(&mut self) -> &mut Self { self.env.clear(); self }
pub fn current_dir(&mut self, cwd: impl AsRef<Path>) -> &mut Self { self.cwd = cwd.as_ref().into(); self }
/// Isolate the child group so kill does not stop the caller's group.
pub fn new_group(&mut self) -> &mut Self { self.group = Some(0); self }
pub fn stdin(&mut self, mode: Stdio) -> &mut Self { self.stdin = mode; self }
pub fn stdout(&mut self, mode: Stdio) -> &mut Self { self.stdout = mode; self }
pub fn stderr(&mut self, mode: Stdio) -> &mut Self { self.stderr = mode; self }
/// Resolve PATH and start the component.
pub fn spawn(&self) -> io::Result<Child> {
let child = host::spawn(&host::Request { program: self.program.clone(), argv: std::iter::once(self.program.clone()).chain(self.args.clone()).collect(),
env: self.env.clone().into_iter().collect(), cwd: self.cwd.to_string_lossy().into_owned(), group: self.group,
stdin: self.stdin, stdout: self.stdout, stderr: self.stderr }).map_err(io::Error::other)?;
Ok(Child { stdin: matches!(self.stdin, Stdio::Pipe).then(|| child.stdin()),
stdout: matches!(self.stdout, Stdio::Pipe).then(|| child.stdout()),
stderr: matches!(self.stderr, Stdio::Pipe).then(|| child.stderr()), child })
}
pub fn status(&self) -> io::Result<i32> { Ok(self.spawn()?.wait()) }
}
/// A child handle. Drop does not kill the child. Drop stdin to send EOF.
pub struct Child {
child: host::Child,
pub stdin: Option<OutputStream>, pub stdout: Option<InputStream>, pub stderr: Option<InputStream>,
}
impl Child {
pub fn id(&self) -> u32 { self.child.pid() }
pub fn subscribe(&self) -> Pollable { self.child.subscribe() }
pub fn try_wait(&self) -> Option<i32> { self.child.status() }
pub fn wait(&self) -> i32 { self.child.wait() }
/// Stop every process in this child's group.
pub fn kill(&self) { self.child.kill(); }
}