Skip to content

Commit 4d7fbc9

Browse files
authored
fix(windows): 活性超时终止后不再空转 + 冒烟判据收紧(验证 d2mcpp#87) (#35)
* fix(windows): 活性超时终止后不再可能空转;冒烟判据收紧到与 linux 一致 两处与「Windows 上 checker 卡住」直接相关。 1) run_lines_idle 终止之后可能永远空转 Windows 分支里 killed 置位后,唯一的出口是 WaitForSingleObject 变有信号。 若终止没生效(Job 纳管被拒、TerminateJobObject 被拒、进程卡在不可中断态), 循环就以 50ms 一轮永远转下去 —— 这个循环存在的意义正是防挂死,它自己挂死 是最糟的结局。改成两级兜底:5s 后补一次 TerminateProcess(Job 路径失败时 还有救),15s 后放弃等待、如实返回 idle_killed,退出码沿用 POSIX 分支被 KILL 时的 128+9。 2) Windows 冒烟判据收紧 此前 Windows 这一档不要求透出编译错误,因为 d2mcpp 的 Provider 在 capture_stdout 里给命令接了 —— cmd.exe 没有 /dev/null,报 "The system cannot find the path specified." 且 mcpp test 根本不执行。 该问题已在 mcpp-community/d2mcpp#87 修掉,这里同步收紧到与 linux 同档, 并额外断言那句 cmd 报错不再出现,防止回归静悄悄溜回来。 TEMP:两处 d2mcpp checkout 暂时指向 #87 的分支以便在真 Windows 上验证;该 PR 合入后改回默认分支。 * ci: d2mcpp ref 改回默认分支,并删掉已失效的 pin 对齐 workaround d2mcpp#87 已合入 main:Provider 的 2>/dev/null 修好了,课程侧的 mcpp pin 也同步 到 2026.8.1.1。于是两件事可以收尾: - 两处 d2mcpp checkout 的临时 ref 去掉,回到默认分支 —— 此后 Windows 的收紧判据 长期守的是 d2mcpp main,回归会立刻现形。 - 两处「Align course mcpp pin」workaround 删除:它存在的唯一理由是课程钉在 0.0.104 会撞 mcpplibs 索引下限,现在课程自己已经钉对了。
1 parent 28f3a02 commit 4d7fbc9

2 files changed

Lines changed: 46 additions & 30 deletions

File tree

.github/workflows/ci.yml

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -149,14 +149,6 @@ jobs:
149149
repository: mcpp-community/d2mcpp
150150
path: d2mcpp
151151

152-
# d2mcpp 上游仍钉在 mcpp 0.0.104,而 mcpplibs 索引已把下限抬到 0.0.109
153-
# (E0006),照原样装课程工具链会直接构建失败。这里对齐到 d2x 自己钉的
154-
# 版本——本冒烟测的是 d2x checker 跑不跑得起来,不是课程的版本钉法。
155-
# 上游 d2mcpp 跟进后可移除本步骤。
156-
- name: Align course mcpp pin with d2x (upstream d2mcpp still pins 0.0.104)
157-
shell: bash
158-
run: sed -i 's/"0\.0\.104"/"${{ env.MCPP_VERSION }}"/g' d2mcpp/.xlings.json
159-
160152
- name: Install course toolchain (per d2mcpp .xlings.json)
161153
shell: bash
162154
run: cd d2mcpp && xlings install -y
@@ -170,9 +162,9 @@ jobs:
170162
mcpp build -p d2x/buildtools
171163
mcpp run -q -p d2x/buildtools -- describe
172164
173-
# 直接跑一遍课程 Provider 的 check,把原始 NDJSON 摆进日志。Windows 上
174-
# 该链路目前拿不到 `mcpp test --message-format json` 的记录(见下一步的
175-
# 说明),留下原始输出便于上游定位,也让日后的回归有据可查
165+
# 直接跑一遍课程 Provider 的 check,把原始 NDJSON 摆进日志。这条链路
166+
# (mcpp test → Provider verdict)曾在 Windows 上整条断掉,原始输出留在
167+
# 日志里,回归时一眼能看出是哪一层没给记录
176168
- name: Provider check diagnostics (raw NDJSON)
177169
shell: bash
178170
continue-on-error: true
@@ -194,22 +186,28 @@ jobs:
194186
echo "------------------ checker output (tail) ------------------"
195187
tail -n 40 checker.out || true
196188
echo "-----------------------------------------------------------"
197-
# 判据只覆盖 d2x 自己的职责:checker 必须真的走完「读配置 → 拉起
198-
# Provider → 枚举练习 → 选中第一题 → 渲染练习页」,而不是卡在加载
199-
# 日志上。这三个串同时出现才说明页面渲染出来了
189+
# 判据已收紧到与 linux 一致:除了「渲染出第一题」,还要求真的透出课程
190+
# 的编译错误 —— 也就是 mcpp test 确实跑起来了、Provider 确实给出了
191+
# verdict
200192
#
201-
# 与 linux job 的差别是刻意的:linux 还额外要求透出课程的编译错误,
202-
# Windows 不要求 —— 课程 Provider 在 Windows 上拿不到
203-
# `mcpp test --message-format json` 的记录(上一步的原始诊断可见),
204-
# mcpp / d2mcpp 上游缺口。d2x 侧行为是正确的:协议规定「Provider
205-
# 没给 verdict 即 fail」(tests/e2e.sh 1b 钉的就是这条),页面也如实
206-
# 显示 failed。上游补齐后,这里应当收紧到与 linux 一致
193+
# 此前 Windows 这一档是放宽的,因为课程 Provider 的 capture_stdout 给
194+
# 命令尾巴接了 ` 2>/dev/null`:Windows 的 _popen 走 cmd.exe,那里没有
195+
# /dev/null,cmd 当成「重定向到 \dev\null」并报 "The system cannot find
196+
# the path specified.",整条 mcpp test 根本不执行,一条 JSON 都没有。
197+
# d2mcpp 侧修好后(mcpp-community/d2mcpp#87),这条就该和 linux 同档,
198+
# 否则同样的回归还会静悄悄溜回来
207199
if grep -q "hello-mcpp" checker.out \
208200
&& grep -q "Exercise:" checker.out \
209-
&& grep -q "Status:" checker.out; then
210-
echo "OK: checker reached and rendered the first exercise (not stuck on the loading log)"
201+
&& grep -q "Status:" checker.out \
202+
&& grep -qiE "error" checker.out; then
203+
echo "OK: checker reached and reported the first exercise (not stuck on the loading log)"
211204
else
212-
echo "FAIL: checker never rendered the first exercise within the window (stuck on load?)"
205+
echo "FAIL: checker produced no exercise build output within the window (stuck on load?)"
206+
exit 1
207+
fi
208+
# Provider 真的跑到了 mcpp test —— 这句 cmd 报错出现即代表回归复现
209+
if grep -q "cannot find the path specified" checker.out; then
210+
echo "FAIL: Provider 又踩到 cmd.exe 的 /dev/null 重定向(d2mcpp 回归)"
213211
exit 1
214212
fi
215213
@@ -243,11 +241,6 @@ jobs:
243241
repository: mcpp-community/d2mcpp
244242
path: d2mcpp
245243

