fix: add gateway stop/restart commands
This commit is contained in:
@@ -4,7 +4,6 @@ import { defaultRuntime } from "../runtime.js";
|
||||
import type { GatewayRpcOpts } from "./gateway-rpc.js";
|
||||
import { addGatewayClientOptions, callGatewayFromCli } from "./gateway-rpc.js";
|
||||
|
||||
|
||||
async function warnIfCronSchedulerDisabled(opts: GatewayRpcOpts) {
|
||||
try {
|
||||
const res = (await callGatewayFromCli("cron.status", opts, {})) as {
|
||||
|
||||
@@ -13,6 +13,9 @@ const forceFreePortAndWait = vi.fn(async () => ({
|
||||
waitedMs: 0,
|
||||
escalatedToSigkill: false,
|
||||
}));
|
||||
const serviceStop = vi.fn().mockResolvedValue(undefined);
|
||||
const serviceRestart = vi.fn().mockResolvedValue(undefined);
|
||||
const serviceIsLoaded = vi.fn().mockResolvedValue(true);
|
||||
|
||||
const runtimeLogs: string[] = [];
|
||||
const runtimeErrors: string[] = [];
|
||||
@@ -74,6 +77,20 @@ vi.mock("./ports.js", () => ({
|
||||
forceFreePortAndWait: (port: number) => forceFreePortAndWait(port),
|
||||
}));
|
||||
|
||||
vi.mock("../daemon/service.js", () => ({
|
||||
resolveGatewayService: () => ({
|
||||
label: "LaunchAgent",
|
||||
loadedText: "loaded",
|
||||
notLoadedText: "not loaded",
|
||||
install: vi.fn(),
|
||||
uninstall: vi.fn(),
|
||||
stop: serviceStop,
|
||||
restart: serviceRestart,
|
||||
isLoaded: serviceIsLoaded,
|
||||
readCommand: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("gateway-cli coverage", () => {
|
||||
it("registers call/health/status/send/agent commands and routes to callGateway", async () => {
|
||||
runtimeLogs.length = 0;
|
||||
@@ -228,6 +245,51 @@ describe("gateway-cli coverage", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("supports gateway stop/restart via service helper", async () => {
|
||||
runtimeLogs.length = 0;
|
||||
runtimeErrors.length = 0;
|
||||
serviceStop.mockClear();
|
||||
serviceRestart.mockClear();
|
||||
serviceIsLoaded.mockResolvedValue(true);
|
||||
|
||||
const { registerGatewayCli } = await import("./gateway-cli.js");
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerGatewayCli(program);
|
||||
|
||||
await program.parseAsync(["gateway", "stop"], { from: "user" });
|
||||
await program.parseAsync(["gateway", "restart"], { from: "user" });
|
||||
|
||||
expect(serviceStop).toHaveBeenCalledTimes(1);
|
||||
expect(serviceRestart).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("prints stop hints on GatewayLockError when service is loaded", async () => {
|
||||
runtimeLogs.length = 0;
|
||||
runtimeErrors.length = 0;
|
||||
serviceIsLoaded.mockResolvedValue(true);
|
||||
|
||||
const { GatewayLockError } = await import("../infra/gateway-lock.js");
|
||||
startGatewayServer.mockRejectedValueOnce(
|
||||
new GatewayLockError("another gateway instance is already listening"),
|
||||
);
|
||||
|
||||
const { registerGatewayCli } = await import("./gateway-cli.js");
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerGatewayCli(program);
|
||||
|
||||
await expect(
|
||||
program.parseAsync(["gateway", "--allow-unconfigured"], {
|
||||
from: "user",
|
||||
}),
|
||||
).rejects.toThrow("__exit__:1");
|
||||
|
||||
expect(startGatewayServer).toHaveBeenCalled();
|
||||
expect(runtimeErrors.join("\n")).toContain("Gateway failed to start:");
|
||||
expect(runtimeErrors.join("\n")).toContain("clawdbot gateway stop");
|
||||
});
|
||||
|
||||
it("uses env/config port when --port is omitted", async () => {
|
||||
await withEnvOverride({ CLAWDBOT_GATEWAY_PORT: "19001" }, async () => {
|
||||
runtimeLogs.length = 0;
|
||||
|
||||
@@ -6,6 +6,12 @@ import {
|
||||
loadConfig,
|
||||
resolveGatewayPort,
|
||||
} from "../config/config.js";
|
||||
import {
|
||||
GATEWAY_LAUNCH_AGENT_LABEL,
|
||||
GATEWAY_SYSTEMD_SERVICE_NAME,
|
||||
GATEWAY_WINDOWS_TASK_NAME,
|
||||
} from "../daemon/constants.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
||||
import { startGatewayServer } from "../gateway/server.js";
|
||||
import {
|
||||
@@ -45,6 +51,62 @@ function parsePort(raw: unknown): number | null {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function renderGatewayServiceStopHints(): string[] {
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
return [
|
||||
"Tip: clawdbot gateway stop",
|
||||
`Or: launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}`,
|
||||
];
|
||||
case "linux":
|
||||
return [
|
||||
"Tip: clawdbot gateway stop",
|
||||
`Or: systemctl --user stop ${GATEWAY_SYSTEMD_SERVICE_NAME}.service`,
|
||||
];
|
||||
case "win32":
|
||||
return [
|
||||
"Tip: clawdbot gateway stop",
|
||||
`Or: schtasks /End /TN "${GATEWAY_WINDOWS_TASK_NAME}"`,
|
||||
];
|
||||
default:
|
||||
return ["Tip: clawdbot gateway stop"];
|
||||
}
|
||||
}
|
||||
|
||||
function renderGatewayServiceStartHints(): string[] {
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
return [
|
||||
`launchctl bootstrap gui/$UID ~/Library/LaunchAgents/${GATEWAY_LAUNCH_AGENT_LABEL}.plist`,
|
||||
];
|
||||
case "linux":
|
||||
return [`systemctl --user start ${GATEWAY_SYSTEMD_SERVICE_NAME}.service`];
|
||||
case "win32":
|
||||
return [`schtasks /Run /TN "${GATEWAY_WINDOWS_TASK_NAME}"`];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function maybeExplainGatewayServiceStop() {
|
||||
const service = resolveGatewayService();
|
||||
let loaded: boolean | null = null;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
} catch {
|
||||
loaded = null;
|
||||
}
|
||||
if (loaded === false) return;
|
||||
defaultRuntime.error(
|
||||
loaded
|
||||
? `Gateway service appears ${service.loadedText}. Stop it first.`
|
||||
: "Gateway service status unknown; if supervised, stop it first.",
|
||||
);
|
||||
for (const hint of renderGatewayServiceStopHints()) {
|
||||
defaultRuntime.error(hint);
|
||||
}
|
||||
}
|
||||
|
||||
async function runGatewayLoop(params: {
|
||||
start: () => Promise<Awaited<ReturnType<typeof startGatewayServer>>>;
|
||||
runtime: typeof defaultRuntime;
|
||||
@@ -285,8 +347,22 @@ export function registerGatewayCli(program: Command) {
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof GatewayLockError) {
|
||||
defaultRuntime.error(`Gateway failed to start: ${err.message}`);
|
||||
if (
|
||||
err instanceof GatewayLockError ||
|
||||
(err &&
|
||||
typeof err === "object" &&
|
||||
(err as { name?: string }).name === "GatewayLockError")
|
||||
) {
|
||||
const errMessage =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: typeof err === "object" && err !== null && "message" in err
|
||||
? String((err as { message?: unknown }).message ?? "")
|
||||
: String(err);
|
||||
defaultRuntime.error(
|
||||
`Gateway failed to start: ${errMessage}\nIf the gateway is supervised, stop it with: clawdbot gateway stop`,
|
||||
);
|
||||
await maybeExplainGatewayServiceStop();
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
@@ -486,8 +562,22 @@ export function registerGatewayCli(program: Command) {
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof GatewayLockError) {
|
||||
defaultRuntime.error(`Gateway failed to start: ${err.message}`);
|
||||
if (
|
||||
err instanceof GatewayLockError ||
|
||||
(err &&
|
||||
typeof err === "object" &&
|
||||
(err as { name?: string }).name === "GatewayLockError")
|
||||
) {
|
||||
const errMessage =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: typeof err === "object" && err !== null && "message" in err
|
||||
? String((err as { message?: unknown }).message ?? "")
|
||||
: String(err);
|
||||
defaultRuntime.error(
|
||||
`Gateway failed to start: ${errMessage}\nIf the gateway is supervised, stop it with: clawdbot gateway stop`,
|
||||
);
|
||||
await maybeExplainGatewayServiceStop();
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
@@ -635,6 +725,59 @@ export function registerGatewayCli(program: Command) {
|
||||
}),
|
||||
);
|
||||
|
||||
gateway
|
||||
.command("stop")
|
||||
.description("Stop the Gateway service (launchd/systemd/schtasks)")
|
||||
.action(async () => {
|
||||
const service = resolveGatewayService();
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
if (!loaded) {
|
||||
defaultRuntime.log(`Gateway service ${service.notLoadedText}.`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await service.stop({ stdout: process.stdout });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway stop failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
gateway
|
||||
.command("restart")
|
||||
.description("Restart the Gateway service (launchd/systemd/schtasks)")
|
||||
.action(async () => {
|
||||
const service = resolveGatewayService();
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
if (!loaded) {
|
||||
defaultRuntime.log(`Gateway service ${service.notLoadedText}.`);
|
||||
for (const hint of renderGatewayServiceStartHints()) {
|
||||
defaultRuntime.log(`Start with: ${hint}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await service.restart({ stdout: process.stdout });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway restart failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Build default deps (keeps parity with other commands; future-proofing).
|
||||
void createDefaultDeps();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user