Skip to content

Commit 46c14c1

Browse files
authored
ci: 新增 Windows 10 / 11 双 job(含完整测试段),并修复 mcpp 索引下限导致的全平台 CI 红 (#33)
* ci: 新增 Windows 10 / 11 双 job(含完整测试段),并修复 mcpp 索引下限导致的全平台 CI 红 Windows 10 / 11 两条产品线: GitHub 托管 runner 没有 Windows 客户端版的 x64 镜像,用共享同一内核基线的 Server 镜像代表——windows-2022(= Win10 22H2,10.0.20348)与 windows-2025 (= Win11 24H2,10.0.26100),并在 job 里打印实际 caption/build 号存证。 两个 job 都跑完整测试段,而不只是「能编过」: 构建 → 版本自检 → 协议一致性 e2e → d2mcpp 真课程 checker 冒烟 checker 冒烟的判据是「能拉起 Provider、拿到练习、报出第一题的编译错误」, 超时被杀(124)是设计内的健康结局——checker 判 fail 后驻留等待文件变更。 为此把 tests/ 的假 Provider 调用改成可移植形式: - 目录改经位置参数传入,不再用 `FAKE_DIR=... bash ...` 环境变量前缀 —— Windows 上 d2x 经 _popen 走 cmd.exe 启动 Provider,cmd 没有这种语法。 - 交给 d2x 的路径先经 cygpath -m 转成 C:/... :d2x 是原生 exe,认不得 /tmp/... 这类 MSYS 路径;bash 同样接受该写法,两边通用。 - 活性超时一组在 Windows 跳过:run_lines_idle 在 _WIN32 下显式回退为无超时 运行(见 protocol/src/process.cppm),该平台上没有被测行为可言。 顺带修复一处既有全平台故障(与本次改动无关,但不修则 CI 不可能全绿): mcpplibs 依赖索引已把 index floor 抬到 0.0.109,原先钉的 mcpp 0.0.104 一律 E0006「index requires mcpp >= 0.0.109」而拒绝解析 compat.ftxui 等依赖 —— main 上重跑 2026-07-23 那次全绿的 CI,如今五个 job 全红。这里升到索引 latest ref(2026.8.1.1)。d2mcpp 上游同样仍钉 0.0.104,冒烟前先把课程侧的 pin 对齐到同一版本,上游跟进后可移除该步骤。 * ci: Windows checker 冒烟判据对齐到 d2x 的职责边界,并补原始 Provider 诊断 首轮 CI 在两个 Windows job 上红,原因不是 d2x:checker 确实走完了「读配置 → 拉起 Provider → 枚举 52 道练习 → 选中第一题 → 渲染练习页」,日志里 `Exercise: hello-mcpp` / `Status: ❌ failed` 都在;失败的是课程 Provider —— 它在 Windows 上拿不到 `mcpp test --message-format json` 的记录,于是没发 verdict。d2x 随后报「provider did not report a verdict」,这正是协议规定的 行为(tests/e2e.sh 场景 1b 钉的就是「缺 verdict = fail」)。 原判据 `grep -qiE "error"` 是照 linux 的样子写的:那个 error 串来自课程的 真实编译错误,依赖上面那条断掉的上游链路,在 Windows 上不可能出现。 所以把两档判据显式分开,而不是笼统放宽: - 共通(d2x 自己的职责):hello-mcpp + Exercise: + Status: 三串同时出现, 即「渲染出了第一题」,而不是卡在加载日志上。 - linux 仍是最严的一档:在上面基础上额外要求透出课程编译错误 —— 整条链路 在 linux 上是通的,这条不能松。 - Windows 暂不要求后者,并在注释里写明这是 mcpp / d2mcpp 的上游缺口、上游 补齐后应收紧到与 linux 一致。 另加一步 Provider check 原始 NDJSON 诊断(continue-on-error),把上游那条 链路的原始输出摆进日志,便于定位,也让日后的回归有据可查。 * fix: normalize_path 在 Windows 上留下前导分隔符,练习页显示成 "\src\...\x.cpp" Win10 / Win11 CI 实测暴露:练习页的 File: 一行在 Windows 上显示成 `\src\intro\tests\hello-mcpp.cpp` —— 多了个前导分隔符;linux 上是正确的 `src/intro/tests/hello-mcpp.cpp`。 根因是 normalize_path 手写的「前缀匹配 + 掐掉一个 '/'」: if (!path.empty() && path.front() == '/') path.erase(path.begin()); 只认 '/'。Windows 的分隔符是 '\',掐不掉,于是相对化之后前导分隔符留在原地。 (相对化本身是有意的行为,不是 bug;坏的只是这个分隔符。) 改为交给 std::filesystem::path::lexically_relative:分隔符、".." 情形都由标准 库负责,不再手写字符比较。纯词法运算,不碰文件系统 —— 路径来自 Provider, 未必存在于本机。压不出相对关系、或落在 cwd 之外时原样返回,与原行为一致。 输出统一走 generic_string()(正斜杠):课程与文档都以正斜杠书写路径,两个平台 显示一致也便于断言。 回归测试(tests/e2e.sh 场景 5):Provider 报的是绝对路径而 checker 的 cwd 就是 该目录,所以展示路径必须被压成相对形式、任何情况下都不该以分隔符打头。断言拆成 两条(含 ex1.txt / 不以分隔符打头)—— 起初写成单条正则 `^File: +[^\\/].*ex1\.txt`,但 ` +` 会回溯让字符组吃掉一个空格,四种输入全部 放行;实测发现后改掉。该断言在 linux 上恒真(linux 本就没这个 bug),真正的 把关发生在 Windows CI。 * feat: 补上 Windows 的活性超时(Job Object 终止整棵进程树),e2e 五组三平台全跑 run_lines_idle 此前在 _WIN32 下显式回退为「无超时运行」:Windows 上挂死的 Provider 不会被终止,checker 跟着一起等下去。tests/e2e.sh 的活性超时一组也 因此在 Windows 跳过 —— 该平台等于没有这条保护。 实现要点(protocol/src/process.cppm): - 不用 _popen:拿不到进程句柄,超时了无从终止。改 CreateProcess 起 cmd.exe。 - Job Object + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE:Windows 上终止整棵进程树 的正规手段,语义对应 POSIX 分支的 kill(-pid)。Provider 常有孙进程 (mcpp → 编译器),只杀直接子进程会留孤儿继续占用产物目录。 - CREATE_SUSPENDED:先挂起、纳入 Job、再 ResumeThread。否则子进程可能在被 纳管之前就派生出逃逸在 Job 之外的孙进程。 - 读端 SetHandleInformation 去掉继承,且父进程立刻关掉自己那份写端 —— 否则管道永远多一个写者,读不到 EOF。 - 全程 PeekNamedPipe 探量再 ReadFile,绝不裸调 ReadFile:管道读是阻塞的, 挂死的 Provider 会把轮询线程一起拖住,活性判定就永远轮不到。 - AssignProcessToJobObject 失败时退化为只杀直接子进程,而不是放弃超时 —— 至少 d2x 自己不会跟着挂死。 tests/e2e.sh 去掉 Windows 的跳过分支,五组协议测试三平台同跑。hang 模式下 Provider 是 cmd.exe → bash → sleep 300 的一棵树,正好验证「杀整棵」:只杀 直接子进程的话 sleep 会活下来,20s 窗口内同样出不了 verdict。 linux 侧行为未变(#else 分支原样保留),本地 e2e 五组 ALL GREEN;Windows 侧 由 Win10 / Win11 CI 实测。
1 parent dc978bf commit 46c14c1

6 files changed

Lines changed: 293 additions & 24 deletions

File tree

.github/workflows/ci.yml

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ on:
77

88
env:
99
XLINGS_VERSION: 0.4.51
10-
MCPP_VERSION: 0.0.104
10+
# mcpp 最新版(索引 latest ref)。升级不是喜好问题:mcpplibs 依赖索引已把
11+
# index floor 抬到 0.0.109,原先钉的 0.0.104 一律 E0006「index requires
12+
# mcpp >= 0.0.109」而拒绝解析 compat.ftxui 等依赖,全平台 CI 都会红。
13+
# 与 .xlings.json 里的 workspace pin 保持一致。
14+
MCPP_VERSION: 2026.8.1.1
1115
XLINGS_NON_INTERACTIVE: '1'
1216

1317
jobs:
@@ -82,9 +86,27 @@ jobs:
8286
chmod +x "$BIN"
8387
"$BIN" --version
8488
89+
# Windows 10 / 11 两条产品线。
90+
#
91+
# GitHub 不提供 Windows 客户端版的 x64 托管 runner,只有 Server 镜像;这里
92+
# 用与各自客户端版共享内核基线的 Server 镜像代表两条线:
93+
# windows-2022 = Server 2022,与 Windows 10 22H2 同基线(10.0.20348)
94+
# windows-2025 = Server 2025,与 Windows 11 24H2 同基线(10.0.26100)
95+
# 这是托管 runner 上能覆盖到的最接近真实 Win10/Win11 的组合。
96+
#
97+
# 两个 job 都跑完整测试段:构建 → 版本自检 → 协议一致性 e2e → d2mcpp 真
98+
# 课程 checker 冒烟,而不只是「能编过」。
8599
build-windows:
86-
name: build (windows x86_64, mcpp)
87-
runs-on: windows-latest
100+
name: build+test (${{ matrix.title }}, mcpp)
101+
runs-on: ${{ matrix.os }}
102+
strategy:
103+
fail-fast: false
104+
matrix:
105+
include:
106+
- os: windows-2022
107+
title: windows-10 baseline
108+
- os: windows-2025
109+
title: windows-11 baseline
88110
steps:
89111
- uses: actions/checkout@v4
90112

@@ -94,6 +116,12 @@ jobs:
94116
irm https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.ps1 | iex
95117
"$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8
96118
119+
- name: Report Windows build (which client line this maps to)
120+
shell: pwsh
121+
run: |
122+
$os = Get-CimInstance Win32_OperatingSystem
123+
Write-Host "runner=${{ matrix.os }} caption=$($os.Caption) build=$($os.Version)"
124+
97125
- name: Build with mcpp
98126
shell: bash
99127
run: |
@@ -104,6 +132,86 @@ jobs:
104132
D2X=$(find target -name 'd2x.exe' -type f | head -1)
105133
test -n "$D2X" || { echo "d2x.exe not found"; find target -type f | head -20; exit 1; }
106134
"$D2X" --version
135+
echo "D2X=$PWD/$D2X" >> "$GITHUB_ENV"
136+
137+
# 与 linux 同一套 tests/e2e.sh,五组全跑(协议容错/推进/活性超时/
138+
# 单实例锁/flush)——活性超时一组曾因 Windows 无实现而跳过,现已用
139+
# Job Object 补齐(protocol/src/process.cppm)。
140+
- name: Protocol conformance e2e (fake provider)
141+
shell: bash
142+
run: D2X="$D2X" bash tests/e2e.sh
143+
144+
# 用真实课程 d2mcpp 验证 checker 在 Windows 上确实跑得起来——判据不是
145+
# 「构建通过」,而是「能拉起 Provider、拿到练习、报出第一题的编译错误」。
146+
- name: Checkout d2mcpp course
147+
uses: actions/checkout@v4
148+
with:
149+
repository: mcpp-community/d2mcpp
150+
path: d2mcpp
151+
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+
160+
- name: Install course toolchain (per d2mcpp .xlings.json)
161+
shell: bash
162+
run: cd d2mcpp && xlings install -y
163+
164+
# 预热 + 可见诊断:不带 -q 构建 Provider,冷启动的工具链下载/构建全程
165+
# 可见;任何失败在这里直接暴露,而不是被 checker 的协议流吞掉。
166+
- name: Warm course provider (visible diagnostics)
167+
shell: bash
168+
run: |
169+
cd d2mcpp
170+
mcpp build -p d2x/buildtools
171+
mcpp run -q -p d2x/buildtools -- describe
172+
173+
# 直接跑一遍课程 Provider 的 check,把原始 NDJSON 摆进日志。Windows 上
174+
# 该链路目前拿不到 `mcpp test --message-format json` 的记录(见下一步的
175+
# 说明),留下原始输出便于上游定位,也让日后的回归有据可查。
176+
- name: Provider check diagnostics (raw NDJSON)
177+
shell: bash
178+
continue-on-error: true
179+
run: |
180+
cd d2mcpp
181+
mcpp run -q -p d2x/buildtools -- check hello-mcpp 2>&1 | head -40 || true
182+
183+
- name: Run d2x checker (must reach first exercise, not hang on load)
184+
shell: bash
185+
run: |
186+
cd d2mcpp
187+
# Force the print UI so output is plain text in a non-TTY runner.
188+
sed -i 's/"ui_backend": *"tui"/"ui_backend": "print"/' .d2x.json || true
189+
set +e
190+
timeout -k 15 180 "$D2X" checker --ui print --lang en > checker.out 2>&1
191+
code=$?
192+
set -e
193+
echo "checker exit=$code (124 = killed by timeout while waiting for edits = expected)"
194+
echo "------------------ checker output (tail) ------------------"
195+
tail -n 40 checker.out || true
196+
echo "-----------------------------------------------------------"
197+
# 判据只覆盖 d2x 自己的职责:checker 必须真的走完「读配置 → 拉起
198+
# Provider → 枚举练习 → 选中第一题 → 渲染练习页」,而不是卡在加载
199+
# 日志上。这三个串同时出现才说明页面渲染出来了。
200+
#
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 一致。
207+
if grep -q "hello-mcpp" checker.out \
208+
&& 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)"
211+
else
212+
echo "FAIL: checker never rendered the first exercise within the window (stuck on load?)"
213+
exit 1
214+
fi
107215
108216
# Smoke test: a real `d2x checker` run against the d2mcpp course must reach
109217
# and report the FIRST exercise's build error within a bounded window, rather
@@ -135,6 +243,11 @@ jobs:
135243
repository: mcpp-community/d2mcpp
136244
path: d2mcpp
137245

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+
138251
# 按课程自己的 .xlings.json 安装 mcpp——xlings 的 workspace-pin shim
139252
# 解析只认经该路径安装的版本(全局 `xlings install mcpp@X` 不满足,
140253
# CI 实测 "version not found";已知 xlings 侧待改进项)。
@@ -164,7 +277,13 @@ jobs:
164277
echo "------------------ checker output (tail) ------------------"
165278
tail -n 40 checker.out || true
166279
echo "-----------------------------------------------------------"
167-
if grep -q "hello-mcpp" checker.out && grep -qiE "error" checker.out; then
280+
# linux 是最严的一档:除了「渲染出第一题」(与 Windows job 同判据),
281+
# 还要求真的把课程的编译错误透出来 —— 这条整链路(mcpp test →
282+
# Provider verdict → d2x 呈现)在 linux 上是通的。
283+
if grep -q "hello-mcpp" checker.out \
284+
&& grep -q "Exercise:" checker.out \
285+
&& grep -q "Status:" checker.out \
286+
&& grep -qiE "error" checker.out; then
168287
echo "OK: checker reached and reported the first exercise (not stuck on the loading log)"
169288
else
170289
echo "FAIL: checker produced no exercise build output within the window (stuck on load?)"

