// runtime.onProcesses / runtime.kill(pid): an embedder sees the processes a guest spawns // (npm run dev, then its server) and which of them owns each port. import { test, expect } from "@playwright/test"; import "./guest"; const PARENT = ` const { spawn } = require("node:child_process"); const child = spawn("node", ["child.cjs", "--flag"], { cwd: "/app/srv", stdio: "inherit" }); child.on("exit", (code, signal) => console.log("child", code, signal)); `; const CHILD = ` const http = require("node:http"); http.createServer((req, res) => res.end("ok")).listen(4600, () => { const extra = http.createServer((req, res) => res.end()); extra.listen(4601, () => setTimeout(() => extra.close(), 50)); }); `; test("the table tracks a guest's child, its ports, and its end by kill(pid)", async ({ page }) => { await page.goto("/demo/fixture.html"); await page.waitForFunction(() => window.__runtime !== undefined, undefined, { timeout: 20_000 }); const result = await page.evaluate(async ({ parent, child }) => { const runtime = window.__runtime!; runtime.writeTree({ "/app/parent.cjs": parent, "/app/srv/child.cjs": child }); const tables: unknown[] = []; runtime.onProcesses((table) => tables.push(table)); const proc = runtime.exec("/app/parent.cjs", { argv: ["a"] }); const pid = await proc.pid; const url = await runtime.waitForPort(4600); await new Promise((resolve) => { const off = runtime.onUnlistening((port) => { if (port === 4601) { off(); resolve(); } }); }); runtime.kill(pid + 1); const code = await proc.exited; const status = (await fetch(url)).status; let out = ""; const reader = proc.stdout.getReader(); for (let r = await reader.read(); !r.done; r = await reader.read()) out += new TextDecoder().decode(r.value); return { pid, tables, code, status, out }; }, { parent: PARENT, child: CHILD }); const parentRow = { pid: 1, ppid: null, argv: ["node", "/app/parent.cjs", "a"], cwd: "/app", ports: [] }; const childRow = (ports: number[]) => ({ pid: 2, ppid: 1, argv: ["node", "/app/srv/child.cjs", "--flag"], cwd: "/app/srv", ports }); expect(result.pid).toBe(1); expect(result.tables).toEqual([ [], [parentRow], [parentRow, childRow([])], [parentRow, childRow([4600])], [parentRow, childRow([4600, 4601])], [parentRow, childRow([4600])], [parentRow, childRow([])], [parentRow], [], ]); expect(result.out).toBe("child null SIGTERM\n"); expect(result.code).toBe(0); expect(result.status).toBe(502); }); test("a late subscriber gets the current table, and kill(pid) takes a signal", async ({ page }) => { await page.goto("/demo/fixture.html"); await page.waitForFunction(() => window.__runtime !== undefined, undefined, { timeout: 20_000 }); const result = await page.evaluate(async () => { const runtime = window.__runtime!; runtime.writeTree({ "/app/srv.cjs": 'require("node:http").createServer((q, s) => s.end()).listen(4602);' }); const proc = runtime.exec("/app/srv.cjs"); await runtime.waitForPort(4602); const first = await new Promise((resolve) => { const off = runtime.onProcesses((table) => { off(); resolve(table); }); }); runtime.kill(await proc.pid, "SIGINT"); runtime.kill(99); return { first, code: await proc.exited, signal: await proc.signal }; }); expect(result).toEqual({ first: [{ pid: 1, ppid: null, argv: ["node", "/app/srv.cjs"], cwd: "/app", ports: [4602] }], code: 130, signal: "SIGINT", }); });