You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
133 lines
3.5 KiB
133 lines
3.5 KiB
"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;
|
|
|