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:
@@ -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>) {
|
||||
|
||||
Reference in New Issue
Block a user