mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 02:32:44 +00:00
fix(daemon): stabilize LaunchAgent restart and proxy env passthrough (#27276)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: b08797a995
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
112
src/daemon/launchd.integration.test.ts
Normal file
112
src/daemon/launchd.integration.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
installLaunchAgent,
|
||||
readLaunchAgentRuntime,
|
||||
restartLaunchAgent,
|
||||
resolveLaunchAgentPlistPath,
|
||||
uninstallLaunchAgent,
|
||||
} from "./launchd.js";
|
||||
import type { GatewayServiceEnv } from "./service-types.js";
|
||||
|
||||
const WAIT_INTERVAL_MS = 200;
|
||||
const WAIT_TIMEOUT_MS = 15_000;
|
||||
|
||||
function canRunLaunchdIntegration(): boolean {
|
||||
if (process.platform !== "darwin") {
|
||||
return false;
|
||||
}
|
||||
if (typeof process.getuid !== "function") {
|
||||
return false;
|
||||
}
|
||||
const domain = `gui/${process.getuid()}`;
|
||||
const probe = spawnSync("launchctl", ["print", domain], { encoding: "utf8" });
|
||||
if (probe.error) {
|
||||
return false;
|
||||
}
|
||||
return probe.status === 0;
|
||||
}
|
||||
|
||||
const describeLaunchdIntegration = canRunLaunchdIntegration() ? describe : describe.skip;
|
||||
|
||||
async function waitForRunningRuntime(params: {
|
||||
env: GatewayServiceEnv;
|
||||
pidNot?: number;
|
||||
timeoutMs?: number;
|
||||
}): Promise<{ pid: number }> {
|
||||
const timeoutMs = params.timeoutMs ?? WAIT_TIMEOUT_MS;
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastStatus = "unknown";
|
||||
let lastPid: number | undefined;
|
||||
while (Date.now() < deadline) {
|
||||
const runtime = await readLaunchAgentRuntime(params.env);
|
||||
lastStatus = runtime.status;
|
||||
lastPid = runtime.pid;
|
||||
if (
|
||||
runtime.status === "running" &&
|
||||
typeof runtime.pid === "number" &&
|
||||
runtime.pid > 1 &&
|
||||
(params.pidNot === undefined || runtime.pid !== params.pidNot)
|
||||
) {
|
||||
return { pid: runtime.pid };
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, WAIT_INTERVAL_MS);
|
||||
});
|
||||
}
|
||||
throw new Error(
|
||||
`Timed out waiting for launchd runtime (status=${lastStatus}, pid=${lastPid ?? "none"})`,
|
||||
);
|
||||
}
|
||||
|
||||
describeLaunchdIntegration("launchd integration", () => {
|
||||
let env: GatewayServiceEnv | undefined;
|
||||
let homeDir = "";
|
||||
const stdout = new PassThrough();
|
||||
|
||||
beforeAll(async () => {
|
||||
const testId = randomUUID().slice(0, 8);
|
||||
homeDir = await fs.mkdtemp(path.join(os.tmpdir(), `openclaw-launchd-int-${testId}-`));
|
||||
env = {
|
||||
HOME: homeDir,
|
||||
OPENCLAW_LAUNCHD_LABEL: `ai.openclaw.launchd-int-${testId}`,
|
||||
OPENCLAW_LOG_PREFIX: `gateway-launchd-int-${testId}`,
|
||||
};
|
||||
await installLaunchAgent({
|
||||
env,
|
||||
stdout,
|
||||
programArguments: [process.execPath, "-e", "setInterval(() => {}, 1000);"],
|
||||
});
|
||||
await waitForRunningRuntime({ env });
|
||||
}, 30_000);
|
||||
|
||||
afterAll(async () => {
|
||||
if (env) {
|
||||
try {
|
||||
await uninstallLaunchAgent({ env, stdout });
|
||||
} catch {
|
||||
// Best-effort cleanup in case launchctl state already changed.
|
||||
}
|
||||
}
|
||||
if (homeDir) {
|
||||
await fs.rm(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
it("restarts launchd service and keeps it running with a new pid", async () => {
|
||||
if (!env) {
|
||||
throw new Error("launchd integration env was not initialized");
|
||||
}
|
||||
const before = await waitForRunningRuntime({ env });
|
||||
await restartLaunchAgent({ env, stdout });
|
||||
const after = await waitForRunningRuntime({ env, pidNot: before.pid });
|
||||
expect(after.pid).toBeGreaterThan(1);
|
||||
expect(after.pid).not.toBe(before.pid);
|
||||
await fs.access(resolveLaunchAgentPlistPath(env));
|
||||
}, 30_000);
|
||||
});
|
||||
Reference in New Issue
Block a user