Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid breaking existing consumers who use startPort >= 40000 #86

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# node-portfinder [![Build Status](https://api.travis-ci.org/indexzero/node-portfinder.svg)](https://travis-ci.org/indexzero/node-portfinder)
# node-portfinder [![Build Status](https://api.travis-ci.org/http-party/node-portfinder.svg)](https://travis-ci.org/http-party/node-portfinder)

## Installation

Expand Down Expand Up @@ -43,13 +43,13 @@ If `portfinder.getPortPromise()` is called on a Node version without Promise (<4

### Ports search scope

By default `portfinder` will start searching from `8000` and scan until maximum port number (`65535`) is reached.
By default `portfinder` will start searching from `8000` and scan until maximum port number (`40000`) is reached. `40000` is the highest safe port across platforms. If you are not on OSX, you can safely override the max port up to 65535.

You can change this globally by setting:

```js
portfinder.basePort = 3000; // default: 8000
portfinder.highestPort = 3333; // default: 65535
portfinder.highestPort = 3333; // default: 40000; cannot be overriden above 65535
```

or by passing optional options object on each invocation:
Expand All @@ -61,6 +61,16 @@ portfinder.getPort({
}, callback);
```

### Port scan strategy

By default, portfinder will perform a linear scan from the startPort to the stopPort until it has found a free port (or the number of free ports specified in `getPorts`). This can lead to race conditions if `getPort` is called several times in quick succession: the same port may be returned for multiple calls. To avoid this, you can pass `useRandom: true` into the options, in which case ports in the range from `startPort` to `stopPort` will be checked in random order:

```js
portfinder.getPort({
useRandom: true
}, callback);
```

## Run Tests
``` bash
$ npm test
Expand Down
99 changes: 67 additions & 32 deletions lib/portfinder.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,50 +33,55 @@ internals.testPort = function(options, callback) {
//
});

debugTestPort("entered testPort(): trying", options.host, "port", options.port);
var candidatePorts = options.ports.slice()
var currentPort = candidatePorts.pop()

debugTestPort("entered testPort(): trying", options.host, "port", currentPort);

function onListen () {
debugTestPort("done w/ testPort(): OK", options.host, "port", options.port);
debugTestPort("done w/ testPort(): OK", options.host, "port", currentPort);

options.server.removeListener('error', onError);
options.server.close();
callback(null, options.port);
callback(null, currentPort);
}

function onError (err) {
debugTestPort("done w/ testPort(): failed", options.host, "w/ port", options.port, "with error", err.code);
debugTestPort("done w/ testPort(): failed", options.host, "w/ port", currentPort, "with error", err.code);

options.server.removeListener('listening', onListen);

if (!(err.code == 'EADDRINUSE' || err.code == 'EACCES')) {
return callback(err);
}

var nextPort = exports.nextPort(options.port);
if (candidatePorts.length === 0) {
var msg = 'No open ports found in between ' + options.startPort + ' and ' + options.stopPort;
return callback(new Error(msg));

if (nextPort > exports.highestPort) {
return callback(new Error('No open ports available'));
}

internals.testPort({
port: nextPort,
ports: candidatePorts,
host: options.host,
server: options.server
server: options.server,
startPort: options.startPort,
stopPort: options.stopPort
}, callback);
}

options.server.once('error', onError);
options.server.once('listening', onListen);

if (options.host) {
options.server.listen(options.port, options.host);
options.server.listen(currentPort, options.host);
} else {
/*
Judgement of service without host
example:
express().listen(options.port)
*/
options.server.listen(options.port);
options.server.listen(currentPort);
}
};

Expand All @@ -89,8 +94,13 @@ exports.basePort = 8000;
//
// ### @highestPort {Number}
// Largest port number is an unsigned short 2**16 -1=65335
// To avoid collisions with the MacBook Pro touchbar, which binds
// to a random port in the range 40,000 - ~60,000, we only search
// up to 40,000 unless the user explicitly specifies a stopPort higher
// than that. In any event, we will not search above _highestPort (65535).
//
exports.highestPort = 65535;
exports.highestPort = 40000; // The safe highest port; user can override to search higher
var _highestPort = 65535; // The absolute highest port;

