Skip to content

Commit 419f02e

Browse files
misticevilebottnawi
authored andcommitted
feat: random port retry logic (#1692)
1 parent 5d1476e commit 419f02e

File tree

5 files changed

+176
-24
lines changed

5 files changed

+176
-24
lines changed

bin/webpack-dev-server.js

+31-24
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,25 @@ const debug = require('debug')('webpack-dev-server');
1212
const fs = require('fs');
1313
const net = require('net');
1414

15-
const portfinder = require('portfinder');
1615
const importLocal = require('import-local');
1716

1817
const yargs = require('yargs');
1918
const webpack = require('webpack');
2019

2120
const options = require('./options');
22-
2321
const Server = require('../lib/Server');
2422

2523
const addEntries = require('../lib/utils/addEntries');
2624
const colors = require('../lib/utils/colors');
2725
const createConfig = require('../lib/utils/createConfig');
2826
const createDomain = require('../lib/utils/createDomain');
2927
const createLogger = require('../lib/utils/createLogger');
28+
const defaultTo = require('../lib/utils/defaultTo');
29+
const findPort = require('../lib/utils/findPort');
3030
const getVersions = require('../lib/utils/getVersions');
3131
const runBonjour = require('../lib/utils/runBonjour');
3232
const status = require('../lib/utils/status');
33+
const tryParseInt = require('../lib/utils/tryParseInt');
3334

3435
let server;
3536

@@ -93,6 +94,15 @@ const config = require('webpack-cli/bin/convert-argv')(yargs, argv, {
9394
// we should use portfinder.
9495
const DEFAULT_PORT = 8080;
9596

97+
// Try to find unused port and listen on it for 3 times,
98+
// if port is not specified in options.
99+
// Because NaN == null is false, defaultTo fails if parseInt returns NaN
100+
// so the tryParseInt function is introduced to handle NaN
101+
const defaultPortRetry = defaultTo(
102+
tryParseInt(process.env.DEFAULT_PORT_RETRY),
103+
3
104+
);
105+
96106
function processOptions(config) {
97107
// processOptions {Promise}
98108
if (typeof config.then === 'function') {
@@ -106,24 +116,7 @@ function processOptions(config) {
106116
}
107117

108118
const options = createConfig(config, argv, { port: DEFAULT_PORT });
109-
110-
portfinder.basePort = DEFAULT_PORT;
111-
112-
if (options.port != null) {
113-
startDevServer(config, options);
114-
115-
return;
116-
}
117-
118-
portfinder.getPort((err, port) => {
119-
if (err) {
120-
throw err;
121-
}
122-
123-
options.port = port;
124-
125-
startDevServer(config, options);
126-
});
119+
startDevServer(config, options);
127120
}
128121

129122
function startDevServer(config, options) {
@@ -209,21 +202,35 @@ function startDevServer(config, options) {
209202
status(uri, options, log, argv.color);
210203
});
211204
});
212-
} else {
205+
return;
206+
}
207+
208+
const startServer = () => {
213209
server.listen(options.port, options.host, (err) => {
214210
if (err) {
215211
throw err;
216212
}
217-
218213
if (options.bonjour) {
219214
runBonjour(options);
220215
}
221-
222216
const uri = createDomain(options, server.listeningApp) + suffix;
223-
224217
status(uri, options, log, argv.color);
225218
});
219+
};
220+
221+
if (options.port) {
222+
startServer();
223+
return;
226224
}
225+
226+
// only run port finder if no port as been specified
227+
findPort(server, DEFAULT_PORT, defaultPortRetry, (err, port) => {
228+
if (err) {
229+
throw err;
230+
}
231+
options.port = port;
232+
startServer();
233+
});
227234
}
228235

229236
processOptions(config);

lib/utils/findPort.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
3+
const portfinder = require('portfinder');
4+
5+
function runPortFinder(defaultPort, cb) {
6+
portfinder.basePort = defaultPort;
7+
portfinder.getPort((err, port) => {
8+
cb(err, port);
9+
});
10+
}
11+
12+
function findPort(server, defaultPort, defaultPortRetry, fn) {
13+
let tryCount = 0;
14+
const portFinderRunCb = (err, port) => {
15+
tryCount += 1;
16+
fn(err, port);
17+
};
18+
19+
server.listeningApp.on('error', (err) => {
20+
if (err && err.code !== 'EADDRINUSE') {
21+
throw err;
22+
}
23+
24+
if (tryCount >= defaultPortRetry) {
25+
fn(err);
26+
return;
27+
}
28+
29+
runPortFinder(defaultPort, portFinderRunCb);
30+
});
31+
32+
runPortFinder(defaultPort, portFinderRunCb);
33+
}
34+
35+
module.exports = findPort;

