openclaw/src/daemon/launchd-restart-handoff.ts
Robin Waslander 841ee24340
fix(daemon): address clanker review findings for kickstart restart
Bug 1 (high): replace fixed sleep 1 with caller-PID polling in both
kickstart and start-after-exit handoff modes. The helper now waits until
kill -0 $caller_pid fails before issuing launchctl kickstart -k.

Bug 2 (medium): gate enable+bootstrap fallback on isLaunchctlNotLoaded().
Only attempt re-registration when kickstart -k fails because the job is
absent; all other kickstart failures now re-throw the original error.

Follows up on 3c0fd3dffe.
Fixes #43311, #43406, #43035, #43049
2026-03-12 02:16:24 +01:00

139 lines
3.8 KiB
TypeScript

import { spawn } from "node:child_process";
import os from "node:os";
import path from "node:path";
import { resolveGatewayLaunchAgentLabel } from "./constants.js";
export type LaunchdRestartHandoffMode = "kickstart" | "start-after-exit";
export type LaunchdRestartHandoffResult = {
ok: boolean;
pid?: number;
detail?: string;
};
export type LaunchdRestartTarget = {
domain: string;
label: string;
plistPath: string;
serviceTarget: string;
};
function resolveGuiDomain(): string {
if (typeof process.getuid !== "function") {
return "gui/501";
}
return `gui/${process.getuid()}`;
}
function resolveLaunchAgentLabel(env?: Record<string, string | undefined>): string {
const envLabel = env?.OPENCLAW_LAUNCHD_LABEL?.trim();
if (envLabel) {
return envLabel;
}
return resolveGatewayLaunchAgentLabel(env?.OPENCLAW_PROFILE);
}
export function resolveLaunchdRestartTarget(
env: Record<string, string | undefined> = process.env,
): LaunchdRestartTarget {
const domain = resolveGuiDomain();
const label = resolveLaunchAgentLabel(env);
const home = env.HOME?.trim() || os.homedir();
const plistPath = path.join(home, "Library", "LaunchAgents", `${label}.plist`);
return {
domain,
label,
plistPath,
serviceTarget: `${domain}/${label}`,
};
}
export function isCurrentProcessLaunchdServiceLabel(
label: string,
env: NodeJS.ProcessEnv = process.env,
): boolean {
const launchdLabel =
env.LAUNCH_JOB_LABEL?.trim() || env.LAUNCH_JOB_NAME?.trim() || env.XPC_SERVICE_NAME?.trim();
if (launchdLabel) {
return launchdLabel === label;
}
const configuredLabel = env.OPENCLAW_LAUNCHD_LABEL?.trim();
return Boolean(configuredLabel && configuredLabel === label);
}
function buildLaunchdRestartScript(mode: LaunchdRestartHandoffMode): string {
const waitForCallerPid = `wait_pid="$4"
if [ -n "$wait_pid" ] && [ "$wait_pid" -gt 1 ] 2>/dev/null; then
while kill -0 "$wait_pid" >/dev/null 2>&1; do
sleep 0.1
done
fi
`;
if (mode === "kickstart") {
return `service_target="$1"
domain="$2"
plist_path="$3"
${waitForCallerPid}
if ! launchctl kickstart -k "$service_target" >/dev/null 2>&1; then
launchctl enable "$service_target" >/dev/null 2>&1
if launchctl bootstrap "$domain" "$plist_path" >/dev/null 2>&1; then
launchctl kickstart -k "$service_target" >/dev/null 2>&1 || true
fi
fi
`;
}
return `service_target="$1"
domain="$2"
plist_path="$3"
${waitForCallerPid}
if ! launchctl start "$service_target" >/dev/null 2>&1; then
launchctl enable "$service_target" >/dev/null 2>&1
if launchctl bootstrap "$domain" "$plist_path" >/dev/null 2>&1; then
launchctl start "$service_target" >/dev/null 2>&1 || launchctl kickstart -k "$service_target" >/dev/null 2>&1 || true
else
launchctl kickstart -k "$service_target" >/dev/null 2>&1 || true
fi
fi
`;
}
export function scheduleDetachedLaunchdRestartHandoff(params: {
env?: Record<string, string | undefined>;
mode: LaunchdRestartHandoffMode;
waitForPid?: number;
}): LaunchdRestartHandoffResult {
const target = resolveLaunchdRestartTarget(params.env);
const waitForPid =
typeof params.waitForPid === "number" && Number.isFinite(params.waitForPid)
? Math.floor(params.waitForPid)
: 0;
try {
const child = spawn(
"/bin/sh",
[
"-c",
buildLaunchdRestartScript(params.mode),
"openclaw-launchd-restart-handoff",
target.serviceTarget,
target.domain,
target.plistPath,
String(waitForPid),
],
{
detached: true,
stdio: "ignore",
env: { ...process.env, ...params.env },
},
);
child.unref();
return { ok: true, pid: child.pid ?? undefined };
} catch (err) {
return {
ok: false,
detail: err instanceof Error ? err.message : String(err),
};
}
}