Skip to content

fix(cli): validate channel service pidfile#5300

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/channel-pidfile-validation
Jun 18, 2026
Merged

fix(cli): validate channel service pidfile#5300
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/channel-pidfile-validation

Conversation

@tt-a1i

@tt-a1i tt-a1i commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Hardens the channel service PID-file handling so a malformed or leftover service.pid can no longer be mistaken for a running service or trigger a dangerous signal.

  • Validates the PID file before trusting it. readServiceInfo() now parses the file as unknown and runs it through an isServiceInfo type guard: the record must be an object whose pid is a strictly-positive safe integer (isValidPid), whose startedAt is a parseable date string, and whose channels is an array of strings. Anything that fails (pid: 0, pid: -1, fractional/NaN/string PIDs, wrong shape, corrupt JSON) is treated as invalid.
  • Cleans up malformed files instead of probing them. Both the corrupt-JSON path and the new invalid-shape path delete the file (via a shared unlinkPidFile best-effort helper) and return null, so an invalid PID is never passed to process.kill.
  • Guards the signal/probe/wait helpers. isProcessAlive, signalService, and (transitively) waitForExit now reject non-positive / invalid PIDs up front: isProcessAlive(0) returns false, signalService(0) returns false without signalling, and waitForExit(0, …) returns true immediately without polling. This closes the case where process.kill(0, sig) would target the caller's entire process group (and pid -1 every signalable process).

Net effect: a planted pid: 0 file that previously reported running (PID 0) and let qwen channel stop signal its own process group is now cleaned up and reported as "no service running". Existing behavior for valid live PIDs, stale (dead) PIDs, and already-handled corrupt files is unchanged.

Why it's needed

process.kill(pid, sig) has special POSIX semantics for non-positive PIDs: pid 0 signals the caller's whole process group and pid -1 signals every process the user may signal. The pre-PR code trusted whatever PID sat in ~/.qwen/channels/service.pid. A malformed or leftover pidfile containing pid: 0 would therefore (a) be reported as a running service by qwen channel status, and (b) make qwen channel stop call process.kill(0, 'SIGTERM') — signalling its own process group, i.e. a self-kill / local DoS. This is preventive robustness/security hardening of that path; it is not tied to a specific user-filed issue.

Reviewer Test Plan

How to verify

Author's verification commands (all from the original PR body — kept as-is):

  • npx vitest run packages/cli/src/commands/channel/pidfile.test.ts
  • npx vitest run packages/cli/src/commands/channel
  • npx eslint packages/cli/src/commands/channel/pidfile.ts packages/cli/src/commands/channel/pidfile.test.ts
  • npx prettier --check packages/cli/src/commands/channel/pidfile.ts packages/cli/src/commands/channel/pidfile.test.ts
  • npm run build --workspace=packages/core
  • npm run typecheck --workspace=packages/cli
  • git diff --check upstream/main...HEAD

The new unit tests assert the repro directly: a pid: 0 file is cleaned up and readServiceInfo() returns null without calling process.kill; a battery of malformed records (pid: -1, 1.5, "1234", bad startedAt, non-array / non-string channels) all clean up and return null; signalService(0) returns false and never signals; and waitForExit(0, …) resolves true without polling.

End-to-end (optional, POSIX): plant a ~/.qwen/channels/service.pid with {"pid":0,…}, then run qwen channel status (expected: "No channel service is running", file removed) and qwen channel stop inside a contained process group (expected: no signal sent, group untouched).

Evidence (Before & After)

N/A — non-visible logic/safety fix in the channel PID-file helpers; covered by the unit tests above (the new tests assert the pid: 0 and malformed-record paths clean up and never call process.kill). A separate end-to-end A/B verification of the real qwen channel status / stop commands is recorded in a maintainer comment on this PR thread.

Tested on

OS Status
🍏 macOS ✅ CI
🪟 Windows ✅ CI
🐧 Linux ✅ CI · ✅ local

✅ tested · ⚠️ not tested · N/A

