diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ddac6fa4..734ab7e6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ Docs: https://docs.openclaw.ai - CLI/Doctor: ensure `openclaw doctor --fix --non-interactive --yes` exits promptly after completion so one-shot automation no longer hangs. (#18502) - CLI/Doctor: auto-repair `dmPolicy="open"` configs missing wildcard allowlists and write channel-correct repair paths (including `channels.googlechat.dm.allowFrom`) so `openclaw doctor --fix` no longer leaves Google Chat configs invalid after attempted repair. (#18544) - CLI/Doctor: detect gateway service token drift when the gateway token is only provided via environment variables, keeping service repairs aligned after token rotation. +- CLI/Update: run a standalone restart helper after updates, honoring service-name overrides and reporting restart initiation separately from confirmed restarts. (#18050) - CLI/Daemon: warn when a gateway restart sees a stale service token so users can reinstall with `openclaw gateway install --force`, and skip drift warnings for non-gateway service restarts. (#18018) - CLI/Status: fix `openclaw status --all` token summaries for bot-token-only channels so Mattermost/Zalo no longer show a bot+app warning. (#18527) Thanks @echo931. - CLI/Configure: make the `/model picker` allowlist prompt searchable with tokenized matching in `openclaw configure` so users can filter huge model lists by typing terms like `gpt-5.2 openai/`. (#19010) Thanks @bjesuiter. diff --git a/src/cli/update-cli/restart-helper.test.ts b/src/cli/update-cli/restart-helper.test.ts index 9755e5a77..ddeb8df34 100644 --- a/src/cli/update-cli/restart-helper.test.ts +++ b/src/cli/update-cli/restart-helper.test.ts @@ -41,6 +41,22 @@ describe("restart-helper", () => { } }); + it("uses OPENCLAW_SYSTEMD_UNIT override for systemd scripts", async () => { + Object.defineProperty(process, "platform", { value: "linux" }); + const scriptPath = await prepareRestartScript({ + OPENCLAW_PROFILE: "default", + OPENCLAW_SYSTEMD_UNIT: "custom-gateway", + }); + + expect(scriptPath).toBeTruthy(); + const content = await fs.readFile(scriptPath!, "utf-8"); + expect(content).toContain("systemctl --user restart 'custom-gateway.service'"); + + if (scriptPath) { + await fs.unlink(scriptPath); + } + }); + it("creates a launchd restart script on macOS", async () => { Object.defineProperty(process, "platform", { value: "darwin" }); process.getuid = () => 501; @@ -62,6 +78,24 @@ describe("restart-helper", () => { } }); + it("uses OPENCLAW_LAUNCHD_LABEL override on macOS", async () => { + Object.defineProperty(process, "platform", { value: "darwin" }); + process.getuid = () => 501; + + const scriptPath = await prepareRestartScript({ + OPENCLAW_PROFILE: "default", + OPENCLAW_LAUNCHD_LABEL: "com.custom.openclaw", + }); + + expect(scriptPath).toBeTruthy(); + const content = await fs.readFile(scriptPath!, "utf-8"); + expect(content).toContain("launchctl kickstart -k 'gui/501/com.custom.openclaw'"); + + if (scriptPath) { + await fs.unlink(scriptPath); + } + }); + it("creates a schtasks restart script on Windows", async () => { Object.defineProperty(process, "platform", { value: "win32" }); @@ -84,6 +118,24 @@ describe("restart-helper", () => { } }); + it("uses OPENCLAW_WINDOWS_TASK_NAME override on Windows", async () => { + Object.defineProperty(process, "platform", { value: "win32" }); + + const scriptPath = await prepareRestartScript({ + OPENCLAW_PROFILE: "default", + OPENCLAW_WINDOWS_TASK_NAME: "OpenClaw Gateway (custom)", + }); + + expect(scriptPath).toBeTruthy(); + const content = await fs.readFile(scriptPath!, "utf-8"); + expect(content).toContain('schtasks /End /TN "OpenClaw Gateway (custom)"'); + expect(content).toContain('schtasks /Run /TN "OpenClaw Gateway (custom)"'); + + if (scriptPath) { + await fs.unlink(scriptPath); + } + }); + it("uses custom profile in service names", async () => { Object.defineProperty(process, "platform", { value: "linux" }); const scriptPath = await prepareRestartScript({ diff --git a/src/cli/update-cli/restart-helper.ts b/src/cli/update-cli/restart-helper.ts index 4ac822319..d8f828af0 100644 --- a/src/cli/update-cli/restart-helper.ts +++ b/src/cli/update-cli/restart-helper.ts @@ -23,6 +23,30 @@ function isBatchSafe(value: string): boolean { return /^[A-Za-z0-9 _\-().]+$/.test(value); } +function resolveSystemdUnit(env: NodeJS.ProcessEnv): string { + const override = env.OPENCLAW_SYSTEMD_UNIT?.trim(); + if (override) { + return override.endsWith(".service") ? override : `${override}.service`; + } + return `${resolveGatewaySystemdServiceName(env.OPENCLAW_PROFILE)}.service`; +} + +function resolveLaunchdLabel(env: NodeJS.ProcessEnv): string { + const override = env.OPENCLAW_LAUNCHD_LABEL?.trim(); + if (override) { + return override; + } + return resolveGatewayLaunchAgentLabel(env.OPENCLAW_PROFILE); +} + +function resolveWindowsTaskName(env: NodeJS.ProcessEnv): string { + const override = env.OPENCLAW_WINDOWS_TASK_NAME?.trim(); + if (override) { + return override; + } + return resolveGatewayWindowsTaskName(env.OPENCLAW_PROFILE); +} + /** * Prepares a standalone script to restart the gateway service. * This script is written to a temporary directory and does not depend on @@ -41,8 +65,8 @@ export async function prepareRestartScript( try { if (platform === "linux") { - const serviceName = resolveGatewaySystemdServiceName(env.OPENCLAW_PROFILE); - const escaped = shellEscape(`${serviceName}.service`); + const unitName = resolveSystemdUnit(env); + const escaped = shellEscape(unitName); filename = `openclaw-restart-${timestamp}.sh`; scriptContent = `#!/bin/sh # Standalone restart script — survives parent process termination. @@ -53,7 +77,7 @@ systemctl --user restart '${escaped}' rm -f "$0" `; } else if (platform === "darwin") { - const label = resolveGatewayLaunchAgentLabel(env.OPENCLAW_PROFILE); + const label = resolveLaunchdLabel(env); const escaped = shellEscape(label); // Fallback to 501 if getuid is not available (though it should be on macOS) const uid = process.getuid ? process.getuid() : 501; @@ -67,7 +91,7 @@ launchctl kickstart -k 'gui/${uid}/${escaped}' rm -f "$0" `; } else if (platform === "win32") { - const taskName = resolveGatewayWindowsTaskName(env.OPENCLAW_PROFILE); + const taskName = resolveWindowsTaskName(env); if (!isBatchSafe(taskName)) { return null; } diff --git a/src/cli/update-cli/update-command.ts b/src/cli/update-cli/update-command.ts index f9293ad9b..87f36a9e4 100644 --- a/src/cli/update-cli/update-command.ts +++ b/src/cli/update-cli/update-command.ts @@ -400,9 +400,10 @@ async function maybeRestartService(params: { try { let restarted = false; + let restartInitiated = false; if (params.restartScriptPath) { await runRestartScript(params.restartScriptPath); - restarted = true; + restartInitiated = true; } else { restarted = await runDaemonRestart(); } @@ -423,6 +424,16 @@ async function maybeRestartService(params: { delete process.env.OPENCLAW_UPDATE_IN_PROGRESS; } } + + if (!params.opts.json && restartInitiated) { + defaultRuntime.log(theme.success("Daemon restart initiated.")); + defaultRuntime.log( + theme.muted( + `Verify with \`${replaceCliName(formatCliCommand("openclaw gateway status"), CLI_NAME)}\` once the gateway is back.`, + ), + ); + defaultRuntime.log(""); + } } catch (err) { if (!params.opts.json) { defaultRuntime.log(theme.warn(`Daemon restart failed: ${String(err)}`));