246-
# 见 Windows job 同名步骤:d2mcpp 上游仍钉 0.0.104,会撞 mcpplibs 索引
247-
# 下限(E0006)。对齐到 d2x 自己钉的版本,上游跟进后可移除。
248-
- name: Align course mcpp pin with d2x (upstream d2mcpp still pins 0.0.104)
249-
run: sed -i 's/"0\.0\.104"/"${{ env.MCPP_VERSION }}"/g' d2mcpp/.xlings.json
250-
251244
# 按课程自己的 .xlings.json 安装 mcpp——xlings 的 workspace-pin shim
252245
# 解析只认经该路径安装的版本(全局 `xlings install mcpp@X` 不满足,
253246
# CI 实测 "version not found";已知 xlings 侧待改进项)。

protocol/src/process.cppm

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,9 @@ export RunStatus run_lines_idle(const std::string& cmd,
146146
std::string line;
147147
std::array<char, 4096> buffer{};
148148
auto last_output = std::chrono::steady_clock::now();
149-
bool killed = false;
149+
bool killed = false;
150+
bool escalated = false;
151+
std::chrono::steady_clock::time_point kill_at{};
150152

151153
auto emit = [&](DWORD n) {
152154
for (DWORD i = 0; i < n; ++i) {
@@ -192,10 +194,31 @@ export RunStatus run_lines_idle(const std::string& cmd,
192194
return {static_cast<int>(code), killed};
193195
}
194196

195-
if (!killed && std::chrono::steady_clock::now() - last_output > idle) {
197+
auto now = std::chrono::steady_clock::now();
198+
199+
if (!killed && now - last_output > idle) {
196200
if (job) ::TerminateJobObject(job, 1); // 整棵进程树
197201
else ::TerminateProcess(pi.hProcess, 1);
198202
killed = true;
203+
kill_at = now;
204+
}
205+
206+
// 终止之后进程仍不退出,就不能一直等下去 —— 这个循环存在的意义就是
207+
// 防止挂死,它自己挂死是最糟的结局。分两级兜底:先补一次直杀(Job
208+
// 路径被拒时还有救),再到点就放弃等待、如实返回 idle_killed,由上层
209+
// 报「Provider 被终止」。退出码沿用 POSIX 分支被 KILL 时的 128+9。
210+
if (killed) {
211+
auto since_kill = now - kill_at;
212+
if (!escalated && since_kill > std::chrono::seconds(5)) {
213+
::TerminateProcess(pi.hProcess, 1);
214+
escalated = true;
215+
}
216+
if (since_kill > std::chrono::seconds(15)) {
217+
::CloseHandle(pi.hProcess);
218+
::CloseHandle(rd);
219+
if (job) ::CloseHandle(job); // KILL_ON_JOB_CLOSE 再补一刀
220+
return {137, true};
221+
}
199222
}
200223
::Sleep(50);
201224
}

0 commit comments

Comments
 (0)