Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions packages/provider-hyperframes-local/src/capture-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,16 @@ async function killRenderTree(pid: number): Promise<void> {
}
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]) {
Expand All @@ -40,13 +45,12 @@ async function killRenderTree(pid: number): Promise<void> {
// 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);
}
}
Expand Down Expand Up @@ -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;
Expand Down
30 changes: 29 additions & 1 deletion packages/provider-hyperframes-local/test/capture-process.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 });
}
});