mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-26 20:56:34 +00:00
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:
parent
f365f2fa57
commit
662c589ab9
@ -80,6 +80,14 @@ module.exports = yargs
|
|||||||
return true;
|
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({
|
.options({
|
||||||
help: {
|
help: {
|
||||||
describe: 'show help',
|
describe: 'show help',
|
||||||
@ -107,6 +115,11 @@ module.exports = yargs
|
|||||||
boolean: true,
|
boolean: true,
|
||||||
alias: 'r'
|
alias: 'r'
|
||||||
},
|
},
|
||||||
|
ipc: {
|
||||||
|
describe: 'communicate through a fork IPC channel',
|
||||||
|
boolean: true,
|
||||||
|
alias: 'i'
|
||||||
|
},
|
||||||
yes: {
|
yes: {
|
||||||
describe: 'confirm non-interactively',
|
describe: 'confirm non-interactively',
|
||||||
boolean: true,
|
boolean: true,
|
||||||
@ -132,7 +145,11 @@ module.exports = yargs
|
|||||||
// like *.exe and *.cmd in Windows systems.
|
// like *.exe and *.cmd in Windows systems.
|
||||||
const executable = path.basename(argv[0], path.extname(argv[0])).toLowerCase();
|
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)
|
// In this case, the second argument (e.g: index 1)
|
||||||
// equals `lib/cli/etcher.js`, so the real arguments
|
// equals `lib/cli/etcher.js`, so the real arguments
|
||||||
|
@ -42,7 +42,7 @@ form.run([
|
|||||||
// If `options.yes` is `false`, pass `undefined`,
|
// If `options.yes` is `false`, pass `undefined`,
|
||||||
// otherwise the question will not be asked because
|
// otherwise the question will not be asked because
|
||||||
// `false` is a defined value.
|
// `false` is a defined value.
|
||||||
yes: options.robot || options.yes || undefined
|
yes: options.robot || options.ipc || options.yes || undefined
|
||||||
|
|
||||||
}
|
}
|
||||||
}).then(function(answers) {
|
}).then(function(answers) {
|
||||||
@ -70,6 +70,16 @@ form.run([
|
|||||||
state.eta + 's',
|
state.eta + 's',
|
||||||
Math.floor(state.speed)
|
Math.floor(state.speed)
|
||||||
].join(' '));
|
].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 {
|
} else {
|
||||||
progressBars[state.type].update(state);
|
progressBars[state.type].update(state);
|
||||||
}
|
}
|
||||||
@ -79,6 +89,13 @@ form.run([
|
|||||||
|
|
||||||
if (options.robot) {
|
if (options.robot) {
|
||||||
console.log(`done ${success}`);
|
console.log(`done ${success}`);
|
||||||
|
} else if (options.ipc) {
|
||||||
|
process.send({
|
||||||
|
command: 'done',
|
||||||
|
data: {
|
||||||
|
success: success
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
if (success) {
|
if (success) {
|
||||||
console.log('Your flash is complete!');
|
console.log('Your flash is complete!');
|
||||||
|
@ -60,6 +60,13 @@ analytics.config(function($mixpanelProvider) {
|
|||||||
// http://docs.trackjs.com/tracker/framework-integrations
|
// http://docs.trackjs.com/tracker/framework-integrations
|
||||||
|
|
||||||
analytics.run(function($window) {
|
analytics.run(function($window) {
|
||||||
|
|
||||||
|
// Don't configure TrackJS when
|
||||||
|
// running inside the test suite
|
||||||
|
if (window.mocha) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$window.trackJs.configure({
|
$window.trackJs.configure({
|
||||||
userId: username.sync(),
|
userId: username.sync(),
|
||||||
version: packageJSON.version
|
version: packageJSON.version
|
||||||
|
@ -21,21 +21,20 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const angular = require('angular');
|
const angular = require('angular');
|
||||||
|
const path = require('path');
|
||||||
|
const isRunningInAsar = require('electron-is-running-in-asar');
|
||||||
const electron = require('electron');
|
const electron = require('electron');
|
||||||
|
const childProcess = require('child_process');
|
||||||
if (window.mocha) {
|
const EXIT_CODES = require('../../src/exit-codes');
|
||||||
var writer = electron.remote.require(require('path').join(__dirname, '..', '..', 'src', 'writer'));
|
|
||||||
} else {
|
|
||||||
var writer = electron.remote.require('./src/writer');
|
|
||||||
}
|
|
||||||
|
|
||||||
const MODULE_NAME = 'Etcher.image-writer';
|
const MODULE_NAME = 'Etcher.image-writer';
|
||||||
const imageWriter = angular.module(MODULE_NAME, [
|
const imageWriter = angular.module(MODULE_NAME, [
|
||||||
require('../models/settings'),
|
require('../models/settings'),
|
||||||
|
require('../modules/analytics'),
|
||||||
require('../utils/notifier/notifier')
|
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 self = this;
|
||||||
let flashing = false;
|
let flashing = false;
|
||||||
|
|
||||||
@ -107,6 +106,7 @@ imageWriter.service('ImageWriterService', function($q, $timeout, SettingsModel,
|
|||||||
* @param {Object} drive - drive
|
* @param {Object} drive - drive
|
||||||
* @param {Function} onProgress - in progress callback (state)
|
* @param {Function} onProgress - in progress callback (state)
|
||||||
*
|
*
|
||||||
|
* @fulfil {Boolean} - whether the operation succeeded
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
@ -117,7 +117,73 @@ imageWriter.service('ImageWriterService', function($q, $timeout, SettingsModel,
|
|||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
this.performWrite = function(image, drive, onProgress) {
|
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 = {
|
self.state = {
|
||||||
type: state.type,
|
type: state.type,
|
||||||
progress: Math.floor(state.percentage),
|
progress: state.percentage,
|
||||||
|
|
||||||
// Transform bytes to megabytes preserving only two decimal places
|
// Transform bytes to megabytes preserving only two decimal places
|
||||||
speed: Math.floor(state.speed / 1e+6 * 100) / 100 || 0
|
speed: Math.floor(state.speed / 1e+6 * 100) / 100 || 0
|
||||||
|
@ -22,7 +22,11 @@
|
|||||||
// *won't* attempt to load the `app.asar` application by default, therefore
|
// *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
|
// 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.
|
// 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');
|
require('./cli/etcher');
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -54,6 +54,7 @@
|
|||||||
"bootstrap-sass": "^3.3.5",
|
"bootstrap-sass": "^3.3.5",
|
||||||
"chalk": "^1.1.3",
|
"chalk": "^1.1.3",
|
||||||
"drivelist": "^3.0.0",
|
"drivelist": "^3.0.0",
|
||||||
|
"electron-is-running-in-asar": "^1.0.0",
|
||||||
"etcher-image-stream": "^1.2.0",
|
"etcher-image-stream": "^1.2.0",
|
||||||
"etcher-image-write": "^4.0.2",
|
"etcher-image-write": "^4.0.2",
|
||||||
"flexboxgrid": "^6.3.0",
|
"flexboxgrid": "^6.3.0",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user