UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

310 lines (309 loc) 11.1 kB
import { F as resolveTimerTimeoutMs, P as resolvePositiveTimerTimeoutMs, w as parseStrictPositiveInteger } from "./number-coercion-CLj0HTDM.js"; import "./utils-P__uGsPB.js"; import { t as sleep } from "./sleep-D7nua6TP.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { n as resolveDiagnosticProcessEnv } from "./process-env-DlZFJzq6.js"; import { a as getWindowsSystem32ExePath } from "./windows-install-roots-BBaNTTOQ.js"; import { r as parseWindowsNetstatListeners } from "./ports-netstat-_2CZ3Oho.js"; import { n as probePortUsage } from "./ports-probe-funEe1wt.js"; import { n as resolveLsofCommandSync } from "./ports-lsof-BmdLddJi.js"; import { execFileSync } from "node:child_process"; import { createServer } from "node:net"; //#region src/cli/ports.ts const FUSER_SIGNALS = { SIGTERM: "TERM", SIGKILL: "KILL" }; const PORT_TOOL_TIMEOUT_MS = 1e4; function readExecOutput(value) { if (typeof value === "string") return value; if (value instanceof Buffer) return value.toString("utf8"); return ""; } function withErrnoCode(message, code, cause) { const out = new Error(message, { cause: cause instanceof Error ? cause : void 0 }); out.code = code; return out; } function getErrnoCode(err) { if (!err || typeof err !== "object") return; const direct = err.code; if (typeof direct === "string" && direct.length > 0) return direct; const cause = err.cause; if (cause && typeof cause === "object") { const nested = cause.code; if (typeof nested === "string" && nested.length > 0) return nested; } } function isRecoverableLsofError(err) { const code = getErrnoCode(err); if (code === "ENOENT" || code === "EACCES" || code === "EPERM" || code === "EPROTO") return true; const message = formatErrorMessage(err); return /lsof.*(permission denied|not permitted|operation not permitted|eacces|eperm)/i.test(message); } function parseFuserPidList(output) { if (!output) return []; const values = /* @__PURE__ */ new Set(); for (const token of output.split(/\s+/)) { if (!token) continue; const pid = parseStrictPositiveInteger(token); if (pid !== void 0) values.add(pid); } return [...values]; } function killPortWithFuser(port, signal, beforeSignal) { if (beforeSignal) { const listeners = listPortListenersWithFuser(port); killPids(port, listeners, signal, beforeSignal); return listeners; } const args = [ "-k", `-${FUSER_SIGNALS[signal]}`, `${port}/tcp` ]; try { return parseFuserPidList(execFileSync("fuser", args, { env: resolveDiagnosticProcessEnv(), encoding: "utf-8", stdio: [ "ignore", "pipe", "pipe" ], timeout: PORT_TOOL_TIMEOUT_MS, killSignal: "SIGKILL" })).map((pid) => ({ pid })); } catch (err) { const execErr = err; const code = execErr.code; const status = execErr.status; const parsed = parseFuserPidList([readExecOutput(execErr.stdout), readExecOutput(execErr.stderr)].filter(Boolean).join("\n")); if (status === 1) return parsed.map((pid) => ({ pid })); if (code === "ENOENT") throw withErrnoCode("fuser not found; required for --force when lsof is unavailable", "ENOENT", err); if (code === "EACCES" || code === "EPERM") throw withErrnoCode("fuser permission denied while forcing gateway port", code, err); throw err instanceof Error ? err : new Error(String(err)); } } function listPortListenersWithFuser(port) { try { return parseFuserPidList(execFileSync("fuser", [`${port}/tcp`], { env: resolveDiagnosticProcessEnv(), encoding: "utf-8", stdio: [ "ignore", "pipe", "pipe" ], timeout: PORT_TOOL_TIMEOUT_MS, killSignal: "SIGKILL" })).map((pid) => ({ pid })); } catch (err) { const execErr = err; const parsed = parseFuserPidList(readExecOutput(execErr.stdout)); if (execErr.status === 1) return parsed.map((pid) => ({ pid })); if (execErr.code === "ENOENT") throw withErrnoCode("fuser not found; required for --force when lsof is unavailable", "ENOENT", err); if (execErr.code === "EACCES" || execErr.code === "EPERM") throw withErrnoCode("fuser permission denied while inspecting gateway port", execErr.code, err); throw err instanceof Error ? err : new Error(String(err)); } } async function isPortBusy(port) { return await probePortUsage(port) !== "free"; } function parseLsofOutput(output) { const lines = output.split(/\r?\n/).filter(Boolean); const results = []; let current = {}; for (const line of lines) if (line.startsWith("p")) { if (current.pid) results.push(current); const rawPidToken = line.slice(1); const rawPid = parseStrictPositiveInteger(rawPidToken); if (rawPid === void 0) throw withErrnoCode(`lsof returned malformed PID field: ${JSON.stringify(rawPidToken)}`, "EPROTO", void 0); current = { pid: rawPid }; } else if (line.startsWith("c")) current.command = line.slice(1); if (current.pid) results.push(current); return results; } function listPortListeners(port) { if (process.platform === "win32") try { const out = execFileSync(getWindowsSystem32ExePath("netstat.exe"), ["-ano"], { env: resolveDiagnosticProcessEnv(), encoding: "utf-8", timeout: PORT_TOOL_TIMEOUT_MS, killSignal: "SIGKILL" }); const listeners = parseWindowsNetstatListeners(out, port); const seenPids = /* @__PURE__ */ new Set(); const results = []; for (const listener of listeners) { if (seenPids.has(listener.pid)) continue; seenPids.add(listener.pid); results.push({ pid: listener.pid }); } return results; } catch (err) { throw new Error(`netstat failed: ${String(err)}`, { cause: err }); } try { const lsof = resolveLsofCommandSync(); return parseLsofOutput(execFileSync(lsof, [ "-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-FpFc" ], { env: resolveDiagnosticProcessEnv(), encoding: "utf-8", timeout: PORT_TOOL_TIMEOUT_MS, killSignal: "SIGKILL" })); } catch (err) { const execErr = err; const status = execErr.status ?? void 0; const code = execErr.code; if (code === "ENOENT") throw withErrnoCode("lsof not found; required for --force", "ENOENT", err); if (code === "EACCES" || code === "EPERM") throw withErrnoCode("lsof permission denied while inspecting gateway port", code, err); if (status === 1) { const stderr = readExecOutput(execErr.stderr).trim(); if (stderr && /permission denied|not permitted|operation not permitted|can't stat/i.test(stderr)) throw withErrnoCode(`lsof permission denied while inspecting gateway port: ${stderr}`, "EACCES", err); return []; } throw err instanceof Error ? err : new Error(String(err)); } } function forceFreePort(port, opts = {}) { const listeners = listPortListeners(port); killPids(port, listeners, "SIGTERM", opts.beforeSignal); return listeners; } function killPids(port, listeners, signal, beforeSignal) { for (const proc of listeners) { beforeSignal?.({ port, pid: proc.pid, signal }); try { process.kill(proc.pid, signal); } catch (err) { if (getErrnoCode(err) === "ESRCH") continue; throw new Error(`failed to kill pid ${proc.pid}${proc.command ? ` (${proc.command})` : ""}: ${String(err)}`, { cause: err }); } } } async function forceFreePortAndWait(port, opts = {}) { const timeoutMs = resolveTimerTimeoutMs(opts.timeoutMs, 1500, 0); const intervalMs = resolvePositiveTimerTimeoutMs(opts.intervalMs, 100); const sigtermTimeoutMs = Math.min(resolveTimerTimeoutMs(opts.sigtermTimeoutMs, 600, 0), timeoutMs); let killed = []; let useFuserFallback = false; try { killed = forceFreePort(port, opts.beforeSignal ? { beforeSignal: opts.beforeSignal } : {}); } catch (err) { if (!isRecoverableLsofError(err)) throw err; if (!await isPortBusy(port)) return { killed, waitedMs: 0, escalatedToSigkill: false }; useFuserFallback = true; killed = killPortWithFuser(port, "SIGTERM", opts.beforeSignal); } if (killed.length === 0) { if (await isPortBusy(port)) throw new Error(`port ${port} is still busy after --force, but no listener PID could be determined`); return { killed, waitedMs: 0, escalatedToSigkill: false }; } const checkBusy = async () => useFuserFallback ? isPortBusy(port) : listPortListeners(port).length > 0; if (!await checkBusy()) return { killed, waitedMs: 0, escalatedToSigkill: false }; let waitedMs = 0; while (waitedMs < sigtermTimeoutMs) { if (!await checkBusy()) return { killed, waitedMs, escalatedToSigkill: false }; const sleepMs = Math.min(intervalMs, sigtermTimeoutMs - waitedMs); await sleep(sleepMs); waitedMs += sleepMs; } if (!await checkBusy()) return { killed, waitedMs, escalatedToSigkill: false }; if (useFuserFallback) killPortWithFuser(port, "SIGKILL", opts.beforeSignal); else killPids(port, listPortListeners(port), "SIGKILL", opts.beforeSignal); while (waitedMs < timeoutMs) { if (!await checkBusy()) return { killed, waitedMs, escalatedToSigkill: true }; const sleepMs = Math.min(intervalMs, timeoutMs - waitedMs); await sleep(sleepMs); waitedMs += sleepMs; } if (!await checkBusy()) return { killed, waitedMs, escalatedToSigkill: true }; if (useFuserFallback) throw new Error(`port ${port} still has listeners after --force (fuser fallback)`); const still = listPortListeners(port); throw new Error(`port ${port} still has listeners after --force: ${still.map((p) => p.pid).join(", ")}`); } /** * Attempt a real TCP bind to verify the port is available at the OS level. * Catches TIME_WAIT / kernel-level holds that lsof won't show. * * Resolves false only for EADDRINUSE — a genuinely transient condition * (port still in TIME_WAIT after a --force kill) that the caller should retry. * * All other errors are non-retryable and are rejected immediately: * - EADDRNOTAVAIL: the host address doesn't exist on any local interface * (hard misconfiguration, not a transient kernel hold). * - EACCES: bind to a privileged port as non-root. * - EINVAL, etc.: other unrecoverable OS errors. */ function probePortFree(port, host = "0.0.0.0") { return new Promise((resolve, reject) => { const srv = createServer(); srv.unref(); srv.once("error", (err) => { srv.close(); if (err.code === "EADDRINUSE") resolve(false); else reject(err); }); srv.listen(port, host, () => { srv.close(() => resolve(true)); }); }); } /** * Poll until a real test-bind succeeds, up to `timeoutMs`. * Returns the number of ms waited, or throws if the port never freed. */ async function waitForPortBindable(port, opts = {}) { const timeoutMs = resolveTimerTimeoutMs(opts.timeoutMs, 3e3, 0); const intervalMs = resolvePositiveTimerTimeoutMs(opts.intervalMs, 150); const host = opts.host; let waited = 0; while (waited < timeoutMs) { if (await probePortFree(port, host)) return waited; const sleepMs = Math.min(intervalMs, timeoutMs - waited); await sleep(sleepMs); waited += sleepMs; } if (await probePortFree(port, host)) return waited; throw new Error(`port ${port} still not bindable after ${waited}ms (TIME_WAIT or kernel hold)`); } //#endregion export { forceFreePortAndWait as n, waitForPortBindable as r, forceFreePort as t };