refactor: extract daemon launchd recovery helper

This commit is contained in:
Peter Steinberger
2026-04-05 09:16:16 +01:00
parent 92c498cf7b
commit aa497e9c52
8 changed files with 183 additions and 66 deletions

View File

@@ -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", () => {

View File

@@ -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 = {