Skip to content

Commit

Permalink
refactor: split internals and types
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Sep 6, 2023
1 parent 836ca3c commit cf4317c
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 260 deletions.
90 changes: 90 additions & 0 deletions src/_internal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { createServer, AddressInfo } from "node:net";
import { networkInterfaces } from "node:os";
import { isSafePort } from "./unsafe-ports";
import type { PortNumber, HostAddress } from "./types";

export class GetPortError extends Error {
name = "GetPortError";
constructor(
public message: string,
opts?: any,
) {
super(message, opts);
}
}

export function _log(verbose: boolean, message: string) {
if (verbose) {
console.log("[get-port]", message);
}
}

export function _generateRange(from: number, to: number): number[] {
if (to < from) {
return [];
}
const r = [];
for (let index = from; index < to; index++) {
r.push(index);
}
return r;
}

export function _tryPort(
port: PortNumber,
host: HostAddress,
): Promise<PortNumber | false> {
return new Promise((resolve) => {
const server = createServer();
server.unref();
server.on("error", (error: Error & { code: string }) => {
// Ignore invalid host
if (error.code === "EINVAL" || error.code === "EADDRNOTAVAIL") {
resolve(port !== 0 && isSafePort(port) && port);
} else {
resolve(false);
}
});
server.listen({ port, host }, () => {
const { port } = server.address() as AddressInfo;
server.close(() => {
resolve(isSafePort(port) && port);
});
});
});
}

export function _getLocalHosts(additional: HostAddress[]): HostAddress[] {
const hosts = new Set<HostAddress>(additional);
for (const _interface of Object.values(networkInterfaces())) {
for (const config of _interface || []) {
hosts.add(config.address);
}
}
return [...hosts];
}

export async function _findPort(
ports: number[],
host: HostAddress,
): Promise<PortNumber> {
for (const port of ports) {
const r = await _tryPort(port, host);
if (r) {
return r;
}
}
}

export function _fmtOnHost(hostname: string | undefined) {
return hostname ? `on host ${JSON.stringify(hostname)}` : "on any host";
}

const HOSTNAME_RE = /^(?!-)[\d.A-Za-z-]{1,63}(?<!-)$/;

export function _validateHostname(hostname: string | undefined) {
if (hostname && !HOSTNAME_RE.test(hostname)) {
throw new GetPortError(`Invalid host: ${JSON.stringify(hostname)}`);
}
return hostname;
}
163 changes: 163 additions & 0 deletions src/get-port.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { isSafePort } from "./unsafe-ports";

import type {
GetPortInput,
PortNumber,
GetPortOptions,
HostAddress,
WaitForPortOptions,
} from "./types";

import {
GetPortError,
_findPort,
_fmtOnHost,
_generateRange,
_getLocalHosts,
_tryPort,
_log,
_validateHostname,
} from "./_internal";

export async function getPort(
_userOptions: GetPortInput = {},
): Promise<PortNumber> {
if (typeof _userOptions === "number" || typeof _userOptions === "string") {
_userOptions = { port: Number.parseInt(_userOptions + "") || 0 };
}

const _port = Number(_userOptions.port ?? process.env.PORT ?? 3000);

const options = {
name: "default",
random: _port === 0,
ports: [],
portRange: [],
alternativePortRange: _userOptions.port ? [] : [3000, 3100],
verbose: false,
..._userOptions,
port: _port,
host: _validateHostname(_userOptions.host ?? process.env.HOST),
} as GetPortOptions;

if (options.random) {
return getRandomPort(options.host);
}

// Generate list of ports to check
const portsToCheck: PortNumber[] = [
options.port,
...options.ports,
..._generateRange(...options.portRange),
].filter((port) => {
if (!port) {
return false;
}
if (!isSafePort(port)) {
_log(options.verbose, `Ignoring unsafe port: ${port}`);
return false;
}
return true;
});

// Try to find a port
let availablePort = await _findPort(portsToCheck, options.host);

// Try fallback port range
if (!availablePort && options.alternativePortRange.length > 0) {
availablePort = await _findPort(
_generateRange(...options.alternativePortRange),
options.host,
);
_log(
options.verbose,
`Unable to find an available port (tried ${options.alternativePortRange.join(
"-",
)} ${_fmtOnHost(options.host)}). Using alternative port ${availablePort}`,
);
}

// Try random port
if (!availablePort && _userOptions.random !== false) {
availablePort = await getRandomPort(options.host);
if (availablePort) {
_log(options.verbose, `Using random port ${availablePort}`);
}
}

// Throw error if no port is available
if (!availablePort) {
const triedRanges = [
options.port,
options.portRange.join("-"),
options.alternativePortRange.join("-"),
]
.filter(Boolean)
.join(", ");
throw new GetPortError(
`Unable to find find available port ${_fmtOnHost(
options.host,
)} (tried ${triedRanges})`,
);
}

return availablePort;
}

export async function getRandomPort(host?: HostAddress) {
const port = await checkPort(0, host);
if (port === false) {
throw new GetPortError(
`Unable to find any random port ${_fmtOnHost(host)}`,
);
}
return port;
}

export async function waitForPort(
port: PortNumber,
options: WaitForPortOptions = {},
) {
const delay = options.delay || 500;
const retries = options.retries || 4;
for (let index = retries; index > 0; index--) {
if ((await _tryPort(port, options.host)) === false) {
return;
}
await new Promise((resolve) => setTimeout(resolve, delay));
}
throw new GetPortError(
`Timeout waiting for port ${port} after ${retries} retries with ${delay}ms interval.`,
);
}

export async function checkPort(
port: PortNumber,
host: HostAddress | HostAddress[] = process.env.HOST,
verbose?: boolean,
): Promise<PortNumber | false> {
if (!host) {
host = _getLocalHosts([undefined /* default */, "0.0.0.0"]);
}
if (!Array.isArray(host)) {
return _tryPort(port, host);
}
for (const _host of host) {
const _port = await _tryPort(port, _host);
if (_port === false) {
if (port < 1024 && verbose) {
_log(
verbose,
`Unable to listen to the priviliged port ${port} ${_fmtOnHost(
_host,
)}`,
);
}
return false;
}
if (port === 0 && _port !== 0) {
port = _port;
}
}
return port;
}
Loading

0 comments on commit cf4317c

Please sign in to comment.