diff --git a/lib/cli/cli.js b/lib/cli/cli.js index bec5620a..2302e3a7 100644 --- a/lib/cli/cli.js +++ b/lib/cli/cli.js @@ -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 diff --git a/lib/cli/etcher.js b/lib/cli/etcher.js index 52040842..7935908e 100644 --- a/lib/cli/etcher.js +++ b/lib/cli/etcher.js @@ -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!'); diff --git a/lib/gui/modules/analytics.js b/lib/gui/modules/analytics.js index 7e2c52be..36f593ec 100644 --- a/lib/gui/modules/analytics.js +++ b/lib/gui/modules/analytics.js @@ -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 diff --git a/lib/gui/modules/image-writer.js b/lib/gui/modules/image-writer.js index 324896e5..eff56bf2 100644 --- a/lib/gui/modules/image-writer.js +++ b/lib/gui/modules/image-writer.js @@ -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 diff --git a/lib/start.js b/lib/start.js index 4abbfa77..9ca67e88 100644 --- a/lib/start.js +++ b/lib/start.js @@ -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 { diff --git a/package.json b/package.json index 9abafd77..f91ecbbe 100644 --- a/package.json +++ b/package.json @@ -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",