import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import { fileURLToPath } from "node:url"; import { z } from "zod"; import { waitForExit } from "../process/processFixture.js"; const START_TIMEOUT_MS = 20_000; const REQUEST_TIMEOUT_MS = 20_000; const STOP_TIMEOUT_MS = 1_000; const STDERR_TAIL_CHARS = 4_000; const workerPath = fileURLToPath( new URL("./compiledCliWorker.mjs", import.meta.url), ); const readyMessage = z.object({ ready: z.literal(true) }).strict(); const responseMessage = z .object({ id: z.number(), exitCode: z.number(), stdout: z.string(), error: z.string().optional(), }) .strict(); type CliResponse = z.infer; interface PendingResponse { readonly resolve: (response: CliResponse) => void; readonly reject: (cause: unknown) => void; } interface OwnedStop { readonly child: ChildProcessWithoutNullStreams; attempt: Promise | undefined; } interface SessionState { child: ChildProcessWithoutNullStreams | undefined; starting: Promise | undefined; buffer: string; nextId: number; readonly pending: Map; readonly stopping: OwnedStop[]; queue: Promise; stderr: string; closed: boolean; closing: Promise | undefined; settleStart: ((cause?: Error) => void) | undefined; ready: boolean; readonly requestTimeoutMs: number; readonly stopTimeoutMs: number; readonly spawnWorker: () => ChildProcessWithoutNullStreams; } /** One warm compiled CLI process serving production JSON commands. */ export interface CompiledCliSession { run(args: readonly string[]): Promise; close(): Promise; } /** Limits and process factory for session tests. Production callers omit them. */ export interface CompiledCliSessionOptions { readonly spawnWorker?: () => ChildProcessWithoutNullStreams; readonly requestTimeoutMs?: number; readonly stopTimeoutMs?: number; } /** Start a session that loads the compiled CLI once and reuses that process. */ export const startCompiledCliSession = ( options: CompiledCliSessionOptions = {}, ): CompiledCliSession => { const state = createSessionState(options); return { run: (args) => enqueue(state, () => dispatch(state, args)), close: () => closeSession(state), }; }; const positiveMilliseconds = ( value: number | undefined, fallback: number, ): number => value !== undefined && Number.isSafeInteger(value) && value > 0 ? value : fallback; const defaultSpawnWorker = (): ChildProcessWithoutNullStreams => spawn(process.execPath, [workerPath], { cwd: process.cwd(), env: { ...process.env, REA_LOG_LEVEL: "silent" }, stdio: ["pipe", "pipe", "pipe"], }); const createSessionState = ( options: CompiledCliSessionOptions, ): SessionState => ({ child: undefined, starting: undefined, buffer: "", nextId: 1, pending: new Map(), stopping: [], queue: Promise.resolve(), stderr: "", closed: false, closing: undefined, settleStart: undefined, ready: false, requestTimeoutMs: positiveMilliseconds( options.requestTimeoutMs, REQUEST_TIMEOUT_MS, ), stopTimeoutMs: positiveMilliseconds(options.stopTimeoutMs, STOP_TIMEOUT_MS), spawnWorker: options.spawnWorker ?? defaultSpawnWorker, }); const enqueue = ( state: SessionState, task: () => Promise, ): Promise => { const run = state.queue.then(task); state.queue = run.then( () => undefined, () => undefined, ); return run; }; const dispatch = async ( state: SessionState, args: readonly string[], ): Promise => { await ensureStarted(state); const child = state.child; if (child === undefined) throw new Error("Compiled CLI session is not running"); const id = state.nextId; state.nextId += 1; const response = await request(state, child, id, args); if (response.exitCode !== 0) throw new Error(failureText(state, response)); return JSON.parse(response.stdout) as unknown; }; const request = ( state: SessionState, child: ChildProcessWithoutNullStreams, id: number, args: readonly string[], ): Promise => new Promise((resolve, reject) => { let settled = false; let timer: ReturnType | undefined; const settle = (finish: () => void): void => { if (settled) return; settled = true; clearTimeout(timer); state.pending.delete(id); finish(); }; timer = setTimeout(() => { settle(() => { reject( new Error( `Compiled CLI command did not respond within ${String(state.requestTimeoutMs)} ms`, ), ); retireChild(state, child); }); }, state.requestTimeoutMs); timer.unref(); state.pending.set(id, { resolve: (response) => { settle(() => { resolve(response); }); }, reject: (cause) => { settle(() => { reject(cause); }); }, }); child.stdin.write(`${JSON.stringify({ id, args })}\n`, (cause) => { if (cause === undefined || cause === null) return; settle(() => { reject(cause); }); }); }); const failureText = (state: SessionState, response: CliResponse): string => { const detail = response.error ?? response.stdout.slice(0, 500); const stderr = state.stderr.trim(); return stderr.length === 0 ? `Compiled CLI command failed with status ${String(response.exitCode)}: ${detail}` : `Compiled CLI command failed with status ${String(response.exitCode)}: ${detail}\n${stderr}`; }; const ensureStarted = async (state: SessionState): Promise => { if (state.closed) throw sessionClosed(); await releaseRetired(state); if (state.closed) throw sessionClosed(); if (state.ready && state.child !== undefined) return; state.starting ??= launch(state); await state.starting; if (state.closed) throw sessionClosed(); }; const launch = (state: SessionState): Promise => new Promise((resolve, reject) => { const child = state.spawnWorker(); state.child = child; let settled = false; let timer: ReturnType | undefined; const finish = (cause?: Error): void => { if (settled) return; settled = true; clearTimeout(timer); if (state.settleStart === finish) state.settleStart = undefined; if (cause === undefined) resolve(); else reject(cause); }; timer = setTimeout(() => { signalChild(child, "SIGKILL"); finish( new Error( `Compiled CLI session did not start within ${String(START_TIMEOUT_MS)} ms\n${state.stderr}`, ), ); }, START_TIMEOUT_MS); timer.unref(); state.settleStart = finish; const fail = (cause: unknown): void => { finish(cause instanceof Error ? cause : new Error(String(cause))); }; const currentChild = (): boolean => state.child === child; child.once("error", fail); child.once("exit", (code, signal) => { if (!currentChild()) return; state.child = undefined; state.ready = false; state.starting = undefined; state.buffer = ""; const cause = new Error( `Compiled CLI session exited (${String(code)}, ${String(signal)})\n${state.stderr}`, ); fail(cause); rejectPending(state, cause); }); child.stderr.setEncoding("utf8"); child.stderr.on("data", (chunk: string) => { if (!currentChild()) return; state.stderr = `${state.stderr}${chunk}`.slice(-STDERR_TAIL_CHARS); }); child.stdout.setEncoding("utf8"); child.stdout.on("data", (chunk: string) => { if (!currentChild()) return; state.buffer += chunk; drainLines(state, () => { state.ready = true; finish(); }); }); }); const hasExited = (child: ChildProcessWithoutNullStreams): boolean => child.exitCode !== null || child.signalCode !== null; const signalChild = ( child: ChildProcessWithoutNullStreams, signal: NodeJS.Signals, ): void => { try { child.kill(signal); } catch (error) { const code = error instanceof Error && "code" in error ? error.code : undefined; if (code !== "ESRCH") throw error; } }; const detachChild = ( state: SessionState, child: ChildProcessWithoutNullStreams, ): void => { if (state.child !== child) return; state.child = undefined; state.ready = false; state.starting = undefined; state.buffer = ""; }; const stopOwned = (state: SessionState, entry: OwnedStop): Promise => { entry.attempt ??= (async () => { if (hasExited(entry.child)) return true; signalChild(entry.child, "SIGKILL"); return waitForExit(entry.child, state.stopTimeoutMs); })().finally(() => { entry.attempt = undefined; }); return entry.attempt; }; const trackRetired = ( state: SessionState, child: ChildProcessWithoutNullStreams, ): OwnedStop => { const existing = state.stopping.find((entry) => entry.child === child); if (existing !== undefined) return existing; const entry: OwnedStop = { child, attempt: undefined }; state.stopping.push(entry); if (!hasExited(child)) void stopOwned(state, entry).catch(() => undefined); return entry; }; const releaseRetired = async (state: SessionState): Promise => { let failed = false; for (const entry of [...state.stopping]) { if (hasExited(entry.child)) continue; const stopped = await stopOwned(state, entry).catch(() => false); if (!stopped || !hasExited(entry.child)) failed = true; } const retained = state.stopping.filter((entry) => !hasExited(entry.child)); state.stopping.length = 0; state.stopping.push(...retained); if (failed) throw new Error("Compiled CLI session stayed running after SIGKILL"); }; const retireChild = ( state: SessionState, child: ChildProcessWithoutNullStreams, ): void => { detachChild(state, child); if (hasExited(child)) return; trackRetired(state, child); }; const drainLines = (state: SessionState, onReady: () => void): void => { let newline = state.buffer.indexOf("\n"); while (newline !== -1) { const line = state.buffer.slice(0, newline); state.buffer = state.buffer.slice(newline + 1); if (line.length > 0) acceptLine(state, line, onReady); newline = state.buffer.indexOf("\n"); } }; const acceptLine = ( state: SessionState, line: string, onReady: () => void, ): void => { let message: unknown; try { message = JSON.parse(line) as unknown; } catch (cause) { rejectPending( state, cause instanceof Error ? cause : new Error(String(cause)), ); return; } if (readyMessage.safeParse(message).success) { onReady(); return; } const parsed = responseMessage.safeParse(message); if (!parsed.success) { rejectPending( state, new Error("Compiled CLI session returned an unexpected message"), ); return; } const pending = state.pending.get(parsed.data.id); state.pending.delete(parsed.data.id); if (pending === undefined) { rejectPending( state, new Error( `Compiled CLI session returned unexpected id ${String(parsed.data.id)}`, ), ); return; } pending.resolve(parsed.data); }; const rejectPending = (state: SessionState, cause: Error): void => { for (const pending of state.pending.values()) pending.reject(cause); state.pending.clear(); }; const closeSession = (state: SessionState): Promise => { state.closed = true; if (state.closing !== undefined) return state.closing; let attempt: Promise; attempt = shutdown(state).catch((error: unknown) => { if (state.closing === attempt) state.closing = undefined; throw error; }); state.closing = attempt; return attempt; }; const shutdown = async (state: SessionState): Promise => { const child = state.child; let forced = false; const stillRunning: OwnedStop[] = []; if (child !== undefined) { rejectPending(state, sessionClosed()); state.settleStart?.(sessionClosed()); detachChild(state, child); const stopped = await stopGracefully(state, child); if (stopped === "forced") forced = true; if (stopped === "stuck" && !hasExited(child)) stillRunning.push({ child, attempt: undefined }); } try { await releaseRetired(state); } finally { state.stopping.push(...stillRunning); } if (state.stopping.length > 0) throw new Error("Compiled CLI session stayed running after SIGKILL"); if (forced) throw new Error("Compiled CLI session did not exit after stdin closed"); }; const stopGracefully = async ( state: SessionState, child: ChildProcessWithoutNullStreams, ): Promise<"clean" | "forced" | "stuck"> => { child.stdin.end(); if (await waitForExit(child, state.stopTimeoutMs)) return "clean"; signalChild(child, "SIGTERM"); if (await waitForExit(child, state.stopTimeoutMs)) return "forced"; signalChild(child, "SIGKILL"); if (await waitForExit(child, state.stopTimeoutMs)) return "forced"; return "stuck"; }; const sessionClosed = (): Error => new Error("Compiled CLI session is closed");