Jonas Hermsmeier 07b6dd247d feat(robot): Support logging arbitrary data to the console (#1212)
This adds a new command recognition for message type "log",
to enable `console.log()`ing arbitrary data to the parent process console.

Not sure if we'd really want this in, but it's very handy for debugging,
and would keep us from writing these 3 lines every time
we want to inspect something in between those processes.

Change-Type: minor
2017-04-06 14:40:38 -04:00

199 lines
5.1 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) => {
try {
const parsedMessage = robot.parseMessage(message);
// 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 === 'log') {
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;
};