Skip to content

Commit 9da7163

Browse files
2026.9.4.3 —— run 报告程序自己的退出码,且没有构建进程比 mcpp 活得更久 (#555)
* docs(plan): 生态批次之后剩下的四个缺口与各自的方案 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.9.4.3 —— run 报告程序自己的退出码,且没有构建进程比 mcpp 活得更久 两条缺陷,来自 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. --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent 1c2603c commit 9da7163

15 files changed

Lines changed: 992 additions & 53 deletions

.agents/docs/2026-09-04-four-gaps-after-the-ecosystem-batch.md

Lines changed: 359 additions & 0 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,52 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [2026.9.4.3] — 2026-09-04
7+
8+
### ⭐⭐ `mcpp run` 报告程序自己的退出码
9+
10+
在此之前所有非零退出码都被折成 `1`,为的是让 `2` 表示「起不来」以区别于「跑了但
11+
失败」。区别值得保留,代价不值得:`main` 返回 `3` 的程序让 `mcpp run` 退 `1`,
12+
qemu 报 `3` 的裸机镜像同样到达为 `1`。一条报不出退出码的命令没法写进脚本,而这
13+
正是 `mcpp run` 的主要用途。
14+
15+
取值空间分三段,只有第一段属于程序:
16+
17+
| 区间 | 含义 |
18+
|---|---|
19+
| `0``124` | 程序自己的退出码,原样透传 |
20+
| `125``127` | 尝试启动但被拒绝(`127` 找不到 / `126` 不可执行 / `125` 其他) |
21+
| `2` | mcpp 在尝试启动之前就拒绝了(用法、配置、解析) |
22+
23+
中间那段是 `env``timeout``nice` 早已在用且被 shell 文档化的取值,所以 `126`
24+
`127` 带着惯常含义到达。程序自己也可以退 `125``127`,mcpp 不靠数字区分 ——
25+
启动失败一定向 stderr 写出原因,程序自己的退出码从不写。
26+
27+
`mcpp test` 不变,仍为 `0`/`1`:它聚合多个程序,没有单一退出码可透传。
28+
29+
**兼容性**:mcpp 自身的配置错误仍是 `2`,与其余所有命令一致 —— 变的只有「尝试
30+
启动后被拒」这一种情况,而那时程序根本没运行。契约写在 `docs/11` §6。
31+
32+
### ⚠️⚠️ 没有构建进程能比启动它的 mcpp 活得更久
33+
34+
实测:每一次被 `timeout` 终止的 `mcpp run` 都留下一个空转占满一个核的 ninja,
35+
其中一个的工作目录已经是 `(deleted)`、比它所属的整个沙箱活得还久。任何用
36+
`timeout` 包住 mcpp 的 CI,每超时一次泄漏一个忙核。
37+
38+
子进程现在进入**自己的进程组**(Windows 上是 job object),mcpp 在收到
39+
SIGINT/SIGTERM/SIGHUP 时对该组发 **SIGKILL**。用 SIGKILL 而不是 SIGTERM 是必要
40+
:ninja 把信号记进标志位,只在等待子进程处才检查;一个没有命令在跑的 ninja
41+
永远到不了那个检查点,礼貌的信号被记录且永不执行 —— 那正是那些孤儿所处的状态。
42+
43+
守卫从单槽改为**多槽登记表**:一个跨构建的 `[hooks]` 命令与构建自己的 ninja 会
44+
同时被守卫,单槽会让后注册者解除前者的守卫。
45+
46+
### `MCPP_NINJA_DEBUG`
47+
48+
设置后向 ninja 追加 `-d <topics>`(如 `explain`)。用于那个还没定因的 ninja 空转:
49+
`ptrace_scope=1``perf_event_paranoid=4` 都取不到栈,而把 gdb 变成 ninja 的父
50+
进程之后空转就不再发生 —— `-d explain` 是唯一能对这种进程取证的手段。
51+
652
## [2026.9.4.2] — 2026-09-04
753

854
### ⭐⭐ Cortex-M 有 C 库了:`libdir` 填上,而它的键是**三元组**

docs/11-machine-output.md

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,38 @@ output, and a warning would land in the middle of it.
179179
Both spellings are produced from the same source, so they always describe the
180180
same thing — one answer, two shapes.
181181

182-
## 6. Stability guarantees
182+
## 6. Exit status
183+
184+
`mcpp run` REPORTS THE PROGRAM'S OWN EXIT STATUS. Three bands divide the space,
185+
and only the first belongs to the program:
186+
187+
| range | meaning |
188+
|---|---|
189+
| `0``124` | the program ran; this is its own status, passed through unchanged |
190+
| `125``127` | the spawn was attempted and refused — `127` not found, `126` found but not executable, `125` anything else |
191+
| `2` | mcpp refused before attempting anything: a usage, configuration or resolution error |
192+
193+
Until 2026.9.4.3 every non-zero status was folded to `1`, so that `2` could mean
194+
"could not start" as distinct from "ran and failed". The distinction was worth
195+
keeping; the price was not. A program whose `main` returned `3` made `mcpp run`
196+
exit `1`, and a bare-metal image that qemu reported as `3` arrived as `1` as
197+
well — so the command this project tells people to type could not be branched on.
198+
199+
The middle band is the one `env`, `timeout` and `nice` already use and that
200+
shells document, so `126` and `127` arrive with their usual meanings rather than
201+
as numbers this project allocated.
202+
203+
A PROGRAM MAY ITSELF EXIT `125``127`, AND mcpp DOES NOT TRY TO DISAMBIGUATE BY
204+
NUMBER. What separates the two is that a launcher failure always writes a reason
205+
to stderr and a program's own status never does. A client that must be certain
206+
should read stderr, or use `--format json` where the status is a field rather
207+
than a channel.
208+
209+
`mcpp test` is unchanged and remains `0` or `1`: it aggregates many programs, so
210+
there is no single status to pass through. Per-test codes are in the JSON
211+
stream's `exit_code` field (§8).
212+
213+
## 7. Stability guarantees
183214

184215
For each `kind`, within a `kindVersion`:
185216

@@ -194,7 +225,7 @@ schema — `xlings interface --list` declares 20 capabilities whose
194225
`outputSchema` is, for all 20, only `{"exitCode": integer}`, and a client that
195226
sees a version number assumes there is a contract behind it.
196227

197-
## 7. Kinds
228+
## 8. Kinds
198229

199230
### `mcpp.env` — where mcpp keeps things
200231

@@ -316,13 +347,13 @@ them apart:
316347
"impl": "openkal-musl@0.3.5", "origin": "graph" } ]
317348
```
318349

319-
A field was added rather than `cLibrary` renamed or `mode` widened, because §6
350+
A field was added rather than `cLibrary` renamed or `mode` widened, because §7
320351
promises that fields are added and never removed and that a field's meaning
321352
never changes.
322353

323354
⚠️ **`layers[].interface` changed VALUE for a payload-supplied glibc in
324355
2026.9.1.1** — from `gnu` to `glibc`, and on Windows from `gnu` to `ucrt`. The
325-
field's meaning is unchanged (it still names the implementation), so §6 holds;
356+
field's meaning is unchanged (it still names the implementation), so §7 holds;
326357
what changed is that it stopped reporting the triple's env segment, which is a
327358
request rather than an implementation and is not the name of any C library. The
328359
values are now the ones [14 — The Target Side](14-target-side.md) has always
@@ -367,7 +398,7 @@ mcpp test [pattern] [--workspace] --message-format json
367398

368399
This stream predates the envelope of §2 and is not wrapped in it: it is NDJSON,
369400
one record per test as each finishes, then one summary record per member. A
370-
`--workspace` run ends with one `workspace_summary` record. The §6 guarantees
401+
`--workspace` run ends with one `workspace_summary` record. The §7 guarantees
371402
apply to it — fields are added and never removed, and a field's meaning never
372403
changes — and the fields below are the contract as of 2026.9.2.1.
373404

docs/zh/11-machine-output.md

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,32 @@ mcpp cache list --json -> {"root": …, "entries": [ … ]}
153153

154154
两种拼写由同一个来源产出,所以永远描述同一件事:一个答案,两种形状。
155155

156-
## 6. 稳定性承诺
156+
## 6. 退出码
157+
158+
`mcpp run` 报告程序自己的退出码。整个取值空间分三段,只有第一段属于程序:
159+
160+
| 区间 | 含义 |
161+
|---|---|
162+
| `0``124` | 程序跑过了,这是它自己的退出码,原样透传 |
163+
| `125``127` | 尝试启动但被拒绝 —— `127` 找不到,`126` 找到但不可执行,`125` 其他 |
164+
| `2` | mcpp 在尝试启动之前就拒绝了:用法、配置或解析错误 |
165+
166+
2026.9.4.3 之前,所有非零退出码都被折成 `1`,为的是让 `2` 表示「起不来」以区别于
167+
「跑了但失败」。这个区别值得保留,代价不值得:`main` 返回 `3` 的程序会让
168+
`mcpp run` 退 `1`,qemu 报 `3` 的裸机镜像同样到达为 `1` —— 本项目让人使用的这条
169+
命令因此无法用于分支判断。
170+
171+
中间那一段是 `env``timeout``nice` 早已在用、且被 shell 文档化的取值,所以
172+
`126``127` 带着它们惯常的含义到达,而不是本项目分配的编号。
173+
174+
**程序自己也可以退 `125``127`,mcpp 不试图靠数字区分。** 区分二者的是:启动失败
175+
一定向 stderr 写出原因,而程序自己的退出码从不写。需要确定的客户端应当读 stderr,
176+
或使用 `--format json` —— 那里退出码是一个字段而不是一条通道。
177+
178+
`mcpp test` 不变,仍为 `0``1`:它聚合多个程序,没有单一退出码可以透传。
179+
每个测试各自的退出码在 JSON 流的 `exit_code` 字段里(§8)。
180+
181+
## 7. 稳定性承诺
157182

158183
对每个 `kind`,在同一 `kindVersion` 内:
159184

@@ -166,7 +191,7 @@ mcpp cache list --json -> {"root": …, "entries": [ … ]}
166191
capability,其 `outputSchema` 全部只有 `{"exitCode": integer}`,而客户端看到版本号就会
167192
以为背后有契约。
168193

169-
## 7. 各 kind
194+
## 8. 各 kind
170195

171196
### `mcpp.env` —— mcpp 把东西放在哪
172197

@@ -280,12 +305,12 @@ mcpp why toolchain [--target <triple>] [--toolchain <spec>] --format json
280305
"impl": "openkal-musl@0.3.5", "origin": "graph" } ]
281306
```
282307

