Skip to content

A killed build is resurrected by the shared log it died writing

scope: generic · severity: finding · confidence: proven · subsystem: statusline

The question — porthole pkg stop returned stopped pid 22437, and the status line went on showing the build for about two minutes:

device-google-taimen [▎─────────────────] 1% quiet 1m41s 87h54m build 1008/55856 ·

Nothing was running. Why does a cancelled build keep a row, and where does an 87-hour ETA come from?

The answer — build_snapshot’s last resort synthesises a row from the shared pmbootstrap log, for builds that publish no status file at all (sandbox shell --command, or one started after the last tracked build wrote done). It has three guards against inventing a build, and a kill defeats all three at once:

guard what a kill does to it
log_outcome() — did the log say how this build ended? abuild was killed mid-step. No outcome line was ever written.
log_invocation_ended() — did pmbootstrap print DONE!? _kill_inside() is pkill -f 'pmbootstrap.*build'. A pkilled process prints nothing.
names_a_build() + fresh mtime — is a build writing this log now? The tail is genuine ninja steps, and the mtime is fresh because the build was killed a moment ago.

So the row survives for LOG_FRESH_S (120 s) and then vanishes, which is the “phantom for a little bit”. The ETA is extrapolated from statusline-samples.json, whose samples stopped advancing at the kill — 87h54m is what 1008 of 55856 steps looks like at a rate that has gone to nearly zero.

The part worth keeping: this checkout had already published the right answer and it was not consulted. _stop writes state: failed into pkg-status.json before returning, and build_snapshot reads that file only on the paths where it says running. A verdict this checkout wrote is better evidence than a log every pmbootstrap invocation in the workspace shares.

What this rules out — the status file is NOT stale; _stop updates it correctly and pkg status --json says failed throughout. pkg stop did NOT fail to kill anything; both sides of the container boundary are dead. The status line does NOT cache a render or replay one across a reattach — every refresh is a fresh process that recomputes this from files. And the row is not evidence that something survived inside the container; do not go hunting for a process to kill.

How it was established — the three guards were read in porthole_progress.live_build_from_log, then reproduced without a container: a staged APKBUILD naming the package, a log tail of ninja steps with no DONE! and an mtime 101 s old, and a pkg-status.json saying failed. The synthesised snapshot came back state: running, 1008/55856, last_age: 101.0 — the reported row, digit for digit.

The fix keys on the two things that separate a corpse from an untracked build: the published verdict must name the same package, and the log must have been quiet since that verdict. A log that moved on is a new build starting, not the old one’s remains.

This is the same shape as a-freshness-indicator-must-measure-the-thing-you-actually-read, in a second place: the mtime being measured is not the thing whose liveness is claimed.