Test (macos-latest / windows-latest / ubuntu-latest, Node 22.x) all pass on this PR. Linux additionally has the maintainer's real-binary qwen channel A/B verification above (the platform where the bad signal semantics actually bite).

Environment (optional)

Unit tests run via npm workspaces (packages/cli). The dangerous kill(0|-1) semantics are POSIX; the isValidPid guard is platform-agnostic.

Risk & Scope

  • Main risk or tradeoff: Low; scoped to packages/cli/src/commands/channel/pidfile.ts. The behavior change is limited to rejecting/cleaning up invalid pidfiles (non-positive, non-safe-integer, NaN, or wrong-shape records); valid live/stale-PID and already-handled corrupt-file paths are unchanged.
  • Not validated / out of scope: PID reuse — a structurally valid but recycled PID (e.g. a stale file whose PID was reused, or pid: 1) is still trusted; that is a separate problem this PR does not claim to solve. No unrelated refactors, public API changes, or UI changes. startedAt validation is intentionally lenient (any parseable date), since writeServiceInfo only ever emits ISO strings.
  • Breaking changes / migration notes: None.

Linked Issues

No linked issue — this is a preventive robustness/security fix for the channel PID-file path; there is no corresponding open GitHub issue this PR closes.

中文说明

这个 PR 做了什么

加固 channel 服务的 PID 文件处理,使得一个异常或残留的 service.pid 不再可能被误判为「正在运行的服务」,也不再可能触发危险的信号调用。

  • 在信任之前校验 PID 文件。 readServiceInfo() 现在先把文件解析为 unknown,再通过 isServiceInfo 类型守卫:记录必须是对象,其 pid 为严格正的安全整数(isValidPid),startedAt 为可解析的日期字符串,channels 为字符串数组。任何不满足的情况(pid: 0pid: -1、小数/NaN/字符串 PID、结构错误、损坏 JSON)都被视为无效。
  • 对异常文件直接清理而非探测。 损坏 JSON 路径与新增的「结构无效」路径都会删除文件(通过共享的尽力而为辅助函数 unlinkPidFile)并返回 null,因此无效 PID 永远不会被传给 process.kill
  • 守卫发信号/探测/等待辅助函数。 isProcessAlivesignalService、以及(间接地)waitForExit 现在在入口处就拒绝非正数 / 无效 PID:isProcessAlive(0) 返回 falsesignalService(0) 不发信号直接返回 falsewaitForExit(0, …) 不轮询直接返回 true。这堵上了 process.kill(0, sig) 会作用于调用方整个进程组(而 pid -1 作用于所有可发信号的进程)的口子。

最终效果:一个预置的 pid: 0 文件,之前会被报告为 running (PID 0) 并让 qwen channel stop 向自己的进程组发信号,现在会被清理并报告为「无服务运行」。对有效存活 PID、陈旧(已死)PID、以及原本已处理的损坏文件,行为保持不变。

为什么需要

process.kill(pid, sig) 对非正数 PID 有特殊的 POSIX 语义:pid 0 会向调用方的整个进程组发信号,pid -1 会向该用户能发信号的所有进程发信号。PR 之前的代码无条件信任 ~/.qwen/channels/service.pid 里的 PID。因此一个含 pid: 0 的异常或残留 pidfile 会:(a) 被 qwen channel status 报告为正在运行的服务;(b) 让 qwen channel stop 调用 process.kill(0, 'SIGTERM')——向自己的进程组发信号,即自杀式 / 本地 DoS。这是对该路径的预防性健壮性/安全加固,并非针对某个用户提交的 issue。

审查者测试计划

如何验证

作者的验证命令(全部来自原始 PR 描述,原样保留):

  • npx vitest run packages/cli/src/commands/channel/pidfile.test.ts
  • npx vitest run packages/cli/src/commands/channel
  • npx eslint packages/cli/src/commands/channel/pidfile.ts packages/cli/src/commands/channel/pidfile.test.ts
  • npx prettier --check packages/cli/src/commands/channel/pidfile.ts packages/cli/src/commands/channel/pidfile.test.ts
  • npm run build --workspace=packages/core
  • npm run typecheck --workspace=packages/cli
  • git diff --check upstream/main...HEAD

