From 9b35e048d22a6a57992b159b648f79e1743ead3f Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 2 Aug 2026 02:21:07 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(windows):=20=E6=B4=BB=E6=80=A7=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E7=BB=88=E6=AD=A2=E5=90=8E=E4=B8=8D=E5=86=8D=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E7=A9=BA=E8=BD=AC;=E5=86=92=E7=83=9F=E5=88=A4?= =?UTF-8?q?=E6=8D=AE=E6=94=B6=E7=B4=A7=E5=88=B0=E4=B8=8E=20linux=20?= =?UTF-8?q?=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两处与「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 合入后改回默认分支。 --- .github/workflows/ci.yml | 36 ++++++++++++++++++++++++------------ protocol/src/process.cppm | 27 +++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d7ee66..8339929 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,6 +147,9 @@ jobs: uses: actions/checkout@v4 with: repository: mcpp-community/d2mcpp + # TEMP(验证用):指向 d2mcpp#87 的分支,验证 Windows 上 Provider 的 + # `2>/dev/null` 修复。该 PR 合入后改回默认分支。 + ref: sync/d2x-2026.08.02.1-mcpp-2026.8.1.1 path: d2mcpp # d2mcpp 上游仍钉在 mcpp 0.0.104,而 mcpplibs 索引已把下限抬到 0.0.109 @@ -194,22 +197,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 @@ -241,6 +250,9 @@ jobs: uses: actions/checkout@v4 with: repository: mcpp-community/d2mcpp + # TEMP(验证用):指向 d2mcpp#87 的分支,验证 Windows 上 Provider 的 + # `2>/dev/null` 修复。该 PR 合入后改回默认分支。 + ref: sync/d2x-2026.08.02.1-mcpp-2026.8.1.1 path: d2mcpp # 见 Windows job 同名步骤:d2mcpp 上游仍钉 0.0.104,会撞 mcpplibs 索引 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); } From 71652941d7e1afd34eddd048afb3202d88a17e1c Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 2 Aug 2026 03:11:32 +0800 Subject: [PATCH 2/2] =?UTF-8?q?ci:=20d2mcpp=20ref=20=E6=94=B9=E5=9B=9E?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=88=86=E6=94=AF,=E5=B9=B6=E5=88=A0?= =?UTF-8?q?=E6=8E=89=E5=B7=B2=E5=A4=B1=E6=95=88=E7=9A=84=20pin=20=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=20workaround?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 索引下限,现在课程自己已经钉对了。 --- .github/workflows/ci.yml | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8339929..695a4d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,19 +147,8 @@ jobs: uses: actions/checkout@v4 with: repository: mcpp-community/d2mcpp - # TEMP(验证用):指向 d2mcpp#87 的分支,验证 Windows 上 Provider 的 - # `2>/dev/null` 修复。该 PR 合入后改回默认分支。 - ref: sync/d2x-2026.08.02.1-mcpp-2026.8.1.1 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 @@ -173,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 @@ -250,16 +239,8 @@ jobs: uses: actions/checkout@v4 with: repository: mcpp-community/d2mcpp - # TEMP(验证用):指向 d2mcpp#87 的分支,验证 Windows 上 Provider 的 - # `2>/dev/null` 修复。该 PR 合入后改回默认分支。 - ref: sync/d2x-2026.08.02.1-mcpp-2026.8.1.1 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 侧待改进项)。