Files
clawdbot/src/infra/ports.ts
Peter Steinberger c379191f80 chore: migrate to oxlint and oxfmt
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
2026-01-14 15:02:19 +00:00

93 lines
3.2 KiB
TypeScript

import net from "node:net";
import { danger, info, shouldLogVerbose, warn } from "../globals.js";
import { logDebug } from "../logger.js";
import type { RuntimeEnv } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
import { formatPortDiagnostics } from "./ports-format.js";
import { inspectPortUsage } from "./ports-inspect.js";
import type { PortListener, PortListenerKind, PortUsage, PortUsageStatus } from "./ports-types.js";
class PortInUseError extends Error {
port: number;
details?: string;
constructor(port: number, details?: string) {
super(`Port ${port} is already in use.`);
this.name = "PortInUseError";
this.port = port;
this.details = details;
}
}
function isErrno(err: unknown): err is NodeJS.ErrnoException {
return Boolean(err && typeof err === "object" && "code" in err);
}
export async function describePortOwner(port: number): Promise<string | undefined> {
const diagnostics = await inspectPortUsage(port);
if (diagnostics.listeners.length === 0) return undefined;
return formatPortDiagnostics(diagnostics).join("\n");
}
export async function ensurePortAvailable(port: number): Promise<void> {
// Detect EADDRINUSE early with a friendly message.
try {
await new Promise<void>((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") {
const details = await describePortOwner(port);
throw new PortInUseError(port, details);
}
throw err;
}
}
export async function handlePortError(
err: unknown,
port: number,
context: string,
runtime: RuntimeEnv = defaultRuntime,
): Promise<never> {
// Uniform messaging for EADDRINUSE with optional owner details.
if (err instanceof PortInUseError || (isErrno(err) && err.code === "EADDRINUSE")) {
const details = err instanceof PortInUseError ? err.details : await describePortOwner(port);
runtime.error(danger(`${context} failed: port ${port} is already in use.`));
if (details) {
runtime.error(info("Port listener details:"));
runtime.error(details);
if (/clawdbot|src\/index\.ts|dist\/index\.js/.test(details)) {
runtime.error(
warn(
"It looks like another clawdbot instance is already running. Stop it or pick a different port.",
),
);
}
}
runtime.error(
info("Resolve by stopping the process using the port or passing --port <free-port>."),
);
runtime.exit(1);
}
runtime.error(danger(`${context} failed: ${String(err)}`));
if (shouldLogVerbose()) {
const stdout = (err as { stdout?: string })?.stdout;
const stderr = (err as { stderr?: string })?.stderr;
if (stdout?.trim()) logDebug(`stdout: ${stdout.trim()}`);
if (stderr?.trim()) logDebug(`stderr: ${stderr.trim()}`);
}
return runtime.exit(1);
}
export { PortInUseError };
export type { PortListener, PortListenerKind, PortUsage, PortUsageStatus };
export { buildPortHints, classifyPortListener, formatPortDiagnostics } from "./ports-format.js";
export { inspectPortUsage } from "./ports-inspect.js";