diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d7ee66..695a4d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,14 +149,6 @@ jobs: repository: mcpp-community/d2mcpp path: d2mcpp - # d2mcpp 上游仍钉在 mcpp 0.0.104,而 mcpplibs 索引已把下限抬到 0.0.109 - # (E0006),照原样装课程工具链会直接构建失败。这里对齐到 d2x 自己钉的 - # 版本——本冒烟测的是 d2x checker 跑不跑得起来,不是课程的版本钉法。 - # 上游 d2mcpp 跟进后可移除本步骤。 - - name: Align course mcpp pin with d2x (upstream d2mcpp still pins 0.0.104) - shell: bash - run: sed -i 's/"0\.0\.104"/"${{ env.MCPP_VERSION }}"/g' d2mcpp/.xlings.json - - name: Install course toolchain (per d2mcpp .xlings.json) shell: bash run: cd d2mcpp && xlings install -y @@ -170,9 +162,9 @@ jobs: mcpp build -p d2x/buildtools mcpp run -q -p d2x/buildtools -- describe - # 直接跑一遍课程 Provider 的 check,把原始 NDJSON 摆进日志。Windows 上 - # 该链路目前拿不到 `mcpp test --message-format json` 的记录(见下一步的 - # 说明),留下原始输出便于上游定位,也让日后的回归有据可查。 + # 直接跑一遍课程 Provider 的 check,把原始 NDJSON 摆进日志。这条链路 + # (mcpp test → Provider verdict)曾在 Windows 上整条断掉,原始输出留在 + # 日志里,回归时一眼能看出是哪一层没给记录。 - name: Provider check diagnostics (raw NDJSON) shell: bash continue-on-error: true @@ -194,22 +186,28 @@ jobs: echo "------------------ checker output (tail) ------------------" tail -n 40 checker.out || true echo "-----------------------------------------------------------" - # 判据只覆盖 d2x 自己的职责:checker 必须真的走完「读配置 → 拉起 - # Provider → 枚举练习 → 选中第一题 → 渲染练习页」,而不是卡在加载 - # 日志上。这三个串同时出现才说明页面渲染出来了。 + # 判据已收紧到与 linux 一致:除了「渲染出第一题」,还要求真的透出课程 + # 的编译错误 —— 也就是 mcpp test 确实跑起来了、Provider 确实给出了 + # verdict。 # - # 与 linux job 的差别是刻意的:linux 还额外要求透出课程的编译错误, - # Windows 不要求 —— 课程 Provider 在 Windows 上拿不到 - # `mcpp test --message-format json` 的记录(上一步的原始诊断可见), - # 属 mcpp / d2mcpp 上游缺口。d2x 侧行为是正确的:协议规定「Provider - # 没给 verdict 即 fail」(tests/e2e.sh 1b 钉的就是这条),页面也如实 - # 显示 failed。上游补齐后,这里应当收紧到与 linux 一致。 + # 此前 Windows 这一档是放宽的,因为课程 Provider 的 capture_stdout 给 + # 命令尾巴接了 ` 2>/dev/null`:Windows 的 _popen 走 cmd.exe,那里没有 + # /dev/null,cmd 当成「重定向到 \dev\null」并报 "The system cannot find + # the path specified.",整条 mcpp test 根本不执行,一条 JSON 都没有。 + # d2mcpp 侧修好后(mcpp-community/d2mcpp#87),这条就该和 linux 同档, + # 否则同样的回归还会静悄悄溜回来。 if grep -q "hello-mcpp" checker.out \ && grep -q "Exercise:" checker.out \ - && grep -q "Status:" checker.out; then - echo "OK: checker reached and rendered the first exercise (not stuck on the loading log)" + && grep -q "Status:" checker.out \ + && grep -qiE "error" checker.out; then + echo "OK: checker reached and reported the first exercise (not stuck on the loading log)" else - echo "FAIL: checker never rendered the first exercise within the window (stuck on load?)" + echo "FAIL: checker produced no exercise build output within the window (stuck on load?)" + exit 1 + fi + # Provider 真的跑到了 mcpp test —— 这句 cmd 报错出现即代表回归复现 + if grep -q "cannot find the path specified" checker.out; then + echo "FAIL: Provider 又踩到 cmd.exe 的 /dev/null 重定向(d2mcpp 回归)" exit 1 fi @@ -243,11 +241,6 @@ jobs: repository: mcpp-community/d2mcpp path: d2mcpp - # 见 Windows job 同名步骤:d2mcpp 上游仍钉 0.0.104,会撞 mcpplibs 索引 - # 下限(E0006)。对齐到 d2x 自己钉的版本,上游跟进后可移除。 - - name: Align course mcpp pin with d2x (upstream d2mcpp still pins 0.0.104) - run: sed -i 's/"0\.0\.104"/"${{ env.MCPP_VERSION }}"/g' d2mcpp/.xlings.json - # 按课程自己的 .xlings.json 安装 mcpp——xlings 的 workspace-pin shim # 解析只认经该路径安装的版本(全局 `xlings install mcpp@X` 不满足, # CI 实测 "version not found";已知 xlings 侧待改进项)。 diff --git a/protocol/src/process.cppm b/protocol/src/process.cppm index 93cfac8..a000960 100644 --- a/protocol/src/process.cppm +++ b/protocol/src/process.cppm @@ -146,7 +146,9 @@ export RunStatus run_lines_idle(const std::string& cmd, std::string line; std::array buffer{}; auto last_output = std::chrono::steady_clock::now(); - bool killed = false; + bool killed = false; + bool escalated = false; + std::chrono::steady_clock::time_point kill_at{}; auto emit = [&](DWORD n) { for (DWORD i = 0; i < n; ++i) { @@ -192,10 +194,31 @@ export RunStatus run_lines_idle(const std::string& cmd, return {static_cast(code), killed}; } - if (!killed && std::chrono::steady_clock::now() - last_output > idle) { + auto now = std::chrono::steady_clock::now(); + + if (!killed && now - last_output > idle) { if (job) ::TerminateJobObject(job, 1); // 整棵进程树 else ::TerminateProcess(pi.hProcess, 1); killed = true; + kill_at = now; + } + + // 终止之后进程仍不退出,就不能一直等下去 —— 这个循环存在的意义就是 + // 防止挂死,它自己挂死是最糟的结局。分两级兜底:先补一次直杀(Job + // 路径被拒时还有救),再到点就放弃等待、如实返回 idle_killed,由上层 + // 报「Provider 被终止」。退出码沿用 POSIX 分支被 KILL 时的 128+9。 + if (killed) { + auto since_kill = now - kill_at; + if (!escalated && since_kill > std::chrono::seconds(5)) { + ::TerminateProcess(pi.hProcess, 1); + escalated = true; + } + if (since_kill > std::chrono::seconds(15)) { + ::CloseHandle(pi.hProcess); + ::CloseHandle(rd); + if (job) ::CloseHandle(job); // KILL_ON_JOB_CLOSE 再补一刀 + return {137, true}; + } } ::Sleep(50); }