mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 04:40:43 +00:00
doctor: clean up legacy Linux gateway services (#21188)
* Doctor: clean up legacy Linux gateway services * doctor: refactor legacy service cleanup flow * doctor: fix legacy systemd cleanup map key typing * doctor: add changelog entry for legacy Linux service cleanup --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -627,6 +627,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: 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: 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/Doctor: detect gateway service token drift when the gateway token is only provided via environment variables, keeping service repairs aligned after token rotation.
|
||||||
|
- CLI/Doctor: clean up legacy Linux gateway services (`clawdbot`/`moltbot`) during `doctor --fix`, while keeping non-legacy user/system services untouched. (#21063) Thanks @Phineas1500.
|
||||||
- Gateway/Update: prevent restart crash loops after failed self-updates by restarting only on successful updates, stopping early on failed install/build steps, and running `openclaw doctor --fix` during updates to sanitize config. (#18131) Thanks @RamiNoodle733.
|
- Gateway/Update: prevent restart crash loops after failed self-updates by restarting only on successful updates, stopping early on failed install/build steps, and running `openclaw doctor --fix` during updates to sanitize config. (#18131) Thanks @RamiNoodle733.
|
||||||
- Gateway/Update: preserve update.run restart delivery context so post-update status replies route back to the initiating channel/thread. (#18267) Thanks @yinghaosang.
|
- Gateway/Update: preserve update.run restart delivery context so post-update status replies route back to the initiating channel/thread. (#18267) Thanks @yinghaosang.
|
||||||
- CLI/Update: run a standalone restart helper after updates, honoring service-name overrides and reporting restart initiation separately from confirmed restarts. (#18050)
|
- CLI/Update: run a standalone restart helper after updates, honoring service-name overrides and reporting restart initiation separately from confirmed restarts. (#18050)
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ const mocks = vi.hoisted(() => ({
|
|||||||
buildGatewayInstallPlan: vi.fn(),
|
buildGatewayInstallPlan: vi.fn(),
|
||||||
resolveGatewayPort: vi.fn(() => 18789),
|
resolveGatewayPort: vi.fn(() => 18789),
|
||||||
resolveIsNixMode: vi.fn(() => false),
|
resolveIsNixMode: vi.fn(() => false),
|
||||||
|
findExtraGatewayServices: vi.fn().mockResolvedValue([]),
|
||||||
|
renderGatewayServiceCleanupHints: vi.fn().mockReturnValue([]),
|
||||||
|
uninstallLegacySystemdUnits: vi.fn().mockResolvedValue([]),
|
||||||
note: vi.fn(),
|
note: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -18,8 +21,8 @@ vi.mock("../config/paths.js", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../daemon/inspect.js", () => ({
|
vi.mock("../daemon/inspect.js", () => ({
|
||||||
findExtraGatewayServices: vi.fn().mockResolvedValue([]),
|
findExtraGatewayServices: mocks.findExtraGatewayServices,
|
||||||
renderGatewayServiceCleanupHints: vi.fn().mockReturnValue([]),
|
renderGatewayServiceCleanupHints: mocks.renderGatewayServiceCleanupHints,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../daemon/runtime-paths.js", () => ({
|
vi.mock("../daemon/runtime-paths.js", () => ({
|
||||||
@@ -42,6 +45,10 @@ vi.mock("../daemon/service.js", () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("../daemon/systemd.js", () => ({
|
||||||
|
uninstallLegacySystemdUnits: mocks.uninstallLegacySystemdUnits,
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("../terminal/note.js", () => ({
|
vi.mock("../terminal/note.js", () => ({
|
||||||
note: mocks.note,
|
note: mocks.note,
|
||||||
}));
|
}));
|
||||||
@@ -50,7 +57,10 @@ vi.mock("./daemon-install-helpers.js", () => ({
|
|||||||
buildGatewayInstallPlan: mocks.buildGatewayInstallPlan,
|
buildGatewayInstallPlan: mocks.buildGatewayInstallPlan,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { maybeRepairGatewayServiceConfig } from "./doctor-gateway-services.js";
|
import {
|
||||||
|
maybeRepairGatewayServiceConfig,
|
||||||
|
maybeScanExtraGatewayServices,
|
||||||
|
} from "./doctor-gateway-services.js";
|
||||||
|
|
||||||
function makeDoctorIo() {
|
function makeDoctorIo() {
|
||||||
return { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
return { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
||||||
@@ -163,3 +173,58 @@ describe("maybeRepairGatewayServiceConfig", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("maybeScanExtraGatewayServices", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mocks.findExtraGatewayServices.mockResolvedValue([]);
|
||||||
|
mocks.renderGatewayServiceCleanupHints.mockReturnValue([]);
|
||||||
|
mocks.uninstallLegacySystemdUnits.mockResolvedValue([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes legacy Linux user systemd services", async () => {
|
||||||
|
mocks.findExtraGatewayServices.mockResolvedValue([
|
||||||
|
{
|
||||||
|
platform: "linux",
|
||||||
|
label: "moltbot-gateway.service",
|
||||||
|
detail: "unit: /home/test/.config/systemd/user/moltbot-gateway.service",
|
||||||
|
scope: "user",
|
||||||
|
legacy: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
mocks.uninstallLegacySystemdUnits.mockResolvedValue([
|
||||||
|
{
|
||||||
|
name: "moltbot-gateway",
|
||||||
|
unitPath: "/home/test/.config/systemd/user/moltbot-gateway.service",
|
||||||
|
enabled: true,
|
||||||
|
exists: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
||||||
|
const prompter = {
|
||||||
|
confirm: vi.fn(),
|
||||||
|
confirmRepair: vi.fn(),
|
||||||
|
confirmAggressive: vi.fn(),
|
||||||
|
confirmSkipInNonInteractive: vi.fn().mockResolvedValue(true),
|
||||||
|
select: vi.fn(),
|
||||||
|
shouldRepair: false,
|
||||||
|
shouldForce: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
await maybeScanExtraGatewayServices({ deep: false }, runtime, prompter);
|
||||||
|
|
||||||
|
expect(mocks.uninstallLegacySystemdUnits).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mocks.uninstallLegacySystemdUnits).toHaveBeenCalledWith({
|
||||||
|
env: process.env,
|
||||||
|
stdout: process.stdout,
|
||||||
|
});
|
||||||
|
expect(mocks.note).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("moltbot-gateway.service"),
|
||||||
|
"Legacy gateway removed",
|
||||||
|
);
|
||||||
|
expect(runtime.log).toHaveBeenCalledWith(
|
||||||
|
"Legacy gateway services removed. Installing OpenClaw gateway next.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import path from "node:path";
|
|||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { resolveGatewayPort, resolveIsNixMode } from "../config/paths.js";
|
import { resolveGatewayPort, resolveIsNixMode } from "../config/paths.js";
|
||||||
import { findExtraGatewayServices, renderGatewayServiceCleanupHints } from "../daemon/inspect.js";
|
import {
|
||||||
|
findExtraGatewayServices,
|
||||||
|
renderGatewayServiceCleanupHints,
|
||||||
|
type ExtraGatewayService,
|
||||||
|
} from "../daemon/inspect.js";
|
||||||
import { renderSystemNodeWarning, resolveSystemNodeInfo } from "../daemon/runtime-paths.js";
|
import { renderSystemNodeWarning, resolveSystemNodeInfo } from "../daemon/runtime-paths.js";
|
||||||
import {
|
import {
|
||||||
auditGatewayServiceConfig,
|
auditGatewayServiceConfig,
|
||||||
@@ -13,6 +17,7 @@ import {
|
|||||||
SERVICE_AUDIT_CODES,
|
SERVICE_AUDIT_CODES,
|
||||||
} from "../daemon/service-audit.js";
|
} from "../daemon/service-audit.js";
|
||||||
import { resolveGatewayService } from "../daemon/service.js";
|
import { resolveGatewayService } from "../daemon/service.js";
|
||||||
|
import { uninstallLegacySystemdUnits } from "../daemon/systemd.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { note } from "../terminal/note.js";
|
import { note } from "../terminal/note.js";
|
||||||
import { buildGatewayInstallPlan } from "./daemon-install-helpers.js";
|
import { buildGatewayInstallPlan } from "./daemon-install-helpers.js";
|
||||||
@@ -98,6 +103,95 @@ async function cleanupLegacyLaunchdService(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function classifyLegacyServices(legacyServices: ExtraGatewayService[]): {
|
||||||
|
darwinUserServices: ExtraGatewayService[];
|
||||||
|
linuxUserServices: ExtraGatewayService[];
|
||||||
|
failed: string[];
|
||||||
|
} {
|
||||||
|
const darwinUserServices: ExtraGatewayService[] = [];
|
||||||
|
const linuxUserServices: ExtraGatewayService[] = [];
|
||||||
|
const failed: string[] = [];
|
||||||
|
|
||||||
|
for (const svc of legacyServices) {
|
||||||
|
if (svc.platform === "darwin") {
|
||||||
|
if (svc.scope === "user") {
|
||||||
|
darwinUserServices.push(svc);
|
||||||
|
} else {
|
||||||
|
failed.push(`${svc.label} (${svc.scope})`);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (svc.platform === "linux") {
|
||||||
|
if (svc.scope === "user") {
|
||||||
|
linuxUserServices.push(svc);
|
||||||
|
} else {
|
||||||
|
failed.push(`${svc.label} (${svc.scope})`);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
failed.push(`${svc.label} (${svc.platform})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { darwinUserServices, linuxUserServices, failed };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupLegacyDarwinServices(
|
||||||
|
services: ExtraGatewayService[],
|
||||||
|
): Promise<{ removed: string[]; failed: string[] }> {
|
||||||
|
const removed: string[] = [];
|
||||||
|
const failed: string[] = [];
|
||||||
|
|
||||||
|
for (const svc of services) {
|
||||||
|
const plistPath = extractDetailPath(svc.detail, "plist:");
|
||||||
|
if (!plistPath) {
|
||||||
|
failed.push(`${svc.label} (missing plist path)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const dest = await cleanupLegacyLaunchdService({
|
||||||
|
label: svc.label,
|
||||||
|
plistPath,
|
||||||
|
});
|
||||||
|
removed.push(dest ? `${svc.label} -> ${dest}` : svc.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { removed, failed };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupLegacyLinuxUserServices(
|
||||||
|
services: ExtraGatewayService[],
|
||||||
|
runtime: RuntimeEnv,
|
||||||
|
): Promise<{ removed: string[]; failed: string[] }> {
|
||||||
|
const removed: string[] = [];
|
||||||
|
const failed: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const removedUnits = await uninstallLegacySystemdUnits({
|
||||||
|
env: process.env,
|
||||||
|
stdout: process.stdout,
|
||||||
|
});
|
||||||
|
const removedByLabel: Map<string, (typeof removedUnits)[number]> = new Map(
|
||||||
|
removedUnits.map((unit) => [`${unit.name}.service`, unit] as const),
|
||||||
|
);
|
||||||
|
for (const svc of services) {
|
||||||
|
const removedUnit = removedByLabel.get(svc.label);
|
||||||
|
if (!removedUnit) {
|
||||||
|
failed.push(`${svc.label} (legacy unit name not recognized)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
removed.push(`${svc.label} -> ${removedUnit.unitPath}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
runtime.error(`Legacy Linux gateway cleanup failed: ${String(err)}`);
|
||||||
|
for (const svc of services) {
|
||||||
|
failed.push(`${svc.label} (linux cleanup failed)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { removed, failed };
|
||||||
|
}
|
||||||
|
|
||||||
export async function maybeRepairGatewayServiceConfig(
|
export async function maybeRepairGatewayServiceConfig(
|
||||||
cfg: OpenClawConfig,
|
cfg: OpenClawConfig,
|
||||||
mode: "local" | "remote",
|
mode: "local" | "remote",
|
||||||
@@ -246,27 +340,21 @@ export async function maybeScanExtraGatewayServices(
|
|||||||
});
|
});
|
||||||
if (shouldRemove) {
|
if (shouldRemove) {
|
||||||
const removed: string[] = [];
|
const removed: string[] = [];
|
||||||
const failed: string[] = [];
|
const { darwinUserServices, linuxUserServices, failed } =
|
||||||
for (const svc of legacyServices) {
|
classifyLegacyServices(legacyServices);
|
||||||
if (svc.platform !== "darwin") {
|
|
||||||
failed.push(`${svc.label} (${svc.platform})`);
|
if (darwinUserServices.length > 0) {
|
||||||
continue;
|
const result = await cleanupLegacyDarwinServices(darwinUserServices);
|
||||||
}
|
removed.push(...result.removed);
|
||||||
if (svc.scope !== "user") {
|
failed.push(...result.failed);
|
||||||
failed.push(`${svc.label} (${svc.scope})`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const plistPath = extractDetailPath(svc.detail, "plist:");
|
|
||||||
if (!plistPath) {
|
|
||||||
failed.push(`${svc.label} (missing plist path)`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const dest = await cleanupLegacyLaunchdService({
|
|
||||||
label: svc.label,
|
|
||||||
plistPath,
|
|
||||||
});
|
|
||||||
removed.push(dest ? `${svc.label} -> ${dest}` : svc.label);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (linuxUserServices.length > 0) {
|
||||||
|
const result = await cleanupLegacyLinuxUserServices(linuxUserServices, runtime);
|
||||||
|
removed.push(...result.removed);
|
||||||
|
failed.push(...result.failed);
|
||||||
|
}
|
||||||
|
|
||||||
if (removed.length > 0) {
|
if (removed.length > 0) {
|
||||||
note(removed.map((line) => `- ${line}`).join("\n"), "Legacy gateway removed");
|
note(removed.map((line) => `- ${line}`).join("\n"), "Legacy gateway removed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
GATEWAY_LAUNCH_AGENT_LABEL,
|
GATEWAY_LAUNCH_AGENT_LABEL,
|
||||||
GATEWAY_SYSTEMD_SERVICE_NAME,
|
GATEWAY_SYSTEMD_SERVICE_NAME,
|
||||||
GATEWAY_WINDOWS_TASK_NAME,
|
GATEWAY_WINDOWS_TASK_NAME,
|
||||||
|
LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES,
|
||||||
normalizeGatewayProfile,
|
normalizeGatewayProfile,
|
||||||
resolveGatewayLaunchAgentLabel,
|
resolveGatewayLaunchAgentLabel,
|
||||||
resolveGatewayProfileSuffix,
|
resolveGatewayProfileSuffix,
|
||||||
@@ -128,3 +129,10 @@ describe("resolveGatewayServiceDescription", () => {
|
|||||||
).toBe("OpenClaw Gateway (profile: work, vremote)");
|
).toBe("OpenClaw Gateway (profile: work, vremote)");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES", () => {
|
||||||
|
it("includes known pre-rebrand gateway unit names", () => {
|
||||||
|
expect(LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES).toContain("clawdbot-gateway");
|
||||||
|
expect(LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES).toContain("moltbot-gateway");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ export const NODE_SERVICE_MARKER = "openclaw";
|
|||||||
export const NODE_SERVICE_KIND = "node";
|
export const NODE_SERVICE_KIND = "node";
|
||||||
export const NODE_WINDOWS_TASK_SCRIPT_NAME = "node.cmd";
|
export const NODE_WINDOWS_TASK_SCRIPT_NAME = "node.cmd";
|
||||||
export const LEGACY_GATEWAY_LAUNCH_AGENT_LABELS: string[] = [];
|
export const LEGACY_GATEWAY_LAUNCH_AGENT_LABELS: string[] = [];
|
||||||
export const LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES: string[] = [];
|
export const LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES: string[] = [
|
||||||
|
"clawdbot-gateway",
|
||||||
|
"moltbot-gateway",
|
||||||
|
];
|
||||||
export const LEGACY_GATEWAY_WINDOWS_TASK_NAMES: string[] = [];
|
export const LEGACY_GATEWAY_WINDOWS_TASK_NAMES: string[] = [];
|
||||||
|
|
||||||
export function normalizeGatewayProfile(profile?: string): string | null {
|
export function normalizeGatewayProfile(profile?: string): string | null {
|
||||||
|
|||||||
Reference in New Issue
Block a user