From e57ebd641c6bbcd351238db6b36230087cef1867 Mon Sep 17 00:00:00 2001 From: Benedikt Schackenberg <6381261+BenediktSchackenberg@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:38:12 +0000 Subject: [PATCH] fix: track effective error detail across retries, detect 'Could not find domain' Address review feedback: - Use effectiveDetail to propagate the most relevant error message - Only show GUI-session guidance when failure is actually gui-domain related - Add 'could not find domain for' to isUnsupportedGuiDomain() detection - New test for the exact error string from issue #8619 --- src/daemon/launchd.test.ts | 13 +++++++++++++ src/daemon/launchd.ts | 28 +++++++++++++++++++--------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/daemon/launchd.test.ts b/src/daemon/launchd.test.ts index c150149a15b..f101a17a687 100644 --- a/src/daemon/launchd.test.ts +++ b/src/daemon/launchd.test.ts @@ -497,6 +497,19 @@ describe("launchd install", () => { expect(bootoutAfterBootstrap).toBeGreaterThanOrEqual(0); }); + it("falls back to launchctl load for 'Could not find domain' error from issue #8619", async () => { + state.bootstrapError = "Could not find domain for user gui: 1000"; + // loadError is empty → launchctl load succeeds + const env = createDefaultLaunchdEnv(); + const stdout = new PassThrough(); + await installLaunchAgent({ + env, + stdout, + programArguments: defaultProgramArguments, + }); + expect(state.launchctlCalls.some((c) => c[0] === "load" && c[1] === "-w")).toBe(true); + }); + it("surfaces generic bootstrap failures without GUI-specific guidance", async () => { state.bootstrapError = "Operation not permitted"; const env = createDefaultLaunchdEnv(); diff --git a/src/daemon/launchd.ts b/src/daemon/launchd.ts index 08c79005030..c85a4607a25 100644 --- a/src/daemon/launchd.ts +++ b/src/daemon/launchd.ts @@ -201,6 +201,10 @@ async function bootstrapLaunchAgentOrThrow(params: { return; } const detail = (boot.stderr || boot.stdout).trim(); + // Track the most relevant error detail across retries so that the final + // error message shown to the user reflects the actual failure, not a stale + // "already bootstrapped" message from an earlier attempt. + let effectiveDetail = detail; // If the service is already registered (e.g. re-install without prior uninstall), // bootout the stale registration and retry once. @@ -211,6 +215,7 @@ async function bootstrapLaunchAgentOrThrow(params: { return; } const retryDetail = (retry.stderr || retry.stdout).trim(); + effectiveDetail = retryDetail; if (isUnsupportedGuiDomain(retryDetail)) { // Fall through to the legacy load fallback below. } else if (!isAlreadyBootstrapped(retryDetail)) { @@ -220,18 +225,22 @@ async function bootstrapLaunchAgentOrThrow(params: { // If the gui/ domain is unavailable (SSH, headless, or sudo), try the // deprecated-but-universal `launchctl load` as a last resort before giving up. - if (isUnsupportedGuiDomain(detail) || isAlreadyBootstrapped(detail)) { + if (isUnsupportedGuiDomain(effectiveDetail) || isAlreadyBootstrapped(effectiveDetail)) { const load = await execLaunchctl(["load", "-w", params.plistPath]); if (load.code === 0) { return; } - // `launchctl load` also failed — throw the original gui-domain error so the - // user gets the actionable hint about desktop sessions. - throwBootstrapGuiSessionError({ - detail, - domain: params.domain, - actionHint: params.actionHint, - }); + const loadDetail = (load.stderr || load.stdout).trim(); + // Only show GUI-session guidance when the failure is actually gui-domain related. + // For other load failures (e.g. plist syntax, permissions), surface the real error. + if (isUnsupportedGuiDomain(effectiveDetail)) { + throwBootstrapGuiSessionError({ + detail: effectiveDetail, + domain: params.domain, + actionHint: params.actionHint, + }); + } + throw new Error(`launchctl load failed: ${loadDetail || effectiveDetail}`); } throw new Error(`launchctl bootstrap failed: ${detail}`); } @@ -469,7 +478,8 @@ function isUnsupportedGuiDomain(detail: string): boolean { const normalized = detail.toLowerCase(); return ( normalized.includes("domain does not support specified action") || - normalized.includes("bootstrap failed: 125") + normalized.includes("bootstrap failed: 125") || + normalized.includes("could not find domain for") ); }