fix: migrate legacy gateway services

This commit is contained in:
Peter Steinberger
2026-01-30 04:01:31 +01:00
parent d47b4e6f81
commit 02576615cb
11 changed files with 165 additions and 27 deletions

View File

@@ -64,10 +64,11 @@ export function formatCliBannerLine(version: string, options: BannerOptions = {}
const LOBSTER_ASCII = [
"▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄",
"█████░█████░█████░█░░░█░█████░█░░░░░███░█░░░█",
"█░░░█░█░░░█░███░░░██░░█░█░░░░░█░░░░░█░░░█░█░░█",
"█████░████░░█████░█░░██░█████░█████░█████░█░██",
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀",
"█████░█████░█████░█░░░█░█████░█░░░░░█████░█░░░█",
"█░░░█░█░░░█░█░░░░░██░░█░█░░░░░█░░░░░█░░░█░█░░█",
"█░░░█░█████░████░░█░█░█░█░░░░░█░░░░░█████░█░██",
"█░░░█░█░░░░░█░░░░░█░░██░█░░░░░█░░░░░█░░░█░██░██",
"█████░█░░░░░█████░█░░░█░█████░█████░█░░░█░█░░░█",
" 🦞 OPENCLAW 🦞 ",
" ",
];

View File

@@ -1,4 +1,8 @@
import { execFile } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { promisify } from "node:util";
import type { OpenClawConfig } from "../config/config.js";
import { resolveGatewayPort, resolveIsNixMode } from "../config/paths.js";
@@ -16,6 +20,8 @@ import { buildGatewayInstallPlan } from "./daemon-install-helpers.js";
import { DEFAULT_GATEWAY_DAEMON_RUNTIME, type GatewayDaemonRuntime } from "./daemon-runtime.js";
import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js";
const execFileAsync = promisify(execFile);
function detectGatewayRuntime(programArguments: string[] | undefined): GatewayDaemonRuntime {
const first = programArguments?.[0];
if (first) {
@@ -37,6 +43,42 @@ function normalizeExecutablePath(value: string): string {
return path.resolve(value);
}
function extractDetailPath(detail: string, prefix: string): string | null {
if (!detail.startsWith(prefix)) return null;
const value = detail.slice(prefix.length).trim();
return value.length > 0 ? value : null;
}
async function cleanupLegacyLaunchdService(params: {
label: string;
plistPath: string;
}): Promise<string | null> {
const domain = typeof process.getuid === "function" ? `gui/${process.getuid()}` : "gui/501";
await execFileAsync("launchctl", ["bootout", domain, params.plistPath]).catch(() => undefined);
await execFileAsync("launchctl", ["unload", params.plistPath]).catch(() => undefined);
const trashDir = path.join(os.homedir(), ".Trash");
try {
await fs.mkdir(trashDir, { recursive: true });
} catch {
// ignore
}
try {
await fs.access(params.plistPath);
} catch {
return null;
}
const dest = path.join(trashDir, `${params.label}-${Date.now()}.plist`);
try {
await fs.rename(params.plistPath, dest);
return dest;
} catch {
return null;
}
}
export async function maybeRepairGatewayServiceConfig(
cfg: OpenClawConfig,
mode: "local" | "remote",
@@ -150,7 +192,11 @@ export async function maybeRepairGatewayServiceConfig(
}
}
export async function maybeScanExtraGatewayServices(options: DoctorOptions) {
export async function maybeScanExtraGatewayServices(
options: DoctorOptions,
runtime: RuntimeEnv,
prompter: DoctorPrompter,
) {
const extraServices = await findExtraGatewayServices(process.env, {
deep: options.deep,
});
@@ -161,6 +207,47 @@ export async function maybeScanExtraGatewayServices(options: DoctorOptions) {
"Other gateway-like services detected",
);
const legacyServices = extraServices.filter((svc) => svc.legacy === true);
if (legacyServices.length > 0) {
const shouldRemove = await prompter.confirmSkipInNonInteractive({
message: "Remove legacy gateway services (clawdbot/moltbot) now?",
initialValue: true,
});
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);
}
if (removed.length > 0) {
note(removed.map((line) => `- ${line}`).join("\n"), "Legacy gateway removed");
}
if (failed.length > 0) {
note(failed.map((line) => `- ${line}`).join("\n"), "Legacy gateway cleanup skipped");
}
if (removed.length > 0) {
runtime.log("Legacy gateway services removed. Installing OpenClaw gateway next.");
}
}
}
const cleanupHints = renderGatewayServiceCleanupHints();
if (cleanupHints.length > 0) {
note(cleanupHints.map((hint) => `- ${hint}`).join("\n"), "Cleanup hints");

View File

@@ -185,7 +185,7 @@ export async function doctorCommand(
cfg = await maybeRepairSandboxImages(cfg, runtime, prompter);
noteSandboxScopeWarnings(cfg);
await maybeScanExtraGatewayServices(options);
await maybeScanExtraGatewayServices(options, runtime, prompter);
await maybeRepairGatewayServiceConfig(cfg, resolveMode(cfg), runtime, prompter);
await noteMacLaunchAgentOverrides();
await noteMacLaunchctlGatewayEnvOverrides(cfg);

View File

@@ -65,9 +65,11 @@ export function randomToken(): string {
export function printWizardHeader(runtime: RuntimeEnv) {
const header = [
"▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄",
"█████░█████░█████░█░░░█░█████░█░░░░░███░█░░░█",
"█░░░█░█░░░█░███░░░██░░█░█░░░░░█░░░░░█░░░█░█░░█",
"█████░████░░█████░█░░██░█████░█████░█████░█░██",
"█████░█████░█████░█░░░█░█████░█░░░░░█████░█░░░█",
"█░░░█░█░░░█░█░░░░░██░░█░█░░░░░█░░░░░█░░░█░█░░█",
"█░░░█░█████░████░░█░█░█░█░░░░░█░░░░░█████░█░██",
"█░░░█░█░░░░░█░░░░░█░░██░█░░░░░█░░░░░█░░░█░██░██",
"█████░█░░░░░█████░█░░░█░█████░█████░█░░░█░█░░░█",
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀",
" 🦞 FRESH DAILY 🦞 ",
" ",

View File

@@ -16,13 +16,15 @@ export type ExtraGatewayService = {
label: string;
detail: string;
scope: "user" | "system";
marker?: "openclaw" | "clawdbot" | "moltbot";
legacy?: boolean;
};
export type FindExtraGatewayServicesOptions = {
deep?: boolean;
};
const EXTRA_MARKERS = ["openclaw"];
const EXTRA_MARKERS = ["openclaw", "clawdbot", "moltbot"] as const;
const execFileAsync = promisify(execFile);
export function renderGatewayServiceCleanupHints(
@@ -56,9 +58,14 @@ function resolveHomeDir(env: Record<string, string | undefined>): string {
return home;
}
function containsMarker(content: string): boolean {
type Marker = (typeof EXTRA_MARKERS)[number];
function detectMarker(content: string): Marker | null {
const lower = content.toLowerCase();
return EXTRA_MARKERS.some((marker) => lower.includes(marker));
for (const marker of EXTRA_MARKERS) {
if (lower.includes(marker)) return marker;
}
return null;
}
function hasGatewayServiceMarker(content: string): boolean {
@@ -111,6 +118,11 @@ function isIgnoredSystemdName(name: string): boolean {
return name === resolveGatewaySystemdServiceName();
}
function isLegacyLabel(label: string): boolean {
const lower = label.toLowerCase();
return lower.includes("clawdbot") || lower.includes("moltbot");
}
async function scanLaunchdDir(params: {
dir: string;
scope: "user" | "system";
@@ -134,15 +146,18 @@ async function scanLaunchdDir(params: {
} catch {
continue;
}
if (!containsMarker(contents)) continue;
const marker = detectMarker(contents);
if (!marker) continue;
const label = tryExtractPlistLabel(contents) ?? labelFromName;
if (isIgnoredLaunchdLabel(label)) continue;
if (isOpenClawGatewayLaunchdService(label, contents)) continue;
if (marker === "openclaw" && isOpenClawGatewayLaunchdService(label, contents)) continue;
results.push({
platform: "darwin",
label,
detail: `plist: ${fullPath}`,
scope: params.scope,
marker,
legacy: marker !== "openclaw" || isLegacyLabel(label),
});
}
@@ -172,13 +187,16 @@ async function scanSystemdDir(params: {
} catch {
continue;
}
if (!containsMarker(contents)) continue;
if (isOpenClawGatewaySystemdService(name, contents)) continue;
const marker = detectMarker(contents);
if (!marker) continue;
if (marker === "openclaw" && isOpenClawGatewaySystemdService(name, contents)) continue;
results.push({
platform: "linux",
label: entry,
detail: `unit: ${fullPath}`,
scope: params.scope,
marker,
legacy: marker !== "openclaw",
});
}
@@ -336,15 +354,21 @@ export async function findExtraGatewayServices(
if (isOpenClawGatewayTaskName(name)) continue;
const lowerName = name.toLowerCase();
const lowerCommand = task.taskToRun?.toLowerCase() ?? "";
const matches = EXTRA_MARKERS.some(
(marker) => lowerName.includes(marker) || lowerCommand.includes(marker),
);
if (!matches) continue;
let marker: Marker | null = null;
for (const candidate of EXTRA_MARKERS) {
if (lowerName.includes(candidate) || lowerCommand.includes(candidate)) {
marker = candidate;
break;
}
}
if (!marker) continue;
push({
platform: "win32",
label: name,
detail: task.taskToRun ? `task: ${name}, run: ${task.taskToRun}` : name,
scope: "system",
marker,
legacy: marker !== "openclaw",
});
}
return results;