| import { spawn } from "node:child_process"; |
| import net from "node:net"; |
| import path from "node:path"; |
| import process from "node:process"; |
|
|
| import { afterEach, describe, expect, it } from "vitest"; |
|
|
| import { attachChildProcessBridge } from "./child-process-bridge.js"; |
|
|
| function waitForLine(stream: NodeJS.ReadableStream, timeoutMs = 10_000): Promise<string> { |
| return new Promise((resolve, reject) => { |
| let buffer = ""; |
|
|
| const timeout = setTimeout(() => { |
| cleanup(); |
| reject(new Error("timeout waiting for line")); |
| }, timeoutMs); |
|
|
| const onData = (chunk: Buffer | string): void => { |
| buffer += chunk.toString(); |
| const idx = buffer.indexOf("\n"); |
| if (idx >= 0) { |
| const line = buffer.slice(0, idx).trim(); |
| cleanup(); |
| resolve(line); |
| } |
| }; |
|
|
| const onError = (err: unknown): void => { |
| cleanup(); |
| reject(err); |
| }; |
|
|
| const cleanup = (): void => { |
| clearTimeout(timeout); |
| stream.off("data", onData); |
| stream.off("error", onError); |
| }; |
|
|
| stream.on("data", onData); |
| stream.on("error", onError); |
| }); |
| } |
|
|
| function canConnect(port: number): Promise<boolean> { |
| return new Promise((resolve) => { |
| const socket = net.createConnection({ host: "127.0.0.1", port }); |
| socket.once("connect", () => { |
| socket.end(); |
| resolve(true); |
| }); |
| socket.once("error", () => resolve(false)); |
| }); |
| } |
|
|
| describe("attachChildProcessBridge", () => { |
| const children: Array<{ kill: (signal?: NodeJS.Signals) => boolean }> = []; |
| const detachments: Array<() => void> = []; |
|
|
| afterEach(() => { |
| for (const detach of detachments) { |
| try { |
| detach(); |
| } catch { |
| |
| } |
| } |
| detachments.length = 0; |
| for (const child of children) { |
| try { |
| child.kill("SIGKILL"); |
| } catch { |
| |
| } |
| } |
| children.length = 0; |
| }); |
|
|
| it("forwards SIGTERM to the wrapped child", async () => { |
| const childPath = path.resolve(process.cwd(), "test/fixtures/child-process-bridge/child.js"); |
|
|
| const beforeSigterm = new Set(process.listeners("SIGTERM")); |
| const child = spawn(process.execPath, [childPath], { |
| stdio: ["ignore", "pipe", "inherit"], |
| env: process.env, |
| }); |
| const { detach } = attachChildProcessBridge(child); |
| detachments.push(detach); |
| children.push(child); |
| const afterSigterm = process.listeners("SIGTERM"); |
| const addedSigterm = afterSigterm.find((listener) => !beforeSigterm.has(listener)); |
|
|
| if (!child.stdout) { |
| throw new Error("expected stdout"); |
| } |
| const portLine = await waitForLine(child.stdout); |
| const port = Number(portLine); |
| expect(Number.isFinite(port)).toBe(true); |
|
|
| expect(await canConnect(port)).toBe(true); |
|
|
| |
| if (!addedSigterm) { |
| throw new Error("expected SIGTERM listener"); |
| } |
| addedSigterm(); |
|
|
| await new Promise<void>((resolve, reject) => { |
| const timeout = setTimeout(() => reject(new Error("timeout waiting for child exit")), 10_000); |
| child.once("exit", () => { |
| clearTimeout(timeout); |
| resolve(); |
| }); |
| }); |
|
|
| await new Promise((r) => setTimeout(r, 250)); |
| expect(await canConnect(port)).toBe(false); |
| }, 20_000); |
| }); |
|
|