diff --git a/docs/plans/M3b/11-brokerd-container.md b/docs/plans/M3b/11-brokerd-container.md index 8a45093..e891468 100644 --- a/docs/plans/M3b/11-brokerd-container.md +++ b/docs/plans/M3b/11-brokerd-container.md @@ -13,8 +13,10 @@ proxy is task 12; in this task every call runs its container with no egress dire ## Files -- Copy: `crates/brokerd/tests/support/fake_podman.rs`, `crates/brokerd/tests/container.rs` -- Create: `crates/brokerd/src/container.rs` +- Copy: `crates/brokerd/tests/support/fake_podman.rs`, `crates/brokerd/tests/container.rs`, and + the **skeleton** `crates/brokerd/src/container.rs` (every signature, the constants, and `todo!()` + bodies with the steps as comments) +- Fill in: `crates/brokerd/src/container.rs` - Modify: `crates/brokerd/src/lib.rs` (`pub mod container;`), `docs/implementer-log.md` ## Interfaces @@ -88,13 +90,30 @@ the `serial()` lock first, because writing a script and running it at once races forks ("Text file busy"). The time-limit test's fake ends with `exec sleep 30`, so killing the process kills the sleep and nothing keeps the pipes open. +## How to work (read this first) + +Two earlier sessions on this task ended with nothing written: each tried to plan the whole file +in one turn and ran out of room while still deciding. So: + +- **Start from the skeleton.** It compiles. Replace the `todo!()`s **one function at a time**, in + this order: `read_capped`, `new`, `egress_dir`, `podman`, `wait`, `run`. After each, run + `cargo check -p brokerd` and fix what it says before going on. +- **Let the compiler answer API questions.** If you are unsure whether something compiles (how to + call the log, which trait a type has), write it and run `cargo check`. The log is called as + `(self.log)(&line)`. +- Keep each turn short: write, check, next. Do not restate the task to yourself. + ## Steps - [ ] **1. Copy.** `git switch m3b`, then `cp docs/plans/M3b/files/crates/brokerd/tests/support/fake_podman.rs crates/brokerd/tests/support/` and `cp docs/plans/M3b/files/crates/brokerd/tests/container.rs crates/brokerd/tests/` -- [ ] **2. See it fail.** `cargo test -p brokerd --test container`. Expected: it does not compile. -- [ ] **3. Write `container.rs`.** Run `cargo fmt --all`. + and `cp docs/plans/M3b/files/crates/brokerd/src/container.rs crates/brokerd/src/`, then add + `pub mod container;` to `crates/brokerd/src/lib.rs` after `pub mod config;`. +- [ ] **2. See it fail.** `cargo test -p brokerd --test container`. Expected: it compiles (with + warnings about unused variables) and all 11 fail on `todo!()`. +- [ ] **3. Fill in `container.rs`** as in "How to work". Delete the skeleton paragraph from the + module doc. Run `cargo fmt --all`. - [ ] **4. See it pass.** `cargo test -p brokerd --test container`. Expected: 11 passed. Run it ten times; it must pass every time. - [ ] **5. Walk the table.** Point at the line of your code for each row, and check that no row diff --git a/docs/plans/M3b/README.md b/docs/plans/M3b/README.md index ff18927..c8db2a0 100644 --- a/docs/plans/M3b/README.md +++ b/docs/plans/M3b/README.md @@ -69,6 +69,11 @@ At the end: `make gate` prints `gate: ok` with about 638 tests. `egress_dir` for task 12 but gave nothing that read it, so clippy's `dead_code` would fail the gate and `#[allow]` is forbidden: a task that could not be written as described (tip T16). The reference had a public `egress_dir()` getter; the task now gives it. Resume from task 11. +- 2026-09-23, task 11 again: the second session also ended with nothing written, cut off while + deliberating over API details the compiler would have settled. The task now hands over a + compiling skeleton of `container.rs` (signatures, constants, the steps as comments, `todo!()` + bodies), checked against the given tests (11 red), and says to fill one function at a time with + `cargo check` between. Resume from task 11. ## Running it diff --git a/docs/plans/M3b/files/crates/brokerd/src/container.rs b/docs/plans/M3b/files/crates/brokerd/src/container.rs new file mode 100644 index 0000000..3ce0cfb --- /dev/null +++ b/docs/plans/M3b/files/crates/brokerd/src/container.rs @@ -0,0 +1,82 @@ +//! 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, running +//! `cargo check -p brokerd` after each. Then delete this paragraph. + +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, ExitStatus, Stdio}; +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 Runtime for Podman { + fn run(&self, spec: &RunSpec) -> Result { + // 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. + 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!() +}