新增的单元测试直接验证了复现:pid: 0 的文件被清理、readServiceInfo() 返回 null 且未调用 process.kill;一批异常记录(pid: -11.5"1234"、错误的 startedAt、非数组 / 非字符串 channels)全部被清理并返回 nullsignalService(0) 返回 false 且从不发信号;waitForExit(0, …) 不轮询直接 resolve 为 true

端到端(可选,POSIX):预置一个 {"pid":0,…}~/.qwen/channels/service.pid,然后运行 qwen channel status(预期:「No channel service is running」,文件被删除)以及在受控进程组里运行 qwen channel stop(预期:不发信号,进程组不受影响)。

证据(前 & 后)

N/A —— channel PID 文件辅助函数中的非可见逻辑/安全修复,由上述单元测试覆盖(新增测试断言 pid: 0 与异常记录路径会被清理且从不调用 process.kill)。针对真实 qwen channel status / stop 命令的端到端 A/B 验证记录在本 PR 讨论串的一条维护者评论中。

测试平台

操作系统 状态
🍏 macOS ✅ CI
🪟 Windows ✅ CI
🐧 Linux ✅ CI · ✅ 本地

✅ 已测试 · ⚠️ 未测试 · N/A

本 PR 的 Test (macos-latest / windows-latest / ubuntu-latest, Node 22.x) 全部通过。Linux 另有上述维护者的真实二进制 qwen channel A/B 验证(这恰恰是危险信号语义会真正触发的平台)。

环境(可选)

单元测试通过 npm workspaces(packages/cli)运行。危险的 kill(0|-1) 语义属于 POSIX;isValidPid 守卫与平台无关。

风险与范围

  • 主要风险或权衡: 低;范围限于 packages/cli/src/commands/channel/pidfile.ts。行为变化仅限于拒绝/清理无效的 pidfile(非正数、非安全整数、NaN 或结构错误的记录);有效存活/陈旧 PID 以及原本已处理的损坏文件路径不变。
  • 未验证 / 范围外: PID 复用——一个结构有效但被复用的 PID(例如残留文件的 PID 已被复用,或 pid: 1)仍会被信任;那是本 PR 不声称解决的另一类问题。无无关重构、公共 API 变更或 UI 变更。startedAt 校验有意保持宽松(任何可解析的日期),因为 writeServiceInfo 只会写 ISO 字符串。
  • 破坏性变更 / 迁移说明: 无。

关联 Issue

无关联 issue——这是对 channel PID 文件路径的预防性健壮性/安全修复,没有对应的开放 GitHub issue 被本 PR 关闭。

AI Assistance Disclosure

I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@tt-a1i
tt-a1i marked this pull request as ready for review June 18, 2026 17:01
@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR, @tt-a1i! The fix itself looks sensible — validating PIDs before using them is a real safety concern. However, the PR body is missing several required sections from the PR template:

  • Why it's needed — what problem or risk motivates this change? (e.g., did you encounter a malformed pidfile in the wild, or is this a preventive hardening?)
  • Reviewer Test Plan — the template asks for structured sections: How to verify, Evidence (Before & After), and a Tested on table (macOS / Windows / Linux). Your "Test Plan" lists unit-test commands but doesn't follow this structure.
  • Risk & Scope — any tradeoffs, out-of-scope items, or breaking changes?
  • Linked Issues — is there an issue this closes?
  • 中文说明 — the template includes a <details> block for a Chinese translation.

Could you update the PR description to fill in these sections? The unit-test commands you listed are great — they just need to sit inside the template's structure so reviewers can quickly verify. Happy to re-review once updated.

中文说明

