|
| 1 | +// Kill -9 the warm process (or a cold check) mid busy-loop; count every chrome-headless-shell process 5 s later. |
| 2 | +import { execSync, spawn } from "node:child_process"; |
| 3 | +import { existsSync, mkdirSync, mkdtempSync, readdirSync, writeFileSync } from "node:fs"; |
| 4 | +import { tmpdir } from "node:os"; |
| 5 | +import { join } from "node:path"; |
| 6 | +const win = process.platform === "win32"; |
| 7 | +const cli = join(process.cwd(), "packages/cli"); |
| 8 | +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); |
| 9 | +function pids() { |
| 10 | + try { |
| 11 | + return win |
| 12 | + ? execSync('tasklist /FI "IMAGENAME eq chrome-headless-shell.exe" /NH /FO CSV', { encoding: "utf8" }).split("\n").filter((l) => /chrome-headless-shell/i.test(l)).map((l) => Number(l.split(",")[1]?.replace(/"/g, ""))) |
| 13 | + : execSync("pgrep -f chrome-headless-shell || true", { encoding: "utf8" }).split("\n").filter(Boolean).map(Number); |
| 14 | + } catch { return []; } |
| 15 | +} |
| 16 | +const count = () => pids().length; |
| 17 | +function hangProject() { |
| 18 | + const dir = mkdtempSync(join(tmpdir(), "hf-orphan-")); |
| 19 | + mkdirSync(join(dir, "compositions")); |
| 20 | + writeFileSync(join(dir, "index.html"), `<!doctype html><html><body><div data-composition-id="main" data-width="320" data-height="180" data-duration="1"><div data-composition-id="hang" data-composition-src="compositions/hang.html" data-start="0" data-duration="1"></div></div><script>window.__timelines={main:{duration:()=>1,seek:()=>{},pause:()=>{}}};</script></body></html>`); |
| 21 | + writeFileSync(join(dir, "compositions/hang.html"), `<template id="hang"><div data-composition-id="hang"><div id="box" style="width:1px;height:1px"></div><script>const el=document.getElementById("box");while(el.offsetWidth<2){el.style.height="1px";}</script></div></template>`); |
| 22 | + return dir; |
| 23 | +} |
| 24 | +const serving = (dir) => existsSync(join(dir, ".hyperframes")) && readdirSync(join(dir, ".hyperframes")).length > 0; |
| 25 | +async function waitFor(cond, ms) { const end = Date.now() + ms; while (!cond()) { if (Date.now() > end) return false; await sleep(200); } return true; } |
| 26 | +const env = { ...process.env, HYPERFRAMES_NO_TELEMETRY: "1", CI: "" }; |
| 27 | +delete env.CI; |
| 28 | +async function scenario(kind) { |
| 29 | + const dir = hangProject(); |
| 30 | + const base = count(); |
| 31 | + let victim; |
| 32 | + if (kind === "warm") { |
| 33 | + victim = spawn(process.execPath, ["dist/warmCheck.js", "60000", dir, "--json", "--samples=1", "--no-browser-gpu"], { cwd: cli, env, stdio: "ignore" }); |
| 34 | + await waitFor(() => count() > base, 30_000); |
| 35 | + await sleep(2000); |
| 36 | + const client = spawn(process.execPath, ["bin/hyperframes.mjs", "check", dir, "--json", "--samples=1", "--no-browser-gpu"], { cwd: cli, env, stdio: ["ignore", "pipe", "pipe"] }); |
| 37 | + client.stdout.on("data", () => {}); client.stderr.on("data", () => {}); |
| 38 | + } else { |
| 39 | + victim = spawn(process.execPath, ["bin/hyperframes.mjs", "check", dir, "--json", "--samples=1", "--no-browser-gpu"], { cwd: cli, env: { ...env, CI: "1" }, stdio: ["ignore", "pipe", "pipe"] }); |
| 40 | + victim.stdout.on("data", () => {}); victim.stderr.on("data", () => {}); |
| 41 | + } |
| 42 | + const busy = await waitFor(() => serving(dir), 60_000); |
| 43 | + await sleep(3000); |
| 44 | + const during = count(); |
| 45 | + process.kill(victim.pid, "SIGKILL"); |
| 46 | + await sleep(5000); |
| 47 | + const after = count(); |
| 48 | + console.log(`ORPHANS ${process.platform} ${kind}: busy=${busy} baseline=${base} during=${during} 5s-after-kill=${after} ${after > base ? "SURVIVORS" : "clean"}`); |
| 49 | + // clean up whatever survived so the next scenario starts from zero |
| 50 | + for (const pid of pids()) { try { process.kill(pid, "SIGKILL"); } catch {} } |
| 51 | + await sleep(2000); |
| 52 | +} |
| 53 | +for (const kind of ["cold", "warm", "cold", "warm"]) await scenario(kind); |
0 commit comments