//
// ### @basePath {string}
Expand All @@ -108,23 +118,39 @@ exports.getPort = function (options, callback) {
if (!callback) {
callback = options;
options = {};

}

options.port = Number(options.port) || Number(exports.basePort);
options.host = options.host || null;
options.stopPort = Number(options.stopPort) || Number(exports.highestPort);
options.host = options.host || null;

// Validate startPort and stopPort
if (!options.stopPort) {
options.stopPort = Number(exports.highestPort);
} else {
options.stopPort = Math.min(Number(options.stopPort), Number(_highestPort))
}

if(!options.startPort) {
options.startPort = Number(options.port);
if(options.startPort < 0) {
throw Error('Provided options.startPort(' + options.startPort + ') is less than 0, which are cannot be bound.');
}
if(options.stopPort < options.startPort) {
throw Error('Provided options.stopPort(' + options.stopPort + 'is less than options.startPort (' + options.startPort + ')');
}

if (options.startPort > _highestPort) {
throw Error('Provided options.startPort(' + options.startPort + ') is higher than the maximum available port (' + _highestPort + ').');
}

if (options.startPort > exports.highestPort) {
console.warn('Provided options.startPort(' + options.startPort + ') is higher than the recommended stopPort (' + exports.stopPort + '). This may cause issues on some systems');
BarthesSimpson marked this conversation as resolved.
Show resolved Hide resolved
if(options.stopPort <= options.startPort) {
options.stopPort = _highestPort
}
}

if(options.stopPort < options.startPort) {
throw Error('Provided options.stopPort(' + options.stopPort + 'is less than options.startPort (' + options.startPort + ')');
BarthesSimpson marked this conversation as resolved.
Show resolved Hide resolved
}

if (options.host) {

var hasUserGivenHost;
Expand All @@ -141,11 +167,21 @@ exports.getPort = function (options, callback) {

}

var candidatePorts = []
for (i = options.port; i < options.stopPort; i++) {
candidatePorts.push(i)
}

if (options.useRandom) {
randomShuffle(candidatePorts)
} else {
candidatePorts.reverse()
}
var openPorts = [], currentHost;
return async.eachSeries(exports._defaultHosts, function(host, next) {
debugGetPort("in eachSeries() iteration callback: host is", host);

return internals.testPort({ host: host, port: options.port }, function(err, port) {
return internals.testPort({ host: host, ports: candidatePorts, startPort: options.startPort, stopPort: options.stopPort }, function(err, port) {
if (err) {
debugGetPort("in eachSeries() iteration callback testPort() callback", "with an err:", err.code);
currentHost = host;
Expand Down Expand Up @@ -243,17 +279,11 @@ exports.getPorts = function (count, options, callback) {
options = {};
}

var lastPort = null;
async.timesSeries(count, function(index, asyncCallback) {
if (lastPort) {
options.port = exports.nextPort(lastPort);
}

exports.getPort(options, function (err, port) {
if (err) {
asyncCallback(err);
} else {
lastPort = port;
asyncCallback(null, port);
}
});
Expand Down Expand Up @@ -349,14 +379,19 @@ exports.getSocket = function (options, callback) {
};

//
// ### function nextPort (port)
// #### @port {Number} Port to increment from.
// Gets the next port in sequence from the
// specified `port`.
// ### function RandomShuffle (array)
// #### @array {Array} array (of ports) to shuffle.
// Uses the Fisher-Yates algorithm to randomly shuffle
// the array in linear time. This is performed in-place.
//
exports.nextPort = function (port) {
return port + 1;
};
function randomShuffle(array) {
for (var i = array.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = array[i]
array[i] = array[j]
array[j] = tmp
}
}

//
// ### function nextSocket (socketPath)
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "portfinder",
"description": "A simple tool to find an open port on the current machine",
"version": "1.0.21",
"version": "1.0.22",
"author": "Charlie Robbins <[email protected]>",
"repository": {
"type": "git",
Expand Down
26 changes: 18 additions & 8 deletions test/port-finder-multiple-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,19 @@ vows.describe('portfinder').addBatch({
topic: function () {
testHelper(servers, this.callback);
},
"the getPorts() method with an argument of 3": {
"the getPorts() method with an argument of 3 and useRandom: true": {
topic: function () {
portfinder.getPorts(3, this.callback);
portfinder.getPorts(3, {useRandom: true}, this.callback);
},
"should respond with the first three available ports (32773, 32774, 32775)": function (err, ports) {
"should respond with three distinct available ports (>= 32773)": function (err, ports) {
if (err) { debugVows(err); }
assert.isTrue(!err);
assert.deepEqual(ports, [32773, 32774, 32775]);
var seen = new Set()
for (var port of ports) {
assert.isFalse(seen.has(port))
assert.isTrue(port >= 32773)
seen.add(port)
}
}
}
}
Expand All @@ -47,14 +52,19 @@ vows.describe('portfinder').addBatch({

return null;
},
"the getPorts() method with an argument of 3": {
"the getPorts() method with an argument of 3 and useRandom: true": {
topic: function () {
portfinder.getPorts(3, this.callback);
portfinder.getPorts(3, {useRandom: true}, this.callback);
},
"should respond with the first three available ports (32768, 32769, 32770)": function (err, ports) {
"should respond with three distinct available ports (>= 32768)": function (err, ports) {
if (err) { debugVows(err); }
assert.isTrue(!err);
assert.deepEqual(ports, [32768, 32769, 32770]);
var seen = new Set()
for (var port of ports) {
assert.isFalse(seen.has(port))
assert.isTrue(port >= 32773)
seen.add(port)
}
}
}
}
Expand Down
Loading