134 lines
		
	
	
		
			3.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			134 lines
		
	
	
		
			3.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| "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;
 |