cli: gateway subcommands, drop ipc probes
This commit is contained in:
52
src/cli/ports.ts
Normal file
52
src/cli/ports.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
|
||||||
|
export type PortProcess = { pid: number; command?: string };
|
||||||
|
|
||||||
|
export function parseLsofOutput(output: string): PortProcess[] {
|
||||||
|
const lines = output.split(/\r?\n/).filter(Boolean);
|
||||||
|
const results: PortProcess[] = [];
|
||||||
|
let current: Partial<PortProcess> = {};
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith("p")) {
|
||||||
|
if (current.pid) results.push(current as PortProcess);
|
||||||
|
current = { pid: Number.parseInt(line.slice(1), 10) };
|
||||||
|
} else if (line.startsWith("c")) {
|
||||||
|
current.command = line.slice(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current.pid) results.push(current as PortProcess);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listPortListeners(port: number): PortProcess[] {
|
||||||
|
try {
|
||||||
|
const out = execFileSync(
|
||||||
|
"lsof",
|
||||||
|
["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-FpFc"],
|
||||||
|
{ encoding: "utf-8" },
|
||||||
|
);
|
||||||
|
return parseLsofOutput(out);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const status = (err as { status?: number }).status;
|
||||||
|
const code = (err as { code?: string }).code;
|
||||||
|
if (code === "ENOENT") {
|
||||||
|
throw new Error("lsof not found; required for --force");
|
||||||
|
}
|
||||||
|
if (status === 1) return []; // no listeners
|
||||||
|
throw err instanceof Error ? err : new Error(String(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function forceFreePort(port: number): PortProcess[] {
|
||||||
|
const listeners = listPortListeners(port);
|
||||||
|
for (const proc of listeners) {
|
||||||
|
try {
|
||||||
|
process.kill(proc.pid, "SIGTERM");
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
`failed to kill pid ${proc.pid}${proc.command ? ` (${proc.command})` : ""}: ${String(err)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners;
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import { execFileSync } from "node:child_process";
|
|
||||||
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
import { agentCommand } from "../commands/agent.js";
|
import { agentCommand } from "../commands/agent.js";
|
||||||
@@ -17,58 +15,12 @@ import { defaultRuntime } from "../runtime.js";
|
|||||||
import { VERSION } from "../version.js";
|
import { VERSION } from "../version.js";
|
||||||
import { startWebChatServer } from "../webchat/server.js";
|
import { startWebChatServer } from "../webchat/server.js";
|
||||||
import { createDefaultDeps } from "./deps.js";
|
import { createDefaultDeps } from "./deps.js";
|
||||||
|
import {
|
||||||
export type PortProcess = { pid: number; command?: string };
|
forceFreePort,
|
||||||
|
listPortListeners,
|
||||||
export function parseLsofOutput(output: string): PortProcess[] {
|
PortProcess,
|
||||||
const lines = output.split(/\r?\n/).filter(Boolean);
|
parseLsofOutput,
|
||||||
const results: PortProcess[] = [];
|
} from "./ports.js";
|
||||||
let current: Partial<PortProcess> = {};
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.startsWith("p")) {
|
|
||||||
if (current.pid) results.push(current as PortProcess);
|
|
||||||
current = { pid: Number.parseInt(line.slice(1), 10) };
|
|
||||||
} else if (line.startsWith("c")) {
|
|
||||||
current.command = line.slice(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (current.pid) results.push(current as PortProcess);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function listPortListeners(port: number): PortProcess[] {
|
|
||||||
try {
|
|
||||||
const out = execFileSync(
|
|
||||||
"lsof",
|
|
||||||
["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-FpFc"],
|
|
||||||
{ encoding: "utf-8" },
|
|
||||||
);
|
|
||||||
return parseLsofOutput(out);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const status = (err as { status?: number }).status;
|
|
||||||
const code = (err as { code?: string }).code;
|
|
||||||
if (code === "ENOENT") {
|
|
||||||
throw new Error("lsof not found; required for --force");
|
|
||||||
}
|
|
||||||
// lsof returns exit status 1 when no processes match
|
|
||||||
if (status === 1) return [];
|
|
||||||
throw err instanceof Error ? err : new Error(String(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function forceFreePort(port: number): PortProcess[] {
|
|
||||||
const listeners = listPortListeners(port);
|
|
||||||
for (const proc of listeners) {
|
|
||||||
try {
|
|
||||||
process.kill(proc.pid, "SIGTERM");
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(
|
|
||||||
`failed to kill pid ${proc.pid}${proc.command ? ` (${proc.command})` : ""}: ${String(err)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return listeners;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildProgram() {
|
export function buildProgram() {
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
@@ -386,6 +338,8 @@ Examples:
|
|||||||
// Only spawn if there is clearly no listener.
|
// Only spawn if there is clearly no listener.
|
||||||
const url = new URL(opts.url ?? "ws://127.0.0.1:18789");
|
const url = new URL(opts.url ?? "ws://127.0.0.1:18789");
|
||||||
const port = Number(url.port || 18789);
|
const port = Number(url.port || 18789);
|
||||||
|
const listeners = listPortListeners(port);
|
||||||
|
if (listeners.length > 0) throw err;
|
||||||
await startGatewayServer(port);
|
await startGatewayServer(port);
|
||||||
return await attempt();
|
return await attempt();
|
||||||
}
|
}
|
||||||
@@ -554,6 +508,11 @@ Examples:
|
|||||||
.option("--json", "Output JSON instead of text", false)
|
.option("--json", "Output JSON instead of text", false)
|
||||||
.option("--timeout <ms>", "Connection timeout in milliseconds", "10000")
|
.option("--timeout <ms>", "Connection timeout in milliseconds", "10000")
|
||||||
.option("--verbose", "Verbose logging", false)
|
.option("--verbose", "Verbose logging", false)
|
||||||
|
.option(
|
||||||
|
"--probe",
|
||||||
|
"Also attempt a live Baileys connect (can conflict if gateway is already connected)",
|
||||||
|
false,
|
||||||
|
)
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
setVerbose(Boolean(opts.verbose));
|
setVerbose(Boolean(opts.verbose));
|
||||||
const timeout = opts.timeout
|
const timeout = opts.timeout
|
||||||
@@ -568,7 +527,11 @@ Examples:
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await healthCommand(
|
await healthCommand(
|
||||||
{ json: Boolean(opts.json), timeoutMs: timeout },
|
{
|
||||||
|
json: Boolean(opts.json),
|
||||||
|
timeoutMs: timeout,
|
||||||
|
probe: Boolean(opts.probe),
|
||||||
|
},
|
||||||
defaultRuntime,
|
defaultRuntime,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ async function probeTelegram(
|
|||||||
|
|
||||||
export async function getHealthSnapshot(
|
export async function getHealthSnapshot(
|
||||||
timeoutMs?: number,
|
timeoutMs?: number,
|
||||||
|
opts?: { probe?: boolean },
|
||||||
): Promise<HealthSummary> {
|
): Promise<HealthSummary> {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const linked = await webAuthExists();
|
const linked = await webAuthExists();
|
||||||
@@ -210,7 +211,8 @@ export async function getHealthSnapshot(
|
|||||||
|
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const cappedTimeout = Math.max(1000, timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
const cappedTimeout = Math.max(1000, timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
||||||
const connect = linked ? await probeWebConnect(cappedTimeout) : undefined;
|
const connect =
|
||||||
|
linked && opts?.probe ? await probeWebConnect(cappedTimeout) : undefined;
|
||||||
|
|
||||||
const telegramToken =
|
const telegramToken =
|
||||||
process.env.TELEGRAM_BOT_TOKEN ?? cfg.telegram?.botToken ?? "";
|
process.env.TELEGRAM_BOT_TOKEN ?? cfg.telegram?.botToken ?? "";
|
||||||
@@ -237,10 +239,12 @@ export async function getHealthSnapshot(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function healthCommand(
|
export async function healthCommand(
|
||||||
opts: { json?: boolean; timeoutMs?: number },
|
opts: { json?: boolean; timeoutMs?: number; probe?: boolean },
|
||||||
runtime: RuntimeEnv,
|
runtime: RuntimeEnv,
|
||||||
) {
|
) {
|
||||||
const summary = await getHealthSnapshot(opts.timeoutMs);
|
const summary = await getHealthSnapshot(opts.timeoutMs, {
|
||||||
|
probe: opts.probe,
|
||||||
|
});
|
||||||
const fatal =
|
const fatal =
|
||||||
!summary.web.linked ||
|
!summary.web.linked ||
|
||||||
(summary.web.connect && !summary.web.connect.ok) ||
|
(summary.web.connect && !summary.web.connect.ok) ||
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { CliDeps } from "../cli/deps.js";
|
import type { CliDeps } from "../cli/deps.js";
|
||||||
|
import { listPortListeners } from "../cli/ports.js";
|
||||||
import { info, success } from "../globals.js";
|
import { info, success } from "../globals.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
||||||
@@ -78,8 +79,15 @@ export async function sendCommand(
|
|||||||
result = await sendViaGateway();
|
result = await sendViaGateway();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!opts.spawnGateway) throw err;
|
if (!opts.spawnGateway) throw err;
|
||||||
await startGatewayServer(18789);
|
// Only spawn when nothing is listening.
|
||||||
result = await sendViaGateway();
|
try {
|
||||||
|
const listeners = listPortListeners(18789);
|
||||||
|
if (listeners.length > 0) throw err;
|
||||||
|
await startGatewayServer(18789);
|
||||||
|
result = await sendViaGateway();
|
||||||
|
} catch {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
runtime.log(
|
runtime.log(
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
|||||||
presenceVersion += 1;
|
presenceVersion += 1;
|
||||||
const snapshot = buildSnapshot();
|
const snapshot = buildSnapshot();
|
||||||
// Fill health asynchronously for snapshot
|
// Fill health asynchronously for snapshot
|
||||||
const health = await getHealthSnapshot();
|
const health = await getHealthSnapshot(undefined, { probe: false });
|
||||||
snapshot.health = health;
|
snapshot.health = health;
|
||||||
snapshot.stateVersion.health = ++healthVersion;
|
snapshot.stateVersion.health = ++healthVersion;
|
||||||
const helloOk = {
|
const helloOk = {
|
||||||
|
|||||||
Reference in New Issue
Block a user