fix(core): pass --no-ask-password to systemd-inhibit to prevent TUI corruption - #5318
Conversation
1539d9f to
b7d448a
Compare
|
@qwen-code /triage |
|
Thanks for the PR, @OrbitZore! Template looks good ✓ — all required sections present, bilingual, clear test plan. Direction: This is a straightforward bug fix. Issue #5281 reports that Approach: The scope is minimal and focused — two files, ~145 net lines added, no unrelated changes. The feature-detection-via- Moving on to code review. 🔍 中文说明感谢 PR,@OrbitZore! 模板完整 ✓ — 所有必需章节齐全,双语,测试计划清晰。 方向: 这是一个直接的 bug 修复。Issue #5281 报告在带有桌面环境的 Linux 系统上通过 SSH 运行时, 方案: 范围最小且聚焦 — 两个文件,净增约 145 行,无无关改动。通过 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewIndependent proposal (before reading the diff): To fix polkit auth prompts from Comparison with the diff: The PR's approach matches my independent proposal exactly. The implementation is clean — No critical blockers found. No AGENTS.md violations. The code is straightforward — no over-abstraction, no unnecessary duplication. The Minor observation (not a blocker): The PR description says "18 tests" but there are actually 21 (the Unit TestsAll 21/21 tests pass in Real-Scenario TestingThis bug requires a Linux system with a desktop environment accessed over SSH — the polkit auth prompt only triggers when Smoke test — basic Before (installed build, v0.18.3)After (this PR, dev build)Both builds produce expected output with no errors. The sleep inhibitor code path is not exercised in this environment (headless, no DE) — Note: Full verification of this fix requires a reviewer with a Linux DE + SSH setup to confirm the polkit prompt is suppressed and the TUI stays responsive. The unit tests thoroughly cover the probe logic, caching, and fallback behavior. 中文说明代码审查独立方案(阅读 diff 前):修复 Linux SSH 环境下 与 diff 对比: PR 的方案与我的独立方案完全一致。实现干净 — 未发现关键阻断问题。无 AGENTS.md 违规。代码直截了当 — 无过度抽象、无不必要的重复。 单元测试全部 21/21 测试通过 ✅ 实际场景测试此 bug 需要带桌面环境的 Linux 系统通过 SSH 访问 — polkit 认证提示仅在 冒烟测试显示两个版本均正常工作,无可见的回归。完整验证需要拥有 Linux DE + SSH 环境的审查者确认 polkit 提示被抑制且 TUI 保持响应。 — Qwen Code · qwen3.7-max |
ReflectionThis is a clean, well-motivated bug fix. Let me step back: The problem is real and well-documented — #5281 shows that The implementation matches my independent proposal exactly: probe via The diff is minimal and focused: two files, no drive-by refactors, no scope creep. Every line serves the stated goal. The test coverage is thorough — 21 tests covering the happy path, unsupported systemd, probe errors, and the existing behavior with the new probe step. The smoke test shows no regressions. The specific bug can't be reproduced in CI (needs DE + SSH), but the unit tests exhaustively verify the probe logic, and the code paths that would be exercised in the bug scenario are well-covered. Verdict: This is ready to ship. The approach is correct, the implementation is clean, the tests are thorough, and the risk is minimal (one additional sub-millisecond spawn on first ✅ Approving. 中文说明反思这是一个干净、动机充分的 bug 修复。 问题是真实且有据可查的 — #5281 显示在带有桌面环境的 Linux 系统上通过 SSH 运行时, 实现与我的独立方案完全一致:通过 Diff 最小且聚焦:两个文件,无顺手重构,无范围蔓延。每一行都服务于既定目标。测试覆盖全面 — 21 个测试覆盖了正常路径、不支持的 systemd、探测错误以及带新探测步骤的现有行为。 冒烟测试显示无回归。特定 bug 无法在 CI 中复现(需要 DE + SSH),但单元测试详尽地验证了探测逻辑。 结论: 可以合入。方案正确、实现干净、测试充分、风险最小。 ✅ 批准。 — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reflection
The idea is sound: --no-ask-password is a low-cost addition that prevents polkit from hijacking the TTY when no interactive agent is available. Combined with the existing isHeadlessSshSession() skip, it provides good defense-in-depth for the remaining Linux scenarios.
But the PR can't merge as-is. The diff only touches the test — the source code that actually constructs the systemd-inhibit command was never updated. This means the test will fail, and even if it passed, the flag wouldn't actually reach systemd-inhibit at runtime.
It's a one-line fix in the source file (sleepInhibitor.ts, ~line 258, add '--no-ask-password' to the args array). Once that's in and the PR template is filled in, this should be a quick merge.
Requesting changes for the missing source update.
中文说明
反思
思路是对的:--no-ask-password 是一个低成本的补充,可以在没有交互式 agent 时防止 polkit 劫持 TTY。与现有的 isHeadlessSshSession() 跳过机制结合,为其余 Linux 场景提供了良好的纵深防御。
但 PR 目前无法合并。diff 仅修改了测试——实际构造 systemd-inhibit 命令的源代码从未更新。这意味着测试会失败,即使测试通过,该标志也不会在运行时传递给 systemd-inhibit。
源码文件只需要一行改动(sleepInhibitor.ts,约第 258 行,在 args 数组中添加 '--no-ask-password')。补上这个并填写 PR 模板后,应该可以快速合并。
请求修改,原因是缺少源码更新。
— Qwen Code · qwen3.7-max
|
@OrbitZore Thanks for this — the approach is right: One thing is blocking it, though: the diff only updates the test — Could you add 中文说明@OrbitZore 感谢!思路是对的: 不过有一处卡住了:这个 diff 只改了测试—— 能否麻烦你在 |
b7d448a to
e35813a
Compare
a9ae386 to
23ae075
Compare
|
@qwen-code /triage |
On Linux systems with a desktop environment, running qwen-code over SSH triggers a polkit authentication prompt from systemd-inhibit that corrupts the TUI input stream. Probe systemd-inhibit --help to detect whether --no-ask-password is supported, cache the result, and prepend the flag when spawning the inhibitor. Fixes QwenLM#5281
23ae075 to
041810d
Compare
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
✅ Real-build verification — fix behaves correctly (one minor, non-blocking note)I built the actual Honesty up front: this host is macOS, and the actual polkit prompt / TUI corruption is a Linux-only runtime behavior — I can't reproduce the live polkit interaction here. So I verified the real shipped logic via the class's own dependency-injection surface ( 1. Unit tests — 21/21 on PR head
2. Mutation test — the tests genuinely guard the fixReverted only 3. Behavioral A/B — real compiled
|
scenario (platform:'linux') |
PRE-FIX | FIXED (this PR) |
|---|---|---|
--help advertises the flag |
1 spawn, no probe, no --no-ask-password |
probe once → real args **--no-ask-password** --what=sleep … ✅ |
older systemd (--help lacks it) |
1 spawn, no flag | probe → graceful fallback, no flag |
| probe spawn errors (ENOENT) | 1 spawn, no flag | probe error → fallback, no flag |
acquire() ×3 |
— | 1 --help probe total (cached once per process) ✅ |
headless SSH (SSH_TTY, no DISPLAY) |
skipped (0 spawns) | skipped (0 spawns) — no regression |
So the fix probes once, prepends --no-ask-password when supported, falls back cleanly when not, caches the result, and still skips headless-SSH — exactly as designed.
Minor, non-blocking note (latent race introduced by the async probe)
The old start() was synchronous; the new one defers spawning until the async --help probe resolves. If acquire() is followed by release() inside that one-time probe window, release() drops activeCount to 0 and stop() is a no-op (this.child is still undefined), then the probe callback runs doStart() and spawns an inhibitor with activeCount === 0 — an orphaned systemd-inhibit (doStart has no activeCount guard). It's near-unreachable in practice (both acquire and release are driven by slow events — a user toggle or a seconds-long request lifecycle — not a sub-50 ms window) and dispose() cleans it up at shutdown, so not a blocker. A one-line guard would close it:
this.probeNoAskPassword(() => {
this.probing = false;
if (this.activeCount > 0) this.doStart(reason); // skip if released during the probe
});Verdict: correct, well-tested, no regression — LGTM. Worth considering the one-line activeCount guard above, but it's optional. 👍
中文说明(完整对应)
✅ 真实构建验证 —— 修复行为正确(一个次要、非阻塞的提示)
我在隔离 worktree 里从本 PR 的 head 构建了真实 core,并在 tmux 中驱动真实编译的 SleepInhibitor,对照 pre-fix 构建做 A/B。
先把话说清楚: 本机是 macOS,而真正的 polkit 弹窗 / TUI 损坏是 仅 Linux 的运行时行为 —— 我无法在这里复现真实的 polkit 交互。所以我通过这个类自带的依赖注入面({platform, env, spawn, logger})验证真实发布的逻辑:注入 platform:'linux' + 一个模拟 systemd-inhibit --help 的假 spawn,观察真实的参数/探测逻辑。(--no-ask-password 是 systemd 标准选项,systemd-inhibit 继承它,所以"跳过交互式 polkit 授权"这个前提是成立的。)
1. 单元测试 —— PR head 21/21
vitest run src/services/sleepInhibitor.test.ts → 21 passed(PR 把用例从 18 扩到 21)。撰写时 CI(Lint/CodeQL/Test×3)仍在跑。
2. 变异测试 —— 测试确实守住了修复
只把 sleepInhibitor.ts 回退到 base(保留 PR 测试):11/21 失败,含 3 个显式 --no-ask-password 测试,以及所有改用 vi.waitFor 等"探测→spawn"序列的测试(那些 ~1000ms 是超时 —— base 根本不会 spawn --help 探测)。
3. 行为 A/B —— 真实编译的 SleepInhibitor、模拟的 systemd(tmux)
场景(platform:'linux') |
PRE-FIX | FIXED(本 PR) |
|---|---|---|
--help 含该 flag |
1 次 spawn、无探测、无 --no-ask-password |
先探测一次 → 真实参数 **--no-ask-password** --what=sleep … ✅ |
旧 systemd(--help 无该 flag) |
1 次 spawn、无 flag | 探测 → 优雅降级,不加 flag |
| 探测 spawn 出错(ENOENT) | 1 次 spawn、无 flag | 探测出错 → 降级,不加 flag |
acquire() ×3 |
— | 总共只探测 1 次 --help(每进程缓存一次)✅ |
headless SSH(SSH_TTY、无 DISPLAY) |
跳过(0 spawn) | 跳过(0 spawn)—— 无回归 |
所以修复:探测一次、支持时前置 --no-ask-password、不支持时干净降级、缓存结果、且仍跳过 headless-SSH —— 完全符合设计。
次要、非阻塞提示(异步探测引入的潜在竞态)
旧 start() 是同步的;新的把 spawn 推迟到异步 --help 探测完成。如果 acquire() 之后在那一次性的探测窗口内紧接着 release(),release() 会把 activeCount 降到 0、而 stop() 是空操作(此时 this.child 还是 undefined);随后探测回调跑 doStart(),在 activeCount === 0 的情况下 spawn 出一个 inhibitor —— 一个孤儿 systemd-inhibit(doStart 没有 activeCount 守卫)。实践中几乎不可达(acquire 和 release 都由慢事件驱动 —— 用户开关或数秒级的请求生命周期,不是 50ms 级窗口),且 dispose() 会在退出时清理,所以不是阻塞项。一行守卫即可堵上:
this.probeNoAskPassword(() => {
this.probing = false;
if (this.activeCount > 0) this.doStart(reason); // 探测期间已 release 则跳过
});结论:正确、测试充分、无回归 —— LGTM。可以考虑上面那行 activeCount 守卫,但属可选。 👍
|
@wenshao |
|
| Failing test | line | cause |
|---|---|---|
| starts systemd-inhibit on linux and stops it after the final release | 121 | bug #1 |
| skips systemd-inhibit for headless SSH sessions on Linux | 137 | bug #2 |
| ignores duplicate releases | 231 | bug #1 |
| restarts after an unexpected exit when acquired again | 288 | bug #1 |
| dispose kills the active child, resets state, and is idempotent | 313 | bug #1 |
| ignores a late error event from an already-replaced child | 405 | bug #1 |
(typecheck ✅ exit 0, eslint ✅, prettier ✅ — the failures are logic, not types.)
2. 🐞 Bug #1 — isRunning() is now always false
isRunning(): boolean {
- return this.child !== undefined;
+ return this.child !== undefined && this.probing;
}probing is set back to false before doStart() sets this.child, so after a successful start child !== undefined && probing = true && false = false. The method can never return true. This is an unrelated edit that breaks 5 tests. Fix: revert to return this.child !== undefined; (the && this.probing looks accidental).
3. 🐞 Bug #2 — the probe spawns even for headless-SSH sessions
start() runs the systemd-inhibit --help probe on Linux before the headless-SSH check in getCommand(). So a headless SSH session — where the inhibitor is intentionally skipped and nothing should spawn — now spawns an extra systemd-inhibit --help process. That breaks the "skips systemd-inhibit for headless SSH" contract (expect(spawn).not.toHaveBeenCalled()). Fix: gate the probe behind the same condition that decides systemd-inhibit will actually be used (i.e. don't probe when isHeadlessSshSession(env)).
4. ✅ What works — the actual fix (verified live in the real service)
I drove the real SleepInhibitor against a systemd-inhibit shim on PATH (real spawn, real probe + command construction). The shim records the exact argv and stands in for the inhibitor (no real sleep-block / polkit):
# SCENARIO: SSH session WITH a desktop (DISPLAY set) — the fix's target
systemd-inhibit --help
systemd-inhibit --no-ask-password --what=sleep --who=Qwen Code --why=verify-5318 --mode=block sleep infinity
^^^^^^^^^^^^^^^^^ the fix: prepended because the probe found support ✅
getActiveCount() = 1 isRunning() = false ← bug #1 visible live (child IS running)
# SCENARIO: real headless SSH (no DISPLAY) — inhibitor correctly skipped, but…
systemd-inhibit --help ← bug #2 visible live: the probe still spawned (old code spawned nothing)
The three feature-specific unit tests also pass: includes / omits / omits --no-ask-password for supported / unsupported / unavailable. So the core logic — including the graceful fallback on old systemd — is sound.
Recommendation
Good and useful fix (it solves the polkit-corrupts-TUI-over-SSH problem). Before merge, please:
- Revert
isRunning()toreturn this.child !== undefined;(bug pre-release: fix ci #1). - Skip the probe for headless-SSH (and generally when systemd-inhibit won't be used) so no extra process is spawned (bug Where is the config saved? #2).
- Re-run
vitest run src/services/sleepInhibitor.test.ts— all 21 should pass.
🇨🇳 中文版(点击展开)
⚠️ 维护者验证 —— 核心修复是对的,但该 PR 跑不过自己的测试(请修复后再合并)
实际改动——探测 systemd-inhibit --help 并在前面加上 --no-ask-password——是正确的,在真实服务里也能工作。但 PR 还夹带了两个无关的回归,导致其 21 个测试里有 6 个失败(描述说"全部 18 个应通过"——实际是 21 个,其中 6 个失败)。建议:request changes——修掉这两个 bug 后就是个好功能。
环境
- 在 PR head(
041810df,基于当前main,MERGEABLE)上独立git worktree,全新npm ci。 - Linux · Node
v22.22.2· 本机真实的systemd-inhibit确实列出了--no-ask-password。
1. 测试套件 —— ❌ 6 失败 / 15 通过(共 21)
npx vitest run src/services/sleepInhibitor.test.ts → Tests 6 failed | 15 passed (21)
| 失败的测试 | 行 | 原因 |
|---|---|---|
| starts systemd-inhibit on linux and stops it after the final release | 121 | bug #1 |
| skips systemd-inhibit for headless SSH sessions on Linux | 137 | bug #2 |
| ignores duplicate releases | 231 | bug #1 |
| restarts after an unexpected exit when acquired again | 288 | bug #1 |
| dispose kills the active child, resets state, and is idempotent | 313 | bug #1 |
| ignores a late error event from an already-replaced child | 405 | bug #1 |
(typecheck ✅ 退出码 0,eslint ✅,prettier ✅ —— 失败是逻辑问题,不是类型问题。)
2. 🐞 Bug #1 —— isRunning() 现在恒为 false
isRunning(): boolean {
- return this.child !== undefined;
+ return this.child !== undefined && this.probing;
}probing 在 doStart() 设置 this.child 之前就被置回 false 了,所以一次成功启动后 child !== undefined && probing = true && false = false。该方法永远不可能返回 true。这是一处无关的改动,破坏了 5 个测试。修复: 改回 return this.child !== undefined;(&& this.probing 看起来是误加的)。
3. 🐞 Bug #2 —— 探测在 headless-SSH 会话下也会 spawn
start() 在 Linux 上会先跑 systemd-inhibit --help 探测,然后才在 getCommand() 里做 headless-SSH 判断。于是在 headless SSH 会话里——本该完全跳过、不 spawn 任何东西——现在会多 spawn 一个 systemd-inhibit --help 进程,破坏了"headless SSH 跳过 systemd-inhibit"的约定(expect(spawn).not.toHaveBeenCalled())。修复: 把探测放到"确实会用 systemd-inhibit"的同一判断之后(即 isHeadlessSshSession(env) 时不要探测)。
4. ✅ 可用的部分 —— 实际修复(在真实服务里实测)
我用 PATH 上的 systemd-inhibit 垫片驱动了真实的 SleepInhibitor(真实 spawn、真实探测与命令构造)。垫片记录确切 argv,并代替真正的 inhibitor(不真正阻止休眠 / 不触发 polkit):
# 场景:SSH 会话但有桌面(设置了 DISPLAY)—— 修复的目标场景
systemd-inhibit --help
systemd-inhibit --no-ask-password --what=sleep --who=Qwen Code --why=verify-5318 --mode=block sleep infinity
^^^^^^^^^^^^^^^^^ 修复:因为探测到支持,所以加在前面 ✅
getActiveCount() = 1 isRunning() = false ← bug #1 实地可见(子进程其实在运行)
# 场景:真实 headless SSH(无 DISPLAY)—— inhibitor 被正确跳过,但……
systemd-inhibit --help ← bug #2 实地可见:探测仍然 spawn 了(旧代码什么都不 spawn)
三个针对该功能的单测也通过:在支持 / 不支持 / 不可用时分别 包含 / 省略 / 省略 --no-ask-password。所以核心逻辑——包括在旧 systemd 上的优雅降级——是可靠的。
建议
是个有用的修复(解决了 SSH 下 polkit 破坏 TUI 的问题)。合并前请:
- 把
isRunning()改回return this.child !== undefined;(bug pre-release: fix ci #1)。 - headless-SSH 时跳过探测(以及通常在不会用到 systemd-inhibit 时),不要多 spawn 进程(bug Where is the config saved? #2)。
- 重新跑
vitest run src/services/sleepInhibitor.test.ts—— 21 个应全部通过。
- Revert isRunning() to return this.child !== undefined (probing flag was incorrectly ANDed, returning false after probe completed) - Skip --no-ask-password probe when systemd-inhibit won't be used (headless SSH sessions), avoiding unnecessary process spawn - Guard probe callback with activeCount > 0 to prevent orphaned child if dispose() is called during probe Fixes 6 failing tests in sleepInhibitor.test.ts
qqqys
left a comment
There was a problem hiding this comment.
The prior critical issues are resolved in the latest head: isRunning() is back to the child-state check, headless SSH no longer runs the probe, and the probe callback now avoids starting an inhibitor after release/dispose. I rechecked the updated diff and did not find any new critical blocker.
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
✅ Maintainer verification — real local + tmux testing (backs my approval)Built the PR head ( What it fixesOn Linux the sleep inhibitor spawns Real-TUI A/B (tmux) — faithful polkit stand-inThe bug's mechanism is "polkit writes to
Mutation (deterministic, code-level)Revert No-regression21 / 21 Reverse-audit notes
VerdictCorrect, well-tested fix for a real Linux TUI-corruption issue; the faithful tmux A/B shows the polkit prompt leaking onto the pane pre-fix and a clean TUI post-fix, with the 🇨🇳 中文版(点击展开)✅ 维护者验证 —— 本地真实 + tmux 测试(为我的 approve 背书)在隔离 worktree(Node v22.22.2)构建 PR head( 修了什么Linux 上 sleep inhibitor 会 spawn 真实 TUI A/B(tmux)—— 忠实的 polkit 替身bug 机制是*"polkit 绕过
变异测试(确定性,代码层)把 无回归FIXED 构建上 反向审计补充
结论针对一个真实的 Linux TUI 损坏问题的正确、测试充分的修复;忠实的 tmux A/B 显示修复前 polkit 提示泄漏到 pane、修复后 TUI 干净,且 Method: isolated worktree build of |
What this PR does
Adds feature detection for
--no-ask-passwordin the sleep inhibitor. On firstacquire()on Linux, the code spawnssystemd-inhibit --helpand inspects the output to determine whether the flag is supported. The result is cached so the probe only runs once per process lifetime. When supported,--no-ask-passwordis prepended to thesystemd-inhibitarguments, preventing polkit from prompting for interactive authentication.Why it's needed
On Linux systems with a desktop environment, running qwen-code over SSH (without a local DE session) causes
systemd-inhibitto trigger a polkit authentication prompt (AUTHENTICATING FOR org.freedesktop.login1.inhibit-block-sleep). This prompt writes directly to the TUI's input stream, corrupting it and making the TUI unresponsive to user input. The--no-ask-passwordflag tellssystemd-inhibitto skip interactive authorization, which is the correct behavior for a headless/SSH session where no user is present to respond to the prompt.Reviewer Test Plan
How to verify
/settings.systemd-inhibitdoes not support--no-ask-password(older systemd), verify that sleep inhibition still works without the flag (graceful fallback).npx vitest run src/services/sleepInhibitor.test.ts— all 18 tests should pass.Evidence (Before & After)
N/A (non-UI change; behavior is observed via TUI responsiveness and absence of polkit prompts)
Tested on
Environment (optional)
Local Linux with systemd, Node.js v26.
Risk & Scope
systemd-inhibit --helpon first acquire on Linux. The probe is fast (sub-millisecond) and the result is cached.Linked Issues
Fixes #5281
中文说明
本 PR 做了什么
为 sleep inhibitor 添加了
--no-ask-password的特性检测。在 Linux 上首次调用acquire()时,代码会启动systemd-inhibit --help并检查输出以判断是否支持该标志。结果会被缓存,因此探测每个进程生命周期只运行一次。当支持该标志时,--no-ask-password会被添加到systemd-inhibit参数的前面,防止 polkit 弹出交互式认证提示。为什么需要
在带有桌面环境的 Linux 系统上,通过 SSH 运行 qwen-code(未登录本地桌面环境)会导致
systemd-inhibit触发 polkit 认证提示(AUTHENTICATING FOR org.freedesktop.login1.inhibit-block-sleep)。该提示直接写入 TUI 的输入流,导致 TUI 无法响应用户输入。--no-ask-password标志告诉systemd-inhibit跳过交互式授权,这对于没有用户在场响应提示的无头/SSH 会话是正确的行为。审查者测试计划
如何验证
/settings中启用"防止系统休眠"。--no-ask-password的系统上(较旧的 systemd),验证休眠抑制功能仍然正常(优雅降级)。npx vitest run src/services/sleepInhibitor.test.ts— 全部 18 个测试应通过。证据(修改前与修改后)
N/A(非 UI 变更;行为通过 TUI 响应性和 polkit 提示的 absence 来观察)
测试平台
环境(可选)
本地 Linux + systemd,Node.js v26。
风险与范围
systemd-inhibit --help的 spawn。探测速度很快(亚毫秒级),且结果会被缓存。关联 Issue
Fixes #5281