@rubytech/create-sitedesk-code 0.1.514 → 0.1.516
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/launchd-bootstrap.test.js +164 -18
- package/dist/index.js +15 -14
- package/dist/launchd-bootstrap.js +86 -17
- package/package.json +1 -1
- package/payload/platform/docs/superpowers/plans/2026-07-27-task-2016-disabled-agent-routing.md +624 -0
- package/payload/platform/docs/superpowers/specs/2026-07-27-task-2016-disabled-agent-routing-design.md +139 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js +14 -16
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/specialist-registry.d.ts +14 -0
- package/payload/platform/plugins/admin/mcp/dist/specialist-registry.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/specialist-registry.js +57 -0
- package/payload/platform/plugins/admin/mcp/dist/specialist-registry.js.map +1 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +66 -5
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
- package/payload/platform/plugins/docs/references/admin-ui.md +38 -3
- package/payload/platform/plugins/docs/references/deployment.md +4 -0
- package/payload/platform/plugins/email/PLUGIN.md +1 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-metadata-calendar.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-metadata-calendar.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-metadata-calendar.test.js +94 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-metadata-calendar.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/index.js +1 -1
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +21 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js +49 -8
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
- package/payload/platform/plugins/email/references/email-reference.md +4 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-retry.test.js +143 -2
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-retry.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js +33 -3
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-paging.test.js +142 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-paging.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts +8 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js +40 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.js +54 -3
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.js.map +1 -1
- package/payload/platform/plugins/outlook/references/graph-surfaces.md +9 -3
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +2 -2
- package/payload/platform/scripts/__tests__/agents-md-bootstrap.test.sh +47 -0
- package/payload/platform/scripts/lib/agents-md-bootstrap.sh +58 -1
- package/payload/platform/services/claude-session-manager/dist/config.d.ts +48 -12
- package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/config.js +65 -15
- package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +7 -4
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.js +17 -2
- package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.js.map +1 -1
- package/payload/server/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/{AdminLoginScreens-Cs1jSnOF.js → AdminLoginScreens-B5qKb3YK.js} +1 -1
- package/payload/server/public/assets/AdminLoginScreens-B5qKb3YK.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-B5qKb3YK.js.gz +0 -0
- package/payload/server/public/assets/{AdminShell-DkrURZnF.js → AdminShell-BgJEq2zJ.js} +1 -1
- package/payload/server/public/assets/AdminShell-BgJEq2zJ.js.br +0 -0
- package/payload/server/public/assets/AdminShell-BgJEq2zJ.js.gz +0 -0
- package/payload/server/public/assets/{activity-BgZISMuE.js → activity-DmnVUDYO.js} +1 -1
- package/payload/server/public/assets/activity-DmnVUDYO.js.br +0 -0
- package/payload/server/public/assets/activity-DmnVUDYO.js.gz +0 -0
- package/payload/server/public/assets/{admin-DjFysc3-.js → admin-Dpge4OG9.js} +1 -1
- package/payload/server/public/assets/admin-Dpge4OG9.js.br +0 -0
- package/payload/server/public/assets/admin-Dpge4OG9.js.gz +0 -0
- package/payload/server/public/assets/{agents-Bx-MSZW6.js → agents-Bmq9X7P3.js} +1 -1
- package/payload/server/public/assets/agents-Bmq9X7P3.js.br +0 -0
- package/payload/server/public/assets/agents-Bmq9X7P3.js.gz +0 -0
- package/payload/server/public/assets/{browser-1I7syhHu.js → browser-B1G-W80B.js} +1 -1
- package/payload/server/public/assets/browser-B1G-W80B.js.br +0 -0
- package/payload/server/public/assets/browser-B1G-W80B.js.gz +0 -0
- package/payload/server/public/assets/{calendar-D_04JIo1.js → calendar-CHgrOFIN.js} +1 -1
- package/payload/server/public/assets/calendar-CHgrOFIN.js.br +0 -0
- package/payload/server/public/assets/calendar-CHgrOFIN.js.gz +0 -0
- package/payload/server/public/assets/chat-CIwmmtwP.js +1 -0
- package/payload/server/public/assets/chat-CIwmmtwP.js.br +0 -0
- package/payload/server/public/assets/chat-CIwmmtwP.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-EQjfaQ-n.js +1 -0
- package/payload/server/public/assets/chevron-left-EQjfaQ-n.js.br +0 -0
- package/payload/server/public/assets/chevron-right-Ct8jZUGP.js +1 -0
- package/payload/server/public/assets/chevron-right-Ct8jZUGP.js.br +0 -0
- package/payload/server/public/assets/clock-Cxz5ybck.js +1 -0
- package/payload/server/public/assets/clock-Cxz5ybck.js.br +0 -0
- package/payload/server/public/assets/clock-Cxz5ybck.js.gz +0 -0
- package/payload/server/public/assets/data-BQVXKVsJ.js +1 -0
- package/payload/server/public/assets/data-BQVXKVsJ.js.br +1 -0
- package/payload/server/public/assets/data-BQVXKVsJ.js.gz +0 -0
- package/payload/server/public/assets/{file-text-DkvKqHMx.js → file-text-BVqqaDun.js} +1 -1
- package/payload/server/public/assets/file-text-BVqqaDun.js.br +0 -0
- package/payload/server/public/assets/file-text-BVqqaDun.js.gz +0 -0
- package/payload/server/public/assets/{graph-CCZhrwZX.js → graph-CEcME0lp.js} +1 -1
- package/payload/server/public/assets/graph-CEcME0lp.js.br +0 -0
- package/payload/server/public/assets/graph-CEcME0lp.js.gz +0 -0
- package/payload/server/public/assets/{graph-labels-BJ7puQkX.js → graph-labels-Zl32_AOH.js} +1 -1
- package/payload/server/public/assets/graph-labels-Zl32_AOH.js.br +0 -0
- package/payload/server/public/assets/graph-labels-Zl32_AOH.js.gz +0 -0
- package/payload/server/public/assets/{maximize-2-Xdj804U1.js → maximize-2-rkkMyAhf.js} +1 -1
- package/payload/server/public/assets/maximize-2-rkkMyAhf.js.br +0 -0
- package/payload/server/public/assets/maximize-2-rkkMyAhf.js.gz +0 -0
- package/payload/server/public/assets/{operator-BkfNpBs7.js → operator-DlL023ch.js} +1 -1
- package/payload/server/public/assets/operator-DlL023ch.js.br +0 -0
- package/payload/server/public/assets/operator-DlL023ch.js.gz +0 -0
- package/payload/server/public/assets/{page-D5n8ShE7.js → page-BAmYYqxp.js} +1 -1
- package/payload/server/public/assets/page-BAmYYqxp.js.br +0 -0
- package/payload/server/public/assets/page-BAmYYqxp.js.gz +0 -0
- package/payload/server/public/assets/{page-BoYc9c4a.js → page-k0BhBkA2.js} +1 -1
- package/payload/server/public/assets/page-k0BhBkA2.js.br +0 -0
- package/payload/server/public/assets/page-k0BhBkA2.js.gz +0 -0
- package/payload/server/public/assets/{public-CX5XSrxD.js → public-Cqgfmo1D.js} +1 -1
- package/payload/server/public/assets/public-Cqgfmo1D.js.br +0 -0
- package/payload/server/public/assets/public-Cqgfmo1D.js.gz +0 -0
- package/payload/server/public/assets/{rotate-ccw-dh_OhL23.js → rotate-ccw-O3RmqoTy.js} +1 -1
- package/payload/server/public/assets/rotate-ccw-O3RmqoTy.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-O3RmqoTy.js.gz +0 -0
- package/payload/server/public/assets/{routines-bb7dxcrr.js → routines-DMYObMUy.js} +1 -1
- package/payload/server/public/assets/routines-DMYObMUy.js.br +0 -0
- package/payload/server/public/assets/routines-DMYObMUy.js.gz +0 -0
- package/payload/server/public/assets/{skills-TeMwxXmJ.js → skills-DON1BKu5.js} +1 -1
- package/payload/server/public/assets/skills-DON1BKu5.js.br +0 -0
- package/payload/server/public/assets/skills-DON1BKu5.js.gz +0 -0
- package/payload/server/public/assets/{tasks-BqBBJlkg.js → tasks-C2PhUSrf.js} +1 -1
- package/payload/server/public/assets/tasks-C2PhUSrf.js.br +0 -0
- package/payload/server/public/assets/tasks-C2PhUSrf.js.gz +0 -0
- package/payload/server/public/assets/{time-entry-format-Cy_T-wiD.js → time-entry-format-4zgmuQI0.js} +1 -1
- package/payload/server/public/assets/time-entry-format-4zgmuQI0.js.br +0 -0
- package/payload/server/public/assets/time-entry-format-4zgmuQI0.js.gz +0 -0
- package/payload/server/public/assets/{triangle-alert-D-bSbPnX.js → triangle-alert-By5D7wuI.js} +1 -1
- package/payload/server/public/assets/triangle-alert-By5D7wuI.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-By5D7wuI.js.gz +0 -0
- package/payload/server/public/assets/{useCopyFeedback-L7L55nFl.js → useCopyFeedback-Dx0vtm9s.js} +1 -1
- package/payload/server/public/assets/useCopyFeedback-Dx0vtm9s.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-Dx0vtm9s.js.gz +0 -0
- package/payload/server/public/assets/{useSubAccountSwitcher-BYl2c7xA.css → useSubAccountSwitcher-UQQDcj0B.css} +1 -1
- package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css.gz +0 -0
- package/payload/server/public/assets/{useVoiceRecorder-MuZ7lpet.js → useVoiceRecorder-BgwOCCwj.js} +1 -1
- package/payload/server/public/assets/useVoiceRecorder-BgwOCCwj.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-BgwOCCwj.js.gz +0 -0
- package/payload/server/public/assets/{wrench-DUG1SZQB.js → wrench-CHfMwLza.js} +1 -1
- package/payload/server/public/assets/wrench-CHfMwLza.js.br +0 -0
- package/payload/server/public/assets/wrench-CHfMwLza.js.gz +0 -0
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +7 -7
- package/payload/server/public/chat.html +13 -13
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +9 -9
- package/payload/server/public/index.html +14 -14
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +13 -13
- package/payload/server/public/routines.html +6 -6
- package/payload/server/public/skills.html +5 -5
- package/payload/server/public/tasks.html +6 -6
- package/payload/server/server.js +139 -17
- package/payload/server/public/assets/AdminLoginScreens-Cs1jSnOF.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-Cs1jSnOF.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-DkrURZnF.js.br +0 -0
- package/payload/server/public/assets/AdminShell-DkrURZnF.js.gz +0 -0
- package/payload/server/public/assets/activity-BgZISMuE.js.br +0 -0
- package/payload/server/public/assets/activity-BgZISMuE.js.gz +0 -0
- package/payload/server/public/assets/admin-DjFysc3-.js.br +0 -0
- package/payload/server/public/assets/admin-DjFysc3-.js.gz +0 -0
- package/payload/server/public/assets/agents-Bx-MSZW6.js.br +0 -0
- package/payload/server/public/assets/agents-Bx-MSZW6.js.gz +0 -0
- package/payload/server/public/assets/browser-1I7syhHu.js.br +0 -0
- package/payload/server/public/assets/browser-1I7syhHu.js.gz +0 -0
- package/payload/server/public/assets/calendar-D_04JIo1.js.br +0 -0
- package/payload/server/public/assets/calendar-D_04JIo1.js.gz +0 -0
- package/payload/server/public/assets/chat-BFH3FM5a.js +0 -1
- package/payload/server/public/assets/chat-BFH3FM5a.js.br +0 -0
- package/payload/server/public/assets/chat-BFH3FM5a.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-Ux9-qLkk.js +0 -1
- package/payload/server/public/assets/chevron-right-BwIuCfyG.js +0 -1
- package/payload/server/public/assets/chevron-right-BwIuCfyG.js.br +0 -0
- package/payload/server/public/assets/clock-DhJdZRhC.js +0 -1
- package/payload/server/public/assets/clock-DhJdZRhC.js.br +0 -0
- package/payload/server/public/assets/data-ChY_uSIk.js +0 -1
- package/payload/server/public/assets/data-ChY_uSIk.js.br +0 -0
- package/payload/server/public/assets/data-ChY_uSIk.js.gz +0 -0
- package/payload/server/public/assets/file-text-DkvKqHMx.js.br +0 -0
- package/payload/server/public/assets/file-text-DkvKqHMx.js.gz +0 -0
- package/payload/server/public/assets/graph-CCZhrwZX.js.br +0 -0
- package/payload/server/public/assets/graph-CCZhrwZX.js.gz +0 -0
- package/payload/server/public/assets/graph-labels-BJ7puQkX.js.br +0 -0
- package/payload/server/public/assets/graph-labels-BJ7puQkX.js.gz +0 -0
- package/payload/server/public/assets/maximize-2-Xdj804U1.js.br +0 -0
- package/payload/server/public/assets/maximize-2-Xdj804U1.js.gz +0 -0
- package/payload/server/public/assets/operator-BkfNpBs7.js.br +0 -0
- package/payload/server/public/assets/operator-BkfNpBs7.js.gz +0 -0
- package/payload/server/public/assets/page-BoYc9c4a.js.br +0 -0
- package/payload/server/public/assets/page-BoYc9c4a.js.gz +0 -0
- package/payload/server/public/assets/page-D5n8ShE7.js.br +0 -0
- package/payload/server/public/assets/page-D5n8ShE7.js.gz +0 -0
- package/payload/server/public/assets/public-CX5XSrxD.js.br +0 -0
- package/payload/server/public/assets/public-CX5XSrxD.js.gz +0 -0
- package/payload/server/public/assets/rotate-ccw-dh_OhL23.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-dh_OhL23.js.gz +0 -0
- package/payload/server/public/assets/routines-bb7dxcrr.js.br +0 -0
- package/payload/server/public/assets/routines-bb7dxcrr.js.gz +0 -0
- package/payload/server/public/assets/skills-TeMwxXmJ.js.br +0 -0
- package/payload/server/public/assets/skills-TeMwxXmJ.js.gz +0 -0
- package/payload/server/public/assets/tasks-BqBBJlkg.js.br +0 -0
- package/payload/server/public/assets/tasks-BqBBJlkg.js.gz +0 -0
- package/payload/server/public/assets/time-entry-format-Cy_T-wiD.js.br +0 -3
- package/payload/server/public/assets/time-entry-format-Cy_T-wiD.js.gz +0 -0
- package/payload/server/public/assets/triangle-alert-D-bSbPnX.js.br +0 -1
- package/payload/server/public/assets/triangle-alert-D-bSbPnX.js.gz +0 -0
- package/payload/server/public/assets/useCopyFeedback-L7L55nFl.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-L7L55nFl.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BYl2c7xA.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BYl2c7xA.css.gz +0 -0
- package/payload/server/public/assets/useVoiceRecorder-MuZ7lpet.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-MuZ7lpet.js.gz +0 -0
- package/payload/server/public/assets/wrench-DUG1SZQB.js.br +0 -0
- package/payload/server/public/assets/wrench-DUG1SZQB.js.gz +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-DH0P6QMV.js → useSubAccountSwitcher-CRWoQXuU.js} +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-DH0P6QMV.js.br → useSubAccountSwitcher-CRWoQXuU.js.br} +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-DH0P6QMV.js.gz → useSubAccountSwitcher-CRWoQXuU.js.gz} +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
// acceptance grid for launchd-bootstrap.ts (Task 1401).
|
|
1
|
+
// acceptance grid for launchd-bootstrap.ts (Task 1401, reshaped by Task 2069).
|
|
2
2
|
//
|
|
3
|
-
// Locks the darwin
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// Locks the darwin LaunchAgent bootstrap contract: bootout (idempotent, rc 3
|
|
4
|
+
// "No such process" ignored), then poll `launchctl print` until the label
|
|
5
|
+
// leaves the domain, then bootstrap, with a bounded retry on exit 5
|
|
6
|
+
// ("Input/output error" — returned for as long as the previous instance is
|
|
7
|
+
// still terminating). Pure logic — the launchctl runner, the sleep and the
|
|
8
|
+
// clock are all injected, so no real launchctl and no wall-clock delay runs
|
|
9
|
+
// here.
|
|
8
10
|
//
|
|
9
11
|
// Compiles to dist/__tests__/launchd-bootstrap.test.js so
|
|
10
12
|
// `node --test dist/__tests__/*.test.js` picks it up after build.
|
|
@@ -13,33 +15,74 @@ import assert from "node:assert/strict";
|
|
|
13
15
|
import { bootstrapLaunchAgent } from "../launchd-bootstrap.js";
|
|
14
16
|
const OPTS = { gui: "gui/502", label: "com.rubytech.host-claude-session-manager", plistPath: "/tmp/sm.plist" };
|
|
15
17
|
const noSleep = () => { };
|
|
16
|
-
|
|
18
|
+
/** A fake clock the injected sleepFn advances, so the wait-budget arithmetic
|
|
19
|
+
* runs with no wall-clock cost. */
|
|
20
|
+
function fakeClock() {
|
|
21
|
+
let t = 0;
|
|
22
|
+
return { now: () => t, sleep: (ms) => { t += ms; } };
|
|
23
|
+
}
|
|
24
|
+
test("waits for the label to leave the domain before bootstrapping", () => {
|
|
25
|
+
const clock = fakeClock();
|
|
17
26
|
const calls = [];
|
|
27
|
+
let prints = 0;
|
|
18
28
|
const run = (args) => {
|
|
19
29
|
calls.push(args);
|
|
20
|
-
|
|
30
|
+
if (args[0] === "print") {
|
|
31
|
+
prints += 1;
|
|
32
|
+
// The old instance lingers for 3 polls, then leaves the domain.
|
|
33
|
+
return prints <= 3 ? { status: 0, stderr: "" } : { status: 113, stderr: "" };
|
|
34
|
+
}
|
|
35
|
+
if (args[0] === "bootstrap")
|
|
36
|
+
return { status: 0, stderr: "" };
|
|
37
|
+
return { status: 0, stderr: "" };
|
|
21
38
|
};
|
|
22
|
-
const r = bootstrapLaunchAgent(run, { ...OPTS, sleepFn:
|
|
39
|
+
const r = bootstrapLaunchAgent(run, { ...OPTS, sleepFn: clock.sleep, nowFn: clock.now });
|
|
23
40
|
assert.equal(r.ok, true);
|
|
24
41
|
assert.equal(r.attempts, 1);
|
|
42
|
+
assert.equal(prints, 4); // 3 polls with the label present, 1 showing it gone
|
|
43
|
+
assert.equal(r.totalWaitedMs, 1500); // 3 sleeps of 500ms before the gone poll
|
|
44
|
+
const bootstrapIdx = calls.findIndex((c) => c[0] === "bootstrap");
|
|
45
|
+
assert.ok(bootstrapIdx > 3, `bootstrap issued at index ${bootstrapIdx}, before the label was gone`);
|
|
46
|
+
});
|
|
47
|
+
test("fresh install — bootout, one absence poll, bootstrap, attempts=1", () => {
|
|
48
|
+
const clock = fakeClock();
|
|
49
|
+
const calls = [];
|
|
50
|
+
const run = (args) => {
|
|
51
|
+
calls.push(args);
|
|
52
|
+
if (args[0] === "print")
|
|
53
|
+
return { status: 113, stderr: "" };
|
|
54
|
+
if (args[0] === "bootstrap")
|
|
55
|
+
return { status: 0, stderr: "" };
|
|
56
|
+
return { status: 3, stderr: "No such process" };
|
|
57
|
+
};
|
|
58
|
+
const r = bootstrapLaunchAgent(run, { ...OPTS, sleepFn: clock.sleep, nowFn: clock.now });
|
|
59
|
+
assert.equal(r.ok, true);
|
|
60
|
+
assert.equal(r.attempts, 1);
|
|
61
|
+
assert.equal(r.totalWaitedMs, 0); // gone on the first poll — a fresh install never waits
|
|
25
62
|
assert.deepEqual(calls[0], ["bootout", "gui/502/com.rubytech.host-claude-session-manager"]);
|
|
26
|
-
assert.deepEqual(calls[1], ["
|
|
27
|
-
assert.
|
|
63
|
+
assert.deepEqual(calls[1], ["print", "gui/502/com.rubytech.host-claude-session-manager"]);
|
|
64
|
+
assert.deepEqual(calls[2], ["bootstrap", "gui/502", "/tmp/sm.plist"]);
|
|
65
|
+
assert.equal(calls.length, 3);
|
|
28
66
|
});
|
|
29
67
|
test("exit 5 twice then 0 — retries and reports the winning attempt", () => {
|
|
68
|
+
const clock = fakeClock();
|
|
30
69
|
let bootstraps = 0;
|
|
31
70
|
const sleeps = [];
|
|
32
71
|
const run = (args) => {
|
|
72
|
+
if (args[0] === "print")
|
|
73
|
+
return { status: 113, stderr: "" };
|
|
33
74
|
if (args[0] !== "bootstrap")
|
|
34
75
|
return { status: 3, stderr: "No such process" };
|
|
35
76
|
bootstraps += 1;
|
|
36
77
|
return bootstraps < 3 ? { status: 5, stderr: "Bootstrap failed: 5: Input/output error" } : { status: 0, stderr: "" };
|
|
37
78
|
};
|
|
38
|
-
const r = bootstrapLaunchAgent(run, {
|
|
79
|
+
const r = bootstrapLaunchAgent(run, {
|
|
80
|
+
...OPTS, sleepFn: (ms) => { sleeps.push(ms); clock.sleep(ms); }, nowFn: clock.now,
|
|
81
|
+
});
|
|
39
82
|
assert.equal(r.ok, true);
|
|
40
83
|
assert.equal(r.attempts, 3);
|
|
41
84
|
assert.equal(bootstraps, 3);
|
|
42
|
-
assert.equal(sleeps.length, 2); // slept before each of the two retries
|
|
85
|
+
assert.equal(sleeps.filter((ms) => ms === 500).length, 2); // slept before each of the two retries
|
|
43
86
|
});
|
|
44
87
|
test("exit 5 on every attempt — fails after the attempt cap, not before", () => {
|
|
45
88
|
let bootstraps = 0;
|
|
@@ -69,17 +112,120 @@ test("non-5 non-zero bootstrap aborts immediately — no retry", () => {
|
|
|
69
112
|
assert.equal(r.bootstrapStatus, 9);
|
|
70
113
|
assert.equal(bootstraps, 1);
|
|
71
114
|
});
|
|
72
|
-
test("
|
|
115
|
+
test("label never leaves the domain — bootstraps anyway at the budget, wait is reported", () => {
|
|
116
|
+
const clock = fakeClock();
|
|
117
|
+
const lines = [];
|
|
118
|
+
const run = (args) => {
|
|
119
|
+
if (args[0] === "print")
|
|
120
|
+
return { status: 0, stderr: "" }; // always present
|
|
121
|
+
if (args[0] === "bootstrap")
|
|
122
|
+
return { status: 5, stderr: "Bootstrap failed: 5: Input/output error" };
|
|
123
|
+
return { status: 0, stderr: "" };
|
|
124
|
+
};
|
|
125
|
+
const r = bootstrapLaunchAgent(run, {
|
|
126
|
+
...OPTS, maxAttempts: 1, sleepFn: clock.sleep, nowFn: clock.now, logger: (l) => lines.push(l),
|
|
127
|
+
});
|
|
128
|
+
assert.equal(r.ok, false);
|
|
129
|
+
assert.equal(r.lastPresent, true);
|
|
130
|
+
assert.equal(r.totalWaitedMs, 60_000);
|
|
131
|
+
assert.ok(lines.some((l) => /op=wait-timeout waitedMs=60000 polls=\d+ present=true probeFailures=0 action=bootstrap-anyway/.test(l)));
|
|
132
|
+
});
|
|
133
|
+
test("the wait budget is shared across attempts, not spent per attempt", () => {
|
|
134
|
+
const clock = fakeClock();
|
|
135
|
+
const run = (args) => {
|
|
136
|
+
if (args[0] === "print")
|
|
137
|
+
return { status: 0, stderr: "" };
|
|
138
|
+
if (args[0] === "bootstrap")
|
|
139
|
+
return { status: 5, stderr: "eio" };
|
|
140
|
+
return { status: 0, stderr: "" };
|
|
141
|
+
};
|
|
142
|
+
const r = bootstrapLaunchAgent(run, { ...OPTS, maxAttempts: 3, sleepFn: clock.sleep, nowFn: clock.now });
|
|
143
|
+
assert.equal(r.attempts, 3);
|
|
144
|
+
// 60s of waiting in total across three attempts, not 180s.
|
|
145
|
+
assert.equal(r.totalWaitedMs, 60_000);
|
|
146
|
+
});
|
|
147
|
+
test("each exit-5 retry re-enters the wait rather than firing after retryDelayMs", () => {
|
|
148
|
+
const clock = fakeClock();
|
|
149
|
+
const seq = [];
|
|
150
|
+
let prints = 0;
|
|
151
|
+
const run = (args) => {
|
|
152
|
+
seq.push(args[0]);
|
|
153
|
+
if (args[0] === "print") {
|
|
154
|
+
prints += 1;
|
|
155
|
+
// present for one poll per attempt, then gone
|
|
156
|
+
return prints % 2 === 1 ? { status: 0, stderr: "" } : { status: 113, stderr: "" };
|
|
157
|
+
}
|
|
158
|
+
if (args[0] === "bootstrap")
|
|
159
|
+
return { status: 5, stderr: "eio" };
|
|
160
|
+
return { status: 0, stderr: "" };
|
|
161
|
+
};
|
|
162
|
+
bootstrapLaunchAgent(run, { ...OPTS, maxAttempts: 2, sleepFn: clock.sleep, nowFn: clock.now });
|
|
163
|
+
assert.deepEqual(seq, [
|
|
164
|
+
"bootout", "print", "print", "bootstrap",
|
|
165
|
+
"bootout", "print", "print", "bootstrap",
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
test("a probe that cannot run is not read as an absent label", () => {
|
|
169
|
+
const clock = fakeClock();
|
|
170
|
+
const lines = [];
|
|
171
|
+
const run = (args) => {
|
|
172
|
+
// status null = launchctl could not be spawned, or was killed by the
|
|
173
|
+
// spawnSync timeout. That is not evidence the label is gone.
|
|
174
|
+
if (args[0] === "print")
|
|
175
|
+
return { status: null, stderr: "" };
|
|
176
|
+
if (args[0] === "bootstrap")
|
|
177
|
+
return { status: 5, stderr: "eio" };
|
|
178
|
+
return { status: 0, stderr: "" };
|
|
179
|
+
};
|
|
180
|
+
const r = bootstrapLaunchAgent(run, {
|
|
181
|
+
...OPTS, maxAttempts: 1, sleepFn: clock.sleep, nowFn: clock.now, logger: (l) => lines.push(l),
|
|
182
|
+
});
|
|
183
|
+
assert.equal(r.lastPresent, true); // never confirmed gone
|
|
184
|
+
assert.equal(r.totalWaitedMs, 60_000); // the full wait was served, not skipped
|
|
185
|
+
assert.ok(r.probeFailures > 0);
|
|
186
|
+
assert.ok(lines.some((l) => /op=wait-poll present=true probeRc=null/.test(l)));
|
|
187
|
+
});
|
|
188
|
+
test("the wait terminates on the poll cap even if the clock never advances", () => {
|
|
189
|
+
// A wall clock that steps backwards (an NTP correction) would leave the
|
|
190
|
+
// elapsed-time test permanently unsatisfied. Termination must not depend on
|
|
191
|
+
// time moving forwards.
|
|
192
|
+
const frozenNow = () => 0;
|
|
193
|
+
let polls = 0;
|
|
194
|
+
const run = (args) => {
|
|
195
|
+
if (args[0] === "print") {
|
|
196
|
+
polls += 1;
|
|
197
|
+
return { status: 0, stderr: "" };
|
|
198
|
+
}
|
|
199
|
+
if (args[0] === "bootstrap")
|
|
200
|
+
return { status: 5, stderr: "eio" };
|
|
201
|
+
return { status: 0, stderr: "" };
|
|
202
|
+
};
|
|
203
|
+
const r = bootstrapLaunchAgent(run, {
|
|
204
|
+
...OPTS, maxAttempts: 1, sleepFn: noSleep, nowFn: frozenNow,
|
|
205
|
+
});
|
|
206
|
+
assert.equal(r.ok, false);
|
|
207
|
+
assert.equal(r.totalWaitedMs, 0); // the frozen clock measures no elapsed time
|
|
208
|
+
assert.equal(polls, 121); // ceil(60000/500) + 1 — the cap, not an infinite loop
|
|
209
|
+
});
|
|
210
|
+
test("emits one structured line per lifecycle step, correlated by agent label", () => {
|
|
211
|
+
const clock = fakeClock();
|
|
73
212
|
let bootstraps = 0;
|
|
74
213
|
const lines = [];
|
|
75
214
|
const run = (args) => {
|
|
215
|
+
if (args[0] === "print")
|
|
216
|
+
return { status: 113, stderr: "" };
|
|
76
217
|
if (args[0] !== "bootstrap")
|
|
77
218
|
return { status: 3, stderr: "No such process" };
|
|
78
219
|
bootstraps += 1;
|
|
79
220
|
return bootstraps < 2 ? { status: 5, stderr: "eio" } : { status: 0, stderr: "" };
|
|
80
221
|
};
|
|
81
|
-
bootstrapLaunchAgent(run, { ...OPTS, sleepFn:
|
|
82
|
-
|
|
83
|
-
assert.
|
|
84
|
-
|
|
222
|
+
bootstrapLaunchAgent(run, { ...OPTS, sleepFn: clock.sleep, nowFn: clock.now, logger: (l) => lines.push(l) });
|
|
223
|
+
const ops = lines.map((l) => l.match(/op=([a-z-]+)/)[1]);
|
|
224
|
+
assert.deepEqual(ops, [
|
|
225
|
+
"bootout", "wait-poll", "gone", "bootstrap",
|
|
226
|
+
"bootout", "wait-poll", "gone", "bootstrap", "result",
|
|
227
|
+
]);
|
|
228
|
+
assert.ok(lines.every((l) => l.startsWith("[launchd] agent=com.rubytech.host-claude-session-manager ")));
|
|
229
|
+
assert.match(lines[3], /op=bootstrap rc=5 attempt=1 waitedMs=0/);
|
|
230
|
+
assert.match(lines[8], /op=result ok=true attempts=2 totalWaitedMs=0 probeFailures=0 rc=0/);
|
|
85
231
|
});
|
package/dist/index.js
CHANGED
|
@@ -1361,8 +1361,9 @@ function resetNeo4jAuth(port = DEFAULT_NEO4J_PORT, dataDir = "/var/lib/neo4j", p
|
|
|
1361
1361
|
const label = `${launchdLabel()}-neo4j`;
|
|
1362
1362
|
console.log(" Resetting dedicated Neo4j auth (launchd)...");
|
|
1363
1363
|
spawnSync("launchctl", ["bootout", `${gui()}/${label}`], { stdio: "pipe" });
|
|
1364
|
-
// launchctl bootout returns
|
|
1365
|
-
// graceful multi-second store flush on SIGTERM — so
|
|
1364
|
+
// launchctl bootout returns as soon as launchd accepts the request (Task
|
|
1365
|
+
// 2069), and neo4j does a graceful multi-second store flush on SIGTERM — so
|
|
1366
|
+
// the JVM can still hold
|
|
1366
1367
|
// the port (and the store) after bootout returns. Wait for the bolt port to
|
|
1367
1368
|
// free before clearing the auth db and re-seeding: set-initial-password only
|
|
1368
1369
|
// takes effect against a stopped store, and rm must not race a live flush.
|
|
@@ -1386,7 +1387,7 @@ function resetNeo4jAuth(port = DEFAULT_NEO4J_PORT, dataDir = "/var/lib/neo4j", p
|
|
|
1386
1387
|
const plistPath = join(launchAgentsDir(), `${label}.plist`);
|
|
1387
1388
|
const boot = bootstrapLaunchAgent((a) => { const r = spawnSync("launchctl", a, { stdio: "pipe", encoding: "utf-8", timeout: 15_000 }); return { status: r.status, stderr: (r.stderr ?? "").toString() }; }, { gui: gui(), label, plistPath, logger: (l) => { console.log(` ${l}`); logFile(` ${l}`); } });
|
|
1388
1389
|
if (!boot.ok) {
|
|
1389
|
-
throw new Error(`launchctl bootstrap ${label} failed (exit ${boot.bootstrapStatus}) after ${boot.attempts} attempt(s) during dedicated darwin auth reset: ${boot.stderr}`);
|
|
1390
|
+
throw new Error(`launchctl bootstrap ${label} failed (exit ${boot.bootstrapStatus}) after ${boot.attempts} attempt(s), waited ${boot.totalWaitedMs}ms, stillPresent=${boot.lastPresent}, probeFailures=${boot.probeFailures} during dedicated darwin auth reset: ${boot.stderr}`);
|
|
1390
1391
|
}
|
|
1391
1392
|
logFile(` [neo4j-darwin-auth-reset] dedicated datadir=${dataDir} conf=${p.confDir} label=${label}`);
|
|
1392
1393
|
}
|
|
@@ -1804,7 +1805,7 @@ function setupDedicatedNeo4jDarwin() {
|
|
|
1804
1805
|
logFile(` ${plistPath} written (${plist.length} bytes)`);
|
|
1805
1806
|
const boot = bootstrapLaunchAgent((a) => { const r = spawnSync("launchctl", a, { stdio: "pipe", encoding: "utf-8", timeout: 15_000 }); return { status: r.status, stderr: (r.stderr ?? "").toString() }; }, { gui: gui(), label, plistPath, logger: (l) => { console.log(` ${l}`); logFile(` ${l}`); } });
|
|
1806
1807
|
if (!boot.ok) {
|
|
1807
|
-
throw new Error(`launchctl bootstrap ${label} failed (exit ${boot.bootstrapStatus}) after ${boot.attempts} attempt(s): ${boot.stderr}`);
|
|
1808
|
+
throw new Error(`launchctl bootstrap ${label} failed (exit ${boot.bootstrapStatus}) after ${boot.attempts} attempt(s), waited ${boot.totalWaitedMs}ms, stillPresent=${boot.lastPresent}, probeFailures=${boot.probeFailures}: ${boot.stderr}`);
|
|
1808
1809
|
}
|
|
1809
1810
|
// Health-check the brand's bolt port — loud failure, no silent fallback to the
|
|
1810
1811
|
// shared instance (matches the Linux dedicated bind check).
|
|
@@ -3891,12 +3892,12 @@ function installServiceDarwin() {
|
|
|
3891
3892
|
});
|
|
3892
3893
|
if (smBoot.ok) {
|
|
3893
3894
|
console.log(` [launchd] bootstrap ${gui()}/${smLabel} ok`);
|
|
3894
|
-
logFile(` [create-maxy] launchd-plist=${smLabel} loaded=true attempts=${smBoot.attempts}`);
|
|
3895
|
+
logFile(` [create-maxy] launchd-plist=${smLabel} loaded=true attempts=${smBoot.attempts} waitedMs=${smBoot.totalWaitedMs}`);
|
|
3895
3896
|
}
|
|
3896
3897
|
else {
|
|
3897
|
-
console.error(` [launchd] bootstrap ${smLabel} returned ${smBoot.bootstrapStatus} after ${smBoot.attempts} attempt(s): ${smBoot.stderr}`);
|
|
3898
|
-
logFile(` [create-maxy] launchd-plist=${smLabel} loaded=false exit=${smBoot.bootstrapStatus} attempts=${smBoot.attempts}`);
|
|
3899
|
-
throw new Error(`launchctl bootstrap ${smLabel} failed (exit ${smBoot.bootstrapStatus}) after ${smBoot.attempts} attempt(s): ${smBoot.stderr}`);
|
|
3898
|
+
console.error(` [launchd] bootstrap ${smLabel} returned ${smBoot.bootstrapStatus} after ${smBoot.attempts} attempt(s), waited ${smBoot.totalWaitedMs}ms, stillPresent=${smBoot.lastPresent}, probeFailures=${smBoot.probeFailures}: ${smBoot.stderr}`);
|
|
3899
|
+
logFile(` [create-maxy] launchd-plist=${smLabel} loaded=false exit=${smBoot.bootstrapStatus} attempts=${smBoot.attempts} waitedMs=${smBoot.totalWaitedMs} stillPresent=${smBoot.lastPresent} probeFailures=${smBoot.probeFailures}`);
|
|
3900
|
+
throw new Error(`launchctl bootstrap ${smLabel} failed (exit ${smBoot.bootstrapStatus}) after ${smBoot.attempts} attempt(s), waited ${smBoot.totalWaitedMs}ms, stillPresent=${smBoot.lastPresent}, probeFailures=${smBoot.probeFailures}: ${smBoot.stderr}`);
|
|
3900
3901
|
}
|
|
3901
3902
|
// Health-probe the manager (/healthz) so a silent boot failure surfaces at
|
|
3902
3903
|
// install time, not at first session spawn as `manager-unreachable`.
|
|
@@ -3947,11 +3948,11 @@ function installServiceDarwin() {
|
|
|
3947
3948
|
logger: (line) => { console.log(` ${line}`); logFile(` ${line}`); },
|
|
3948
3949
|
});
|
|
3949
3950
|
if (hbBoot.ok) {
|
|
3950
|
-
logFile(` [create-maxy] launchd-plist=${hbLabel} loaded=true attempts=${hbBoot.attempts}`);
|
|
3951
|
+
logFile(` [create-maxy] launchd-plist=${hbLabel} loaded=true attempts=${hbBoot.attempts} waitedMs=${hbBoot.totalWaitedMs}`);
|
|
3951
3952
|
}
|
|
3952
3953
|
else {
|
|
3953
|
-
console.error(` WARNING: heartbeat LaunchAgent ${hbLabel} bootstrap returned ${hbBoot.bootstrapStatus} after ${hbBoot.attempts} attempt(s): ${hbBoot.stderr} — scheduling ticks will not fire until a re-install succeeds.`);
|
|
3954
|
-
logFile(` [create-maxy] launchd-plist=${hbLabel} loaded=false exit=${hbBoot.bootstrapStatus} attempts=${hbBoot.attempts}`);
|
|
3954
|
+
console.error(` WARNING: heartbeat LaunchAgent ${hbLabel} bootstrap returned ${hbBoot.bootstrapStatus} after ${hbBoot.attempts} attempt(s), waited ${hbBoot.totalWaitedMs}ms, stillPresent=${hbBoot.lastPresent}, probeFailures=${hbBoot.probeFailures}: ${hbBoot.stderr} — scheduling ticks will not fire until a re-install succeeds.`);
|
|
3955
|
+
logFile(` [create-maxy] launchd-plist=${hbLabel} loaded=false exit=${hbBoot.bootstrapStatus} attempts=${hbBoot.attempts} waitedMs=${hbBoot.totalWaitedMs} stillPresent=${hbBoot.lastPresent} probeFailures=${hbBoot.probeFailures}`);
|
|
3955
3956
|
}
|
|
3956
3957
|
// Task 1706 — the log rotator. On Linux a */15 entry in the per-brand cron
|
|
3957
3958
|
// block ticks logs-rotate.sh (installCrons/buildBrandCronBlock); darwin has no
|
|
@@ -3987,11 +3988,11 @@ function installServiceDarwin() {
|
|
|
3987
3988
|
logger: (line) => { console.log(` ${line}`); logFile(` ${line}`); },
|
|
3988
3989
|
});
|
|
3989
3990
|
if (lrBoot.ok) {
|
|
3990
|
-
logFile(` [create-maxy] launchd-plist=${lrLabel} loaded=true attempts=${lrBoot.attempts}`);
|
|
3991
|
+
logFile(` [create-maxy] launchd-plist=${lrLabel} loaded=true attempts=${lrBoot.attempts} waitedMs=${lrBoot.totalWaitedMs}`);
|
|
3991
3992
|
}
|
|
3992
3993
|
else {
|
|
3993
|
-
console.error(` WARNING: logs-rotate LaunchAgent ${lrLabel} bootstrap returned ${lrBoot.bootstrapStatus} after ${lrBoot.attempts} attempt(s): ${lrBoot.stderr} — brand logs will not be rotated until a re-install succeeds.`);
|
|
3994
|
-
logFile(` [create-maxy] launchd-plist=${lrLabel} loaded=false exit=${lrBoot.bootstrapStatus} attempts=${lrBoot.attempts}`);
|
|
3994
|
+
console.error(` WARNING: logs-rotate LaunchAgent ${lrLabel} bootstrap returned ${lrBoot.bootstrapStatus} after ${lrBoot.attempts} attempt(s), waited ${lrBoot.totalWaitedMs}ms, stillPresent=${lrBoot.lastPresent}, probeFailures=${lrBoot.probeFailures}: ${lrBoot.stderr} — brand logs will not be rotated until a re-install succeeds.`);
|
|
3995
|
+
logFile(` [create-maxy] launchd-plist=${lrLabel} loaded=false exit=${lrBoot.bootstrapStatus} attempts=${lrBoot.attempts} waitedMs=${lrBoot.totalWaitedMs} stillPresent=${lrBoot.lastPresent} probeFailures=${lrBoot.probeFailures}`);
|
|
3995
3996
|
}
|
|
3996
3997
|
// Wait for the server to come up.
|
|
3997
3998
|
console.log(" Waiting for web server...");
|
|
@@ -1,19 +1,34 @@
|
|
|
1
|
-
// Task 1401 — darwin
|
|
1
|
+
// Task 1401 / Task 2069 — darwin LaunchAgent bootstrap, made idempotent and
|
|
2
2
|
// EIO-tolerant.
|
|
3
3
|
//
|
|
4
|
-
// `launchctl bootstrap`
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// `launchctl bootstrap` returns exit 5 ("Bootstrap failed: 5: Input/output
|
|
5
|
+
// error") when the label's previous instance has not finished terminating.
|
|
6
|
+
// 1401 read this as a transient racey failure and retried three times 500 ms
|
|
7
|
+
// apart; 2069 measured it instead. `launchctl bootout` returns as soon as
|
|
8
|
+
// launchd accepts the request, but the job's process keeps running — a
|
|
9
|
+
// dedicated Neo4j's JVM took 14.2 s to reach "Stopped." on the Muvin iMac while
|
|
10
|
+
// all three retries fired inside 1.043 s and every one got exit 5. The failure
|
|
11
|
+
// is deterministic for the length of that shutdown, not random.
|
|
10
12
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
// So this helper boots the label out, polls `launchctl print <gui>/<label>`
|
|
14
|
+
// until the label leaves the domain, and only then bootstraps. `print` returns
|
|
15
|
+
// 0 for the whole time the old process lives and non-zero once it exits
|
|
16
|
+
// (measured: rc 113), which makes it an exit-code probe rather than prose
|
|
17
|
+
// parsing. The exit-5 retry is kept for the residual case, and each retry
|
|
18
|
+
// re-enters the wait.
|
|
19
|
+
//
|
|
20
|
+
// What `print`'s exit code does NOT report is liveness: a job that loads and
|
|
21
|
+
// exits immediately still returns rc 0, with "state = not running" appearing
|
|
22
|
+
// only in stdout. Detecting a dead-on-arrival agent therefore needs a different
|
|
23
|
+
// primitive and is not attempted here.
|
|
24
|
+
//
|
|
25
|
+
// The launchctl runner, the sleep, and the clock are all injected, so the logic
|
|
26
|
+
// is unit-tested with no real launchctl and no wall-clock delay. Kept
|
|
27
|
+
// synchronous to match the spawnSync-based installer body (installService is a
|
|
28
|
+
// synchronous `void`).
|
|
29
|
+
/** `launchctl bootstrap` exit code for "Input/output error" — returned for as
|
|
30
|
+
* long as the label's previous instance is still terminating. The wait above
|
|
31
|
+
* is what removes that window; the retry covers what is left. */
|
|
17
32
|
const BOOTSTRAP_EIO = 5;
|
|
18
33
|
/** Blocking sleep with no imports, safe inside the synchronous installer. */
|
|
19
34
|
function blockingSleep(ms) {
|
|
@@ -24,20 +39,69 @@ function blockingSleep(ms) {
|
|
|
24
39
|
export function bootstrapLaunchAgent(run, o) {
|
|
25
40
|
const maxAttempts = Math.max(1, o.maxAttempts ?? 3);
|
|
26
41
|
const retryDelayMs = o.retryDelayMs ?? 500;
|
|
42
|
+
const waitBudgetMs = o.waitBudgetMs ?? 60_000;
|
|
43
|
+
const pollIntervalMs = Math.max(1, o.pollIntervalMs ?? 500);
|
|
44
|
+
const now = o.nowFn ?? Date.now;
|
|
27
45
|
const sleepFn = o.sleepFn ?? blockingSleep;
|
|
28
46
|
const log = o.logger ?? (() => { });
|
|
47
|
+
const target = `${o.gui}/${o.label}`;
|
|
48
|
+
const tag = `[launchd] agent=${o.label}`;
|
|
49
|
+
// The wall clock is not monotonic — an NTP step backwards during the wait
|
|
50
|
+
// would leave the elapsed-time test below permanently unsatisfied. This poll
|
|
51
|
+
// allowance bounds the loop independently of the clock, so termination never
|
|
52
|
+
// depends on time moving forwards.
|
|
53
|
+
let pollsRemaining = Math.ceil(waitBudgetMs / pollIntervalMs) + 1;
|
|
54
|
+
let totalWaitedMs = 0;
|
|
55
|
+
let lastPresent = false;
|
|
56
|
+
let probeFailures = 0;
|
|
29
57
|
let attempt = 0;
|
|
30
58
|
let last = { status: null, stderr: "" };
|
|
31
59
|
for (attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
32
60
|
// Idempotent: bootout any prior instance so the following bootstrap starts
|
|
33
61
|
// clean. rc 3 ("No such process") is the expected fresh case; it and any
|
|
34
62
|
// other bootout rc are ignored — bootout is cleanup, not the verified op.
|
|
35
|
-
const bootout = run(["bootout",
|
|
63
|
+
const bootout = run(["bootout", target]);
|
|
64
|
+
log(`${tag} op=bootout rc=${bootout.status ?? "null"} attempt=${attempt}`);
|
|
65
|
+
// bootout returns as soon as launchd accepts the request, but the job's
|
|
66
|
+
// process can keep running for seconds (Neo4j's JVM: 14.2 s measured on the
|
|
67
|
+
// Muvin iMac). `launchctl print` returns 0 for that whole window and
|
|
68
|
+
// non-zero once the process is gone, so it is the absence probe.
|
|
69
|
+
// Bootstrapping before that is what returns EIO.
|
|
70
|
+
const waitStart = now();
|
|
71
|
+
let present = true;
|
|
72
|
+
let polls = 0;
|
|
73
|
+
for (;;) {
|
|
74
|
+
const p = run(["print", target]);
|
|
75
|
+
polls += 1;
|
|
76
|
+
pollsRemaining -= 1;
|
|
77
|
+
// Three outcomes, not two. rc 0 = still in the domain. A non-zero rc
|
|
78
|
+
// (113) = gone. A null status means the probe could not run at all —
|
|
79
|
+
// that is not evidence of absence, so the wait continues rather than
|
|
80
|
+
// treating an unrunnable probe as a clear domain.
|
|
81
|
+
const gone = p.status !== null && p.status !== 0;
|
|
82
|
+
if (p.status === null)
|
|
83
|
+
probeFailures += 1;
|
|
84
|
+
present = !gone;
|
|
85
|
+
const elapsedMs = now() - waitStart;
|
|
86
|
+
log(`${tag} op=wait-poll present=${present} probeRc=${p.status ?? "null"} elapsedMs=${elapsedMs}`);
|
|
87
|
+
if (gone) {
|
|
88
|
+
log(`${tag} op=gone goneMs=${elapsedMs} polls=${polls} attempt=${attempt}`);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (totalWaitedMs + elapsedMs >= waitBudgetMs || pollsRemaining <= 0) {
|
|
92
|
+
log(`${tag} op=wait-timeout waitedMs=${totalWaitedMs + elapsedMs} polls=${polls} present=true probeFailures=${probeFailures} action=bootstrap-anyway`);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
sleepFn(pollIntervalMs);
|
|
96
|
+
}
|
|
97
|
+
totalWaitedMs += Math.max(0, now() - waitStart);
|
|
98
|
+
lastPresent = present;
|
|
36
99
|
const bootstrap = run(["bootstrap", o.gui, o.plistPath]);
|
|
37
100
|
last = { status: bootstrap.status, stderr: (bootstrap.stderr ?? "").trim() };
|
|
38
|
-
log(
|
|
101
|
+
log(`${tag} op=bootstrap rc=${bootstrap.status ?? "null"} attempt=${attempt} waitedMs=${totalWaitedMs}`);
|
|
39
102
|
if (bootstrap.status === 0) {
|
|
40
|
-
|
|
103
|
+
log(`${tag} op=result ok=true attempts=${attempt} totalWaitedMs=${totalWaitedMs} probeFailures=${probeFailures} rc=0`);
|
|
104
|
+
return { ok: true, attempts: attempt, bootstrapStatus: 0, stderr: last.stderr, totalWaitedMs, lastPresent, probeFailures };
|
|
41
105
|
}
|
|
42
106
|
// Only exit 5 (EIO) is retried, and only while attempts remain. Every other
|
|
43
107
|
// non-zero status is a real failure that aborts now rather than burning the
|
|
@@ -48,10 +112,15 @@ export function bootstrapLaunchAgent(run, o) {
|
|
|
48
112
|
}
|
|
49
113
|
break;
|
|
50
114
|
}
|
|
115
|
+
const attempts = Math.min(attempt, maxAttempts);
|
|
116
|
+
log(`${tag} op=result ok=false attempts=${attempts} totalWaitedMs=${totalWaitedMs} probeFailures=${probeFailures} rc=${last.status ?? "null"}`);
|
|
51
117
|
return {
|
|
52
118
|
ok: false,
|
|
53
|
-
attempts
|
|
119
|
+
attempts,
|
|
54
120
|
bootstrapStatus: last.status,
|
|
55
121
|
stderr: last.stderr,
|
|
122
|
+
totalWaitedMs,
|
|
123
|
+
lastPresent,
|
|
124
|
+
probeFailures,
|
|
56
125
|
};
|
|
57
126
|
}
|
package/package.json
CHANGED