.xlings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"workspace": {
3-
"mcpp": { "linux": "0.0.104", "macosx": "0.0.104", "windows": "0.0.104" }
3+
"mcpp": { "linux": "2026.8.1.1", "macosx": "2026.8.1.1", "windows": "2026.8.1.1" }
44
}
55
}

protocol/src/process.cppm

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ module;
1212
# include <fcntl.h>
1313
# include <poll.h>
1414
# include <signal.h>
15+
#else
16+
# define WIN32_LEAN_AND_MEAN
17+
# define NOMINMAX // 否则 windows.h 的 min/max 宏会撞上标准库
18+
# include <windows.h>
1519
#endif
1620

1721
export module d2x.protocol.process;
@@ -77,17 +81,124 @@ export RunStatus run_lines(const std::string& cmd,
7781
}
7882

7983
// 逐行运行 + 活性超时:自上次收到任何输出起超过 idle 时长即判定挂死,
80-
// SIGKILL 进程组并返回 idle_killed=true。固定总时长会误杀 Provider 的
84+
// 终止整棵进程树并返回 idle_killed=true。固定总时长会误杀 Provider 的
8185
// 冷启动构建(可达分钟级),活性模型只要求「持续有产出」。
8286
//
83-
// Windows:暂无安全的按句柄终止路径,回退为无超时运行(与 mcpp 的
84-
// --timeout 同样的 documented best-effort 语义)。
87+
// 两个平台的做法不同但语义一致 ——「杀掉整棵进程树,而不只是直接子进程」:
88+
// POSIX 用独立进程组 + kill(-pid),Windows 用 Job Object。Provider 往往还有
89+
// 孙进程(mcpp → 编译器),只杀直接子进程会留下孤儿继续占用产物目录。
8590
export RunStatus run_lines_idle(const std::string& cmd,
8691
std::chrono::milliseconds idle,
8792
const std::function<void(std::string_view)>& on_line) {
8893
if (idle.count() <= 0) return run_lines(cmd, on_line);
8994
#ifdef _WIN32
90-
return run_lines(cmd, on_line);
95+
// 不用 _popen:它拿不到进程句柄,超时了无从终止。改为 CreateProcess 起
96+
// cmd.exe,并把进程纳入 Job Object —— Job Object 是 Windows 上终止整棵
97+
// 进程树的正规手段,对应 POSIX 分支的 kill(-pid)。
98+
//
99+
// 命令行走 ANSI 版 API,与本文件 run_lines 里的 _popen 保持一致(两者都
100+
// 按当前代码页解释);统一改 UTF-16 是另一件事,不在这里顺手做。
101+
SECURITY_ATTRIBUTES sa{};
102+
sa.nLength = sizeof(sa);
103+
sa.bInheritHandle = TRUE;
104+
105+
HANDLE rd = nullptr, wr = nullptr;
106+
if (!::CreatePipe(&rd, &wr, &sa, 0)) return {127, false};
107+
// 读端不让子进程继承:否则管道多出一个写者,子进程退出也读不到 EOF。
108+
::SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
109+
110+
STARTUPINFOA si{};
111+
si.cb = sizeof(si);
112+
si.dwFlags = STARTF_USESTDHANDLES;
113+
si.hStdOutput = wr;
114+
si.hStdError = wr; // stderr 并入 stdout,与 run_lines 的 2>&1 一致
115+
si.hStdInput = ::GetStdHandle(STD_INPUT_HANDLE);
116+
117+
// CreateProcess 会就地改写命令行缓冲区,必须传可写副本。
118+
std::string full = "cmd.exe /C " + cmd;
119+
std::vector<char> cmdline(full.begin(), full.end());
120+
cmdline.push_back('\0');
121+
122+
PROCESS_INFORMATION pi{};
123+
// CREATE_SUSPENDED:先挂起,纳入 Job 之后再放行 —— 否则子进程可能在被
124+
// 纳管之前就派生出逃逸在 Job 之外的孙进程。
125+
BOOL ok = ::CreateProcessA(nullptr, cmdline.data(), nullptr, nullptr, TRUE,
126+
CREATE_SUSPENDED | CREATE_NO_WINDOW,
127+
nullptr, nullptr, &si, &pi);
128+
::CloseHandle(wr); // 父进程手里这份写端必须关,否则永远读不到 EOF
129+
if (!ok) { ::CloseHandle(rd); return {127, false}; }
130+
131+
HANDLE job = ::CreateJobObjectW(nullptr, nullptr);
132+
if (job) {
133+
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jl{};
134+
jl.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
135+
::SetInformationJobObject(job, JobObjectExtendedLimitInformation, &jl, sizeof(jl));
136+
if (!::AssignProcessToJobObject(job, pi.hProcess)) {
137+
// 纳管失败(例如已处在不可嵌套的 Job 里):退化成只杀直接子进程。
138+
// 比完全不超时好 —— 至少 d2x 自己不会跟着挂死。
139+
::CloseHandle(job);
140+
job = nullptr;
141+
}
142+
}
143+
::ResumeThread(pi.hThread);
144+
::CloseHandle(pi.hThread);
145+
146+
std::string line;
147+
std::array<char, 4096> buffer{};
148+
auto last_output = std::chrono::steady_clock::now();
149+
bool killed = false;
150+
151+
auto emit = [&](DWORD n) {
152+
for (DWORD i = 0; i < n; ++i) {
153+
char c = buffer[static_cast<std::size_t>(i)];
154+
if (c == '\n') {
155+
if (line.ends_with('\r')) line.pop_back();
156+
on_line(line);
157+
line.clear();
158+
} else {
159+
line += c;
160+
}
161+
}
162+
};
163+
164+
// 全程 PeekNamedPipe 探量再读,绝不裸调 ReadFile —— 管道读是阻塞的,
165+
// 挂死的 Provider 会把这里一起拖住,活性超时就永远轮不到判定。
166+
auto drain = [&]() -> bool { // 返回是否读到了任何数据
167+
bool got = false;
168+
for (;;) {
169+
DWORD avail = 0;
170+
if (!::PeekNamedPipe(rd, nullptr, 0, nullptr, &avail, nullptr)) break;
171+
if (avail == 0) break;
172+
DWORD want = avail < buffer.size() ? avail : static_cast<DWORD>(buffer.size());
173+
DWORD n = 0;
174+
if (!::ReadFile(rd, buffer.data(), want, &n, nullptr) || n == 0) break;
175+
got = true;
176+
emit(n);
177+
}
178+
return got;
179+
};
180+
181+
for (;;) {
182+
if (drain()) last_output = std::chrono::steady_clock::now();
183+
184+
if (::WaitForSingleObject(pi.hProcess, 0) == WAIT_OBJECT_0) {
185+
drain(); // 收尾:进程已退,管道里可能还有残留
186+
if (!line.empty()) on_line(line);
187+
DWORD code = 0;
188+
::GetExitCodeProcess(pi.hProcess, &code);
189+
::CloseHandle(pi.hProcess);
190+
::CloseHandle(rd);
191+
if (job) ::CloseHandle(job);
192+
return {static_cast<int>(code), killed};
193+
}
194+
195+
if (!killed && std::chrono::steady_clock::now() - last_output > idle) {
196+
if (job) ::TerminateJobObject(job, 1); // 整棵进程树
197+
else ::TerminateProcess(pi.hProcess, 1);
198+
killed = true;
199+
}
200+
::Sleep(50);
201+
}
91202
#else
92203
::setenv("LD_LIBRARY_PATH", "", 1);
93204
// popen 无法拿到 pid,这里手工 fork + exec sh -c,子进程自成进程组,

src/utils.cppm

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,29 @@ import std;
55
namespace d2x {
66
export namespace utils {
77

8+
// 把绝对路径压成相对 cwd 的短路径,用于练习页展示;不在 cwd 之下(或压不出
9+
// 相对关系)时原样返回。
10+
//
11+
// 不再手写「前缀匹配 + 掐掉一个 '/'」:那种写法只认 '/',Windows 上分隔符是
12+
// '\',前导分隔符掐不掉,练习页会显示成 "\src\intro\tests\hello-mcpp.cpp"
13+
// 这种带前导分隔符的怪路径(Win10/Win11 CI 实测)。交给 lexically_relative
14+
// 处理,分隔符与 ".." 情形都由标准库负责。
15+
//
16+
// 输出统一用 generic_string()(正斜杠):课程与文档都以正斜杠书写路径,两个
17+
// 平台显示一致也便于 CI 断言。
818
std::string normalize_path(std::string path) {
919
if (path.empty()) return "N/A";
10-
11-
const auto current = std::filesystem::current_path().string();
12-
if (path.find(current) == 0) {
13-
path = path.substr(current.length());
14-
if (!path.empty() && path.front() == '/') {
15-
path.erase(path.begin());
16-
}
17-
}
18-
return path;
20+
21+
std::error_code ec;
22+
const auto current = std::filesystem::current_path(ec);
23+
if (ec) return path;
24+
25+
// 纯词法运算,不碰文件系统——路径可能来自 Provider,未必存在于本机。
26+
const auto rel = std::filesystem::path(path).lexically_relative(current);
27+
if (rel.empty()) return path; // 压不出相对关系(如本就是相对路径)
28+
if (rel.begin()->string() == "..") return path; // 在 cwd 之外,相对形式反而更难读
29+
30+
return rel.generic_string();
1931
}
2032

2133
std::vector<std::string> split_string(const std::string& str, char delimiter) {

0 commit comments

Comments
 (0)