From f93ec5c55ceab6b11eb13a06fbeebd7fa745ae6c Mon Sep 17 00:00:00 2001 From: liuguanghui <2285190803@qq.com> Date: Wed, 16 Sep 2026 07:33:55 +0800 Subject: [PATCH] fix: clean up render processes without ps --- .../src/capture-process.ts | 26 +++++++++------- .../test/capture-process.test.ts | 30 ++++++++++++++++++- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/packages/provider-hyperframes-local/src/capture-process.ts b/packages/provider-hyperframes-local/src/capture-process.ts index b932578a..f198a09b 100644 --- a/packages/provider-hyperframes-local/src/capture-process.ts +++ b/packages/provider-hyperframes-local/src/capture-process.ts @@ -25,11 +25,16 @@ async function killRenderTree(pid: number): Promise { } return; } - const { stdout } = await exec("ps", ["-A", "-o", "pid=,ppid="], { timeout: cleanupMs }); - const rows = stdout.trim().split("\n").map((line) => line.trim().split(/\s+/u).map(Number)); const descendants = [pid]; for (let i = 0; i < descendants.length; i++) { - for (const [child, parent] of rows) if (parent === descendants[i] && child !== undefined) descendants.push(child); + const children = await exec("pgrep", ["-P", String(descendants[i])], { timeout: cleanupMs }) + .then(({ stdout }) => stdout.trim().split(/\s+/u).filter(Boolean).map(Number), (error) => { + if ((error as { code?: string | number }).code === 1) return []; + throw error; + }); + for (const child of children) { + if (Number.isInteger(child) && !descendants.includes(child)) descendants.push(child); + } } for (const child of descendants.reverse()) { for (const target of [-child, child]) { @@ -40,13 +45,12 @@ async function killRenderTree(pid: number): Promise { // parent is still alive to reap its child; zombies no longer hold resources. const deadline = Date.now() + cleanupMs; while (true) { - const state = await exec("ps", ["-p", String(child), "-o", "stat="], { timeout: cleanupMs }) - .then(({ stdout }) => stdout.trim(), (error) => { - if (error.code === 1 && !error.stdout?.trim()) return ""; - throw error; - }); - if (state === "" || state.startsWith("Z")) break; - if (Date.now() >= deadline) throw new Error(`Render process ${child} did not stop after SIGKILL`); + try { process.kill(child, 0); } + catch (error) { + if ((error as NodeJS.ErrnoException).code === "ESRCH") break; + throw error; + } + if (Date.now() >= deadline) break; await delay(20); } } @@ -76,7 +80,7 @@ export async function runCaptureProcess( const kill = () => { if (grace !== undefined) clearTimeout(grace); killing ??= (child.pid === undefined ? Promise.resolve() : killRenderTree(child.pid)).catch((error) => { - failure = new Error(`${failure?.message ?? "Render cleanup failed"}; ${String(error)}`); + if (!completed) failure = new Error(`${failure?.message ?? "Render cleanup failed"}; ${String(error)}`); child.kill("SIGKILL"); }); return killing; diff --git a/packages/provider-hyperframes-local/test/capture-process.test.ts b/packages/provider-hyperframes-local/test/capture-process.test.ts index e95e9351..82f921d6 100644 --- a/packages/provider-hyperframes-local/test/capture-process.test.ts +++ b/packages/provider-hyperframes-local/test/capture-process.test.ts @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; @@ -59,3 +59,31 @@ test("renderer stdout and stderr diagnostics are drained before reporting succes assert.ok(messages.some((item) => item.stream === "stderr" && item.message.includes("render diagnostic"))); } finally { await rm(root, { recursive: true, force: true }); } }); + +test("a completed render remains successful when ps is unavailable", async () => { + if (process.platform === "win32") return; + const root = await mkdtemp(join(tmpdir(), "hypit-render-no-ps-")); + const previousPath = process.env.PATH; + try { + const entry = join(root, "complete.mjs"); + const ps = join(root, "ps"); + const pgrep = join(root, "pgrep"); + await Promise.all([ + writeFile(entry, "process.once('message', () => process.send({ type: 'completed' }));"), + writeFile(ps, "#!/bin/sh\nexit 126\n"), + writeFile(pgrep, "#!/bin/sh\nexit 1\n"), + ]); + await Promise.all([chmod(ps, 0o755), chmod(pgrep, 0o755)]); + process.env.PATH = root; + + await runCaptureProcess( + { config: resolveExecutionOptions({}) } as CaptureInput, + new AbortController().signal, + () => {}, + pathToFileURL(entry), + ); + } finally { + process.env.PATH = previousPath; + await rm(root, { recursive: true, force: true }); + } +});