283-
**新增一个字段**而不是给 `cLibrary` 改名或给 `mode` 加取值,因为 §6 承诺字段
308+
**新增一个字段**而不是给 `cLibrary` 改名或给 `mode` 加取值,因为 §7 承诺字段
284309
只增不删、且一个字段的含义永不改变。
285310

286311
⚠️ **2026.9.1.1 起,载荷供给的 glibc 让 `layers[].interface`**取值**变了** ——
287312
`gnu` 变为 `glibc`,Windows 上从 `gnu` 变为 `ucrt`。字段的**含义**没变(它仍然是
288-
「哪个实现」),所以 §6 仍然成立;变的是它不再报三元组的 env 段 —— 那是一次请求而
313+
「哪个实现」),所以 §7 仍然成立;变的是它不再报三元组的 env 段 —— 那是一次请求而
289314
不是一个实现,也不是任何一个 C 库的名字。现在的取值就是
290315
[14 —— 目标侧](14-target-side.md)一直列着的那些,并且包可以在
291316
`cfg(c-abi = …)` 谓词里与它们比较。按字面量 `gnu` 取值的客户端需要更新;
@@ -324,7 +349,7 @@ mcpp test [pattern] [--workspace] --message-format json
324349
```
325350

326351
这条流早于 §2 的信封,也不被信封包裹:它是 NDJSON,每个测试结束时一条记录,随后每个
327-
成员一条汇总记录。`--workspace` 运行以一条 `workspace_summary` 记录结束。§6 的保证
352+
成员一条汇总记录。`--workspace` 运行以一条 `workspace_summary` 记录结束。§7 的保证
328353
对它同样成立 —— 字段只增不减,字段含义不变 —— 下表是 2026.9.2.1 时的契约。
329354

330355
每个测试:

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "2026.9.4.2"
3+
version = "2026.9.4.3"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

modules/platform/src/process.cppm

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ void stop_background(const BackgroundCommand& child,
189189
// user can no longer name. Only one command is guarded at a time; a build owns
190190
// at most one.
191191
void guard_background_on_signal(const BackgroundCommand& child);
192-
void clear_background_guard();
192+
void clear_background_guard(const BackgroundCommand& child);
193193

194194
// `spawn_error`: run_exec's contract; with it null a refused spawn is
195195
// formatted into `output`, as capture_exec does.
@@ -596,24 +596,75 @@ int run_exec(const std::vector<std::string>& argv,
596596
for (auto& a : argv) cargv.push_back(const_cast<char*>(a.c_str()));
597597
cargv.push_back(nullptr);
598598

599+
// THE CHILD GETS ITS OWN PROCESS GROUP, AND mcpp KILLS THAT GROUP IF IT IS
600+
// ITSELF KILLED.
601+
//
602+
// Without this, terminating mcpp leaves the child running. Measured: every
603+
// `timeout`-terminated `mcpp run` left an orphaned ninja spinning at 100%
604+
// of a core, and one of them outlived the removal of the entire sandbox it
605+
// belonged to — its working directory read `(deleted)` and it was still
606+
// burning a core half an hour later. Any CI that wraps mcpp in `timeout`
607+
// leaks a busy core per timeout.
608+
//
609+
// The group rather than the pid, because the child starts children of its
610+
// own: killing ninja alone would leave its compilers behind.
611+
//
612+
// A signal is not enough on its own, which is why the guard sends SIGKILL.
613+
// ninja records a signal in a flag and acts on it where it waits for a
614+
// subprocess; a ninja with no command running never reaches that check, so
615+
// a polite signal is recorded and never obeyed.
616+
posix_spawnattr_t attr;
617+
::posix_spawnattr_init(&attr);
618+
::posix_spawnattr_setpgroup(&attr, 0); // 0 ⇒ new group, id == pid
619+
::posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETPGROUP);
620+
599621
pid_t pid = 0;
600-
if (int sp = ::posix_spawnp(&pid, cargv[0], nullptr, nullptr, cargv.data(), envp.data());
601-
sp != 0) {
622+
int sp = ::posix_spawnp(&pid, cargv[0], nullptr, &attr, cargv.data(), envp.data());
623+
::posix_spawnattr_destroy(&attr);
624+
if (sp != 0) {
602625
// Reported once: by the caller when it asked for the errno, here
603626
// otherwise. Never dropped — the errno in hand at this line is the
604627
// whole difference between "Exec format error" and a blank line.
605628
if (spawn_error) *spawn_error = sp;
606629
else std::fputs(spawn_failure(argv.front(), sp).c_str(), stderr);
607630
return 127;
608631
}
632+
mcpp::platform::unixproc::guard_group_on_signal(pid);
609633
int status = 0;
610634
while (::waitpid(pid, &status, 0) < 0) { /* EINTR retry */ }
635+
mcpp::platform::unixproc::unguard_group(pid);
611636
return normalize_exit_code(status);
612637
#else
638+
// THE SAME OWNERSHIP AS THE POSIX BRANCH, EXPRESSED IN THIS PLATFORM'S TERMS.
639+
//
640+
// `std::system` gave the child away: it runs through a cmd.exe mcpp does not
641+
// hold a handle to, so terminating mcpp left the tree running exactly as the
642+
// POSIX branch did before its process group. A job object with
643+
// JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE is the equivalent unit — it takes the
644+
// whole tree, which matters here for the same reason the group does there:
645+
// the child starts compilers of its own.
646+
//
647+
// The command line is built by the same `windows_shell_command_line` the
648+
// rest of this file uses, so the cmd.exe quoting rule has ONE derivation.
649+
// Re-deriving it here is how `/d /s /c` loses an argument.
613650
std::string prefix = mcpp::platform::env::build_env_prefix(extraEnv);
614651
// wrap only — run_exec inherits stdio on purpose (see finalize_shell_command).
615-
std::string cmd = wrap_for_cmd_c(prefix + command_from_argv(argv));
616-
return normalize_exit_code(std::system(cmd.c_str()));
652+
std::string cmd = windows_shell_command_line(prefix + command_from_argv(argv));
653+
auto child = mcpp::platform::winproc::spawn_background(cmd.c_str(), nullptr, 1);
654+
if (!child.ok) {
655+
const int refused = static_cast<int>(child.refused);
656+
if (spawn_error) *spawn_error = refused;
657+
else std::fputs(spawn_failure(argv.front(), refused).c_str(), stderr);
658+
return 127;
659+
}
660+
mcpp::platform::winproc::guard_job_on_signal(child.job);
661+
int code = 127;
662+
mcpp::platform::winproc::wait_background(child.process, &code);
663+
mcpp::platform::winproc::unguard_job(child.job);
664+
// Closes both handles; the child has already exited, so this is cleanup
665+
// rather than a kill.
666+
mcpp::platform::winproc::background_stop(child.job, child.process, 0);
667+
return code;
617668
#endif
618669
}
619670

@@ -652,10 +703,22 @@ RunResult capture_exec(
652703
::posix_spawn_file_actions_addclose(&fa, fds[0]);
653704
::posix_spawn_file_actions_addclose(&fa, fds[1]);
654705

706+
// Owned exactly as `run_exec`'s child is, and for the same reason: this is
707+
// the launcher a FULL build uses, so a `mcpp build` interrupted here is the
708+
// common case rather than the rare one. Fixing only `run_exec` left the
709+
// orphan in place — measured, with the two launchers giving opposite
710+
// answers to the same test.
711+
posix_spawnattr_t attr;
712+
::posix_spawnattr_init(&attr);
713+
::posix_spawnattr_setpgroup(&attr, 0);
714+
::posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETPGROUP);
715+
655716
pid_t pid = 0;
656-
int sp = ::posix_spawnp(&pid, cargv[0], &fa, nullptr, cargv.data(), envp.data());
717+
int sp = ::posix_spawnp(&pid, cargv[0], &fa, &attr, cargv.data(), envp.data());
718+
::posix_spawnattr_destroy(&attr);
657719
::posix_spawn_file_actions_destroy(&fa);
658720
::close(fds[1]);
721+
if (sp == 0) mcpp::platform::unixproc::guard_group_on_signal(pid);
659722
if (sp != 0) {
660723
::close(fds[0]);
661724
result.exit_code = 127;
@@ -671,6 +734,7 @@ RunResult capture_exec(
671734
::close(fds[0]);
672735
int status = 0;
673736
while (::waitpid(pid, &status, 0) < 0) { /* EINTR retry */ }
737+
mcpp::platform::unixproc::unguard_group(pid);
674738
result.exit_code = normalize_exit_code(status);
675739
return result;
676740
#else
@@ -889,11 +953,14 @@ void guard_background_on_signal(const BackgroundCommand& child) {
889953
mcpp::platform::unixproc::guard_group_on_signal(child.group);
890954
}
891955

892-
void clear_background_guard() {
956+
// Releases THIS child, not the guard as a whole: a build's ninja is guarded at
957+
// the same time as a spanning hook, and disarming everything when either
958+
// finishes would leave the other able to outlive mcpp.
959+
void clear_background_guard(const BackgroundCommand& child) {
893960
if constexpr (mcpp::platform::is_windows)
894-
mcpp::platform::winproc::clear_job_guard();
961+
mcpp::platform::winproc::unguard_job(child.job);
895962
else
896-
mcpp::platform::unixproc::clear_group_guard();
963+
mcpp::platform::unixproc::unguard_group(child.group);
897964
}
898965

899966
RunResult capture_exec_deadline(

0 commit comments

Comments
 (0)