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:
Generated
+50
@@ -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"
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Generated
+444
@@ -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"
|
||||
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "process-probe"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
[workspace]
|
||||
[dependencies]
|
||||
wit-bindgen = "0.46"
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ps
|
||||
kill
|
||||
Reference in New Issue
Block a user