diff --git a/bin/utils.js b/bin/utils.js index 21873a1509..b0c03fc19a 100644 --- a/bin/utils.js +++ b/bin/utils.js @@ -8,6 +8,7 @@ space-before-function-paren */ const open = require('opn'); +const portfinder = require('portfinder'); const colors = { info (useColor, msg) { @@ -105,10 +106,44 @@ function bonjour (options) { }); } +function tryParseInt(input) { + const output = parseInt(input, 10); + if (Number.isNaN(output)) { + return null; + } + return output; +} + +function findPort(server, defaultPort, defaultPortRetry, fn) { + let tryCount = 0; + server.listeningApp.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + if (tryCount < defaultPortRetry) { + portfinder.basePort = defaultPort; + tryCount += 1; + portfinder.getPort((err, port) => { + fn(err, port); + }); + } else { + fn(err); + } + } else { + throw err; + } + }); + portfinder.basePort = defaultPort; + tryCount += 1; + portfinder.getPort((err, port) => { + fn(err, port); + }); +} + module.exports = { status, colors, version, bonjour, - defaultTo + defaultTo, + tryParseInt, + findPort }; diff --git a/bin/webpack-dev-server.js b/bin/webpack-dev-server.js index a58a86564a..cb4097101e 100755 --- a/bin/webpack-dev-server.js +++ b/bin/webpack-dev-server.js @@ -19,7 +19,6 @@ const fs = require('fs'); const net = require('net'); const path = require('path'); -const portfinder = require('portfinder'); const importLocal = require('import-local'); const yargs = require('yargs'); @@ -32,7 +31,9 @@ const { status, version, bonjour, - defaultTo + defaultTo, + tryParseInt, + findPort } = require('./utils'); const Server = require('../lib/Server'); @@ -97,6 +98,12 @@ const config = require('webpack-cli/bin/convert-argv')(yargs, argv, { // we should use portfinder. const DEFAULT_PORT = 8080; +// Try to find unused port and listen on it for 3 times, +// if port is not specified in options. +// Because NaN == null is false, defaultTo fails if parseInt returns NaN +// so the tryParseInt function is introduced to handle NaN +const defaultPortRetry = defaultTo(tryParseInt(process.env.DEFAULT_PORT_RETRY), 3); + function processOptions (config) { // processOptions {Promise} if (typeof config.then === 'function') { @@ -281,23 +288,7 @@ function processOptions (config) { ? defaultTo(options.port, argv.port) : defaultTo(argv.port, options.port); - if (options.port != null) { - startDevServer(config, options); - - return; - } - - portfinder.basePort = DEFAULT_PORT; - - portfinder.getPort((err, port) => { - if (err) { - throw err; - } - - options.port = port; - - startDevServer(config, options); - }); + startDevServer(config, options); } function startDevServer(config, options) { @@ -380,21 +371,35 @@ function startDevServer(config, options) { status(uri, options, log, argv.color); }); }); - } else { + return; + } + + const startServer = () => { server.listen(options.port, options.host, (err) => { if (err) { throw err; } - if (options.bonjour) { bonjour(options); } - const uri = createDomain(options, server.listeningApp) + suffix; - status(uri, options, log, argv.color); }); + }; + + if (options.port) { + startServer(); + return; } + + // only run port finder if no port as been specified + findPort(server, DEFAULT_PORT, defaultPortRetry, (err, port) => { + if (err) { + throw err; + } + options.port = port; + startServer(); + }); } processOptions(config); diff --git a/test/Util.test.js b/test/Util.test.js index 76d010686f..2fe0460e79 100644 --- a/test/Util.test.js +++ b/test/Util.test.js @@ -1,9 +1,12 @@ 'use strict'; +const EventEmitter = require('events'); +const assert = require('assert'); const webpack = require('webpack'); const internalIp = require('internal-ip'); const Server = require('../lib/Server'); const createDomain = require('../lib/utils/createDomain'); +const findPort = require('../bin/utils').findPort; const config = require('./fixtures/simple-config/webpack.config'); describe('check utility functions', () => { @@ -104,3 +107,38 @@ describe('check utility functions', () => { } }); }); + +describe('findPort cli utility function', () => { + let mockServer = null; + beforeEach(() => { + mockServer = { + listeningApp: new EventEmitter() + }; + }); + afterEach(() => { + mockServer.listeningApp.removeAllListeners('error'); + mockServer = null; + }); + it('should find empty port starting from defaultPort', (done) => { + findPort(mockServer, 8180, 3, (err, port) => { + assert(err == null); + assert(port === 8180); + done(); + }); + }); + it('should retry finding port for up to defaultPortRetry times', (done) => { + let count = 0; + const defaultPortRetry = 5; + findPort(mockServer, 8180, defaultPortRetry, (err) => { + if (err == null) { + count += 1; + const mockError = new Error('EADDRINUSE'); + mockError.code = 'EADDRINUSE'; + mockServer.listeningApp.emit('error', mockError); + return; + } + assert(count === defaultPortRetry); + done(); + }); + }); +}); diff --git a/test/cli.test.js b/test/cli.test.js index ad6a7d3f74..aea899ab19 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -85,4 +85,61 @@ describe('CLI', () => { done(); }); }).timeout(18000); + + it('should use different random port when multiple instances are started on different processes', (done) => { + const cliPath = path.resolve(__dirname, '../bin/webpack-dev-server.js'); + const examplePath = path.resolve(__dirname, '../examples/cli/public'); + + const cp = execa('node', [ cliPath ], { cwd: examplePath }); + const cp2 = execa('node', [ cliPath ], { cwd: examplePath }); + + const runtime = { + cp: { + port: null, + done: false + }, + cp2: { + port: null, + done: false + } + }; + + cp.stdout.on('data', (data) => { + const bits = data.toString(); + const portMatch = /Project is running at http:\/\/localhost:(\d*)\//.exec(bits); + if (portMatch) { + runtime.cp.port = portMatch[1]; + } + if (/Compiled successfully/.test(bits)) { + assert(cp.pid !== 0); + cp.kill('SIGINT'); + } + }); + cp2.stdout.on('data', (data) => { + const bits = data.toString(); + const portMatch = /Project is running at http:\/\/localhost:(\d*)\//.exec(bits); + if (portMatch) { + runtime.cp2.port = portMatch[1]; + } + if (/Compiled successfully/.test(bits)) { + assert(cp.pid !== 0); + cp2.kill('SIGINT'); + } + }); + + cp.on('exit', () => { + runtime.cp.done = true; + if (runtime.cp2.done) { + assert(runtime.cp.port !== runtime.cp2.port); + done(); + } + }); + cp2.on('exit', () => { + runtime.cp2.done = true; + if (runtime.cp.done) { + assert(runtime.cp.port !== runtime.cp2.port); + done(); + } + }); + }).timeout(18000); });