mirror of https://github.com/openclaw/openclaw.git
refactor: extract daemon launchd recovery helper
This commit is contained in:
parent
92c498cf7b
commit
aa497e9c52
|
|
@ -0,0 +1,67 @@
|
|||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const launchAgentPlistExists = vi.hoisted(() => vi.fn());
|
||||
const repairLaunchAgentBootstrap = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../daemon/launchd.js", () => ({
|
||||
launchAgentPlistExists: (env: Record<string, string | undefined>) => launchAgentPlistExists(env),
|
||||
repairLaunchAgentBootstrap: (args: { env?: Record<string, string | undefined> }) =>
|
||||
repairLaunchAgentBootstrap(args),
|
||||
}));
|
||||
|
||||
let recoverInstalledLaunchAgent: typeof import("./launchd-recovery.js").recoverInstalledLaunchAgent;
|
||||
let LAUNCH_AGENT_RECOVERY_MESSAGE: typeof import("./launchd-recovery.js").LAUNCH_AGENT_RECOVERY_MESSAGE;
|
||||
|
||||
describe("recoverInstalledLaunchAgent", () => {
|
||||
beforeAll(async () => {
|
||||
({ recoverInstalledLaunchAgent, LAUNCH_AGENT_RECOVERY_MESSAGE } =
|
||||
await import("./launchd-recovery.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
launchAgentPlistExists.mockReset();
|
||||
repairLaunchAgentBootstrap.mockReset();
|
||||
launchAgentPlistExists.mockResolvedValue(false);
|
||||
repairLaunchAgentBootstrap.mockResolvedValue({ ok: true, status: "repaired" });
|
||||
});
|
||||
|
||||
it("returns null outside macOS", async () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
|
||||
|
||||
await expect(recoverInstalledLaunchAgent({ result: "started" })).resolves.toBeNull();
|
||||
expect(launchAgentPlistExists).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns null when the LaunchAgent plist is missing", async () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
|
||||
launchAgentPlistExists.mockResolvedValue(false);
|
||||
|
||||
await expect(recoverInstalledLaunchAgent({ result: "started" })).resolves.toBeNull();
|
||||
expect(repairLaunchAgentBootstrap).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns a loaded recovery result when bootstrap repair succeeds", async () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
|
||||
launchAgentPlistExists.mockResolvedValue(true);
|
||||
|
||||
await expect(recoverInstalledLaunchAgent({ result: "restarted" })).resolves.toEqual({
|
||||
result: "restarted",
|
||||
loaded: true,
|
||||
message: LAUNCH_AGENT_RECOVERY_MESSAGE,
|
||||
});
|
||||
expect(launchAgentPlistExists).toHaveBeenCalledWith(process.env);
|
||||
expect(repairLaunchAgentBootstrap).toHaveBeenCalledWith({ env: process.env });
|
||||
});
|
||||
|
||||
it("returns null when bootstrap repair fails", async () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
|
||||
launchAgentPlistExists.mockResolvedValue(true);
|
||||
repairLaunchAgentBootstrap.mockResolvedValue({
|
||||
ok: false,
|
||||
status: "kickstart-failed",
|
||||
detail: "permission denied",
|
||||
});
|
||||
|
||||
await expect(recoverInstalledLaunchAgent({ result: "started" })).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { launchAgentPlistExists, repairLaunchAgentBootstrap } from "../../daemon/launchd.js";
|
||||
|
||||
const LAUNCH_AGENT_RECOVERY_MESSAGE =
|
||||
"Gateway LaunchAgent was installed but not loaded; re-bootstrapped launchd service.";
|
||||
|
||||
type LaunchAgentRecoveryAction = "started" | "restarted";
|
||||
|
||||
type LaunchAgentRecoveryResult = {
|
||||
result: LaunchAgentRecoveryAction;
|
||||
loaded: true;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export async function recoverInstalledLaunchAgent(params: {
|
||||
result: LaunchAgentRecoveryAction;
|
||||
env?: Record<string, string | undefined>;
|
||||
}): Promise<LaunchAgentRecoveryResult | null> {
|
||||
if (process.platform !== "darwin") {
|
||||
return null;
|
||||
}
|
||||
const env = params.env ?? (process.env as Record<string, string | undefined>);
|
||||
const plistExists = await launchAgentPlistExists(env).catch(() => false);
|
||||
if (!plistExists) {
|
||||
return null;
|
||||
}
|
||||
const repaired = await repairLaunchAgentBootstrap({ env }).catch(() => ({
|
||||
ok: false as const,
|
||||
status: "bootstrap-failed" as const,
|
||||
}));
|
||||
if (!repaired.ok) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
result: params.result,
|
||||
loaded: true,
|
||||
message: LAUNCH_AGENT_RECOVERY_MESSAGE,
|
||||
};
|
||||
}
|
||||
|
||||
export { LAUNCH_AGENT_RECOVERY_MESSAGE };
|
||||
|
|
@ -30,14 +30,14 @@ type RestartPostCheckContext = {
|
|||
fail: (message: string, hints?: string[]) => void;
|
||||
};
|
||||
|
||||
type NotLoadedActionResult = {
|
||||
type ServiceRecoveryResult = {
|
||||
result: "started" | "stopped" | "restarted";
|
||||
message?: string;
|
||||
warnings?: string[];
|
||||
loaded?: boolean;
|
||||
};
|
||||
|
||||
type NotLoadedActionContext = {
|
||||
type ServiceRecoveryContext = {
|
||||
json: boolean;
|
||||
stdout: Writable;
|
||||
fail: (message: string, hints?: string[]) => void;
|
||||
|
|
@ -187,7 +187,7 @@ export async function runServiceStart(params: {
|
|||
service: GatewayService;
|
||||
renderStartHints: () => string[];
|
||||
opts?: DaemonLifecycleOptions;
|
||||
onNotLoaded?: (ctx: NotLoadedActionContext) => Promise<NotLoadedActionResult | null>;
|
||||
onNotLoaded?: (ctx: ServiceRecoveryContext) => Promise<ServiceRecoveryResult | null>;
|
||||
}) {
|
||||
const json = Boolean(params.opts?.json);
|
||||
const { stdout, emit, fail } = createDaemonActionContext({ action: "start", json });
|
||||
|
|
@ -278,7 +278,7 @@ export async function runServiceStop(params: {
|
|||
serviceNoun: string;
|
||||
service: GatewayService;
|
||||
opts?: DaemonLifecycleOptions;
|
||||
onNotLoaded?: (ctx: NotLoadedActionContext) => Promise<NotLoadedActionResult | null>;
|
||||
onNotLoaded?: (ctx: ServiceRecoveryContext) => Promise<ServiceRecoveryResult | null>;
|
||||
}) {
|
||||
const json = Boolean(params.opts?.json);
|
||||
const { stdout, emit, fail } = createDaemonActionContext({ action: "stop", json });
|
||||
|
|
@ -349,12 +349,12 @@ export async function runServiceRestart(params: {
|
|||
opts?: DaemonLifecycleOptions;
|
||||
checkTokenDrift?: boolean;
|
||||
postRestartCheck?: (ctx: RestartPostCheckContext) => Promise<GatewayServiceRestartResult | void>;
|
||||
onNotLoaded?: (ctx: NotLoadedActionContext) => Promise<NotLoadedActionResult | null>;
|
||||
onNotLoaded?: (ctx: ServiceRecoveryContext) => Promise<ServiceRecoveryResult | null>;
|
||||
}): Promise<boolean> {
|
||||
const json = Boolean(params.opts?.json);
|
||||
const { stdout, emit, fail } = createDaemonActionContext({ action: "restart", json });
|
||||
const warnings: string[] = [];
|
||||
let handledNotLoaded: NotLoadedActionResult | null = null;
|
||||
let handledRecovery: ServiceRecoveryResult | null = null;
|
||||
let recoveredLoadedState: boolean | null = null;
|
||||
const emitScheduledRestart = (
|
||||
restartStatus: ReturnType<typeof describeGatewayServiceRestart>,
|
||||
|
|
@ -397,12 +397,12 @@ export async function runServiceRestart(params: {
|
|||
|
||||
if (!loaded) {
|
||||
try {
|
||||
handledNotLoaded = (await params.onNotLoaded?.({ json, stdout, fail })) ?? null;
|
||||
handledRecovery = (await params.onNotLoaded?.({ json, stdout, fail })) ?? null;
|
||||
} catch (err) {
|
||||
fail(`${params.serviceNoun} restart failed: ${String(err)}`);
|
||||
return false;
|
||||
}
|
||||
if (!handledNotLoaded) {
|
||||
if (!handledRecovery) {
|
||||
await handleServiceNotLoaded({
|
||||
serviceNoun: params.serviceNoun,
|
||||
service: params.service,
|
||||
|
|
@ -413,10 +413,10 @@ export async function runServiceRestart(params: {
|
|||
});
|
||||
return false;
|
||||
}
|
||||
if (handledNotLoaded.warnings?.length) {
|
||||
warnings.push(...handledNotLoaded.warnings);
|
||||
if (handledRecovery.warnings?.length) {
|
||||
warnings.push(...handledRecovery.warnings);
|
||||
}
|
||||
recoveredLoadedState = handledNotLoaded.loaded ?? null;
|
||||
recoveredLoadedState = handledRecovery.loaded ?? null;
|
||||
}
|
||||
|
||||
if (loaded && params.checkTokenDrift) {
|
||||
|
|
@ -486,12 +486,12 @@ export async function runServiceRestart(params: {
|
|||
emit({
|
||||
ok: true,
|
||||
result: "restarted",
|
||||
message: handledNotLoaded?.message,
|
||||
message: handledRecovery?.message,
|
||||
service: buildDaemonServiceSnapshot(params.service, restarted),
|
||||
warnings: warnings.length ? warnings : undefined,
|
||||
});
|
||||
if (!json && handledNotLoaded?.message) {
|
||||
defaultRuntime.log(handledNotLoaded.message);
|
||||
if (!json && handledRecovery?.message) {
|
||||
defaultRuntime.log(handledRecovery.message);
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -51,8 +51,7 @@ const probeGateway = vi.fn<
|
|||
>();
|
||||
const isRestartEnabled = vi.fn<(config?: { commands?: unknown }) => boolean>(() => true);
|
||||
const loadConfig = vi.hoisted(() => vi.fn(() => ({})));
|
||||
const launchAgentPlistExists = vi.hoisted(() => vi.fn());
|
||||
const repairLaunchAgentBootstrap = vi.hoisted(() => vi.fn());
|
||||
const recoverInstalledLaunchAgent = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../config/config.js", () => ({
|
||||
loadConfig: () => loadConfig(),
|
||||
|
|
@ -84,10 +83,9 @@ vi.mock("../../daemon/service.js", () => ({
|
|||
resolveGatewayService: () => service,
|
||||
}));
|
||||
|
||||
vi.mock("../../daemon/launchd.js", () => ({
|
||||
launchAgentPlistExists: (env: Record<string, string | undefined>) => launchAgentPlistExists(env),
|
||||
repairLaunchAgentBootstrap: (args: { env?: Record<string, string | undefined> }) =>
|
||||
repairLaunchAgentBootstrap(args),
|
||||
vi.mock("./launchd-recovery.js", () => ({
|
||||
recoverInstalledLaunchAgent: (args: { result: "started" | "restarted" }) =>
|
||||
recoverInstalledLaunchAgent(args),
|
||||
}));
|
||||
|
||||
vi.mock("./restart-health.js", () => ({
|
||||
|
|
@ -160,8 +158,7 @@ describe("runDaemonRestart health checks", () => {
|
|||
probeGateway.mockReset();
|
||||
isRestartEnabled.mockReset();
|
||||
loadConfig.mockReset();
|
||||
launchAgentPlistExists.mockReset();
|
||||
repairLaunchAgentBootstrap.mockReset();
|
||||
recoverInstalledLaunchAgent.mockReset();
|
||||
|
||||
service.readCommand.mockResolvedValue({
|
||||
programArguments: ["openclaw", "gateway", "--port", "18789"],
|
||||
|
|
@ -169,8 +166,7 @@ describe("runDaemonRestart health checks", () => {
|
|||
});
|
||||
service.restart.mockResolvedValue({ outcome: "completed" });
|
||||
runServiceStart.mockResolvedValue(undefined);
|
||||
launchAgentPlistExists.mockResolvedValue(false);
|
||||
repairLaunchAgentBootstrap.mockResolvedValue({ ok: true });
|
||||
recoverInstalledLaunchAgent.mockResolvedValue(null);
|
||||
|
||||
runServiceRestart.mockImplementation(async (params: RestartParams) => {
|
||||
const fail = (message: string, hints?: string[]) => {
|
||||
|
|
@ -213,15 +209,18 @@ describe("runDaemonRestart health checks", () => {
|
|||
|
||||
it("re-bootstraps an installed LaunchAgent when start finds it not loaded", async () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
|
||||
launchAgentPlistExists.mockResolvedValue(true);
|
||||
recoverInstalledLaunchAgent.mockResolvedValue({
|
||||
result: "started",
|
||||
loaded: true,
|
||||
message: "Gateway LaunchAgent was installed but not loaded; re-bootstrapped launchd service.",
|
||||
});
|
||||
runServiceStart.mockImplementation(async (params: { onNotLoaded?: () => Promise<unknown> }) => {
|
||||
await params.onNotLoaded?.();
|
||||
});
|
||||
|
||||
await runDaemonStart({ json: true });
|
||||
|
||||
expect(launchAgentPlistExists).toHaveBeenCalledWith(process.env);
|
||||
expect(repairLaunchAgentBootstrap).toHaveBeenCalledWith({ env: process.env });
|
||||
expect(recoverInstalledLaunchAgent).toHaveBeenCalledWith({ result: "started" });
|
||||
});
|
||||
|
||||
it("kills stale gateway pids and retries restart", async () => {
|
||||
|
|
@ -344,21 +343,24 @@ describe("runDaemonRestart health checks", () => {
|
|||
|
||||
it("prefers unmanaged restart over launchd repair when a gateway listener is present", async () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
|
||||
launchAgentPlistExists.mockResolvedValue(true);
|
||||
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([4200]);
|
||||
mockUnmanagedRestart({ runPostRestartCheck: true });
|
||||
|
||||
await runDaemonRestart({ json: true });
|
||||
|
||||
expect(signalVerifiedGatewayPidSync).toHaveBeenCalledWith(4200, "SIGUSR1");
|
||||
expect(repairLaunchAgentBootstrap).not.toHaveBeenCalled();
|
||||
expect(recoverInstalledLaunchAgent).not.toHaveBeenCalled();
|
||||
expect(waitForGatewayHealthyListener).toHaveBeenCalledTimes(1);
|
||||
expect(waitForGatewayHealthyRestart).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("re-bootstraps an installed LaunchAgent on restart when no unmanaged listener exists", async () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
|
||||
launchAgentPlistExists.mockResolvedValue(true);
|
||||
recoverInstalledLaunchAgent.mockResolvedValue({
|
||||
result: "restarted",
|
||||
loaded: true,
|
||||
message: "Gateway LaunchAgent was installed but not loaded; re-bootstrapped launchd service.",
|
||||
});
|
||||
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([]);
|
||||
runServiceRestart.mockImplementation(
|
||||
async (params: RestartParams & { onNotLoaded?: () => Promise<unknown> }) => {
|
||||
|
|
@ -377,7 +379,7 @@ describe("runDaemonRestart health checks", () => {
|
|||
|
||||
await runDaemonRestart({ json: true });
|
||||
|
||||
expect(repairLaunchAgentBootstrap).toHaveBeenCalledWith({ env: process.env });
|
||||
expect(recoverInstalledLaunchAgent).toHaveBeenCalledWith({ result: "restarted" });
|
||||
expect(signalVerifiedGatewayPidSync).not.toHaveBeenCalled();
|
||||
expect(waitForGatewayHealthyListener).not.toHaveBeenCalled();
|
||||
expect(waitForGatewayHealthyRestart).toHaveBeenCalledTimes(1);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { isRestartEnabled } from "../../config/commands.js";
|
||||
import { readBestEffortConfig, resolveGatewayPort } from "../../config/config.js";
|
||||
import { launchAgentPlistExists, repairLaunchAgentBootstrap } from "../../daemon/launchd.js";
|
||||
import { resolveGatewayService } from "../../daemon/service.js";
|
||||
import { probeGateway } from "../../gateway/probe.js";
|
||||
import {
|
||||
|
|
@ -11,6 +10,7 @@ import {
|
|||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
import { formatCliCommand } from "../command-format.js";
|
||||
import { recoverInstalledLaunchAgent } from "./launchd-recovery.js";
|
||||
import {
|
||||
runServiceRestart,
|
||||
runServiceStart,
|
||||
|
|
@ -131,28 +131,6 @@ async function restartGatewayWithoutServiceManager(port: number) {
|
|||
};
|
||||
}
|
||||
|
||||
async function repairLaunchAgentIfInstalled(params: { result: "started" | "restarted" }) {
|
||||
if (process.platform !== "darwin") {
|
||||
return null;
|
||||
}
|
||||
const serviceEnv = process.env as Record<string, string | undefined>;
|
||||
const plistExists = await launchAgentPlistExists(serviceEnv).catch(() => false);
|
||||
if (!plistExists) {
|
||||
return null;
|
||||
}
|
||||
const repaired = await repairLaunchAgentBootstrap({ env: serviceEnv }).catch(() => ({
|
||||
ok: false,
|
||||
}));
|
||||
if (!repaired.ok) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
result: params.result,
|
||||
loaded: true,
|
||||
message: "Gateway LaunchAgent was installed but not loaded; re-bootstrapped launchd service.",
|
||||
} as const;
|
||||
}
|
||||
|
||||
export async function runDaemonUninstall(opts: DaemonLifecycleOptions = {}) {
|
||||
return await runServiceUninstall({
|
||||
serviceNoun: "Gateway",
|
||||
|
|
@ -170,7 +148,7 @@ export async function runDaemonStart(opts: DaemonLifecycleOptions = {}) {
|
|||
renderStartHints: renderGatewayServiceStartHints,
|
||||
onNotLoaded:
|
||||
process.platform === "darwin"
|
||||
? async () => await repairLaunchAgentIfInstalled({ result: "started" })
|
||||
? async () => await recoverInstalledLaunchAgent({ result: "started" })
|
||||
: undefined,
|
||||
opts,
|
||||
});
|
||||
|
|
@ -216,7 +194,7 @@ export async function runDaemonRestart(opts: DaemonLifecycleOptions = {}): Promi
|
|||
restartedWithoutServiceManager = true;
|
||||
return handled;
|
||||
}
|
||||
return await repairLaunchAgentIfInstalled({ result: "restarted" });
|
||||
return await recoverInstalledLaunchAgent({ result: "restarted" });
|
||||
},
|
||||
postRestartCheck: async ({ warnings, fail, stdout }) => {
|
||||
if (restartedWithoutServiceManager) {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ vi.mock("../daemon/launchd.js", async () => {
|
|||
isLaunchAgentListed: vi.fn(async () => false),
|
||||
isLaunchAgentLoaded: vi.fn(async () => false),
|
||||
launchAgentPlistExists: vi.fn(async () => false),
|
||||
repairLaunchAgentBootstrap: vi.fn(async () => ({ ok: true })),
|
||||
repairLaunchAgentBootstrap: vi.fn(async () => ({ ok: true, status: "repaired" })),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ describe("launchd bootstrap repair", () => {
|
|||
OPENCLAW_PROFILE: "default",
|
||||
};
|
||||
const repair = await repairLaunchAgentBootstrap({ env });
|
||||
expect(repair.ok).toBe(true);
|
||||
expect(repair).toEqual({ ok: true, status: "repaired" });
|
||||
|
||||
const { serviceId, bootstrapIndex } = expectLaunchctlEnableBootstrapOrder(env);
|
||||
const kickstartIndex = state.launchctlCalls.findIndex(
|
||||
|
|
@ -268,7 +268,7 @@ describe("launchd bootstrap repair", () => {
|
|||
|
||||
const repair = await repairLaunchAgentBootstrap({ env });
|
||||
|
||||
expect(repair.ok).toBe(true);
|
||||
expect(repair).toEqual({ ok: true, status: "already-loaded" });
|
||||
expect(state.launchctlCalls.filter((call) => call[0] === "kickstart")).toHaveLength(1);
|
||||
});
|
||||
|
||||
|
|
@ -282,7 +282,7 @@ describe("launchd bootstrap repair", () => {
|
|||
|
||||
const repair = await repairLaunchAgentBootstrap({ env });
|
||||
|
||||
expect(repair.ok).toBe(true);
|
||||
expect(repair).toEqual({ ok: true, status: "already-loaded" });
|
||||
expect(state.launchctlCalls.filter((call) => call[0] === "kickstart")).toHaveLength(1);
|
||||
});
|
||||
|
||||
|
|
@ -295,10 +295,30 @@ describe("launchd bootstrap repair", () => {
|
|||
|
||||
const repair = await repairLaunchAgentBootstrap({ env });
|
||||
|
||||
expect(repair.ok).toBe(false);
|
||||
expect(repair.detail).toContain("Could not find specified service");
|
||||
expect(repair).toMatchObject({
|
||||
ok: false,
|
||||
status: "bootstrap-failed",
|
||||
detail: expect.stringContaining("Could not find specified service"),
|
||||
});
|
||||
expect(state.launchctlCalls.some((call) => call[0] === "kickstart")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns a typed kickstart failure", async () => {
|
||||
state.kickstartError = "launchctl kickstart failed: permission denied";
|
||||
state.kickstartFailuresRemaining = 1;
|
||||
const env: Record<string, string | undefined> = {
|
||||
HOME: "/Users/test",
|
||||
OPENCLAW_PROFILE: "default",
|
||||
};
|
||||
|
||||
const repair = await repairLaunchAgentBootstrap({ env });
|
||||
|
||||
expect(repair).toEqual({
|
||||
ok: false,
|
||||
status: "kickstart-failed",
|
||||
detail: "launchctl kickstart failed: permission denied",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("launchd install", () => {
|
||||
|
|
|
|||
|
|
@ -313,9 +313,13 @@ export async function readLaunchAgentRuntime(
|
|||
};
|
||||
}
|
||||
|
||||
export type LaunchAgentBootstrapRepairResult =
|
||||
| { ok: true; status: "repaired" | "already-loaded" }
|
||||
| { ok: false; status: "bootstrap-failed" | "kickstart-failed"; detail?: string };
|
||||
|
||||
export async function repairLaunchAgentBootstrap(args: {
|
||||
env?: Record<string, string | undefined>;
|
||||
}): Promise<{ ok: boolean; detail?: string }> {
|
||||
}): Promise<LaunchAgentBootstrapRepairResult> {
|
||||
const env = args.env ?? (process.env as Record<string, string | undefined>);
|
||||
const domain = resolveGuiDomain();
|
||||
const label = resolveLaunchAgentLabel({ env });
|
||||
|
|
@ -324,19 +328,25 @@ export async function repairLaunchAgentBootstrap(args: {
|
|||
// (matches the same guard in installLaunchAgent and restartLaunchAgent).
|
||||
await execLaunchctl(["enable", `${domain}/${label}`]);
|
||||
const boot = await execLaunchctl(["bootstrap", domain, plistPath]);
|
||||
let repairStatus: LaunchAgentBootstrapRepairResult["status"] = "repaired";
|
||||
if (boot.code !== 0) {
|
||||
const detail = (boot.stderr || boot.stdout).trim();
|
||||
const normalized = detail.toLowerCase();
|
||||
const alreadyLoaded = boot.code === 130 || normalized.includes("already exists in domain");
|
||||
if (!alreadyLoaded) {
|
||||
return { ok: false, detail: detail || undefined };
|
||||
return { ok: false, status: "bootstrap-failed", detail: detail || undefined };
|
||||
}
|
||||
repairStatus = "already-loaded";
|
||||
}
|
||||
const kick = await execLaunchctl(["kickstart", "-k", `${domain}/${label}`]);
|
||||
if (kick.code !== 0) {
|
||||
return { ok: false, detail: (kick.stderr || kick.stdout).trim() || undefined };
|
||||
return {
|
||||
ok: false,
|
||||
status: "kickstart-failed",
|
||||
detail: (kick.stderr || kick.stdout).trim() || undefined,
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
return { ok: true, status: repairStatus };
|
||||
}
|
||||
|
||||
export type LegacyLaunchAgent = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue