import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { VERSION } from "../version.js"; import { assertFutureConfigActionAllowed } from "./future-config-guard.js"; import { installLaunchAgent, isLaunchAgentLoaded, readLaunchAgentProgramArguments, readLaunchAgentRuntime, restartLaunchAgent, stageLaunchAgent, stopLaunchAgent, uninstallLaunchAgent, } from "./launchd.js"; import { installScheduledTask, isScheduledTaskInstalled, readScheduledTaskCommand, readScheduledTaskRuntime, restartScheduledTask, stageScheduledTask, stopScheduledTask, uninstallScheduledTask, } from "./schtasks.js"; import type { GatewayServiceRuntime } from "./service-runtime.js"; import type { GatewayServiceCommandConfig, GatewayServiceControlArgs, GatewayServiceEnv, GatewayServiceEnvArgs, GatewayServiceInstallArgs, GatewayServiceManageArgs, GatewayServiceRestartResult, GatewayServiceStartRepairIssue, GatewayServiceStartResult, GatewayServiceStageArgs, GatewayServiceState, } from "./service-types.js"; import { installSystemdService, isSystemdServiceEnabled, readSystemdServiceExecStart, readSystemdServiceRuntime, restartSystemdService, stageSystemdService, stopSystemdService, uninstallSystemdService, } from "./systemd.js"; export type { GatewayServiceCommandConfig, GatewayServiceControlArgs, GatewayServiceEnv, GatewayServiceEnvArgs, GatewayServiceInstallArgs, GatewayServiceManageArgs, GatewayServiceRestartResult, GatewayServiceStartRepairIssue, GatewayServiceStartResult, GatewayServiceStageArgs, GatewayServiceState, } from "./service-types.js"; function ignoreServiceWriteResult( write: (args: TArgs) => Promise, ): (args: TArgs) => Promise { return async (args: TArgs) => { await write(args); }; } export type GatewayService = { label: string; loadedText: string; notLoadedText: string; stage: (args: GatewayServiceStageArgs) => Promise; install: (args: GatewayServiceInstallArgs) => Promise; uninstall: (args: GatewayServiceManageArgs) => Promise; stop: (args: GatewayServiceControlArgs) => Promise; restart: (args: GatewayServiceControlArgs) => Promise; isLoaded: (args: GatewayServiceEnvArgs) => Promise; readCommand: (env: GatewayServiceEnv) => Promise; readRuntime: (env: GatewayServiceEnv) => Promise; }; function mergeGatewayServiceEnv( baseEnv: GatewayServiceEnv, command: GatewayServiceCommandConfig | null, ): GatewayServiceEnv { if (!command?.environment) { return baseEnv; } return { ...baseEnv, ...command.environment, }; } const TEMP_PROGRAM_ROOTS = [os.tmpdir(), "/tmp", "/private/tmp", "/var/tmp"].map((entry) => path.resolve(entry), ); function pathIsSameOrChild(candidate: string, parent: string): boolean { return candidate === parent || candidate.startsWith(`${parent}${path.sep}`); } function isTemporaryProgramPath(value: string | undefined): boolean { if (!value || !path.isAbsolute(value)) { return false; } const resolved = path.resolve(value); return TEMP_PROGRAM_ROOTS.some((root) => pathIsSameOrChild(resolved, root)); } function isMissingProgramPath(value: string | undefined): boolean { if (!value || !path.isAbsolute(value)) { return false; } return !fs.existsSync(value); } function collectGatewayServiceStartRepairIssues( state: GatewayServiceState, ): GatewayServiceStartRepairIssue[] { const command = state.command; if (!state.loaded || !command) { return []; } const issues: GatewayServiceStartRepairIssue[] = []; const serviceVersion = command.environment?.OPENCLAW_SERVICE_VERSION?.trim(); if (serviceVersion && serviceVersion !== VERSION) { issues.push({ code: "version-mismatch", message: `service was installed by OpenClaw ${serviceVersion}, current CLI is ${VERSION}`, }); } for (const candidate of command.programArguments.slice(0, 2)) { if (isTemporaryProgramPath(candidate)) { issues.push({ code: "temporary-program", message: `service command points at a temporary path: ${candidate}`, }); continue; } if (isMissingProgramPath(candidate)) { issues.push({ code: "missing-program", message: `service command points at a missing path: ${candidate}`, }); } } return issues; } export function formatGatewayServiceStartRepairIssues( issues: GatewayServiceStartRepairIssue[], ): string { return issues.map((issue) => issue.message).join("; "); } export async function readGatewayServiceState( service: GatewayService, args: GatewayServiceEnvArgs = {}, ): Promise { const baseEnv = args.env ?? (process.env as GatewayServiceEnv); const command = await service.readCommand(baseEnv).catch(() => null); const env = mergeGatewayServiceEnv(baseEnv, command); const [loaded, runtime] = await Promise.all([ service.isLoaded({ env }).catch(() => false), service.readRuntime(env).catch(() => undefined), ]); return { installed: command !== null, loaded, running: runtime?.status === "running", env, command, runtime, }; } export async function startGatewayService( service: GatewayService, args: GatewayServiceControlArgs, ): Promise { const state = await readGatewayServiceState(service, { env: args.env }); if (!state.loaded && !state.installed) { return { outcome: "missing-install", state, }; } const repairIssues = collectGatewayServiceStartRepairIssues(state); if (repairIssues.length > 0) { return { outcome: "repair-required", state, issues: repairIssues, }; } try { const restartResult = await service.restart({ ...args, env: state.env }); const nextState = await readGatewayServiceState(service, { env: state.env }); return { outcome: restartResult.outcome === "scheduled" ? "scheduled" : "started", state: nextState, }; } catch (err) { const nextState = await readGatewayServiceState(service, { env: state.env }); if (!nextState.installed) { return { outcome: "missing-install", state: nextState, }; } throw err; } } export function describeGatewayServiceRestart( serviceNoun: string, result: GatewayServiceRestartResult, ): { scheduled: boolean; daemonActionResult: "restarted" | "scheduled"; message: string; progressMessage: string; } { if (result.outcome === "scheduled") { return { scheduled: true, daemonActionResult: "scheduled", message: `restart scheduled, ${normalizeLowercaseStringOrEmpty(serviceNoun)} will restart momentarily`, progressMessage: `${serviceNoun} service restart scheduled.`, }; } return { scheduled: false, daemonActionResult: "restarted", message: `${serviceNoun} service restarted.`, progressMessage: `${serviceNoun} service restarted.`, }; } type SupportedGatewayServicePlatform = "darwin" | "linux" | "win32"; const GATEWAY_SERVICE_REGISTRY: Record = { darwin: { label: "LaunchAgent", loadedText: "loaded", notLoadedText: "not loaded", stage: ignoreServiceWriteResult(stageLaunchAgent), install: ignoreServiceWriteResult(installLaunchAgent), uninstall: uninstallLaunchAgent, stop: stopLaunchAgent, restart: restartLaunchAgent, isLoaded: isLaunchAgentLoaded, readCommand: readLaunchAgentProgramArguments, readRuntime: readLaunchAgentRuntime, }, linux: { label: "systemd user", loadedText: "enabled", notLoadedText: "disabled", stage: ignoreServiceWriteResult(stageSystemdService), install: ignoreServiceWriteResult(installSystemdService), uninstall: uninstallSystemdService, stop: stopSystemdService, restart: restartSystemdService, isLoaded: isSystemdServiceEnabled, readCommand: readSystemdServiceExecStart, readRuntime: readSystemdServiceRuntime, }, win32: { label: "Scheduled Task", loadedText: "registered", notLoadedText: "missing", stage: ignoreServiceWriteResult(stageScheduledTask), install: ignoreServiceWriteResult(installScheduledTask), uninstall: uninstallScheduledTask, stop: stopScheduledTask, restart: restartScheduledTask, isLoaded: isScheduledTaskInstalled, readCommand: readScheduledTaskCommand, readRuntime: readScheduledTaskRuntime, }, }; function withFutureConfigGuard(service: GatewayService): GatewayService { return { ...service, stage: async (args) => { await assertFutureConfigActionAllowed("rewrite the gateway service"); return await service.stage(args); }, install: async (args) => { await assertFutureConfigActionAllowed("install or rewrite the gateway service"); return await service.install(args); }, uninstall: async (args) => { await assertFutureConfigActionAllowed("uninstall the gateway service"); return await service.uninstall(args); }, stop: async (args) => { await assertFutureConfigActionAllowed("stop the gateway service"); return await service.stop(args); }, restart: async (args) => { await assertFutureConfigActionAllowed("restart the gateway service"); return await service.restart(args); }, }; } function isSupportedGatewayServicePlatform( platform: NodeJS.Platform, ): platform is SupportedGatewayServicePlatform { return Object.hasOwn(GATEWAY_SERVICE_REGISTRY, platform); } export function resolveGatewayService(): GatewayService { if (isSupportedGatewayServicePlatform(process.platform)) { return withFutureConfigGuard(GATEWAY_SERVICE_REGISTRY[process.platform]); } throw new Error(`Gateway service install not supported on ${process.platform}`); }