Skip to content

Commit c37db98

Browse files
committed
fix: retry transient launchd bootstrap failures
1 parent 0269bef commit c37db98

3 files changed

Lines changed: 37 additions & 13 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opencode-supertask",
3-
"version": "0.1.32-rc.1",
3+
"version": "0.1.32-rc.2",
44
"description": "AI Agent 任务调度系统 — OpenCode 插件 + CLI + Gateway 常驻进程",
55
"type": "module",
66
"main": "dist/plugin/supertask.js",

src/daemon/pm2.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,29 @@ export function isMacLaunchAgentConfigured(
417417
}
418418
}
419419

420+
function bootstrapMacLaunchAgent(domain: string, path: string) {
421+
const retryDeadline = Date.now() + 2_000;
422+
const sleeper = new Int32Array(new SharedArrayBuffer(4));
423+
let result = spawnSync(launchctlBin(), ["bootstrap", domain, path], {
424+
encoding: "utf8",
425+
timeout: pm2CommandTimeoutMs(),
426+
killSignal: "SIGKILL",
427+
});
428+
429+
while (result.status !== 0 && Date.now() < retryDeadline) {
430+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
431+
if (result.status !== 5 && !output.includes("Bootstrap failed: 5:")) break;
432+
Atomics.wait(sleeper, 0, 0, 100);
433+
result = spawnSync(launchctlBin(), ["bootstrap", domain, path], {
434+
encoding: "utf8",
435+
timeout: pm2CommandTimeoutMs(),
436+
killSignal: "SIGKILL",
437+
});
438+
}
439+
440+
return result;
441+
}
442+
420443
export function installMacLaunchAgent(expectedRuntime = currentGatewayRuntime()): string {
421444
if (typeof process.getuid !== "function") {
422445
throw new Error("[supertask] 当前运行时无法获取 macOS 用户 ID");
@@ -488,11 +511,7 @@ export function installMacLaunchAgent(expectedRuntime = currentGatewayRuntime())
488511
timeout: pm2CommandTimeoutMs(),
489512
killSignal: "SIGKILL",
490513
});
491-
const loaded = spawnSync(launchctlBin(), ["bootstrap", domain, path], {
492-
encoding: "utf8",
493-
timeout: pm2CommandTimeoutMs(),
494-
killSignal: "SIGKILL",
495-
});
514+
const loaded = bootstrapMacLaunchAgent(domain, path);
496515
const configuredVerifyTimeout = Number(process.env.SUPERTASK_LAUNCH_AGENT_VERIFY_TIMEOUT_MS ?? 2_000);
497516
const verifyTimeoutMs = Number.isFinite(configuredVerifyTimeout) && configuredVerifyTimeout > 0
498517
? configuredVerifyTimeout
@@ -523,11 +542,7 @@ export function installMacLaunchAgent(expectedRuntime = currentGatewayRuntime())
523542

524543
let rollbackFailure = "";
525544
if (wasLoaded && previousPlist != null) {
526-
const restored = spawnSync(launchctlBin(), ["bootstrap", domain, path], {
527-
encoding: "utf8",
528-
timeout: pm2CommandTimeoutMs(),
529-
killSignal: "SIGKILL",
530-
});
545+
const restored = bootstrapMacLaunchAgent(domain, path);
531546
if (restored.status !== 0) {
532547
rollbackFailure = `;旧 LaunchAgent 恢复失败: ${`${restored.stdout ?? ""}${restored.stderr ?? ""}`.trim() || `退出码 ${restored.status}`}`;
533548
}

tests/pm2.test.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ if (args[0] === 'install') writeFileSync(${JSON.stringify(moduleState)}, 'instal
112112
]);
113113
});
114114

115-
test('macOS 用户级 LaunchAgent 长期监督 PM2 且无需 sudo', () => {
115+
test('macOS 用户级 LaunchAgent 在 bootout 竞态后重试并长期监督 PM2', () => {
116116
const dir = mkdtempSync(join(tmpdir(), 'supertask-launch-agent-'));
117117
dirs.push(dir);
118118
const fakePm2 = join(dir, 'pm2 & tool');
@@ -132,9 +132,17 @@ if (args[0] === 'install') writeFileSync(${JSON.stringify(moduleState)}, 'instal
132132
pm2_env: { args: [gateway], pm_exec_path: process.execPath, pm_cwd: process.cwd(), env: expectedEnv },
133133
}]));
134134
writeFileSync(fakeLaunchctl, `#!/usr/bin/env bun
135-
import { appendFileSync } from 'fs';
135+
import { appendFileSync, readFileSync } from 'fs';
136136
const args = Bun.argv.slice(2);
137137
appendFileSync(${JSON.stringify(launchctlLog)}, JSON.stringify(args) + '\\n');
138+
if (args[0] === 'bootstrap') {
139+
const calls = readFileSync(${JSON.stringify(launchctlLog)}, 'utf8').trim().split('\\n')
140+
.map((line) => JSON.parse(line));
141+
if (calls.filter((call) => call[0] === 'bootstrap').length === 1) {
142+
console.error('Bootstrap failed: 5: Input/output error');
143+
process.exit(5);
144+
}
145+
}
138146
if (args[0] === 'print') {
139147
console.log('path = ${plist}');
140148
console.log('program = ${process.execPath}');
@@ -164,6 +172,7 @@ if (args[0] === 'print') {
164172
expect(calls).toEqual([
165173
['bootout', `gui/${process.getuid()}/com.supertask.pm2-resurrect`],
166174
['bootstrap', `gui/${process.getuid()}`, plist],
175+
['bootstrap', `gui/${process.getuid()}`, plist],
167176
['print', `gui/${process.getuid()}/com.supertask.pm2-resurrect`],
168177
]);
169178
});

0 commit comments

Comments
 (0)