mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +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: 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: 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: 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)
|
||||
|
||||
@@ -9,6 +9,9 @@ const mocks = vi.hoisted(() => ({
|
||||
buildGatewayInstallPlan: vi.fn(),
|
||||
resolveGatewayPort: vi.fn(() => 18789),
|
||||
resolveIsNixMode: vi.fn(() => false),
|
||||
findExtraGatewayServices: vi.fn().mockResolvedValue([]),
|
||||
renderGatewayServiceCleanupHints: vi.fn().mockReturnValue([]),
|
||||
uninstallLegacySystemdUnits: vi.fn().mockResolvedValue([]),
|
||||
note: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -18,8 +21,8 @@ vi.mock("../config/paths.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../daemon/inspect.js", () => ({
|
||||
findExtraGatewayServices: vi.fn().mockResolvedValue([]),
|
||||
renderGatewayServiceCleanupHints: vi.fn().mockReturnValue([]),
|
||||
findExtraGatewayServices: mocks.findExtraGatewayServices,
|
||||
renderGatewayServiceCleanupHints: mocks.renderGatewayServiceCleanupHints,
|
||||
}));
|
||||
|
||||
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", () => ({
|
||||
note: mocks.note,
|
||||
}));
|
||||
@@ -50,7 +57,10 @@ vi.mock("./daemon-install-helpers.js", () => ({
|
||||
buildGatewayInstallPlan: mocks.buildGatewayInstallPlan,
|
||||
}));
|
||||
|
||||
import { maybeRepairGatewayServiceConfig } from "./doctor-gateway-services.js";
|
||||
import {
|
||||
maybeRepairGatewayServiceConfig,
|
||||
maybeScanExtraGatewayServices,
|
||||
} from "./doctor-gateway-services.js";
|
||||
|
||||
function makeDoctorIo() {
|
||||
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 type { OpenClawConfig } from "../config/config.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 {
|
||||
auditGatewayServiceConfig,
|
||||
@@ -13,6 +17,7 @@ import {
|
||||
SERVICE_AUDIT_CODES,
|
||||
} from "../daemon/service-audit.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { uninstallLegacySystemdUnits } from "../daemon/systemd.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { note } from "../terminal/note.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(
|
||||
cfg: OpenClawConfig,
|
||||
mode: "local" | "remote",
|
||||
@@ -246,27 +340,21 @@ export async function maybeScanExtraGatewayServices(
|
||||
});
|
||||
if (shouldRemove) {
|
||||
const removed: string[] = [];
|
||||
const failed: string[] = [];
|
||||
for (const svc of legacyServices) {
|
||||
if (svc.platform !== "darwin") {
|
||||
failed.push(`${svc.label} (${svc.platform})`);
|
||||
continue;
|
||||
}
|
||||
if (svc.scope !== "user") {
|
||||
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);
|
||||
const { darwinUserServices, linuxUserServices, failed } =
|
||||
classifyLegacyServices(legacyServices);
|
||||
|
||||
if (darwinUserServices.length > 0) {
|
||||
const result = await cleanupLegacyDarwinServices(darwinUserServices);
|
||||
removed.push(...result.removed);
|
||||
failed.push(...result.failed);
|
||||
}
|
||||
|
||||
if (linuxUserServices.length > 0) {
|
||||
const result = await cleanupLegacyLinuxUserServices(linuxUserServices, runtime);
|
||||
removed.push(...result.removed);
|
||||
failed.push(...result.failed);
|
||||
}
|
||||
|
||||
if (removed.length > 0) {
|
||||
note(removed.map((line) => `- ${line}`).join("\n"), "Legacy gateway removed");
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
GATEWAY_LAUNCH_AGENT_LABEL,
|
||||
GATEWAY_SYSTEMD_SERVICE_NAME,
|
||||
GATEWAY_WINDOWS_TASK_NAME,
|
||||
LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES,
|
||||
normalizeGatewayProfile,
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
resolveGatewayProfileSuffix,
|
||||
@@ -128,3 +129,10 @@ describe("resolveGatewayServiceDescription", () => {
|
||||
).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_WINDOWS_TASK_SCRIPT_NAME = "node.cmd";
|
||||
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 function normalizeGatewayProfile(profile?: string): string | null {
|
||||
|
||||
Reference in New Issue
Block a user