fix(gateway): handle missing launchd gui domains

This commit is contained in:
Ayaan Zaidi
2026-06-10 10:31:17 +05:30
parent c39dea917a
commit c7b4c6bfc5
3 changed files with 20 additions and 13 deletions

View File

@@ -59,14 +59,14 @@ type LaunchAgentBootstrapDoctorOutcome =
function noteGatewayRuntime(
serviceRuntime: GatewayServiceRuntime | undefined,
env: Record<string, string | undefined>,
): boolean {
): void {
const summary = formatGatewayRuntimeSummary(serviceRuntime);
const hints = buildGatewayRuntimeHints(serviceRuntime, {
platform: process.platform,
env,
});
if (!summary && hints.length === 0) {
return false;
return;
}
const lines: string[] = [];
@@ -75,7 +75,6 @@ function noteGatewayRuntime(
}
lines.push(...hints);
note(lines.join("\n"), "Gateway");
return true;
}
async function maybeRepairLaunchAgentBootstrap(params: {
@@ -208,6 +207,8 @@ export async function maybeRepairGatewayDaemon(params: {
const serviceRepairPolicy = resolveServiceRepairPolicy();
const serviceRepairExternal = isServiceRepairExternallyManaged(serviceRepairPolicy);
const service = resolveGatewayService();
const isLocalDarwinGateway =
process.platform === "darwin" && params.cfg.gateway?.mode !== "remote";
// systemd can throw in containers/WSL; treat as "not loaded" and fall back to hints.
let loaded;
try {
@@ -225,8 +226,7 @@ export async function maybeRepairGatewayDaemon(params: {
...command.environment,
} satisfies NodeJS.ProcessEnv)
: process.env;
const shouldReadRuntime =
loaded || (process.platform === "darwin" && params.cfg.gateway?.mode !== "remote");
const shouldReadRuntime = loaded || isLocalDarwinGateway;
if (shouldReadRuntime) {
serviceRuntime = await service.readRuntime(serviceEnv).catch(() => undefined);
}
@@ -237,7 +237,7 @@ export async function maybeRepairGatewayDaemon(params: {
}
}
if (process.platform === "darwin" && params.cfg.gateway?.mode !== "remote") {
if (isLocalDarwinGateway) {
const gatewayRepair = serviceRuntime?.missingGuiSession
? ({ status: "gui-session-unavailable", detail: serviceRuntime.detail ?? "" } as const)
: await maybeRepairLaunchAgentBootstrap({
@@ -296,7 +296,7 @@ export async function maybeRepairGatewayDaemon(params: {
if (!loaded) {
if (
process.platform === "darwin" &&
isLocalDarwinGateway &&
(serviceRuntime?.missingGuiSession ||
serviceRuntime?.missingSupervision ||
serviceRuntime?.cachedLabel)

View File

@@ -428,10 +428,13 @@ describe("launchd runtime state", () => {
expect(runtime.detail).toBe("Could not find service");
});
it("marks installed LaunchAgents unavailable when the GUI domain is missing", async () => {
it.each([
"Bootstrap failed: 125: Domain does not support specified action",
"Could not find domain for user gui: 999999",
])("marks installed LaunchAgents unavailable when launchd reports %s", async (detail) => {
const env = createDefaultLaunchdEnv();
state.files.set(resolveLaunchAgentPlistPath(env), "<plist/>");
state.printError = "Bootstrap failed: 125: Domain does not support specified action";
state.printError = detail;
state.printFailuresRemaining = 1;
const runtime = await readLaunchAgentRuntime(env);
@@ -439,7 +442,7 @@ describe("launchd runtime state", () => {
expect(runtime.status).toBe("unknown");
expect(runtime.missingSupervision).toBe(true);
expect(runtime.missingGuiSession).toBe(true);
expect(runtime.detail).toContain("Domain does not support specified action");
expect(runtime.detail).toBe(detail);
});
it("marks a missing unit when launchd has no job and no plist exists", async () => {
@@ -793,8 +796,11 @@ describe("launchd bootstrap repair", () => {
expect(launchctlCommandNames()).not.toContain("kickstart");
});
it("classifies headless GUI bootstrap failures separately from generic not-loaded repair", async () => {
state.bootstrapError = "Bootstrap failed: 125: Domain does not support specified action";
it.each([
"Bootstrap failed: 125: Domain does not support specified action",
"Could not find domain for user gui: 999999",
])("classifies %s separately from generic not-loaded repair", async (detail) => {
state.bootstrapError = detail;
const env = createDefaultLaunchdEnv();
const repair = await repairLaunchAgentBootstrap({ env });
@@ -802,7 +808,7 @@ describe("launchd bootstrap repair", () => {
expect(repair).toEqual({
ok: false,
status: "gui-session-unavailable",
detail: "Bootstrap failed: 125: Domain does not support specified action",
detail,
domain: typeof process.getuid === "function" ? `gui/${process.getuid()}` : "gui/501",
});
expect(launchctlCommandNames()).not.toContain("kickstart");

View File

@@ -707,6 +707,7 @@ function isUnsupportedGuiDomain(detail: string): boolean {
const normalized = normalizeLowercaseStringOrEmpty(detail);
return (
normalized.includes("domain does not support specified action") ||
normalized.includes("could not find domain for user gui") ||
normalized.includes("bootstrap failed: 125")
);
}