Files
boxmaker/docs/plans/M3b/files/crates/brokerd/src/container.rs
T
kyleandClaude Opus 5.5 4a1c6fa0a5 M3b plan: task 11's skeleton writes run as glue over small helpers
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>
2026-09-23 08:36:00 -07:00

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!()
}