133 lines
3.5 KiB
JavaScript
Executable file
133 lines
3.5 KiB
JavaScript
Executable file
"use strict";
|
|
|
|
/*
|
|
* Based on the packages get-port https://www.npmjs.com/package/get-port
|
|
* and portfinder https://www.npmjs.com/package/portfinder
|
|
* The code structure is similar to get-port, but it searches
|
|
* ports deterministically like portfinder
|
|
*/
|
|
const net = require("net");
|
|
const os = require("os");
|
|
|
|
const minPort = 1024;
|
|
const maxPort = 65_535;
|
|
|
|
/**
|
|
* @return {Set<string|undefined>}
|
|
*/
|
|
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.
|
|
// eslint-disable-next-line no-undefined
|
|
const results = new Set([undefined, "0.0.0.0"]);
|
|
|
|
for (const _interface of Object.values(interfaces)) {
|
|
if (_interface) {
|
|
for (const config of _interface) {
|
|
results.add(config.address);
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
};
|
|
|
|
/**
|
|
* @param {number} basePort
|
|
* @param {string | undefined} host
|
|
* @return {Promise<number>}
|
|
*/
|
|
const checkAvailablePort = (basePort, host) =>
|
|
new Promise((resolve, reject) => {
|
|
const server = net.createServer();
|
|
server.unref();
|
|
server.on("error", reject);
|
|
|
|
server.listen(basePort, host, () => {
|
|
// Next line should return AdressInfo because we're calling it after listen() and before close()
|
|
const { port } = /** @type {import("net").AddressInfo} */ (
|
|
server.address()
|
|
);
|
|
server.close(() => {
|
|
resolve(port);
|
|
});
|
|
});
|
|
});
|
|
|
|
/**
|
|
* @param {number} port
|
|
* @param {Set<string|undefined>} hosts
|
|
* @return {Promise<number>}
|
|
*/
|
|
const getAvailablePort = async (port, hosts) => {
|
|
/**
|
|
* Errors that mean that host is not available.
|
|
* @type {Set<string | undefined>}
|
|
*/
|
|
const nonExistentInterfaceErrors = new Set(["EADDRNOTAVAIL", "EINVAL"]);
|
|
/* Check if the post is available on every local host name */
|
|
for (const host of hosts) {
|
|
try {
|
|
await checkAvailablePort(port, host); // eslint-disable-line no-await-in-loop
|
|
} catch (error) {
|
|
/* We throw an error only if the interface exists */
|
|
if (
|
|
!nonExistentInterfaceErrors.has(
|
|
/** @type {NodeJS.ErrnoException} */ (error).code
|
|
)
|
|
) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
return port;
|
|
};
|
|
|
|
/**
|
|
* @param {number} basePort
|
|
* @param {string=} host
|
|
* @return {Promise<number>}
|
|
*/
|
|
async function getPorts(basePort, host) {
|
|
if (basePort < minPort || basePort > maxPort) {
|
|
throw new Error(`Port number must lie between ${minPort} and ${maxPort}`);
|
|
}
|
|
|
|
let port = basePort;
|
|
const localhosts = getLocalHosts();
|
|
let hosts;
|
|
if (host && !localhosts.has(host)) {
|
|
hosts = new Set([host]);
|
|
} else {
|
|
/* If the host is equivalent to localhost
|
|
we need to check every equivalent host
|
|
else the port might falsely appear as available
|
|
on some operating systems */
|
|
hosts = localhosts;
|
|
}
|
|
/** @type {Set<string | undefined>} */
|
|
const portUnavailableErrors = new Set(["EADDRINUSE", "EACCES"]);
|
|
while (port <= maxPort) {
|
|
try {
|
|
const availablePort = await getAvailablePort(port, hosts); // eslint-disable-line no-await-in-loop
|
|
return availablePort;
|
|
} catch (error) {
|
|
/* Try next port if port is busy; throw for any other error */
|
|
if (
|
|
!portUnavailableErrors.has(
|
|
/** @type {NodeJS.ErrnoException} */ (error).code
|
|
)
|
|
) {
|
|
throw error;
|
|
}
|
|
port += 1;
|
|
}
|
|
}
|
|
|
|
throw new Error("No available ports found");
|
|
}
|
|
|
|
module.exports = getPorts;
|