UNPKG

3.55 kBJavaScriptView Raw
1"use strict";
2
3/*
4 * Based on the packages get-port https://www.npmjs.com/package/get-port
5 * and portfinder https://www.npmjs.com/package/portfinder
6 * The code structure is similar to get-port, but it searches
7 * ports deterministically like portfinder
8 */
9const net = require("net");
10const os = require("os");
11
12const minPort = 1024;
13const maxPort = 65_535;
14
15/**
16 * @return {Set<string|undefined>}
17 */
18const getLocalHosts = () => {
19 const interfaces = os.networkInterfaces();
20
21 // Add undefined value for createServer function to use default host,
22 // and default IPv4 host in case createServer defaults to IPv6.
23 // eslint-disable-next-line no-undefined
24 const results = new Set([undefined, "0.0.0.0"]);
25
26 for (const _interface of Object.values(interfaces)) {
27 if (_interface) {
28 for (const config of _interface) {
29 results.add(config.address);
30 }
31 }
32 }
33
34 return results;
35};
36
37/**
38 * @param {number} basePort
39 * @param {string | undefined} host
40 * @return {Promise<number>}
41 */
42const checkAvailablePort = (basePort, host) =>
43 new Promise((resolve, reject) => {
44 const server = net.createServer();
45 server.unref();
46 server.on("error", reject);
47
48 server.listen(basePort, host, () => {
49 // Next line should return AdressInfo because we're calling it after listen() and before close()
50 const { port } = /** @type {import("net").AddressInfo} */ (
51 server.address()
52 );
53 server.close(() => {
54 resolve(port);
55 });
56 });
57 });
58
59/**
60 * @param {number} port
61 * @param {Set<string|undefined>} hosts
62 * @return {Promise<number>}
63 */
64const getAvailablePort = async (port, hosts) => {
65 /**
66 * Errors that mean that host is not available.
67 * @type {Set<string | undefined>}
68 */
69 const nonExistentInterfaceErrors = new Set(["EADDRNOTAVAIL", "EINVAL"]);
70 /* Check if the post is available on every local host name */
71 for (const host of hosts) {
72 try {
73 await checkAvailablePort(port, host); // eslint-disable-line no-await-in-loop
74 } catch (error) {
75 /* We throw an error only if the interface exists */
76 if (
77 !nonExistentInterfaceErrors.has(
78 /** @type {NodeJS.ErrnoException} */ (error).code
79 )
80 ) {
81 throw error;
82 }
83 }
84 }
85
86 return port;
87};
88
89/**
90 * @param {number} basePort
91 * @param {string=} host
92 * @return {Promise<number>}
93 */
94async function getPorts(basePort, host) {
95 if (basePort < minPort || basePort > maxPort) {
96 throw new Error(`Port number must lie between ${minPort} and ${maxPort}`);
97 }
98
99 let port = basePort;
100 const localhosts = getLocalHosts();
101 let hosts;
102 if (host && !localhosts.has(host)) {
103 hosts = new Set([host]);
104 } else {
105 /* If the host is equivalent to localhost
106 we need to check every equivalent host
107 else the port might falsely appear as available
108 on some operating systems */
109 hosts = localhosts;
110 }
111 /** @type {Set<string | undefined>} */
112 const portUnavailableErrors = new Set(["EADDRINUSE", "EACCES"]);
113 while (port <= maxPort) {
114 try {
115 const availablePort = await getAvailablePort(port, hosts); // eslint-disable-line no-await-in-loop
116 return availablePort;
117 } catch (error) {
118 /* Try next port if port is busy; throw for any other error */
119 if (
120 !portUnavailableErrors.has(
121 /** @type {NodeJS.ErrnoException} */ (error).code
122 )
123 ) {
124 throw error;
125 }
126 port += 1;
127 }
128 }
129
130 throw new Error("No available ports found");
131}
132
133module.exports = getPorts;