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>
This commit is contained in:
2026-09-23 08:36:00 -07:00
co-authored by Claude Opus 5.5
parent 62d1da44d6
commit 4a1c6fa0a5
4 changed files with 98 additions and 18 deletions
@@ -2,12 +2,15 @@
//! 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, running
//! `cargo check -p brokerd` after each. Then delete this paragraph.
//! 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};
@@ -61,16 +64,79 @@ impl Podman {
}
}
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> {
// 1. `n` and `name`.
// 2. `args`, `input`, `limit`.
// 3. Spawn podman with all three standard streams piped; a failure is Unavailable.
// 4. Three threads: write `input` then drop stdin; `read_capped` stdout with the cap;
// `read_capped` stderr with STDERR_KEPT.
// 5. `self.wait(…)`.
// 6. Join the three threads (`join().ok()`, `unwrap_or_default()`).
// 7. The answer, by the table in the task.
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!()
}
}