mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* fix(ci): repair desktop rpm dependency and teardown checks Co-authored-by: Qwen-Coder <[email protected]> * test(desktop): use a counted space class in the EPEL checksum pin `scripts/lint.js --eslint` runs on every file the PR touches, and the new rpm dependency test spelled the two spaces in the pinned `echo` line as literals: `no-regex-spaces` rejects them, so Lint & Static failed on the incremental ESLint step. `[0-9a-f]{64} {2}` matches the same two spaces the workflow emits; the assertion still pins exactly what the release step writes. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmvzj4h0j9a * fix(desktop): keep the runtime group as a teardown target Teardown collected daemon pids through the zombie-aware liveness predicate and escalated SIGKILL only on leaders that were still alive, so a leader that had already exited — reaped, replaced, or an unreaped zombie — silently dropped its process group, which is teardown's only handle on it. The children the leader spawned (MCP servers, tool subprocesses) then survived and kept writing under the workspace about to be deleted, and the smoke reported a pass. Collect every recorded pid, escalate on every collected group after the drain, and keep the zombie-aware predicate on the "survived teardown" report so the false positive this PR exists to fix stays fixed. The new fixture drives a runtime whose leader exits while a writer in its group ignores SIGTERM. Also pins the EPEL download URL and digest literals, drops the duplicate local liveness predicate in favour of the shared helper, and covers the signal-probe error arm (ESRCH gone; EPERM/EACCES alive, the rule the core helper documents). Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmvzpjyde9j * fix(desktop): decide teardown on the runtime group, not its leader The round-2 escalation signals the recorded pid's group (`kill(-pid)`), but the verdict and the drain still asked `isProcessAlive(pid)`, which answers "gone" as soon as the leader exits — the exact topology `SMOKE_FIXTURE_EXIT_AFTER_SPAWN` reproduces. In that state `waitForExit` returned on its first poll, so the 5 s drain was skipped and the `survived teardown` branch was unreachable. `isTeardownTargetAlive` probes the group first and falls back to the leader where there is no group to signal (win32), so the drain and the leak verdict cover the children the group can still hold. Also drops the "mirrors the canonical … keep both in sync" claim from the desktop probe's comment: it diverges from core's `isPidAlive` on the pid <= 0 guard, the zombie exclusion on the EPERM/EACCES arm, and the vanished-/proc answer. The comment now states those differences and why teardown wants the dead-on-ENOENT direction. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmvzrp44z9n * fix(desktop): stop counting unreaped zombies as a live teardown group `waitForExit` asks `isTeardownTargetAlive` whether a daemon's group still exists. `kill(-pid, 0)` also answers for members that can no longer write, so a daemon that exited and that the container's PID 1 has not reaped kept its group "alive": the drain burned its five-second window, the second probe still returned the pid, and teardown reported "the packaged runtime survived teardown" for a runtime that was already gone. On Linux the group probe now counts only members that can still write: scan `/proc` for entries whose pgrp is the recorded pid, and report the group stopped when every member is a zombie. A scan that observes no member keeps the group probe's answer, so a hidden /proc cannot end a drain early, and the group handle still wins over the leader handle. Measured on a real zombie group (setsid + _exit, parent never reaps): /proc state Z, `kill(-pgid, 0)` exit 0, `isProcessAlive` false, and `isTeardownTargetAlive` true before this change, false after it. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmvzy4lf49w --------- Co-authored-by: Qwen-Coder <[email protected]>
94 lines
3.1 KiB
JavaScript
94 lines
3.1 KiB
JavaScript
import fs from 'node:fs';
|
|
|
|
// Teardown-only probe, deliberately narrower than the canonical
|
|
// packages/core/src/utils/process-liveness.ts: it takes no `pid <= 0` guard,
|
|
// skips the zombie exclusion on the EPERM/EACCES arm, and reads a vanished
|
|
// /proc entry as dead — the safe answer when the question is whether this
|
|
// script's own runtime leaked.
|
|
export function isProcessAlive(
|
|
pid,
|
|
{
|
|
platform = process.platform,
|
|
kill = process.kill,
|
|
readStat = (statPath) => fs.readFileSync(statPath, 'utf8'),
|
|
} = {},
|
|
) {
|
|
try {
|
|
kill(pid, 0);
|
|
} catch (error) {
|
|
// EPERM/EACCES: it exists under another owner.
|
|
return error?.code === 'EPERM' || error?.code === 'EACCES';
|
|
}
|
|
if (platform !== 'linux') return true;
|
|
try {
|
|
const stat = readStat(`/proc/${pid}/stat`);
|
|
const closingParen = stat.lastIndexOf(')');
|
|
if (closingParen < 0 || stat[closingParen + 1] !== ' ') return true;
|
|
return stat[closingParen + 2] !== 'Z';
|
|
} catch (error) {
|
|
return error?.code !== 'ENOENT';
|
|
}
|
|
}
|
|
|
|
// A daemon record's pid is also its process group, and the group outlives the
|
|
// leader that recorded it: after the leader exits a bare pid probe answers
|
|
// "gone" while the group's children keep writing under the workspace.
|
|
export function isTeardownTargetAlive(
|
|
pid,
|
|
{
|
|
platform = process.platform,
|
|
kill = process.kill,
|
|
readDir = (dir) => fs.readdirSync(dir),
|
|
readStat = (statPath) => fs.readFileSync(statPath, 'utf8'),
|
|
} = {},
|
|
) {
|
|
if (platform === 'win32') return isProcessAlive(pid, { platform, kill });
|
|
try {
|
|
kill(-pid, 0);
|
|
} catch (error) {
|
|
if (error?.code === 'ESRCH') return false;
|
|
if (error?.code === 'EPERM' || error?.code === 'EACCES') return true;
|
|
return isProcessAlive(pid, { platform, kill });
|
|
}
|
|
// The group probe also answers for members that can no longer write: an
|
|
// exited daemon the container's PID 1 has not reaped keeps its group
|
|
// "alive" as a zombie, which would fail teardown on a runtime that is gone.
|
|
if (platform !== 'linux') return true;
|
|
return groupHasLiveMember(pid, { readDir, readStat });
|
|
}
|
|
|
|
function groupHasLiveMember(pgid, { readDir, readStat }) {
|
|
let entries;
|
|
try {
|
|
entries = readDir('/proc');
|
|
} catch {
|
|
// Membership unknown: keep the group probe's answer rather than ending a
|
|
// drain on a failed scan.
|
|
return true;
|
|
}
|
|
let members = 0;
|
|
for (const entry of entries) {
|
|
if (!/^\d+$/.test(entry)) continue;
|
|
let stat;
|
|
try {
|
|
stat = readStat(`/proc/${entry}/stat`);
|
|
} catch {
|
|
// The member exited between readdir and read, which is the wanted state.
|
|
continue;
|
|
}
|
|
const closingParen = stat.lastIndexOf(')');
|
|
if (closingParen < 0) continue;
|
|
// `/proc/<pid>/stat` after `comm`: state, ppid, pgrp, ...
|
|
const fields = stat
|
|
.slice(closingParen + 2)
|
|
.trim()
|
|
.split(' ');
|
|
if (fields.length < 3 || Number(fields[2]) !== pgid) continue;
|
|
if (fields[0] !== 'Z') return true;
|
|
members += 1;
|
|
}
|
|
// Nothing observed is not the same as "only zombies": when /proc hides the
|
|
// group, the group probe that answered alive stays the better evidence.
|
|
return members === 0;
|
|
}
|