Skip to content

Commit

Permalink
Check all local hosts for port availability (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
mihkeleidast committed Oct 3, 2021
1 parent 260c667 commit c3bbed9
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 14 deletions.
8 changes: 6 additions & 2 deletions .github/workflows/main.yml
Expand Up @@ -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
Expand Down
59 changes: 48 additions & 11 deletions index.js
@@ -1,5 +1,6 @@
'use strict';
const net = require('net');
const os = require('os');

class Locked extends Error {
constructor(port) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 5 additions & 1 deletion 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

Expand Down Expand Up @@ -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`.
Expand Down
18 changes: 18 additions & 0 deletions test.js
Expand Up @@ -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]);
});

0 comments on commit c3bbed9

Please sign in to comment.