fix(desktop): strip Windows verbatim prefix from workspace paths - #8619
Conversation
On Windows, std::fs::canonicalize returns extended-length verbatim paths (\\?\C:\dev\MathDesk). The desktop shell persisted that form to desktop-state.json and passed it as the bundled Node runtime's cwd and --workspace argument, and Node's bootstrap (resolveMainPath -> realpathSync) crashed with EISDIR: lstat 'C:', so Desktop 0.1.0 failed to start at all. Because the verbatim path is persisted, every subsequent launch crashed the same way. Use dunce::canonicalize at both canonicalization sites (start_runtime_async, resolve_workspace). It behaves like fs::canonicalize but simplifies the result to a plain drive-letter path whenever that is safe, and delegates to fs::canonicalize unchanged on non-Windows platforms. Since start_runtime_async re-canonicalizes the persisted workspace on every launch, affected installs recover on their first launch after this fix, and the simplified form is written back to desktop-state.json. dunce is already in Cargo.lock transitively via the tauri plugins, so no new code is compiled. Adds a regression test asserting resolve_workspace never returns a \\?-prefixed path. Fixes QwenLM#8615
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Thanks for the fast turnaround on #8615, @yiliang114 — the root-cause trace matches what the issue triage found, and dunce at the two canonicalization sites is the direction we'd expect. Before this can go to code review, the PR body needs to follow the PR template — right now it's free-form (## What / ## Why / ## Verification) and missing:
- Reviewer Test Plan → Tested on: the important one here. This is a Windows-only crash; please fill the OS matrix and say plainly whether you built and ran the change on Windows. The Verification section reads as "cargo test only, the crash itself not reproduced" — if that's right, state it explicitly.
- How to verify / Evidence (Before & After): what should a reviewer run or look at —
cargo test --manifest-path src-tauri/Cargo.tomloutput at minimum; for the startup crash itself, either a Windows repro or an explicit "not reproduced, needs a Windows machine". - Risk & Scope: e.g.
dunce::canonicalizekeeps the\\?\prefix when the path genuinely requires it (very long paths, unusual components) — that residual case is worth spelling out, plus a "not validated" line. - Linked Issues:
Fixes #8615is already in the body (auto-close wiring works), just move it under the heading.
Why this matters more than usual here: PR CI runs the desktop-shell cargo suite only on ubuntu-22.04, where the new regression test passes with or without the fix (Linux canonicalization never produces \\?\ paths), and the windows-latest / macos-latest jobs only run in the merge queue. So the Tested-on evidence for the platform this bug lives on has to come from the PR body itself.
Once the body is updated, re-run with @qwen-code /triage and it moves to code review.
中文说明
感谢对 #8615 的快速响应 —— 根因追踪与 issue triage 的结论一致,在两处 canonicalization 位置使用 dunce 正是预期方向。进入代码审查前,PR 描述需要先按 PR 模板 补全——目前是自由结构(## What / ## Why / ## Verification),缺少:
- Reviewer Test Plan → Tested on:本 PR 最关键的一项。这是 Windows 专属崩溃,请填写 OS 矩阵,并明确说明是否在 Windows 上构建并运行过本改动。Verification 部分读起来是"只跑了 cargo test、崩溃本身未复现"——如果确实如此,请直接写明。
- How to verify / Evidence (Before & After):reviewer 应该运行或查看什么——至少给出
cargo test --manifest-path src-tauri/Cargo.toml的输出;启动崩溃本身要么提供 Windows 复现,要么明确写"未复现,需要 Windows 环境"。 - Risk & Scope:例如
dunce::canonicalize在路径确实需要 verbatim 形式时(超长路径、特殊组成部分)仍会保留\\?\前缀——这一残留场景值得写明,并补充"未验证"项。 - Linked Issues:
Fixes #8615已在正文中(自动关闭已生效),移到该标题下即可。
为什么这次格外重要:PR CI 仅在 ubuntu-22.04 上运行 desktop-shell 的 cargo 测试套件,而新增回归测试在 Linux 上有无修复都会通过(Linux 的 canonicalization 从不产生 \\?\ 路径),windows-latest / macos-latest 任务只在 merge queue 中运行。因此这个 bug 所在平台的测试证据必须来自 PR 描述本身。
描述补全后,用 @qwen-code /triage 重新运行,即可进入代码审查。
— Qwen Code · qwen3.8-max
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
中文说明
— qwen3.8-max via Qwen Code /review (v0.21.6)
|
Refreshed this branch with the latest The previous Ubuntu failure was a timeout in CI is rerunning. No screenshot was added because this is path normalization and test-only evidence without a meaningful UI state. 中文说明已通过 merge commit |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
中文说明
已审查——无阻断问题。 建议见行内评论。
— qwen3.8-max via Qwen Code /review (v0.21.6)
|
Resolved the three active suggestion threads with no code changes in this pass.\n\n- A PR-gated Windows Rust job is a valid follow-up, but adding a new platform CI leg expands workflow scope beyond this path-normalization fix. The current workflow does not provide that Windows signal.\n- Residual Windows long-path and UNC handling remains the explicitly disclosed limitation. Rejecting those inputs would change compatibility and failure behavior, so it needs a maintainer decision.\n- The proposed shared canonicalization helper was not extracted because it is a refactor rather than a correctness requirement for this PR.\n\nCurrent PR checks are green. The focused Rust suite previously passed 23/23.\n\n \n
中文摘要\n\n本轮无代码修改。Windows Rust CI 属于合理后续,但新增平台矩阵会扩大本次路径修复范围;当前工作流并没有该 Windows 验证。Windows 长路径和 UNC 仍是 PR 已披露的限制,直接拒绝会改变兼容性与失败语义,需要维护者决策。共享 canonicalization helper 属于重构建议,本轮不采纳。当前 PR 检查为绿色,之前的定向 Rust 测试为 23/23。\n\n |
|
Implemented the maintainer-gated follow-up in
No screenshot: this is Rust path handling and CI behavior with no visual state. 中文摘要已在 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/ci.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
中文说明
已审查。 建议见行内评论。 未检查(工具限制,非阻断):the executable-script lint — .github/workflows/ci.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
— qwen3.8-max via Qwen Code /review (v0.21.6)
|
All three active review threads are addressed in 9a60c06 and resolved:
Verification: Linux cargo test 23/23, rustfmt on the changed Rust file, Prettier, actionlint, diff check, and two final reviews (C=0 / S=0). The pushed matrix will execute the Windows-only regression. @qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/ci.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
中文说明
已审查。 建议见行内评论。 未检查(工具限制,非阻断):the executable-script lint — .github/workflows/ci.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
— qwen3.8-max via Qwen Code /review (v0.21.6)
doudouOUC
left a comment
There was a problem hiding this comment.
.github/workflows/ci.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
中文说明
.github/workflows/ci.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
— kimi-k3 via Qwen Code /review (v0.21.6)
Review Summary — PR #8619PR: Diff: 5 files, +83/-26, 204 source lines Findings: 0All 14 review agents (Issue Fidelity, Correctness, Security, Reuse, Altitude, Consistency, Performance, Test Coverage, 3 adversarial personas, Removed-behavior, Cross-file tracer, Build & Test) returned zero findings. Two reverse audit rounds both returned dry with substantive evidence. Key validations:
Verdict: Comment (downgraded from Approve)CI is failing on Cost217 model calls · 13.3M input (96% cached) · 280k output · 38 min wall Reviewed with qwen-code-review-plugin (review-only mode) |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 |
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Autofix review round — PR #8619Addressed the new round-4 suggestion and re-verified the earlier findings. One commit: Feedback dispositions
VerificationCommands actually run this round:
中文说明Autofix 审查轮次 — PR #8619本轮处理了第 4 轮新增建议,并复核了此前的各项发现。提交一个 commit: 反馈处理结论
Verification(验证)本轮实际执行的命令:
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/ci.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
中文说明
未发现问题。LGTM!✅
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/ci.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
— qwen3.8-max via Qwen Code /review (v0.21.7)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/ci.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
中文说明
未发现问题。LGTM!✅
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/ci.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
— qwen3.8-max via Qwen Code /review (v0.21.7)
|
Reviewed the remaining thread against the Windows verbatim-path fix scope. The duplicate canonicalization point is a cleanup/refactor follow-up, not required for this PR, so I am resolving it without code changes. |
🔍 Maintainer local verification — verdict:
|
| cell | suite | resolve_workspace probe (10 path shapes) |
|---|---|---|
base 63a8ed43 |
22 passed / 0 failed | output set O |
head 1f7830e2 |
23 passed / 0 failed | byte-identical to O (PID-normalized diff) |
Probe shapes: plain dir, symlink→dir, ., .., trailing slash, a directory literally named \\?\C:\fake (legal on APFS — not rejected, resolves identically on both sides, so the Component::Prefix guard cannot misfire off Windows), non-UTF-8 name (error branch), file-not-dir (error branch), nonexistent (error branch), relative path.
Vacuity / mutation matrix (head, macOS)
| mutation | result | expectation | reads as |
|---|---|---|---|
| M0 unmutated control | 23/23 green | green | suite healthy |
M1 revert dunce→fs::canonicalize |
23/23 green | green | new regression test is vacuous off-Windows — the prefix can never occur there |
M2 drop ensure_supported_workspace_path call |
23/23 green | green | pinned only by the two #[cfg(windows)] tests |
| M3 positive control (flip loopback check) | 2 failed | 2 failed | suite + harness can fail |
M1 makes the PR body's "most meaningful on Windows CI" precise: the real pinning is the Windows leg this PR adds. Verified live — Desktop Shell (windows-2022) ran at head 1f7830e, 25/25 passed including all three new tests by name:
Secondary claims — all verified
duncealready transitive, no new code compiled: baseCargo.lockalready pinsdunce 1.0.5; the lock diff adds exactly one dependency edge, no package entries.- Non-Windows delegation: confirmed in the vendored source (
dunce-1.0.5/src/lib.rs:#[cfg(not(windows))] { fs::canonicalize(path) }). - Single resolve point + self-heal: exactly one canonicalize call site remains; all three workspace entries (launch restore, folder picker, restart) funnel through
start_runtime_async→ resolve →set_workspace→ spawn, so a\\?\path persisted by 0.1.0 is rewritten on next launch as claimed. - ci.yml:
actionlintclean;cargo teststep runs on both matrix legs, Linux-only steps correctly gated onrunner.os. - Trial merge into current
main(edb42039): 0 conflicts, diff stat identical to the PR (5 files, +86/−29);mainhas not touched these files since the base.
Notes (non-blocking)
- Cross-platform regression test is vacuous off-Windows (M1) — acceptable because the same PR adds the Windows leg that pins it.
- PR body's "23 tests" is the macOS/Linux count; Windows runs 25. Body already scopes this correctly.
- Not-a-directory error at the
start_runtime_asyncsite now reads"Desktop workspace is not a directory: …"(was"Workspace is not a directory: …"inline on base) — matches the string base's runtime path already produced; cosmetic.
Not covered
- Literal end-to-end Windows crash reproduction (needs a Windows machine) — taken from [Desktop 0.1.0 / Windows] Bundled runtime crashes on startup: EISDIR lstat 'C:' when opening workspace #8615 + the green Windows leg, not re-derived locally.
- Real >260-char path / UNC rejection semantics on live Windows — covered only via the two
#[cfg(windows)]tests in CI. - Exotic
duncesafety edges (drive-relative,GLOBALROOT, …) beyond what the new tests pin. - JS/TS CI legs (correctly skipped by the change filter).
Methodology
Two detached worktrees at the exact baseRefOid/headRefOid (no HEAD^1 assumptions), shared CARGO_TARGET_DIR; the probe is an identical #[cfg(test)] module appended to both trees' runtime.rs calling the crate-internal resolve_workspace, oracle = PID-normalized diff; mutations applied/restored by script with encoded expectations (exited 0); CI evidence via gh pr checks / gh run view --log. Full artifacts (harnesses, raw logs, PNGs): tmp/pr8619-verify-20260807-235644/ on the verifying machine; report + verdict.txt + assertions.json included there.
|
Released in v0.21.8. |



What this PR does
This PR replaces
std::fs::canonicalizewithdunce::canonicalizeat the two workspace-path canonicalization sites in the desktop shell:start_runtime_asyncinmain.rs(before the workspace is persisted todesktop-state.jsonand handed to the runtime spawn) andresolve_workspaceinruntime.rs(before the path becomes the child process cwd and the--workspaceargument). It addsdunceas a direct dependency (already present inCargo.locktransitively via tauri plugins, so no new code is compiled) and a regression test assertingresolve_workspacenever returns a\\?\-prefixed path.Why it's needed
Fixes #8615. On Windows,
std::fs::canonicalizereturns extended-length verbatim paths such as\\?\C:\dev\MathDesk. The desktop shell persisted that form and used it as the bundled Node runtime's cwd and--workspaceargument. Node's own bootstrap (resolveMainPath→realpathSync) interprets the\\?\prefix as a UNC path, the entry-script resolution collapses to the bare drive rootC:, and the runtime crashes on startup withEISDIR: lstat 'C:'— the app cannot start at all. Because the verbatim path is persisted indesktop-state.json, every subsequent launch crashes identically, with no self-healing path.dunce::canonicalizebehaves likefs::canonicalizebut simplifies the result back to a plain drive-letter path whenever that is safe (it keeps the verbatim form only when the path genuinely requires it), and on non-Windows platforms it delegates tofs::canonicalizeunchanged. Sincestart_runtime_asyncre-canonicalizes the persisted workspace on every launch, already-affected installs (with a\\?\path saved by Desktop 0.1.0) recover automatically on their first launch after this fix, and the simplified path is written back todesktop-state.json.Reviewer Test Plan
How to verify
From
packages/desktop-shell, runcargo test --manifest-path src-tauri/Cargo.toml. All 23 desktop-shell tests pass, including the newruntime::tests::resolve_workspace_strips_windows_verbatim_prefixregression test:Expected behavior:
resolve_workspacenever returns a\\?\-prefixed path, and on non-Windows platforms behavior is unchanged (duncedelegates tostd::fs::canonicalize). The literal crash requires Windows (\\?\verbatim prefixes are a Windows-only path form); the regression test asserts the de-verbatimized output and is most meaningful on Windows CI.Evidence (Before & After)
N/A from local — the literal crash requires a Windows machine, which is not available here; the
cargo testoutput above is the local evidence, and the change has no TUI surface (Rust-side path handling only).Tested on
macOS: desktop-shell unit suite (23/23 passed). Windows: not tested locally; the new PR-gated
windows-2022Rust job validates both safe simplification and residual verbatim-path rejection. Linux: behavior unchanged,dunce::canonicalizedelegates tostd::fs::canonicalizeon non-Windows.Environment (optional)
Unit tests only (
cargo test --manifest-path src-tauri/Cargo.tomlinpackages/desktop-shell); no app runtime needed.Risk & Scope
packages/desktopmonorepo is untouched.start_runtime_asyncre-canonicalizes the persisted workspace.Linked Issues
Fixes #8615
Related to #8400, #7139 (fixed in #7228), #8308 — investigated as duplicate candidates during triage; distinct mechanisms but the same Windows path-handling area.
中文说明
本 PR 做了什么
将 desktop shell 中两处 workspace 路径规范化从
std::fs::canonicalize替换为dunce::canonicalize:main.rs的start_runtime_async(在 workspace 持久化到desktop-state.json并传给 runtime 启动逻辑之前)和runtime.rs的resolve_workspace(在该路径成为子进程 cwd 和--workspace参数之前)。新增dunce直接依赖(它已通过 tauri 插件存在于Cargo.lock传递依赖中,不会新增编译代码),并新增回归测试,断言resolve_workspace不会返回带\\?\前缀的路径。为什么需要
修复 #8615。在 Windows 上,
std::fs::canonicalize返回扩展长度 verbatim 路径(如\\?\C:\dev\MathDesk)。desktop shell 将该形式持久化,并用作内置 Node runtime 的 cwd 和--workspace参数。Node 自身引导过程(resolveMainPath→realpathSync)将\\?\前缀解释为 UNC 路径,入口脚本解析坍缩到裸盘符根C:,runtime 启动时以EISDIR: lstat 'C:'崩溃——应用完全无法启动。由于 verbatim 路径已持久化到desktop-state.json,之后每次启动都会同样崩溃,无法自愈。dunce::canonicalize行为与fs::canonicalize相同,但会在安全时将结果简化为普通盘符路径(仅在路径确实需要时才保留 verbatim 形式),在非 Windows 平台上直接委托给fs::canonicalize,行为不变。由于start_runtime_async每次启动都会对已持久化的 workspace 重新规范化,受影响的用户(Desktop 0.1.0 已保存\\?\路径)在应用本修复后的首次启动即可自动恢复,且简化后的路径会被写回desktop-state.json。评审者测试计划
如何验证
在
packages/desktop-shell下运行cargo test --manifest-path src-tauri/Cargo.toml。desktop-shell 全部 23 个测试通过,包括新增的runtime::tests::resolve_workspace_strips_windows_verbatim_prefix回归测试(输出见英文部分)。预期行为:resolve_workspace永不返回\\?\前缀路径;非 Windows 平台行为不变。崩溃本身需要 Windows 环境(\\?\verbatim 前缀是 Windows 独有路径形式);回归测试断言去 verbatim 化的输出,在 Windows CI 上最有意义。证据(修复前后)
本地 N/A——字面崩溃需要 Windows 机器,本地不具备;上方
cargo test输出即本地证据。该改动无 TUI 界面变化(仅 Rust 侧路径处理)。测试平台
macOS:desktop-shell 单测套件(23/23 通过)。Windows:本地未做字面复现;新增的 PR 门控
windows-2022Rust 任务会验证安全去前缀和残留 verbatim 路径拒绝。Linux:行为不变,dunce::canonicalize在非 Windows 上委托给std::fs::canonicalize。环境(可选)
仅单元测试(
packages/desktop-shell下cargo test --manifest-path src-tauri/Cargo.toml),无需应用运行时。风险与范围
packages/desktopmonorepo。start_runtime_async会对已持久化的 workspace 重新规范化。关联 Issue
Fixes #8615
Related to #8400、#7139(已在 #7228 修复)、#8308——triage 阶段作为重复候选排查过;机制不同,但同属 Windows 路径处理领域。