感谢 @tt-a1i 的贡献!修复本身是合理的——在使用 PID 前进行验证确实是一个重要的安全措施。不过 PR 描述缺少 PR 模板 中的几个必填部分:

  • Why it's needed(为什么需要) — 这个改动解决了什么问题或风险?(比如是否在真实场景中遇到过损坏的 pidfile,还是预防性加固?)
  • Reviewer Test Plan(审查者测试计划) — 模板要求有结构化的部分:How to verifyEvidence (Before & After)、以及 Tested on 表格(macOS / Windows / Linux)。当前的 "Test Plan" 列出了单元测试命令,但没有遵循这个结构。
  • Risk & Scope(风险与范围) — 有什么权衡、不在范围内的项目或破坏性变更吗?
  • Linked Issues(关联 Issue) — 是否有关联的 issue?
  • 中文说明 — 模板包含一个 <details> 区块用于中文翻译。

请补充这些部分,更新后我们会重新审查。

Qwen Code · qwen3.7-max

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer verification — real qwen channel test under tmux

Verified this PR locally by driving the actual qwen channel status / stop commands against malformed pidfiles in a real terminal (tmux), in addition to the unit suite. Verdict: a correct and worthwhile security/robustness fix — good to merge. This adds 🐧 Linux coverage (the platform where the bad signal semantics actually bite).

Environment

  • Isolated git worktree at the PR head (1939cd4c, on current upstream main), fresh npm ci (no symlinked node_modules), real esbuild bundle.
  • Linux · Node v22.22.2 · bundled qwen 0.18.3.
  • Scope is clean: exactly 2 files, +109 / −19, single commit.

1. Static checks (reproducing the PR test plan)

Check Command Result
Unit tests vitest run …/channel/pidfile.test.ts 16/16 (incl. 4 new: pid-0 cleanup, malformed cleanup, no-signal, no-poll)
Unit tests vitest run …/commands/channel 35/35 (whole channel dir)
Lint eslint pidfile.ts(.test) ✅ clean
Format prettier --check ✅ clean
Whitespace git diff --check ✅ clean
Core build npm run build -w packages/core ✅ exit 0
Typecheck npm run typecheck -w packages/cli exit 0, 0 errors

2. Why this matters (the bug)

process.kill(pid, sig) has special semantics for non-positive pids: pid 0 targets the caller's entire process group, and pid -1 targets every process the user may signal. The pre-PR code trusted whatever pid sat in ~/.qwen/channels/service.pid, so a malformed/leftover pidfile with pid: 0 would (a) be reported as a running service, and (b) make qwen channel stop signal its own process group — a self-kill / DoS.

3. Real command A/B in tmux

Drove the real binary against planted pidfiles (sandboxed HOME). status cases use signal 0 (probe only — safe). The stop case is contained in a fresh setsid process group with a sentinel sleep, so a stray kill(0) is observed without touching anything outside it.

[A] qwen channel status:

pidfile contents OLD (pre-PR) NEW (this PR)
valid live pid (control) running (PID …) · kept running (PID …) · kept
pid: 0 ⚠️ running (PID 0) · kept No channel service is running · cleaned up
pid: -1 ⚠️ running (PID -1) · kept No channel service is running · cleaned up
pid: "abc" No channel service is running · cleaned same
missing pid No channel service is running · cleaned same
corrupt JSON No channel service is running · cleaned same

The already-handled cases (string/missing pid, corrupt JSON) behave identically — no regression — while pid 0 / pid -1 are the cases the fix closes. Valid running services are still detected and their pidfile preserved.

[B] qwen channel stop with pid: 0 (contained in a setsid group + sentinel):

OLD (pre-PR):  Stopping channel service (PID 0)...
               SENTINEL: KILLED -> kill(0) signaled the whole process group (pid-0 DANGER)

NEW (this PR): No channel service is running.   (stop_exit=0)
               SENTINEL: ALIVE  -> no kill(0); process group untouched (SAFE)

The pre-PR binary printed Stopping … (PID 0)… and then process.kill(0, 'SIGTERM') took down the entire process group (the sentinel died); the PR binary treats the malformed file as "not running" and never signals. Exactly issue #5337's concern, demonstrated safely. (I deliberately did not run stop with pid: -1, since kill(-1, …) is not bounded by a process group — setsid couldn't contain it. The guard makes that path safe too.)

