// Packaged desktop supervised-daemon regressions. These run against the real// electron-builder bundle and its bundled executor because the supervised attach// path is production-only (`app.isPackaged`).import { type ChildProcess, execFile, execFileSync, spawn } from "node:child_process";import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync,} from "node:fs";import { createServer, type IncomingMessage } from "node:http";import net from "node:net";import { homedir, tmpdir } from "node:os";import { dirname, join } from "node:path";import { promisify } from "node:util"; import { expect, it } from "@effect/vitest";import { Effect } from "effect";import { normalizeExecutorServerConnection, serializeExecutorLocalServerManifest,} from "@executor-js/sdk/shared"; import { scenario } from "../src/scenario";import { RunDir } from "../src/services";import { waitForHttp } from "../setup/boot"; const execFileAsync = promisify(execFile);const SERVICE_LABEL = "sh.executor.daemon"; interface PackagedExecutorBridge { readonly getSettings: () => Promise<{ readonly port: number }>; readonly updateSettings: (patch: { readonly port: number }) => Promise<unknown>; readonly restartServer: () => Promise<unknown>; readonly getServerConnection: () => Promise<{ readonly origin: string } | null>;} interface PackagedApp { readonly child: ChildProcess; cdp: CdpPage; readonly debugPort: string; readonly output: () => string;} interface CdpResponse<T> { readonly id: number; readonly result?: T; readonly error?: { readonly message?: string; readonly data?: string };} interface CdpEvaluateResult { readonly result: { readonly value?: unknown }; readonly exceptionDetails?: unknown;} interface CdpTarget { readonly type: string; readonly url: string; readonly webSocketDebuggerUrl?: string;} class CdpPage { private nextId = 1; private readonly pending = new Map< number, { readonly resolve: (value: unknown) => void; readonly reject: (error: Error) => void; } >(); private constructor(private readonly socket: WebSocket) { socket.addEventListener("message", (event) => { const data = event.data; if (typeof data !== "string") return; const message = JSON.parse(data) as CdpResponse<unknown>; if (!message.id) return; const pending = this.pending.get(message.id); if (!pending) return; this.pending.delete(message.id); if (message.error) { pending.reject(new Error(message.error.message ?? "CDP command failed")); return; } pending.resolve(message.result); }); socket.addEventListener("close", () => { for (const [, pending] of this.pending) { pending.reject(new Error("CDP socket closed")); } this.pending.clear(); }); } static connect = (url: string): Promise<CdpPage> => new Promise((resolve, reject) => { const socket = new WebSocket(url); const timer = setTimeout(() => { socket.close(); // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: WebSocket connection promise adapter reject(new Error(`Timed out connecting to page CDP target ${url}`)); }, 30_000); socket.addEventListener( "open", () => { clearTimeout(timer); resolve(new CdpPage(socket)); }, { once: true }, ); socket.addEventListener( "error", () => { clearTimeout(timer); // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: WebSocket connection promise adapter reject(new Error(`Failed to connect to page CDP target ${url}`)); }, { once: true }, ); }); command = async <T>(method: string, params: Record<string, unknown> = {}): Promise<T> => { const id = this.nextId; this.nextId += 1; const result = new Promise<T>((resolve, reject) => { this.pending.set(id, { resolve: (value) => resolve(value as T), reject, }); }); this.socket.send(JSON.stringify({ id, method, params })); return result; }; evaluate = async <T>(expression: string): Promise<T> => { const result = await this.command<CdpEvaluateResult>("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true, }); if (result.exceptionDetails) { throw new Error(`CDP evaluation failed: ${JSON.stringify(result.exceptionDetails)}`); } return result.result.value as T; }; waitForText = async (text: string, timeoutMs: number): Promise<void> => { const deadline = Date.now() + timeoutMs; const expression = `document.body?.innerText.includes(${JSON.stringify(text)}) ?? false`; for (;;) { if (await this.evaluate<boolean>(expression).catch(() => false)) return; if (Date.now() >= deadline) throw new Error(`Timed out waiting for text: ${text}`); await new Promise((resolve) => setTimeout(resolve, 250)); } }; waitForExpression = async ( expression: string, timeoutMs: number, description: string, ): Promise<void> => { const deadline = Date.now() + timeoutMs; for (;;) { if (await this.evaluate<boolean>(`Boolean(${expression})`).catch(() => false)) return; if (Date.now() >= deadline) throw new Error(`Timed out waiting for ${description}`); await new Promise((resolve) => setTimeout(resolve, 250)); } }; textPresent = async (text: string): Promise<boolean> => this.evaluate<boolean>(`document.body?.innerText.includes(${JSON.stringify(text)}) ?? false`); setViewport = async (width: number, height: number): Promise<void> => { await this.command("Emulation.setDeviceMetricsOverride", { width, height, deviceScaleFactor: 1, mobile: false, }); }; wheel = async (x: number, y: number, deltaY: number): Promise<void> => { await this.command("Input.dispatchMouseEvent", { type: "mouseWheel", x, y, deltaX: 0, deltaY, }); }; screenshot = async (path: string): Promise<void> => { const result = await this.command<{ readonly data: string }>("Page.captureScreenshot", { format: "png", fromSurface: true, }); writeFileSync(path, Buffer.from(result.data, "base64")); }; close = (): void => { this.socket.close(); };} declare global { interface Window { readonly executor: PackagedExecutorBridge; }} const appExe = process.env.E2E_DESKTOP_APP_EXE;const executorBin = process.env.E2E_DESKTOP_EXECUTOR_BIN; const guiAvailable = (): boolean => { if (process.platform === "darwin") { try { return execFileSync("launchctl", ["managername"], { encoding: "utf8" }).trim() === "Aqua"; } catch { return false; } } if (process.platform === "linux") return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY); return true;}; const packagedSingleInstanceAvailable = (): boolean => { if (process.platform !== "darwin" || !appExe) return true; try { const lines = execFileSync("pgrep", ["-fl", "Executor.app/Contents/MacOS/Executor"], { encoding: "utf8", }) .split("\n") .filter(Boolean); return !lines.some((line) => !line.includes(appExe)); } catch { return true; }}; const requireBundle = (): { readonly app: string; readonly executor: string } => { if (!appExe || !executorBin) { throw new Error( "E2E_DESKTOP_APP_EXE / E2E_DESKTOP_EXECUTOR_BIN not set — did desktop-packaged.globalsetup run?", ); } return { app: appExe, executor: executorBin };}; const currentUid = (): number => { const getuid = (process as { readonly getuid?: () => number }).getuid; return typeof getuid === "function" ? getuid.call(process) : 0;}; const serviceTarget = (): string => `gui/${currentUid()}/${SERVICE_LABEL}`;const launchAgentPath = (): string => join(homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);const isolatedDesktopSettingsDir = (home: string): string => join(home, ".executor-desktop-settings");const desktopSettingsDirs = (home: string): readonly string[] => { if (process.platform === "darwin") { const support = join(home, "Library", "Application Support"); return [ isolatedDesktopSettingsDir(home), join(support, "@executor-js", "desktop"), join(support, "Executor"), ]; } if (process.platform === "linux") { return [ isolatedDesktopSettingsDir(home), join(home, ".config", "@executor-js", "desktop"), join(home, ".config", "Executor"), ]; } const roaming = join(home, "AppData", "Roaming"); return [ isolatedDesktopSettingsDir(home), join(roaming, "@executor-js", "desktop"), join(roaming, "Executor"), ];}; const packagedAppEnv = (home: string): NodeJS.ProcessEnv => { return { ...process.env, HOME: home, EXECUTOR_DESKTOP_SETTINGS_DIR: isolatedDesktopSettingsDir(home), };}; interface LaunchdServiceSnapshot { readonly plist: string | null; readonly wasLoaded: boolean;} const launchctl = async (args: ReadonlyArray<string>): Promise<boolean> => { try { await execFileAsync("launchctl", [...args]); return true; } catch { return false; }}; const captureLaunchdService = (): LaunchdServiceSnapshot | null => { if (process.platform !== "darwin") return null; const path = launchAgentPath(); const plist = existsSync(path) ? readFileSync(path, "utf8") : null; let wasLoaded = false; try { execFileSync("launchctl", ["print", serviceTarget()], { stdio: "ignore" }); wasLoaded = true; } catch { wasLoaded = false; } return { plist, wasLoaded };}; const restoreLaunchdService = async (snapshot: LaunchdServiceSnapshot | null): Promise<void> => { if (!snapshot) return; const target = serviceTarget(); await launchctl(["bootout", target]); const path = launchAgentPath(); if (snapshot.plist === null) { rmSync(path, { force: true }); return; } mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, snapshot.plist, { mode: 0o600 }); chmodSync(path, 0o600); await launchctl(["enable", target]); if (snapshot.wasLoaded) { const bootstrapped = await launchctl(["bootstrap", `gui/${currentUid()}`, path]); if (bootstrapped) await launchctl(["kickstart", "-k", target]); }}; const freePort = (): Promise<number> => new Promise((resolve, reject) => { const srv = net.createServer(); srv.on("error", reject); srv.listen(0, "127.0.0.1", () => { const port = (srv.address() as net.AddressInfo).port; srv.close(() => resolve(port)); }); }); interface DaemonStart { readonly child: ChildProcess; readonly ready: boolean; readonly stderr: string;} const startSupervisedDaemon = ( env: NodeJS.ProcessEnv, port: number, hostname = "127.0.0.1",): Promise<DaemonStart> => new Promise((resolve) => { const { executor } = requireBundle(); const child = spawn( executor, ["daemon", "run", "--foreground", "--port", String(port), "--hostname", hostname], { env, stdio: ["ignore", "pipe", "pipe"] }, ); let stderr = ""; let settled = false; const settle = (ready: boolean) => { if (settled) return; settled = true; resolve({ child, ready, stderr }); }; const timer = setTimeout(() => settle(false), 60_000); child.stdout.on("data", (chunk: Buffer) => { if (/Daemon ready on http:\/\//.test(chunk.toString())) { clearTimeout(timer); settle(true); } }); child.stderr.on("data", (chunk: Buffer) => { stderr += chunk.toString(); }); child.on("exit", () => { clearTimeout(timer); settle(false); }); }); const stopProcess = async (child: ChildProcess | undefined): Promise<void> => { if (!child || child.exitCode !== null || child.signalCode !== null) return; await new Promise<void>((resolve) => { const timeout = setTimeout(() => { child.kill("SIGKILL"); resolve(); }, 5_000); child.once("exit", () => { clearTimeout(timeout); resolve(); }); child.kill("SIGTERM"); });}; const waitForPageWebSocket = async (debugPort: string): Promise<string> => { const deadline = Date.now() + 120_000; for (;;) { const targets = (await fetch(`http://127.0.0.1:${debugPort}/json/list`) .then((response) => (response.ok ? response.json() : [])) .catch(() => [])) as ReadonlyArray<CdpTarget>; const page = targets.find( (target) => target.type === "page" && target.webSocketDebuggerUrl && !target.url.startsWith("devtools://"), ); if (page?.webSocketDebuggerUrl) return page.webSocketDebuggerUrl; if (Date.now() >= deadline) { throw new Error("Timed out waiting for packaged app page CDP target"); } await new Promise((resolve) => setTimeout(resolve, 100)); }}; const launchPackaged = async (home: string): Promise<PackagedApp> => { const { app } = requireBundle(); let output = ""; let settled = false; const child = spawn(app, ["--remote-debugging-port=0"], { env: packagedAppEnv(home), stdio: ["ignore", "pipe", "pipe"], }); const browserCdpUrl = await new Promise<string>((resolve, reject) => { const timer = setTimeout(() => { // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: packaged-app launch promise adapter reject(new Error(`Timed out waiting for packaged app CDP URL\n${output}`)); }, 120_000); const settle = (fn: () => void) => { if (settled) return; settled = true; clearTimeout(timer); fn(); }; const collectOutput = (chunk: Buffer) => { const text = chunk.toString(); output = (output + text).slice(-16_384); const match = output.match(/DevTools listening on (ws:\/\/[^\s]+)/); if (match) settle(() => resolve(match[1])); }; child.stdout?.on("data", collectOutput); child.stderr?.on("data", collectOutput); // oxlint-disable-next-line executor/no-promise-reject -- boundary: packaged-app launch promise adapter child.once("error", (error) => settle(() => reject(error))); child.once("exit", (code, signal) => settle(() => // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: packaged-app launch promise adapter reject( new Error(`Packaged app exited before CDP (code=${code} signal=${signal})\n${output}`), ), ), ); }); const debugPort = new URL(browserCdpUrl).port; const pageCdpUrl = await waitForPageWebSocket(debugPort); const cdp = await CdpPage.connect(pageCdpUrl); await cdp.command("Runtime.enable"); await cdp.command("Page.enable"); return { child, cdp, debugPort, output: () => output };}; const reconnectPackagedPage = async (app: PackagedApp): Promise<CdpPage> => { app.cdp.close(); const pageCdpUrl = await waitForPageWebSocket(app.debugPort); const cdp = await CdpPage.connect(pageCdpUrl); await cdp.command("Runtime.enable"); await cdp.command("Page.enable"); app.cdp = cdp; return cdp;}; const closePackaged = async (app: PackagedApp | undefined): Promise<void> => { app?.cdp.close(); await stopProcess(app?.child);}; const waitForServerConnectionLabel = async ( page: CdpPage, expectedText: string, timeoutMs: number,): Promise<string> => { const deadline = Date.now() + timeoutMs; let label = ""; for (;;) { label = await pageLines 1–500 of 1108. Load more
// Packaged desktop supervised-daemon regressions. These run against the real // electron-builder bundle and its bundled executor because the supervised attach // path is production-only (`app.isPackaged`). import { type ChildProcess, execFile, execFileSync, spawn } from "node:child_process"; import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs"; import { createServer, type IncomingMessage } from "node:http"; import net from "node:net"; import { homedir, tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { promisify } from "node:util"; import { expect, it } from "@effect/vitest"; import { Effect } from "effect"; import { normalizeExecutorServerConnection, serializeExecutorLocalServerManifest, } from "@executor-js/sdk/shared"; import { scenario } from "../src/scenario"; import { RunDir } from "../src/services"; import { waitForHttp } from "../setup/boot"; const execFileAsync = promisify(execFile); const SERVICE_LABEL = "sh.executor.daemon"; interface PackagedExecutorBridge { readonly getSettings: () => Promise<{ readonly port: number }>; readonly updateSettings: (patch: { readonly port: number }) => Promise<unknown>; readonly restartServer: () => Promise<unknown>; readonly getServerConnection: () => Promise<{ readonly origin: string } | null>; } interface PackagedApp { readonly child: ChildProcess; cdp: CdpPage; readonly debugPort: string; readonly output: () => string; } interface CdpResponse<T> { readonly id: number; readonly result?: T; readonly error?: { readonly message?: string; readonly data?: string }; } interface CdpEvaluateResult { readonly result: { readonly value?: unknown }; readonly exceptionDetails?: unknown; } interface CdpTarget { readonly type: string; readonly url: string; readonly webSocketDebuggerUrl?: string; } class CdpPage { private nextId = 1; private readonly pending = new Map< number, { readonly resolve: (value: unknown) => void; readonly reject: (error: Error) => void; } >(); private constructor(private readonly socket: WebSocket) { socket.addEventListener("message", (event) => { const data = event.data; if (typeof data !== "string") return; const message = JSON.parse(data) as CdpResponse<unknown>; if (!message.id) return; const pending = this.pending.get(message.id); if (!pending) return; this.pending.delete(message.id); if (message.error) { pending.reject(new Error(message.error.message ?? "CDP command failed")); return; } pending.resolve(message.result); }); socket.addEventListener("close", () => { for (const [, pending] of this.pending) { pending.reject(new Error("CDP socket closed")); } this.pending.clear(); }); } static connect = (url: string): Promise<CdpPage> => new Promise((resolve, reject) => { const socket = new WebSocket(url); const timer = setTimeout(() => { socket.close(); // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: WebSocket connection promise adapter reject(new Error(`Timed out connecting to page CDP target ${url}`)); }, 30_000); socket.addEventListener( "open", () => { clearTimeout(timer); resolve(new CdpPage(socket)); }, { once: true }, ); socket.addEventListener( "error", () => { clearTimeout(timer); // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: WebSocket connection promise adapter reject(new Error(`Failed to connect to page CDP target ${url}`)); }, { once: true }, ); }); command = async <T>(method: string, params: Record<string, unknown> = {}): Promise<T> => { const id = this.nextId; this.nextId += 1; const result = new Promise<T>((resolve, reject) => { this.pending.set(id, { resolve: (value) => resolve(value as T), reject, }); }); this.socket.send(JSON.stringify({ id, method, params })); return result; }; evaluate = async <T>(expression: string): Promise<T> => { const result = await this.command<CdpEvaluateResult>("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true, }); if (result.exceptionDetails) { throw new Error(`CDP evaluation failed: ${JSON.stringify(result.exceptionDetails)}`); } return result.result.value as T; }; waitForText = async (text: string, timeoutMs: number): Promise<void> => { const deadline = Date.now() + timeoutMs; const expression = `document.body?.innerText.includes(${JSON.stringify(text)}) ?? false`; for (;;) { if (await this.evaluate<boolean>(expression).catch(() => false)) return; if (Date.now() >= deadline) throw new Error(`Timed out waiting for text: ${text}`); await new Promise((resolve) => setTimeout(resolve, 250)); } }; waitForExpression = async ( expression: string, timeoutMs: number, description: string, ): Promise<void> => { const deadline = Date.now() + timeoutMs; for (;;) { if (await this.evaluate<boolean>(`Boolean(${expression})`).catch(() => false)) return; if (Date.now() >= deadline) throw new Error(`Timed out waiting for ${description}`); await new Promise((resolve) => setTimeout(resolve, 250)); } }; textPresent = async (text: string): Promise<boolean> => this.evaluate<boolean>(`document.body?.innerText.includes(${JSON.stringify(text)}) ?? false`); setViewport = async (width: number, height: number): Promise<void> => { await this.command("Emulation.setDeviceMetricsOverride", { width, height, deviceScaleFactor: 1, mobile: false, }); }; wheel = async (x: number, y: number, deltaY: number): Promise<void> => { await this.command("Input.dispatchMouseEvent", { type: "mouseWheel", x, y, deltaX: 0, deltaY, }); }; screenshot = async (path: string): Promise<void> => { const result = await this.command<{ readonly data: string }>("Page.captureScreenshot", { format: "png", fromSurface: true, }); writeFileSync(path, Buffer.from(result.data, "base64")); }; close = (): void => { this.socket.close(); }; } declare global { interface Window { readonly executor: PackagedExecutorBridge; } } const appExe = process.env.E2E_DESKTOP_APP_EXE; const executorBin = process.env.E2E_DESKTOP_EXECUTOR_BIN; const guiAvailable = (): boolean => { if (process.platform === "darwin") { try { return execFileSync("launchctl", ["managername"], { encoding: "utf8" }).trim() === "Aqua"; } catch { return false; } } if (process.platform === "linux") return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY); return true; }; const packagedSingleInstanceAvailable = (): boolean => { if (process.platform !== "darwin" || !appExe) return true; try { const lines = execFileSync("pgrep", ["-fl", "Executor.app/Contents/MacOS/Executor"], { encoding: "utf8", }) .split("\n") .filter(Boolean); return !lines.some((line) => !line.includes(appExe)); } catch { return true; } }; const requireBundle = (): { readonly app: string; readonly executor: string } => { if (!appExe || !executorBin) { throw new Error( "E2E_DESKTOP_APP_EXE / E2E_DESKTOP_EXECUTOR_BIN not set — did desktop-packaged.globalsetup run?", ); } return { app: appExe, executor: executorBin }; }; const currentUid = (): number => { const getuid = (process as { readonly getuid?: () => number }).getuid; return typeof getuid === "function" ? getuid.call(process) : 0; }; const serviceTarget = (): string => `gui/${currentUid()}/${SERVICE_LABEL}`; const launchAgentPath = (): string => join(homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`); const isolatedDesktopSettingsDir = (home: string): string => join(home, ".executor-desktop-settings"); const desktopSettingsDirs = (home: string): readonly string[] => { if (process.platform === "darwin") { const support = join(home, "Library", "Application Support"); return [ isolatedDesktopSettingsDir(home), join(support, "@executor-js", "desktop"), join(support, "Executor"), ]; } if (process.platform === "linux") { return [ isolatedDesktopSettingsDir(home), join(home, ".config", "@executor-js", "desktop"), join(home, ".config", "Executor"), ]; } const roaming = join(home, "AppData", "Roaming"); return [ isolatedDesktopSettingsDir(home), join(roaming, "@executor-js", "desktop"), join(roaming, "Executor"), ]; }; const packagedAppEnv = (home: string): NodeJS.ProcessEnv => { return { ...process.env, HOME: home, EXECUTOR_DESKTOP_SETTINGS_DIR: isolatedDesktopSettingsDir(home), }; }; interface LaunchdServiceSnapshot { readonly plist: string | null; readonly wasLoaded: boolean; } const launchctl = async (args: ReadonlyArray<string>): Promise<boolean> => { try { await execFileAsync("launchctl", [...args]); return true; } catch { return false; } }; const captureLaunchdService = (): LaunchdServiceSnapshot | null => { if (process.platform !== "darwin") return null; const path = launchAgentPath(); const plist = existsSync(path) ? readFileSync(path, "utf8") : null; let wasLoaded = false; try { execFileSync("launchctl", ["print", serviceTarget()], { stdio: "ignore" }); wasLoaded = true; } catch { wasLoaded = false; } return { plist, wasLoaded }; }; const restoreLaunchdService = async (snapshot: LaunchdServiceSnapshot | null): Promise<void> => { if (!snapshot) return; const target = serviceTarget(); await launchctl(["bootout", target]); const path = launchAgentPath(); if (snapshot.plist === null) { rmSync(path, { force: true }); return; } mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, snapshot.plist, { mode: 0o600 }); chmodSync(path, 0o600); await launchctl(["enable", target]); if (snapshot.wasLoaded) { const bootstrapped = await launchctl(["bootstrap", `gui/${currentUid()}`, path]); if (bootstrapped) await launchctl(["kickstart", "-k", target]); } }; const freePort = (): Promise<number> => new Promise((resolve, reject) => { const srv = net.createServer(); srv.on("error", reject); srv.listen(0, "127.0.0.1", () => { const port = (srv.address() as net.AddressInfo).port; srv.close(() => resolve(port)); }); }); interface DaemonStart { readonly child: ChildProcess; readonly ready: boolean; readonly stderr: string; } const startSupervisedDaemon = ( env: NodeJS.ProcessEnv, port: number, hostname = "127.0.0.1", ): Promise<DaemonStart> => new Promise((resolve) => { const { executor } = requireBundle(); const child = spawn( executor, ["daemon", "run", "--foreground", "--port", String(port), "--hostname", hostname], { env, stdio: ["ignore", "pipe", "pipe"] }, ); let stderr = ""; let settled = false; const settle = (ready: boolean) => { if (settled) return; settled = true; resolve({ child, ready, stderr }); }; const timer = setTimeout(() => settle(false), 60_000); child.stdout.on("data", (chunk: Buffer) => { if (/Daemon ready on http:\/\//.test(chunk.toString())) { clearTimeout(timer); settle(true); } }); child.stderr.on("data", (chunk: Buffer) => { stderr += chunk.toString(); }); child.on("exit", () => { clearTimeout(timer); settle(false); }); }); const stopProcess = async (child: ChildProcess | undefined): Promise<void> => { if (!child || child.exitCode !== null || child.signalCode !== null) return; await new Promise<void>((resolve) => { const timeout = setTimeout(() => { child.kill("SIGKILL"); resolve(); }, 5_000); child.once("exit", () => { clearTimeout(timeout); resolve(); }); child.kill("SIGTERM"); }); }; const waitForPageWebSocket = async (debugPort: string): Promise<string> => { const deadline = Date.now() + 120_000; for (;;) { const targets = (await fetch(`http://127.0.0.1:${debugPort}/json/list`) .then((response) => (response.ok ? response.json() : [])) .catch(() => [])) as ReadonlyArray<CdpTarget>; const page = targets.find( (target) => target.type === "page" && target.webSocketDebuggerUrl && !target.url.startsWith("devtools://"), ); if (page?.webSocketDebuggerUrl) return page.webSocketDebuggerUrl; if (Date.now() >= deadline) { throw new Error("Timed out waiting for packaged app page CDP target"); } await new Promise((resolve) => setTimeout(resolve, 100)); } }; const launchPackaged = async (home: string): Promise<PackagedApp> => { const { app } = requireBundle(); let output = ""; let settled = false; const child = spawn(app, ["--remote-debugging-port=0"], { env: packagedAppEnv(home), stdio: ["ignore", "pipe", "pipe"], }); const browserCdpUrl = await new Promise<string>((resolve, reject) => { const timer = setTimeout(() => { // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: packaged-app launch promise adapter reject(new Error(`Timed out waiting for packaged app CDP URL\n${output}`)); }, 120_000); const settle = (fn: () => void) => { if (settled) return; settled = true; clearTimeout(timer); fn(); }; const collectOutput = (chunk: Buffer) => { const text = chunk.toString(); output = (output + text).slice(-16_384); const match = output.match(/DevTools listening on (ws:\/\/[^\s]+)/); if (match) settle(() => resolve(match[1])); }; child.stdout?.on("data", collectOutput); child.stderr?.on("data", collectOutput); // oxlint-disable-next-line executor/no-promise-reject -- boundary: packaged-app launch promise adapter child.once("error", (error) => settle(() => reject(error))); child.once("exit", (code, signal) => settle(() => // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: packaged-app launch promise adapter reject( new Error(`Packaged app exited before CDP (code=${code} signal=${signal})\n${output}`), ), ), ); }); const debugPort = new URL(browserCdpUrl).port; const pageCdpUrl = await waitForPageWebSocket(debugPort); const cdp = await CdpPage.connect(pageCdpUrl); await cdp.command("Runtime.enable"); await cdp.command("Page.enable"); return { child, cdp, debugPort, output: () => output }; }; const reconnectPackagedPage = async (app: PackagedApp): Promise<CdpPage> => { app.cdp.close(); const pageCdpUrl = await waitForPageWebSocket(app.debugPort); const cdp = await CdpPage.connect(pageCdpUrl); await cdp.command("Runtime.enable"); await cdp.command("Page.enable"); app.cdp = cdp; return cdp; }; const closePackaged = async (app: PackagedApp | undefined): Promise<void> => { app?.cdp.close(); await stopProcess(app?.child); }; const waitForServerConnectionLabel = async ( page: CdpPage, expectedText: string, timeoutMs: number, ): Promise<string> => { const deadline = Date.now() + timeoutMs; let label = ""; for (;;) { label = await page