


























@@ -8,6 +8,7 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest";
88import {
99installLaunchAgent,
1010readLaunchAgentRuntime,
11+repairLaunchAgentBootstrap,
1112restartLaunchAgent,
1213resolveLaunchAgentPlistPath,
1314stopLaunchAgent,
@@ -37,6 +38,10 @@ function canRunLaunchdIntegration(): boolean {
37383839const describeLaunchdIntegration = canRunLaunchdIntegration() ? describe : describe.skip;
394041+function resolveGuiDomain(): string {
42+return `gui/${process.getuid?.() ?? 501}`;
43+}
44+4045async function withTimeout<T>(params: {
4146run: () => Promise<T>;
4247timeoutMs: number;
@@ -133,6 +138,29 @@ async function initializeLaunchdRuntime(launchEnv: GatewayServiceEnv, stdout: Pa
133138});
134139}
135140141+async function writeLaunchAgentProbeScript(params: {
142+eventsPath: string;
143+scriptPath: string;
144+}): Promise<void> {
145+await fs.writeFile(
146+params.scriptPath,
147+[
148+'const fs = require("node:fs");',
149+`const eventsPath = ${JSON.stringify(params.eventsPath)};`,
150+"fs.appendFileSync(eventsPath, `start ${process.pid}\\n`);",
151+'for (const signal of ["SIGHUP", "SIGINT", "SIGTERM"]) {',
152+" process.on(signal, () => {",
153+" fs.appendFileSync(eventsPath, `${signal} ${process.pid}\\n`);",
154+" process.exit(0);",
155+" });",
156+"}",
157+"setInterval(() => {}, 1000);",
158+"",
159+].join("\n"),
160+"utf8",
161+);
162+}
163+136164async function expectRuntimePidReplaced(params: {
137165env: GatewayServiceEnv;
138166previousPid: number;
@@ -231,4 +259,42 @@ describeLaunchdIntegration("launchd integration", () => {
231259await restartLaunchAgent({ env: launchEnv, stdout });
232260await expectRuntimePidReplaced({ env: launchEnv, previousPid: before.pid });
233261}, 60_000);
262+263+it("repairs a missing bootstrap without kickstarting the fresh LaunchAgent", async () => {
264+const launchEnv = launchEnvOrThrow(env);
265+const eventsPath = path.join(homeDir, "repair-probe.events.log");
266+const scriptPath = path.join(homeDir, "repair-probe.cjs");
267+await writeLaunchAgentProbeScript({ eventsPath, scriptPath });
268+await installLaunchAgent({
269+env: launchEnv,
270+ stdout,
271+programArguments: [process.execPath, scriptPath],
272+});
273+await waitForRunningRuntime({ env: launchEnv });
274+const bootout = spawnSync(
275+"launchctl",
276+["bootout", resolveGuiDomain(), resolveLaunchAgentPlistPath(launchEnv)],
277+{ encoding: "utf8" },
278+);
279+expect(bootout.status).toBe(0);
280+await waitForNotRunningRuntime({ env: launchEnv });
281+await fs.access(resolveLaunchAgentPlistPath(launchEnv));
282+await fs.writeFile(eventsPath, "", "utf8");
283+284+const repair = await withTimeout({
285+run: async () => repairLaunchAgentBootstrap({ env: launchEnv }),
286+timeoutMs: STARTUP_TIMEOUT_MS,
287+message: "Timed out repairing launchd integration runtime",
288+});
289+expect(repair).toEqual({ ok: true, status: "repaired" });
290+await waitForRunningRuntime({ env: launchEnv });
291+292+await new Promise((resolve) => {
293+setTimeout(resolve, 1_500);
294+});
295+const events = await fs.readFile(eventsPath, "utf8");
296+const lines = events.trim().split(/\r?\n/).filter(Boolean);
297+expect(lines.filter((line) => line.startsWith("start "))).toHaveLength(1);
298+expect(lines.some((line) => /^(SIGHUP|SIGINT|SIGTERM) /.test(line))).toBe(false);
299+}, 60_000);
234300});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。