//! The Podman runtime: one fresh container per call, with the limits of `[runner]`. Whatever the //! tool prints is the result's content; every failure is a fixed sentence, and what Podman itself //! said goes only to `brokerd`'s log. M3b spec, section 6. //! //! SKELETON from task 11: replace every `todo!()`, one function at a time, in the order the task //! gives, running `cargo check -p brokerd` after each. `run` and `run_container` are already //! written. Then delete this paragraph. use std::ffi::OsString; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::process::{Child, Command, ExitStatus, Stdio}; use std::thread::JoinHandle; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{Duration, Instant}; use crate::config::Runner; use crate::podman; use crate::runner::{RunError, RunOutput, RunSpec, Runtime}; pub const RUNBOOK: &str = "see docs/runbook.md#runner-unavailable"; pub const COULD_NOT_RUN: &str = "the tool could not run"; pub const CANNOT_START: &str = "the tool runner could not start the container"; pub const KILLED: &str = "the tool was stopped: it ran out of memory or was killed"; pub const TIMED_OUT: &str = "the tool ran past its time limit"; pub const UNEXPECTED: &str = "the tool failed with an unexpected status"; /// How often a running container is checked. pub const POLL: Duration = Duration::from_millis(50); /// How much of Podman's standard error is kept for the log. pub const STDERR_KEPT: usize = 4096; /// A log sink. Call it as `(self.log)("a line")`. pub type Log = Arc; pub struct Podman { runner: Runner, egress_dir: PathBuf, log: Log, next: AtomicU64, } impl Podman { /// `egress_dir` is `Config::egress_dir()`; task 12 uses it. pub fn new(runner: Runner, egress_dir: PathBuf, log: Log) -> Podman { todo!("store the four fields; `next` starts at 0") } /// Where `http_fetch` calls get their directories. Public, so the field counts as read. pub fn egress_dir(&self) -> &Path { todo!() } /// Run one short `podman` command (`kill`, `rm -f`) whose result only matters for the log. pub(crate) fn podman(&self, args: &[&str]) { todo!("`.status()` with the three standard streams null; log a line if it is not a success") } /// Wait for `child` until `limit` has passed since `started`. `Some(status)` if it ended; /// `None` if it ran too long, after `podman kill `, `podman rm -f `, /// `child.kill()` and `child.wait()` (step 5). fn wait(&self, child: &mut Child, name: &str, started: Instant, limit: Duration) -> Option { todo!() } } impl Podman { /// Steps 3 to 7 for one container: spawn, feed and read it, wait, answer. Task 12 calls it too. /// Already written: it only joins the helpers below. fn run_container( &self, name: &str, args: Vec, input: String, limit: Duration, ) -> Result { let Some(mut child) = self.spawn(args) else { return Err(RunError::Unavailable(CANNOT_START.to_string())); }; let started = Instant::now(); let cap = usize::try_from(self.runner.output_cap).unwrap_or(usize::MAX); let io = Io::start(&mut child, input, cap); let status = self.wait(&mut child, name, started, limit); let (out, truncated, err) = io.finish(); self.answer(name, status, out, truncated, &err) } /// Step 3: spawn `podman` with `args` and all three standard streams piped. On failure, log /// `brokerd: cannot start {podman path}: {e}` + "\n" + RUNBOOK and return `None`. fn spawn(&self, args: Vec) -> Option { todo!() } /// Step 7: the answer, by the table in the task. `status` is `None` when the time limit was /// passed. `out` is the kept standard output, `err` the kept standard error (for the log only: /// it never goes into a `RunError`). fn answer( &self, name: &str, status: Option, out: Vec, truncated: bool, err: &str, ) -> Result { todo!() } } impl Runtime for Podman { /// Steps 1 and 2, then `run_container`. Already written. fn run(&self, spec: &RunSpec) -> Result { let n = self.next.fetch_add(1, Ordering::SeqCst); let name = podman::container_name(spec.session(), spec.call(), n); let args = podman::tool_args(spec, &self.runner, &name, None); let input = spec.arguments().canonical_json(); let limit = self.runner.time_limit(spec.tool()); self.run_container(&name, args, input, limit) } } /// Step 4: the three threads that feed and read one container, so no pipe can block another. struct Io { writer: Option>, stdout: Option, bool)>>, stderr: Option, bool)>>, } impl Io { /// Take `child`'s three pipes (`child.stdin.take()` and so on) and start one thread for each: /// write `input` to standard input and then drop it; `read_capped` standard output with `cap`; /// `read_capped` standard error with `STDERR_KEPT`. A pipe that is `None` gets no thread. fn start(child: &mut Child, input: String, cap: usize) -> Io { todo!() } /// Step 6: join the three threads. A thread that is missing or panicked counts as empty /// (`join().ok()`, `unwrap_or_default()`). Returns the kept standard output, whether there was /// more, and the kept standard error decoded with `from_utf8_lossy`. fn finish(self) -> (Vec, bool, String) { todo!() } } /// Everything `from` gives, keeping the first `cap` bytes; `true` if there was more. Reads on past /// the cap, so the writer is never blocked or broken by a closed pipe. No indexing: `buf.get(..n)`. fn read_capped(from: impl Read, cap: usize) -> (Vec, bool) { todo!() }