Observations (non-blocking)

  1. Scope is intentionally narrow: the guard rejects invalid pids (≤ 0, non-integer, non-safe-integer, NaN, wrong shape). A valid-but-recycled pid (e.g. a stale file whose pid was reused, or pid: 1) is still trusted — but that's PID-reuse, a separate problem this PR doesn't claim to solve. Fine to leave out of scope.
  2. startedAt validation is lenient (!Number.isNaN(Date.parse(...))), but pid — the security-relevant field — is strictly validated, and writeServiceInfo only ever emits ISO strings, so real files pass. No concern.
  3. channels: [] passes isServiceInfo ([].every(...) is true). Reasonable — a structurally valid record with zero channels.
  4. Cross-platform: the dangerous kill(0|-1) semantics are POSIX; the isValidPid guard is platform-agnostic and correct everywhere. Verified on Linux, where the risk is real.

🇨🇳 中文版(点击展开)

✅ 维护者验证 —— 在 tmux 中对 qwen channel 进行真实测试

我在本地通过驱动真实的 qwen channel status / stop 命令针对各种异常 pidfile 进行了验证(除单测之外)。结论:这是一个正确且有价值的安全/健壮性修复,建议合并。 本次还补上了 🐧 Linux 平台覆盖(恰恰是这种危险信号语义会真正触发的平台)。

环境

  • 在 PR head(1939cd4c,基于当前上游 main)上独立 git worktree,全新 npm ci(绝不软链 node_modules),真实 esbuild 打包。
  • Linux · Node v22.22.2 · 打包后的 qwen 0.18.3
  • 改动范围干净:恰好 2 个文件,+109 / −19,单个提交

1. 静态检查(复现 PR 测试计划)

检查项 命令 结果
单元测试 vitest run …/channel/pidfile.test.ts 16/16(含 4 个新增:pid-0 清理、异常清理、不发信号、不轮询)
单元测试 vitest run …/commands/channel 35/35(整个 channel 目录)
Lint eslint pidfile.ts(.test) ✅ 干净
格式 prettier --check ✅ 干净
空白字符 git diff --check ✅ 干净
core 构建 npm run build -w packages/core ✅ 退出码 0
类型检查 npm run typecheck -w packages/cli 退出码 0,0 错误

2. 为什么重要(这个 bug)

process.kill(pid, sig) 对非正数 pid 有特殊语义:pid 0 会作用于调用方的整个进程组pid -1 会作用于该用户能发送信号的所有进程。PR 之前的代码无条件信任 ~/.qwen/channels/service.pid 里的 pid,因此一个异常/残留的、pid: 0 的 pidfile 会:(a) 被当作正在运行的服务,(b) 让 qwen channel stop 向自己的进程组发信号——即自杀式 / DoS。

3. tmux 中真实命令的 A/B

针对预置的 pidfile 驱动真实二进制(沙箱化 HOME)。status 用信号 0(仅探测——安全)。stop 用例封装在一个全新的 setsid 进程组里,并放置一个哨兵 sleep,这样即便发生 kill(0) 也能被观测到,且不会影响进程组之外的任何东西。

[A] qwen channel status

pidfile 内容 旧版(PR 前) 新版(本 PR)
有效存活 pid(对照) running (PID …) · 保留 running (PID …) · 保留
pid: 0 ⚠️ running (PID 0) · 保留 No channel service is running · 已清理
pid: -1 ⚠️ running (PID -1) · 保留 No channel service is running · 已清理
pid: "abc" No channel service is running · 已清理 同上
缺少 pid No channel service is running · 已清理 同上
损坏 JSON No channel service is running · 已清理 同上

原本已处理的情形(字符串/缺失 pid、损坏 JSON)两版表现一致——无回归——而 pid 0 / pid -1 正是本修复要堵上的口子。有效运行中的服务仍能被正确识别,pidfile 也保留。

[B] qwen channel stoppid: 0(封装在 setsid 进程组 + 哨兵):

