688 lines · 26.7 KB · TypeScript
RawDownload
import { execFile } from "node:child_process";import { homedir, userInfo } from "node:os";import { FileSystem, Path } from "effect";import type { PlatformError } from "effect/PlatformError";import * as Effect from "effect/Effect"; import { resolveExecutorDataDir } from "./local-server-manifest"; // ---------------------------------------------------------------------------// OS service backends for the supervised Executor daemon.//// The long-lived gateway must outlive the GUI app and survive machine restarts.// That means the OS service manager — not a foreground process — owns its// lifecycle: launchd on macOS, systemd --user on Linux, Task Scheduler on// Windows. Each backend registers the SAME running contract: spawn// `<executor> daemon run --foreground --port <p>`, bind loopback, write// `server.json`, and get restarted on crash but not on a clean stop.//// macOS (launchd), Linux (systemd --user + lingering), and Windows (Task// Scheduler S4U/AtStartup) are all reboot-survival verified in real VMs.// --------------------------------------------------------------------------- export const SERVICE_LABEL = "sh.executor.daemon"; /** * The supervised service binds this port by default. It matches the desktop * connect-card port (4789, not the `executor daemon run` default of 4788) so * existing desktop MCP-client configs keep resolving. The exact value is * low-stakes: clients discover the live port from `server.json`. */export const DEFAULT_SERVICE_PORT = 4789; export interface ServiceDescriptor {  /** Absolute path to the `executor` binary the service should run. */  readonly executablePath: string;  readonly port: number;  /** Installing CLI version, baked in for drift detection on upgrade. */  readonly version: string;} // No secret is part of the descriptor: the supervised daemon mints/loads its// bearer token from the 0600 `auth.json` (under EXECUTOR_DATA_DIR) on start, and// clients read the same file. Keeping the secret out of the plist/unit means// `launchctl print`/`list` and `systemctl cat` never expose it. export type ServicePlatform = "darwin" | "linux" | "win32" | "unsupported"; export interface ServiceStatus {  readonly platform: ServicePlatform;  /** The OS manager has a unit/plist/task on disk for the service. */  readonly registered: boolean;  /** The OS manager reports the service currently loaded/active. */  readonly running: boolean;  readonly pid: number | null;  /** Extra human-readable lines (e.g. manual steps on unsupported platforms). */  readonly detail: ReadonlyArray<string>;} export interface ServiceBackend {  readonly platform: ServicePlatform;  /** True when this backend actually drives the OS manager (vs. printing steps). */  readonly automated: boolean;  readonly install: (    descriptor: ServiceDescriptor,  ) => Effect.Effect<void, Error | PlatformError, FileSystem.FileSystem | Path.Path>;  readonly uninstall: () => Effect.Effect<    void,    Error | PlatformError,    FileSystem.FileSystem | Path.Path  >;  readonly status: () => Effect.Effect<    ServiceStatus,    Error | PlatformError,    FileSystem.FileSystem | Path.Path  >;  readonly restart: () => Effect.Effect<void, Error, FileSystem.FileSystem | Path.Path>;} // ---------------------------------------------------------------------------// Process helper — run an OS command and capture (stdout, stderr, exit code).// Resolves on a non-zero exit so callers can branch; fails only when the// command itself cannot be spawned (e.g. launchctl missing).// --------------------------------------------------------------------------- interface CommandResult {  readonly stdout: string;  readonly stderr: string;  readonly code: number;} const runCommand = (  cmd: string,  args: ReadonlyArray<string>,  env?: Record<string, string | undefined>,): Effect.Effect<CommandResult, Error> =>  Effect.callback<CommandResult, Error>((resume) => {    const options = env      ? { encoding: "utf8" as const, env: { ...process.env, ...env } }      : { encoding: "utf8" as const };    execFile(cmd, [...args], options, (error, stdout, stderr) => {      // A string `code` (ENOENT etc.) means the command could not be spawned.      if (error && typeof (error as { code?: unknown }).code === "string") {        resume(          Effect.fail(new Error(`Failed to run \`${cmd}\`: ${(error as { code: string }).code}`)),        );        return;      }      const code =        error && typeof (error as { code?: unknown }).code === "number"          ? (error as { code: number }).code          : 0;      resume(Effect.succeed({ stdout: stdout ?? "", stderr: stderr ?? "", code }));    });  }); const currentUid = (): number => {  const getuid = (process as { getuid?: () => number }).getuid;  if (typeof getuid === "function") return getuid.call(process);  return userInfo().uid;}; const xmlEscape = (value: string): string =>  value    .replaceAll("&", "&amp;")    .replaceAll("<", "&lt;")    .replaceAll(">", "&gt;")    .replaceAll('"', "&quot;")    .replaceAll("'", "&apos;"); // ---------------------------------------------------------------------------// Shared service environment + program args// --------------------------------------------------------------------------- const serviceProgramArguments = (descriptor: ServiceDescriptor): ReadonlyArray<string> => [  descriptor.executablePath,  "daemon",  "run",  "--foreground",  "--port",  String(descriptor.port),]; const serviceEnvironment = (  descriptor: ServiceDescriptor,  dataDir: string,): Record<string, string> => ({  // Marks the process as OS-supervised so the daemon resolves its bearer token  // from the durable 0600 auth.json (the secret is never in the unit itself).  EXECUTOR_SUPERVISED: "1",  // Pin the data dir explicitly: launchd/systemd give a minimal environment and  // we never want the daemon to fall back to a different home than the user's.  EXECUTOR_DATA_DIR: dataDir,  // Stamp the installing version so `service status` can flag drift after an  // upgrade where the unit still points at an older binary path.  EXECUTOR_SERVICE_VERSION: descriptor.version,  // A launchd/systemd unit starts with a bare PATH — without the user's PATH  // the daemon can't find pyenv/nvm/volta/Homebrew tools that integrations may  // shell out to. `service install` runs from the user's shell, so its own  // PATH is the right one to bake in. (Reference: opencode shell-env capture.)  ...(process.env.PATH ? { PATH: process.env.PATH } : {}),}); // ---------------------------------------------------------------------------// macOS — launchd LaunchAgent (fully built)// --------------------------------------------------------------------------- const launchAgentsDir = (path: Path.Path): string =>  path.join(homedir(), "Library", "LaunchAgents"); const launchdPlistPath = (path: Path.Path): string =>  path.join(launchAgentsDir(path), `${SERVICE_LABEL}.plist`); const serviceLogDir = (path: Path.Path): string => path.join(resolveExecutorDataDir(path), "logs"); export interface LaunchdPlistOptions {  readonly label: string;  readonly programArguments: ReadonlyArray<string>;  readonly environment: Record<string, string>;  readonly stdoutPath: string;  readonly stderrPath: string;  readonly workingDirectory: string;} /** * Render a user LaunchAgent plist. Pure (snapshot-tested). KeepAlive uses * `SuccessfulExit=false` so launchd restarts the daemon on a crash/non-zero * exit but leaves it stopped after a clean `bootout` (which sends SIGTERM → * the daemon exits 0). RunAtLoad starts it on login; ProcessType=Background * keeps it off the foreground scheduler. */export const generateLaunchdPlist = (options: LaunchdPlistOptions): string => {  const programArgs = options.programArguments    .map((arg) => `    <string>${xmlEscape(arg)}</string>`)    .join("\n");  const envEntries = Object.entries(options.environment)    .map(      ([key, value]) =>        `    <key>${xmlEscape(key)}</key>\n    <string>${xmlEscape(value)}</string>`,    )    .join("\n");  return `<?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>${xmlEscape(options.label)}</string>  <key>ProgramArguments</key>  <array>${programArgs}  </array>  <key>EnvironmentVariables</key>  <dict>${envEntries}  </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(options.workingDirectory)}</string>  <key>StandardOutPath</key>  <string>${xmlEscape(options.stdoutPath)}</string>  <key>StandardErrorPath</key>  <string>${xmlEscape(options.stderrPath)}</string></dict></plist>`;}; const parseLaunchctlPid = (printOutput: string): number | null => {  const match = printOutput.match(/\bpid\s*=\s*(\d+)/);  if (!match) return null;  const pid = Number.parseInt(match[1], 10);  return Number.isInteger(pid) && pid > 0 ? pid : null;}; const makeLaunchdBackend = (): ServiceBackend => {  const serviceTarget = (uid: number): string => `gui/${uid}/${SERVICE_LABEL}`;   return {    platform: "darwin",    automated: true,    install: (descriptor) =>      Effect.gen(function* () {        const fs = yield* FileSystem.FileSystem;        const path = yield* Path.Path;        const uid = currentUid();        const dataDir = resolveExecutorDataDir(path);        const logs = serviceLogDir(path);         yield* fs.makeDirectory(launchAgentsDir(path), { recursive: true });        yield* fs.makeDirectory(logs, { recursive: true });         const plist = generateLaunchdPlist({          label: SERVICE_LABEL,          programArguments: serviceProgramArguments(descriptor),          environment: serviceEnvironment(descriptor, dataDir),          stdoutPath: path.join(logs, "daemon.log"),          stderrPath: path.join(logs, "daemon.error.log"),          workingDirectory: dataDir,        });        const plistFile = launchdPlistPath(path);        // 0600: the plist is owner-only. It carries no secret — the daemon reads        // the bearer from auth.json at boot — but stays tight regardless.        yield* fs.writeFileString(plistFile, plist, { mode: 0o600 });         // Re-bootstrap cleanly: a stale registration from a prior install would        // make `bootstrap` fail with "service already loaded".        yield* runCommand("launchctl", ["bootout", serviceTarget(uid)]).pipe(Effect.ignore);        const bootstrap = yield* runCommand("launchctl", ["bootstrap", `gui/${uid}`, plistFile]);        if (bootstrap.code !== 0) {          return yield* Effect.fail(            new Error(              `launchctl bootstrap failed (exit ${bootstrap.code}): ${bootstrap.stderr.trim() || bootstrap.stdout.trim()}`,            ),          );        }        yield* runCommand("launchctl", ["enable", serviceTarget(uid)]).pipe(Effect.ignore);      }),    uninstall: () =>      Effect.gen(function* () {        const fs = yield* FileSystem.FileSystem;        const path = yield* Path.Path;        const uid = currentUid();        yield* runCommand("launchctl", ["bootout", serviceTarget(uid)]).pipe(Effect.ignore);        yield* runCommand("launchctl", ["disable", serviceTarget(uid)]).pipe(Effect.ignore);        yield* fs.remove(launchdPlistPath(path), { force: true });      }),    status: () =>      Effect.gen(function* () {        const fs = yield* FileSystem.FileSystem;        const path = yield* Path.Path;        const uid = currentUid();        const registered = yield* fs.exists(launchdPlistPath(path));        const print = yield* runCommand("launchctl", ["print", serviceTarget(uid)]);        const running = print.code === 0;        return {          platform: "darwin" as const,          registered,          running,          pid: running ? parseLaunchctlPid(print.stdout) : null,          detail: [],        };      }),    restart: () =>      Effect.gen(function* () {        const uid = currentUid();        const result = yield* runCommand("launchctl", ["kickstart", "-k", serviceTarget(uid)]);        if (result.code !== 0) {          return yield* Effect.fail(            new Error(              `launchctl kickstart failed (exit ${result.code}): ${result.stderr.trim() || result.stdout.trim()}`,            ),          );        }      }),  };}; // ---------------------------------------------------------------------------// Linux — systemd --user + lingering (reboot-survival verified in an Ubuntu VM)// --------------------------------------------------------------------------- const systemdUnitDir = (path: Path.Path): string =>  path.join(homedir(), ".config", "systemd", "user"); const systemdUnitPath = (path: Path.Path): string =>  path.join(systemdUnitDir(path), `${SERVICE_LABEL}.service`); export interface SystemdUnitOptions {  readonly execStart: ReadonlyArray<string>;  readonly environment: Record<string, string>;  readonly workingDirectory: string;  readonly stdoutPath: string;  readonly stderrPath: string;} /** Render a systemd --user unit. Pure (snapshot-tested). */export const generateSystemdUnit = (options: SystemdUnitOptions): string => {  const execStart = options.execStart.map((arg) => (/\s/.test(arg) ? `"${arg}"` : arg)).join(" ");  const env = Object.entries(options.environment)    .map(([key, value]) => `Environment=${key}=${value}`)    .join("\n");  return `[Unit]Description=Executor supervised daemonAfter=default.target [Service]Type=simpleExecStart=${execStart}${env}WorkingDirectory=${options.workingDirectory}StandardOutput=append:${options.stdoutPath}StandardError=append:${options.stderrPath}Restart=on-failureRestartSec=5s [Install]WantedBy=default.target`;}; const makeSystemdBackend = (): ServiceBackend => {  const unitName = `${SERVICE_LABEL}.service`;  return {    platform: "linux",    automated: true,    install: (descriptor) =>      Effect.gen(function* () {        const fs = yield* FileSystem.FileSystem;        const path = yield* Path.Path;        const dataDir = resolveExecutorDataDir(path);        const logs = serviceLogDir(path);        yield* fs.makeDirectory(systemdUnitDir(path), { recursive: true });        yield* fs.makeDirectory(logs, { recursive: true });        const unit = generateSystemdUnit({          execStart: serviceProgramArguments(descriptor),          environment: serviceEnvironment(descriptor, dataDir),          workingDirectory: dataDir,          stdoutPath: path.join(logs, "daemon.log"),          stderrPath: path.join(logs, "daemon.error.log"),        });        yield* fs.writeFileString(systemdUnitPath(path), unit, { mode: 0o600 });        // `systemctl --user` needs XDG_RUNTIME_DIR to reach the user bus. Supply        // it if the caller's environment lacks it (e.g. a non-login shell) so        // install is robust regardless of how it was invoked.        const username = userInfo().username;        const sdEnv = {          XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR ?? `/run/user/${currentUid()}`,        };        yield* runCommand("systemctl", ["--user", "daemon-reload"], sdEnv).pipe(Effect.ignore);        const enable = yield* runCommand(          "systemctl",          ["--user", "enable", "--now", unitName],          sdEnv,        );        if (enable.code !== 0) {          return yield* Effect.fail(            new Error(              `systemctl --user enable failed (exit ${enable.code}): ${enable.stderr.trim()}`,            ),          );        }        // Enable lingering so the user manager — and this enabled service —        // starts at BOOT, not just on login, so the daemon survives a reboot        // unattended (verified in a real Ubuntu VM via loginctl). Best-effort:        // if the platform needs privilege, the service still works for the        // logged-in case and `service status` flags the missing linger.        yield* runCommand("loginctl", ["enable-linger", username], sdEnv).pipe(Effect.ignore);      }),    uninstall: () =>      Effect.gen(function* () {        const fs = yield* FileSystem.FileSystem;        const path = yield* Path.Path;        const sdEnv = {          XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR ?? `/run/user/${currentUid()}`,        };        yield* runCommand("systemctl", ["--user", "disable", "--now", unitName], sdEnv).pipe(          Effect.ignore,        );        yield* fs.remove(systemdUnitPath(path), { force: true });        yield* runCommand("systemctl", ["--user", "daemon-reload"], sdEnv).pipe(Effect.ignore);        yield* runCommand("loginctl", ["disable-linger", userInfo().username], sdEnv).pipe(          Effect.ignore,        );      }),    status: () =>      Effect.gen(function* () {        const fs = yield* FileSystem.FileSystem;        const path = yield* Path.Path;        const sdEnv = {          XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR ?? `/run/user/${currentUid()}`,        };        const registered = yield* fs.exists(systemdUnitPath(path));        const active = yield* runCommand("systemctl", ["--user", "is-active", unitName], sdEnv);        const running = active.stdout.trim() === "active";        const linger = yield* runCommand("loginctl", [          "show-user",          userInfo().username,          "-p",          "Linger",          "--value",        ]);        const lingerOn = linger.stdout.trim() === "yes";        return {          platform: "linux" as const,          registered,          running,          pid: null,          detail: lingerOn            ? []            : [                "Lingering is off — the daemon won't start until you log in. Run `loginctl enable-linger`.",              ],        };      }),    restart: () =>      Effect.gen(function* () {        const sdEnv = {          XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR ?? `/run/user/${currentUid()}`,        };        const result = yield* runCommand("systemctl", ["--user", "restart", unitName], sdEnv);        if (result.code !== 0) {          return yield* Effect.fail(            new Error(              `systemctl --user restart failed (exit ${result.code}): ${result.stderr.trim()}`,            ),          );        }      }),  };}; // ---------------------------------------------------------------------------// Windows — Task Scheduler (S4U / AtStartup; reboot-survival verified)// --------------------------------------------------------------------------- /** Scheduled Task name registered for the supervised daemon. */export const WINDOWS_TASK_NAME = "ExecutorDaemon"; /** * Make a value safe to embed in a cmd.exe `set "KEY=VALUE"` line. A literal `"` * would close the quoted argument early — in PATH (built from arbitrary * installer entries) that lets `& cmd &` fragments execute when Task Scheduler * runs the wrapper at boot, so strip them (a `"` is illegal in a Windows path * anyway). A literal `%` is re-expanded by cmd at run time against the boot * environment, silently diverging from the value captured at install; double it * so the daemon sees exactly what was captured. */export const cmdSetValue = (value: string): string =>  value.replaceAll('"', "").replaceAll("%", "%%"); /** * The batch wrapper the Scheduled Task executes. Task Scheduler has no field * for environment variables, so the supervised env (EXECUTOR_SUPERVISED, data

Lines 1–500 of 688.