import net from "node:net"; import { runExec } from "../process/exec.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { danger, info, isVerbose, logVerbose, warn } from "../globals.js"; import { logDebug } from "../logger.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 { // Best-effort process info for a listening port (macOS/Linux). try { const { stdout } = await runExec("lsof", [ "-i", `tcp:${port}`, "-sTCP:LISTEN", "-nP", ]); const trimmed = stdout.trim(); if (trimmed) return trimmed; } catch (err) { logVerbose(`lsof unavailable: ${String(err)}`); } return undefined; } export async function ensurePortAvailable(port: number): Promise { // Detect EADDRINUSE early with a friendly message. 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") { 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 { // 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 (/warelay|src\/index\.ts|dist\/index\.js/.test(details)) { runtime.error( warn( "It looks like another warelay 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 .", ), ); runtime.exit(1); } runtime.error(danger(`${context} failed: ${String(err)}`)); if (isVerbose()) { 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 };