旧版(PR 前): Stopping channel service (PID 0)...
              哨兵:被杀 -> kill(0) 向整个进程组发了信号(pid-0 危险)

新版(本 PR): No channel service is running.   (stop_exit=0)
              哨兵:存活 -> 没有 kill(0);进程组未受影响(安全)

PR 前的二进制先打印 Stopping … (PID 0)…,随后 process.kill(0, 'SIGTERM') 端掉了整个进程组(哨兵被杀);PR 后的二进制把异常文件当作「未运行」,从不发信号。这正是 issue #5337 的关切,并以安全的方式被复现。(我特意没有pid: -1stop,因为 kill(-1, …) 不受进程组约束,setsid 无法封装它;而该修复同样让这条路径变安全。)

观察项(非阻断)

  1. 范围有意收窄: 该守卫只拒绝无效 pid(≤ 0、非整数、非安全整数、NaN、结构不对)。一个有效但被复用的 pid(例如残留文件的 pid 已被复用,或 pid: 1)仍会被信任——但那属于 PID 复用,是本 PR 不声称解决的另一类问题,留在范围外是合理的。
  2. startedAt 校验较宽松!Number.isNaN(Date.parse(...))),但与安全相关的 pid 字段是严格校验的,且 writeServiceInfo 只会写 ISO 字符串,真实文件都能通过。无需担心。
  3. channels: [] 能通过 isServiceInfo[].every(...)true)。合理——一个零频道但结构有效的记录。
  4. 跨平台: 危险的 kill(0|-1) 语义属于 POSIX;isValidPid 守卫与平台无关、处处正确。已在 Linux(风险真实存在的平台)验证。

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @tt-a1i!

Template looks good — all required sections filled in ✓

On direction: this is a real safety issue. process.kill(0, sig) targeting the caller's entire process group is a well-known POSIX footgun, and the pidfile code was trusting its input blindly. A planted or corrupted pid: 0 file causing a local DoS via self-signal is exactly the kind of thing worth fixing before someone gets burned. Channel/daemon PID management is squarely within the CLI's core responsibility.

On approach: the scope is tight — 2 files, +109/−19, single-purpose. Three layers of defense (validate on read → guard isProcessAlive → guard signalService/waitForExit) is the right depth. The unlinkPidFile helper consolidation is a natural byproduct, not scope creep. Nothing to cut.

Moving on to code review. 🔍

中文说明

感谢贡献,@tt-a1i

模板完整 ✓

方向:这是一个真实的安全问题。process.kill(0, sig) 作用于调用方整个进程组是众所周知的 POSIX 陷阱,而 pidfile 代码对输入完全没有校验。一个被植入或损坏的 pid: 0 文件通过自信号导致本地 DoS,正是值得在出问题前修复的事情。Channel/daemon 的 PID 管理属于 CLI 的核心职责范围。

方案:范围紧凑——2 个文件,+109/−19,目的单一。三层防御(读取时校验 → 守卫 isProcessAlive → 守卫 signalService/waitForExit)深度合理。unlinkPidFile 辅助函数的提取是自然的附带整理,不属于范围蔓延。没有可以砍掉的部分。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Code review: Clean implementation. My independent proposal before reading the diff was: add a isValidPid guard (positive safe integer), parse JSON as unknown with a type guard, and add defensive guards on isProcessAlive/signalService/waitForExit. The PR matches this exactly, with the bonus of consolidating the four scattered try { unlinkSync } blocks into unlinkPidFile.

  • isValidPid correctly uses Number.isSafeInteger + pid > 0 — covers 0, -1, floats, NaN, strings, Infinity.
  • isServiceInfo type guard is thorough: checks object shape, pid validity, startedAt parseability, and channels array element types.
  • The guard placement in isProcessAlive means waitForExit is transitively protected without needing its own explicit check — good layering.
  • No correctness bugs, security holes, or regressions found.

No blockers.

Testing:

Unit tests: 16/16 pass (12 original + 4 new). ESLint and Prettier clean.

