From c3bbed9545ce749331b32e40ae59712266e84c1b Mon Sep 17 00:00:00 2001 From: Mihkel Eidast Date: Sun, 3 Oct 2021 19:39:27 +0300 Subject: [PATCH] Check all local hosts for port availability (#56) --- .github/workflows/main.yml | 8 ++++-- index.js | 59 +++++++++++++++++++++++++++++++------- readme.md | 6 +++- test.js | 18 ++++++++++++ 4 files changed, 77 insertions(+), 14 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 18531b3..64fc85b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,11 +4,15 @@ on: - pull_request jobs: test: - name: Node.js ${{ matrix.node-version }} - runs-on: ubuntu-latest + name: Node.js ${{ matrix.node-version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest node-version: - 14 - 12 diff --git a/index.js b/index.js index f3e2210..73a7199 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ 'use strict'; const net = require('net'); +const os = require('os'); class Locked extends Error { constructor(port) { @@ -20,17 +21,51 @@ const releaseOldLockedPortsIntervalMs = 1000 * 15; // Lazily create interval on first use let interval; -const getAvailablePort = options => new Promise((resolve, reject) => { - const server = net.createServer(); - server.unref(); - server.on('error', reject); - server.listen(options, () => { - const {port} = server.address(); - server.close(() => { - resolve(port); +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. + const results = new Set([undefined, '0.0.0.0']); + + for (const _interface of Object.values(interfaces)) { + for (const config of _interface) { + results.add(config.address); + } + } + + return results; +}; + +const checkAvailablePort = options => + new Promise((resolve, reject) => { + const server = net.createServer(); + server.unref(); + server.on('error', reject); + server.listen(options, () => { + const {port} = server.address(); + server.close(() => { + resolve(port); + }); }); }); -}); + +const getAvailablePort = async (options, hosts) => { + if (options.host || options.port === 0) { + return checkAvailablePort(options); + } + + for (const host of hosts) { + try { + await checkAvailablePort({port: options.port, host}); // eslint-disable-line no-await-in-loop + } catch (error) { + if (!['EADDRNOTAVAIL', 'EINVAL'].includes(error.code)) { + throw error; + } + } + } + + return options.port; +}; const portCheckSequence = function * (ports) { if (ports) { @@ -59,15 +94,17 @@ module.exports = async options => { } } + const hosts = getLocalHosts(); + for (const port of portCheckSequence(ports)) { try { - let availablePort = await getAvailablePort({...options, port}); // eslint-disable-line no-await-in-loop + let availablePort = await getAvailablePort({...options, port}, hosts); // eslint-disable-line no-await-in-loop while (lockedPorts.old.has(availablePort) || lockedPorts.young.has(availablePort)) { if (port !== 0) { throw new Locked(port); } - availablePort = await getAvailablePort({...options, port}); // eslint-disable-line no-await-in-loop + availablePort = await getAvailablePort({...options, port}, hosts); // eslint-disable-line no-await-in-loop } lockedPorts.young.add(availablePort); diff --git a/readme.md b/readme.md index 5c5f09c..f16fcd6 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,8 @@ # get-port -> Get an available [TCP port](https://en.wikipedia.org/wiki/Port_(computer_networking)) +> Get an available [TCP port](https://en.wikipedia.org/wiki/Port_(computer_networking)). +> +> ## Install @@ -68,6 +70,8 @@ Type: `string` The host on which port resolution should be performed. Can be either an IPv4 or IPv6 address. +By default, `get-port` checks availability on all local addresses defined in [OS network interfaces](https://nodejs.org/api/os.html#os_os_networkinterfaces). If this option is set, it will only check the specified host. + ### getPort.makeRange(from, to) Make a range of ports `from`...`to`. diff --git a/test.js b/test.js index ba4c794..59e41f1 100644 --- a/test.js +++ b/test.js @@ -153,3 +153,21 @@ test('ports are locked for up to 30 seconds', async t => { t.is(port3, port); global.setInterval = setInterval; }); + +const bindPort = async ({port, host}) => { + const server = net.createServer(); + await promisify(server.listen.bind(server))({port, host}); + return server; +}; + +test('preferred ports is bound up with different hosts', async t => { + const desiredPorts = [10990, 10991, 10992, 10993]; + + await bindPort({port: desiredPorts[0]}); + await bindPort({port: desiredPorts[1], host: '0.0.0.0'}); + await bindPort({port: desiredPorts[2], host: '127.0.0.1'}); + + const port = await getPort({port: desiredPorts}); + + t.is(port, desiredPorts[3]); +});