mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-23 06:47:17 +00:00

Currently, if the child writer receives a message from the writer process that is not a valid robot object, then it will throw an error. Now, we check if the message is a robot message, and if so, we try to parse it (throwing errors if its indeed a malformed/incomplete robot message), however we log it if not. The main motivation behind this feature is that it will allows us to print debugging information on the mountutils module, and have it redirected to DevTools. Change-Type: minor Signed-off-by: Juan Cruz Viotti <jviotti@openmailbox.org>
227 lines
5.8 KiB
JavaScript
227 lines
5.8 KiB
JavaScript
/*
|
|
* Copyright 2016 resin.io
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const EventEmitter = require('events').EventEmitter;
|
|
const _ = require('lodash');
|
|
const childProcess = require('child_process');
|
|
const ipc = require('node-ipc');
|
|
const rendererUtils = require('./renderer-utils');
|
|
const cli = require('./cli');
|
|
const CONSTANTS = require('./constants');
|
|
const EXIT_CODES = require('../shared/exit-codes');
|
|
const robot = require('../shared/robot');
|
|
|
|
/**
|
|
* @summary Perform a write
|
|
* @function
|
|
* @public
|
|
*
|
|
* @param {String} image - image
|
|
* @param {Object} drive - drive
|
|
* @param {Object} options - options
|
|
* @returns {EventEmitter} event emitter
|
|
*
|
|
* @example
|
|
* const child = childWriter.write('path/to/rpi.img', {
|
|
* device: '/dev/disk2'
|
|
* }, {
|
|
* validateWriteOnSuccess: true,
|
|
* unmountOnSuccess: true
|
|
* });
|
|
*
|
|
* child.on('progress', (state) => {
|
|
* console.log(state);
|
|
* });
|
|
*
|
|
* child.on('error', (error) => {
|
|
* throw error;
|
|
* });
|
|
*
|
|
* child.on('done', () => {
|
|
* console.log('Validation was successful!');
|
|
* });
|
|
*/
|
|
exports.write = (image, drive, options) => {
|
|
const emitter = new EventEmitter();
|
|
|
|
const argv = cli.getArguments({
|
|
entryPoint: rendererUtils.getApplicationEntryPoint(),
|
|
image,
|
|
device: drive.device,
|
|
validateWriteOnSuccess: options.validateWriteOnSuccess,
|
|
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();
|
|
|
|
/**
|
|
* @summary Safely terminate the IPC server
|
|
* @function
|
|
* @private
|
|
*
|
|
* @example
|
|
* terminateServer();
|
|
*/
|
|
const terminateServer = () => {
|
|
|
|
// Turns out we need to destroy all sockets for
|
|
// the server to actually close. Otherwise, it
|
|
// just stops receiving any further connections,
|
|
// but remains open if there are active ones.
|
|
_.each(ipc.server.sockets, (socket) => {
|
|
socket.destroy();
|
|
});
|
|
|
|
ipc.server.stop();
|
|
};
|
|
|
|
/**
|
|
* @summary Emit an error to the client
|
|
* @function
|
|
* @private
|
|
*
|
|
* @param {Error} error - error
|
|
*
|
|
* @example
|
|
* emitError(new Error('foo bar'));
|
|
*/
|
|
const emitError = (error) => {
|
|
terminateServer();
|
|
emitter.emit('error', error);
|
|
};
|
|
|
|
/**
|
|
* @summary Bridge robot message to the child writer caller
|
|
* @function
|
|
* @private
|
|
*
|
|
* @param {String} message - robot message
|
|
*
|
|
* @example
|
|
* bridgeRobotMessage(robot.buildMessage('foo', {
|
|
* bar: 'baz'
|
|
* }));
|
|
*/
|
|
const bridgeRobotMessage = (message) => {
|
|
const ROBOT_COMMAND_LOG = 'log';
|
|
|
|
const parsedMessage = _.attempt(() => {
|
|
if (robot.isMessage(message)) {
|
|
return robot.parseMessage(message);
|
|
}
|
|
|
|
// Don't be so strict. If a message doesn't look like
|
|
// a robot message, then make the child writer log it
|
|
// for debugging purposes.
|
|
return robot.parseMessage(robot.buildMessage(ROBOT_COMMAND_LOG, {
|
|
message
|
|
}));
|
|
|
|
});
|
|
|
|
if (_.isError(parsedMessage)) {
|
|
emitError(parsedMessage);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
|
|
// These are lighweight accessor methods for
|
|
// the properties of the parsed message
|
|
const messageCommand = robot.getCommand(parsedMessage);
|
|
const messageData = robot.getData(parsedMessage);
|
|
|
|
// The error object is decomposed by the CLI for serialisation
|
|
// purposes. We compose it back to an `Error` here in order
|
|
// to provide better encapsulation.
|
|
if (messageCommand === 'error') {
|
|
emitError(robot.recomposeErrorMessage(parsedMessage));
|
|
} else if (messageCommand === ROBOT_COMMAND_LOG) {
|
|
|
|
// If the message data is an object and it contains a
|
|
// message string then log the message string only.
|
|
if (_.isPlainObject(messageData) && _.isString(messageData.message)) {
|
|
console.log(messageData.message);
|
|
} else {
|
|
console.log(messageData);
|
|
}
|
|
|
|
} else {
|
|
emitter.emit(messageCommand, messageData);
|
|
}
|
|
|
|
} catch (error) {
|
|
emitError(error);
|
|
}
|
|
};
|
|
|
|
ipc.server.on('error', emitError);
|
|
ipc.server.on('message', bridgeRobotMessage);
|
|
|
|
ipc.server.on('start', () => {
|
|
const child = childProcess.fork(CONSTANTS.WRITER_PROXY_SCRIPT, argv, {
|
|
silent: true,
|
|
env: process.env
|
|
});
|
|
|
|
child.stdout.on('data', (data) => {
|
|
console.info(`WRITER: ${data.toString()}`);
|
|
});
|
|
|
|
child.stderr.on('data', (data) => {
|
|
bridgeRobotMessage(data.toString());
|
|
|
|
// This function causes the `close` event to be emitted
|
|
child.kill();
|
|
|
|
});
|
|
|
|
child.on('error', emitError);
|
|
|
|
child.on('close', (code) => {
|
|
terminateServer();
|
|
|
|
if (code === EXIT_CODES.CANCELLED) {
|
|
return emitter.emit('done', {
|
|
cancelled: true
|
|
});
|
|
}
|
|
|
|
// We shouldn't emit the `done` event manually here
|
|
// since the writer process will take care of it.
|
|
if (code === EXIT_CODES.SUCCESS || code === EXIT_CODES.VALIDATION_ERROR) {
|
|
return null;
|
|
}
|
|
|
|
return emitError(new Error(`Child process exited with error code: ${code}`));
|
|
});
|
|
});
|
|
|
|
ipc.server.start();
|
|
|
|
return emitter;
|
|
};
|