Skip to content

Commit dbded36

Browse files
knagaitsevhiroppy
authored andcommitted
refactor(server): move socket handling into server.listen (#2061)
1 parent 27d12e1 commit dbded36

9 files changed

+484
-68
lines changed

bin/webpack-dev-server.js

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
/* eslint-disable no-shadow, no-console */
66

7-
const fs = require('fs');
8-
const net = require('net');
97
const debug = require('debug')('webpack-dev-server');
108
const importLocal = require('import-local');
119
const yargs = require('yargs');
@@ -116,42 +114,10 @@ function startDevServer(config, options) {
116114
}
117115

118116
if (options.socket) {
119-
server.listeningApp.on('error', (e) => {
120-
if (e.code === 'EADDRINUSE') {
121-
const clientSocket = new net.Socket();
122-
123-
clientSocket.on('error', (err) => {
124-
if (err.code === 'ECONNREFUSED') {
125-
// No other server listening on this socket so it can be safely removed
126-
fs.unlinkSync(options.socket);
127-
128-
server.listen(options.socket, options.host, (error) => {
129-
if (error) {
130-
throw error;
131-
}
132-
});
133-
}
134-
});
135-
136-
clientSocket.connect({ path: options.socket }, () => {
137-
throw new Error('This socket is already used');
138-
});
139-
}
140-
});
141-
142117
server.listen(options.socket, options.host, (err) => {
143118
if (err) {
144119
throw err;
145120
}
146-
147-
// chmod 666 (rw rw rw)
148-
const READ_WRITE = 438;
149-
150-
fs.chmod(options.socket, READ_WRITE, (err) => {
151-
if (err) {
152-
throw err;
153-
}
154-
});
155121
});
156122
} else {
157123
server.listen(options.port, options.host, (err) => {

lib/Server.js

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ const runBonjour = require('./utils/runBonjour');
3434
const routes = require('./utils/routes');
3535
const getSocketServerImplementation = require('./utils/getSocketServerImplementation');
3636
const handleStdin = require('./utils/handleStdin');
37+
const tryParseInt = require('./utils/tryParseInt');
38+
const startUnixSocket = require('./utils/startUnixSocket');
3739
const schema = require('./options.json');
3840

3941
// Workaround for node ^8.6.0, ^9.0.0
@@ -728,23 +730,54 @@ class Server {
728730
listen(port, hostname, fn) {
729731
this.hostname = hostname;
730732

731-
return this.listeningApp.listen(port, hostname, (err) => {
733+
const setupCallback = () => {
732734
this.createSocketServer();
733735

734736
if (this.options.bonjour) {
735737
runBonjour(this.options);
736738
}
737739

738740
this.showStatus();
741+
};
739742

740-
if (fn) {
743+
// between setupCallback and userCallback should be any other needed handling,
744+
// specifically so that things are done in the right order to prevent
745+
// backwards compatability issues
746+
let userCallbackCalled = false;
747+
const userCallback = (err) => {
748+
if (fn && !userCallbackCalled) {
749+
userCallbackCalled = true;
741750
fn.call(this.listeningApp, err);
742751
}
752+
};
743753

754+
const onListeningCallback = () => {
744755
if (typeof this.options.onListening === 'function') {
745756
this.options.onListening(this);
746757
}
747-
});
758+
};
759+
760+
const fullCallback = (err) => {
761+
setupCallback();
762+
userCallback(err);
763+
onListeningCallback();
764+
};
765+
766+
// try to follow the Node standard in terms of deciding
767+
// whether this is a socket or a port that we will listen on:
768+
// https://github.com/nodejs/node/blob/64219741218aa87e259cf8257596073b8e747f0a/lib/net.js#L196
769+
if (typeof port === 'string' && tryParseInt(port) === null) {
770+
// in this case the "port" argument is actually a socket path
771+
const socket = port;
772+
// set this so that status helper can identify how the project is being run correctly
773+
this.options.socket = socket;
774+
775+
startUnixSocket(this.listeningApp, socket, fullCallback);
776+
} else {
777+
this.listeningApp.listen(port, hostname, fullCallback);
778+
}
779+
780+
return this.listeningApp;
748781
}
749782

750783
close(cb) {

lib/utils/startUnixSocket.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use strict';
2+
3+
const fs = require('fs');
4+
const net = require('net');
5+
const { promisify } = require('util');
6+
7+
const accessAsync = promisify(fs.access);
8+
9+
async function startUnixSocket(listeningApp, socket, cb) {
10+
const chmodSocket = (done) => {
11+
// chmod 666 (rw rw rw) - octal
12+
const READ_WRITE = 438;
13+
fs.chmod(socket, READ_WRITE, done);
14+
};
15+
16+
const startSocket = () => {
17+
listeningApp.on('error', (err) => {
18+
cb(err);
19+
});
20+
21+
// 511 is the default value for the server.listen backlog parameter
22+
// https://nodejs.org/api/net.html#net_server_listen
23+
listeningApp.listen(socket, 511, (err) => {
24+
if (err) {
25+
cb(err);
26+
} else {
27+
chmodSocket(cb);
28+
}
29+
});
30+
};
31+
32+
try {
33+
await accessAsync(socket, fs.constants.F_OK);
34+
} catch (e) {
35+
// file does not exist
36+
startSocket();
37+
return;
38+
}
39+
40+
// file exists
41+
42+
const clientSocket = new net.Socket();
43+
44+
clientSocket.on('error', (err) => {
45+
if (err.code === 'ECONNREFUSED' || err.code === 'ENOTSOCK') {
46+
// No other server listening on this socket so it can be safely removed
47+
fs.unlinkSync(socket);
48+
49+
startSocket();
50+
}
51+
});
52+
53+
clientSocket.connect({ path: socket }, () => {
54+
// if a client socket successfully connects to the given socket path,
55+
// it means that the socket is in use
56+
const err = new Error('This socket is already used');
57+
clientSocket.destroy();
58+
cb(err);
59+
});
60+
}
61+
62+
module.exports = startUnixSocket;

test/cli/cli.test.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const { unlinkAsync } = require('../helpers/fs');
1010
const testBin = require('../helpers/test-bin');
1111
const port1 = require('../ports-map').cli[0];
1212
const timer = require('../helpers/timer');
13+
const { skipTestOnWindows } = require('../helpers/conditional-test');
1314

1415
const httpsCertificateDirectory = resolve(
1516
__dirname,
@@ -94,14 +95,14 @@ describe('CLI', () => {
9495
.catch(done);
9596
});
9697

97-
// The Unix socket to listen to (instead of a host).
98-
it('--socket', async () => {
99-
const socketPath = join('.', 'webpack.sock');
100-
const { exitCode, stdout } = await testBin(`--socket ${socketPath}`);
101-
expect(exitCode).toEqual(0);
98+
describe('Unix socket', () => {
99+
if (skipTestOnWindows('Unix sockets are not supported on Windows')) {
100+
return;
101+
}
102102

103-
if (process.platform !== 'win32') {
104-
expect(stdout.includes(socketPath)).toBe(true);
103+
// The Unix socket to listen to (instead of a host).
104+
it('--socket', async () => {
105+
const socketPath = join('.', 'webpack.sock');
105106

106107
testBin(`--socket ${socketPath}`)
107108
.then((output) => {

test/helpers/test-server.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,19 @@ function startFullSetup(config, options, done) {
4040

4141
server = new Server(compiler, options);
4242

43-
const port = Object.prototype.hasOwnProperty.call(options, 'port')
44-
? options.port
45-
: 8080;
43+
// originally the fallback default was 8080, but it should be
44+
// undefined so that the server.listen method can choose it for us,
45+
// and thus prevent port mapping collision between tests
46+
let port;
47+
if (Object.prototype.hasOwnProperty.call(options, 'port')) {
48+
port = options.port;
49+
} else if (Object.prototype.hasOwnProperty.call(options, 'socket')) {
50+
port = options.socket;
51+
} else {
52+
// TODO: remove this when findPort is implemented in the server.listen method
53+
port = 8080;
54+
}
55+
4656
const host = Object.prototype.hasOwnProperty.call(options, 'host')
4757
? options.host
4858
: 'localhost';
@@ -65,10 +75,12 @@ function startFullSetup(config, options, done) {
6575

6676
function startAwaitingCompilationFullSetup(config, options, done) {
6777
let readyCount = 0;
68-
const ready = () => {
78+
let err;
79+
const ready = (e) => {
80+
err = e instanceof Error || (typeof e === 'object' && e.code) ? e : err;
6981
readyCount += 1;
7082
if (readyCount === 2) {
71-
done();
83+
done(err);
7284
}
7385
};
7486

test/helpers/test-unix-socket.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict';
2+
3+
const http = require('http');
4+
5+
const TestUnixSocket = class TestUnixSocket {
6+
constructor() {
7+
this.server = http.createServer();
8+
this.sockets = new Set();
9+
this.server.on('connection', (socket) => {
10+
this.sockets.add(socket);
11+
socket.on('close', () => {
12+
this.sockets.delete(socket);
13+
});
14+
});
15+
}
16+
17+
close(done) {
18+
if (this.server.listening) {
19+
// get rid of connected sockets
20+
for (const socket of this.sockets.values()) {
21+
socket.destroy();
22+
}
23+
this.server.close(done);
24+
} else {
25+
done();
26+
}
27+
}
28+
};
29+
30+
module.exports = TestUnixSocket;
Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,71 @@
11
'use strict';
22

3+
const { unlink } = require('fs');
4+
const { join } = require('path');
35
const testServer = require('../helpers/test-server');
46
const config = require('../fixtures/simple-config/webpack.config');
57
const port = require('../ports-map')['onListening-option'];
8+
const { skipTestOnWindows } = require('../helpers/conditional-test');
69

710
describe('onListening option', () => {
8-
let onListeningIsRunning = false;
9-
10-
beforeAll((done) => {
11-
testServer.start(
12-
config,
13-
{
14-
onListening: (devServer) => {
15-
if (!devServer) {
16-
throw new Error('webpack-dev-server is not defined');
17-
}
18-
19-
onListeningIsRunning = true;
11+
describe('with host and port', () => {
12+
let onListeningIsRunning = false;
13+
14+
beforeAll((done) => {
15+
testServer.start(
16+
config,
17+
{
18+
onListening: (devServer) => {
19+
if (!devServer) {
20+
throw new Error('webpack-dev-server is not defined');
21+
}
22+
23+
onListeningIsRunning = true;
24+
},
25+
port,
2026
},
21-
port,
22-
},
23-
done
24-
);
27+
done
28+
);
29+
});
30+
31+
afterAll(testServer.close);
32+
33+
it('should run onListening callback', () => {
34+
expect(onListeningIsRunning).toBe(true);
35+
});
2536
});
2637

27-
afterAll(testServer.close);
38+
describe('with Unix socket', () => {
39+
if (skipTestOnWindows('Unix sockets are not supported on Windows')) {
40+
return;
41+
}
42+
43+
const socketPath = join('.', 'onListening.webpack.sock');
44+
let onListeningIsRunning = false;
45+
46+
beforeAll((done) => {
47+
testServer.start(
48+
config,
49+
{
50+
onListening: (devServer) => {
51+
if (!devServer) {
52+
throw new Error('webpack-dev-server is not defined');
53+
}
54+
55+
onListeningIsRunning = true;
56+
},
57+
socket: socketPath,
58+
},
59+
done
60+
);
61+
});
62+
63+
afterAll(testServer.close);
64+
65+
it('should run onListening callback', (done) => {
66+
expect(onListeningIsRunning).toBe(true);
2867

29-
it('should runs onListening callback', () => {
30-
expect(onListeningIsRunning).toBe(true);
68+
unlink(socketPath, done);
69+
});
3170
});
3271
});

0 commit comments

Comments
 (0)