host: wasip1 processes, wasi:http, identity, stdio options, services (T12)

- Core modules run on the host-side preview1 over the shared VFS (D41).
- wasi:http default handler in the linker; anonymous HTTPS git clone works.
  Sockets stay denied until P2.
- Each process carries the conversation (from Exec, inherited by children,
  shown in /proc/processes) and a host-assigned SAUSAGE_PID.
- Spawn requests choose pipe, inherit or null per stream.
- /config/services.yaml supervisor: live edits, restarts, crash limit 5/10 s,
  graceful stop (stdin EOF, then kill after 5 s) (D44).
- Compile caches keyed by content; PATH=/usr/local/bin:/bin.

world.rs also carries T13's optional read-only /bin mount (D43).
This commit is contained in:
2026-11-06 17:21:51 +00:00
parent afff3ccda5
commit 665b8a2a03
13 changed files with 1385 additions and 41 deletions
Generated
+50
View File
@@ -4583,6 +4583,7 @@ dependencies = [
"aws-lc-rs",
"log",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
@@ -4703,13 +4704,18 @@ dependencies = [
"deno_web",
"deno_webidl",
"rusqlite",
"rustls",
"sausage-vfs",
"serde",
"serde_json",
"serde_yaml_ng",
"sys_traits",
"tempfile",
"tokio",
"wasmtime",
"wasmtime-wasi",
"wasmtime-wasi-http",
"wat",
]
[[package]]
@@ -4898,6 +4904,19 @@ dependencies = [
"thiserror 2.0.21",
]
[[package]]
name = "serde_yaml_ng"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "serdect"
version = "0.2.0"
@@ -6158,6 +6177,12 @@ dependencies = [
"subtle",
]
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "untrusted"
version = "0.7.1"
@@ -6713,6 +6738,31 @@ dependencies = [
"winx",
]
[[package]]
name = "wasmtime-wasi-http"
version = "49.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9248a9ab044a9a4f414afdd14ffb38d7c58d4e2cc0f119ace9464d698f6f3bde"
dependencies = [
"async-trait",
"bytes",
"futures",
"http",
"http-body",
"http-body-util",
"hyper",
"pin-project-lite",
"rustls",
"tokio",
"tokio-rustls",
"tokio-util",
"tracing",
"wasmtime",
"wasmtime-wasi",
"wasmtime-wasi-io",
"webpki-roots",
]
[[package]]
name = "wasmtime-wasi-io"
version = "49.0.3"
+5
View File
@@ -29,6 +29,11 @@ rusqlite = { version = "0.38", features = ["bundled"] }
sys_traits = { version = "0.1", features = ["real", "libc"] }
wasmtime = "49"
wasmtime-wasi = "49"
wasmtime-wasi-http = "=49.0.3"
serde = { version = "1.0.229", features = ["derive"] }
serde_yaml_ng = "0.10.0"
rustls = "0.23"
[dev-dependencies]
tempfile = "3"
wat = "1"
+145 -38
View File
@@ -1,4 +1,4 @@
//! Runs components in independent stores over the world root. Owns process groups, stdio, and Exec.
//! Runs WASI programs in independent stores over the world root. Owns process groups, stdio, and Exec.
use crate::{
exec::{Command, Exec, Options, Stream},
world::World,
@@ -26,7 +26,7 @@ use wasmtime::{
};
use wasmtime_wasi::{
WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView,
cli::{AsyncStdinStream, AsyncStdoutStream},
cli::AsyncStdoutStream,
p2::{
DynInputStream, DynOutputStream,
bindings::CommandPre,
@@ -44,7 +44,17 @@ mod bindings {
},
});
}
use bindings::sausage::process::host::{Host, HostChild, Request};
use bindings::sausage::process::host::{Host, HostChild, Request, Stdio};
mod services;
mod stdio;
pub use services::supervise;
use stdio::Io;
#[derive(Clone)]
enum Compiled {
Component(CommandPre<State>),
Module(wasmtime::InstancePre<State>),
}
/// A finite snapshot of process metadata exposed through /proc/processes.
#[derive(Clone, Debug)]
@@ -53,6 +63,7 @@ pub struct ProcessInfo {
pub argv: Vec<String>,
pub cwd: String,
pub state: String,
pub conversation: Option<String>,
}
pub type Processes = Arc<Mutex<std::collections::BTreeMap<u32, ProcessInfo>>>;
@@ -66,7 +77,7 @@ struct Entry {
pub struct Runtime {
world: World,
engine: Engine,
cache: Mutex<HashMap<Vec<u8>, CommandPre<State>>>,
cache: Mutex<HashMap<Vec<u8>, Compiled>>,
entries: Mutex<HashMap<u32, Entry>>,
next: AtomicU32,
alive: Arc<AtomicBool>,
@@ -84,6 +95,19 @@ struct State {
pid: u32,
command: u32,
group: u32,
conversation: Option<String>,
stdio: Io,
preview1: Option<sausage_vfs::preview1::State>,
http: wasmtime_wasi_http::WasiHttpCtx,
}
impl wasmtime_wasi_http::WasiHttpView for State {
fn http(&mut self) -> wasmtime_wasi_http::WasiHttpCtxView<'_> {
wasmtime_wasi_http::WasiHttpCtxView {
ctx: &mut self.http,
table: &mut self.table,
hooks: Default::default(),
}
}
}
impl WasiView for State {
fn ctx(&mut self) -> WasiCtxView<'_> {
@@ -105,15 +129,17 @@ struct Started {
pid: u32,
group: u32,
code: watch::Receiver<Option<i32>>,
input: DuplexStream,
output: DuplexStream,
error: DuplexStream,
input: Option<DuplexStream>,
output: Option<DuplexStream>,
error: Option<DuplexStream>,
}
impl Runtime {
pub fn open(path: &Path) -> anyhow::Result<Arc<Self>> {
Self::new(World::open(path)?)
}
pub fn new(world: World) -> anyhow::Result<Arc<Self>> {
// Deno and WASI HTTP enable different providers. Select one before HTTPS creates a client.
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let mut config = wasmtime::Config::new();
config.epoch_interruption(true);
let mut cache = wasmtime::CacheConfig::new();
@@ -138,6 +164,10 @@ impl Runtime {
next: AtomicU32::new(1),
}))
}
/// Number of distinct program contents compiled by this runtime.
pub fn compiled_programs(&self) -> usize {
self.cache.lock().unwrap().len()
}
fn kill_command(&self, command: u32) {
for entry in self.entries.lock().unwrap().values() {
if entry.command == command {
@@ -170,6 +200,8 @@ impl Runtime {
parent: u32,
command: u32,
inherited_group: u32,
conversation: Option<String>,
inherited_io: Option<Io>,
) -> anyhow::Result<Started> {
anyhow::ensure!(!request.argv.is_empty(), "empty argv");
let cancelled = if parent == 0 {
@@ -182,6 +214,11 @@ impl Runtime {
.map(|entry| entry.cancelled.clone())
.ok_or_else(|| anyhow::anyhow!("parent exited"))?
};
if !request.env.iter().any(|(k, _)| k == "PATH") {
request
.env
.push(("PATH".into(), "/usr/local/bin:/bin".into()));
}
request.cwd = sausage_vfs::resolve("/", &request.cwd);
request.env.retain(|(name, _)| name != "PWD");
request.env.push(("PWD".into(), request.cwd.clone()));
@@ -205,19 +242,29 @@ impl Runtime {
if let Some(pre) = cache.get(&bytes) {
pre.clone()
} else {
let component = Component::new(&self.engine, &bytes)?;
let mut linker = Linker::new(&self.engine);
wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;
// The library linker includes exit-with-code. Only Wasmtime's CLI gates it.
adapter::add_to_linker(&mut linker, |s: &mut State| View {
adapter: &mut s.fs,
table: &mut s.table,
})?;
bindings::sausage::process::host::add_to_linker::<
_,
wasmtime::component::HasSelf<_>,
>(&mut linker, |s| s)?;
let pre = CommandPre::new(linker.instantiate_pre(&component)?)?;
let pre = if bytes.get(..8) == Some(b"\0asm\x01\0\0\0") {
let module = wasmtime::Module::new(&self.engine, &bytes)?;
let mut linker = wasmtime::Linker::new(&self.engine);
sausage_vfs::preview1::add_to_linker(&mut linker, |s: &mut State| {
s.preview1.as_mut().unwrap()
})?;
Compiled::Module(linker.instantiate_pre(&module)?)
} else {
let component = Component::new(&self.engine, &bytes)?;
let mut linker = Linker::new(&self.engine);
wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;
// The library linker includes exit-with-code. Only Wasmtime's CLI gates it.
wasmtime_wasi_http::p2::add_only_http_to_linker_async(&mut linker)?;
adapter::add_to_linker(&mut linker, |s: &mut State| View {
adapter: &mut s.fs,
table: &mut s.table,
})?;
bindings::sausage::process::host::add_to_linker::<
_,
wasmtime::component::HasSelf<_>,
>(&mut linker, |s| s)?;
Compiled::Component(CommandPre::new(linker.instantiate_pre(&component)?)?)
};
cache.insert(bytes, pre.clone());
pre
}
@@ -230,20 +277,45 @@ impl Runtime {
None if inherited_group != 0 => inherited_group,
None => pid,
};
request.env.retain(|(name, _)| name != "SAUSAGE_PID");
request.env.push(("SAUSAGE_PID".into(), pid.to_string()));
request.env.push(("PID".into(), pid.to_string()));
request.env.push(("PPID".into(), parent.to_string()));
let (input, child_input) = tokio::io::duplex(8192);
let (output, child_output) = tokio::io::duplex(8192);
let (error, child_error) = tokio::io::duplex(8192);
let (input, child_input) = stdio::input(request.stdin, inherited_io.as_ref());
let (output, child_output) =
stdio::output(request.stdout, inherited_io.as_ref().map(|io| &io.output));
let (error, child_error) =
stdio::output(request.stderr, inherited_io.as_ref().map(|io| &io.error));
let stdio = Io {
input: child_input,
output: child_output,
error: child_error,
};
let preview1 = if matches!(pre, Compiled::Module(_)) {
Some(sausage_vfs::preview1::State::new(
self.world.root.clone(),
&request.cwd,
request.argv.clone(),
request.env.clone(),
stdio.input(),
Box::new(stdio.output.clone()),
Box::new(stdio.error.clone()),
))
} else {
None
};
let mut mounts = Mounts::default();
mounts.mount("/", self.world.root.clone(), false);
let mut wasi = WasiCtxBuilder::new();
wasi.args(&request.argv)
.envs(&request.env)
.initial_cwd(&request.cwd)
.stdin(AsyncStdinStream::new(child_input))
.stdout(AsyncStdoutStream::new(8192, child_output))
.stderr(AsyncStdoutStream::new(8192, child_error));
.stdin(stdio.input.clone())
.stdout(AsyncStdoutStream::new(8192, stdio.output.clone()))
.stderr(AsyncStdoutStream::new(8192, stdio.error.clone()))
.allow_tcp(false)
.allow_udp(false)
.allow_ip_name_lookup(false);
let mut store = Store::new(
&self.engine,
State {
@@ -254,6 +326,10 @@ impl Runtime {
pid,
command,
group,
conversation: conversation.clone(),
stdio,
preview1,
http: wasmtime_wasi_http::WasiHttpCtx::new(),
},
);
store.epoch_deadline_async_yield_and_update(1);
@@ -264,6 +340,7 @@ impl Runtime {
argv: request.argv,
cwd: request.cwd,
state: "running".into(),
conversation,
},
);
let (tx, code) = watch::channel(None);
@@ -281,8 +358,21 @@ impl Runtime {
return;
}
let result = async {
let command = pre.instantiate_async(&mut store).await?;
match command.wasi_cli_run().call_run(&mut store).await {
let result = match pre {
Compiled::Component(pre) => {
let command = pre.instantiate_async(&mut store).await?;
command.wasi_cli_run().call_run(&mut store).await
}
Compiled::Module(pre) => {
let module = pre.instantiate_async(&mut store).await?;
module
.get_typed_func::<(), ()>(&mut store, "_start")?
.call_async(&mut store, ())
.await
.map(|()| Ok(()))
}
};
match result {
Ok(Ok(())) => Ok(0),
Ok(Err(())) => Ok(1),
Err(e) => match e.downcast_ref::<wasmtime_wasi::I32Exit>() {
@@ -348,7 +438,14 @@ impl Host for State {
) -> wasmtime::Result<Result<Resource<Child>, String>> {
let child = match self
.runtime
.start(request, self.pid, self.command, self.group)
.start(
request,
self.pid,
self.command,
self.group,
self.conversation.clone(),
Some(self.stdio.clone()),
)
.await
{
Ok(child) => child,
@@ -358,9 +455,15 @@ impl Host for State {
pid: child.pid,
group: child.group,
code: child.code,
input: Some(Box::new(AsyncWriteStream::new(8192, child.input))),
output: Some(Box::new(AsyncReadStream::new(child.output))),
error: Some(Box::new(AsyncReadStream::new(child.error))),
input: child
.input
.map(|s| Box::new(AsyncWriteStream::new(8192, s)) as DynOutputStream),
output: child
.output
.map(|s| Box::new(AsyncReadStream::new(s)) as DynInputStream),
error: child
.error
.map(|s| Box::new(AsyncReadStream::new(s)) as DynInputStream),
})?))
}
async fn kill(&mut self, pid: u32) -> wasmtime::Result<Result<(), String>> {
@@ -463,10 +566,7 @@ impl Exec for Arc<Runtime> {
],
Command::Argv(argv) => argv,
};
let mut env = options.env;
if !env.iter().any(|(k, _)| k == "PATH") {
env.push(("PATH".into(), "/bin".into()));
}
let env = options.env;
let start = self.start(
Request {
program: argv.first().cloned().unwrap_or_default(),
@@ -474,10 +574,15 @@ impl Exec for Arc<Runtime> {
env,
cwd: options.cwd,
group: None,
stdin: Stdio::Pipe,
stdout: Stdio::Pipe,
stderr: Stdio::Pipe,
},
0,
0,
0,
options.conversation,
None,
);
let mut child = if let Some(deadline) = deadline {
tokio::time::timeout_at(deadline, start)
@@ -491,13 +596,15 @@ impl Exec for Arc<Runtime> {
group: child.pid,
};
drop(child.input);
let mut stdout = child.output.take().unwrap();
let mut stderr = child.error.take().unwrap();
let run = async {
let (mut out_open, mut err_open) = (true, true);
let (mut out, mut err) = ([0; 8192], [0; 8192]);
while out_open || err_open {
tokio::select! {
n = child.output.read(&mut out), if out_open => { let n = n?; out_open = n != 0; if n != 0 { output(Stream::Stdout, &out[..n]); } }
n = child.error.read(&mut err), if err_open => { let n = n?; err_open = n != 0; if n != 0 { output(Stream::Stderr, &err[..n]); } }
n = stdout.read(&mut out), if out_open => { let n = n?; out_open = n != 0; if n != 0 { output(Stream::Stdout, &out[..n]); } }
n = stderr.read(&mut err), if err_open => { let n = n?; err_open = n != 0; if n != 0 { output(Stream::Stderr, &err[..n]); } }
}
}
loop {
+169
View File
@@ -0,0 +1,169 @@
//! Supervises the world's services list, live edits, crash limits, and graceful stops.
use super::{GroupGuard, Request, Runtime, Started, Stdio};
use serde::Deserialize;
use std::{
collections::{BTreeMap, VecDeque},
sync::Arc,
time::Duration,
};
use tokio::time::Instant;
#[derive(Clone, Deserialize, PartialEq, Eq)]
struct Spec {
name: String,
argv: Vec<String>,
#[serde(default = "root")]
cwd: String,
#[serde(default)]
env: BTreeMap<String, String>,
}
fn root() -> String {
"/".into()
}
struct Live {
child: Started,
_guard: GroupGuard,
}
struct Service {
spec: Spec,
live: Option<Live>,
crashes: VecDeque<Instant>,
stopped: bool,
}
impl Service {
async fn start(&mut self, runtime: &Arc<Runtime>) {
match runtime
.start(
Request {
program: self.spec.argv.first().cloned().unwrap_or_default(),
argv: self.spec.argv.clone(),
cwd: self.spec.cwd.clone(),
env: self
.spec
.env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
group: None,
stdin: Stdio::Pipe,
stdout: Stdio::Null,
stderr: Stdio::Null,
},
0,
0,
0,
None,
None,
)
.await
{
Ok(child) => {
let guard = GroupGuard {
runtime: runtime.clone(),
group: child.pid,
};
self.live = Some(Live {
child,
_guard: guard,
});
}
Err(_) => self.crashed(),
}
}
fn crashed(&mut self) {
let now = Instant::now();
while self
.crashes
.front()
.is_some_and(|t| now.duration_since(*t) >= Duration::from_secs(10))
{
self.crashes.pop_front();
}
self.crashes.push_back(now);
self.stopped = self.crashes.len() > 5;
}
async fn stop(&mut self) {
if let Some(mut live) = self.live.take() {
drop(live.child.input.take());
let _ = tokio::time::timeout(Duration::from_secs(5), async {
loop {
if live.child.code.borrow().is_some() {
break;
}
if live.child.code.changed().await.is_err() {
break;
}
}
})
.await;
// The guard stops remaining descendants, or the process after its grace period.
}
}
}
/// Start the supervisor. Aborting its task stops its service processes.
/// The YAML file contains name, argv, optional cwd, and an optional map of environment variables.
pub fn supervise(runtime: Arc<Runtime>) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let mut services: BTreeMap<String, Service> = BTreeMap::new();
let mut previous = Vec::new();
let mut interval = tokio::time::interval(Duration::from_millis(100));
loop {
interval.tick().await;
let content = match runtime
.world
.root
.open("/config/services.yaml", Default::default())
.await
{
Ok(file) => match file.stat().await {
Ok(stat) => file.read(0, stat.size as usize).await.ok(),
Err(_) => None,
},
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Some(b"[]".to_vec()),
Err(_) => None,
};
if let Some(content) = content.filter(|c| *c != previous)
&& let Ok(specs) = serde_yaml_ng::from_slice::<Vec<Spec>>(&content)
{
let specs: BTreeMap<_, _> =
specs.into_iter().map(|s| (s.name.clone(), s)).collect();
let removed: Vec<_> = services
.keys()
.filter(|name| !specs.contains_key(*name))
.cloned()
.collect();
for name in removed {
services.remove(&name).unwrap().stop().await;
}
for (name, spec) in specs {
let service = services.entry(name).or_insert_with(|| Service {
spec: spec.clone(),
live: None,
crashes: VecDeque::new(),
stopped: false,
});
if service.spec != spec {
service.stop().await;
}
service.spec = spec;
service.crashes.clear();
service.stopped = false;
}
previous = content;
}
for service in services.values_mut() {
if service
.live
.as_ref()
.is_some_and(|live| live.child.code.borrow().is_some())
{
service.live = None;
service.crashed();
}
if service.live.is_none() && !service.stopped {
service.start(&runtime).await;
}
}
}
})
}
+73
View File
@@ -0,0 +1,73 @@
//! Shares process stdio across stores without a parent-side IO pump.
use super::bindings::sausage::process::host::Stdio;
use std::{
pin::Pin,
sync::{Arc, Mutex},
task::{Context, Poll},
};
use tokio::io::AsyncWrite;
use wasmtime_wasi::cli::{AsyncStdinStream, StdinStream};
#[derive(Clone)]
pub(super) struct Io {
pub input: Arc<AsyncStdinStream>,
pub output: Writer,
pub error: Writer,
}
impl Io {
pub fn input(&self) -> sausage_vfs::preview1::Input {
Box::new(Pin::from(self.input.async_stream()))
}
}
#[derive(Clone)]
pub(super) struct Writer(Arc<Mutex<Box<dyn AsyncWrite + Send + Sync + Unpin>>>);
impl Writer {
pub fn new(writer: impl AsyncWrite + Send + Sync + Unpin + 'static) -> Self {
Self(Arc::new(Mutex::new(Box::new(writer))))
}
}
impl AsyncWrite for Writer {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
bytes: &[u8],
) -> Poll<std::io::Result<usize>> {
Pin::new(&mut **self.0.lock().unwrap()).poll_write(cx, bytes)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Pin::new(&mut **self.0.lock().unwrap()).poll_flush(cx)
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
// Closing one inherited handle must not close the other handles.
self.poll_flush(cx)
}
}
pub(super) fn input(
mode: Stdio,
parent: Option<&Io>,
) -> (Option<tokio::io::DuplexStream>, Arc<AsyncStdinStream>) {
match mode {
Stdio::Pipe => {
let (host, guest) = tokio::io::duplex(8192);
(Some(host), Arc::new(AsyncStdinStream::new(guest)))
}
Stdio::Inherit => (
None,
parent.expect("inherit requires a parent").input.clone(),
),
Stdio::Null => (None, Arc::new(AsyncStdinStream::new(tokio::io::empty()))),
}
}
pub(super) fn output(
mode: Stdio,
parent: Option<&Writer>,
) -> (Option<tokio::io::DuplexStream>, Writer) {
match mode {
Stdio::Pipe => {
let (host, guest) = tokio::io::duplex(8192);
(Some(host), Writer::new(guest))
}
Stdio::Inherit => (None, parent.expect("inherit requires a parent").clone()),
Stdio::Null => (None, Writer::new(tokio::io::sink())),
}
}
+16 -3
View File
@@ -22,9 +22,14 @@ pub struct World {
impl World {
/// Open (and create if missing) the world directory `dir`, including `/proc` and `/run` mounts.
pub fn open(dir: &Path) -> anyhow::Result<Self> {
Self::open_with_userland(dir, None)
}
/// Mount an optional host bundle read-only at `/bin` (D43).
pub fn open_with_userland(dir: &Path, userland: Option<&Path>) -> anyhow::Result<Self> {
let root_dir = dir.join("world");
let host_dir = dir.join("host");
std::fs::create_dir_all(&root_dir)?;
std::fs::create_dir_all(root_dir.join("usr/local/bin"))?;
std::fs::create_dir_all(&host_dir)?;
let mut mounts = Mounts::default();
mounts.mount(
@@ -37,18 +42,26 @@ impl World {
let snapshot = processes.clone();
let mut proc = Generated::default();
proc.insert("/processes", move || {
let mut text = String::from("PID\tPPID\tSTATE\tCWD\tARGV\n");
let mut text = String::from("PID\tPPID\tSTATE\tCWD\tCONVERSATION\tARGV\n");
for (pid, info) in snapshot.lock().unwrap().iter() {
text.push_str(&format!(
"{pid}\t{}\t{}\t{}\t{}\n",
"{pid}\t{}\t{}\t{}\t{}\t{}\n",
info.parent,
info.state,
info.cwd,
info.conversation.as_deref().unwrap_or("-"),
info.argv.join(" ")
));
}
Ok(text.into_bytes())
});
if let Some(bundle) = userland {
mounts.mount(
"/bin",
Arc::new(Host::new(bundle, Process::default())?),
true,
);
}
mounts.mount("/proc", Arc::new(proc), true);
mounts.mount("/run", Arc::new(Memory::default()), false);
Ok(World {
+393
View File
@@ -9,6 +9,363 @@ use std::{
sync::{Arc, Once},
time::Duration,
};
mod features {
//! Acceptance coverage for preview1, identity, inherited IO, services, HTTP, and bundle aliases.
use super::*;
use std::{collections::BTreeMap, time::Instant};
async fn poll(mut condition: impl FnMut() -> bool) {
tokio::time::timeout(Duration::from_secs(10), async {
while !condition() {
tokio::task::yield_now().await;
}
})
.await
.unwrap();
}
fn lines(path: &Path) -> Vec<String> {
std::fs::read_to_string(path)
.unwrap_or_default()
.lines()
.map(str::to_owned)
.collect()
}
async fn prepare(runtime: &Arc<Runtime>, argv: Vec<String>) {
assert_eq!(
runtime
.exec(
Command::Argv(argv),
Options {
cwd: "/".into(),
..Default::default()
},
&mut |_, _| {}
)
.await
.unwrap(),
0
);
}
#[tokio::test(flavor = "multi_thread")]
async fn core_module_exit_and_cancellation() {
let _serial = SERIAL.lock().await;
let (temp, runtime, processes) = world();
let bin = temp.path().join("world/bin");
std::fs::write(bin.join("exit23"), wat::parse_str(r#"(module (import "wasi_snapshot_preview1" "proc_exit" (func (param i32))) (memory (export "memory") 1) (func (export "_start") i32.const 23 call 0))"#).unwrap()).unwrap();
assert_eq!(
run(&runtime, Command::Argv(vec!["/bin/exit23".into()]), "/")
.await
.0,
23
);
std::fs::write(
bin.join("spin"),
wat::parse_str(
r#"(module (memory (export "memory") 1) (func (export "_start") (loop br 0)))"#,
)
.unwrap(),
)
.unwrap();
let result = runtime
.exec(
Command::Argv(vec!["/bin/spin".into()]),
Options {
timeout: Some(Duration::from_millis(100)),
..Default::default()
},
&mut |_, _| {},
)
.await;
assert!(result.unwrap_err().to_string().contains("timed out"));
empty(&processes).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn preview1_pipeline_and_esbuild() {
let _serial = SERIAL.lock().await;
let (temp, runtime, _) = world();
warm(&runtime).await;
let root = temp.path().join("world");
std::fs::copy("/tmp/wasi-userland-lab/qjs-wasi.wasm", root.join("bin/qjs")).unwrap();
std::fs::copy("/tmp/s6-esbuild/esbuild.wasm", root.join("bin/esbuild")).unwrap();
prepare(&runtime, vec!["/bin/qjs".into(), "--help".into()]).await;
let direct = run(
&runtime,
Command::Argv(vec![
"/bin/qjs".into(),
"--std".into(),
"-e".into(),
"console.log(42)".into(),
]),
"/",
)
.await;
assert_eq!(direct.0, 0, "{}", String::from_utf8_lossy(&direct.2));
assert_eq!(direct.1, b"42\n");
let (code, out, err) = run(
&runtime,
Command::Shell(
"echo 'console.log(6*7)' | qjs --std -e 'std.evalScript(std.in.readAsString())' | cat"
.into(),
),
"/",
)
.await;
assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err));
assert_eq!(out, b"42\n");
assert_eq!(
run(
&runtime,
Command::Shell("qjs --std -e 'std.exit(7)'".into()),
"/"
)
.await
.0,
7
);
for name in ["app.tsx", "package.json", "package-lock.json"] {
std::fs::copy(repo().join("spikes/s6-esbuild").join(name), root.join(name)).unwrap();
}
assert!(
std::process::Command::new("cp")
.args(["-r", "/tmp/s6-esbuild/app/node_modules"])
.arg(&root)
.status()
.unwrap()
.success()
);
prepare(&runtime, vec!["/bin/esbuild".into(), "--version".into()]).await;
let flags = [
"app.tsx",
"--bundle",
"--minify",
"--format=iife",
"--platform=browser",
"--jsx=automatic",
"--jsx-import-source=preact",
"--legal-comments=none",
"--log-level=error",
];
assert!(
std::process::Command::new("/tmp/s6-esbuild/esbuild-native")
.current_dir(&root)
.args(flags)
.arg("--outfile=native.js")
.status()
.unwrap()
.success()
);
let script = format!("esbuild {} --outfile=module.js", flags.join(" "));
let (code, _, err) = run(&runtime, Command::Shell(script), "/").await;
assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err));
assert_eq!(
std::fs::read(root.join("native.js")).unwrap(),
std::fs::read(root.join("module.js")).unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn inherited_stdio_identity_and_conversation() {
let _serial = SERIAL.lock().await;
let (_temp, runtime, processes) = world();
warm(&runtime).await;
let (code, out, err) = run(
&runtime,
Command::Shell(
"echo inherited | /bin/launcher inherit /bin/launcher read | cat".into(),
),
"/",
)
.await;
assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err));
assert_eq!(out, b"inherited\n");
let (code, out, err) = run(
&runtime,
Command::Shell("/bin/launcher inherit /bin/launcher emit".into()),
"/",
)
.await;
assert_eq!(code, 0);
assert_eq!(out, b"out\n");
assert_eq!(err, b"err\n");
let (code, out, err) = run(
&runtime,
Command::Shell("echo unused | /bin/launcher null /bin/launcher check-null".into()),
"/",
)
.await;
assert_eq!(code, 0);
assert!(out.is_empty());
assert!(err.is_empty());
assert_eq!(
run(
&runtime,
Command::Argv(vec!["/bin/launcher".into(), "path".into()]),
"/"
)
.await
.1,
b"/usr/local/bin:/bin\n"
);
let mut out = Vec::new();
let code = runtime
.exec(
Command::Argv(vec![
"/bin/launcher".into(),
"identity".into(),
"/bin/launcher".into(),
]),
Options {
cwd: "/".into(),
env: vec![("SAUSAGE_PID".into(), "spoof".into())],
..Default::default()
},
&mut |_, bytes| out.extend_from_slice(bytes),
)
.await
.unwrap();
assert_eq!(code, 0);
let text = String::from_utf8(out).unwrap();
let ids: BTreeMap<_, _> = text
.lines()
.map(|line| {
let (name, value) = line.split_once('=').unwrap();
(name, value.parse::<u32>().unwrap())
})
.collect();
assert_eq!(ids["pid"], ids["child"]);
assert_ne!(ids["pid"], ids["parent"]);
let exec = runtime.clone();
let task = tokio::spawn(async move {
exec.exec(
Command::Shell("sleep 60 | cat".into()),
Options {
conversation: Some("c1".into()),
..Default::default()
},
&mut |_, _| {},
)
.await
});
poll(|| processes.lock().unwrap().len() >= 3).await;
assert!(
processes
.lock()
.unwrap()
.values()
.all(|p| p.conversation.as_deref() == Some("c1"))
);
let (_, out, _) = run(&runtime, Command::Shell("cat /proc/processes".into()), "/").await;
assert!(String::from_utf8(out).unwrap().matches("\tc1\t").count() >= 3);
task.abort();
let _ = task.await;
empty(&processes).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn relative_aliases_share_compilation() {
let _serial = SERIAL.lock().await;
let (temp, runtime, _) = world();
let bin = temp.path().join("world/bin");
for name in ["true", "echo"] {
std::os::unix::fs::symlink("cat", bin.join(name)).unwrap();
}
assert_eq!(
run(&runtime, Command::Argv(vec!["/bin/true".into()]), "/")
.await
.0,
0
);
assert_eq!(runtime.compiled_programs(), 1);
let (code, out, _) = run(
&runtime,
Command::Argv(vec!["/bin/echo".into(), "shared".into()]),
"/",
)
.await;
assert_eq!(code, 0);
assert_eq!(out, b"shared\n");
assert_eq!(runtime.compiled_programs(), 1);
}
#[tokio::test(flavor = "multi_thread")]
async fn services_restart_edit_limit_and_eof() {
let _serial = SERIAL.lock().await;
let (temp, runtime, processes) = world();
prepare(&runtime, vec!["/bin/launcher".into()]).await;
let root = temp.path().join("world");
std::fs::create_dir_all(root.join("config")).unwrap();
let spec = |name: &str, mode: &str, log: &str| serde_json::json!({"name": name, "argv": ["/bin/launcher", mode, log]});
let write = |specs: Vec<serde_json::Value>| {
std::fs::write(
root.join("config/services.yaml"),
serde_yaml_ng::to_string(&specs).unwrap(),
)
.unwrap()
};
write(vec![
spec("a", "server", "/a.log"),
spec("crash", "crash", "/crash.log"),
]);
let supervisor = sausage::process::supervise(runtime.clone());
poll(|| {
lines(&root.join("a.log")) == ["start"] && lines(&root.join("crash.log")).len() == 6
})
.await;
prepare(&runtime, vec!["/bin/launcher".into()]).await;
assert_eq!(lines(&root.join("crash.log")).len(), 6);
assert!(
processes
.lock()
.unwrap()
.values()
.all(|p| p.conversation.is_none())
);
write(vec![spec("b", "server", "/b.log")]);
poll(|| {
lines(&root.join("a.log")) == ["start", "stop"]
&& lines(&root.join("b.log")) == ["start"]
})
.await;
let mut changed = spec("b", "server", "/b.log");
changed["env"] = serde_json::json!({"REV": "new"});
write(vec![changed]);
poll(|| lines(&root.join("b.log")) == ["start", "stop", "start"]).await;
write(vec![]);
poll(|| lines(&root.join("b.log")) == ["start", "stop", "start", "stop"]).await;
write(vec![spec("linger", "linger", "/linger.log")]);
poll(|| lines(&root.join("linger.log")) == ["start"]).await;
let stop = Instant::now();
write(vec![]);
poll(|| processes.lock().unwrap().is_empty()).await;
assert!(stop.elapsed() >= Duration::from_secs(5));
supervisor.abort();
let _ = supervisor.await;
empty(&processes).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn git_local_and_anonymous_https_clone() {
let _serial = SERIAL.lock().await;
let (temp, runtime, _) = world();
warm(&runtime).await;
prepare(&runtime, vec!["/bin/git".into(), "--help".into()]).await;
let mut out = Vec::new();
let mut err = Vec::new();
let code = runtime.exec(Command::Shell("git init /repo && cd /repo && echo hello > file && git add file && git commit -m 'process test' && git log --oneline".into()),
Options { cwd: "/".into(), env: vec![("GIT_AUTHOR_NAME".into(), "Test".into()), ("GIT_AUTHOR_EMAIL".into(), "test@example.com".into()), ("GIT_COMMITTER_NAME".into(), "Test".into()), ("GIT_COMMITTER_EMAIL".into(), "test@example.com".into())], ..Default::default() },
&mut |stream, bytes| match stream { Stream::Stdout => out.extend_from_slice(bytes), Stream::Stderr => err.extend_from_slice(bytes) }).await.unwrap();
assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err));
assert!(String::from_utf8(out).unwrap().contains("process test"));
let (code, _, err) = run(
&runtime,
Command::Shell("git clone https://github.com/octocat/Hello-World.git /clone".into()),
"/",
)
.await;
assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err));
assert!(
std::fs::read_to_string(temp.path().join("world/clone/README"))
.unwrap()
.contains("Hello World")
);
}
}
static BUILD: Once = Once::new();
// Share the persistent compile cache without concurrent cold compilation of identical components.
static SERIAL: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
@@ -57,6 +414,32 @@ fn build() {
&["nu", "--no-config-file", "userland/brush/build.nu"],
&root,
);
run(
&[
"cargo",
"build",
"--release",
"--target",
"wasm32-wasip2",
"--manifest-path",
"brush/probe/Cargo.toml",
"--locked",
],
&root.join("userland"),
);
run(
&[
"cargo",
"build",
"--release",
"-p",
"git",
"--target-dir",
"../target/process-userland",
"--locked",
],
&root.join("userland"),
);
});
}
fn fixture(root: &Path) {
@@ -98,6 +481,16 @@ fn world() -> (tempfile::TempDir, Arc<Runtime>, sausage::process::Processes) {
bin.join("sh"),
)
.unwrap();
std::fs::copy(
repo().join("userland/brush/probe/target/wasm32-wasip2/release/process-probe.wasm"),
bin.join("launcher"),
)
.unwrap();
std::fs::copy(
repo().join("target/process-userland/wasm32-wasip2/release/git.wasm"),
bin.join("git"),
)
.unwrap();
(temp, Runtime::new(world).unwrap(), processes)
}
async fn run(runtime: &Arc<Runtime>, command: Command, cwd: &str) -> (i32, Vec<u8>, Vec<u8>) {
+4
View File
@@ -3,6 +3,7 @@ package sausage:process@0.1.0;
/// Programs share one world. These resources provide lifecycle and bounded stdio transport.
interface host {
use wasi:io/streams@0.2.12.{input-stream, output-stream};
enum stdio { pipe, inherit, null }
record request {
program: string,
argv: list<string>,
@@ -10,6 +11,9 @@ interface host {
cwd: string,
/// None inherits the parent's group. Zero creates a group. A nonzero ID joins a group.
group: option<u32>,
stdin: stdio,
stdout: stdio,
stderr: stdio,
}
resource child {
pid: func() -> u32;
+444
View File
@@ -0,0 +1,444 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "bitflags"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "equivalent"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "futures"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-executor"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
[[package]]
name = "futures-macro"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "futures-sink"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"slab",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
"serde",
"serde_core",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "log"
version = "0.4.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn 2.0.119",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "process-probe"
version = "0.1.0"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "serde_json"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "wasm-encoder"
version = "0.239.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.239.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.239.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]]
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
dependencies = [
"bitflags",
"futures",
"once_cell",
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn 2.0.119",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn 2.0.119",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.239.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.239.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+7
View File
@@ -0,0 +1,7 @@
[package]
name = "process-probe"
version = "0.1.0"
edition = "2024"
[workspace]
[dependencies]
wit-bindgen = "0.46"
+74
View File
@@ -0,0 +1,74 @@
//! Exercises inherited stdio and the service lifecycle through ordinary guest APIs.
use std::io::{Read, Write};
mod bindings {
wit_bindgen::generate!({ path: "../../../host/wit", world: "program", generate_all });
}
use bindings::sausage::process::host::{self, Request, Stdio};
fn mark(path: &str, text: &str) {
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.unwrap();
writeln!(file, "{text}").unwrap();
}
fn main() {
let args: Vec<_> = std::env::args().collect();
match args.get(1).map(String::as_str) {
Some(mode @ ("inherit" | "identity" | "null")) => {
let io = if mode == "null" {
Stdio::Null
} else {
Stdio::Inherit
};
let child = host::spawn(&Request {
program: args[2].clone(),
argv: args[2..].to_vec(),
cwd: std::env::current_dir().unwrap().to_str().unwrap().into(),
env: std::env::vars().collect(),
group: None,
stdin: io,
stdout: io,
stderr: io,
})
.unwrap();
if mode == "identity" {
println!("parent={}", std::env::var("SAUSAGE_PID").unwrap());
println!("child={}", child.pid());
}
std::process::exit(child.wait());
}
Some("server") => {
mark(&args[2], "start");
let mut input = Vec::new();
std::io::stdin().read_to_end(&mut input).unwrap();
mark(&args[2], "stop");
}
Some("linger") => {
mark(&args[2], "start");
std::thread::sleep(std::time::Duration::from_secs(60));
}
Some("crash") => {
mark(&args[2], "crash");
std::process::exit(1);
}
Some("emit") => {
println!("out");
eprintln!("err");
}
Some("check-null") => {
let mut input = Vec::new();
std::io::stdin().read_to_end(&mut input).unwrap();
assert!(input.is_empty());
println!("out");
eprintln!("err");
}
Some("path") => println!("{}", std::env::var("PATH").unwrap()),
Some("read") => {
let mut input = Vec::new();
std::io::stdin().read_to_end(&mut input).unwrap();
std::io::stdout().write_all(&input).unwrap();
}
_ => println!("pid={}", std::env::var("SAUSAGE_PID").unwrap()),
}
}
+3
View File
@@ -38,6 +38,9 @@ pub(crate) fn spawn(mut request: LaunchRequest) -> std::io::Result<ChildProcess>
crate::processes::ProcessGroup::Inherit => None,
_ => Some(0),
},
stdin: host::Stdio::Pipe,
stdout: host::Stdio::Pipe,
stderr: host::Stdio::Pipe,
})
.map_err(std::io::Error::other)?;
let pid = child.pid() as i32;
+2
View File
@@ -0,0 +1,2 @@
ps
kill