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
This commit is contained in:
Benedikt Schackenberg 2026-03-16 20:38:12 +00:00
parent 1684577259
commit e57ebd641c
2 changed files with 32 additions and 9 deletions

View File

@ -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();

View File

@ -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")
);
}