[6a3a178] | 1 | /*
|
---|
| 2 | * portfinder.js: A simple tool to find an open port on the current machine.
|
---|
| 3 | *
|
---|
| 4 | * (C) 2011, Charlie Robbins
|
---|
| 5 | *
|
---|
| 6 | */
|
---|
| 7 |
|
---|
| 8 | "use strict";
|
---|
| 9 |
|
---|
| 10 | var fs = require('fs'),
|
---|
| 11 | os = require('os'),
|
---|
| 12 | net = require('net'),
|
---|
| 13 | path = require('path'),
|
---|
| 14 | _async = require('async'),
|
---|
| 15 | debug = require('debug'),
|
---|
| 16 | mkdirp = require('mkdirp').mkdirp;
|
---|
| 17 |
|
---|
| 18 | var debugTestPort = debug('portfinder:testPort'),
|
---|
| 19 | debugGetPort = debug('portfinder:getPort'),
|
---|
| 20 | debugDefaultHosts = debug('portfinder:defaultHosts');
|
---|
| 21 |
|
---|
| 22 | var internals = {};
|
---|
| 23 |
|
---|
| 24 | internals.testPort = function(options, callback) {
|
---|
| 25 | if (!callback) {
|
---|
| 26 | callback = options;
|
---|
| 27 | options = {};
|
---|
| 28 | }
|
---|
| 29 |
|
---|
| 30 | options.server = options.server || net.createServer(function () {
|
---|
| 31 | //
|
---|
| 32 | // Create an empty listener for the port testing server.
|
---|
| 33 | //
|
---|
| 34 | });
|
---|
| 35 |
|
---|
| 36 | debugTestPort("entered testPort(): trying", options.host, "port", options.port);
|
---|
| 37 |
|
---|
| 38 | function onListen () {
|
---|
| 39 | debugTestPort("done w/ testPort(): OK", options.host, "port", options.port);
|
---|
| 40 |
|
---|
| 41 | options.server.removeListener('error', onError);
|
---|
| 42 | options.server.close();
|
---|
| 43 | callback(null, options.port);
|
---|
| 44 | }
|
---|
| 45 |
|
---|
| 46 | function onError (err) {
|
---|
| 47 | debugTestPort("done w/ testPort(): failed", options.host, "w/ port", options.port, "with error", err.code);
|
---|
| 48 |
|
---|
| 49 | options.server.removeListener('listening', onListen);
|
---|
| 50 |
|
---|
| 51 | if (!(err.code == 'EADDRINUSE' || err.code == 'EACCES')) {
|
---|
| 52 | return callback(err);
|
---|
| 53 | }
|
---|
| 54 |
|
---|
| 55 | var nextPort = exports.nextPort(options.port);
|
---|
| 56 |
|
---|
| 57 | if (nextPort > exports.highestPort) {
|
---|
| 58 | return callback(new Error('No open ports available'));
|
---|
| 59 | }
|
---|
| 60 |
|
---|
| 61 | internals.testPort({
|
---|
| 62 | port: nextPort,
|
---|
| 63 | host: options.host,
|
---|
| 64 | server: options.server
|
---|
| 65 | }, callback);
|
---|
| 66 | }
|
---|
| 67 |
|
---|
| 68 | options.server.once('error', onError);
|
---|
| 69 | options.server.once('listening', onListen);
|
---|
| 70 |
|
---|
| 71 | if (options.host) {
|
---|
| 72 | options.server.listen(options.port, options.host);
|
---|
| 73 | } else {
|
---|
| 74 | /*
|
---|
| 75 | Judgement of service without host
|
---|
| 76 | example:
|
---|
| 77 | express().listen(options.port)
|
---|
| 78 | */
|
---|
| 79 | options.server.listen(options.port);
|
---|
| 80 | }
|
---|
| 81 | };
|
---|
| 82 |
|
---|
| 83 | //
|
---|
| 84 | // ### @basePort {Number}
|
---|
| 85 | // The lowest port to begin any port search from
|
---|
| 86 | //
|
---|
| 87 | exports.basePort = 8000;
|
---|
| 88 |
|
---|
| 89 | //
|
---|
| 90 | // ### @highestPort {Number}
|
---|
| 91 | // Largest port number is an unsigned short 2**16 -1=65335
|
---|
| 92 | //
|
---|
| 93 | exports.highestPort = 65535;
|
---|
| 94 |
|
---|
| 95 | //
|
---|
| 96 | // ### @basePath {string}
|
---|
| 97 | // Default path to begin any socket search from
|
---|
| 98 | //
|
---|
| 99 | exports.basePath = '/tmp/portfinder'
|
---|
| 100 |
|
---|
| 101 | //
|
---|
| 102 | // ### function getPort (options, callback)
|
---|
| 103 | // #### @options {Object} Settings to use when finding the necessary port
|
---|
| 104 | // #### @callback {function} Continuation to respond to when complete.
|
---|
| 105 | // Responds with a unbound port on the current machine.
|
---|
| 106 | //
|
---|
| 107 | exports.getPort = function (options, callback) {
|
---|
| 108 | if (!callback) {
|
---|
| 109 | callback = options;
|
---|
| 110 | options = {};
|
---|
| 111 |
|
---|
| 112 | }
|
---|
| 113 |
|
---|
| 114 | options.port = Number(options.port) || Number(exports.basePort);
|
---|
| 115 | options.host = options.host || null;
|
---|
| 116 | options.stopPort = Number(options.stopPort) || Number(exports.highestPort);
|
---|
| 117 |
|
---|
| 118 | if(!options.startPort) {
|
---|
| 119 | options.startPort = Number(options.port);
|
---|
| 120 | if(options.startPort < 0) {
|
---|
| 121 | throw Error('Provided options.startPort(' + options.startPort + ') is less than 0, which are cannot be bound.');
|
---|
| 122 | }
|
---|
| 123 | if(options.stopPort < options.startPort) {
|
---|
| 124 | throw Error('Provided options.stopPort(' + options.stopPort + 'is less than options.startPort (' + options.startPort + ')');
|
---|
| 125 | }
|
---|
| 126 | }
|
---|
| 127 |
|
---|
| 128 | if (options.host) {
|
---|
| 129 |
|
---|
| 130 | var hasUserGivenHost;
|
---|
| 131 | for (var i = 0; i < exports._defaultHosts.length; i++) {
|
---|
| 132 | if (exports._defaultHosts[i] === options.host) {
|
---|
| 133 | hasUserGivenHost = true;
|
---|
| 134 | break;
|
---|
| 135 | }
|
---|
| 136 | }
|
---|
| 137 |
|
---|
| 138 | if (!hasUserGivenHost) {
|
---|
| 139 | exports._defaultHosts.push(options.host);
|
---|
| 140 | }
|
---|
| 141 |
|
---|
| 142 | }
|
---|
| 143 |
|
---|
| 144 | var openPorts = [], currentHost;
|
---|
| 145 | return _async.eachSeries(exports._defaultHosts, function(host, next) {
|
---|
| 146 | debugGetPort("in eachSeries() iteration callback: host is", host);
|
---|
| 147 |
|
---|
| 148 | return internals.testPort({ host: host, port: options.port }, function(err, port) {
|
---|
| 149 | if (err) {
|
---|
| 150 | debugGetPort("in eachSeries() iteration callback testPort() callback", "with an err:", err.code);
|
---|
| 151 | currentHost = host;
|
---|
| 152 | return next(err);
|
---|
| 153 | } else {
|
---|
| 154 | debugGetPort("in eachSeries() iteration callback testPort() callback",
|
---|
| 155 | "with a success for port", port);
|
---|
| 156 | openPorts.push(port);
|
---|
| 157 | return next();
|
---|
| 158 | }
|
---|
| 159 | });
|
---|
| 160 | }, function(err) {
|
---|
| 161 |
|
---|
| 162 | if (err) {
|
---|
| 163 | debugGetPort("in eachSeries() result callback: err is", err);
|
---|
| 164 | // If we get EADDRNOTAVAIL it means the host is not bindable, so remove it
|
---|
| 165 | // from exports._defaultHosts and start over. For ubuntu, we use EINVAL for the same
|
---|
| 166 | if (err.code === 'EADDRNOTAVAIL' || err.code === 'EINVAL') {
|
---|
| 167 | if (options.host === currentHost) {
|
---|
| 168 | // if bad address matches host given by user, tell them
|
---|
| 169 | //
|
---|
| 170 | // NOTE: We may need to one day handle `my-non-existent-host.local` if users
|
---|
| 171 | // report frustration with passing in hostnames that DONT map to bindable
|
---|
| 172 | // hosts, without showing them a good error.
|
---|
| 173 | var msg = 'Provided host ' + options.host + ' could NOT be bound. Please provide a different host address or hostname';
|
---|
| 174 | return callback(Error(msg));
|
---|
| 175 | } else {
|
---|
| 176 | var idx = exports._defaultHosts.indexOf(currentHost);
|
---|
| 177 | exports._defaultHosts.splice(idx, 1);
|
---|
| 178 | return exports.getPort(options, callback);
|
---|
| 179 | }
|
---|
| 180 | } else {
|
---|
| 181 | // error is not accounted for, file ticket, handle special case
|
---|
| 182 | return callback(err);
|
---|
| 183 | }
|
---|
| 184 | }
|
---|
| 185 |
|
---|
| 186 | // sort so we can compare first host to last host
|
---|
| 187 | openPorts.sort(function(a, b) {
|
---|
| 188 | return a - b;
|
---|
| 189 | });
|
---|
| 190 |
|
---|
| 191 | debugGetPort("in eachSeries() result callback: openPorts is", openPorts);
|
---|
| 192 |
|
---|
| 193 | if (openPorts[0] === openPorts[openPorts.length-1]) {
|
---|
| 194 | // if first === last, we found an open port
|
---|
| 195 | if(openPorts[0] <= options.stopPort) {
|
---|
| 196 | return callback(null, openPorts[0]);
|
---|
| 197 | }
|
---|
| 198 | else {
|
---|
| 199 | var msg = 'No open ports found in between '+ options.startPort + ' and ' + options.stopPort;
|
---|
| 200 | return callback(Error(msg));
|
---|
| 201 | }
|
---|
| 202 | } else {
|
---|
| 203 | // otherwise, try again, using sorted port, aka, highest open for >= 1 host
|
---|
| 204 | return exports.getPort({ port: openPorts.pop(), host: options.host, startPort: options.startPort, stopPort: options.stopPort }, callback);
|
---|
| 205 | }
|
---|
| 206 |
|
---|
| 207 | });
|
---|
| 208 | };
|
---|
| 209 |
|
---|
| 210 | //
|
---|
| 211 | // ### function getPortPromise (options)
|
---|
| 212 | // #### @options {Object} Settings to use when finding the necessary port
|
---|
| 213 | // Responds a promise to an unbound port on the current machine.
|
---|
| 214 | //
|
---|
| 215 | exports.getPortPromise = function (options) {
|
---|
| 216 | if (typeof Promise !== 'function') {
|
---|
| 217 | throw Error('Native promise support is not available in this version of node.' +
|
---|
| 218 | 'Please install a polyfill and assign Promise to global.Promise before calling this method');
|
---|
| 219 | }
|
---|
| 220 | if (!options) {
|
---|
| 221 | options = {};
|
---|
| 222 | }
|
---|
| 223 | return new Promise(function(resolve, reject) {
|
---|
| 224 | exports.getPort(options, function(err, port) {
|
---|
| 225 | if (err) {
|
---|
| 226 | return reject(err);
|
---|
| 227 | }
|
---|
| 228 | resolve(port);
|
---|
| 229 | });
|
---|
| 230 | });
|
---|
| 231 | }
|
---|
| 232 |
|
---|
| 233 | //
|
---|
| 234 | // ### function getPorts (count, options, callback)
|
---|
| 235 | // #### @count {Number} The number of ports to find
|
---|
| 236 | // #### @options {Object} Settings to use when finding the necessary port
|
---|
| 237 | // #### @callback {function} Continuation to respond to when complete.
|
---|
| 238 | // Responds with an array of unbound ports on the current machine.
|
---|
| 239 | //
|
---|
| 240 | exports.getPorts = function (count, options, callback) {
|
---|
| 241 | if (!callback) {
|
---|
| 242 | callback = options;
|
---|
| 243 | options = {};
|
---|
| 244 | }
|
---|
| 245 |
|
---|
| 246 | var lastPort = null;
|
---|
| 247 | _async.timesSeries(count, function(index, asyncCallback) {
|
---|
| 248 | if (lastPort) {
|
---|
| 249 | options.port = exports.nextPort(lastPort);
|
---|
| 250 | }
|
---|
| 251 |
|
---|
| 252 | exports.getPort(options, function (err, port) {
|
---|
| 253 | if (err) {
|
---|
| 254 | asyncCallback(err);
|
---|
| 255 | } else {
|
---|
| 256 | lastPort = port;
|
---|
| 257 | asyncCallback(null, port);
|
---|
| 258 | }
|
---|
| 259 | });
|
---|
| 260 | }, callback);
|
---|
| 261 | };
|
---|
| 262 |
|
---|
| 263 | //
|
---|
| 264 | // ### function getSocket (options, callback)
|
---|
| 265 | // #### @options {Object} Settings to use when finding the necessary port
|
---|
| 266 | // #### @callback {function} Continuation to respond to when complete.
|
---|
| 267 | // Responds with a unbound socket using the specified directory and base
|
---|
| 268 | // name on the current machine.
|
---|
| 269 | //
|
---|
| 270 | exports.getSocket = function (options, callback) {
|
---|
| 271 | if (!callback) {
|
---|
| 272 | callback = options;
|
---|
| 273 | options = {};
|
---|
| 274 | }
|
---|
| 275 |
|
---|
| 276 | options.mod = options.mod || parseInt(755, 8);
|
---|
| 277 | options.path = options.path || exports.basePath + '.sock';
|
---|
| 278 |
|
---|
| 279 | //
|
---|
| 280 | // Tests the specified socket
|
---|
| 281 | //
|
---|
| 282 | function testSocket () {
|
---|
| 283 | fs.stat(options.path, function (err) {
|
---|
| 284 | //
|
---|
| 285 | // If file we're checking doesn't exist (thus, stating it emits ENOENT),
|
---|
| 286 | // we should be OK with listening on this socket.
|
---|
| 287 | //
|
---|
| 288 | if (err) {
|
---|
| 289 | if (err.code == 'ENOENT') {
|
---|
| 290 | callback(null, options.path);
|
---|
| 291 | }
|
---|
| 292 | else {
|
---|
| 293 | callback(err);
|
---|
| 294 | }
|
---|
| 295 | }
|
---|
| 296 | else {
|
---|
| 297 | //
|
---|
| 298 | // This file exists, so it isn't possible to listen on it. Lets try
|
---|
| 299 | // next socket.
|
---|
| 300 | //
|
---|
| 301 | options.path = exports.nextSocket(options.path);
|
---|
| 302 | exports.getSocket(options, callback);
|
---|
| 303 | }
|
---|
| 304 | });
|
---|
| 305 | }
|
---|
| 306 |
|
---|
| 307 | //
|
---|
| 308 | // Create the target `dir` then test connection
|
---|
| 309 | // against the socket.
|
---|
| 310 | //
|
---|
| 311 | function createAndTestSocket (dir) {
|
---|
| 312 | mkdirp(dir, options.mod, function (err) {
|
---|
| 313 | if (err) {
|
---|
| 314 | return callback(err);
|
---|
| 315 | }
|
---|
| 316 |
|
---|
| 317 | options.exists = true;
|
---|
| 318 | testSocket();
|
---|
| 319 | });
|
---|
| 320 | }
|
---|
| 321 |
|
---|
| 322 | //
|
---|
| 323 | // Check if the parent directory of the target
|
---|
| 324 | // socket path exists. If it does, test connection
|
---|
| 325 | // against the socket. Otherwise, create the directory
|
---|
| 326 | // then test connection.
|
---|
| 327 | //
|
---|
| 328 | function checkAndTestSocket () {
|
---|
| 329 | var dir = path.dirname(options.path);
|
---|
| 330 |
|
---|
| 331 | fs.stat(dir, function (err, stats) {
|
---|
| 332 | if (err || !stats.isDirectory()) {
|
---|
| 333 | return createAndTestSocket(dir);
|
---|
| 334 | }
|
---|
| 335 |
|
---|
| 336 | options.exists = true;
|
---|
| 337 | testSocket();
|
---|
| 338 | });
|
---|
| 339 | }
|
---|
| 340 |
|
---|
| 341 | //
|
---|
| 342 | // If it has been explicitly stated that the
|
---|
| 343 | // target `options.path` already exists, then
|
---|
| 344 | // simply test the socket.
|
---|
| 345 | //
|
---|
| 346 | return options.exists
|
---|
| 347 | ? testSocket()
|
---|
| 348 | : checkAndTestSocket();
|
---|
| 349 | };
|
---|
| 350 |
|
---|
| 351 | //
|
---|
| 352 | // ### function nextPort (port)
|
---|
| 353 | // #### @port {Number} Port to increment from.
|
---|
| 354 | // Gets the next port in sequence from the
|
---|
| 355 | // specified `port`.
|
---|
| 356 | //
|
---|
| 357 | exports.nextPort = function (port) {
|
---|
| 358 | return port + 1;
|
---|
| 359 | };
|
---|
| 360 |
|
---|
| 361 | //
|
---|
| 362 | // ### function nextSocket (socketPath)
|
---|
| 363 | // #### @socketPath {string} Path to increment from
|
---|
| 364 | // Gets the next socket path in sequence from the
|
---|
| 365 | // specified `socketPath`.
|
---|
| 366 | //
|
---|
| 367 | exports.nextSocket = function (socketPath) {
|
---|
| 368 | var dir = path.dirname(socketPath),
|
---|
| 369 | name = path.basename(socketPath, '.sock'),
|
---|
| 370 | match = name.match(/^([a-zA-z]+)(\d*)$/i),
|
---|
| 371 | index = parseInt(match[2]),
|
---|
| 372 | base = match[1];
|
---|
| 373 | if (isNaN(index)) {
|
---|
| 374 | index = 0;
|
---|
| 375 | }
|
---|
| 376 |
|
---|
| 377 | index += 1;
|
---|
| 378 | return path.join(dir, base + index + '.sock');
|
---|
| 379 | };
|
---|
| 380 |
|
---|
| 381 | /**
|
---|
| 382 | * @desc List of internal hostnames provided by your machine. A user
|
---|
| 383 | * provided hostname may also be provided when calling portfinder.getPort,
|
---|
| 384 | * which would then be added to the default hosts we lookup and return here.
|
---|
| 385 | *
|
---|
| 386 | * @return {array}
|
---|
| 387 | *
|
---|
| 388 | * Long Form Explantion:
|
---|
| 389 | *
|
---|
| 390 | * - Input: (os.networkInterfaces() w/ MacOS 10.11.5+ and running a VM)
|
---|
| 391 | *
|
---|
| 392 | * { lo0:
|
---|
| 393 | * [ { address: '::1',
|
---|
| 394 | * netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
|
---|
| 395 | * family: 'IPv6',
|
---|
| 396 | * mac: '00:00:00:00:00:00',
|
---|
| 397 | * scopeid: 0,
|
---|
| 398 | * internal: true },
|
---|
| 399 | * { address: '127.0.0.1',
|
---|
| 400 | * netmask: '255.0.0.0',
|
---|
| 401 | * family: 'IPv4',
|
---|
| 402 | * mac: '00:00:00:00:00:00',
|
---|
| 403 | * internal: true },
|
---|
| 404 | * { address: 'fe80::1',
|
---|
| 405 | * netmask: 'ffff:ffff:ffff:ffff::',
|
---|
| 406 | * family: 'IPv6',
|
---|
| 407 | * mac: '00:00:00:00:00:00',
|
---|
| 408 | * scopeid: 1,
|
---|
| 409 | * internal: true } ],
|
---|
| 410 | * en0:
|
---|
| 411 | * [ { address: 'fe80::a299:9bff:fe17:766d',
|
---|
| 412 | * netmask: 'ffff:ffff:ffff:ffff::',
|
---|
| 413 | * family: 'IPv6',
|
---|
| 414 | * mac: 'a0:99:9b:17:76:6d',
|
---|
| 415 | * scopeid: 4,
|
---|
| 416 | * internal: false },
|
---|
| 417 | * { address: '10.0.1.22',
|
---|
| 418 | * netmask: '255.255.255.0',
|
---|
| 419 | * family: 'IPv4',
|
---|
| 420 | * mac: 'a0:99:9b:17:76:6d',
|
---|
| 421 | * internal: false } ],
|
---|
| 422 | * awdl0:
|
---|
| 423 | * [ { address: 'fe80::48a8:37ff:fe34:aaef',
|
---|
| 424 | * netmask: 'ffff:ffff:ffff:ffff::',
|
---|
| 425 | * family: 'IPv6',
|
---|
| 426 | * mac: '4a:a8:37:34:aa:ef',
|
---|
| 427 | * scopeid: 8,
|
---|
| 428 | * internal: false } ],
|
---|
| 429 | * vnic0:
|
---|
| 430 | * [ { address: '10.211.55.2',
|
---|
| 431 | * netmask: '255.255.255.0',
|
---|
| 432 | * family: 'IPv4',
|
---|
| 433 | * mac: '00:1c:42:00:00:08',
|
---|
| 434 | * internal: false } ],
|
---|
| 435 | * vnic1:
|
---|
| 436 | * [ { address: '10.37.129.2',
|
---|
| 437 | * netmask: '255.255.255.0',
|
---|
| 438 | * family: 'IPv4',
|
---|
| 439 | * mac: '00:1c:42:00:00:09',
|
---|
| 440 | * internal: false } ] }
|
---|
| 441 | *
|
---|
| 442 | * - Output:
|
---|
| 443 | *
|
---|
| 444 | * [
|
---|
| 445 | * '0.0.0.0',
|
---|
| 446 | * '::1',
|
---|
| 447 | * '127.0.0.1',
|
---|
| 448 | * 'fe80::1',
|
---|
| 449 | * '10.0.1.22',
|
---|
| 450 | * 'fe80::48a8:37ff:fe34:aaef',
|
---|
| 451 | * '10.211.55.2',
|
---|
| 452 | * '10.37.129.2'
|
---|
| 453 | * ]
|
---|
| 454 | *
|
---|
| 455 | * Note we export this so we can use it in our tests, otherwise this API is private
|
---|
| 456 | */
|
---|
| 457 | exports._defaultHosts = (function() {
|
---|
| 458 | var interfaces = {};
|
---|
| 459 | try{
|
---|
| 460 | interfaces = os.networkInterfaces();
|
---|
| 461 | }
|
---|
| 462 | catch(e) {
|
---|
| 463 | // As of October 2016, Windows Subsystem for Linux (WSL) does not support
|
---|
| 464 | // the os.networkInterfaces() call and throws instead. For this platform,
|
---|
| 465 | // assume 0.0.0.0 as the only address
|
---|
| 466 | //
|
---|
| 467 | // - https://github.com/Microsoft/BashOnWindows/issues/468
|
---|
| 468 | //
|
---|
| 469 | // - Workaround is a mix of good work from the community:
|
---|
| 470 | // - https://github.com/http-party/node-portfinder/commit/8d7e30a648ff5034186551fa8a6652669dec2f2f
|
---|
| 471 | // - https://github.com/yarnpkg/yarn/pull/772/files
|
---|
| 472 | if (e.syscall === 'uv_interface_addresses') {
|
---|
| 473 | // swallow error because we're just going to use defaults
|
---|
| 474 | // documented @ https://github.com/nodejs/node/blob/4b65a65e75f48ff447cabd5500ce115fb5ad4c57/doc/api/net.md#L231
|
---|
| 475 | } else {
|
---|
| 476 | throw e;
|
---|
| 477 | }
|
---|
| 478 | }
|
---|
| 479 |
|
---|
| 480 | var interfaceNames = Object.keys(interfaces),
|
---|
| 481 | hiddenButImportantHost = '0.0.0.0', // !important - dont remove, hence the naming :)
|
---|
| 482 | results = [hiddenButImportantHost];
|
---|
| 483 | for (var i = 0; i < interfaceNames.length; i++) {
|
---|
| 484 | var _interface = interfaces[interfaceNames[i]];
|
---|
| 485 | for (var j = 0; j < _interface.length; j++) {
|
---|
| 486 | var curr = _interface[j];
|
---|
| 487 | results.push(curr.address);
|
---|
| 488 | }
|
---|
| 489 | }
|
---|
| 490 |
|
---|
| 491 | // add null value, For createServer function, do not use host.
|
---|
| 492 | results.push(null);
|
---|
| 493 |
|
---|
| 494 | debugDefaultHosts("exports._defaultHosts is: %o", results);
|
---|
| 495 |
|
---|
| 496 | return results;
|
---|
| 497 | }());
|
---|