| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
use crate::microvm::{Outcome, Sink}; |
| 14 |
use crate::syscall::{lines, self_hash, FAILED}; |
| 15 |
use anyhow::{bail, Context, Result}; |
| 16 |
use flow_core::api::Record; |
| 17 |
use flow_core::{Environment, Job, MountSource, Sandbox}; |
| 18 |
use serde::{Deserialize, Serialize}; |
| 19 |
use std::path::{Path, PathBuf}; |
| 20 |
use std::sync::mpsc; |
| 21 |
use std::time::{Duration, Instant}; |
| 22 |
|
| 23 |
|
| 24 |
pub const WASMTIME: &str = "49.0.4"; |
| 25 |
|
| 26 |
|
| 27 |
|
| 28 |
const NOTE: &str = "\u{1e}N "; |
| 29 |
|
| 30 |
|
| 31 |
const TRAPPED: i32 = 134; |
| 32 |
|
| 33 |
|
| 34 |
#[derive(Serialize, Deserialize)] |
| 35 |
struct Spec { |
| 36 |
|
| 37 |
workspace: PathBuf, |
| 38 |
|
| 39 |
modules: Vec<String>, |
| 40 |
|
| 41 |
args: Vec<String>, |
| 42 |
env: Vec<(String, String)>, |
| 43 |
memory_mib: u32, |
| 44 |
|
| 45 |
preopens: Vec<(PathBuf, String)>, |
| 46 |
} |
| 47 |
|
| 48 |
|
| 49 |
|
| 50 |
|
| 51 |
pub fn environment(sandbox: &Sandbox, job: &Job, modules: &[(String, String)]) -> Environment { |
| 52 |
Environment { |
| 53 |
runtime: "wasm".into(), |
| 54 |
parts: vec![ |
| 55 |
("wasmtime".into(), WASMTIME.into()), |
| 56 |
("flow".into(), self_hash().to_string()), |
| 57 |
("memory_mib".into(), sandbox.memory_mib.to_string()), |
| 58 |
] |
| 59 |
.into_iter() |
| 60 |
.chain(modules.iter().map(|(path, hash)| (format!("module {path}"), hash.clone()))) |
| 61 |
.collect(), |
| 62 |
rootfs: String::new(), |
| 63 |
argv: job.exec.clone(), |
| 64 |
env: job.env.iter().map(|(k, v)| (k.clone(), v.clone())).collect(), |
| 65 |
} |
| 66 |
} |
| 67 |
|
| 68 |
|
| 69 |
|
| 70 |
|
| 71 |
pub fn modules(workspace: &Path, program: &str) -> Result<Vec<String>> { |
| 72 |
let rel = program.strip_prefix("/src/").unwrap_or(program).trim_start_matches("./"); |
| 73 |
if rel.is_empty() || rel.starts_with('/') || rel.split('/').any(|p| p == "..") { |
| 74 |
bail!("module {program:?}: give a path inside the job's files, such as target/wasm32-wasip1/debug/app.wasm"); |
| 75 |
} |
| 76 |
let (dir, last) = rel.rsplit_once('/').unwrap_or(("", rel)); |
| 77 |
if dir.contains('*') { |
| 78 |
bail!("module {program:?}: only the file name may hold `*`"); |
| 79 |
} |
| 80 |
if !last.contains('*') { |
| 81 |
if !workspace.join(rel).is_file() { |
| 82 |
bail!("module {rel} is not among the job's files (its scope or an earlier job's artifact)"); |
| 83 |
} |
| 84 |
return Ok(vec![rel.to_string()]); |
| 85 |
} |
| 86 |
let mut found: Vec<String> = std::fs::read_dir(workspace.join(dir)) |
| 87 |
.with_context(|| format!("module {program:?}: the directory {dir} is not among the job's files"))? |
| 88 |
.filter_map(|e| e.ok()) |
| 89 |
.filter(|e| e.file_type().is_ok_and(|t| t.is_file())) |
| 90 |
.filter_map(|e| e.file_name().into_string().ok()) |
| 91 |
.filter(|name| wildcard(last, name)) |
| 92 |
.map(|name| if dir.is_empty() { name } else { format!("{dir}/{name}") }) |
| 93 |
.collect(); |
| 94 |
found.sort(); |
| 95 |
if found.is_empty() { |
| 96 |
bail!("no module matches {program:?}"); |
| 97 |
} |
| 98 |
Ok(found) |
| 99 |
} |
| 100 |
|
| 101 |
|
| 102 |
fn wildcard(pattern: &str, name: &str) -> bool { |
| 103 |
let parts: Vec<&str> = pattern.split('*').collect(); |
| 104 |
let (first, last) = (parts[0], parts[parts.len() - 1]); |
| 105 |
if parts.len() == 1 { |
| 106 |
return pattern == name; |
| 107 |
} |
| 108 |
if !name.starts_with(first) || !name[first.len()..].ends_with(last) || name.len() < first.len() + last.len() { |
| 109 |
return false; |
| 110 |
} |
| 111 |
let mut rest = &name[first.len()..name.len() - last.len()]; |
| 112 |
for middle in &parts[1..parts.len() - 1] { |
| 113 |
match rest.find(middle) { |
| 114 |
Some(i) => rest = &rest[i + middle.len()..], |
| 115 |
None => return false, |
| 116 |
} |
| 117 |
} |
| 118 |
true |
| 119 |
} |
| 120 |
|
| 121 |
|
| 122 |
|
| 123 |
|
| 124 |
pub fn run(sandbox: &Sandbox, job: &Job, workspace: &Path, scratch: &Path, timeout: Duration, sink: &mut dyn Sink) -> Result<Outcome> { |
| 125 |
if !job.secrets.is_empty() || !job.identities.is_empty() { |
| 126 |
bail!("job {}: the WASM tier takes no secrets or identities yet; run it in a microvm or syscall sandbox", job.name); |
| 127 |
} |
| 128 |
let Some(program) = job.exec.first() else { bail!("job {}: exec names no module", job.name) }; |
| 129 |
let modules = modules(workspace, program)?; |
| 130 |
let hashed: Vec<(String, String)> = |
| 131 |
modules.iter().map(|m| Ok((m.clone(), crate::home::hash_file(&workspace.join(m))?))).collect::<Result<_>>()?; |
| 132 |
let environment = environment(sandbox, job, &hashed); |
| 133 |
let mut preopens = Vec::new(); |
| 134 |
let mut disks = 0; |
| 135 |
for (i, m) in job.mounts.iter().enumerate() { |
| 136 |
let dir = match m.source { |
| 137 |
MountSource::Disk => { |
| 138 |
disks += 1; |
| 139 |
crate::syscall::disk_dir(scratch, disks - 1) |
| 140 |
} |
| 141 |
MountSource::Tmpfs => scratch.join(format!("tmpfs-{i}")), |
| 142 |
}; |
| 143 |
std::fs::create_dir_all(&dir)?; |
| 144 |
preopens.push((dir, m.target.clone())); |
| 145 |
} |
| 146 |
let spec = Spec { |
| 147 |
workspace: workspace.to_path_buf(), |
| 148 |
modules, |
| 149 |
args: job.exec[1..].to_vec(), |
| 150 |
env: job.env.iter().map(|(k, v)| (k.clone(), v.clone())).collect(), |
| 151 |
memory_mib: sandbox.memory_mib, |
| 152 |
preopens, |
| 153 |
}; |
| 154 |
if in_process() { |
| 155 |
return run_in_thread(&spec, environment, timeout, sink); |
| 156 |
} |
| 157 |
let spec_path = scratch.join("wasm.json"); |
| 158 |
std::fs::write(&spec_path, serde_json::to_vec(&spec)?)?; |
| 159 |
|
| 160 |
let started = Instant::now(); |
| 161 |
let mut child = std::process::Command::new("/proc/self/exe") |
| 162 |
.arg("__wasm") |
| 163 |
.arg(&spec_path) |
| 164 |
.env_clear() |
| 165 |
.current_dir(scratch) |
| 166 |
.stdin(std::process::Stdio::null()) |
| 167 |
.stdout(std::process::Stdio::piped()) |
| 168 |
.stderr(std::process::Stdio::piped()) |
| 169 |
.spawn() |
| 170 |
.context("start the WASM helper")?; |
| 171 |
let (tx, rx) = mpsc::channel::<(bool, Vec<u8>)>(); |
| 172 |
let out = child.stdout.take().unwrap(); |
| 173 |
let err = child.stderr.take().unwrap(); |
| 174 |
let tx2 = tx.clone(); |
| 175 |
let readers = [std::thread::spawn(move || lines(out, true, tx)), std::thread::spawn(move || lines(err, false, tx2))]; |
| 176 |
let t = |s: &Instant| s.elapsed().as_millis() as u64; |
| 177 |
let (mut timed_out, mut cancelled, mut failed_to_start) = (false, false, false); |
| 178 |
let mut peak_memory_mib = None; |
| 179 |
let deadline = started + timeout; |
| 180 |
loop { |
| 181 |
if let Some(m) = crate::facts::peak_rss_mib(child.id()) { |
| 182 |
peak_memory_mib = Some(m); |
| 183 |
} |
| 184 |
match rx.recv_timeout(Duration::from_millis(200)) { |
| 185 |
Ok((stdout, line)) => { |
| 186 |
let text = String::from_utf8_lossy(&line).into_owned(); |
| 187 |
let rec = |stream: &str, text: &str| Record { t: t(&started), stream: stream.into(), text: text.into() }; |
| 188 |
if let Some(note) = text.strip_prefix(NOTE).filter(|_| !stdout) { |
| 189 |
sink.record(rec("flow", note)); |
| 190 |
} else if let Some(why) = text.strip_prefix(FAILED).filter(|_| !stdout) { |
| 191 |
failed_to_start = true; |
| 192 |
sink.record(rec("flow", &format!("the WASM tier could not run the job: {why}"))); |
| 193 |
} else { |
| 194 |
sink.record(rec(if stdout { "out" } else { "err" }, &text)); |
| 195 |
} |
| 196 |
} |
| 197 |
Err(mpsc::RecvTimeoutError::Timeout) => {} |
| 198 |
Err(mpsc::RecvTimeoutError::Disconnected) => break, |
| 199 |
} |
| 200 |
if !timed_out && !cancelled && sink.cancelled() { |
| 201 |
cancelled = true; |
| 202 |
let _ = child.kill(); |
| 203 |
sink.record(Record { t: t(&started), stream: "flow".into(), text: "stopped: the run was cancelled".into() }); |
| 204 |
} |
| 205 |
if Instant::now() >= deadline && !timed_out && !cancelled { |
| 206 |
timed_out = true; |
| 207 |
let _ = child.kill(); |
| 208 |
sink.record(Record { |
| 209 |
t: t(&started), |
| 210 |
stream: "flow".into(), |
| 211 |
text: format!("stopped at the time limit of {} s", timeout.as_secs()), |
| 212 |
}); |
| 213 |
} |
| 214 |
} |
| 215 |
let status = child.wait()?; |
| 216 |
for r in readers { |
| 217 |
let _ = r.join(); |
| 218 |
} |
| 219 |
let _ = std::fs::remove_file(&spec_path); |
| 220 |
let exit_code = status.code(); |
| 221 |
if exit_code.is_none() && !timed_out && !cancelled && !failed_to_start { |
| 222 |
sink.record(Record { |
| 223 |
t: t(&started), |
| 224 |
stream: "flow".into(), |
| 225 |
text: format!("the WASM helper ended without an exit code ({status})"), |
| 226 |
}); |
| 227 |
} |
| 228 |
Ok(Outcome { |
| 229 |
exit_code: if timed_out || cancelled { None } else { exit_code }, |
| 230 |
duration_ms: t(&started), |
| 231 |
environment, |
| 232 |
timed_out, |
| 233 |
cancelled, |
| 234 |
peak_memory_mib, |
| 235 |
pruned: Vec::new(), |
| 236 |
}) |
| 237 |
} |
| 238 |
|
| 239 |
|
| 240 |
|
| 241 |
|
| 242 |
|
| 243 |
pub fn in_process() -> bool { |
| 244 |
cfg!(target_os = "android") || std::env::var_os("FLOW_WASM_IN_PROCESS").is_some() |
| 245 |
} |
| 246 |
|
| 247 |
|
| 248 |
const PIPE_BYTES: usize = 64 << 20; |
| 249 |
|
| 250 |
|
| 251 |
|
| 252 |
|
| 253 |
fn run_in_thread(spec: &Spec, environment: Environment, timeout: Duration, sink: &mut dyn Sink) -> Result<Outcome> { |
| 254 |
use wasmtime_wasi::p2::pipe::MemoryOutputPipe; |
| 255 |
let mut config = wasmtime::Config::new(); |
| 256 |
config.epoch_interruption(true); |
| 257 |
let engine = ww(wasmtime::Engine::new(&config), || "start wasmtime".into())?; |
| 258 |
let (stdout, stderr) = (MemoryOutputPipe::new(PIPE_BYTES), MemoryOutputPipe::new(PIPE_BYTES)); |
| 259 |
let (notes_tx, notes) = mpsc::channel::<String>(); |
| 260 |
let io = Io::Pipes { stdout: stdout.clone(), stderr: stderr.clone(), notes: notes_tx }; |
| 261 |
let started = Instant::now(); |
| 262 |
let t = |s: &Instant| s.elapsed().as_millis() as u64; |
| 263 |
let deadline = started + timeout; |
| 264 |
let (mut timed_out, mut cancelled) = (false, false); |
| 265 |
|
| 266 |
let mut said = [(0usize, Vec::<u8>::new()), (0usize, Vec::<u8>::new())]; |
| 267 |
let mut pass_on = |sink: &mut dyn Sink, last: bool| { |
| 268 |
for (i, pipe) in [&stdout, &stderr].into_iter().enumerate() { |
| 269 |
let all = pipe.contents(); |
| 270 |
let (offset, partial) = &mut said[i]; |
| 271 |
partial.extend_from_slice(&all[*offset..]); |
| 272 |
*offset = all.len(); |
| 273 |
while let Some(n) = partial.iter().position(|b| *b == b'\n') { |
| 274 |
let line: Vec<u8> = partial.drain(..=n).collect(); |
| 275 |
let text = String::from_utf8_lossy(&line[..line.len() - 1]).into_owned(); |
| 276 |
sink.record(Record { t: t(&started), stream: if i == 0 { "out" } else { "err" }.into(), text }); |
| 277 |
} |
| 278 |
if last && !partial.is_empty() { |
| 279 |
let text = String::from_utf8_lossy(partial).into_owned(); |
| 280 |
partial.clear(); |
| 281 |
sink.record(Record { t: t(&started), stream: if i == 0 { "out" } else { "err" }.into(), text }); |
| 282 |
} |
| 283 |
} |
| 284 |
while let Ok(note) = notes.try_recv() { |
| 285 |
sink.record(Record { t: t(&started), stream: "flow".into(), text: note }); |
| 286 |
} |
| 287 |
}; |
| 288 |
let ran = std::thread::scope(|scope| { |
| 289 |
let worker = scope.spawn(|| run_modules(&engine, spec, &io)); |
| 290 |
while !worker.is_finished() { |
| 291 |
std::thread::sleep(Duration::from_millis(100)); |
| 292 |
pass_on(sink, false); |
| 293 |
if !timed_out && !cancelled && sink.cancelled() { |
| 294 |
cancelled = true; |
| 295 |
engine.increment_epoch(); |
| 296 |
sink.record(Record { t: t(&started), stream: "flow".into(), text: "stopped: the run was cancelled".into() }); |
| 297 |
} |
| 298 |
if Instant::now() >= deadline && !timed_out && !cancelled { |
| 299 |
timed_out = true; |
| 300 |
engine.increment_epoch(); |
| 301 |
sink.record(Record { |
| 302 |
t: t(&started), |
| 303 |
stream: "flow".into(), |
| 304 |
text: format!("stopped at the time limit of {} s", timeout.as_secs()), |
| 305 |
}); |
| 306 |
} |
| 307 |
} |
| 308 |
worker.join().unwrap_or_else(|_| Err(anyhow::anyhow!("the WASM worker panicked"))) |
| 309 |
}); |
| 310 |
pass_on(sink, true); |
| 311 |
let (exit_code, peak) = match ran { |
| 312 |
Ok((code, peak)) => (Some(code), Some(peak)), |
| 313 |
Err(e) => { |
| 314 |
sink.record(Record { t: t(&started), stream: "flow".into(), text: format!("the WASM tier could not run the job: {e:#}") }); |
| 315 |
(Some(125), None) |
| 316 |
} |
| 317 |
}; |
| 318 |
Ok(Outcome { |
| 319 |
exit_code: if timed_out || cancelled { None } else { exit_code }, |
| 320 |
duration_ms: t(&started), |
| 321 |
environment, |
| 322 |
timed_out, |
| 323 |
cancelled, |
| 324 |
peak_memory_mib: peak.map(|b| (b as u64).div_ceil(1 << 20)), |
| 325 |
pruned: Vec::new(), |
| 326 |
}) |
| 327 |
} |
| 328 |
|
| 329 |
|
| 330 |
|
| 331 |
|
| 332 |
pub fn wasm_main(spec_path: &Path) -> i32 { |
| 333 |
let ran = (|| { |
| 334 |
let spec: Spec = serde_json::from_slice(&std::fs::read(spec_path)?).context("read the spec")?; |
| 335 |
let engine = ww(wasmtime::Engine::new(&wasmtime::Config::new()), || "start wasmtime".into())?; |
| 336 |
run_modules(&engine, &spec, &Io::Inherit) |
| 337 |
})(); |
| 338 |
match ran { |
| 339 |
Ok((code, _)) => code, |
| 340 |
Err(e) => { |
| 341 |
eprintln!("{FAILED}{e:#}"); |
| 342 |
125 |
| 343 |
} |
| 344 |
} |
| 345 |
} |
| 346 |
|
| 347 |
|
| 348 |
fn ww<T>(r: wasmtime::Result<T>, what: impl FnOnce() -> String) -> Result<T> { |
| 349 |
r.map_err(|e| anyhow::anyhow!("{}: {e:#}", what())) |
| 350 |
} |
| 351 |
|
| 352 |
|
| 353 |
|
| 354 |
enum Io { |
| 355 |
Inherit, |
| 356 |
Pipes { |
| 357 |
stdout: wasmtime_wasi::p2::pipe::MemoryOutputPipe, |
| 358 |
stderr: wasmtime_wasi::p2::pipe::MemoryOutputPipe, |
| 359 |
notes: mpsc::Sender<String>, |
| 360 |
}, |
| 361 |
} |
| 362 |
|
| 363 |
impl Io { |
| 364 |
fn note(&self, text: String) { |
| 365 |
match self { |
| 366 |
Io::Inherit => eprintln!("{NOTE}{text}"), |
| 367 |
Io::Pipes { notes, .. } => { |
| 368 |
let _ = notes.send(text); |
| 369 |
} |
| 370 |
} |
| 371 |
} |
| 372 |
} |
| 373 |
|
| 374 |
|
| 375 |
struct Limits { |
| 376 |
memory: usize, |
| 377 |
peak: usize, |
| 378 |
} |
| 379 |
|
| 380 |
impl wasmtime::ResourceLimiter for Limits { |
| 381 |
fn memory_growing(&mut self, _current: usize, desired: usize, maximum: Option<usize>) -> wasmtime::Result<bool> { |
| 382 |
let ok = desired <= self.memory && maximum.is_none_or(|m| desired <= m); |
| 383 |
if ok { |
| 384 |
self.peak = self.peak.max(desired); |
| 385 |
} |
| 386 |
Ok(ok) |
| 387 |
} |
| 388 |
|
| 389 |
fn table_growing(&mut self, _current: usize, desired: usize, maximum: Option<usize>) -> wasmtime::Result<bool> { |
| 390 |
Ok(maximum.is_none_or(|m| desired <= m)) |
| 391 |
} |
| 392 |
} |
| 393 |
|
| 394 |
struct State { |
| 395 |
wasi: wasmtime_wasi::p1::WasiP1Ctx, |
| 396 |
limits: Limits, |
| 397 |
} |
| 398 |
|
| 399 |
|
| 400 |
|
| 401 |
fn run_modules(engine: &wasmtime::Engine, spec: &Spec, io: &Io) -> Result<(i32, usize)> { |
| 402 |
let mut linker: wasmtime::Linker<State> = wasmtime::Linker::new(engine); |
| 403 |
ww(wasmtime_wasi::p1::add_to_linker_sync(&mut linker, |s: &mut State| &mut s.wasi), || "link WASI".into())?; |
| 404 |
let (mut first_failure, mut peak) = (0, 0); |
| 405 |
for path in &spec.modules { |
| 406 |
if spec.modules.len() > 1 { |
| 407 |
io.note(format!("running {path}")); |
| 408 |
} |
| 409 |
let (code, held) = run_module(engine, &linker, spec, path, io)?; |
| 410 |
peak = peak.max(held); |
| 411 |
if spec.modules.len() > 1 { |
| 412 |
io.note(format!("{path}: exit {code}")); |
| 413 |
} |
| 414 |
if first_failure == 0 && code != 0 { |
| 415 |
first_failure = code; |
| 416 |
} |
| 417 |
} |
| 418 |
Ok((first_failure, peak)) |
| 419 |
} |
| 420 |
|
| 421 |
fn run_module(engine: &wasmtime::Engine, linker: &wasmtime::Linker<State>, spec: &Spec, path: &str, io: &Io) -> Result<(i32, usize)> { |
| 422 |
use wasmtime_wasi::{FsPerms, WasiCtxBuilder}; |
| 423 |
let module = ww(wasmtime::Module::from_file(engine, spec.workspace.join(path)), || format!("module {path} does not load"))?; |
| 424 |
let name = path.rsplit('/').next().unwrap_or(path); |
| 425 |
let mut argv = vec![name.to_string()]; |
| 426 |
argv.extend(spec.args.iter().cloned()); |
| 427 |
let mut wasi = WasiCtxBuilder::new(); |
| 428 |
match io { |
| 429 |
Io::Inherit => { |
| 430 |
wasi.inherit_stdout().inherit_stderr(); |
| 431 |
} |
| 432 |
Io::Pipes { stdout, stderr, .. } => { |
| 433 |
wasi.stdout(stdout.clone()).stderr(stderr.clone()); |
| 434 |
} |
| 435 |
} |
| 436 |
wasi.args(&argv).envs(&spec.env).env("PWD", "/src").allow_blocking_current_thread(true); |
| 437 |
let open = |wasi: &mut WasiCtxBuilder, dir: &Path, at: &str| { |
| 438 |
ww(wasi.preopened_dir(dir, at, FsPerms::ReadWrite).map(|_| ()), || format!("open {} as {at}", dir.display())) |
| 439 |
}; |
| 440 |
open(&mut wasi, &spec.workspace, "/src")?; |
| 441 |
|
| 442 |
open(&mut wasi, &spec.workspace, ".")?; |
| 443 |
for (dir, target) in &spec.preopens { |
| 444 |
open(&mut wasi, dir, target)?; |
| 445 |
} |
| 446 |
let limits = Limits { memory: spec.memory_mib as usize * 1024 * 1024, peak: 0 }; |
| 447 |
let mut store = wasmtime::Store::new(engine, State { wasi: wasi.build_p1(), limits }); |
| 448 |
store.limiter(|s| &mut s.limits); |
| 449 |
|
| 450 |
|
| 451 |
store.set_epoch_deadline(1); |
| 452 |
let instance = ww(linker.instantiate(&mut store, &module), || format!("module {path} cannot start here"))?; |
| 453 |
let start = |
| 454 |
ww(instance.get_typed_func::<(), ()>(&mut store, "_start"), || format!("module {path} has no _start: is it a WASI command?"))?; |
| 455 |
let code = match start.call(&mut store, ()) { |
| 456 |
Ok(()) => 0, |
| 457 |
Err(e) => match e.downcast_ref::<wasmtime_wasi::I32Exit>() { |
| 458 |
Some(exit) => exit.0, |
| 459 |
None => { |
| 460 |
io.note(format!("{path} trapped: {e:#}")); |
| 461 |
TRAPPED |
| 462 |
} |
| 463 |
}, |
| 464 |
}; |
| 465 |
Ok((code, store.data().limits.peak)) |
| 466 |
} |
| 467 |
|
| 468 |
#[cfg(test)] |
| 469 |
mod tests { |
| 470 |
use super::*; |
| 471 |
|
| 472 |
|
| 473 |
fn spec_of(dir: &Path, text: &str) -> Spec { |
| 474 |
std::fs::write(dir.join("m.wasm"), wat::parse_str(text).unwrap()).unwrap(); |
| 475 |
Spec { workspace: dir.to_path_buf(), modules: vec!["m.wasm".into()], args: vec![], env: vec![], memory_mib: 64, preopens: vec![] } |
| 476 |
} |
| 477 |
|
| 478 |
fn env() -> Environment { |
| 479 |
Environment { runtime: "wasm".into(), parts: vec![], rootfs: String::new(), argv: vec![], env: vec![] } |
| 480 |
} |
| 481 |
|
| 482 |
|
| 483 |
struct Collect(Vec<Record>, bool); |
| 484 |
impl Sink for Collect { |
| 485 |
fn record(&mut self, r: Record) { |
| 486 |
self.0.push(r); |
| 487 |
} |
| 488 |
fn cancelled(&mut self) -> bool { |
| 489 |
self.1 |
| 490 |
} |
| 491 |
} |
| 492 |
|
| 493 |
const PRINT: &str = r#"(module |
| 494 |
(import "wasi_snapshot_preview1" "fd_write" (func $w (param i32 i32 i32 i32) (result i32))) |
| 495 |
(memory (export "memory") 2) |
| 496 |
(data (i32.const 16) "line one\nline two") |
| 497 |
(func (export "_start") |
| 498 |
(i32.store (i32.const 0) (i32.const 16)) (i32.store (i32.const 4) (i32.const 17)) |
| 499 |
(drop (call $w (i32.const 1) (i32.const 0) (i32.const 1) (i32.const 8)))))"#; |
| 500 |
|
| 501 |
const SPIN: &str = r#"(module (func (export "_start") (loop $l (br $l))))"#; |
| 502 |
|
| 503 |
#[test] |
| 504 |
fn in_process_output_streams_as_lines() { |
| 505 |
let dir = tempfile::tempdir().unwrap(); |
| 506 |
let mut sink = Collect(vec![], false); |
| 507 |
let o = run_in_thread(&spec_of(dir.path(), PRINT), env(), Duration::from_secs(30), &mut sink).unwrap(); |
| 508 |
assert_eq!(o.exit_code, Some(0)); |
| 509 |
let out: Vec<&str> = sink.0.iter().filter(|r| r.stream == "out").map(|r| r.text.as_str()).collect(); |
| 510 |
assert_eq!(out, ["line one", "line two"], "a last line without its newline still comes"); |
| 511 |
assert_eq!(o.peak_memory_mib, Some(1), "two 64 KiB pages, in whole MiB"); |
| 512 |
} |
| 513 |
|
| 514 |
#[test] |
| 515 |
fn in_process_stops_at_the_time_limit_and_when_cancelled() { |
| 516 |
let dir = tempfile::tempdir().unwrap(); |
| 517 |
let mut sink = Collect(vec![], false); |
| 518 |
let started = Instant::now(); |
| 519 |
let o = run_in_thread(&spec_of(dir.path(), SPIN), env(), Duration::from_millis(300), &mut sink).unwrap(); |
| 520 |
assert!(o.timed_out && o.exit_code.is_none(), "{:?}", sink.0); |
| 521 |
assert!(started.elapsed() < Duration::from_secs(5), "the endless loop was interrupted"); |
| 522 |
let mut sink = Collect(vec![], true); |
| 523 |
let o = run_in_thread(&spec_of(dir.path(), SPIN), env(), Duration::from_secs(30), &mut sink).unwrap(); |
| 524 |
assert!(o.cancelled && o.exit_code.is_none()); |
| 525 |
assert!(sink.0.iter().any(|r| r.text == "stopped: the run was cancelled")); |
| 526 |
} |
| 527 |
|
| 528 |
#[test] |
| 529 |
fn globs_match_names_with_stars() { |
| 530 |
assert!(wildcard("*.wasm", "oxy_core-1a2b.wasm")); |
| 531 |
assert!(wildcard("oxy_core-*.wasm", "oxy_core-1a2b.wasm")); |
| 532 |
assert!(!wildcard("oxy_core-*.wasm", "oxy_core-1a2b.d")); |
| 533 |
assert!(wildcard("a*b*c", "aXbYc") && !wildcard("a*b*c", "aXcYb")); |
| 534 |
assert!(wildcard("plain.wasm", "plain.wasm") && !wildcard("plain.wasm", "other.wasm")); |
| 535 |
} |
| 536 |
|
| 537 |
#[test] |
| 538 |
fn modules_are_files_of_the_job() { |
| 539 |
let dir = tempfile::tempdir().unwrap(); |
| 540 |
let deps = dir.path().join("target/wasm32-wasip1/debug/deps"); |
| 541 |
std::fs::create_dir_all(&deps).unwrap(); |
| 542 |
for f in ["b-2.wasm", "a-1.wasm", "a-1.d"] { |
| 543 |
std::fs::write(deps.join(f), b"").unwrap(); |
| 544 |
} |
| 545 |
let ws = dir.path(); |
| 546 |
assert_eq!( |
| 547 |
modules(ws, "/src/target/wasm32-wasip1/debug/deps/*.wasm").unwrap(), |
| 548 |
["target/wasm32-wasip1/debug/deps/a-1.wasm", "target/wasm32-wasip1/debug/deps/b-2.wasm"] |
| 549 |
); |
| 550 |
assert_eq!(modules(ws, "./target/wasm32-wasip1/debug/deps/b-2.wasm").unwrap(), ["target/wasm32-wasip1/debug/deps/b-2.wasm"]); |
| 551 |
assert!(modules(ws, "target/wasm32-wasip1/debug/deps/*.so").unwrap_err().to_string().contains("no module matches")); |
| 552 |
assert!(modules(ws, "../etc/passwd").is_err()); |
| 553 |
assert!(modules(ws, "/etc/x.wasm").is_err()); |
| 554 |
assert!(modules(ws, "target/*/debug/a.wasm").unwrap_err().to_string().contains("only the file name")); |
| 555 |
} |
| 556 |
} |