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
139 lines
3.8 KiB
TypeScript
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),
|
|
};
|
|
}
|
|
}
|