UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

805 lines (804 loc) • 38.1 kB
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs"; import { S as parseStrictInteger, w as parseStrictPositiveInteger } from "./number-coercion-CuVhdCBs.mjs"; import { u as resolveGatewayLaunchAgentLabel } from "./constants-BLghGsId.mjs"; import { a as isPidAlive, n as getFileLockProcessStartTime, o as isPidDefinitelyDead } from "./pid-alive-CPS_shDt.mjs"; import { h as sleep } from "./utils-DgZn2dLu.mjs"; import { t as resolveDaemonHomeDir } from "./paths-DMJOvLwv.mjs"; import { r as hasCommandProcessCleanupError } from "./exec-result-s7WOth0N.mjs"; import { t as resolveServiceManagerEnv } from "./service-process-env-C6isS50W.mjs"; import { c as withGatewayServiceInstallationRecovery, i as assertGatewayServiceUpdateCurrent, o as isUpdateOwnedGatewayServiceCommand } from "./service-update-authority-CoX8GMUL.mjs"; import { r as ServiceInspectionError } from "./service-inspection-error-Bsltcpi4.mjs"; import { n as resolveLaunchAgentLabel, t as assertValidLaunchAgentLabel } from "./launchd-label-CDVrpZuO.mjs"; import { t as resolveUpdateInstallRoot } from "./update-install-root-DH-onosx.mjs"; import { t as createManagedHandoffLeaseStore } from "./update-managed-service-handoff-lease-C2LEup70.mjs"; import { r as readGatewayOwnerLease } from "./gateway-owner-lease-0RgOZO_d.mjs"; import { r as getSelfAndAncestorPidsSync, t as cleanStaleGatewayProcessesSync } from "./restart-stale-pids-BSJ5Iyx5.mjs"; import { l as readLockPayloadSync, u as resolveGatewayLockPaths } from "./gateway-lock-B6mKYs2x.mjs"; import { n as probePortUsage } from "./ports-probe-DMGtt9dg.mjs"; import { t as mergeGatewayServiceEnv } from "./service-env-merge-BU8q8IlG.mjs"; import { b as isLaunchctlNotLoaded, c as resolveLaunchAgentPlistPath, d as writeLaunchAgentPlist, g as isSystemLaunchDaemonOwnershipError, i as readExistingLaunchAgentPlist, l as resolveLaunchAgentPlistPathForLabel, n as captureLaunchAgentInstallFiles, p as assertNoSystemLaunchDaemonOwnership, u as rewriteLaunchAgentPlistForRestart, v as execLaunchctl, y as formatLaunchctlResultDetail } from "./launchd-service-files-UOyeu4f-.mjs"; import { s as readLaunchAgentProgramArgumentsFromFile } from "./launchd-plist-CCfoVvU9.mjs"; import { n as normalizeWindowsPathSeparators, r as writeFormattedLines, t as formatLine } from "./output-EUQq_O0z.mjs"; import { a as isLaunchctlAlreadyLoaded, f as readLaunchAgentProgramArguments, g as resolveLaunchAgentGuiDomain, h as resolveLaunchAgentGatewayContext, l as parseLaunchctlJob, o as isUnsupportedGuiDomain, p as readLaunchAgentRuntime, r as isLaunchAgentEnabled, t as bootstrapLaunchAgentOrThrow, u as probeLaunchAgentState } from "./launchd-runtime-CkSyqekH.mjs"; import { t as inspectServiceProcessMembershipSync } from "./service-process-membership-CUvHuMvA.mjs"; import { r as formatPortDiagnostics } from "./ports-format-B_qarAEO.mjs"; import { n as inspectPortUsage } from "./ports-inspect-CMru5KsE.mjs"; import { n as scheduleDetachedLaunchdRestartHandoff, t as scheduleDetachedLaunchdMaintenancePark } from "./launchd-restart-handoff-CcxD20VC.mjs"; import { t as createGatewayLifecycleMutationReporter } from "./service-mutation-BITjAaaK.mjs"; import { a as prepareGatewayRestartIntentLegacyProcess, c as GatewayRestartPreparationError, s as writeGatewayServiceRestartIntentSync } from "./restart-intent-D-FdkNH0.mjs"; import path from "node:path"; import fs from "node:fs/promises"; import { spawnSync } from "node:child_process"; import { isDeepStrictEqual } from "node:util"; //#region src/daemon/launchd-current-service.ts /** Detects native launchd membership, including children reparented after ancestor exit. */ /** Environment hints for launchd identity; inherited markers do not prove ancestry. */ function isCurrentProcessLaunchdServiceLabel(label, env = process.env) { return [ env.LAUNCH_JOB_LABEL, env.LAUNCH_JOB_NAME, env.XPC_SERVICE_NAME ].some((value) => normalizeOptionalString(value) === label) || normalizeOptionalString(env.OPENCLAW_LAUNCHD_LABEL) === label && normalizeOptionalString(env.OPENCLAW_SERVICE_MARKER) === "openclaw" && Boolean(normalizeOptionalString(env.OPENCLAW_SERVICE_KIND)); } /** A completed parent walk alone cannot exclude reparented members of a launchd job. */ async function isCurrentProcessInsideLaunchdService(label) { const probe = await probeLaunchAgentState(`${resolveLaunchAgentGuiDomain()}/${label}`); if (probe.state === "running" && probe.runtime.pid !== void 0) { const ancestors = getSelfAndAncestorPidsSync(); if (ancestors.has(probe.runtime.pid)) return true; const membership = inspectServiceProcessMembershipSync(probe.runtime.pid, "darwin"); if (membership === "inside") return true; if (membership === "outside") return false; if (membership === "unknown") throw new ServiceInspectionError("service-membership-unverified"); if (!ancestors.has(1)) throw new ServiceInspectionError("service-ancestry-unverified"); return false; } if (probe.state === "unknown" || probe.state === "running") throw new ServiceInspectionError("service-membership-unverified"); return false; } //#endregion //#region src/daemon/launchd-install.ts /** Transactional LaunchAgent installation, staging, rollback, and removal. */ async function uninstallLaunchAgent({ env, stdout }) { await assertExternalLaunchAgentMutation(env, "uninstall"); const domain = resolveLaunchAgentGuiDomain(); const label = resolveLaunchAgentLabel(env); const plistPath = resolveLaunchAgentPlistPath(env); if ((await probeLaunchAgentState(`${domain}/${label}`)).state !== "not-loaded") { const bootout = await execLaunchctl([ "bootout", domain, plistPath ]); if (bootout.code !== 0 && !isLaunchctlNotLoaded(bootout)) throw new Error(`launchctl bootout failed: ${formatLaunchctlResultDetail(bootout)}`); } try { await fs.lstat(plistPath); } catch (error) { if (error.code !== "ENOENT") throw createLaunchAgentRemovalError(error); stdout.write(`LaunchAgent not found at ${plistPath}\n`); return; } const home = normalizeWindowsPathSeparators(resolveDaemonHomeDir(env)); const trashDir = path.posix.join(home, ".Trash"); const dest = path.join(trashDir, `${label}.plist`); try { await fs.mkdir(trashDir, { recursive: true }); await fs.rename(plistPath, dest); stdout.write(`${formatLine("Moved LaunchAgent to Trash", dest)}\n`); } catch (error) { if (error.code === "ENOENT") try { await fs.lstat(plistPath); } catch (accessError) { if (accessError.code === "ENOENT") { stdout.write(`LaunchAgent not found at ${plistPath}\n`); return; } throw createLaunchAgentRemovalError(accessError); } throw createLaunchAgentRemovalError(error); } } function createLaunchAgentRemovalError(error) { const code = error.code; return /* @__PURE__ */ new Error(`LaunchAgent removal failed${code ? ` (${code})` : ""}. Check permissions and retry.`); } async function currentGatewayLaunchAgentLabel(targetEnv) { const configuredCurrentLabel = process.env.OPENCLAW_LAUNCHD_LABEL?.trim(); const candidates = /* @__PURE__ */ new Set([resolveLaunchAgentLabel(targetEnv), ...configuredCurrentLabel ? [assertValidLaunchAgentLabel(configuredCurrentLabel)] : []]); for (const label of candidates) if (await isCurrentProcessInsideLaunchdService(label)) return label; } async function assertExternalLaunchAgentMutation(env, action) { const currentLabel = await currentGatewayLaunchAgentLabel(env); if (!currentLabel) return; throw new Error(`Refusing to ${action} LaunchAgent ${resolveLaunchAgentLabel(env)} from inside ${currentLabel}; run this command from an external shell.`); } async function stageLaunchAgent(args) { const { plistPath, stdoutPath } = await writeLaunchAgentPlist(args); writeFormattedLines(args.stdout, [{ label: "Staged LaunchAgent", value: plistPath }, { label: "Logs", value: stdoutPath }], { leadingBlankLine: true }); return { plistPath }; } async function snapshotLaunchAgentLoadedState(plistContents, serviceTarget) { const probe = await probeLaunchAgentState(serviceTarget); if (probe.state === "unknown") throw new Error(`launchctl print could not determine whether ${serviceTarget} is loaded: ${probe.detail ?? "unknown error"}`); const loaded = probe.state !== "not-loaded"; if (loaded && plistContents === null) throw new Error(`LaunchAgent ${serviceTarget} is loaded but its plist is missing; refusing an install that cannot restore the current definition if activation fails.`); return loaded; } async function deactivateLaunchAgentDefinition(domain, plistPath) { for (const args of [[ "bootout", domain, plistPath ], ["unload", plistPath]]) { assertGatewayServiceUpdateCurrent(); const result = await execLaunchctl(args); assertGatewayServiceUpdateCurrent(); if (result.code !== 0 && !isLaunchctlNotLoaded(result)) throw new Error(`launchctl ${args[0]} failed during LaunchAgent install: ${formatLaunchctlResultDetail(result)}`); } } async function installLaunchAgent(args) { const targetPlistPath = resolveLaunchAgentPlistPath(args.env); const label = resolveLaunchAgentLabel(args.env); const domain = resolveLaunchAgentGuiDomain(); const serviceTarget = `${domain}/${label}`; const { publication, loaded, enabled } = await withGatewayServiceInstallationRecovery(async () => { await assertExternalLaunchAgentMutation(args.env, "install"); const captured = args.definitionTransaction ? { kind: "transaction", hooks: args.definitionTransaction } : { kind: "local", files: await captureLaunchAgentInstallFiles(args.env) }; const previous = captured.kind === "local" ? captured.files.originals.get(targetPlistPath).snapshot : await readExistingLaunchAgentPlist(targetPlistPath); const wasEnabled = args.preserveAutoStart ? await isLaunchAgentEnabled({ env: args.env }) : void 0; return { publication: captured, loaded: await snapshotLaunchAgentLoadedState(previous?.contents ?? null, serviceTarget), enabled: wasEnabled }; }, async () => false); let activationAttempted = false; const install = async () => { const published = await writeLaunchAgentPlist(args, publication.kind === "local" ? publication.files : void 0); await (publication.kind === "local" ? publication.files.assertCurrent() : publication.hooks.beforeWrite()); await assertNoSystemLaunchDaemonOwnership(label); assertGatewayServiceUpdateCurrent(); activationAttempted = true; if (loaded) await deactivateLaunchAgentDefinition(domain, published.plistPath); await bootstrapLaunchAgentOrThrow({ domain, serviceTarget, plistPath: published.plistPath, actionHint: "openclaw gateway install --force", retryPendingTeardown: true, assertCurrent: assertGatewayServiceUpdateCurrent, preserveAutoStart: args.preserveAutoStart, preservedEnabled: enabled }); assertGatewayServiceUpdateCurrent(); return published; }; const { plistPath, stdoutPath } = publication.kind === "transaction" ? await install() : await withGatewayServiceInstallationRecovery(install, async () => { const files = publication.files; if (activationAttempted) { await files.assertCurrent(); const current = await probeLaunchAgentState(serviceTarget); if (current.state === "unknown") throw new Error(`launchctl print could not determine whether ${serviceTarget} is loaded during LaunchAgent rollback: ${current.detail ?? "unknown error"}`); if (current.state !== "not-loaded") { await files.assertCurrent(); const bootout = await execLaunchctl(["bootout", serviceTarget]); if (bootout.code !== 0 && !isLaunchctlNotLoaded(bootout)) throw new Error(`launchctl bootout failed: ${formatLaunchctlResultDetail(bootout)}`); } } const restored = await files.restore(); if (activationAttempted && loaded) { await files.assertCurrent(); await assertNoSystemLaunchDaemonOwnership(label); await bootstrapLaunchAgentOrThrow({ domain, serviceTarget, plistPath: targetPlistPath, actionHint: "openclaw gateway start", retryPendingTeardown: true, assertCurrent: assertGatewayServiceUpdateCurrent, preserveAutoStart: args.preserveAutoStart, preservedEnabled: enabled }); } return restored || activationAttempted; }); writeFormattedLines(args.stdout, [{ label: "Installed LaunchAgent", value: plistPath }, { label: "Logs", value: stdoutPath }], { leadingBlankLine: true }); return { plistPath }; } //#endregion //#region src/daemon/launchd-lifecycle.ts /** LaunchAgent bootstrap recovery plus start and restart lifecycle controls. */ const LAUNCHCTL_PROTECTED_PID_TIMEOUT_MS = 2e3; function readLaunchAgentPidForCleanupSync(serviceTarget) { const probe = spawnSync("launchctl", ["print", serviceTarget], { env: resolveServiceManagerEnv(), encoding: "utf8", timeout: LAUNCHCTL_PROTECTED_PID_TIMEOUT_MS }); const result = { stdout: probe.stdout ?? "", stderr: probe.error?.message ?? probe.stderr ?? "", code: probe.error ? 1 : probe.status ?? 1 }; if (result.code !== 0) throw new Error(`launchctl print failed: ${formatLaunchctlResultDetail(result)}`); const pid = parseLaunchctlJob(result.stdout || result.stderr || "", serviceTarget).runtime.pid; if (pid === void 0) throw new Error("launchctl print did not report a running pid"); return pid; } async function repairLaunchAgentBootstrap(args) { const env = args.env ?? process.env; const domain = resolveLaunchAgentGuiDomain(); const label = resolveLaunchAgentLabel(env); const plistPath = resolveLaunchAgentPlistPath(env); const serviceTarget = `${domain}/${label}`; try { await assertNoSystemLaunchDaemonOwnership(label); } catch (error) { if (!isSystemLaunchDaemonOwnershipError(error)) throw error; return { ok: false, status: error.ownership.status === "unverifiable" ? "system-launchdaemon-unverifiable" : "system-launchdaemon-conflict", detail: error.message }; } const warn = args.warn ?? ((message) => console.warn(formatLine("Warning", message))); await rewriteLaunchAgentPlistForRestart({ env, label, plistPath, warn }); try { if (await bootstrapLaunchAgentOrThrow({ domain, serviceTarget, plistPath, actionHint: "openclaw gateway start" }) === "bootstrapped") return { ok: true, status: "repaired" }; } catch (error) { if (hasCommandProcessCleanupError(error)) throw error; const boot = error instanceof Error ? error.cause : void 0; if (!boot || typeof boot !== "object" || !("stderr" in boot) || typeof boot.stderr !== "string" || !("stdout" in boot) || typeof boot.stdout !== "string" || !("code" in boot) || typeof boot.code !== "number" || !("termination" in boot)) throw error; const detail = (boot.stderr || boot.stdout).trim(); if (isUnsupportedGuiDomain(detail)) return { ok: false, status: "gui-session-unavailable", detail, domain }; if (boot.termination !== "exit" || !isLaunchctlAlreadyLoaded({ code: boot.code, termination: "exit", stderr: boot.stderr, stdout: boot.stdout })) return { ok: false, status: "bootstrap-failed", detail: detail || void 0 }; } if ((await readLaunchAgentRuntime(env)).status === "running") return { ok: true, status: "already-loaded" }; const kick = await execLaunchctl(["kickstart", serviceTarget]); if (kick.code !== 0) return { ok: false, status: "kickstart-failed", detail: (kick.stderr || kick.stdout).trim() || void 0 }; return { ok: true, status: "already-loaded" }; } function writeLaunchAgentActionLine(stdout, label, value) { try { stdout.write(`${formatLine(label, value)}\n`); } catch (err) { if (err?.code !== "EPIPE") throw err; } } async function ensureLaunchAgentLoadedAfterFailure(params) { params.assertCurrent?.(); const probe = await execLaunchctl(["print", params.serviceTarget]); params.assertCurrent?.(); if (probe.code === 0) return { loaded: true }; try { await bootstrapLaunchAgentOrThrow({ ...params, actionHint: "openclaw gateway start" }); return { loaded: true }; } catch (error) { if (hasCommandProcessCleanupError(error)) throw error; return { loaded: false, detail: error instanceof Error ? error.message : String(error) }; } } async function rethrowLaunchAgentActivationFailure(params, error) { if (hasCommandProcessCleanupError(error)) throw error; const restored = await ensureLaunchAgentLoadedAfterFailure(params); const failure = error instanceof Error ? error.message : String(error); throw new Error(restored.loaded ? `${failure}\nLaunchAgent ${params.serviceTarget} is loaded; launchd can retry its KeepAlive job. Run openclaw gateway status --deep to inspect startup.` : [ failure, `LaunchAgent ${params.serviceTarget} is not loaded and could not be restored: ${restored.detail}`, "The gateway is down and launchd has no job left to respawn it.", `Fix: run \`openclaw gateway start\`, or \`launchctl bootstrap ${params.domain} ${params.plistPath}\`.` ].join("\n"), { cause: error }); } async function needsLaunchAgentBootstrap(kickstart, serviceTarget, assertCurrent) { if (kickstart.code === 0 || isLaunchctlNotLoaded(kickstart)) return kickstart.code !== 0; assertCurrent?.(); const observed = await probeLaunchAgentState(serviceTarget); assertCurrent?.(); return observed.state === "not-loaded"; } async function startLaunchAgent({ stdout, env, onMutation, assertCurrent, preserveAutoStart, preserveDefinition }) { const serviceEnv = env ?? process.env; const domain = resolveLaunchAgentGuiDomain(); const label = resolveLaunchAgentLabel(serviceEnv); const plistPath = resolveLaunchAgentPlistPath(serviceEnv); const serviceTarget = `${domain}/${label}`; const reportMutation = createGatewayLifecycleMutationReporter(onMutation); await assertNoSystemLaunchDaemonOwnership(label); let enabled = false; if (!preserveAutoStart) { assertCurrent?.(); enabled = (await execLaunchctl(["enable", serviceTarget])).code === 0; if (enabled) reportMutation("enable"); } try { assertCurrent?.(); let start = await execLaunchctl(["kickstart", serviceTarget]); if (await needsLaunchAgentBootstrap(start, serviceTarget, assertCurrent)) { await bootstrapLaunchAgentOrThrow({ domain, serviceTarget, plistPath, actionHint: "openclaw gateway start", onMutation: reportMutation, skipEnable: enabled, preserveAutoStart, assertCurrent, retryPendingTeardown: preserveDefinition }); assertCurrent?.(); start = await execLaunchctl(["kickstart", serviceTarget]); } if (start.code !== 0) throw new Error(`launchctl kickstart failed: ${start.stderr || start.stdout}`.trim()); } catch (error) { await rethrowLaunchAgentActivationFailure({ domain, serviceTarget, plistPath, onMutation: reportMutation, assertCurrent, retryPendingTeardown: preserveDefinition, preserveAutoStart }, error); } reportMutation("kickstart"); writeLaunchAgentActionLine(stdout, "Started LaunchAgent", serviceTarget); } async function restartLaunchAgent({ onRestartAttempted, preserveDefinition, preserveAutoStart, stdout, env, warn, onMutation, assertCurrent }) { const serviceEnv = env ?? process.env; const domain = resolveLaunchAgentGuiDomain(); const label = resolveLaunchAgentLabel(serviceEnv); const plistPath = resolveLaunchAgentPlistPath(serviceEnv); const serviceTarget = `${domain}/${label}`; const reportMutation = createGatewayLifecycleMutationReporter(onMutation); await assertNoSystemLaunchDaemonOwnership(label); const detached = await isCurrentProcessInsideLaunchdService(label); if (!detached) { const { env: cleanupEnv, port: cleanupPort, probeHosts } = await resolveLaunchAgentGatewayContext(serviceEnv); if (cleanupPort !== null) { assertGatewayServiceUpdateCurrent(); cleanStaleGatewayProcessesSync(cleanupPort, { env: cleanupEnv, assertCurrent: assertGatewayServiceUpdateCurrent, resolveProtectedPid: () => { const pid = readLaunchAgentPidForCleanupSync(serviceTarget); assertGatewayServiceUpdateCurrent(); return pid; } }); const diagnostics = await inspectPortUsage(cleanupPort, { probeHosts }).catch(() => null); if (diagnostics?.status === "busy") { const managedPid = (await readLaunchAgentRuntime(serviceEnv)).pid; if (!(managedPid !== void 0 && diagnostics.listeners.length > 0 && diagnostics.listeners.every((listener) => listener.pid === managedPid))) throw new Error([`gateway port ${cleanupPort} is busy but is not verifiably owned by LaunchAgent ${label}`, ...formatPortDiagnostics(diagnostics)].join("\n")); } } } const plistReloadNeeded = !preserveDefinition && await rewriteLaunchAgentPlistForRestart({ env: serviceEnv, label, plistPath, stdout, warn }); if (detached) { if (isUpdateOwnedGatewayServiceCommand()) throw new Error("UPDATE_NATIVE_AUTHORITY: update-owned native restart requires an external executor, not a detached service handoff."); const handoff = scheduleDetachedLaunchdRestartHandoff({ env: serviceEnv, mode: plistReloadNeeded ? "reload" : "kickstart", waitForPid: process.pid }); if (!handoff.ok) throw new Error(`launchd restart handoff failed: ${handoff.error}`); reportMutation(plistReloadNeeded ? "handoff-reload" : "handoff-kickstart"); writeLaunchAgentActionLine(stdout, "Scheduled LaunchAgent restart", serviceTarget); return { outcome: "scheduled" }; } try { if (!preserveAutoStart) { assertCurrent?.(); if ((await execLaunchctl(["enable", serviceTarget])).code === 0) reportMutation("enable"); } if (plistReloadNeeded) { assertCurrent?.(); const bootout = await execLaunchctl(["bootout", serviceTarget]); if (bootout.code !== 0 && !isLaunchctlNotLoaded(bootout)) throw new Error(`launchctl bootout failed: ${formatLaunchctlResultDetail(bootout)}`); if (bootout.code === 0) reportMutation("bootout"); await bootstrapLaunchAgentOrThrow({ domain, serviceTarget, plistPath, actionHint: "openclaw gateway restart", onMutation: reportMutation, assertCurrent, preserveAutoStart, retryPendingTeardown: true }); } else { assertCurrent?.(); onRestartAttempted?.(); const start = await execLaunchctl([ "kickstart", "-k", serviceTarget ]); if (start.code === 0) reportMutation("kickstart"); else { if (!await needsLaunchAgentBootstrap(start, serviceTarget, assertCurrent)) throw new Error(`launchctl kickstart failed: ${start.stderr || start.stdout}`.trim()); await bootstrapLaunchAgentOrThrow({ domain, serviceTarget, plistPath, actionHint: "openclaw gateway restart", onMutation: reportMutation, assertCurrent, preserveAutoStart, retryPendingTeardown: preserveDefinition }); if (preserveDefinition) { assertCurrent?.(); const kick = await execLaunchctl(["kickstart", serviceTarget]); if (kick.code !== 0) throw new Error(`launchctl kickstart failed: ${kick.stderr || kick.stdout}`.trim()); reportMutation("kickstart"); } } } } catch (error) { await rethrowLaunchAgentActivationFailure({ domain, serviceTarget, plistPath, onMutation: reportMutation, assertCurrent, preserveAutoStart, retryPendingTeardown: preserveDefinition || plistReloadNeeded }, error); } writeLaunchAgentActionLine(stdout, "Restarted LaunchAgent", serviceTarget); return { outcome: "completed" }; } //#endregion //#region src/daemon/launchd-stop.ts /** LaunchAgent stop semantics and in-service maintenance parking. */ const LAUNCH_AGENT_STOP_PORT_RELEASE_TIMEOUT_MS = 2e4; const LAUNCH_AGENT_STOP_PORT_RELEASE_POLL_MS = 100; const LAUNCH_AGENT_STOP_TIMEOUT_MS = 3e4; function launchAgentStopError(serviceTarget, detail) { return /* @__PURE__ */ new Error(`${detail}. Run \`launchctl bootout ${serviceTarget}\` from an external terminal in the service owner's logged-in macOS GUI session.`); } function verifyLaunchAgentStopProbe(serviceTarget, probe) { if (probe.state === "unknown") throw launchAgentStopError(serviceTarget, `launchctl print could not verify LaunchAgent stop: ${probe.detail ?? "unknown error"}`); if (probe.state !== "running") return; if (probe.runtime.pid === void 0) throw launchAgentStopError(serviceTarget, "launchctl print reported a running job without a PID"); return probe.runtime.pid; } async function waitForLaunchAgentUnloaded(serviceTarget, initialPid, assertCurrent) { const pids = new Set(initialPid === void 0 ? [] : [initialPid]); const deadline = Date.now() + LAUNCH_AGENT_STOP_TIMEOUT_MS; for (;;) { const probe = await probeLaunchAgentState(serviceTarget, 5e3); assertCurrent?.(); const observedPid = verifyLaunchAgentStopProbe(serviceTarget, probe); if (observedPid !== void 0) pids.add(observedPid); const alivePids = [...pids].filter((pid) => !isPidDefinitelyDead(pid)); if (probe.state === "not-loaded" && alivePids.length === 0) return; if (Date.now() >= deadline) throw launchAgentStopError(serviceTarget, `LaunchAgent stop did not complete: ${serviceTarget} is ${probe.state === "not-loaded" ? "unloaded" : "still loaded"}${alivePids.length ? `; PID ${alivePids.join(", ")} is still alive` : ""}`); await sleep(Math.min(100, Math.max(0, deadline - Date.now()))); } } async function waitForGatewayPortRelease(port, probeHosts) { const deadline = Date.now() + LAUNCH_AGENT_STOP_PORT_RELEASE_TIMEOUT_MS; while (Date.now() < deadline) { await sleep(Math.min(LAUNCH_AGENT_STOP_PORT_RELEASE_POLL_MS, deadline - Date.now())); if (await probePortUsage(port, probeHosts) === "free") return true; } return false; } async function assertGatewayPortReleasedAfterStop(env, assertCurrent) { const { env: cleanupEnv, port, probeHosts } = await resolveLaunchAgentGatewayContext(env); if (port === null) return; await assertCurrent(); assertGatewayServiceUpdateCurrent(); cleanStaleGatewayProcessesSync(port, { env: cleanupEnv, assertCurrent: assertGatewayServiceUpdateCurrent }); const diagnostics = await inspectPortUsage(port, { probeHosts }).catch(() => null); if (diagnostics?.status !== "busy") return; if (await waitForGatewayPortRelease(port, probeHosts)) return; throw new Error([`gateway port ${port} is still busy after LaunchAgent stop`, ...formatPortDiagnostics(diagnostics)].join("\n")); } async function stopLaunchAgent({ stdout, env, disable: persistDisable, onMutation, assertCurrent, updateHandoff }) { const serviceEnv = env ?? process.env; const domain = resolveLaunchAgentGuiDomain(); const label = resolveLaunchAgentLabel(serviceEnv); const serviceTarget = `${domain}/${label}`; const reportMutation = createGatewayLifecycleMutationReporter(onMutation); const updateOwned = isUpdateOwnedGatewayServiceCommand(); let intentEnv = serviceEnv; const assertNativeCurrent = () => { assertCurrent?.(); assertGatewayServiceUpdateCurrent(); }; const insideService = await isCurrentProcessInsideLaunchdService(label); const assertStopCurrent = async () => { assertNativeCurrent(); const handoff = updateOwned && updateHandoff ? createManagedHandoffLeaseStore().read(resolveUpdateInstallRoot(updateHandoff.root)) : void 0; const transferred = handoff?.kind === "current" && (handoff.lease.helper.pid !== handoff.lease.executor.pid || handoff.lease.helper.startIdentity !== handoff.lease.executor.startIdentity); if (insideService || transferred || updateOwned && intentEnv.OPENCLAW_UPDATE_RUN_HANDOFF === "1") { if (!(updateOwned && updateHandoff && await (await import("./update-managed-service-handoff-current-BnQSJd4t.mjs")).isCurrentManagedServiceUpdateHandoffProcess({ ...updateHandoff, env: intentEnv }))) throw launchAgentStopError(serviceTarget, `Refusing to stop LaunchAgent ${label} from inside the same launchd service`); } assertNativeCurrent(); }; await assertStopCurrent(); const initialPid = verifyLaunchAgentStopProbe(serviceTarget, await probeLaunchAgentState(serviceTarget, 5e3)); let clearIntent; let assertServingCurrent = assertNativeCurrent; let warning; try { if (updateOwned) { const command = await readLaunchAgentProgramArguments(serviceEnv, { requireEffective: true }); assertNativeCurrent(); if (!command) throw new GatewayRestartPreparationError("service-command"); intentEnv = mergeGatewayServiceEnv(serviceEnv, command); await assertStopCurrent(); const owner = readGatewayOwnerLease({ env: intentEnv, current: true }); const legacyLockPath = owner ? void 0 : resolveGatewayLockPaths(intentEnv).stateLockPath; const legacyLock = legacyLockPath ? readLockPayloadSync(legacyLockPath, true) : void 0; const legacyProcess = await prepareGatewayRestartIntentLegacyProcess({ env: intentEnv, command, runtimePid: initialPid, readRuntime: () => readLaunchAgentRuntime(serviceEnv), assertCurrent: assertNativeCurrent }); const currentProbe = await probeLaunchAgentState(serviceTarget, 5e3); await assertStopCurrent(); if (verifyLaunchAgentStopProbe(serviceTarget, currentProbe) !== initialPid) throw new GatewayRestartPreparationError("serving-owner"); const assertIntentCurrent = () => { assertNativeCurrent(); if (legacyProcess && (!legacyLock || !legacyLockPath || !isPidAlive(legacyProcess.pid) || getFileLockProcessStartTime(legacyProcess.pid, intentEnv) !== legacyProcess.startTime || !isPidAlive(legacyLock.pid) || getFileLockProcessStartTime(legacyLock.pid, intentEnv) !== legacyLock.startTime || !isDeepStrictEqual(legacyLock, readLockPayloadSync(legacyLockPath, true)))) throw new GatewayRestartPreparationError("serving-owner"); const currentOwner = readGatewayOwnerLease({ env: intentEnv, current: true }); if (currentOwner?.owner !== owner?.owner || currentOwner?.pid !== owner?.pid || currentOwner?.startedAt !== owner?.startedAt || currentOwner?.state !== owner?.state) throw new GatewayRestartPreparationError("serving-owner"); }; assertServingCurrent = assertIntentCurrent; writeGatewayServiceRestartIntentSync({ env: intentEnv, service: { kind: "launchd", name: label }, nativePid: initialPid, nativeStopped: currentProbe.state !== "running", legacyProcess, reason: "update.run", assertCurrent: assertIntentCurrent, onRecorded: (clear) => { clearIntent = clear; } }); } if (persistDisable) { await assertStopCurrent(); assertServingCurrent(); const disabled = await execLaunchctl(["disable", serviceTarget]); if (disabled.code === 0) reportMutation("disable"); else warning = `launchctl disable failed; used bootout fallback without persisting disable: ${formatLaunchctlResultDetail(disabled)}`; } await assertStopCurrent(); assertServingCurrent(); const bootout = await execLaunchctl(["bootout", serviceTarget], LAUNCH_AGENT_STOP_TIMEOUT_MS); if (bootout.code !== 0 && !isLaunchctlNotLoaded(bootout)) throw launchAgentStopError(serviceTarget, `launchctl bootout failed: ${formatLaunchctlResultDetail(bootout)}`); } catch (error) { clearIntent?.(); throw error; } reportMutation(persistDisable ? "disable-bootout" : "bootout"); await waitForLaunchAgentUnloaded(serviceTarget, initialPid, assertCurrent); if (warning) stdout.write(`${formatLine("Warning", warning)}\n`); await assertGatewayPortReleasedAfterStop(serviceEnv, assertStopCurrent); assertCurrent?.(); stdout.write(`${formatLine(warning ? "Stopped LaunchAgent (degraded)" : "Stopped LaunchAgent", serviceTarget)}\n`); } async function parkCurrentLaunchAgentForMaintenance(params = {}) { const serviceEnv = params.env ?? process.env; const domain = resolveLaunchAgentGuiDomain(); const label = resolveLaunchAgentLabel(serviceEnv); if (!await isCurrentProcessInsideLaunchdService(label)) return false; const serviceTarget = `${domain}/${label}`; const disable = await execLaunchctl(["disable", serviceTarget]); if (disable.code !== 0) throw new Error(`launchctl disable failed while parking ${serviceTarget}: ${formatLaunchctlResultDetail(disable)}`); const handoff = scheduleDetachedLaunchdMaintenancePark({ env: serviceEnv, waitForPid: process.pid }); const handoffError = !handoff.ok ? handoff.error : await handoff.value ? void 0 : "helper failed to spawn"; if (handoffError) { const rollback = await execLaunchctl(["enable", serviceTarget]); const rollbackDetail = rollback.code === 0 ? "restored launchd enable state" : `launchctl enable rollback failed: ${formatLaunchctlResultDetail(rollback)}`; throw new Error(`launchd maintenance park handoff failed: ${handoffError}; ${rollbackDetail}`); } return true; } //#endregion //#region src/daemon/launchd-update-jobs.ts /** Discovery and shutdown of stale OpenClaw launchd updater jobs. */ const OPENCLAW_UPDATE_LAUNCHD_LABEL_PREFIX = "ai.openclaw.update."; const MANUAL_UPDATE_LAUNCHD_LABEL_PATTERN = /^ai\.openclaw\.manual-update\.\d+$/; const OPENCLAW_PROFILE_UPDATE_LAUNCHD_LABEL_PATTERN = /^ai\.openclaw\.[A-Za-z0-9._-]+\.update\.[A-Za-z0-9._-]+$/; const OPENCLAW_DIRECT_CLI_NAMES = /* @__PURE__ */ new Set(["openclaw", "openclaw.mjs"]); const OPENCLAW_NODE_RUNTIME_NAMES = /* @__PURE__ */ new Set([ "bun", "bun.exe", "node", "node.exe" ]); function normalizeOpenClawUpdateLaunchdLabelCandidate(label) { if (typeof label !== "string") return null; const trimmed = label.trim(); if (trimmed.startsWith(OPENCLAW_UPDATE_LAUNCHD_LABEL_PREFIX) || MANUAL_UPDATE_LAUNCHD_LABEL_PATTERN.test(trimmed)) return { label: trimmed, requiresMetadata: false }; return OPENCLAW_PROFILE_UPDATE_LAUNCHD_LABEL_PATTERN.test(trimmed) ? { label: trimmed, requiresMetadata: true } : null; } function isCurrentGatewayLaunchdLabel(label, env) { if (label === resolveGatewayLaunchAgentLabel(env.OPENCLAW_PROFILE)) return true; if (env.OPENCLAW_SERVICE_MARKER?.trim() !== "openclaw" || env.OPENCLAW_SERVICE_KIND?.trim() !== "gateway") return false; const configuredLabel = env.OPENCLAW_LAUNCHD_LABEL?.trim(); return Boolean(configuredLabel && label === configuredLabel); } function resolveCurrentOpenClawUpdateLaunchdJobLabel(env = process.env) { for (const label of [ env.LAUNCH_JOB_LABEL, env.LAUNCH_JOB_NAME, env.XPC_SERVICE_NAME, env.OPENCLAW_LAUNCHD_LABEL ]) { const candidate = normalizeOpenClawUpdateLaunchdLabelCandidate(label); if (candidate && !isCurrentGatewayLaunchdLabel(candidate.label, env)) return candidate; } return null; } function parseLaunchctlListOpenClawUpdateJobCandidates(output) { const jobs = []; for (const rawLine of output.split(/\r?\n/)) { const line = rawLine.trim(); if (!line) continue; const [pidRaw, statusRaw, ...labelParts] = line.split(/\s+/); const candidate = normalizeOpenClawUpdateLaunchdLabelCandidate(labelParts.join(" ")); if (!candidate) continue; const pid = pidRaw === "-" ? void 0 : parseStrictPositiveInteger(pidRaw ?? ""); const lastExitStatus = parseStrictInteger(statusRaw ?? ""); jobs.push({ label: candidate.label, requiresMetadata: candidate.requiresMetadata, ...pid !== void 0 ? { pid } : {}, ...lastExitStatus !== void 0 ? { lastExitStatus } : {} }); } return jobs.toSorted((a, b) => a.label.localeCompare(b.label)); } function hasOpenClawUpdateLaunchdMarker(env) { return env?.OPENCLAW_UPDATE_RUN_HANDOFF?.trim() === "1"; } function isOpenClawUpdateCommandPrefix(programArguments, updateIndex) { if (updateIndex === 1) { const cliName = path.basename(programArguments[0] ?? "").toLowerCase(); return OPENCLAW_DIRECT_CLI_NAMES.has(cliName); } if (updateIndex !== 2) return false; const runtimeName = path.basename(programArguments[0] ?? "").toLowerCase(); const entryName = path.basename(programArguments[1] ?? "").toLowerCase(); return OPENCLAW_NODE_RUNTIME_NAMES.has(runtimeName) && entryName === "openclaw.mjs"; } function isOpenClawUpdateProgramArguments(programArguments) { if (!Array.isArray(programArguments) || programArguments.length === 0) return false; const updateIndex = programArguments.findIndex((arg) => arg.trim() === "update"); if (updateIndex < 0 || !programArguments.slice(updateIndex + 1).includes("--yes")) return false; return isOpenClawUpdateCommandPrefix(programArguments, updateIndex) && !programArguments.some((arg) => arg.trim() === "gateway"); } async function isLaunchdJobConfirmedOpenClawUpdater(params) { const plistPath = resolveLaunchAgentPlistPathForLabel(params.env, params.label); const command = await readLaunchAgentProgramArgumentsFromFile(plistPath); return hasOpenClawUpdateLaunchdMarker(command?.environment) || isOpenClawUpdateProgramArguments(command?.programArguments); } async function findStaleOpenClawUpdateLaunchdJobs(env = process.env) { if (process.platform !== "darwin") return []; const result = await execLaunchctl(["list"]); if (result.code !== 0) return []; const jobs = []; for (const { requiresMetadata, ...job } of parseLaunchctlListOpenClawUpdateJobCandidates(result.stdout)) { if (isCurrentGatewayLaunchdLabel(job.label, env)) continue; if (requiresMetadata && !await isLaunchdJobConfirmedOpenClawUpdater({ label: job.label, env })) continue; jobs.push(job); } return jobs; } async function disableCurrentOpenClawUpdateLaunchdJob(env = process.env) { const candidate = resolveCurrentOpenClawUpdateLaunchdJobLabel(env); if (!candidate || process.platform !== "darwin") return false; const trustCurrentEnvMarker = isCurrentProcessLaunchdServiceLabel(candidate.label, env); if (candidate.requiresMetadata && !(trustCurrentEnvMarker && hasOpenClawUpdateLaunchdMarker(env) || await isLaunchdJobConfirmedOpenClawUpdater({ label: candidate.label, env }))) return false; const serviceTarget = `${resolveLaunchAgentGuiDomain()}/${assertValidLaunchAgentLabel(candidate.label)}`; return (await execLaunchctl(["disable", serviceTarget])).code === 0; } //#endregion export { repairLaunchAgentBootstrap as a, installLaunchAgent as c, stopLaunchAgent as i, stageLaunchAgent as l, findStaleOpenClawUpdateLaunchdJobs as n, restartLaunchAgent as o, parkCurrentLaunchAgentForMaintenance as r, startLaunchAgent as s, disableCurrentOpenClawUpdateLaunchdJob as t, uninstallLaunchAgent as u };