Real-scenario test on Linux (Node v22.22.3) — planted malformed pidfiles and drove the real qwen channel status vs the PR code:

Before (installed qwen, pre-PR)

Planted service.pid with {"pid":0,...}:

$ HOME=/tmp/triage-pidfile-NhXr9t qwen channel status
Channel service: running (PID 0)
Uptime:          20h 30m

Channel        Sessions
-----------------------
test           0

Pidfile not cleaned up. Bug confirmed — pid:0 treated as a running service, and process.kill(0, 0) would probe the caller's entire process group.

After (this PR, via npm run dev)

Same pid:0 pidfile:

$ HOME=/tmp/triage-pidfile-NhXr9t npm run dev -- channel status
No channel service is running.

Pidfile cleaned up (deleted). Fix confirmed.

Same test with pid:-1:

$ echo '{"pid":-1,...}' > service.pid
$ HOME=/tmp/triage-pidfile-NhXr9t npm run dev -- channel status
No channel service is running.

Pidfile cleaned up. The isValidPid guard rejects -1 before it ever reaches process.kill.

中文说明

代码审查: 实现干净。我在看 diff 之前的独立方案是:添加 isValidPid 守卫(正安全整数)、将 JSON 解析为 unknown 并加类型守卫、在 isProcessAlive/signalService/waitForExit 上加防御性守卫。PR 完全匹配这个方案,还额外把四处散落的 try { unlinkSync } 块整合成了 unlinkPidFile

  • isValidPid 正确使用 Number.isSafeInteger + pid > 0——覆盖了 0-1、小数、NaN、字符串、Infinity
  • isServiceInfo 类型守卫很全面:检查对象形状、pid 有效性、startedAt 可解析性、channels 数组元素类型。
  • isProcessAlive 中的守卫意味着 waitForExit 被间接保护,无需自己的显式检查——分层合理。
  • 未发现正确性 bug、安全漏洞或回归。

无阻断项。

测试:

单元测试:16/16 通过(12 个原有 + 4 个新增)。ESLint 和 Prettier 均干净。

Linux 真实场景测试(Node v22.22.3)——预置异常 pidfile 并驱动真实 qwen channel status 与 PR 代码对比:见上方英文终端输出。pid:0pid:-1 均被正确拒绝并清理。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Stepping back: this is a textbook defensive fix. The problem is real (POSIX kill(0) semantics are a well-documented footgun), the solution is minimal (3 guard points + 1 helper extraction, no scope creep), and the tests actually prove the fix works — not just "code doesn't crash" but "pid:0 is rejected before reaching process.kill."

My independent proposal from Stage 2 was essentially identical to what the PR does. The unlinkPidFile consolidation was a nice touch I hadn't explicitly planned but is obviously correct.

The tmux A/B test provides clear evidence: the installed build incorrectly reports running (PID 0) and leaves the pidfile in place, while the PR build correctly says "No channel service is running" and cleans up the malformed file. Same result for pid:-1.

Ships cleanly. Approving.

中文说明

退一步看:这是一个教科书级的防御性修复。问题是真实的(POSIX kill(0) 语义是众所周知的陷阱),方案是最小的(3 个守卫点 + 1 个辅助函数提取,无范围蔓延),测试真正证明了修复有效——不仅仅是"代码不崩溃",而是"pid:0 在到达 process.kill 之前就被拒绝了"。

我在 Stage 2 的独立方案与 PR 的做法基本一致。unlinkPidFile 的整合是我没有明确计划但显然正确的细节。

tmux A/B 测试提供了清晰的证据:已安装版本错误地报告 running (PID 0) 并保留 pidfile,PR 版本正确报告"No channel service is running"并清理异常文件。pid:-1 同理。

可以合并。批准中。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — correct and minimal defensive fix for the pid:0/pid:-1 signal footgun. Tests prove the guard works, scope is tight. ✅

@wenshao
wenshao merged commit 41efcf0 into QwenLM:main Jun 18, 2026
38 checks passed
@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants