(
procs: ReadonlyArray<{
readonly cmd: string;
readonly args: ReadonlyArray<string>;
readonly cwd: string;
readonly env?: Record<string, string | undefined>;
/** Append stdout+stderr here (long-lived boots need inspectable logs). */
readonly logFile?: string;
}>,
options: { readonly label: string },
)
| 12 | } |
| 13 | |
| 14 | export const bootProcesses = ( |
| 15 | procs: ReadonlyArray<{ |
| 16 | readonly cmd: string; |
| 17 | readonly args: ReadonlyArray<string>; |
| 18 | readonly cwd: string; |
| 19 | readonly env?: Record<string, string | undefined>; |
| 20 | /** Append stdout+stderr here (long-lived boots need inspectable logs). */ |
| 21 | readonly logFile?: string; |
| 22 | }>, |
| 23 | options: { readonly label: string }, |
| 24 | ): BootedProcesses => { |
| 25 | const children: ChildProcess[] = []; |
| 26 | let tearingDown = false; |
| 27 | for (const proc of procs) { |
| 28 | const log = proc.logFile ? openSync(proc.logFile, "a") : undefined; |
| 29 | const child = spawn(proc.cmd, [...proc.args], { |
| 30 | cwd: proc.cwd, |
| 31 | env: { ...process.env, ...proc.env }, |
| 32 | stdio: |
| 33 | log !== undefined ? ["ignore", log, log] : process.env.E2E_VERBOSE ? "inherit" : "ignore", |
| 34 | // Own process group, so teardown can signal the whole tree — `bunx vite` |
| 35 | // is a wrapper whose actual server child would otherwise outlive the |
| 36 | // kill and squat the port into the NEXT invocation's waitForHttp. |
| 37 | detached: true, |
| 38 | }); |
| 39 | child.on("exit", (code) => { |
| 40 | if (code !== 0 && code !== null && !tearingDown) { |
| 41 | console.error(`[e2e:${options.label}] ${proc.cmd} exited with ${code}`); |
| 42 | } |
| 43 | }); |
| 44 | children.push(child); |
| 45 | } |
| 46 | |
| 47 | // Signal the process GROUP (negative pid); fall back to the direct child |
| 48 | // when the group is already gone. |
| 49 | const signalTree = (child: ChildProcess, signal: NodeJS.Signals) => { |
| 50 | if (child.pid === undefined || child.exitCode !== null) return; |
| 51 | try { |
| 52 | process.kill(-child.pid, signal); |
| 53 | } catch { |
| 54 | child.kill(signal); |
| 55 | } |
| 56 | }; |
| 57 | |
| 58 | const exited = (child: ChildProcess): Promise<void> => |
| 59 | child.exitCode !== null || child.signalCode !== null |
| 60 | ? Promise.resolve() |
| 61 | : new Promise((resolve) => child.once("exit", () => resolve())); |
| 62 | |
| 63 | return { |
| 64 | teardown: async () => { |
| 65 | tearingDown = true; |
| 66 | const allExited = Promise.all(children.map(exited)); |
| 67 | for (const child of children) signalTree(child, "SIGTERM"); |
| 68 | // Wait for a REAL exit (not a fixed sleep) — a lingering server would |
| 69 | // answer the next invocation's waitForHttp as a half-dead zombie. |
| 70 | const graceful = await Promise.race([ |
| 71 | allExited.then(() => true), |
no test coverage detected