openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
106 lines (105 loc) • 6.54 kB
JavaScript
import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-1VuiLaaR.mjs";
import { t as formatCliCommand } from "./command-format-DwEdrDKL.mjs";
import { t as splitArgsPreservingQuotes } from "./arg-split-CR3xkHmb.mjs";
import { t as classifyOpenClawArgv } from "./gateway-process-argv-DMAY-YPv.mjs";
import { t as parseWindowsNativeCommandLine } from "./windows-command-line-BrVAurm9.mjs";
import { n as parseTcpListenerEndpoint } from "./ports-netstat-816BXbAP.mjs";
import net from "node:net";
//#region src/infra/ports-format.ts
/** Classifies a listener as OpenClaw Gateway, SSH tunnel, known non-gateway, or unknown. */
function classifyPortListener(listener) {
const command = normalizeLowercaseStringOrEmpty(listener.command ?? "");
const commandLine = normalizeLowercaseStringOrEmpty(listener.commandLine ?? "");
if (command === "socat" || command === "socat1" || command === "socat.exe") return "non_gateway";
const argv = listener.commandLine ? process.platform === "win32" ? parseWindowsNativeCommandLine(listener.commandLine) ?? [] : splitArgsPreservingQuotes(listener.commandLine, { escapeMode: "backslash-quote-only" }) : [listener.command ?? ""];
if (classifyOpenClawArgv(argv, {
command: "gateway",
pid: listener.pid
}).kind === "openclaw") return "gateway";
if (/(?:^|[/\\])ssh(?:\.exe)?$/.test(command) || /(?:^|[\s"'])(?:(?:"[^"]*[/\\])|(?:'[^']*[/\\])|(?:\S*[/\\]))?ssh(?:\.exe)?(?:[\s"']|$)/.test(commandLine)) return "ssh";
if (command === "sshd" || /(?:^|[/\\])sshd(?:\.exe)?$/.test(command) || /(?:^|[/\\])[^/\\\s]*ssh[^/\\\s]*(?:\.exe)?$/.test(command)) return "non_gateway";
if (/(?:^|[/\\\s])[^/\\\s]*ssh[^/\\\s]*(?:\.exe)?(?:[/\\\s"']|$)/.test(commandLine)) return "non_gateway";
return "unknown";
}
function classifyLoopbackAddressFamily(host) {
if (host === "127.0.0.1" || host === "localhost") return "ipv4";
if (host === "::1" || host === "::ffff:127.0.0.1") return "ipv6";
return null;
}
function isWildcardAddress(host) {
return host === "0.0.0.0" || host === "::" || host === "*";
}
function isExpectedGatewayBindAddress(host) {
return classifyLoopbackAddressFamily(host) !== null || isWildcardAddress(host);
}
function parsePortListeners(listeners, port) {
const parsedListeners = [];
for (const listener of listeners) {
const pid = listener.pid;
if (typeof pid !== "number" || !Number.isFinite(pid) || typeof listener.address !== "string") return null;
const address = parseTcpListenerEndpoint(listener.address);
if (!address || address.port !== port) return null;
parsedListeners.push({
pid,
host: address.host
});
}
return parsedListeners;
}
function parseGatewayListeners(listeners, port) {
if (listeners.some((listener) => classifyPortListener(listener) !== "gateway")) return null;
return parsePortListeners(listeners, port);
}
/** Returns true for one Gateway process represented by separate IPv4 and IPv6 loopback rows. */
function isDualStackLoopbackGatewayListeners(listeners, port) {
if (listeners.length < 2) return false;
const parsed = parseGatewayListeners(listeners, port);
if (!parsed) return false;
return parsedListenersAreDualStackLoopback(parsed);
}
function parsedListenersAreDualStackLoopback(parsed) {
const pids = new Set(parsed.map(({ pid }) => pid));
const families = new Set(parsed.map(({ host }) => classifyLoopbackAddressFamily(host)));
return pids.size === 1 && !families.has(null) && families.has("ipv4") && families.has("ipv6");
}
function parsedListenersOwnSpecificIpv4WithLoopback(parsed) {
return parsed.every(({ pid }) => pid === parsed[0]?.pid) && parsed.some(({ host }) => host === "127.0.0.1") && parsed.some(({ host }) => host !== "127.0.0.1" && net.isIP(host) === 4 && !isWildcardAddress(host));
}
/** Checks one PID owns an expected IPv4 interface and canonical loopback. */
function isSameProcessSpecificIpv4WithLoopbackListeners(listeners, port, expectedSpecificHost) {
if (listeners.length !== 2) return false;
const parsed = parsePortListeners(listeners, port);
return Boolean(parsed && parsedListenersOwnSpecificIpv4WithLoopback(parsed) && parsed.some(({ host }) => host === expectedSpecificHost));
}
/** Returns true when listener rows describe a benign Gateway bind pattern. */
function isExpectedGatewayListeners(listeners, port) {
const parsed = parseGatewayListeners(listeners, port);
if (!parsed) return false;
if (parsed.length === 1) return Boolean(parsed[0] && isExpectedGatewayBindAddress(parsed[0].host));
return parsedListenersAreDualStackLoopback(parsed) || parsed.length === 2 && parsedListenersOwnSpecificIpv4WithLoopback(parsed);
}
/** Builds user-facing remediation hints for processes occupying a port. */
function buildPortHints(listeners, port) {
if (listeners.length === 0) return [];
const kinds = new Set(listeners.map((listener) => classifyPortListener(listener)));
const hints = [];
const expectedGatewayListeners = isExpectedGatewayListeners(listeners, port);
if (kinds.has("gateway") && !expectedGatewayListeners) hints.push(`Gateway already running locally. Stop it (${formatCliCommand("openclaw gateway stop")}) or use a different port.`);
if (kinds.has("ssh")) hints.push("SSH tunnel already bound to this port. Close the tunnel or use a different local port in -L.");
if (kinds.has("unknown") || kinds.has("non_gateway")) hints.push("Another process is listening on this port.");
if (listeners.length > 1 && !expectedGatewayListeners) hints.push("Multiple listeners detected; ensure only one gateway/tunnel per port unless intentionally running isolated profiles.");
return hints;
}
function formatPortListener(listener) {
return `${listener.pid ? `pid ${listener.pid}` : "pid ?"}${listener.user ? ` ${listener.user}` : ""}: ${listener.commandLine || listener.command || "unknown"}${listener.address ? ` (${listener.address})` : ""}`;
}
function formatPortDiagnostics(diagnostics) {
if (diagnostics.status === "free") return [`Port ${diagnostics.port} is free.`];
if (diagnostics.status === "unknown") return [`Port ${diagnostics.port} availability could not be determined.`];
const lines = [`Port ${diagnostics.port} is already in use.`];
for (const listener of diagnostics.listeners) lines.push(`- ${formatPortListener(listener)}`);
for (const hint of diagnostics.hints) lines.push(`- ${hint}`);
return lines;
}
//#endregion
export { isExpectedGatewayListeners as a, isDualStackLoopbackGatewayListeners as i, classifyPortListener as n, isSameProcessSpecificIpv4WithLoopbackListeners as o, formatPortDiagnostics as r, buildPortHints as t };