UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

114 lines (113 loc) 6.83 kB
import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js"; import { t as formatCliCommand } from "./command-format-C7YfyMTd.js"; import { n as parseTcpListenerEndpoint } from "./ports-netstat-_2CZ3Oho.js"; 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, _port) { const command = normalizeLowercaseStringOrEmpty(listener.command ?? ""); const commandLine = normalizeLowercaseStringOrEmpty(listener.commandLine ?? ""); if (command === "socat" || command === "socat1" || command === "socat.exe") return "non_gateway"; if (`${commandLine} ${command}`.includes("openclaw")) return "gateway"; const hasSshCommand = /(?:^|[/\\])ssh(?:\.exe)?$/.test(command); const hasSshExecutable = hasSshCommand || /(?:^|[\s"'])(?:(?:"[^"]*[/\\])|(?:'[^']*[/\\])|(?:\S*[/\\]))?ssh(?:\.exe)?(?:[\s"']|$)/.test(commandLine); if (hasSshCommand) return "ssh"; if (hasSshExecutable) 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") return "ipv6"; if (host.startsWith("::ffff:")) return host.slice(7) === "127.0.0.1" ? "ipv6" : null; 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, port) !== "gateway")) return null; return parsePortListeners(listeners, port); } /** Returns true for one Gateway listener bound to an expected loopback or wildcard address. */ function isSingleExpectedGatewayListener(listeners, port) { if (listeners.length !== 1) return false; const parsed = parseGatewayListeners(listeners, port); return Boolean(parsed?.[0] && isExpectedGatewayBindAddress(parsed[0].host)); } /** 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; 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) { if (new Set(parsed.map(({ pid }) => pid)).size !== 1) return false; const hosts = new Set(parsed.map(({ host }) => host)); const specificHosts = [...hosts].filter((host) => host !== "127.0.0.1" && net.isIP(host) === 4 && !isWildcardAddress(host)); return hosts.has("127.0.0.1") && specificHosts.length > 0; } /** Checks one Gateway PID owns both an exact IPv4 interface and canonical loopback. */ function isSpecificIpv4WithLoopbackGatewayListeners(listeners, port) { if (listeners.length !== 2) return false; const parsed = parseGatewayListeners(listeners, port); return Boolean(parsed && parsedListenersOwnSpecificIpv4WithLoopback(parsed)); } /** 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) { return isSingleExpectedGatewayListener(listeners, port) || isDualStackLoopbackGatewayListeners(listeners, port) || isSpecificIpv4WithLoopbackGatewayListeners(listeners, port); } /** 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, port))); 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; } /** Formats one listener row for CLI diagnostics. */ function formatPortListener(listener) { return `${listener.pid ? `pid ${listener.pid}` : "pid ?"}${listener.user ? ` ${listener.user}` : ""}: ${listener.commandLine || listener.command || "unknown"}${listener.address ? ` (${listener.address})` : ""}`; } /** Formats port diagnostics into CLI output lines. */ 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 };