Implement writing by spawning the CLI as a child process (#400)

After this change, the CLI becomes the only entity actually performing
I/O with the devices, and the GUI is just a wrapper around it.

When you click "Flash", the GUI spawns the CLI with all the appropriate
options, including `--ipc`, which uses an IPC communication channel to
report status back to the parent process.

Signed-off-by: Juan Cruz Viotti <jviottidc@gmail.com>
This commit is contained in:
Juan Cruz Viotti 2016-05-11 17:52:09 -04:00
parent f365f2fa57
commit 662c589ab9
6 changed files with 124 additions and 12 deletions

View File

@ -80,6 +80,14 @@ module.exports = yargs
return true;
})
.check(function(argv) {
if (argv.ipc && !process.send) {
throw new Error('Can\'t establish an IPC channel with the parent process');
}
return true;
})
.options({
help: {
describe: 'show help',
@ -107,6 +115,11 @@ module.exports = yargs
boolean: true,
alias: 'r'
},
ipc: {
describe: 'communicate through a fork IPC channel',
boolean: true,
alias: 'i'
},
yes: {
describe: 'confirm non-interactively',
boolean: true,
@ -132,7 +145,11 @@ module.exports = yargs
// like *.exe and *.cmd in Windows systems.
const executable = path.basename(argv[0], path.extname(argv[0])).toLowerCase();
if (executable === 'node' || executable === 'electron') {
if (_.includes([
'node',
'electron',
'electron helper'
], executable)) {
// In this case, the second argument (e.g: index 1)
// equals `lib/cli/etcher.js`, so the real arguments

View File

@ -42,7 +42,7 @@ form.run([
// If `options.yes` is `false`, pass `undefined`,
// otherwise the question will not be asked because
// `false` is a defined value.
yes: options.robot || options.yes || undefined
yes: options.robot || options.ipc || options.yes || undefined
}
}).then(function(answers) {
@ -70,6 +70,16 @@ form.run([
state.eta + 's',
Math.floor(state.speed)
].join(' '));
} else if (options.ipc) {
process.send({
command: 'progress',
data: {
type: state.type,
percentage: Math.floor(state.percentage),
eta: state.eta,
speed: Math.floor(state.speed)
}
});
} else {
progressBars[state.type].update(state);
}
@ -79,6 +89,13 @@ form.run([
if (options.robot) {
console.log(`done ${success}`);
} else if (options.ipc) {
process.send({
command: 'done',
data: {
success: success
}
});
} else {
if (success) {
console.log('Your flash is complete!');

View File

@ -60,6 +60,13 @@ analytics.config(function($mixpanelProvider) {
// http://docs.trackjs.com/tracker/framework-integrations
analytics.run(function($window) {
// Don't configure TrackJS when
// running inside the test suite
if (window.mocha) {
return;
}
$window.trackJs.configure({
userId: username.sync(),
version: packageJSON.version

View File

@ -21,21 +21,20 @@
*/
const angular = require('angular');
const path = require('path');
const isRunningInAsar = require('electron-is-running-in-asar');
const electron = require('electron');
if (window.mocha) {
var writer = electron.remote.require(require('path').join(__dirname, '..', '..', 'src', 'writer'));
} else {
var writer = electron.remote.require('./src/writer');
}
const childProcess = require('child_process');
const EXIT_CODES = require('../../src/exit-codes');
const MODULE_NAME = 'Etcher.image-writer';
const imageWriter = angular.module(MODULE_NAME, [
require('../models/settings'),
require('../modules/analytics'),
require('../utils/notifier/notifier')
]);
imageWriter.service('ImageWriterService', function($q, $timeout, SettingsModel, NotifierService) {
imageWriter.service('ImageWriterService', function($q, $timeout, SettingsModel, NotifierService, AnalyticsService) {
let self = this;
let flashing = false;
@ -107,6 +106,7 @@ imageWriter.service('ImageWriterService', function($q, $timeout, SettingsModel,
* @param {Object} drive - drive
* @param {Function} onProgress - in progress callback (state)
*
* @fulfil {Boolean} - whether the operation succeeded
* @returns {Promise}
*
* @example
@ -117,7 +117,73 @@ imageWriter.service('ImageWriterService', function($q, $timeout, SettingsModel,
* });
*/
this.performWrite = function(image, drive, onProgress) {
return $q.when(writer.writeImage(image, drive, SettingsModel.data, onProgress));
const argv = [ image ];
argv.push('--drive', drive.device);
// Make use of `child_process.fork()` facility to send
// messages to the parent through an IPC channel.
// This allows us to receive the progress state by
// listening to the `message` event of the
// `ChildProcess` object.
argv.push('--ipc');
// Explicitly set the boolen flag in positive
// or negative way in order to be on the safe
// side in case the Etcher CLI changes the
// default value of these options.
if (SettingsModel.data.unmountOnSuccess) {
argv.push('--unmount');
} else {
argv.push('--no-unmount');
}
if (SettingsModel.data.validateWriteOnSuccess) {
argv.push('--check');
} else {
argv.push('--no-check');
}
return $q(function(resolve, reject) {
let executable;
if (isRunningInAsar()) {
executable = path.join(process.resourcesPath, 'app.asar');
} else {
executable = electron.remote.process.argv[1];
}
AnalyticsService.log(`Forking: ${executable} ${argv.join(' ')}`);
const child = childProcess.fork(executable, argv, {
// Pipe stdout/stderr to the parent
// We're not using it directly but its
// handy for debugging reasons.
silent: true
});
child.on('message', function(message) {
if (message.command === 'progress') {
return onProgress(message.data);
}
});
child.on('error', reject);
child.on('close', function(code) {
if (code === EXIT_CODES.SUCCESS) {
return resolve(true);
}
if (code === EXIT_CODES.VALIDATION_ERROR) {
return resolve(false);
}
return reject(new Error(`Child process exitted with error code: ${code}`));
});
});
};
/**
@ -154,7 +220,7 @@ imageWriter.service('ImageWriterService', function($q, $timeout, SettingsModel,
self.state = {
type: state.type,
progress: Math.floor(state.percentage),
progress: state.percentage,
// Transform bytes to megabytes preserving only two decimal places
speed: Math.floor(state.speed / 1e+6 * 100) / 100 || 0

View File

@ -22,7 +22,11 @@
// *won't* attempt to load the `app.asar` application by default, therefore
// if passing `ELECTRON_RUN_AS_NODE`, you have to pass the path to the asar
// or the entry point file (this file) manually as an argument.
if (process.env.ELECTRON_RUN_AS_NODE) {
//
// We also consider `ATOM_SHELL_INTERNAL_RUN_AS_NODE`, which is basically
// an older equivalent of `ELECTRON_RUN_AS_NODE` that still gets set when
// using `child_process.fork()`.
if (process.env.ELECTRON_RUN_AS_NODE || process.env.ATOM_SHELL_INTERNAL_RUN_AS_NODE) {
require('./cli/etcher');
} else {

View File

@ -54,6 +54,7 @@
"bootstrap-sass": "^3.3.5",
"chalk": "^1.1.3",
"drivelist": "^3.0.0",
"electron-is-running-in-asar": "^1.0.0",
"etcher-image-stream": "^1.2.0",
"etcher-image-write": "^4.0.2",
"flexboxgrid": "^6.3.0",