Skip to content

Commit 3fca456

Browse files
authoredJan 6, 2021
fix(server): clean up close-server logic (#3607)
The main change in behavior is the removal of `dieOnError` method. Previously Karma would send SIGINT to its own process and then trigger clean up logic upon receiving this signal. It is a pretty convoluted way to trigger shutdown. This commit extracts clean up logic into the `_close()` method and calls this method directly everywhere. This change solves two issues: - Makes life easier for other tools (like Angular CLI), which use Karma programmatically from another process and killing whole process on Karma error may not be the most convenient behavior. Instead Karma will clean up all its resources and notify caller using the `done` callback. - Allows to remove last Grunt bits in the future PR. When running unit tests without Grunt wrapper the SIGINT is received by the Mocha process, which stops tests execution midway.
1 parent 1c9c2de commit 3fca456

File tree

2 files changed

+93
-154
lines changed

2 files changed

+93
-154
lines changed
 

‎lib/server.js

+64-56
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,6 @@ class Server extends KarmaEventEmitter {
102102
this._injector = new di.Injector(modules)
103103
}
104104

105-
dieOnError (error) {
106-
this.log.error(error)
107-
process.exitCode = 1
108-
process.kill(process.pid, 'SIGINT')
109-
}
110-
111105
async start () {
112106
const config = this.get('config')
113107
try {
@@ -122,7 +116,8 @@ class Server extends KarmaEventEmitter {
122116
config.port = this._boundServer.address().port
123117
await this._injector.invoke(this._start, this)
124118
} catch (err) {
125-
this.dieOnError(`Server start failed on port ${config.port}: ${err}`)
119+
this.log.error(`Server start failed on port ${config.port}: ${err}`)
120+
this._close(1)
126121
}
127122
}
128123

@@ -187,7 +182,8 @@ class Server extends KarmaEventEmitter {
187182
let singleRunBrowserNotCaptured = false
188183

189184
webServer.on('error', (err) => {
190-
this.dieOnError(`Webserver fail ${err}`)
185+
this.log.error(`Webserver fail ${err}`)
186+
this._close(1)
191187
})
192188

193189
const afterPreprocess = () => {
@@ -206,7 +202,8 @@ class Server extends KarmaEventEmitter {
206202
})
207203
}
208204
if (this.loadErrors.length > 0) {
209-
this.dieOnError(new Error(`Found ${this.loadErrors.length} load error${this.loadErrors.length === 1 ? '' : 's'}`))
205+
this.log.error(new Error(`Found ${this.loadErrors.length} load error${this.loadErrors.length === 1 ? '' : 's'}`))
206+
this._close(1)
210207
}
211208
})
212209
}
@@ -302,9 +299,9 @@ class Server extends KarmaEventEmitter {
302299
}
303300
})
304301

