mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-30 16:20:26 +00:00
fix(update): ignore inherited launchd xpc for respawn (#85789)
This commit is contained in:
@@ -86,6 +86,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/WebChat: hide duplicate `gateway-injected` assistant rows when Cursor ACP already persisted the same `acp-runtime` reply. Fixes #85741. Thanks @lxf-lxf.
|
||||
- WebChat: scope the visible attachment button to its own composer file input so clicking Upload reliably opens the file picker. (#83952, fixes #47983) Thanks @jason-allen-oneal.
|
||||
- Gateway: preserve deferred lifecycle-error cleanup across later non-terminal events so provider timeouts can persist failed session state instead of leaving sessions stuck running. (#85256, fixes #63819) Thanks @samzong.
|
||||
- Gateway/update: stop treating inherited macOS `XPC_SERVICE_NAME` values as launchd supervision during update respawn, so GUI-spawned gateways use detached respawn instead of exiting for a missing LaunchAgent. Fixes #85224. Thanks @richardmqq.
|
||||
- Agents/subagents: report tool-only child progress during timeout summaries instead of showing no visible output.
|
||||
- Telegram/ACP: preserve explicit `:topic:` conversation suffixes when inbound ACP targets do not carry a separate thread id.
|
||||
- Browser/proxy: bypass the managed proxy for the exact local managed Chrome CDP readiness and DevTools WebSocket endpoints, so `openclaw browser start` works when the operator proxy blocks loopback egress. (#83255) Thanks @lightcap.
|
||||
|
||||
@@ -86,11 +86,35 @@ describe("restartGatewayProcessWithFreshPid", () => {
|
||||
expect(spawnMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns supervised when launchd hints are present on macOS (no kickstart)", () => {
|
||||
it("returns supervised when OpenClaw launchd markers are present on macOS (no kickstart)", () => {
|
||||
clearSupervisorHints();
|
||||
expectLaunchdSupervisedWithoutKickstart({ launchJobLabel: "ai.openclaw.gateway" });
|
||||
});
|
||||
|
||||
it("returns supervised for a real gateway launchd job without the injected marker", () => {
|
||||
clearSupervisorHints();
|
||||
setPlatform("darwin");
|
||||
process.env.LAUNCH_JOB_LABEL = "ai.openclaw.gateway";
|
||||
|
||||
const result = restartGatewayProcessWithFreshPid();
|
||||
|
||||
expect(result.mode).toBe("supervised");
|
||||
expect(triggerOpenClawRestartMock).not.toHaveBeenCalled();
|
||||
expect(spawnMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns supervised for a real gateway XPC launchd job without the injected marker", () => {
|
||||
clearSupervisorHints();
|
||||
setPlatform("darwin");
|
||||
process.env.XPC_SERVICE_NAME = "ai.openclaw.gateway";
|
||||
|
||||
const result = restartGatewayProcessWithFreshPid();
|
||||
|
||||
expect(result.mode).toBe("supervised");
|
||||
expect(triggerOpenClawRestartMock).not.toHaveBeenCalled();
|
||||
expect(spawnMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns supervised on macOS when launchd label is set (no kickstart)", () => {
|
||||
expectLaunchdSupervisedWithoutKickstart({ launchJobLabel: "ai.openclaw.gateway" });
|
||||
});
|
||||
@@ -123,12 +147,18 @@ describe("restartGatewayProcessWithFreshPid", () => {
|
||||
expect(spawnMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns supervised when XPC_SERVICE_NAME is set by launchd", () => {
|
||||
it("does not treat inherited XPC_SERVICE_NAME as launchd supervision", () => {
|
||||
clearSupervisorHints();
|
||||
setPlatform("darwin");
|
||||
process.env.XPC_SERVICE_NAME = "ai.openclaw.gateway";
|
||||
process.env.XPC_SERVICE_NAME = "ai.openclaw.mac";
|
||||
process.env.OPENCLAW_PROFILE = "mac";
|
||||
|
||||
const result = restartGatewayProcessWithFreshPid();
|
||||
expect(result.mode).toBe("supervised");
|
||||
|
||||
expect(result).toEqual({
|
||||
mode: "disabled",
|
||||
detail: "unmanaged: use in-process restart to keep custom supervisor PID tracking stable",
|
||||
});
|
||||
expect(triggerOpenClawRestartMock).not.toHaveBeenCalled();
|
||||
expect(spawnMock).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -288,6 +318,29 @@ describe("respawnGatewayProcessForUpdate", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("spawns a detached update process when macOS only has inherited XPC state", () => {
|
||||
clearSupervisorHints();
|
||||
setPlatform("darwin");
|
||||
process.env.XPC_SERVICE_NAME = "ai.openclaw.mac";
|
||||
process.execArgv = [];
|
||||
process.argv = ["/usr/local/bin/node", "/repo/dist/index.js", "gateway", "run"];
|
||||
spawnMock.mockReturnValue({ pid: 6161, unref: vi.fn(), kill: vi.fn() });
|
||||
|
||||
const result = respawnGatewayProcessForUpdate();
|
||||
|
||||
expect(result.mode).toBe("spawned");
|
||||
expect(result.pid).toBe(6161);
|
||||
expect(spawnMock).toHaveBeenCalledWith(
|
||||
process.execPath,
|
||||
["/repo/dist/index.js", "gateway", "run"],
|
||||
{
|
||||
detached: true,
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("returns failed when update detached respawn throws", () => {
|
||||
delete process.env.OPENCLAW_NO_RESPAWN;
|
||||
clearSupervisorHints();
|
||||
|
||||
@@ -13,12 +13,34 @@ describe("SUPERVISOR_HINT_ENV_VARS", () => {
|
||||
});
|
||||
|
||||
describe("detectRespawnSupervisor", () => {
|
||||
it("detects launchd and systemd only from non-blank platform-specific hints", () => {
|
||||
expect(detectRespawnSupervisor({ LAUNCH_JOB_LABEL: " ai.openclaw.gateway " }, "darwin")).toBe(
|
||||
it("detects launchd from OpenClaw's explicit marker or current gateway launchd job", () => {
|
||||
expect(
|
||||
detectRespawnSupervisor({ OPENCLAW_LAUNCHD_LABEL: " ai.openclaw.gateway " }, "darwin"),
|
||||
).toBe("launchd");
|
||||
expect(detectRespawnSupervisor({ OPENCLAW_LAUNCHD_LABEL: " " }, "darwin")).toBeNull();
|
||||
expect(detectRespawnSupervisor({ LAUNCH_JOB_LABEL: "ai.openclaw.gateway" }, "darwin")).toBe(
|
||||
"launchd",
|
||||
);
|
||||
expect(detectRespawnSupervisor({ LAUNCH_JOB_LABEL: " " }, "darwin")).toBeNull();
|
||||
expect(
|
||||
detectRespawnSupervisor(
|
||||
{ LAUNCH_JOB_NAME: "ai.openclaw.work", OPENCLAW_PROFILE: "work" },
|
||||
"darwin",
|
||||
),
|
||||
).toBe("launchd");
|
||||
expect(detectRespawnSupervisor({ LAUNCH_JOB_LABEL: "ai.openclaw.mac" }, "darwin")).toBeNull();
|
||||
expect(detectRespawnSupervisor({ XPC_SERVICE_NAME: "ai.openclaw.mac" }, "darwin")).toBeNull();
|
||||
expect(
|
||||
detectRespawnSupervisor(
|
||||
{ XPC_SERVICE_NAME: "ai.openclaw.mac", OPENCLAW_PROFILE: "mac" },
|
||||
"darwin",
|
||||
),
|
||||
).toBeNull();
|
||||
expect(detectRespawnSupervisor({ XPC_SERVICE_NAME: "ai.openclaw.gateway" }, "darwin")).toBe(
|
||||
"launchd",
|
||||
);
|
||||
});
|
||||
|
||||
it("detects systemd only from non-blank platform-specific hints", () => {
|
||||
expect(detectRespawnSupervisor({ INVOCATION_ID: "abc123" }, "linux")).toBe("systemd");
|
||||
expect(detectRespawnSupervisor({ JOURNAL_STREAM: "" }, "linux")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { GATEWAY_LAUNCH_AGENT_LABEL, resolveGatewayLaunchAgentLabel } from "../daemon/constants.js";
|
||||
|
||||
const SUPERVISOR_HINTS = {
|
||||
launchd: ["LAUNCH_JOB_LABEL", "LAUNCH_JOB_NAME", "XPC_SERVICE_NAME", "OPENCLAW_LAUNCHD_LABEL"],
|
||||
launchd: ["OPENCLAW_LAUNCHD_LABEL"],
|
||||
systemd: ["OPENCLAW_SYSTEMD_UNIT", "INVOCATION_ID", "SYSTEMD_EXEC_PID", "JOURNAL_STREAM"],
|
||||
schtasks: ["OPENCLAW_WINDOWS_TASK_NAME"],
|
||||
} as const;
|
||||
|
||||
export const SUPERVISOR_HINT_ENV_VARS = [
|
||||
"LAUNCH_JOB_LABEL",
|
||||
"LAUNCH_JOB_NAME",
|
||||
"XPC_SERVICE_NAME",
|
||||
...SUPERVISOR_HINTS.launchd,
|
||||
...SUPERVISOR_HINTS.systemd,
|
||||
...SUPERVISOR_HINTS.schtasks,
|
||||
@@ -21,12 +26,24 @@ function hasAnyHint(env: NodeJS.ProcessEnv, keys: readonly string[]): boolean {
|
||||
});
|
||||
}
|
||||
|
||||
function isCurrentGatewayLaunchdJob(env: NodeJS.ProcessEnv): boolean {
|
||||
const expectedLabel = resolveGatewayLaunchAgentLabel(env.OPENCLAW_PROFILE);
|
||||
if (
|
||||
[env.LAUNCH_JOB_LABEL, env.LAUNCH_JOB_NAME].some((value) => value?.trim() === expectedLabel)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return env.XPC_SERVICE_NAME?.trim() === GATEWAY_LAUNCH_AGENT_LABEL;
|
||||
}
|
||||
|
||||
export function detectRespawnSupervisor(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): RespawnSupervisor | null {
|
||||
if (platform === "darwin") {
|
||||
return hasAnyHint(env, SUPERVISOR_HINTS.launchd) ? "launchd" : null;
|
||||
return hasAnyHint(env, SUPERVISOR_HINTS.launchd) || isCurrentGatewayLaunchdJob(env)
|
||||
? "launchd"
|
||||
: null;
|
||||
}
|
||||
if (platform === "linux") {
|
||||
return hasAnyHint(env, SUPERVISOR_HINTS.systemd) ? "systemd" : null;
|
||||
|
||||
Reference in New Issue
Block a user