use anyhow::{bail, Context, Result};
use async_trait::async_trait;
use chrono::Utc;
use std::path::Path;
use std::time::Duration;
use tokio::process::Command;
use tokio::sync::mpsc;
use tracing::{debug, error, info, warn};
use super::{Driver, LogEntry, TaskHandle};
use tatara_core::domain::allocation::TaskRunState;
use tatara_core::domain::job::{DriverType, Task, TaskConfig};
pub struct WasiDriver;
#[async_trait]
impl Driver for WasiDriver {
fn name(&self) -> &str {
"wasi"
}
async fn available(&self) -> bool {
Command::new("wasmtime")
.arg("--version")
.output()
.await
.map(|o| o.status.success())
.unwrap_or(false)
}
async fn start(&self, task: &Task, alloc_dir: &Path) -> Result<TaskHandle> {
let (wasm_path, capabilities, mounts, allowed_services) = match &task.config {
TaskConfig::Wasi {
wasm_path,
capabilities,
mounts,
allowed_services,
} => (wasm_path, capabilities, mounts, allowed_services),
_ => bail!("WasiDriver received non-WASI task config"),
};
let mut cmd = Command::new("wasmtime");
cmd.arg("run");
if capabilities.network {
cmd.args(["--wasi", "inherit-network"]);
}
if capabilities.filesystem {
cmd.args(["--wasi", "inherit-filesystem"]);
}
if capabilities.clocks {
cmd.args(["--wasi", "inherit-clocks"]);
}
if capabilities.random {
cmd.args(["--wasi", "inherit-random"]);
}
if capabilities.stdout || capabilities.stderr {
cmd.args(["--wasi", "inherit-stdio"]);
}
for (host_path, guest_path) in mounts {
cmd.args(["--dir", &format!("{host_path}::{guest_path}")]);
}
for (k, v) in &task.env {
cmd.args(["--env", &format!("{k}={v}")]);
}
if task.resources.cpu_mhz > 0 {
let fuel = task.resources.cpu_mhz * 1_000_000;
cmd.args(["--fuel", &fuel.to_string()]);
}
cmd.arg(wasm_path);
let log_dir = alloc_dir.join(&task.name);
tokio::fs::create_dir_all(&log_dir)
.await
.context("Failed to create WASI log directory")?;
let stdout_file = std::fs::File::create(log_dir.join("stdout.log"))
.context("Failed to create stdout log")?;
let stderr_file = std::fs::File::create(log_dir.join("stderr.log"))
.context("Failed to create stderr log")?;
cmd.stdout(stdout_file);
cmd.stderr(stderr_file);
cmd.kill_on_drop(false);
info!(
task = %task.name,
wasm_path = %wasm_path,
network = capabilities.network,
filesystem = capabilities.filesystem,
"starting WASI component"
);
let child = cmd.spawn().context("Failed to spawn wasmtime")?;
let pid = child.id();
std::mem::forget(child);
Ok(TaskHandle {
driver: DriverType::Wasi,
pid,
container_id: None,
started_at: Utc::now(),
})
}
async fn stop(&self, handle: &TaskHandle, timeout: Duration) -> Result<()> {
let Some(pid) = handle.pid else {
return Ok(());
};
#[cfg(unix)]
{
use nix::sys::signal::{kill, Signal};
use nix::unistd::Pid;
let pid = Pid::from_raw(pid as i32);
if kill(pid, Signal::SIGTERM).is_err() {
return Ok(()); }
let deadline = tokio::time::Instant::now() + timeout;
loop {
if kill(pid, None).is_err() {
return Ok(()); }
if tokio::time::Instant::now() >= deadline {
let _ = kill(pid, Signal::SIGKILL);
return Ok(());
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
#[cfg(not(unix))]
{
warn!("WASI process signal management not supported on this platform");
Ok(())
}
}
async fn status(&self, handle: &TaskHandle) -> Result<TaskRunState> {
let Some(pid) = handle.pid else {
return Ok(TaskRunState::Dead);
};
#[cfg(unix)]
{
use nix::sys::signal::kill;
use nix::unistd::Pid;
let pid = Pid::from_raw(pid as i32);
if kill(pid, None).is_ok() {
Ok(TaskRunState::Running)
} else {
Ok(TaskRunState::Dead)
}
}
#[cfg(not(unix))]
{
Ok(TaskRunState::Dead)
}
}
async fn logs(&self, handle: &TaskHandle) -> Result<mpsc::Receiver<LogEntry>> {
let (tx, rx) = mpsc::channel(256);
tokio::spawn(async move {
let _ = tx;
});
Ok(rx)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_wasi_driver_name() {
let driver = WasiDriver;
assert_eq!(driver.name(), "wasi");
}
#[tokio::test]
async fn test_wasi_driver_available() {
let driver = WasiDriver;
let _ = driver.available().await;
}
}