The third attempt filled five functions, then ran out of room planning all of run in one turn. run and run_container are now given; spawn, Io::start, Io::finish and answer are small todo!()s. Checked fillable: 11 of 11 passed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
149 lines
5.9 KiB
Rust
149 lines
5.9 KiB
Rust
//! 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<dyn Fn(&str) + Send + Sync>;
|
|
|
|
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 <name>`, `podman rm -f <name>`,
|
|
/// `child.kill()` and `child.wait()` (step 5).
|
|
fn wait(&self, child: &mut Child, name: &str, started: Instant, limit: Duration) -> Option<ExitStatus> {
|
|
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<OsString>,
|
|
input: String,
|
|
limit: Duration,
|
|
) -> Result<RunOutput, RunError> {
|
|
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<OsString>) -> Option<Child> {
|
|
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<ExitStatus>,
|
|
out: Vec<u8>,
|
|
truncated: bool,
|
|
err: &str,
|
|
) -> Result<RunOutput, RunError> {
|
|
todo!()
|
|
}
|
|
}
|
|
|
|
impl Runtime for Podman {
|
|
/// Steps 1 and 2, then `run_container`. Already written.
|
|
fn run(&self, spec: &RunSpec) -> Result<RunOutput, RunError> {
|
|
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<JoinHandle<()>>,
|
|
stdout: Option<JoinHandle<(Vec<u8>, bool)>>,
|
|
stderr: Option<JoinHandle<(Vec<u8>, 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<u8>, 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<u8>, bool) {
|
|
todo!()
|
|
}
|