openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
799 lines (798 loc) • 34.4 kB
JavaScript
import { S as parseStrictInteger, w as parseStrictPositiveInteger } from "./number-coercion-CLj0HTDM.js";
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { h as resolveLegacyGatewayLaunchAgentLabels, l as resolveGatewayLaunchAgentLabel } from "./constants-ChqKLfPp.js";
import "./utils-P__uGsPB.js";
import { t as sleep } from "./sleep-D7nua6TP.js";
import { t as resolveDaemonHomeDir } from "./paths-CzCbqt0l.js";
import { d as readLaunchAgentProgramArgumentsFromFile, f as assertValidLaunchAgentLabel, g as isLaunchctlNotLoaded, h as formatLaunchctlResultDetail, i as isSystemLaunchDaemonOwnershipError, m as execLaunchctl, p as resolveLaunchAgentLabel, t as assertNoSystemLaunchDaemonOwnership } from "./launchd-system-BlHvFe-Q.js";
import { n as probePortUsage } from "./ports-probe-funEe1wt.js";
import { i as writeFormattedLines, n as formatLine, r as normalizeWindowsPathSeparators, t as createGatewayLifecycleMutationReporter } from "./service-mutation-BxHhMRDF.js";
import { c as rewriteLaunchAgentPlistForRestart, i as resolveLaunchAgentEnvWrapperPath, l as writeLaunchAgentPlist, n as readExistingLaunchAgentPlist, o as resolveLaunchAgentPlistPath, r as resolveLaunchAgentEnvFilePath, s as resolveLaunchAgentPlistPathForLabel, t as publishLaunchAgentPlist } from "./launchd-service-files-ook2RU8t.js";
import { a as isLaunchctlAlreadyLoaded, f as readLaunchAgentRuntime, h as waitForLaunchAgentStopped, l as parseLaunchctlPrint, m as resolveLaunchAgentGuiDomain, o as isUnsupportedGuiDomain, p as resolveLaunchAgentGatewayContext, t as bootstrapLaunchAgentOrThrow, u as probeLaunchAgentState } from "./launchd-runtime-D4UiKWFz.js";
import { r as formatPortDiagnostics } from "./ports-format-D-ReVpaa.js";
import { n as inspectPortUsage } from "./ports-inspect-CHTlDwBh.js";
import { t as cleanStaleGatewayProcessesSync } from "./restart-stale-pids-CDR-7Ato.js";
import { n as scheduleDetachedLaunchdRestartHandoff, t as scheduleDetachedLaunchdMaintenancePark } from "./launchd-restart-handoff-CMIp-2vM.js";
import path from "node:path";
import fs from "node:fs/promises";
import { spawnSync } from "node:child_process";
import { randomUUID } from "node:crypto";
//#region src/daemon/launchd-current-service.ts
/** Detects whether the current process is running inside a launchd service label. */
/** Checks whether the current process appears to be running under the requested launchd label. */
function isCurrentProcessLaunchdServiceLabel(label, env = process.env, options = {}) {
const currentLabels = [
env.LAUNCH_JOB_LABEL,
env.LAUNCH_JOB_NAME,
env.XPC_SERVICE_NAME
].flatMap((value) => {
const normalized = normalizeOptionalString(value);
return normalized ? [normalized] : [];
});
for (const currentLabel of currentLabels) if (currentLabel === label) return true;
const configuredLabel = normalizeOptionalString(env.OPENCLAW_LAUNCHD_LABEL);
if (!configuredLabel || configuredLabel !== label) return false;
if (normalizeOptionalString(env.OPENCLAW_SERVICE_MARKER) === "openclaw" && Boolean(normalizeOptionalString(env.OPENCLAW_SERVICE_KIND))) return true;
return options.allowConfiguredLabelFallback !== false && currentLabels.length === 0;
}
//#endregion
//#region src/daemon/launchd-install.ts
/** Transactional LaunchAgent installation, staging, rollback, and removal. */
async function uninstallLaunchAgent({ env, stdout }) {
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.`);
}
function currentGatewayLaunchAgentLabel(targetEnv) {
const configuredCurrentLabel = process.env.OPENCLAW_LAUNCHD_LABEL?.trim();
return [.../* @__PURE__ */ new Set([resolveLaunchAgentLabel(targetEnv), ...configuredCurrentLabel ? [assertValidLaunchAgentLabel(configuredCurrentLabel)] : []])].find((label) => isCurrentProcessLaunchdServiceLabel(label, process.env, { allowConfiguredLabelFallback: false }));
}
function assertExternalLaunchAgentMutation(env, action) {
const currentLabel = 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({ stdout, ...args }) {
const { plistPath, stdoutPath } = await writeLaunchAgentPlist({
...args,
stdout
});
writeFormattedLines(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 restoreLaunchAgentOwnedFile(params) {
if (params.contents === null) {
await fs.unlink(params.path).catch((error) => {
if (error.code !== "ENOENT") throw error;
});
return;
}
const temporaryPath = `${params.path}.openclaw-${randomUUID()}.rollback`;
try {
await fs.writeFile(temporaryPath, params.contents.toString("utf8"), {
flag: "wx",
mode: params.mode
});
await fs.rename(temporaryPath, params.path);
await fs.chmod(params.path, params.mode).catch(() => void 0);
} finally {
await fs.unlink(temporaryPath).catch(() => void 0);
}
}
async function restoreLaunchAgentInstallArtifacts(params) {
await restoreLaunchAgentOwnedFile({
path: resolveLaunchAgentEnvFilePath(params.env, params.label),
contents: params.snapshot.envFileContents,
mode: 384
});
await restoreLaunchAgentOwnedFile({
path: resolveLaunchAgentEnvWrapperPath(params.env, params.label),
contents: params.snapshot.wrapperContents,
mode: 448
});
for (const legacy of params.snapshot.legacy) await restoreLaunchAgentOwnedFile({
path: legacy.plistPath,
contents: legacy.contents,
mode: 420
});
if (params.snapshot.plistContents === null) {
await fs.unlink(params.plistPath).catch((error) => {
if (error.code !== "ENOENT") throw error;
});
return;
}
await publishLaunchAgentPlist({
label: params.label,
plistPath: params.plistPath,
contents: params.snapshot.plistContents.toString("utf8")
});
}
async function restoreLaunchAgentInstall(params) {
const serviceTarget = `${params.domain}/${params.label}`;
const currentState = await probeLaunchAgentState(serviceTarget);
if (currentState.state === "unknown") throw new Error(`launchctl print could not determine whether ${serviceTarget} is loaded during LaunchAgent rollback: ${currentState.detail ?? "unknown error"}`);
if (currentState.state !== "not-loaded") {
const bootout = await execLaunchctl(["bootout", serviceTarget]);
if (bootout.code !== 0 && !isLaunchctlNotLoaded(bootout)) throw new Error(`launchctl bootout failed: ${formatLaunchctlResultDetail(bootout)}`);
}
await restoreLaunchAgentInstallArtifacts({
env: params.env,
label: params.label,
plistPath: params.plistPath,
snapshot: params.snapshot
});
if (params.snapshot.loaded && params.snapshot.plistContents !== null) await bootstrapLaunchAgentOrThrow({
domain: params.domain,
serviceTarget,
plistPath: params.plistPath,
actionHint: "openclaw gateway start",
retryPendingTeardown: true
});
for (const legacy of params.snapshot.legacy) {
if (!legacy.loaded || legacy.contents === null) continue;
await bootstrapLaunchAgentOrThrow({
domain: params.domain,
serviceTarget: `${params.domain}/${legacy.label}`,
plistPath: legacy.plistPath,
actionHint: "openclaw gateway start",
retryPendingTeardown: true
});
}
}
async function deactivateLaunchAgentDefinition(domain, plistPath) {
for (const args of [[
"bootout",
domain,
plistPath
], ["unload", plistPath]]) {
const result = await execLaunchctl(args);
if (result.code !== 0 && !isLaunchctlNotLoaded(result)) throw new Error(`launchctl ${args[0]} failed during LaunchAgent install: ${formatLaunchctlResultDetail(result)}`);
}
}
async function activateLaunchAgent(params) {
const domain = resolveLaunchAgentGuiDomain();
const label = resolveLaunchAgentLabel(params.env);
try {
await assertNoSystemLaunchDaemonOwnership(label);
for (const legacy of params.snapshot.legacy) if (legacy.loaded) await deactivateLaunchAgentDefinition(domain, legacy.plistPath);
if (params.snapshot.loaded) await deactivateLaunchAgentDefinition(domain, params.plistPath);
await bootstrapLaunchAgentOrThrow({
domain,
serviceTarget: `${domain}/${label}`,
plistPath: params.plistPath,
actionHint: "openclaw gateway install --force",
retryPendingTeardown: true
});
for (const legacy of params.snapshot.legacy) await fs.unlink(legacy.plistPath).catch((error) => {
if (error.code !== "ENOENT") throw error;
});
} catch (error) {
try {
await restoreLaunchAgentInstall({
domain,
env: params.env,
label,
plistPath: params.plistPath,
snapshot: params.snapshot
});
} catch (rollbackError) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`${detail}\nThe previous LaunchAgent supervision could not be restored.`, { cause: rollbackError });
}
throw error;
}
}
async function installLaunchAgent(args) {
assertExternalLaunchAgentMutation(args.env, "install");
const targetPlistPath = resolveLaunchAgentPlistPath(args.env);
const previousContents = await readExistingLaunchAgentPlist(targetPlistPath);
const label = resolveLaunchAgentLabel(args.env);
const domain = resolveLaunchAgentGuiDomain();
const legacy = await Promise.all(resolveLegacyGatewayLaunchAgentLabels(args.env.OPENCLAW_PROFILE).map(async (legacyLabel) => {
const plistPath = resolveLaunchAgentPlistPathForLabel(args.env, legacyLabel);
const contents = await readExistingLaunchAgentPlist(plistPath);
return {
label: legacyLabel,
plistPath,
contents,
loaded: await snapshotLaunchAgentLoadedState(contents, `${domain}/${legacyLabel}`)
};
}));
const snapshot = {
plistContents: previousContents,
envFileContents: await readExistingLaunchAgentPlist(resolveLaunchAgentEnvFilePath(args.env, label)),
wrapperContents: await readExistingLaunchAgentPlist(resolveLaunchAgentEnvWrapperPath(args.env, label)),
legacy,
loaded: await snapshotLaunchAgentLoadedState(previousContents, `${domain}/${label}`)
};
let plistPath;
let stdoutPath;
try {
({plistPath, stdoutPath} = await writeLaunchAgentPlist(args));
} catch (error) {
try {
await restoreLaunchAgentInstallArtifacts({
env: args.env,
label,
plistPath: targetPlistPath,
snapshot
});
} catch (rollbackError) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`${detail}\nThe previous LaunchAgent files could not be restored.`, { cause: rollbackError });
}
throw error;
}
await activateLaunchAgent({
env: args.env,
plistPath,
snapshot
});
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], {
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 = parseLaunchctlPrint(result.stdout || result.stderr || "").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
});
await execLaunchctl(["enable", serviceTarget]);
const boot = await execLaunchctl([
"bootstrap",
domain,
plistPath
]);
let repairStatus = "repaired";
if (boot.code !== 0) {
const detail = (boot.stderr || boot.stdout).trim();
if (isUnsupportedGuiDomain(detail)) return {
ok: false,
status: "gui-session-unavailable",
detail,
domain
};
if (!isLaunchctlAlreadyLoaded(boot)) return {
ok: false,
status: "bootstrap-failed",
detail: detail || void 0
};
repairStatus = "already-loaded";
}
if (repairStatus === "repaired") return {
ok: true,
status: repairStatus
};
if ((await readLaunchAgentRuntime(env)).status === "running") return {
ok: true,
status: repairStatus
};
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: repairStatus
};
}
function writeLaunchAgentActionLine(stdout, label, value) {
try {
stdout.write(`${formatLine(label, value)}\n`);
} catch (err) {
if (err?.code !== "EPIPE") throw err;
}
}
async function ensureLaunchAgentLoadedAfterFailure(params) {
if ((await execLaunchctl(["print", params.serviceTarget])).code === 0) return { loaded: true };
try {
await bootstrapLaunchAgentOrThrow({
domain: params.domain,
serviceTarget: params.serviceTarget,
plistPath: params.plistPath,
actionHint: "openclaw gateway start",
onMutation: params.onMutation
});
return { loaded: true };
} catch (error) {
return {
loaded: false,
detail: error instanceof Error ? error.message : String(error)
};
}
}
function formatLaunchAgentLeftUnloadedError(params) {
return [
params.failure,
`LaunchAgent ${params.serviceTarget} is not loaded and could not be restored: ${params.restoreDetail}`,
"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");
}
async function startLaunchAgent({ stdout, env, onMutation }) {
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 enabled = (await execLaunchctl(["enable", serviceTarget])).code === 0;
if (enabled) reportMutation("enable");
let start = await execLaunchctl(["kickstart", serviceTarget]);
if (isLaunchctlNotLoaded(start)) {
await bootstrapLaunchAgentOrThrow({
domain,
serviceTarget,
plistPath,
actionHint: "openclaw gateway start",
onMutation: reportMutation,
skipEnable: enabled
});
start = await execLaunchctl(["kickstart", serviceTarget]);
}
if (start.code !== 0) throw new Error(`launchctl kickstart failed: ${start.stderr || start.stdout}`.trim());
reportMutation("kickstart");
writeLaunchAgentActionLine(stdout, "Started LaunchAgent", serviceTarget);
}
async function restartLaunchAgent({ preserveDefinition, stdout, env, warn, onMutation }) {
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 = isCurrentProcessLaunchdServiceLabel(label);
if (!detached) {
const { port: cleanupPort, probeHosts } = await resolveLaunchAgentGatewayContext(serviceEnv);
if (cleanupPort !== null) {
cleanStaleGatewayProcessesSync(cleanupPort, { resolveProtectedPid: () => readLaunchAgentPidForCleanupSync(serviceTarget) });
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) {
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" };
}
if ((await execLaunchctl(["enable", serviceTarget])).code === 0) reportMutation("enable");
if (plistReloadNeeded) {
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");
try {
await bootstrapLaunchAgentOrThrow({
domain,
serviceTarget,
plistPath,
actionHint: "openclaw gateway restart",
onMutation: reportMutation,
retryPendingTeardown: true
});
} catch (error) {
const restored = await ensureLaunchAgentLoadedAfterFailure({
domain,
serviceTarget,
plistPath,
onMutation: reportMutation
});
if (restored.loaded) throw error;
throw new Error(formatLaunchAgentLeftUnloadedError({
domain,
serviceTarget,
plistPath,
failure: error instanceof Error ? error.message : String(error),
restoreDetail: restored.detail
}), { cause: error });
}
writeLaunchAgentActionLine(stdout, "Restarted LaunchAgent", serviceTarget);
return { outcome: "completed" };
}
const start = await execLaunchctl([
"kickstart",
"-k",
serviceTarget
]);
if (start.code === 0) {
reportMutation("kickstart");
writeLaunchAgentActionLine(stdout, "Restarted LaunchAgent", serviceTarget);
return { outcome: "completed" };
}
if (!isLaunchctlNotLoaded(start)) {
const restored = await ensureLaunchAgentLoadedAfterFailure({
domain,
serviceTarget,
plistPath,
onMutation: reportMutation
});
const failure = `launchctl kickstart failed: ${start.stderr || start.stdout}`.trim();
if (restored.loaded) throw new Error(failure);
throw new Error(formatLaunchAgentLeftUnloadedError({
domain,
serviceTarget,
plistPath,
failure,
restoreDetail: restored.detail
}));
}
await bootstrapLaunchAgentOrThrow({
domain,
serviceTarget,
plistPath,
actionHint: "openclaw gateway restart",
onMutation: reportMutation
});
if (preserveDefinition) {
const kick = await execLaunchctl(["kickstart", serviceTarget]);
if (kick.code !== 0) throw new Error(`launchctl kickstart failed: ${kick.stderr || kick.stdout}`.trim());
reportMutation("kickstart");
}
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;
async function bootoutLaunchAgentOrThrow(params) {
const bootout = await execLaunchctl(["bootout", params.serviceTarget]);
if (bootout.code !== 0 && !isLaunchctlNotLoaded(bootout)) throw new Error(`${params.warning}; launchctl bootout failed: ${formatLaunchctlResultDetail(bootout)}`);
params.onMutation?.();
params.stdout.write(`${formatLine("Warning", params.warning)}\n`);
}
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) {
const { port, probeHosts } = await resolveLaunchAgentGatewayContext(env);
if (port === null) return;
cleanStaleGatewayProcessesSync(port);
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 }) {
const serviceEnv = env ?? process.env;
const domain = resolveLaunchAgentGuiDomain();
const label = resolveLaunchAgentLabel(serviceEnv);
const serviceTarget = `${domain}/${label}`;
const reportMutation = createGatewayLifecycleMutationReporter(onMutation);
if (isCurrentProcessLaunchdServiceLabel(label, process.env, { allowConfiguredLabelFallback: false })) throw new Error(`Refusing to stop LaunchAgent ${label} from inside the same launchd service; run this command from an external shell.`);
if (!persistDisable) {
const bootout = await execLaunchctl(["bootout", serviceTarget]);
if (bootout.code !== 0 && !isLaunchctlNotLoaded(bootout)) throw new Error(`launchctl bootout failed: ${formatLaunchctlResultDetail(bootout)}`);
reportMutation("bootout");
await assertGatewayPortReleasedAfterStop(serviceEnv);
stdout.write(`${formatLine("Stopped LaunchAgent", serviceTarget)}\n`);
return;
}
const disableResult = await execLaunchctl(["disable", serviceTarget]);
if (disableResult.code !== 0) {
await bootoutLaunchAgentOrThrow({
serviceTarget,
stdout,
warning: `launchctl disable failed; used bootout fallback and left service unloaded: ${formatLaunchctlResultDetail(disableResult)}`,
onMutation: () => reportMutation("disable-bootout")
});
await assertGatewayPortReleasedAfterStop(serviceEnv);
stdout.write(`${formatLine("Stopped LaunchAgent (degraded)", serviceTarget)}\n`);
return;
}
reportMutation("disable");
const stop = await execLaunchctl(["stop", label]);
if (stop.code !== 0 && !isLaunchctlNotLoaded(stop)) {
await bootoutLaunchAgentOrThrow({
serviceTarget,
stdout,
warning: `launchctl stop failed; used bootout fallback and left service unloaded: ${formatLaunchctlResultDetail(stop)}`,
onMutation: () => reportMutation("disable-bootout")
});
await assertGatewayPortReleasedAfterStop(serviceEnv);
stdout.write(`${formatLine("Stopped LaunchAgent (degraded)", serviceTarget)}\n`);
return;
}
reportMutation("disable-stop");
const stopState = await waitForLaunchAgentStopped(serviceTarget);
if (stopState.state !== "stopped" && stopState.state !== "not-loaded") {
await bootoutLaunchAgentOrThrow({
serviceTarget,
stdout,
warning: stopState.state === "unknown" ? `launchctl print could not confirm stop; used bootout fallback and left service unloaded: ${stopState.detail ?? "unknown error"}` : "launchctl stop did not fully stop the service; used bootout fallback and left service unloaded",
onMutation: () => reportMutation("disable-bootout")
});
await assertGatewayPortReleasedAfterStop(serviceEnv);
stdout.write(`${formatLine("Stopped LaunchAgent (degraded)", serviceTarget)}\n`);
return;
}
await assertGatewayPortReleasedAfterStop(serviceEnv);
stdout.write(`${formatLine("Stopped LaunchAgent", serviceTarget)}\n`);
}
async function parkCurrentLaunchAgentForMaintenance(params = {}) {
const serviceEnv = params.env ?? process.env;
const domain = resolveLaunchAgentGuiDomain();
const label = resolveLaunchAgentLabel(serviceEnv);
if (!isCurrentProcessLaunchdServiceLabel(label, process.env, { allowConfiguredLabelFallback: false })) 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"
]);
const OPENCLAW_SCRIPT_NAMES = /* @__PURE__ */ new Set(["openclaw.mjs"]);
function normalizeOpenClawUpdateLaunchdLabel(label) {
if (typeof label !== "string") return null;
const trimmed = label.trim();
if (trimmed.startsWith(OPENCLAW_UPDATE_LAUNCHD_LABEL_PREFIX)) return trimmed;
return MANUAL_UPDATE_LAUNCHD_LABEL_PATTERN.test(trimmed) ? trimmed : null;
}
function normalizeOpenClawUpdateLaunchdLabelCandidate(label) {
const normalized = normalizeOpenClawUpdateLaunchdLabel(label);
if (normalized) return {
label: normalized,
requiresMetadata: false
};
if (typeof label !== "string") return null;
const trimmed = label.trim();
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) {
if (isCurrentGatewayLaunchdLabel(candidate.label, env)) continue;
return candidate;
}
}
return null;
}
function parseLaunchctlListOpenClawUpdateJobs(output) {
return parseLaunchctlListOpenClawUpdateJobCandidates(output).filter((job) => !job.requiresMetadata).map(({ requiresMetadata: _requiresMetadata, ...job }) => job);
}
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) && OPENCLAW_SCRIPT_NAMES.has(entryName);
}
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 job of parseLaunchctlListOpenClawUpdateJobCandidates(result.stdout)) {
if (isCurrentGatewayLaunchdLabel(job.label, env)) continue;
if (job.requiresMetadata && !await isLaunchdJobConfirmedOpenClawUpdater({
label: job.label,
env
})) continue;
jobs.push({
label: job.label,
...job.pid !== void 0 ? { pid: job.pid } : {},
...job.lastExitStatus !== void 0 ? { lastExitStatus: job.lastExitStatus } : {}
});
}
return jobs;
}
async function disableOpenClawUpdateLaunchdJobCandidate(params) {
if (process.platform !== "darwin") return false;
if (params.candidate.requiresMetadata && !(params.trustCurrentEnvMarker && hasOpenClawUpdateLaunchdMarker(params.env) || await isLaunchdJobConfirmedOpenClawUpdater({
label: params.candidate.label,
env: params.env
}))) return false;
const serviceTarget = `${resolveLaunchAgentGuiDomain()}/${assertValidLaunchAgentLabel(params.candidate.label)}`;
return (await execLaunchctl(["disable", serviceTarget])).code === 0;
}
async function disableOpenClawUpdateLaunchdJob(label, env = process.env) {
const candidate = normalizeOpenClawUpdateLaunchdLabelCandidate(label);
if (!candidate) return false;
return await disableOpenClawUpdateLaunchdJobCandidate({
candidate,
env,
trustCurrentEnvMarker: false
});
}
async function disableCurrentOpenClawUpdateLaunchdJob(env = process.env) {
const candidate = resolveCurrentOpenClawUpdateLaunchdJobLabel(env);
if (!candidate) return false;
return await disableOpenClawUpdateLaunchdJobCandidate({
candidate,
env,
trustCurrentEnvMarker: isCurrentProcessLaunchdServiceLabel(candidate.label, env, { allowConfiguredLabelFallback: false })
});
}
//#endregion
export { parkCurrentLaunchAgentForMaintenance as a, restartLaunchAgent as c, stageLaunchAgent as d, uninstallLaunchAgent as f, parseLaunchctlListOpenClawUpdateJobs as i, startLaunchAgent as l, disableOpenClawUpdateLaunchdJob as n, stopLaunchAgent as o, findStaleOpenClawUpdateLaunchdJobs as r, repairLaunchAgentBootstrap as s, disableCurrentOpenClawUpdateLaunchdJob as t, installLaunchAgent as u };