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("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); // ---------------------------------------------------------------------------// 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, dataLines 1–500 of 688. Load more
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("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); // --------------------------------------------------------------------------- // 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 daemon After=default.target [Service] Type=simple ExecStart=${execStart} ${env} WorkingDirectory=${options.workingDirectory} StandardOutput=append:${options.stdoutPath} StandardError=append:${options.stderrPath} Restart=on-failure RestartSec=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