mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:00:50 +00:00
fix(daemon): tolerate loaded launchctl bootstrap (#71413)
This commit is contained in:
@@ -77,6 +77,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Providers/OpenRouter: treat DeepSeek refs as cache-TTL eligible without injecting Anthropic cache-control markers, aligning context pruning with OpenRouter-managed prompt caching. (#51983) Thanks @QuinnH496.
|
||||
- Discord/cron: deliver text-only isolated cron and heartbeat announce output from the canonical final assistant text once, avoiding duplicate Discord posts when streamed block payloads and the final answer contain the same content. Fixes #71406. Thanks @alexgross21.
|
||||
- macOS Gateway: wait for launchd to reload the exited Gateway LaunchAgent before bootstrapping repair fallback, preventing config-triggered restarts from leaving the service not loaded. Fixes #45178. Thanks @vincentkoc.
|
||||
- macOS Gateway: tolerate launchctl bootstrap's already-loaded exit during restart fallback and use non-killing kickstart after bootstrap, avoiding a second race that can unload the LaunchAgent. Fixes #41934. Thanks @zerone0x.
|
||||
- TTS/hooks: preserve audio-only TTS transcripts for `message_sending` and `message_sent` hooks without rendering the transcript as a media caption. Thanks @zqchris.
|
||||
- WhatsApp/TTS: preserve `audioAsVoice` through shared media payload sends and the WhatsApp outbound adapter, so `[[audio_as_voice]]` reply payloads keep their voice-note intent when routed through `sendPayload`. Fixes #66053. Thanks @masatohoshino.
|
||||
- Control UI/WebChat: hide heartbeat prompts, `HEARTBEAT_OK` acknowledgments, and internal-only runtime context turns from visible chat history while leaving the underlying transcript intact. Fixes #71381. Thanks @gerald1950ggg-ai.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { captureFullEnv } from "../test-utils/env.js";
|
||||
|
||||
const spawnSyncMock = vi.hoisted(() => vi.fn());
|
||||
const resolveLsofCommandSyncMock = vi.hoisted(() => vi.fn());
|
||||
@@ -25,12 +26,16 @@ vi.mock("../config/paths.js", async () => {
|
||||
let __testing: typeof import("./restart-stale-pids.js").__testing;
|
||||
let cleanStaleGatewayProcessesSync: typeof import("./restart-stale-pids.js").cleanStaleGatewayProcessesSync;
|
||||
let findGatewayPidsOnPortSync: typeof import("./restart-stale-pids.js").findGatewayPidsOnPortSync;
|
||||
let triggerOpenClawRestart: typeof import("./restart.js").triggerOpenClawRestart;
|
||||
|
||||
let currentTimeMs = 0;
|
||||
const envSnapshot = captureFullEnv();
|
||||
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
|
||||
|
||||
beforeAll(async () => {
|
||||
({ __testing, cleanStaleGatewayProcessesSync, findGatewayPidsOnPortSync } =
|
||||
await import("./restart-stale-pids.js"));
|
||||
({ triggerOpenClawRestart } = await import("./restart.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -48,11 +53,25 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
envSnapshot.restore();
|
||||
__testing.setSleepSyncOverride(null);
|
||||
__testing.setDateNowOverride(null);
|
||||
if (originalPlatformDescriptor) {
|
||||
Object.defineProperty(process, "platform", originalPlatformDescriptor);
|
||||
}
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function setPlatform(platform: NodeJS.Platform): void {
|
||||
if (!originalPlatformDescriptor) {
|
||||
return;
|
||||
}
|
||||
Object.defineProperty(process, "platform", {
|
||||
...originalPlatformDescriptor,
|
||||
value: platform,
|
||||
});
|
||||
}
|
||||
|
||||
describe.runIf(process.platform !== "win32")("findGatewayPidsOnPortSync", () => {
|
||||
it("parses lsof output and filters non-openclaw/current processes", () => {
|
||||
const gatewayPidA = process.pid + 1000;
|
||||
@@ -164,3 +183,41 @@ describe.runIf(process.platform !== "win32")("cleanStaleGatewayProcessesSync", (
|
||||
expect(killSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("triggerOpenClawRestart", () => {
|
||||
it("continues when launchctl bootstrap reports the service is already loaded", () => {
|
||||
setPlatform("darwin");
|
||||
delete process.env.VITEST;
|
||||
delete process.env.NODE_ENV;
|
||||
process.env.HOME = "/Users/test";
|
||||
process.env.OPENCLAW_PROFILE = "default";
|
||||
const uid = typeof process.getuid === "function" ? process.getuid() : 501;
|
||||
spawnSyncMock.mockImplementation((command: string, args: string[]) => {
|
||||
if (command === "/usr/sbin/lsof") {
|
||||
return { error: undefined, status: 1, stdout: "" };
|
||||
}
|
||||
if (command === "launchctl" && args[0] === "kickstart" && args[1] === "-k") {
|
||||
return { error: undefined, status: 113, stderr: "service not loaded" };
|
||||
}
|
||||
if (command === "launchctl" && args[0] === "bootstrap") {
|
||||
return { error: undefined, status: 37, stderr: "Operation already in progress" };
|
||||
}
|
||||
if (command === "launchctl" && args[0] === "kickstart") {
|
||||
return { error: undefined, status: 0, stdout: "" };
|
||||
}
|
||||
return { error: undefined, status: 1, stdout: "" };
|
||||
});
|
||||
|
||||
const result = triggerOpenClawRestart();
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
method: "launchctl",
|
||||
tried: [
|
||||
`launchctl kickstart -k gui/${uid}/ai.openclaw.gateway`,
|
||||
`launchctl bootstrap gui/${uid} /Users/test/Library/LaunchAgents/ai.openclaw.gateway.plist`,
|
||||
`launchctl kickstart gui/${uid}/ai.openclaw.gateway`,
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ const DEFAULT_DEFERRAL_POLL_MS = 500;
|
||||
// Configurable via gateway.reload.deferralTimeoutMs.
|
||||
const DEFAULT_DEFERRAL_MAX_WAIT_MS = 300_000;
|
||||
const RESTART_COOLDOWN_MS = 30_000;
|
||||
const LAUNCHCTL_ALREADY_LOADED_EXIT_CODE = 37;
|
||||
|
||||
const restartLog = createSubsystemLogger("restart");
|
||||
|
||||
@@ -427,7 +428,12 @@ export function triggerOpenClawRestart(): RestartAttempt {
|
||||
encoding: "utf8",
|
||||
timeout: SPAWN_TIMEOUT_MS,
|
||||
});
|
||||
if (boot.error || (boot.status !== 0 && boot.status !== null)) {
|
||||
if (
|
||||
boot.error ||
|
||||
(boot.status !== 0 &&
|
||||
boot.status !== LAUNCHCTL_ALREADY_LOADED_EXIT_CODE &&
|
||||
boot.status !== null)
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
method: "launchctl",
|
||||
@@ -435,7 +441,7 @@ export function triggerOpenClawRestart(): RestartAttempt {
|
||||
tried,
|
||||
};
|
||||
}
|
||||
const retryArgs = ["kickstart", "-k", target];
|
||||
const retryArgs = ["kickstart", target];
|
||||
tried.push(`launchctl ${retryArgs.join(" ")}`);
|
||||
const retry = spawnSync("launchctl", retryArgs, {
|
||||
encoding: "utf8",
|
||||
|
||||
Reference in New Issue
Block a user