get-port
Version:
Get an available port
200 lines (156 loc) • 4.89 kB
JavaScript
import net from 'node:net';
import os from 'node:os';
class Locked extends Error {
constructor(port) {
super(`${port} is locked`);
}
}
const lockedPorts = {
old: new Set(),
young: new Set(),
};
// On this interval, the old locked ports are discarded,
// the young locked ports are moved to old locked ports,
// and a new young set for locked ports are created.
const releaseOldLockedPortsIntervalMs = 1000 * 15;
// Keep `reserve` deliberately process-wide by port number.
// It is meant to avoid in-process races, not to model every possible
// IPv4/IPv6 or host-specific bind combination.
const reservedPorts = new Set();
const minPort = 1024;
const maxPort = 65_535;
// Lazily create timeout on first use
let timeout;
const getLocalHosts = () => {
const interfaces = os.networkInterfaces();
// Add undefined value for createServer function to use default host,
// and default IPv4 host in case createServer defaults to IPv6.
const results = new Set([undefined, '0.0.0.0']);
for (const _interface of Object.values(interfaces)) {
for (const config of _interface) {
results.add(config.address);
}
}
return results;
};
const checkAvailablePort = options =>
new Promise((resolve, reject) => {
const server = net.createServer();
server.unref();
server.on('error', reject);
server.listen(options, () => {
const {port} = server.address();
server.close(() => {
resolve(port);
});
});
});
const getAvailablePort = async (options, hosts) => {
if (options.host || options.port === 0) {
return checkAvailablePort(options);
}
for (const host of hosts) {
try {
await checkAvailablePort({port: options.port, host}); // eslint-disable-line no-await-in-loop
} catch (error) {
if (!['EADDRNOTAVAIL', 'EINVAL'].includes(error.code)) {
throw error;
}
}
}
return options.port;
};
const isLockedPort = port => lockedPorts.old.has(port) || lockedPorts.young.has(port) || reservedPorts.has(port);
const portCheckSequence = function * (ports) {
if (ports) {
yield * ports;
}
yield 0; // Fall back to 0 if anything else failed
};
export default async function getPorts(options) {
let ports;
let exclude = new Set();
if (options) {
if (options.port) {
ports = typeof options.port === 'number' ? [options.port] : options.port;
}
if (options.exclude) {
const excludeIterable = options.exclude;
if (typeof excludeIterable[Symbol.iterator] !== 'function') {
throw new TypeError('The `exclude` option must be an iterable.');
}
for (const element of excludeIterable) {
if (typeof element !== 'number') {
throw new TypeError('Each item in the `exclude` option must be a number corresponding to the port you want excluded.');
}
if (!Number.isSafeInteger(element)) {
throw new TypeError(`Number ${element} in the exclude option is not a safe integer and can't be used`);
}
}
exclude = new Set(excludeIterable);
}
}
const {reserve, ...netOptions} = options ?? {};
if (timeout === undefined) {
timeout = setTimeout(() => {
timeout = undefined;
lockedPorts.old = lockedPorts.young;
lockedPorts.young = new Set();
}, releaseOldLockedPortsIntervalMs);
// Does not exist in some environments (Electron, Jest jsdom env, browser, etc).
if (timeout.unref) {
timeout.unref();
}
}
const hosts = getLocalHosts();
for (const port of portCheckSequence(ports)) {
try {
if (exclude.has(port)) {
continue;
}
let availablePort = await getAvailablePort({...netOptions, port}, hosts); // eslint-disable-line no-await-in-loop
while (isLockedPort(availablePort)) {
if (port !== 0) {
throw new Locked(port);
}
availablePort = await getAvailablePort({...netOptions, port}, hosts); // eslint-disable-line no-await-in-loop
}
if (reserve) {
reservedPorts.add(availablePort);
} else {
lockedPorts.young.add(availablePort);
}
return availablePort;
} catch (error) {
if (!['EADDRINUSE', 'EACCES'].includes(error.code) && !(error instanceof Locked)) {
throw error;
}
}
}
throw new Error('No available ports found');
}
export function portNumbers(from, to) {
if (!Number.isInteger(from) || !Number.isInteger(to)) {
throw new TypeError('`from` and `to` must be integer numbers');
}
if (from < minPort || from > maxPort) {
throw new RangeError(`'from' must be between ${minPort} and ${maxPort}`);
}
if (to < minPort || to > maxPort) {
throw new RangeError(`'to' must be between ${minPort} and ${maxPort}`);
}
if (from > to) {
throw new RangeError('`to` must be greater than or equal to `from`');
}
const generator = function * (from, to) {
for (let port = from; port <= to; port++) {
yield port;
}
};
return generator(from, to);
}
export function clearLockedPorts() {
lockedPorts.old.clear();
lockedPorts.young.clear();
reservedPorts.clear();
}