-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
278 additions
and
260 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.