use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
pub struct WasiRunRequest {
pub module_key: String,
pub args: Vec<String>,
#[serde(default)]
pub stdin: Option<String>,
#[serde(default)]
pub files: Option<HashMap<String, String>>,
#[serde(default)]
pub output_files: Option<Vec<String>>,
}
#[derive(Debug, Serialize)]
pub struct WasiRunResult {
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub files: Option<HashMap<String, String>>,
}
pub fn run_wasi_module(
wasm_bytes: &[u8],
args: &[String],
stdin: Option<&[u8]>,
files: Option<&HashMap<String, Vec<u8>>>,
output_files: Option<&[String]>,
) -> Result<WasiRunResult, String> {
use base64::Engine;
use wasi_common::pipe::{ReadPipe, WritePipe};
use wasi_common::sync::{Dir, WasiCtxBuilder, ambient_authority};
use wasmtime::{Engine as WasmEngine, Linker, Module, Store};
let temp_dir =
tempfile::tempdir().map_err(|e| format!("Failed to create temp directory: {e}"))?;
let work_dir = temp_dir.path();
if let Some(input_files) = files {
for (path, data) in input_files {
let full_path = work_dir.join(path.trim_start_matches('/'));
if let Some(parent) = full_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create dir {}: {e}", parent.display()))?;
}
std::fs::write(&full_path, data)
.map_err(|e| format!("Failed to write {}: {e}", full_path.display()))?;
}
}
let mut ctx_builder = WasiCtxBuilder::new();
let mut full_args: Vec<String> = vec!["program".to_string()];
full_args.extend_from_slice(args);
ctx_builder
.args(&full_args)
.map_err(|e| format!("Failed to set args: {e}"))?;
if let Some(stdin_data) = stdin {
ctx_builder.stdin(Box::new(ReadPipe::from(stdin_data.to_vec())));
}
let stdout_pipe = WritePipe::new_in_memory();
let stderr_pipe = WritePipe::new_in_memory();
ctx_builder.stdout(Box::new(stdout_pipe.clone()));
ctx_builder.stderr(Box::new(stderr_pipe.clone()));
let preopen_dir = Dir::open_ambient_dir(work_dir, ambient_authority())
.map_err(|e| format!("Failed to open work dir: {e}"))?;
ctx_builder
.preopened_dir(preopen_dir, "/")
.map_err(|e| format!("Failed to preopen dir: {e}"))?;
let preopen_dir_dot = Dir::open_ambient_dir(work_dir, ambient_authority())
.map_err(|e| format!("Failed to open work dir (dot): {e}"))?;
ctx_builder
.preopened_dir(preopen_dir_dot, ".")
.map_err(|e| format!("Failed to preopen dir (.): {e}"))?;
let wasi_ctx = ctx_builder.build();
let engine = if let Some(config) = crate::platform_wasmtime_config() {
WasmEngine::new(&config).map_err(|e| format!("Failed to create wasmtime engine: {e}"))?
} else {
WasmEngine::default()
};
let module = Module::new(&engine, wasm_bytes)
.map_err(|e| format!("Failed to compile WASI module: {e}"))?;
let mut linker = Linker::new(&engine);
wasi_common::sync::add_to_linker(&mut linker, |s| s)
.map_err(|e| format!("Failed to add WASI to linker: {e}"))?;
let mut store = Store::new(&engine, wasi_ctx);
let instance = linker
.instantiate(&mut store, &module)
.map_err(|e| format!("Failed to instantiate WASI module: {e}"))?;
let start_fn = instance
.get_typed_func::<(), ()>(&mut store, "_start")
.map_err(|e| format!("WASI module has no _start export: {e}"))?;
let exit_code = match start_fn.call(&mut store, ()) {
Ok(()) => 0,
Err(e) => {
if let Some(exit) = e.downcast_ref::<wasi_common::I32Exit>() {
exit.0
} else {
let stderr_bytes = stderr_pipe
.try_into_inner()
.map(|c| c.into_inner())
.unwrap_or_default();
let stderr_text = String::from_utf8_lossy(&stderr_bytes);
return Err(format!("WASI module trapped: {e}\nstderr: {stderr_text}"));
}
}
};
drop(store);
let stdout_bytes = stdout_pipe
.try_into_inner()
.map(|c| c.into_inner())
.unwrap_or_default();
let stdout_b64 = base64::engine::general_purpose::STANDARD.encode(&stdout_bytes);
let stderr_bytes = stderr_pipe
.try_into_inner()
.map(|c| c.into_inner())
.unwrap_or_default();
let stderr_text = String::from_utf8_lossy(&stderr_bytes).to_string();
let captured_files = if let Some(paths) = output_files {
let mut result = HashMap::new();
for path in paths {
let full_path = work_dir.join(path.trim_start_matches('/'));
if full_path.exists() {
match std::fs::read(&full_path) {
Ok(data) => {
result.insert(
path.clone(),
base64::engine::general_purpose::STANDARD.encode(&data),
);
}
Err(e) => {
log::warn!(
"[wasi_runner] Failed to read output file {}: {e}",
full_path.display()
);
}
}
}
}
if result.is_empty() {
None
} else {
Some(result)
}
} else {
None
};
Ok(WasiRunResult {
exit_code,
stdout: stdout_b64,
stderr: stderr_text,
files: captured_files,
})
}