2026.9.4.3 —— run 报告程序自己的退出码,且没有构建进程比 mcpp 活得更久 - #555
Merged
Conversation
Gap 4 is analysed first because the plan for it depends on the analysis. The signature — 99.9% of one core, zero children, zero system time, on a graph whose outputs all exist — excludes both branches of ninja's build loop that leave user space, which narrows it to an edge that finishes WITHOUT running a command. This graph has exactly one: the phony fed by the two `restat` staging edges, whose command is regularly skipped while succeeding. That the shape causes the loop remains inference. Three attempts at a stack all failed, and the third is informative: making gdb ninja's parent, which is legal under yama level 1, stops the spin happening at all. Two findings from the same investigation are fully established and independent: mcpp does not kill the ninja it spawned (one orphan outlived the removal of its whole sandbox), and `xim:ninja@1.12.1` names two different binaries. The other three gaps get proposals. Only `mcpp run`'s exit status needs a decision before work starts — it is a compatibility change to a published contract, and the note recommends rather than concludes.
两条缺陷,来自 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
四个缺口的方案,其中 gap 4(ninja 空转)先做了成因分析,因为方案依赖它。
核心:0 子进程 + 0 系统时间 + 图已完成,排除了 ninja 构建循环里两条离开用户态的分支 ⇒ 只剩「不跑命令就完成的边」。这张图里正好只有一条:由两条
restatstaging 边喂养的 phony,而mcpp stage在目标已等价时会跳过写入。成因仍是推断而非测量——三次取栈全失败,其中第三次有信息量:把 gdb 变成 ninja 的父进程(yama level 1 下合法)之后,空转就不再发生。
同一次调查里两条已确证且独立:mcpp 不杀自己生成的 ninja(一个孤儿比它所属沙箱活得久);
xim:ninja@1.12.1指向两个不同的二进制。只有
mcpp run退出码那条需要你先定——它是对已发布契约的兼容性改动,文档给的是建议不是结论。