945 lines · 31.6 KB · TypeScript
RawDownload
/* oxlint-disable executor/no-try-catch-or-throw, executor/no-error-constructor, executor/no-promise-reject -- e2e boundary: drives launchd, a packaged Electron process, CDP, and optional ffmpeg recording */import { type ChildProcess, execFile, execFileSync, spawn } from "node:child_process";import {  chmodSync,  existsSync,  mkdirSync,  mkdtempSync,  readFileSync,  rmSync,  statSync,  writeFileSync,} from "node:fs";import { homedir, tmpdir } from "node:os";import { dirname, join, resolve } from "node:path";import { promisify } from "node:util"; import { expect, it } from "@effect/vitest";import { Effect } from "effect"; import { scenario } from "../src/scenario";import { RunDir } from "../src/services"; const execFileAsync = promisify(execFile);const SERVICE_LABEL = "sh.executor.daemon";const SCENARIO_NAME =  "Desktop packaged supervised attach: stale launchd registration self-heals without a dead boot wait";const RECORDING_FILE = resolve("recordings/stale-launchd-fast-boot.mp4");const RECORDING_WIDTH = 1280;const RECORDING_HEIGHT = 720;const STALE_PLIST_PORT = 58_251; interface PackagedExecutorBridge {  readonly getServerConnection: () => Promise<{    readonly kind: string;    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;} interface ServerManifest {  readonly kind: string;  readonly pid: number;} interface LaunchTiming {  readonly app: PackagedApp;  readonly pageCdpMs: number;} interface BootResult {  readonly pageCdpMs: number;  readonly startupWindowMs: number;  readonly sidecarReadyMs: number;  readonly connectionKind: string;  readonly origin: string;  readonly manifestKind: string;  readonly manifestPid: number;  readonly kickstartFailureLine: string | null;  readonly recoveryLogLine: string | null;  readonly launchdPrintSucceeded: boolean;} declare global {  interface Window {    readonly executor: PackagedExecutorBridge;  }} 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((resolveConnect, rejectConnect) => {      const socket = new WebSocket(url);      const timer = setTimeout(() => {        socket.close();        rejectConnect(new Error(`Timed out connecting to page CDP target ${url}`));      }, 30_000);      socket.addEventListener(        "open",        () => {          clearTimeout(timer);          resolveConnect(new CdpPage(socket));        },        { once: true },      );      socket.addEventListener(        "error",        () => {          clearTimeout(timer);          rejectConnect(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>((resolveCommand, rejectCommand) => {      this.pending.set(id, {        resolve: (value) => resolveCommand(value as T),        reject: rejectCommand,      });    });    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((resolveWait) => setTimeout(resolveWait, 250));    }  };   setViewport = async (width: number, height: number): Promise<void> => {    await this.command("Emulation.setDeviceMetricsOverride", {      width,      height,      deviceScaleFactor: 1,      mobile: false,    });  };   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();  };} const appExe = process.env.E2E_DESKTOP_APP_EXE;const executorBin = process.env.E2E_DESKTOP_EXECUTOR_BIN; const macAquaAvailable = (): boolean => {  if (process.platform !== "darwin") return false;  try {    return execFileSync("launchctl", ["managername"], { encoding: "utf8" }).trim() === "Aqua";  } catch {    return false;  }}; 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 packagedAppEnv = (home: string): NodeJS.ProcessEnv => ({  ...process.env,  HOME: home,  EXECUTOR_DESKTOP_SETTINGS_DIR: isolatedDesktopSettingsDir(home),}); const cleanBootEnv = (home: string): NodeJS.ProcessEnv => ({  ...packagedAppEnv(home),  EXECUTOR_TEST_SKIP_BACKGROUND_SERVICE: "1",  EXECUTOR_TEST_AUTO_CONFIRM_BACKGROUND_SERVICE: "0",}); 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 launchctlPrintService = async (): Promise<string | null> => {  try {    const { stdout, stderr } = await execFileAsync("launchctl", ["print", serviceTarget()], {      encoding: "utf8",    });    return `${stdout}${stderr}`.trim();  } catch {    return null;  }}; 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 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((resolveWait) => setTimeout(resolveWait, 100));  }}; const launchPackaged = async (  env: NodeJS.ProcessEnv,  recorder: BootRecorder | null,): Promise<LaunchTiming> => {  const { app } = requireBundle();  const startedAt = Date.now();  let output = "";  let settled = false;  const child = spawn(app, ["--remote-debugging-port=0"], {    env,    stdio: ["ignore", "pipe", "pipe"],  });  recorder?.start(startedAt);   try {    const browserCdpUrl = await new Promise<string>((resolveLaunch, rejectLaunch) => {      const timer = setTimeout(() => {        rejectLaunch(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(() => resolveLaunch(match[1]));      };      child.stdout?.on("data", collectOutput);      child.stderr?.on("data", collectOutput);      child.once("error", (error) => settle(() => rejectLaunch(error)));      child.once("exit", (code, signal) =>        settle(() =>          rejectLaunch(            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");    await cdp.setViewport(RECORDING_WIDTH, RECORDING_HEIGHT);    recorder?.attachPage(cdp);    return {      app: { child, cdp, debugPort, output: () => output },      pageCdpMs: Date.now() - startedAt,    };  } catch (error) {    await stopProcess(child);    throw error;  }}; const stopProcess = async (child: ChildProcess | undefined): Promise<void> => {  if (!child || child.exitCode !== null || child.signalCode !== null) return;  await new Promise<void>((resolveStop) => {    const timeout = setTimeout(() => {      child.kill("SIGKILL");      resolveStop();    }, 5_000);    child.once("exit", () => {      clearTimeout(timeout);      resolveStop();    });    child.kill("SIGTERM");  });}; const closePackaged = async (app: PackagedApp | undefined): Promise<void> => {  app?.cdp.close();  await stopProcess(app?.child);}; const xmlEscape = (value: string): string =>  value    .replaceAll("&", "&amp;")    .replaceAll('"', "&quot;")    .replaceAll(">", "&gt;")    .replaceAll("<", "&lt;"); const writeStaleLaunchAgentPlist = (home: string): void => {  const { executor } = requireBundle();  const dataDir = join(home, ".executor");  const logs = join(dataDir, "logs");  const plistDir = join(home, "Library", "LaunchAgents");  mkdirSync(plistDir, { recursive: true });  mkdirSync(logs, { recursive: true });  mkdirSync(dataDir, { recursive: true });   const programArguments = [    executor,    "daemon",    "run",    "--foreground",    "--port",    String(STALE_PLIST_PORT),    "--hostname",    "127.0.0.1",  ];  const programArgs = programArguments    .map((arg) => `    <string>${xmlEscape(arg)}</string>`)    .join("\n");  const plist = `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict>  <key>Label</key>  <string>${SERVICE_LABEL}</string>  <key>ProgramArguments</key>  <array>${programArgs}  </array>  <key>EnvironmentVariables</key>  <dict>    <key>EXECUTOR_SUPERVISED</key>    <string>1</string>    <key>EXECUTOR_DATA_DIR</key>    <string>${xmlEscape(dataDir)}</string>    <key>EXECUTOR_SCOPE_DIR</key>    <string>${xmlEscape(dataDir)}</string>    <key>EXECUTOR_CLIENT</key>    <string>desktop</string>  </dict>  <key>RunAtLoad</key>  <true/>  <key>KeepAlive</key>  <dict>    <key>SuccessfulExit</key>    <false/>  </dict>  <key>ProcessType</key>  <string>Background</string>  <key>WorkingDirectory</key>  <string>${xmlEscape(dataDir)}</string>  <key>StandardOutPath</key>  <string>${xmlEscape(join(logs, "daemon.log"))}</string>  <key>StandardErrorPath</key>  <string>${xmlEscape(join(logs, "daemon.error.log"))}</string></dict></plist>`;  writeFileSync(join(plistDir, `${SERVICE_LABEL}.plist`), plist, { mode: 0o600 });}; const readServerManifest = (home: string): ServerManifest => {  const manifestPath = join(home, ".executor", "server-control", "server.json");  return JSON.parse(readFileSync(manifestPath, "utf8")) as ServerManifest;}; interface StaleRecoveryLog {  readonly kickstartFailureLine: string | null;

Lines 1–500 of 945.