mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-29 06:06:33 +00:00
fix(child-writer): Handle exits due to a signal (#1843)
This adds handling for cases where the writer child process exits due to reception of a signal, while also adjusting some peripherals, like the IPC socket directory and inherited process environment. Another addition is that the child process is explicitly killed should an error arise on the IPC. Change-Type: patch
This commit is contained in:
parent
5e77958106
commit
5046af5313
@ -17,6 +17,7 @@
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const os = require('os')
|
||||
|
||||
/**
|
||||
* @summary Child writer constants
|
||||
@ -25,6 +26,13 @@ const path = require('path')
|
||||
*/
|
||||
module.exports = {
|
||||
|
||||
/**
|
||||
* @property {String} TMP_DIRECTORY
|
||||
* @memberof CONSTANTS
|
||||
* @constant
|
||||
*/
|
||||
TMP_DIRECTORY: process.env.XDG_RUNTIME_DIR || os.tmpdir(),
|
||||
|
||||
/**
|
||||
* @property {String} PROJECT_ROOT
|
||||
* @memberof CONSTANTS
|
||||
|
@ -26,6 +26,16 @@ const CONSTANTS = require('./constants')
|
||||
const EXIT_CODES = require('../shared/exit-codes')
|
||||
const robot = require('../shared/robot')
|
||||
|
||||
// There might be multiple Etcher instances running at
|
||||
// the same time, therefore we must ensure each IPC
|
||||
// server/client has a different name.
|
||||
process.env.IPC_SERVER_ID = `etcher-server-${process.pid}`
|
||||
process.env.IPC_CLIENT_ID = `etcher-client-${process.pid}`
|
||||
|
||||
ipc.config.id = process.env.IPC_SERVER_ID
|
||||
ipc.config.socketRoot = CONSTANTS.TMP_DIRECTORY
|
||||
ipc.config.silent = true
|
||||
|
||||
/**
|
||||
* @summary Perform a write
|
||||
* @function
|
||||
@ -67,14 +77,6 @@ exports.write = (image, drive, options) => {
|
||||
unmountOnSuccess: options.unmountOnSuccess
|
||||
})
|
||||
|
||||
// There might be multiple Etcher instances running at
|
||||
// the same time, therefore we must ensure each IPC
|
||||
// server/client has a different name.
|
||||
process.env.IPC_SERVER_ID = `etcher-server-${process.pid}`
|
||||
process.env.IPC_CLIENT_ID = `etcher-client-${process.pid}`
|
||||
|
||||
ipc.config.id = process.env.IPC_SERVER_ID
|
||||
ipc.config.silent = true
|
||||
ipc.serve()
|
||||
|
||||
/**
|
||||
@ -192,7 +194,7 @@ exports.write = (image, drive, options) => {
|
||||
|
||||
child.on('error', emitError)
|
||||
|
||||
child.on('close', (code) => {
|
||||
child.on('exit', (code, signal) => {
|
||||
terminateServer()
|
||||
|
||||
if (code === EXIT_CODES.CANCELLED) {
|
||||
@ -207,7 +209,11 @@ exports.write = (image, drive, options) => {
|
||||
return null
|
||||
}
|
||||
|
||||
return emitError(new Error(`Child process exited with error code: ${code}`))
|
||||
const error = new Error(`Child process exited with code ${code}, signal ${signal}`)
|
||||
error.code = code
|
||||
error.signal = signal
|
||||
|
||||
return emitError(error)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -27,6 +27,9 @@ const EXIT_CODES = require('../shared/exit-codes')
|
||||
const robot = require('../shared/robot')
|
||||
const permissions = require('../shared/permissions')
|
||||
const packageJSON = require('../../package.json')
|
||||
const CONSTANTS = require('./constants')
|
||||
|
||||
/* eslint-disable no-eq-null */
|
||||
|
||||
// This script is in charge of spawning the writer process and
|
||||
// ensuring it has the necessary privileges. It might look a bit
|
||||
@ -64,6 +67,18 @@ const OPTIONS_INDEX_START = 2
|
||||
*/
|
||||
const etcherArguments = process.argv.slice(OPTIONS_INDEX_START)
|
||||
|
||||
ipc.config.id = process.env.IPC_CLIENT_ID
|
||||
ipc.config.socketRoot = CONSTANTS.TMP_DIRECTORY
|
||||
ipc.config.silent = true
|
||||
|
||||
// > If set to 0, the client will NOT try to reconnect.
|
||||
// See https://github.com/RIAEvangelist/node-ipc/
|
||||
//
|
||||
// The purpose behind this change is for this process
|
||||
// to emit a "disconnect" event as soon as the GUI
|
||||
// process is closed, so we can kill the CLI as well.
|
||||
ipc.config.stopRetrying = 0
|
||||
|
||||
permissions.isElevated().then((elevated) => {
|
||||
if (!elevated) {
|
||||
console.log('Attempting to elevate')
|
||||
@ -109,62 +124,84 @@ permissions.isElevated().then((elevated) => {
|
||||
console.log('Re-spawning with elevation')
|
||||
|
||||
return new Bluebird((resolve, reject) => {
|
||||
ipc.config.id = process.env.IPC_CLIENT_ID
|
||||
ipc.config.silent = true
|
||||
let child = null
|
||||
|
||||
// > If set to 0, the client will NOT try to reconnect.
|
||||
// See https://github.com/RIAEvangelist/node-ipc/
|
||||
//
|
||||
// The purpose behind this change is for this process
|
||||
// to emit a "disconnect" event as soon as the GUI
|
||||
// process is closed, so we can kill the CLI as well.
|
||||
ipc.config.stopRetrying = 0
|
||||
/**
|
||||
* @summary Emit an object message to the IPC server
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {Buffer} data - json message data
|
||||
*
|
||||
* @example
|
||||
* emitMessage(Buffer.from(JSON.stringify({
|
||||
* foo: 'bar'
|
||||
* })));
|
||||
*/
|
||||
const emitMessage = (data) => {
|
||||
// Output from stdout/stderr coming from the CLI might be buffered,
|
||||
// causing several progress lines to come up at once as single message.
|
||||
// Trying to parse multiple JSON objects separated by new lines will
|
||||
// of course make the parser confused, causing errors later on.
|
||||
_.each(utils.splitObjectLines(data.toString()), (object) => {
|
||||
ipc.of[process.env.IPC_SERVER_ID].emit('message', object)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Emit an error message over the IPC and shut down the child
|
||||
* @function
|
||||
* @private
|
||||
* @param {Error} error - error
|
||||
* @example
|
||||
* onError(error)
|
||||
*/
|
||||
const onError = (error) => {
|
||||
ipc.of[process.env.IPC_SERVER_ID].emit('message', {
|
||||
error: error.message,
|
||||
data: error.stack
|
||||
})
|
||||
child && child.kill()
|
||||
reject(error)
|
||||
}
|
||||
|
||||
ipc.connectTo(process.env.IPC_SERVER_ID, () => {
|
||||
ipc.of[process.env.IPC_SERVER_ID].on('error', reject)
|
||||
ipc.of[process.env.IPC_SERVER_ID].on('error', onError)
|
||||
ipc.of[process.env.IPC_SERVER_ID].on('disconnect', () => {
|
||||
onError(new Error('Writer process disconnected'))
|
||||
})
|
||||
|
||||
ipc.of[process.env.IPC_SERVER_ID].on('connect', () => {
|
||||
const child = childProcess.spawn(executable, etcherArguments, {
|
||||
env: {
|
||||
// Inherit the parent evnironment
|
||||
const childEnv = _.assign({}, process.env, {
|
||||
|
||||
// The CLI might call operating system utilities (like `diskutil`),
|
||||
// so we must ensure the `PATH` is inherited.
|
||||
PATH: process.env.PATH,
|
||||
// The CLI might call operating system utilities (like `diskutil`),
|
||||
// so we must ensure the `PATH` is inherited.
|
||||
PATH: process.env.PATH,
|
||||
|
||||
ELECTRON_RUN_AS_NODE: 1,
|
||||
ETCHER_CLI_ROBOT: 1,
|
||||
ELECTRON_RUN_AS_NODE: 1,
|
||||
ETCHER_CLI_ROBOT: 1,
|
||||
|
||||
// Enable extra logging from mountutils
|
||||
// See https://github.com/resin-io-modules/mountutils
|
||||
MOUNTUTILS_DEBUG: 1
|
||||
|
||||
}
|
||||
// Enable extra logging from mountutils
|
||||
// See https://github.com/resin-io-modules/mountutils
|
||||
MOUNTUTILS_DEBUG: 1
|
||||
})
|
||||
|
||||
ipc.of[process.env.IPC_SERVER_ID].on('disconnect', _.bind(child.kill, child))
|
||||
child.on('error', reject)
|
||||
child.on('close', resolve)
|
||||
child = childProcess.spawn(executable, etcherArguments, {
|
||||
env: childEnv
|
||||
})
|
||||
|
||||
/**
|
||||
* @summary Emit an object message to the IPC server
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {Buffer} data - json message data
|
||||
*
|
||||
* @example
|
||||
* emitMessage(Buffer.from(JSON.stringify({
|
||||
* foo: 'bar'
|
||||
* })));
|
||||
*/
|
||||
const emitMessage = (data) => {
|
||||
// Output from stdout/stderr coming from the CLI might be buffered,
|
||||
// causing several progress lines to come up at once as single message.
|
||||
// Trying to parse multiple JSON objects separated by new lines will
|
||||
// of course make the parser confused, causing errors later on.
|
||||
_.each(utils.splitObjectLines(data.toString()), (object) => {
|
||||
ipc.of[process.env.IPC_SERVER_ID].emit('message', object)
|
||||
})
|
||||
}
|
||||
child.on('error', onError)
|
||||
child.on('exit', (code, signal) => {
|
||||
if (code != null && signal == null) {
|
||||
resolve(code)
|
||||
} else {
|
||||
const error = new Error(`Exited with code ${code}, signal ${signal}`)
|
||||
error.code = code
|
||||
error.signal = signal
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
|
||||
child.stdout.on('data', emitMessage)
|
||||
child.stderr.on('data', emitMessage)
|
||||
|
Loading…
x
Reference in New Issue
Block a user