Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 21 additions & 28 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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 侧待改进项)。
Expand Down
27 changes: 25 additions & 2 deletions protocol/src/process.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,9 @@ export RunStatus run_lines_idle(const std::string& cmd,
std::string line;
std::array<char, 4096> 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) {
Expand Down Expand Up @@ -192,10 +194,31 @@ export RunStatus run_lines_idle(const std::string& cmd,
return {static_cast<int>(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);
}
Expand Down
Loading