305-
this.on('stop', function (done) {
302+
this.on('stop', (done) => {
306303
this.log.debug('Received stop event, exiting.')
307-
disconnectBrowsers()
304+
this._close()
308305
done()
309306
})
310307

@@ -332,9 +329,9 @@ class Server extends KarmaEventEmitter {
332329
emitRunCompleteIfAllBrowsersDone()
333330
})
334331

335-
this.on('run_complete', function (browsers, results) {
332+
this.on('run_complete', (browsers, results) => {
336333
this.log.debug('Run complete, exiting.')
337-
disconnectBrowsers(results.exitCode)
334+
this._close(results.exitCode)
338335
})
339336

340337
this.emit('run_start', singleRunBrowsers)
@@ -350,52 +347,13 @@ class Server extends KarmaEventEmitter {
350347
})
351348
}
352349

353-
const webServerCloseTimeout = 3000
354-
const disconnectBrowsers = (code) => {
355-
const sockets = socketServer.sockets.sockets
356-
357-
Object.keys(sockets).forEach((id) => {
358-
const socket = sockets[id]
359-
socket.removeAllListeners('disconnect')
360-
if (!socket.disconnected) {
361-
process.nextTick(socket.disconnect.bind(socket))
362-
}
363-
})
364-
365-
this.emitExitAsync(code).catch((err) => {
366-
this.log.error('Error while calling exit event listeners\n' + err.stack || err)
367-
return 1
368-
}).then((code) => {
369-
socketServer.sockets.removeAllListeners()
370-
socketServer.close()
371-
372-
let removeAllListenersDone = false
373-
const removeAllListeners = () => {
374-
if (removeAllListenersDone) {
375-
return
376-
}
377-
removeAllListenersDone = true
378-
webServer.removeAllListeners()
379-
processWrapper.removeAllListeners()
380-
done(code || 0)
381-
}
382-
383-
const closeTimeout = setTimeout(removeAllListeners, webServerCloseTimeout)
384-
385-
webServer.close(() => {
386-
clearTimeout(closeTimeout)
387-
removeAllListeners()
388-
})
389-
})
390-
}
391-
392-
processWrapper.on('SIGINT', () => disconnectBrowsers(process.exitCode))
393-
processWrapper.on('SIGTERM', disconnectBrowsers)
350+
processWrapper.on('SIGINT', () => this._close())
351+
processWrapper.on('SIGTERM', () => this._close())
394352

395353
const reportError = (error) => {
396-
process.emit('infrastructure_error', error)
397-
disconnectBrowsers(1)
398354
this.log.error(error)
355+
process.emit('infrastructure_error', error)
356+
this._close(1)
399357
}
400358

401359
processWrapper.on('unhandledRejection', (error) => {
@@ -429,6 +387,56 @@ class Server extends KarmaEventEmitter {
429387
child.unref()
430388
}
431389

390+
/**
391+
* Cleanup all resources allocated by Karma and call the `done` callback
392+
* with the result of the tests execution.
393+
*
394+
* @param [exitCode] - Optional exit code. If omitted will be computed by
395+
* 'exit' event listeners.
396+
*/
397+
_close (exitCode) {
398+
const webServer = this._injector.get('webServer')
399+
const socketServer = this._injector.get('socketServer')
400+
const done = this._injector.get('done')
401+
402+
const webServerCloseTimeout = 3000
403+
const sockets = socketServer.sockets.sockets
404+
405+
Object.keys(sockets).forEach((id) => {
406+
const socket = sockets[id]
407+
socket.removeAllListeners('disconnect')
408+
if (!socket.disconnected) {
409+
process.nextTick(socket.disconnect.bind(socket))
410+
}
411+
})
412+
413+
this.emitExitAsync(exitCode).catch((err) => {
414+
this.log.error('Error while calling exit event listeners\n' + err.stack || err)
415+
return 1
416+
}).then((code) => {
417+
socketServer.sockets.removeAllListeners()
418+
socketServer.close()
419+
420+
let removeAllListenersDone = false
421+
const removeAllListeners = () => {
422+
if (removeAllListenersDone) {
423+
return
424+
}
425+
removeAllListenersDone = true
426+
webServer.removeAllListeners()
427+
processWrapper.removeAllListeners()
428+
done(code || 0)
429+
}
430+
431+
const closeTimeout = setTimeout(removeAllListeners, webServerCloseTimeout)
432+
433+
webServer.close(() => {
434+
clearTimeout(closeTimeout)
435+
removeAllListeners()
436+
})
437+
})
438+
}
439+
432440
stop () {
433441
return this.emitAsync('stop')
434442
}

‎test/unit/server.spec.js

+29-98
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,23 @@ describe('server', () => {
1515
let mockSocketServer
1616
let mockBoundServer
1717
let mockExecutor
18-
let doneSpy
18+
let doneStub
1919
let logErrorSpy
2020
let server = mockConfig = browserCollection = webServerOnError = null
2121
let fileListOnResolve = fileListOnReject = mockLauncher = null
22-
let mockFileList = mockWebServer = mockSocketServer = mockExecutor = doneSpy = null
22+
let mockFileList = mockWebServer = mockSocketServer = mockExecutor = doneStub = null
2323
const mockSocketEventListeners = new Map()
2424

2525
// Use regular function not arrow so 'this' is mocha 'this'.
2626
beforeEach(function () {
2727
// The first call to new Server() loads plugins and it can take >2000ms.
2828
this.timeout(4000)
2929
browserCollection = new BrowserCollection()
30-
doneSpy = sinon.spy()
30+
doneStub = sinon.stub()
3131
logErrorSpy = sinon.spy(logger.create('karma-server'), 'error')
3232

3333
fileListOnResolve = fileListOnReject = null
3434

35-
doneSpy = sinon.spy()
36-
3735
mockConfig = {
3836
frameworks: [],
3937
port: 9876,
@@ -49,7 +47,7 @@ describe('server', () => {
4947
browserNoActivityTimeout: 0
5048
}
5149

52-
server = new Server(mockConfig, doneSpy)
50+
server = new Server(mockConfig, doneStub)
5351

5452
sinon.stub(server._injector, 'invoke').returns([])
5553

@@ -120,10 +118,10 @@ describe('server', () => {
120118
close: sinon.spy((callback) => callback && callback())
121119
}
122120

123-
sinon
124-
.stub(server._injector, 'get')
125-
.withArgs('webServer').returns(mockWebServer)
126-
.withArgs('socketServer').returns(mockSocketServer)
121+
const injectorStub = sinon.stub(server._injector, 'get')
122+
injectorStub.withArgs('socketServer').returns(mockSocketServer)
123+
injectorStub.withArgs('webServer').returns(mockWebServer)
124+
injectorStub.callThrough()
127125

128126
webServerOnError = null
129127
})
@@ -185,7 +183,7 @@ describe('server', () => {
185183
})
186184

187185
it('should start the web server after all files have been preprocessed successfully', async () => {
188-
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneSpy)
186+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneStub)
189187

190188
expect(mockFileList.refresh).to.have.been.called
191189
expect(fileListOnResolve).not.to.be.null
@@ -199,7 +197,7 @@ describe('server', () => {
199197
})
200198

201199
it('should start the web server after all files have been preprocessed with an error', async () => {
202-
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneSpy)
200+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneStub)
203201

204202
expect(mockFileList.refresh).to.have.been.called
205203
expect(fileListOnReject).not.to.be.null
@@ -215,7 +213,7 @@ describe('server', () => {
215213
})
216214

217215
it('should launch browsers after the web server has started', async () => {
218-
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneSpy)
216+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneStub)
219217

220218
expect(mockWebServer.listen).not.to.have.been.called
221219
expect(webServerOnError).not.to.be.null
@@ -227,7 +225,7 @@ describe('server', () => {
227225
})
228226

229227
it('should emit a listening event once server begin accepting connections', async () => {
230-
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneSpy)
228+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneStub)
231229

232230
const listening = sinon.spy()
233231
server.on('listening', listening)
@@ -239,7 +237,7 @@ describe('server', () => {
239237
})
240238

241239
it('should emit a browsers_ready event once all the browsers are captured', async () => {
242-
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneSpy)
240+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneStub)
243241

244242
const browsersReady = sinon.spy()
245243
server.on('browsers_ready', browsersReady)
@@ -254,7 +252,7 @@ describe('server', () => {
254252
})
255253

256254
it('should emit a browser_register event for each browser added', async () => {
257-
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneSpy)
255+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneStub)
258256

259257
const browsersReady = sinon.spy()
260258
server.on('browsers_ready', browsersReady)
@@ -276,43 +274,28 @@ describe('server', () => {
276274
})
277275
}
278276

279-
it('1 on load errors', async () => {
280-
mockProcess(process)
277+
beforeEach(() => {
278+
doneStub.callsFake((exitCode) => resolveExitCode(exitCode))
279+
})
281280

282-
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, (exitCode) => {
283-
resolveExitCode(exitCode)
284-
})
281+
it('1 on load errors', async () => {
282+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneStub)
285283
server.loadErrors.push(['TestError', 'Test'])
286284
fileListOnResolve()
287285

288-
function mockProcess (process) {
289-
sinon.stub(process, 'kill').callsFake((pid, ev) => process.emit(ev))
290-
}
291-
292286
expect(await exitCode()).to.have.equal(1)
293287
})
294288

295289
it('given on run_complete', async () => {
296-
mockProcess(process)
297-
298-
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, (exitCode) => {
299-
resolveExitCode(exitCode)
300-
})
290+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneStub)
301291

302292
server.emit('run_complete', browserCollection, { exitCode: 15 })
303293

304-
function mockProcess (process) {
305-
sinon.stub(process, 'kill').callsFake((pid, ev) => process.emit(ev))
306-
}
307294
expect(await exitCode()).to.have.equal(15)
308295
})
309296

310297
it('given on run_complete with exit event listener (15)', async () => {
311-
mockProcess(process)
312-
313-
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, (exitCode) => {
314-
resolveExitCode(exitCode)
315-
})
298+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneStub)
316299

317300
// last non-zero exit code will be taken
318301
server.on('exit', (done) => {
@@ -328,18 +311,11 @@ describe('server', () => {
328311
// Provided run_complete exitCode will be overridden by exit listeners
329312
server.emit('run_complete', browserCollection, { exitCode: 5 })
330313

331-
function mockProcess (process) {
332-
sinon.stub(process, 'kill').callsFake((pid, ev) => process.emit(ev))
333-
}
334314
expect(await exitCode()).to.have.equal(15)
335315
})
336316

337317
it('given on run_complete with exit event listener (0)', async () => {
338-
mockProcess(process)
339-
340-
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, (exitCode) => {
341-
resolveExitCode(exitCode)
342-
})
318+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneStub)
343319

344320
// exit listeners can't set exit code back to 0
345321
server.on('exit', (done) => {
@@ -348,37 +324,23 @@ describe('server', () => {
348324

349325
server.emit('run_complete', browserCollection, { exitCode: 15 })
350326

351-
function mockProcess (process) {
352-
sinon.stub(process, 'kill').callsFake((pid, ev) => process.emit(ev))
353-
}
354327
expect(await exitCode()).to.have.equal(15)
355328
})
356329

357330
it('1 on run_complete with exit event listener throws', async () => {
358-
mockProcess(process)
359-
360-
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, (exitCode) => {
361-
resolveExitCode(exitCode)
362-
})
331+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneStub)
363332

364333
server.on('exit', (done) => {
365334
throw new Error('async error from exit event listener')
366335
})
367336

368337
server.emit('run_complete', browserCollection, { exitCode: 0 })
369338

370-
function mockProcess (process) {
371-
sinon.stub(process, 'kill').callsFake((pid, ev) => process.emit(ev))
372-
}
373339
expect(await exitCode()).to.have.equal(1)
374340
})
375341

376342
it('1 on run_complete with exit event listener rejects', async () => {
377-
mockProcess(process)
378-
379-
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, (exitCode) => {
380-
resolveExitCode(exitCode)
381-
})
343+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneStub)
382344

383345
function onExit (done) {
384346
// Need to remove listener to prevent endless loop via unhandledRejection handler
@@ -390,71 +352,40 @@ describe('server', () => {
390352

391353
server.emit('run_complete', browserCollection, { exitCode: 0 })
392354

393-
function mockProcess (process) {
394-
sinon.stub(process, 'kill').callsFake((pid, ev) => process.emit(ev))
395-
}
396355
expect(await exitCode()).to.have.equal(1)
397356
})
398357

399358
it('0 on server stop', async () => {
400-
mockProcess(process)
401-
402-
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, (exitCode) => {
403-
resolveExitCode(exitCode)
404-
})
359+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneStub)
405360

406361
server.stop()
407362

408-
function mockProcess (process) {
409-
sinon.stub(process, 'kill').callsFake((pid, ev) => process.emit(ev))
410-
}
411363
expect(await exitCode()).to.have.equal(0)
412364
})
413365

414366
it('1 on browser_process_failure (singleRunBrowserNotCaptured)', async () => {
415-
mockProcess(process)
416-
417-
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, (exitCode) => {
418-
resolveExitCode(exitCode)
419-
})
367+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneStub)
420368

421369
server.emit('browser_process_failure', { id: 'fake' })
422370

423-
function mockProcess (process) {
424-
sinon.stub(process, 'kill').callsFake((pid, ev) => process.emit(ev))
425-
}
426371
expect(await exitCode()).to.have.equal(1)
427372
})
428373

429374
it('0 on browser_complete_with_no_more_retries', async () => {
430-
mockProcess(process)
431-
432-
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, (exitCode) => {
433-
resolveExitCode(exitCode)
434-
})
375+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneStub)
435376

436377
server.emit('browser_complete_with_no_more_retries', { id: 'fake', remove: () => {} })
437378

438-
function mockProcess (process) {
439-
sinon.stub(process, 'kill').callsFake((pid, ev) => process.emit(ev))
440-
}
441379
expect(await exitCode()).to.have.equal(0)
442380
})
443381

444382
it('1 on browser_complete_with_no_more_retries with config.failOnEmptyTestSuite', async () => {
445-
mockProcess(process)
446-
447383
mockConfig.failOnEmptyTestSuite = true
448384

449-
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, (exitCode) => {
450-
resolveExitCode(exitCode)
451-
})
385+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneStub)
452386

453387
server.emit('browser_complete_with_no_more_retries', { id: 'fake', remove: () => {} })
454388

455-
function mockProcess (process) {
456-
sinon.stub(process, 'kill').callsFake((pid, ev) => process.emit(ev))
457-
}
458389
expect(await exitCode()).to.have.equal(1)
459390
})
460391
})
@@ -465,7 +396,7 @@ describe('server', () => {
465396

466397
beforeEach(async () => {
467398
browserCollection = new BrowserCollection(server)
468-
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneSpy)
399+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneStub)
469400

470401
mockBrowserSocket = {
471402
id: 'browser-socket-id',

0 commit comments

Comments
 (0)
Please sign in to comment.