pick-port
Version:
Get a free TCP or UDP port for the given IP address
106 lines (105 loc) • 4.02 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.pickPort = pickPort;
const net = require("node:net");
const crypto = require("node:crypto");
const Logger_1 = require("./Logger");
const tcp_1 = require("./tcp");
const udp_1 = require("./udp");
const logger = new Logger_1.Logger();
// Store picked ports for the specified reserveTimeout time.
// This Set stores strings/hashes with the form "type:ip:port".
const reserved = new Set();
// Last reserved port (used to optimize the random port lookup).
let lastReservedPort = undefined;
async function pickPort({ type, ip = '0.0.0.0', minPort = 10000, maxPort = 20000, reserveTimeout = 5, }) {
logger.debug(`pickPort() [type:${type}, ip:${ip}, minPort:${minPort}, maxPort:${maxPort}, reserveTimeout:${reserveTimeout}]`);
// Sanity checks.
type = type.toLowerCase();
const family = net.isIP(ip);
if (type !== 'udp' && type !== 'tcp') {
throw new TypeError('invalid type parameter');
}
else if (family !== 4 && family !== 6) {
throw new TypeError('invalid ip parameter');
}
else if (typeof minPort !== 'number' ||
typeof maxPort !== 'number' ||
minPort > maxPort) {
throw new TypeError('invalid minPort/maxPort parameter');
}
else if (typeof reserveTimeout !== 'number') {
throw new TypeError('invalid reserveTimeout parameter');
}
// If last reserved port is not in the given min/max port range, unset it.
if (lastReservedPort !== undefined &&
(lastReservedPort < minPort || lastReservedPort > maxPort)) {
lastReservedPort = undefined;
}
// Take a random port in the range.
// NOTE: Use last reserved port (if any) as initial value since it will be
// incremented at the end of the loop below.
let port = lastReservedPort ?? crypto.randomInt(minPort, maxPort + 1);
let retries = maxPort - minPort + 1;
while (--retries >= 0) {
// Keep the port within the range.
if (++port > maxPort) {
port = minPort;
}
const hash = generateHash(type, ip, port);
// If current port is reserved, try next one.
if (isReserved(hash)) {
continue;
}
// Optimistically reserve the port.
reserve(hash);
try {
switch (type) {
case 'tcp': {
await (0, tcp_1.reserve)(ip, port);
break;
}
case 'udp': {
await (0, udp_1.reserve)(ip, port, family);
break;
}
}
logger.debug(`pickPort() | got available port [type:${type}, ip:${ip}, port:${port}]`);
lastReservedPort = port;
// Unreserve the reserved port after given timeout.
setTimeout(() => unreserve(hash), reserveTimeout * 1000);
return port;
}
catch (error) {
unreserve(hash);
if (error.code === 'EADDRINUSE') {
logger.debug(`pickPort() | port in use [type:${type}, ip:${ip}, port:${port}]`);
continue;
}
else {
logger.warn(`pickPort() | unexpected error trying to bind a port [type:${type}, ip:${ip}, port:${port}]: ${error}`);
throw error;
}
}
}
logger.warn(`pickPort() | no available port in the given port range [type:${type}, ip:${ip}]`);
throw new Error('no available port in the given port range');
}
function generateHash(type, ip, port) {
return `${type}:${ip}:${port}`;
}
function reserve(hash) {
if (isReserved(hash)) {
throw new Error(`reserve() | hash '${hash}' is already reserved`);
}
reserved.add(hash);
}
function unreserve(hash) {
if (!isReserved(hash)) {
throw new Error(`unreserve() | hash '${hash}' is not reserved`);
}
reserved.delete(hash);
}
function isReserved(hash) {
return reserved.has(hash);
}