Skip to content

Commit 44ee295

Browse files
author
metal
committed
fix(daemon): use KeepAlive SuccessfulExit=false to prevent launchd crash-loop
Change macOS LaunchAgent plist from KeepAlive=true to KeepAlive with SuccessfulExit=false. This prevents launchd from restarting the gateway after clean exits, making crash debugging much easier while still restarting on actual failures. Also update the service audit check to recognize both KeepAlive forms.
1 parent edad452 commit 44ee295

3 files changed

Lines changed: 9 additions & 6 deletions

File tree

src/daemon/launchd-plist.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,5 +178,5 @@ export function buildLaunchAgentPlist({
178178
? `\n <key>Comment</key>\n <string>${plistEscape(comment.trim())}</string>`
179179
: "";
180180
const envXml = renderEnvDict(environment);
181-
return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n <dict>\n <key>Label</key>\n <string>${plistEscape(label)}</string>\n ${commentXml}\n <key>RunAtLoad</key>\n <true/>\n <key>KeepAlive</key>\n <true/>\n <key>ThrottleInterval</key>\n <integer>${LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS}</integer>\n <key>Umask</key>\n <integer>${LAUNCH_AGENT_UMASK_DECIMAL}</integer>\n <key>ProgramArguments</key>\n <array>${argsXml}\n </array>\n ${workingDirXml}\n <key>StandardOutPath</key>\n <string>${plistEscape(stdoutPath)}</string>\n <key>StandardErrorPath</key>\n <string>${plistEscape(stderrPath)}</string>${envXml}\n </dict>\n</plist>\n`;
181+
return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n <dict>\n <key>Label</key>\n <string>${plistEscape(label)}</string>\n ${commentXml}\n <key>RunAtLoad</key>\n <true/>\n <key>KeepAlive</key>\n <dict>\n <key>SuccessfulExit</key>\n <false/>\n </dict>\n <key>ThrottleInterval</key>\n <integer>${LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS}</integer>\n <key>Umask</key>\n <integer>${LAUNCH_AGENT_UMASK_DECIMAL}</integer>\n <key>ProgramArguments</key>\n <array>${argsXml}\n </array>\n ${workingDirXml}\n <key>StandardOutPath</key>\n <string>${plistEscape(stdoutPath)}</string>\n <key>StandardErrorPath</key>\n <string>${plistEscape(stderrPath)}</string>${envXml}\n </dict>\n</plist>\n`;
182182
}

src/daemon/launchd.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ describe("launchd install", () => {
527527
expect(state.dirModes.get(tmpDir)).toBe(0o700);
528528
});
529529

530-
it("writes KeepAlive=true policy with restrictive umask", async () => {
530+
it("writes KeepAlive SuccessfulExit=false policy with restrictive umask", async () => {
531531
const env = createDefaultLaunchdEnv();
532532
await installLaunchAgent({
533533
env,
@@ -538,8 +538,9 @@ describe("launchd install", () => {
538538
const plistPath = resolveLaunchAgentPlistPath(env);
539539
const plist = state.files.get(plistPath) ?? "";
540540
expect(plist).toContain("<key>KeepAlive</key>");
541-
expect(plist).toContain("<true/>");
542-
expect(plist).not.toContain("<key>SuccessfulExit</key>");
541+
expect(plist).toContain("<key>SuccessfulExit</key>");
542+
expect(plist).toContain("<false/>");
543+
expect(plist).not.toContain("<key>KeepAlive</key>\n <true/>");
543544
expect(plist).toContain("<key>Umask</key>");
544545
expect(plist).toContain(`<integer>${LAUNCH_AGENT_UMASK_DECIMAL}</integer>`);
545546
expect(plist).toContain("<key>ThrottleInterval</key>");

src/daemon/service-audit.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,9 @@ async function auditLaunchdPlist(
188188
}
189189

190190
const hasRunAtLoad = /<key>RunAtLoad<\/key>\s*<true\s*\/>/i.test(content);
191-
const hasKeepAlive = /<key>KeepAlive<\/key>\s*<true\s*\/>/i.test(content);
191+
const hasKeepAlive =
192+
/<key>KeepAlive<\/key>\s*<true\s*\/>/i.test(content) ||
193+
/<key>KeepAlive<\/key>\s*<dict>\s*<key>SuccessfulExit<\/key>\s*<false\s*\/>\s*<\/dict>/i.test(content);
192194
if (!hasRunAtLoad) {
193195
issues.push({
194196
code: SERVICE_AUDIT_CODES.launchdRunAtLoad,
@@ -200,7 +202,7 @@ async function auditLaunchdPlist(
200202
if (!hasKeepAlive) {
201203
issues.push({
202204
code: SERVICE_AUDIT_CODES.launchdKeepAlive,
203-
message: "LaunchAgent is missing KeepAlive=true",
205+
message: "LaunchAgent is missing KeepAlive policy",
204206
detail: plistPath,
205207
level: "recommended",
206208
});

0 commit comments

Comments
 (0)