getPort.js 3.4 KB

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