mirror of https://github.com/openclaw/openclaw.git
refactor: share daemon launchd and path helpers
This commit is contained in:
parent
2d39c50ee6
commit
49cbcea429
|
|
@ -7,6 +7,7 @@ import {
|
|||
resolveGatewaySystemdServiceName,
|
||||
resolveGatewayWindowsTaskName,
|
||||
} from "./constants.js";
|
||||
import { resolveHomeDir } from "./paths.js";
|
||||
import { execSchtasks } from "./schtasks-exec.js";
|
||||
|
||||
export type ExtraGatewayService = {
|
||||
|
|
@ -49,14 +50,6 @@ export function renderGatewayServiceCleanupHints(
|
|||
}
|
||||
}
|
||||
|
||||
function resolveHomeDir(env: Record<string, string | undefined>): string {
|
||||
const home = env.HOME?.trim() || env.USERPROFILE?.trim();
|
||||
if (!home) {
|
||||
throw new Error("Missing HOME");
|
||||
}
|
||||
return home;
|
||||
}
|
||||
|
||||
type Marker = (typeof EXTRA_MARKERS)[number];
|
||||
|
||||
function detectMarker(content: string): Marker | null {
|
||||
|
|
|
|||
|
|
@ -120,6 +120,58 @@ function resolveGuiDomain(): string {
|
|||
return `gui/${process.getuid()}`;
|
||||
}
|
||||
|
||||
function throwBootstrapGuiSessionError(params: {
|
||||
detail: string;
|
||||
domain: string;
|
||||
actionHint: string;
|
||||
}) {
|
||||
throw new Error(
|
||||
[
|
||||
`launchctl bootstrap failed: ${params.detail}`,
|
||||
`LaunchAgent ${params.actionHint} requires a logged-in macOS GUI session for this user (${params.domain}).`,
|
||||
"This usually means you are running from SSH/headless context or as the wrong user (including sudo).",
|
||||
`Fix: sign in to the macOS desktop as the target user and rerun \`${params.actionHint}\`.`,
|
||||
"Headless deployments should use a dedicated logged-in user session or a custom LaunchDaemon (not shipped): https://docs.openclaw.ai/gateway",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
function writeLaunchAgentActionLine(
|
||||
stdout: NodeJS.WritableStream,
|
||||
label: string,
|
||||
value: string,
|
||||
): void {
|
||||
try {
|
||||
stdout.write(`${formatLine(label, value)}\n`);
|
||||
} catch (err: unknown) {
|
||||
if ((err as NodeJS.ErrnoException)?.code !== "EPIPE") {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function bootstrapLaunchAgentOrThrow(params: {
|
||||
domain: string;
|
||||
serviceTarget: string;
|
||||
plistPath: string;
|
||||
actionHint: string;
|
||||
}) {
|
||||
await execLaunchctl(["enable", params.serviceTarget]);
|
||||
const boot = await execLaunchctl(["bootstrap", params.domain, params.plistPath]);
|
||||
if (boot.code === 0) {
|
||||
return;
|
||||
}
|
||||
const detail = (boot.stderr || boot.stdout).trim();
|
||||
if (isUnsupportedGuiDomain(detail)) {
|
||||
throwBootstrapGuiSessionError({
|
||||
detail,
|
||||
domain: params.domain,
|
||||
actionHint: params.actionHint,
|
||||
});
|
||||
}
|
||||
throw new Error(`launchctl bootstrap failed: ${detail}`);
|
||||
}
|
||||
|
||||
async function ensureSecureDirectory(targetPath: string): Promise<void> {
|
||||
await fs.mkdir(targetPath, { recursive: true, mode: LAUNCH_AGENT_DIR_MODE });
|
||||
try {
|
||||
|
|
@ -414,23 +466,12 @@ export async function installLaunchAgent({
|
|||
await execLaunchctl(["bootout", domain, plistPath]);
|
||||
await execLaunchctl(["unload", plistPath]);
|
||||
// launchd can persist "disabled" state even after bootout + plist removal; clear it before bootstrap.
|
||||
await execLaunchctl(["enable", `${domain}/${label}`]);
|
||||
const boot = await execLaunchctl(["bootstrap", domain, plistPath]);
|
||||
if (boot.code !== 0) {
|
||||
const detail = (boot.stderr || boot.stdout).trim();
|
||||
if (isUnsupportedGuiDomain(detail)) {
|
||||
throw new Error(
|
||||
[
|
||||
`launchctl bootstrap failed: ${detail}`,
|
||||
`LaunchAgent install requires a logged-in macOS GUI session for this user (${domain}).`,
|
||||
"This usually means you are running from SSH/headless context or as the wrong user (including sudo).",
|
||||
"Fix: sign in to the macOS desktop as the target user and rerun `openclaw gateway install --force`.",
|
||||
"Headless deployments should use a dedicated logged-in user session or a custom LaunchDaemon (not shipped): https://docs.openclaw.ai/gateway",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
throw new Error(`launchctl bootstrap failed: ${detail}`);
|
||||
}
|
||||
await bootstrapLaunchAgentOrThrow({
|
||||
domain,
|
||||
serviceTarget: `${domain}/${label}`,
|
||||
plistPath,
|
||||
actionHint: "openclaw gateway install --force",
|
||||
});
|
||||
// `bootstrap` already loads RunAtLoad agents. Avoid `kickstart -k` here:
|
||||
// on slow macOS guests it SIGTERMs the freshly booted gateway and pushes the
|
||||
// real listener startup past onboarding's health deadline.
|
||||
|
|
@ -469,25 +510,13 @@ export async function restartLaunchAgent({
|
|||
if (!handoff.ok) {
|
||||
throw new Error(`launchd restart handoff failed: ${handoff.detail ?? "unknown error"}`);
|
||||
}
|
||||
try {
|
||||
stdout.write(`${formatLine("Scheduled LaunchAgent restart", serviceTarget)}\n`);
|
||||
} catch (err: unknown) {
|
||||
if ((err as NodeJS.ErrnoException)?.code !== "EPIPE") {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
writeLaunchAgentActionLine(stdout, "Scheduled LaunchAgent restart", serviceTarget);
|
||||
return { outcome: "scheduled" };
|
||||
}
|
||||
|
||||
const start = await execLaunchctl(["kickstart", "-k", serviceTarget]);
|
||||
if (start.code === 0) {
|
||||
try {
|
||||
stdout.write(`${formatLine("Restarted LaunchAgent", serviceTarget)}\n`);
|
||||
} catch (err: unknown) {
|
||||
if ((err as NodeJS.ErrnoException)?.code !== "EPIPE") {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
writeLaunchAgentActionLine(stdout, "Restarted LaunchAgent", serviceTarget);
|
||||
return { outcome: "completed" };
|
||||
}
|
||||
|
||||
|
|
@ -496,34 +525,17 @@ export async function restartLaunchAgent({
|
|||
}
|
||||
|
||||
// If the service was previously booted out, re-register the plist and retry.
|
||||
await execLaunchctl(["enable", serviceTarget]);
|
||||
const boot = await execLaunchctl(["bootstrap", domain, plistPath]);
|
||||
if (boot.code !== 0) {
|
||||
const detail = (boot.stderr || boot.stdout).trim();
|
||||
if (isUnsupportedGuiDomain(detail)) {
|
||||
throw new Error(
|
||||
[
|
||||
`launchctl bootstrap failed: ${detail}`,
|
||||
`LaunchAgent restart requires a logged-in macOS GUI session for this user (${domain}).`,
|
||||
"This usually means you are running from SSH/headless context or as the wrong user (including sudo).",
|
||||
"Fix: sign in to the macOS desktop as the target user and rerun `openclaw gateway restart`.",
|
||||
"Headless deployments should use a dedicated logged-in user session or a custom LaunchDaemon (not shipped): https://docs.openclaw.ai/gateway",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
throw new Error(`launchctl bootstrap failed: ${detail}`);
|
||||
}
|
||||
await bootstrapLaunchAgentOrThrow({
|
||||
domain,
|
||||
serviceTarget,
|
||||
plistPath,
|
||||
actionHint: "openclaw gateway restart",
|
||||
});
|
||||
|
||||
const retry = await execLaunchctl(["kickstart", "-k", serviceTarget]);
|
||||
if (retry.code !== 0) {
|
||||
throw new Error(`launchctl kickstart failed: ${retry.stderr || retry.stdout}`.trim());
|
||||
}
|
||||
try {
|
||||
stdout.write(`${formatLine("Restarted LaunchAgent", serviceTarget)}\n`);
|
||||
} catch (err: unknown) {
|
||||
if ((err as NodeJS.ErrnoException)?.code !== "EPIPE") {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
writeLaunchAgentActionLine(stdout, "Restarted LaunchAgent", serviceTarget);
|
||||
return { outcome: "completed" };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue