From b2ac279dc5dfc4e9f6dc67b1b4f9d657dd424b16 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Wed, 11 Mar 2026 15:10:11 +0100 Subject: [PATCH] feat(runtime): add --preopen and --ipc-socket CLI arguments weft-runtime now parses optional flags after : --preopen HOST::GUEST pre-opens a host directory at GUEST path in the WASI filesystem (HOST::GUEST or HOST for same path) --ipc-socket PATH sets WEFT_IPC_SOCKET env var inside the component wasmtime-runtime path applies preopened dirs via cap_std and WasiCtxBuilder, and injects WEFT_IPC_SOCKET when --ipc-socket is present. Stub path ignores both flags. weft-appd: SessionRegistry gains ipc_socket field (set to the appd Unix socket path in run()), extracted alongside compositor_tx in dispatch(), and forwarded to supervise() as ipc_socket_path. supervise() passes --ipc-socket to the spawned runtime when present. cap-std added as optional dep under wasmtime-runtime feature. --- crates/weft-appd/src/main.rs | 10 ++++- crates/weft-appd/src/runtime.rs | 15 +++++--- crates/weft-runtime/Cargo.toml | 3 +- crates/weft-runtime/src/main.rs | 68 ++++++++++++++++++++++++++++----- 4 files changed, 80 insertions(+), 16 deletions(-) diff --git a/crates/weft-appd/src/main.rs b/crates/weft-appd/src/main.rs index b4aa308..30262fa 100644 --- a/crates/weft-appd/src/main.rs +++ b/crates/weft-appd/src/main.rs @@ -25,6 +25,7 @@ struct SessionRegistry { broadcast: tokio::sync::broadcast::Sender, abort_senders: std::collections::HashMap>, compositor_tx: Option, + ipc_socket: Option, } impl Default for SessionRegistry { @@ -36,6 +37,7 @@ impl Default for SessionRegistry { broadcast, abort_senders: std::collections::HashMap::new(), compositor_tx: None, + ipc_socket: None, } } } @@ -128,6 +130,7 @@ async fn run() -> anyhow::Result<()> { tracing::info!(path = %socket_path.display(), "IPC socket listening"); let registry: Registry = Arc::new(Mutex::new(SessionRegistry::default())); + registry.lock().await.ipc_socket = Some(socket_path.clone()); if let Some(path) = compositor_client::socket_path() { let tx = compositor_client::spawn(path); @@ -240,11 +243,13 @@ pub(crate) async fn dispatch(req: Request, registry: &Registry) -> Response { tracing::info!(session_id, %app_id, "launched"); let abort_rx = registry.lock().await.register_abort(session_id); let compositor_tx = registry.lock().await.compositor_tx.clone(); + let ipc_socket = registry.lock().await.ipc_socket.clone(); let reg = Arc::clone(registry); let aid = app_id.clone(); tokio::spawn(async move { if let Err(e) = - runtime::supervise(session_id, &aid, reg, abort_rx, compositor_tx).await + runtime::supervise(session_id, &aid, reg, abort_rx, compositor_tx, ipc_socket) + .await { tracing::warn!(session_id, error = %e, "runtime supervisor error"); } @@ -664,6 +669,7 @@ mod tests { Arc::clone(®istry), abort_rx, None, + None, ) .await .unwrap(); @@ -721,6 +727,7 @@ mod tests { Arc::clone(®istry), abort_rx, None, + None, ) .await .unwrap(); @@ -767,6 +774,7 @@ mod tests { Arc::clone(®istry), abort_rx, None, + None, ) .await .unwrap(); diff --git a/crates/weft-appd/src/runtime.rs b/crates/weft-appd/src/runtime.rs index 01375f0..f95e8e4 100644 --- a/crates/weft-appd/src/runtime.rs +++ b/crates/weft-appd/src/runtime.rs @@ -15,6 +15,7 @@ pub(crate) async fn supervise( registry: Registry, abort_rx: tokio::sync::oneshot::Receiver<()>, compositor_tx: Option, + ipc_socket_path: Option, ) -> anyhow::Result<()> { let mut abort_rx = abort_rx; let bin = match std::env::var("WEFT_RUNTIME_BIN") { @@ -25,14 +26,18 @@ pub(crate) async fn supervise( } }; - let mut child = match tokio::process::Command::new(&bin) - .arg(app_id) + let mut cmd = tokio::process::Command::new(&bin); + cmd.arg(app_id) .arg(session_id.to_string()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) - .stdin(std::process::Stdio::null()) - .spawn() - { + .stdin(std::process::Stdio::null()); + + if let Some(ref sock) = ipc_socket_path { + cmd.arg("--ipc-socket").arg(sock); + } + + let mut child = match cmd.spawn() { Ok(c) => c, Err(e) => { tracing::warn!(session_id, %app_id, error = %e, "failed to spawn runtime; marking session stopped"); diff --git a/crates/weft-runtime/Cargo.toml b/crates/weft-runtime/Cargo.toml index e636340..572eeb2 100644 --- a/crates/weft-runtime/Cargo.toml +++ b/crates/weft-runtime/Cargo.toml @@ -10,7 +10,7 @@ path = "src/main.rs" [features] default = [] -wasmtime-runtime = ["dep:wasmtime", "dep:wasmtime-wasi"] +wasmtime-runtime = ["dep:wasmtime", "dep:wasmtime-wasi", "dep:cap-std"] [dependencies] anyhow = "1.0" @@ -18,3 +18,4 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } wasmtime = { version = "30", optional = true } wasmtime-wasi = { version = "30", optional = true } +cap-std = { version = "3", optional = true } diff --git a/crates/weft-runtime/src/main.rs b/crates/weft-runtime/src/main.rs index 5cd9a92..111f668 100644 --- a/crates/weft-runtime/src/main.rs +++ b/crates/weft-runtime/src/main.rs @@ -12,13 +12,44 @@ fn main() -> anyhow::Result<()> { let args: Vec = std::env::args().collect(); if args.len() < 3 { - anyhow::bail!("usage: weft-runtime "); + anyhow::bail!( + "usage: weft-runtime \ + [--preopen HOST::GUEST]... [--ipc-socket PATH]" + ); } let app_id = &args[1]; let session_id: u64 = args[2] .parse() .with_context(|| format!("invalid session_id: {}", args[2]))?; + let mut preopen: Vec<(String, String)> = Vec::new(); + let mut ipc_socket: Option = None; + + let mut i = 3usize; + while i < args.len() { + match args[i].as_str() { + "--preopen" => { + i += 1; + let spec = args.get(i).context("--preopen requires an argument")?; + if let Some((host, guest)) = spec.split_once("::") { + preopen.push((host.to_string(), guest.to_string())); + } else { + preopen.push((spec.clone(), spec.clone())); + } + } + "--ipc-socket" => { + i += 1; + ipc_socket = Some( + args.get(i) + .context("--ipc-socket requires an argument")? + .clone(), + ); + } + other => anyhow::bail!("unexpected argument: {other}"), + } + i += 1; + } + tracing::info!(session_id, %app_id, "weft-runtime starting"); let pkg_dir = resolve_package(app_id)?; @@ -30,7 +61,7 @@ fn main() -> anyhow::Result<()> { } tracing::info!(session_id, %app_id, wasm = %wasm_path.display(), "executing module"); - run_module(&wasm_path)?; + run_module(&wasm_path, &preopen, ipc_socket.as_deref())?; tracing::info!(session_id, %app_id, "exiting"); Ok(()) @@ -48,19 +79,28 @@ fn resolve_package(app_id: &str) -> anyhow::Result { } #[cfg(not(feature = "wasmtime-runtime"))] -fn run_module(_wasm_path: &std::path::Path) -> anyhow::Result<()> { +fn run_module( + _wasm_path: &std::path::Path, + _preopen: &[(String, String)], + _ipc_socket: Option<&str>, +) -> anyhow::Result<()> { println!("READY"); Ok(()) } #[cfg(feature = "wasmtime-runtime")] -fn run_module(wasm_path: &std::path::Path) -> anyhow::Result<()> { +fn run_module( + wasm_path: &std::path::Path, + preopen: &[(String, String)], + ipc_socket: Option<&str>, +) -> anyhow::Result<()> { + use cap_std::{ambient_authority, fs::Dir}; use wasmtime::{ Config, Engine, Store, component::{Component, Linker}, }; use wasmtime_wasi::{ - ResourceTable, WasiCtx, WasiCtxBuilder, WasiView, add_to_linker_sync, + DirPerms, FilePerms, ResourceTable, WasiCtx, WasiCtxBuilder, WasiView, add_to_linker_sync, bindings::sync::Command, }; @@ -88,10 +128,20 @@ fn run_module(wasm_path: &std::path::Path) -> anyhow::Result<()> { let mut linker: Linker = Linker::new(&engine); add_to_linker_sync(&mut linker).context("add WASI to linker")?; - let ctx = WasiCtxBuilder::new() - .inherit_stdout() - .inherit_stderr() - .build(); + let mut ctx_builder = WasiCtxBuilder::new(); + ctx_builder.inherit_stdout().inherit_stderr(); + + if let Some(socket_path) = ipc_socket { + ctx_builder.env("WEFT_IPC_SOCKET", socket_path); + } + + for (host_path, guest_path) in preopen { + let dir = Dir::open_ambient_dir(host_path, ambient_authority()) + .with_context(|| format!("open preopen dir {host_path}"))?; + ctx_builder.preopened_dir(dir, DirPerms::all(), FilePerms::all(), guest_path); + } + + let ctx = ctx_builder.build(); let mut store = Store::new( &engine, State {