lib/utils/tryParseInt.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use strict';
2+
3+
function tryParseInt(input) {
4+
const output = parseInt(input, 10);
5+
if (Number.isNaN(output)) {
6+
return null;
7+
}
8+
return output;
9+
}
10+
11+
module.exports = tryParseInt;

test/Util.test.js

+38
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
'use strict';
22

3+
const EventEmitter = require('events');
4+
const assert = require('assert');
35
const webpack = require('webpack');
46
const internalIp = require('internal-ip');
57
const Server = require('../lib/Server');
68
const createDomain = require('../lib/utils/createDomain');
9+
const findPort = require('../lib/utils/findPort');
710
const config = require('./fixtures/simple-config/webpack.config');
811

912
describe('check utility functions', () => {
@@ -107,3 +110,38 @@ describe('check utility functions', () => {
107110
});
108111
});
109112
});
113+
114+
describe('findPort cli utility function', () => {
115+
let mockServer = null;
116+
beforeEach(() => {
117+
mockServer = {
118+
listeningApp: new EventEmitter(),
119+
};
120+
});
121+
afterEach(() => {
122+
mockServer.listeningApp.removeAllListeners('error');
123+
mockServer = null;
124+
});
125+
it('should find empty port starting from defaultPort', (done) => {
126+
findPort(mockServer, 8180, 3, (err, port) => {
127+
assert(err == null);
128+
assert(port === 8180);
129+
done();
130+
});
131+
});
132+
it('should retry finding port for up to defaultPortRetry times', (done) => {
133+
let count = 0;
134+
const defaultPortRetry = 5;
135+
findPort(mockServer, 8180, defaultPortRetry, (err) => {
136+
if (err == null) {
137+
count += 1;
138+
const mockError = new Error('EADDRINUSE');
139+
mockError.code = 'EADDRINUSE';
140+
mockServer.listeningApp.emit('error', mockError);
141+
return;
142+
}
143+
assert(count === defaultPortRetry);
144+
done();
145+
});
146+
});
147+
});

test/cli.test.js

+61
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,65 @@ describe('CLI', () => {
9595
done();
9696
});
9797
});
98+
99+
it('should use different random port when multiple instances are started on different processes', (done) => {
100+
const cliPath = path.resolve(__dirname, '../bin/webpack-dev-server.js');
101+
const examplePath = path.resolve(__dirname, '../examples/cli/public');
102+
103+
const cp = execa('node', [cliPath], { cwd: examplePath });
104+
const cp2 = execa('node', [cliPath], { cwd: examplePath });
105+
106+
const runtime = {
107+
cp: {
108+
port: null,
109+
done: false,
110+
},
111+
cp2: {
112+
port: null,
113+
done: false,
114+
},
115+
};
116+
117+
cp.stdout.on('data', (data) => {
118+
const bits = data.toString();
119+
const portMatch = /Project is running at http:\/\/localhost:(\d*)\//.exec(
120+
bits
121+
);
122+
if (portMatch) {
123+
runtime.cp.port = portMatch[1];
124+
}
125+
if (/Compiled successfully/.test(bits)) {
126+
expect(cp.pid !== 0).toBe(true);
127+
cp.kill('SIGINT');
128+
}
129+
});
130+
cp2.stdout.on('data', (data) => {
131+
const bits = data.toString();
132+
const portMatch = /Project is running at http:\/\/localhost:(\d*)\//.exec(
133+
bits
134+
);
135+
if (portMatch) {
136+
runtime.cp2.port = portMatch[1];
137+
}
138+
if (/Compiled successfully/.test(bits)) {
139+
expect(cp.pid !== 0).toBe(true);
140+
cp2.kill('SIGINT');
141+
}
142+
});
143+
144+
cp.on('exit', () => {
145+
runtime.cp.done = true;
146+
if (runtime.cp2.done) {
147+
expect(runtime.cp.port !== runtime.cp2.port).toBe(true);
148+
done();
149+
}
150+
});
151+
cp2.on('exit', () => {
152+
runtime.cp2.done = true;
153+
if (runtime.cp.done) {
154+
expect(runtime.cp.port !== runtime.cp2.port).toBe(true);
155+
done();
156+
}
157+
});
158+
});
98159
});

0 commit comments

Comments
 (0)