tatara_engine/drivers/
wasi.rs1use anyhow::{bail, Context, Result};
12use async_trait::async_trait;
13use chrono::Utc;
14use std::path::Path;
15use std::time::Duration;
16use tokio::process::Command;
17use tokio::sync::mpsc;
18use tracing::{debug, error, info, warn};
19
20use super::{Driver, LogEntry, TaskHandle};
21use tatara_core::domain::allocation::TaskRunState;
22use tatara_core::domain::job::{DriverType, Task, TaskConfig};
23
24pub struct WasiDriver;
30
31#[async_trait]
32impl Driver for WasiDriver {
33 fn name(&self) -> &str {
34 "wasi"
35 }
36
37 async fn available(&self) -> bool {
38 Command::new("wasmtime")
39 .arg("--version")
40 .output()
41 .await
42 .map(|o| o.status.success())
43 .unwrap_or(false)
44 }
45
46 async fn start(&self, task: &Task, alloc_dir: &Path) -> Result<TaskHandle> {
47 let (wasm_path, capabilities, mounts, allowed_services) = match &task.config {
48 TaskConfig::Wasi {
49 wasm_path,
50 capabilities,
51 mounts,
52 allowed_services,
53 } => (wasm_path, capabilities, mounts, allowed_services),
54 _ => bail!("WasiDriver received non-WASI task config"),
55 };
56
57 let mut cmd = Command::new("wasmtime");
58 cmd.arg("run");
59
60 if capabilities.network {
62 cmd.args(["--wasi", "inherit-network"]);
63 }
64 if capabilities.filesystem {
65 cmd.args(["--wasi", "inherit-filesystem"]);
66 }
67 if capabilities.clocks {
68 cmd.args(["--wasi", "inherit-clocks"]);
69 }
70 if capabilities.random {
71 cmd.args(["--wasi", "inherit-random"]);
72 }
73 if capabilities.stdout || capabilities.stderr {
74 cmd.args(["--wasi", "inherit-stdio"]);
75 }
76
77 for (host_path, guest_path) in mounts {
79 cmd.args(["--dir", &format!("{host_path}::{guest_path}")]);
80 }
81
82 for (k, v) in &task.env {
84 cmd.args(["--env", &format!("{k}={v}")]);
85 }
86
87 if task.resources.cpu_mhz > 0 {
89 let fuel = task.resources.cpu_mhz * 1_000_000;
90 cmd.args(["--fuel", &fuel.to_string()]);
91 }
92
93 cmd.arg(wasm_path);
95
96 let log_dir = alloc_dir.join(&task.name);
98 tokio::fs::create_dir_all(&log_dir)
99 .await
100 .context("Failed to create WASI log directory")?;
101
102 let stdout_file = std::fs::File::create(log_dir.join("stdout.log"))
103 .context("Failed to create stdout log")?;
104 let stderr_file = std::fs::File::create(log_dir.join("stderr.log"))
105 .context("Failed to create stderr log")?;
106
107 cmd.stdout(stdout_file);
108 cmd.stderr(stderr_file);
109 cmd.kill_on_drop(false);
110
111 info!(
112 task = %task.name,
113 wasm_path = %wasm_path,
114 network = capabilities.network,
115 filesystem = capabilities.filesystem,
116 "starting WASI component"
117 );
118
119 let child = cmd.spawn().context("Failed to spawn wasmtime")?;
120 let pid = child.id();
121
122 std::mem::forget(child);
124
125 Ok(TaskHandle {
126 driver: DriverType::Wasi,
127 pid,
128 container_id: None,
129 started_at: Utc::now(),
130 })
131 }
132
133 async fn stop(&self, handle: &TaskHandle, timeout: Duration) -> Result<()> {
134 let Some(pid) = handle.pid else {
135 return Ok(());
136 };
137
138 #[cfg(unix)]
139 {
140 use nix::sys::signal::{kill, Signal};
141 use nix::unistd::Pid;
142
143 let pid = Pid::from_raw(pid as i32);
144
145 if kill(pid, Signal::SIGTERM).is_err() {
147 return Ok(()); }
149
150 let deadline = tokio::time::Instant::now() + timeout;
152 loop {
153 if kill(pid, None).is_err() {
154 return Ok(()); }
156 if tokio::time::Instant::now() >= deadline {
157 let _ = kill(pid, Signal::SIGKILL);
159 return Ok(());
160 }
161 tokio::time::sleep(Duration::from_millis(100)).await;
162 }
163 }
164
165 #[cfg(not(unix))]
166 {
167 warn!("WASI process signal management not supported on this platform");
168 Ok(())
169 }
170 }
171
172 async fn status(&self, handle: &TaskHandle) -> Result<TaskRunState> {
173 let Some(pid) = handle.pid else {
174 return Ok(TaskRunState::Dead);
175 };
176
177 #[cfg(unix)]
178 {
179 use nix::sys::signal::kill;
180 use nix::unistd::Pid;
181
182 let pid = Pid::from_raw(pid as i32);
183 if kill(pid, None).is_ok() {
184 Ok(TaskRunState::Running)
185 } else {
186 Ok(TaskRunState::Dead)
187 }
188 }
189
190 #[cfg(not(unix))]
191 {
192 Ok(TaskRunState::Dead)
193 }
194 }
195
196 async fn logs(&self, handle: &TaskHandle) -> Result<mpsc::Receiver<LogEntry>> {
197 let (tx, rx) = mpsc::channel(256);
199
200 tokio::spawn(async move {
205 let _ = tx;
206 });
208
209 Ok(rx)
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[tokio::test]
218 async fn test_wasi_driver_name() {
219 let driver = WasiDriver;
220 assert_eq!(driver.name(), "wasi");
221 }
222
223 #[tokio::test]
224 async fn test_wasi_driver_available() {
225 let driver = WasiDriver;
226 let _ = driver.available().await;
228 }
229}