Files
hyperframes/packages/cli/src/warmCheckClient.ts
T
Miguel Ángel 6cf4b56697 fix(engine): a render on Windows opens no console windows (#6613)
* ci(windows): probe that a render opens no visible window

* ci(windows): the probe names what opened each window

* ci(windows): the probe spawns each binary alone, with and without detached

* ci(windows): the probe logs the processes started during the render

* ci(windows): the probe also spawns Chrome one consoleless hop down

* ci(windows): the probe logs each child started without windowsHide

* ci(windows): the probe logs every child process with its windowsHide and detached

* fix(engine): Chrome keeps the CLI's console on Windows, so a render opens no window

* fix(cli): every child the CLI starts on Windows is hidden unless it opts out

* ci(windows): the probe bisects Chrome's flags and keeps each command's output

* ci(windows): the probe names the Chrome process behind each window

* ci(windows): the probe loads a page in Chrome directly and one hidden hop down

* ci(windows): the probe prints the Chrome main command line and renders with the reporter's flags

* ci(windows): the probe narrows the CLI's Chrome arguments to the ones that open a window

* ci(windows): the probe launches the CLI's Chrome arguments one hidden hop down

* ci(windows): the probe launches Chrome through the engine's browser pool

* ci(windows): the probe varies what the engine passes to Puppeteer

* fix(engine): renders on Windows launch Chrome with a Puppeteer that keeps it attached

* refactor(cli): hideChildConsoles copies its arguments and keeps one comment line
2026-10-28 07:48:40 +00:00

147 lines
4.8 KiB
TypeScript

import "./utils/hideChildConsoles.js";
import { spawn } from "node:child_process";
import { randomBytes } from "node:crypto";
import { dirname, join } from "node:path";
import { connect, type Socket } from "node:net";
import { constants } from "node:os";
import { fileURLToPath } from "node:url";
import {
Frame,
HANDSHAKE_TIMEOUT_MS,
WARM_IDLE_MS,
encodeFrame,
envDigest,
frameReader,
keyed,
sameProof,
warmCheckEndpoint,
type WarmRequest,
} from "./warmCheckProtocol.js";
const distDir = dirname(fileURLToPath(import.meta.url));
const FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"] as const;
const launchEnv = { ...process.env };
export function warmCheckEligible(
argv = process.argv,
env = process.env,
stdoutTTY = process.stdout.isTTY,
stderrTTY = process.stderr.isTTY,
): boolean {
return argv[2] === "check" && !stdoutTTY && !stderrTTY && !env.CI && !argv.includes("--help");
}
export function warmCheckOutcome(
exitCode: number | undefined,
wrote: boolean,
signalled: NodeJS.Signals | undefined,
): { served: boolean; exitCode?: number; message?: string } {
if (exitCode !== undefined) return { served: true, exitCode };
if (signalled) return { served: true, exitCode: 128 + (constants.signals[signalled] ?? 0) };
if (!wrote) return { served: false };
return {
served: true,
exitCode: 1,
message: "The warm check process stopped before finishing.\n",
};
}
function connected(address: string): Promise<Socket | undefined> {
return new Promise((resolveSocket) => {
const socket = connect(address);
socket.once("connect", () => resolveSocket(socket));
socket.once("error", () => resolveSocket(undefined));
});
}
interface ServerProof {
proof?: unknown;
nonce?: unknown;
}
function parsedProof(type: Frame, body: Buffer): ServerProof | undefined {
if (type !== Frame.Proof) return undefined;
try {
return JSON.parse(body.toString()) as ServerProof;
} catch {
return undefined;
}
}
export async function runWarmCheck(endpoint = warmCheckEndpoint(launchEnv)): Promise<boolean> {
if (!endpoint) return false;
const socket = await connected(endpoint.address);
if (!socket) return false;
const nonce = randomBytes(16).toString("hex");
let trusted = false;
let wrote = false;
let signalled: (typeof FORWARDED_SIGNALS)[number] | undefined;
const relay = (signal: (typeof FORWARDED_SIGNALS)[number]) => {
signalled = signal;
if (trusted) socket.write(encodeFrame(Frame.Signal, signal));
else socket.destroy();
};
for (const signal of FORWARDED_SIGNALS) process.on(signal, relay);
try {
return await new Promise<boolean>((resolveRun) => {
let exitCode: number | undefined;
const sinks = new Map<Frame, NodeJS.WritableStream>([
[Frame.Stdout, process.stdout],
[Frame.Stderr, process.stderr],
]);
const handshake = setTimeout(() => socket.destroy(), HANDSHAKE_TIMEOUT_MS);
const trust = (type: Frame, body: Buffer) => {
const server = parsedProof(type, body);
if (!server || !sameProof(keyed(endpoint.token, "server", nonce), server.proof)) {
socket.destroy();
return;
}
trusted = true;
clearTimeout(handshake);
const request: WarmRequest = {
argv: process.argv.slice(2),
script: process.argv[1] ?? "",
cwd: process.cwd(),
envDigest: envDigest(endpoint.token, launchEnv),
proof: keyed(endpoint.token, "client", String(server.nonce)),
};
socket.write(encodeFrame(Frame.Request, JSON.stringify(request)));
};
const read = frameReader((type, body) => {
if (!trusted) return trust(type, body);
if (type === Frame.Exit) exitCode = Number(body.toString());
const sink = sinks.get(type);
if (!sink) return;
sink.write(body);
wrote = true;
});
socket.on("data", (chunk: Buffer) => {
if (!read(chunk)) socket.destroy();
});
socket.on("error", () => {});
socket.on("close", () => {
clearTimeout(handshake);
const outcome = warmCheckOutcome(exitCode, wrote, signalled);
if (outcome.message) process.stderr.write(outcome.message);
if (outcome.served) process.exitCode = outcome.exitCode;
resolveRun(outcome.served);
});
socket.write(encodeFrame(Frame.Hello, nonce));
});
} finally {
for (const signal of FORWARDED_SIGNALS) process.off(signal, relay);
}
}
export function startNextWarmCheck(endpoint = warmCheckEndpoint(launchEnv)): void {
if (!endpoint) return;
const args = [String(WARM_IDLE_MS), ...process.argv.slice(3)];
spawn(process.execPath, [join(distDir, "warmCheck.js"), ...args], {
cwd: process.cwd(),
env: launchEnv,
detached: true,
stdio: "ignore",
windowsHide: true,
}).unref();
}