@gguf/claw
Version:
Multi-channel AI gateway with extensible messaging integrations
317 lines (312 loc) • 9.77 kB
JavaScript
import { t as formatCliCommand } from "./command-format-D3syQOZg.js";
import { t as runCommandWithTimeout } from "./exec-CBKBIMpA.js";
import { a as isErrno } from "./errors-CFEPAPWc.js";
import fs from "node:fs";
import fs$1 from "node:fs/promises";
import net from "node:net";
//#region src/infra/ports-format.ts
function classifyPortListener(listener, port) {
const raw = `${listener.commandLine ?? ""} ${listener.command ?? ""}`.trim().toLowerCase();
if (raw.includes("openclaw")) return "gateway";
if (raw.includes("ssh")) {
const portToken = String(port);
const tunnelPattern = new RegExp(`-(l|r)\\s*${portToken}\\b|-(l|r)${portToken}\\b|:${portToken}\\b`);
if (!raw || tunnelPattern.test(raw)) return "ssh";
return "ssh";
}
return "unknown";
}
function buildPortHints(listeners, port) {
if (listeners.length === 0) return [];
const kinds = new Set(listeners.map((listener) => classifyPortListener(listener, port)));
const hints = [];
if (kinds.has("gateway")) 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")) hints.push("Another process is listening on this port.");
if (listeners.length > 1) 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 !== "busy") return [`Port ${diagnostics.port} is free.`];
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
//#region src/infra/ports-lsof.ts
const LSOF_CANDIDATES = process.platform === "darwin" ? ["/usr/sbin/lsof", "/usr/bin/lsof"] : ["/usr/bin/lsof", "/usr/sbin/lsof"];
async function canExecute(path) {
try {
await fs$1.access(path, fs.constants.X_OK);
return true;
} catch {
return false;
}
}
async function resolveLsofCommand() {
for (const candidate of LSOF_CANDIDATES) if (await canExecute(candidate)) return candidate;
return "lsof";
}
function resolveLsofCommandSync() {
for (const candidate of LSOF_CANDIDATES) try {
fs.accessSync(candidate, fs.constants.X_OK);
return candidate;
} catch {}
return "lsof";
}
//#endregion
//#region src/infra/ports-inspect.ts
async function runCommandSafe(argv, timeoutMs = 5e3) {
try {
const res = await runCommandWithTimeout(argv, { timeoutMs });
return {
stdout: res.stdout,
stderr: res.stderr,
code: res.code ?? 1
};
} catch (err) {
return {
stdout: "",
stderr: "",
code: 1,
error: String(err)
};
}
}
function parseLsofFieldOutput(output) {
const lines = output.split(/\r?\n/).filter(Boolean);
const listeners = [];
let current = {};
for (const line of lines) if (line.startsWith("p")) {
if (current.pid || current.command) listeners.push(current);
const pid = Number.parseInt(line.slice(1), 10);
current = Number.isFinite(pid) ? { pid } : {};
} else if (line.startsWith("c")) current.command = line.slice(1);
else if (line.startsWith("n")) {
if (!current.address) current.address = line.slice(1);
}
if (current.pid || current.command) listeners.push(current);
return listeners;
}
async function resolveUnixCommandLine(pid) {
const res = await runCommandSafe([
"ps",
"-p",
String(pid),
"-o",
"command="
]);
if (res.code !== 0) return;
return res.stdout.trim() || void 0;
}
async function resolveUnixUser(pid) {
const res = await runCommandSafe([
"ps",
"-p",
String(pid),
"-o",
"user="
]);
if (res.code !== 0) return;
return res.stdout.trim() || void 0;
}
async function readUnixListeners(port) {
const errors = [];
const res = await runCommandSafe([
await resolveLsofCommand(),
"-nP",
`-iTCP:${port}`,
"-sTCP:LISTEN",
"-FpFcn"
]);
if (res.code === 0) {
const listeners = parseLsofFieldOutput(res.stdout);
await Promise.all(listeners.map(async (listener) => {
if (!listener.pid) return;
const [commandLine, user] = await Promise.all([resolveUnixCommandLine(listener.pid), resolveUnixUser(listener.pid)]);
if (commandLine) listener.commandLine = commandLine;
if (user) listener.user = user;
}));
return {
listeners,
detail: res.stdout.trim() || void 0,
errors
};
}
const stderr = res.stderr.trim();
if (res.code === 1 && !res.error && !stderr) return {
listeners: [],
detail: void 0,
errors
};
if (res.error) errors.push(res.error);
const detail = [stderr, res.stdout.trim()].filter(Boolean).join("\n");
if (detail) errors.push(detail);
return {
listeners: [],
detail: void 0,
errors
};
}
function parseNetstatListeners(output, port) {
const listeners = [];
const portToken = `:${port}`;
for (const rawLine of output.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line) continue;
if (!line.toLowerCase().includes("listen")) continue;
if (!line.includes(portToken)) continue;
const parts = line.split(/\s+/);
if (parts.length < 4) continue;
const pidRaw = parts.at(-1);
const pid = pidRaw ? Number.parseInt(pidRaw, 10) : NaN;
const localAddr = parts[1];
const listener = {};
if (Number.isFinite(pid)) listener.pid = pid;
if (localAddr?.includes(portToken)) listener.address = localAddr;
listeners.push(listener);
}
return listeners;
}
async function resolveWindowsImageName(pid) {
const res = await runCommandSafe([
"tasklist",
"/FI",
`PID eq ${pid}`,
"/FO",
"LIST"
]);
if (res.code !== 0) return;
for (const rawLine of res.stdout.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line.toLowerCase().startsWith("image name:")) continue;
return line.slice(11).trim() || void 0;
}
}
async function resolveWindowsCommandLine(pid) {
const res = await runCommandSafe([
"wmic",
"process",
"where",
`ProcessId=${pid}`,
"get",
"CommandLine",
"/value"
]);
if (res.code !== 0) return;
for (const rawLine of res.stdout.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line.toLowerCase().startsWith("commandline=")) continue;
return line.slice(12).trim() || void 0;
}
}
async function readWindowsListeners(port) {
const errors = [];
const res = await runCommandSafe([
"netstat",
"-ano",
"-p",
"tcp"
]);
if (res.code !== 0) {
if (res.error) errors.push(res.error);
const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n");
if (detail) errors.push(detail);
return {
listeners: [],
errors
};
}
const listeners = parseNetstatListeners(res.stdout, port);
await Promise.all(listeners.map(async (listener) => {
if (!listener.pid) return;
const [imageName, commandLine] = await Promise.all([resolveWindowsImageName(listener.pid), resolveWindowsCommandLine(listener.pid)]);
if (imageName) listener.command = imageName;
if (commandLine) listener.commandLine = commandLine;
}));
return {
listeners,
detail: res.stdout.trim() || void 0,
errors
};
}
async function tryListenOnHost(port, host) {
try {
await new Promise((resolve, reject) => {
const tester = net.createServer().once("error", (err) => reject(err)).once("listening", () => {
tester.close(() => resolve());
}).listen({
port,
host,
exclusive: true
});
});
return "free";
} catch (err) {
if (isErrno(err) && err.code === "EADDRINUSE") return "busy";
if (isErrno(err) && (err.code === "EADDRNOTAVAIL" || err.code === "EAFNOSUPPORT")) return "skip";
return "unknown";
}
}
async function checkPortInUse(port) {
const hosts = [
"127.0.0.1",
"0.0.0.0",
"::1",
"::"
];
let sawUnknown = false;
for (const host of hosts) {
const result = await tryListenOnHost(port, host);
if (result === "busy") return "busy";
if (result === "unknown") sawUnknown = true;
}
return sawUnknown ? "unknown" : "free";
}
async function inspectPortUsage(port) {
const errors = [];
const result = process.platform === "win32" ? await readWindowsListeners(port) : await readUnixListeners(port);
errors.push(...result.errors);
let listeners = result.listeners;
let status = listeners.length > 0 ? "busy" : "unknown";
if (listeners.length === 0) status = await checkPortInUse(port);
if (status !== "busy") listeners = [];
const hints = buildPortHints(listeners, port);
if (status === "busy" && listeners.length === 0) hints.push("Port is in use but process details are unavailable (install lsof or run as an admin user).");
return {
port,
status,
listeners,
hints,
detail: result.detail,
errors: errors.length > 0 ? errors : void 0
};
}
//#endregion
//#region src/infra/ports.ts
var PortInUseError = class extends Error {
constructor(port, details) {
super(`Port ${port} is already in use.`);
this.name = "PortInUseError";
this.port = port;
this.details = details;
}
};
async function ensurePortAvailable(port) {
try {
await new Promise((resolve, reject) => {
const tester = net.createServer().once("error", (err) => reject(err)).once("listening", () => {
tester.close(() => resolve());
}).listen(port);
});
} catch (err) {
if (isErrno(err) && err.code === "EADDRINUSE") throw new PortInUseError(port);
throw err;
}
}
//#endregion
export { formatPortDiagnostics as i, inspectPortUsage as n, resolveLsofCommandSync as r, ensurePortAvailable as t };