You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
两条缺陷,来自 2026-09-04 那份生态批次留下的四个缺口(§3 与 §5.1 of
`.agents/docs/2026-09-04-four-gaps-after-the-ecosystem-batch.md`)。
## `mcpp run` REPORTS THE PROGRAM'S OWN EXIT STATUS
Both spawn sites ended in `return rc == 0 ? 0 : 1`, so that 2 could mean "could
not start" as distinct from "ran and failed". The distinction was worth keeping;
the price was not. Measured: a program whose `main` returns 3 made `mcpp run`
exit 1, and a bare-metal image qemu reported as 3 arrived as 1 as well. A
command that cannot report a status cannot be used in a script, which is most of
what `mcpp run` is for — and this project's claim is that running on a device is
like running hosted.
Three bands, and only the first belongs to the program:
0-124 the program's own status, passed through
125-127 the spawn was attempted and refused (127 not found,
126 found but not executable, 125 anything else)
2 mcpp refused before attempting anything
THE MIDDLE BAND IS THE SHELL'S, NOT THIS PROJECT'S. `env`, `timeout` and `nice`
already answer 126/127 with these meanings, so a reader who meets one does not
have to look it up, and "could not start" lands there by meaning rather than by
allocation.
A program may itself exit 125-127 and mcpp does not disambiguate by number: what
separates them is that a launcher failure always writes a reason to stderr and a
program's own status never does.
COMPATIBILITY IS THE REASON 2 STAYS WHERE IT IS. mcpp's own refusals — no binary
target, no runner declared, a runner not on PATH — keep exit 2, which is what
every other mcpp command uses. Only "the spawn was refused" moves, and in that
case the program never ran. `mcpp test` is unchanged at 0/1: it aggregates many
programs and has no single status to pass through.
## NO BUILD PROCESS SURVIVES THE mcpp THAT STARTED IT
Measured in the ecosystem sandbox: every `timeout`-terminated `mcpp run` left an
orphaned ninja spinning at 100% of a core, and one outlived the removal of the
entire sandbox it belonged to — its working directory read `(deleted)`. Any CI
that wraps mcpp in `timeout` leaked a busy core per timeout.
The child now gets its own process group (a job object on Windows), and mcpp
SIGKILLs that group on SIGINT/SIGTERM/SIGHUP. THE GROUP RATHER THAN THE PID,
because the child starts children of its own: killing ninja alone would leave
its compilers behind.
SIGKILL RATHER THAN SIGTERM, AND THAT IS NOT AN ESCALATION FOR ITS OWN SAKE.
ninja records a signal in a flag and acts on it where it waits for a subprocess;
a ninja with no command running never reaches that check, so a polite signal is
recorded and never obeyed. That is precisely the state the orphans were in.
THE GUARD BECOMES A REGISTRY. A spanning `[hooks]` command is guarded for the
length of the build and the build's own ninja for the length of its run —
concurrently. With one slot the second registration disarmed the first, so
killing mcpp mid-build would have taken ninja down and left the hook running.
BOTH LAUNCHERS, WHICH THE TEST CAUGHT. `run_exec` was fixed first and the A/B
still showed an orphan: a full build spawns ninja through `capture_exec`, and
fixing one of the two left the defect in the common path.
## `MCPP_NINJA_DEBUG`
Appends `-d <topics>` to both ninja launches. The ninja spin that this batch
could not root-cause is unreachable by debugger — `ptrace_scope=1`,
`perf_event_paranoid=4`, and making gdb its parent stops the spin happening —
so ninja's own `-d explain` is the only instrument left.
## Tests
`339` covers all three exit bands including the boundary a band split gets
wrong: a program exiting 127 itself, which must produce no mcpp error line.
`340` signals mcpp's pid ALONE, because `timeout` and Ctrl-C both signal the
process group and reached ninja even before the fix — a test built on `timeout`
passes either way. It also requires that ninja was seen alive first: an earlier
draft killed mcpp after the build had finished and passed without measuring
anything, and a second draft used 200 parallel units that a 32-core host
finished inside the window. `--jobs 1` makes the window deterministic by
construction. Verified to FAIL against the released 2026.9.4.2, naming the
orphan and its deleted working directory.
`330`'s unrunnable-artifact assertion moves from 2 to 126; the three assertions
around it that cover mcpp's own pre-flight refusals are deliberately unchanged.
0 commit comments