107 lines
2.9 KiB
TypeScript
107 lines
2.9 KiB
TypeScript
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<string | undefined> {
|
|
// 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<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 (/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 <free-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 };
|