Skip to content

Commit ae5e34e

Browse files
committed
fix(gateway): handle missing launchd gui domains
1 parent 328c279 commit ae5e34e

3 files changed

Lines changed: 20 additions & 13 deletions

File tree

src/commands/doctor-gateway-daemon-flow.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,14 @@ type LaunchAgentBootstrapDoctorOutcome =
5959
function noteGatewayRuntime(
6060
serviceRuntime: GatewayServiceRuntime | undefined,
6161
env: Record<string, string | undefined>,
62-
): boolean {
62+
): void {
6363
const summary = formatGatewayRuntimeSummary(serviceRuntime);
6464
const hints = buildGatewayRuntimeHints(serviceRuntime, {
6565
platform: process.platform,
6666
env,
6767
});
6868
if (!summary && hints.length === 0) {
69-
return false;
69+
return;
7070
}
7171

7272
const lines: string[] = [];
@@ -75,7 +75,6 @@ function noteGatewayRuntime(
7575
}
7676
lines.push(...hints);
7777
note(lines.join("\n"), "Gateway");
78-
return true;
7978
}
8079

8180
async function maybeRepairLaunchAgentBootstrap(params: {
@@ -208,6 +207,8 @@ export async function maybeRepairGatewayDaemon(params: {
208207
const serviceRepairPolicy = resolveServiceRepairPolicy();
209208
const serviceRepairExternal = isServiceRepairExternallyManaged(serviceRepairPolicy);
210209
const service = resolveGatewayService();
210+
const isLocalDarwinGateway =
211+
process.platform === "darwin" && params.cfg.gateway?.mode !== "remote";
211212
// systemd can throw in containers/WSL; treat as "not loaded" and fall back to hints.
212213
let loaded;
213214
try {
@@ -225,8 +226,7 @@ export async function maybeRepairGatewayDaemon(params: {
225226
...command.environment,
226227
} satisfies NodeJS.ProcessEnv)
227228
: process.env;
228-
const shouldReadRuntime =
229-
loaded || (process.platform === "darwin" && params.cfg.gateway?.mode !== "remote");
229+
const shouldReadRuntime = loaded || isLocalDarwinGateway;
230230
if (shouldReadRuntime) {
231231
serviceRuntime = await service.readRuntime(serviceEnv).catch(() => undefined);
232232
}
@@ -237,7 +237,7 @@ export async function maybeRepairGatewayDaemon(params: {
237237
}
238238
}
239239

240-
if (process.platform === "darwin" && params.cfg.gateway?.mode !== "remote") {
240+
if (isLocalDarwinGateway) {
241241
const gatewayRepair = serviceRuntime?.missingGuiSession
242242
? ({ status: "gui-session-unavailable", detail: serviceRuntime.detail ?? "" } as const)
243243
: await maybeRepairLaunchAgentBootstrap({
@@ -296,7 +296,7 @@ export async function maybeRepairGatewayDaemon(params: {
296296

297297
if (!loaded) {
298298
if (
299-
process.platform === "darwin" &&
299+
isLocalDarwinGateway &&
300300
(serviceRuntime?.missingGuiSession ||
301301
serviceRuntime?.missingSupervision ||
302302
serviceRuntime?.cachedLabel)

src/daemon/launchd.test.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -428,18 +428,21 @@ describe("launchd runtime state", () => {
428428
expect(runtime.detail).toBe("Could not find service");
429429
});
430430

431-
it("marks installed LaunchAgents unavailable when the GUI domain is missing", async () => {
431+
it.each([
432+
"Bootstrap failed: 125: Domain does not support specified action",
433+
"Could not find domain for user gui: 999999",
434+
])("marks installed LaunchAgents unavailable when launchd reports %s", async (detail) => {
432435
const env = createDefaultLaunchdEnv();
433436
state.files.set(resolveLaunchAgentPlistPath(env), "<plist/>");
434-
state.printError = "Bootstrap failed: 125: Domain does not support specified action";
437+
state.printError = detail;
435438
state.printFailuresRemaining = 1;
436439

437440
const runtime = await readLaunchAgentRuntime(env);
438441

439442
expect(runtime.status).toBe("unknown");
440443
expect(runtime.missingSupervision).toBe(true);
441444
expect(runtime.missingGuiSession).toBe(true);
442-
expect(runtime.detail).toContain("Domain does not support specified action");
445+
expect(runtime.detail).toBe(detail);
443446
});
444447

445448
it("marks a missing unit when launchd has no job and no plist exists", async () => {
@@ -793,16 +796,19 @@ describe("launchd bootstrap repair", () => {
793796
expect(launchctlCommandNames()).not.toContain("kickstart");
794797
});
795798

796-
it("classifies headless GUI bootstrap failures separately from generic not-loaded repair", async () => {
797-
state.bootstrapError = "Bootstrap failed: 125: Domain does not support specified action";
799+
it.each([
800+
"Bootstrap failed: 125: Domain does not support specified action",
801+
"Could not find domain for user gui: 999999",
802+
])("classifies %s separately from generic not-loaded repair", async (detail) => {
803+
state.bootstrapError = detail;
798804
const env = createDefaultLaunchdEnv();
799805

800806
const repair = await repairLaunchAgentBootstrap({ env });
801807

802808
expect(repair).toEqual({
803809
ok: false,
804810
status: "gui-session-unavailable",
805-
detail: "Bootstrap failed: 125: Domain does not support specified action",
811+
detail,
806812
domain: typeof process.getuid === "function" ? `gui/${process.getuid()}` : "gui/501",
807813
});
808814
expect(launchctlCommandNames()).not.toContain("kickstart");

src/daemon/launchd.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,7 @@ function isUnsupportedGuiDomain(detail: string): boolean {
707707
const normalized = normalizeLowercaseStringOrEmpty(detail);
708708
return (
709709
normalized.includes("domain does not support specified action") ||
710+
normalized.includes("could not find domain for user gui") ||
710711
normalized.includes("bootstrap failed: 125")
711712
);
712713
}

0 commit comments

Comments
 (0)