Skip to content

Commit ed0bcb8

Browse files
Desuuuunklayman
authored andcommitted
fix(serve): Kill Electron process gracefully during development (#110)
I refactored the code to give 2 seconds to the child process to handle exiting by itself when we restart it, on SIGINT/SIGTERM and when Ctrl+C is used on Windows. Fixes #108
1 parent 9b4b676 commit ed0bcb8

File tree

3 files changed

+109
-22
lines changed

3 files changed

+109
-22
lines changed

__tests__/commands.spec.js

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const { chainWebpack } = require('../lib/webpackConfig')
1111
// #endregion
1212

1313
// #region Mocks
14+
process.env.IS_TEST = true
1415
const mockYargsParse = jest.fn()
1516
const mockYargsCommand = jest.fn(() => ({ parse: mockYargsParse }))
1617
jest.mock('yargs', () => ({ command: mockYargsCommand }))
@@ -25,10 +26,14 @@ jest.mock('electron-builder/out/cli/install-app-deps.js', () => ({
2526
}))
2627
jest.mock('../lib/webpackConfig.js')
2728
const mockPipe = jest.fn()
29+
const childEvents = {}
2830
const mockExeca = {
29-
on: jest.fn(),
31+
on: jest.fn((eventName, cb) => {
32+
childEvents[eventName] = cb
33+
}),
3034
removeAllListeners: jest.fn(),
3135
kill: jest.fn(),
36+
send: jest.fn(),
3237
stdout: {
3338
pipe: jest.fn(() => ({ pipe: mockPipe }))
3439
},
@@ -384,6 +389,7 @@ describe('electron:serve', () => {
384389

385390
// Mock change of background file
386391
watchCb()
392+
childEvents.exit()
387393

388394
expect(execa).toHaveBeenCalledTimes(2)
389395
expect(execa.mock.calls[0][1]).toEqual([
@@ -442,17 +448,18 @@ describe('electron:serve', () => {
442448
// Proper file is watched
443449
expect(fs.watchFile.mock.calls[0][0]).toBe('projectPath/customBackground')
444450
// Child has not yet been killed or unwatched
445-
expect(mockExeca.kill).not.toBeCalled()
451+
expect(mockExeca.send).not.toBeCalled()
446452
expect(mockExeca.removeAllListeners).not.toBeCalled()
447453
// Main process was bundled and Electron was launched initially
448454
expect(webpack).toHaveBeenCalledTimes(1)
449455
expect(execa).toHaveBeenCalledTimes(1)
450456

451457
// Mock change of background file
452458
watchCb()
459+
childEvents.exit()
453460
// Electron was killed and listeners removed
454-
expect(mockExeca.kill).toHaveBeenCalledTimes(1)
455-
expect(mockExeca.removeAllListeners).toHaveBeenCalledTimes(1)
461+
expect(mockExeca.send).toHaveBeenCalledTimes(1)
462+
expect(mockExeca.send).toHaveBeenCalledWith('graceful-exit')
456463
// Process did not exit on Electron close
457464
expect(process.exit).not.toBeCalled()
458465
// Main process file was recompiled
@@ -484,17 +491,18 @@ describe('electron:serve', () => {
484491
expect(fs.watchFile.mock.calls[0][0]).toBe('projectPath/customBackground')
485492
expect(fs.watchFile.mock.calls[1][0]).toBe('projectPath/listFile')
486493
// Child has not yet been killed or unwatched
487-
expect(mockExeca.kill).not.toBeCalled()
494+
expect(mockExeca.send).not.toBeCalled()
488495
expect(mockExeca.removeAllListeners).not.toBeCalled()
489496
// Main process was bundled and Electron was launched initially
490497
expect(webpack).toHaveBeenCalledTimes(1)
491498
expect(execa).toHaveBeenCalledTimes(1)
492499

493500
// Mock change of listed file
494501
watchCb['projectPath/listFile']()
502+
childEvents.exit()
495503
// Electron was killed and listeners removed
496-
expect(mockExeca.kill).toHaveBeenCalledTimes(1)
497-
expect(mockExeca.removeAllListeners).toHaveBeenCalledTimes(1)
504+
expect(mockExeca.send).toHaveBeenCalledTimes(1)
505+
expect(mockExeca.send).toHaveBeenCalledWith('graceful-exit')
498506
// Process did not exit on Electron close
499507
expect(process.exit).not.toBeCalled()
500508
// Main process file was recompiled
@@ -504,9 +512,10 @@ describe('electron:serve', () => {
504512

505513
// Mock change of background file
506514
watchCb['projectPath/customBackground']()
515+
childEvents.exit()
507516
// Electron was killed and listeners removed
508-
expect(mockExeca.kill).toHaveBeenCalledTimes(2)
509-
expect(mockExeca.removeAllListeners).toHaveBeenCalledTimes(2)
517+
expect(mockExeca.send).toHaveBeenCalledTimes(2)
518+
expect(mockExeca.send).toHaveBeenCalledWith('graceful-exit')
510519
// Process did not exit on Electron close
511520
expect(process.exit).not.toBeCalled()
512521
// Main process file was recompiled

generator/template/src/background.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,12 @@ app.on('ready', async () => {
5959
}
6060
createWindow()
6161
})
62+
63+
// Exit cleanly on request from parent process in development mode.
64+
if (isDevelopment) {
65+
process.on('message', data => {
66+
if (data === 'graceful-exit') {
67+
app.quit()
68+
}
69+
})
70+
}

index.js

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const Config = require('webpack-chain')
44
const merge = require('lodash.merge')
55
const fs = require('fs-extra')
66
const path = require('path')
7+
const readline = require('readline')
78
const {
89
log,
910
done,
@@ -216,17 +217,9 @@ module.exports = (api, options) => {
216217

217218
// Copy package.json so electron can detect app's name
218219
fs.copySync(api.resolve('./package.json'), `${outputDir}/package.json`)
219-
// Electron process
220-
let child
220+
221221
// Function to bundle main process and start Electron
222222
const startElectron = () => {
223-
if (child) {
224-
// Prevent self exit on Electron process death
225-
child.removeAllListeners()
226-
// Kill old Electron process
227-
child.kill()
228-
}
229-
230223
if (bundleMainProcess) {
231224
// Build the main process
232225
const bundle = bundleMain({
@@ -266,13 +259,74 @@ module.exports = (api, options) => {
266259
launchElectron()
267260
}
268261
}
262+
263+
// Electron process
264+
let child
265+
// Auto restart flag
266+
let childRestartOnExit = 0
267+
// Graceful exit timeout
268+
let childExitTimeout
269+
// Function to kill Electron process
270+
const killElectron = () => {
271+
if (!child) {
272+
return
273+
}
274+
275+
// Attempt to kill gracefully
276+
child.send('graceful-exit')
277+
278+
// Kill after 2 seconds if unsuccessful
279+
childExitTimeout = setTimeout(() => {
280+
if (child) {
281+
child.kill()
282+
}
283+
}, 2000)
284+
}
285+
269286
// Initial start of Electron
270287
startElectron()
271288
// Restart on main process file change
272289
mainProcessWatch.forEach(file => {
273-
fs.watchFile(api.resolve(file), startElectron)
290+
fs.watchFile(api.resolve(file), () => {
291+
// Never restart after SIGINT
292+
if (childRestartOnExit < 0) {
293+
return
294+
}
295+
296+
// Set auto restart flag
297+
childRestartOnExit = 1
298+
299+
killElectron()
300+
})
274301
})
275302

303+
// Attempt to kill gracefully on SIGINT and SIGTERM
304+
const signalHandler = () => {
305+
if (!child) {
306+
process.exit(0)
307+
}
308+
309+
// Prevent future restarts
310+
childRestartOnExit = -1
311+
312+
killElectron()
313+
}
314+
315+
if (!process.env.IS_TEST) process.on('SIGINT', signalHandler)
316+
if (!process.env.IS_TEST) process.on('SIGTERM', signalHandler)
317+
318+
// Handle Ctrl+C on Windows
319+
if (process.platform === 'win32' && !process.env.IS_TEST) {
320+
readline
321+
.createInterface({
322+
input: process.stdin,
323+
output: process.stdout
324+
})
325+
.on('SIGINT', () => {
326+
process.emit('SIGINT')
327+
})
328+
}
329+
276330
function launchElectron () {
277331
if (args.debug) {
278332
console.log(info)
@@ -302,6 +356,10 @@ module.exports = (api, options) => {
302356
} else {
303357
info('Launching Electron...')
304358
}
359+
360+
// Disable Electron process auto restart
361+
childRestartOnExit = 0
362+
305363
child = execa(
306364
require('electron'),
307365
[
@@ -316,7 +374,8 @@ module.exports = (api, options) => {
316374
...process.env,
317375
// Disable electron security warnings
318376
ELECTRON_DISABLE_SECURITY_WARNINGS: true
319-
}
377+
},
378+
stdio: [null, null, null, 'ipc']
320379
}
321380
)
322381

@@ -335,8 +394,18 @@ module.exports = (api, options) => {
335394
}
336395

337396
child.on('exit', () => {
338-
// Exit when electron is closed
339-
process.exit(0)
397+
child = null
398+
399+
if (childExitTimeout) {
400+
clearTimeout(childExitTimeout)
401+
childExitTimeout = null
402+
}
403+
404+
if (childRestartOnExit > 0) {
405+
startElectron()
406+
} else {
407+
process.exit(0)
408+
}
340409
})
341410
}
342411
}

0 commit comments

Comments
 (0)