diff --git a/docs/CLI.md b/docs/CLI.md index 480914e6..e73d6771 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -39,6 +39,7 @@ Options --check, -c validate write --robot, -r parse-able output without interactivity --log, -l output log file + --bmap, -b bmap file --yes, -y confirm non-interactively --unmount, -u unmount on success ``` diff --git a/lib/cli/cli.js b/lib/cli/cli.js index 6d7d7a1a..4620d19b 100644 --- a/lib/cli/cli.js +++ b/lib/cli/cli.js @@ -120,6 +120,11 @@ module.exports = yargs string: true, alias: 'l' }, + bmap: { + describe: 'bmap file', + string: true, + alias: 'b' + }, yes: { describe: 'confirm non-interactively', boolean: true, diff --git a/lib/cli/etcher.js b/lib/cli/etcher.js index 1752a542..75ff5a28 100644 --- a/lib/cli/etcher.js +++ b/lib/cli/etcher.js @@ -18,6 +18,7 @@ const _ = require('lodash'); const Bluebird = require('bluebird'); +const fs = Bluebird.promisifyAll(require('fs')); const visuals = require('resin-cli-visuals'); const form = require('resin-cli-form'); const drivelist = Bluebird.promisifyAll(require('drivelist')); @@ -59,8 +60,19 @@ form.run([ check: new visuals.Progress('Validating') }; - return drivelist.listAsync().then((drives) => { - const selectedDrive = _.find(drives, { + return Bluebird.props({ + drives: drivelist.listAsync(), + bmap: _.attempt(() => { + if (!options.bmap) { + return; + } + + return fs.readFileAsync(options.bmap, { + encoding: 'utf8' + }); + }) + }).then((results) => { + const selectedDrive = _.find(results.drives, { device: answers.drive }); @@ -70,7 +82,8 @@ form.run([ return writer.writeImage(options._[0], selectedDrive, { unmountOnSuccess: options.unmount, - validateWriteOnSuccess: options.check + validateWriteOnSuccess: options.check, + bmapContents: results.bmap }, (state) => { if (options.robot) { diff --git a/lib/cli/writer.js b/lib/cli/writer.js index 74a44d82..b8a78cf1 100644 --- a/lib/cli/writer.js +++ b/lib/cli/writer.js @@ -37,6 +37,7 @@ const isWindows = os.platform() === 'win32'; * @param {Object} options - options * @param {Boolean} [options.unmountOnSuccess=false] - unmount on success * @param {Boolean} [options.validateWriteOnSuccess=false] - validate write on success + * @param {String} [options.bmapContents] - bmap file contents * @param {Function} onProgress - on progress callback (state) * * @fulfil {Boolean} - whether the operation was successful @@ -60,7 +61,8 @@ exports.writeImage = (imagePath, drive, options, onProgress) => { }).then((image) => { return imageWrite.write(drive, image, { check: options.validateWriteOnSuccess, - transform: image.transform + transform: image.transform, + bmap: options.bmapContents }); }).then((writer) => { return new Bluebird((resolve, reject) => { diff --git a/lib/gui/models/selection-state.js b/lib/gui/models/selection-state.js index fadc6726..a44f6fd1 100644 --- a/lib/gui/models/selection-state.js +++ b/lib/gui/models/selection-state.js @@ -260,6 +260,20 @@ SelectionStateModel.service('SelectionStateModel', function(DrivesModel) { return _.get(Store.getState().toJS(), 'selection.image.logo'); }; + /** + * @summary Get image bmap + * @function + * @public + * + * @returns {String} image bmap + * + * @example + * const imageBmap = SelectionStateModel.getImageBmap(); + */ + this.getImageBmap = () => { + return _.get(Store.getState().toJS(), 'selection.image.bmap'); + }; + /** * @summary Check if there is a selected drive * @function diff --git a/lib/gui/models/store.js b/lib/gui/models/store.js index bd22ad40..54e29806 100644 --- a/lib/gui/models/store.js +++ b/lib/gui/models/store.js @@ -191,11 +191,7 @@ const storeReducer = (state, action) => { throw new Error('The passedValidation value can\'t be true if the flashing was cancelled'); } - if (action.data.passedValidation && !action.data.sourceChecksum) { - throw new Error('Missing results sourceChecksum'); - } - - if (action.data.passedValidation && !_.isString(action.data.sourceChecksum)) { + if (action.data.passedValidation && action.data.sourceChecksum && !_.isString(action.data.sourceChecksum)) { throw new Error(`Invalid results sourceChecksum: ${action.data.sourceChecksum}`); } diff --git a/lib/gui/modules/image-writer.js b/lib/gui/modules/image-writer.js index 13b75bb0..08537be6 100644 --- a/lib/gui/modules/image-writer.js +++ b/lib/gui/modules/image-writer.js @@ -26,10 +26,11 @@ const childWriter = require('../../src/child-writer'); const MODULE_NAME = 'Etcher.Modules.ImageWriter'; const imageWriter = angular.module(MODULE_NAME, [ require('../models/settings'), + require('../models/selection-state'), require('../models/flash-state') ]); -imageWriter.service('ImageWriterService', function($q, $rootScope, SettingsModel, FlashStateModel) { +imageWriter.service('ImageWriterService', function($q, $rootScope, SettingsModel, SelectionStateModel, FlashStateModel) { /** * @summary Perform write operation @@ -57,7 +58,8 @@ imageWriter.service('ImageWriterService', function($q, $rootScope, SettingsModel return $q((resolve, reject) => { const child = childWriter.write(image, drive, { validateWriteOnSuccess: SettingsModel.get('validateWriteOnSuccess'), - unmountOnSuccess: SettingsModel.get('unmountOnSuccess') + unmountOnSuccess: SettingsModel.get('unmountOnSuccess'), + bmapContents: SelectionStateModel.getImageBmap() }); child.on('error', reject); child.on('done', resolve); diff --git a/lib/gui/os/dialog/services/dialog.js b/lib/gui/os/dialog/services/dialog.js index 76ee5156..256b2d12 100644 --- a/lib/gui/os/dialog/services/dialog.js +++ b/lib/gui/os/dialog/services/dialog.js @@ -76,6 +76,7 @@ module.exports = function($q, SupportedFormatsModel) { path: imagePath, size: metadata.estimatedSize, name: metadata.name, + bmap: metadata.bmap, url: metadata.url, logo: metadata.logo }); diff --git a/lib/gui/pages/finish/templates/success.tpl.html b/lib/gui/pages/finish/templates/success.tpl.html index c78dc05c..07690d02 100644 --- a/lib/gui/pages/finish/templates/success.tpl.html +++ b/lib/gui/pages/finish/templates/success.tpl.html @@ -28,7 +28,8 @@ - CRC32 CHECKSUM : {{ ::finish.checksum }} + CRC32 CHECKSUM : {{ ::finish.checksum }} diff --git a/lib/gui/pages/main/controllers/flash.js b/lib/gui/pages/main/controllers/flash.js index a17b27df..eeb7f827 100644 --- a/lib/gui/pages/main/controllers/flash.js +++ b/lib/gui/pages/main/controllers/flash.js @@ -107,7 +107,7 @@ module.exports = function( return 'Flash!'; } - if (flashState.percentage === 0) { + if (flashState.percentage === 0 && !flashState.speed) { return 'Starting...'; } else if (flashState.percentage === 100) { if (isChecking && SettingsModel.get('unmountOnSuccess')) { diff --git a/lib/gui/pages/main/controllers/image-selection.js b/lib/gui/pages/main/controllers/image-selection.js index fab0f0d0..c4fdadc6 100644 --- a/lib/gui/pages/main/controllers/image-selection.js +++ b/lib/gui/pages/main/controllers/image-selection.js @@ -58,7 +58,13 @@ module.exports = function(SupportedFormatsModel, SelectionStateModel, AnalyticsS } SelectionStateModel.setImage(image); - AnalyticsService.logEvent('Select image', _.omit(image, 'logo')); + + // An easy way so we can quickly identify if we're making use of + // certain features without printing pages of text to DevTools. + image.logo = Boolean(image.logo); + image.bmap = Boolean(image.bmap); + + AnalyticsService.logEvent('Select image', image); }; /** diff --git a/lib/src/child-writer/index.js b/lib/src/child-writer/index.js index 95108250..c315a560 100644 --- a/lib/src/child-writer/index.js +++ b/lib/src/child-writer/index.js @@ -17,6 +17,9 @@ 'use strict'; const EventEmitter = require('events').EventEmitter; +const _ = require('lodash'); +const Bluebird = require('bluebird'); +const fs = Bluebird.promisifyAll(require('fs')); const childProcess = require('child_process'); const rendererUtils = require('./renderer-utils'); const utils = require('./utils'); @@ -58,10 +61,22 @@ const EXIT_CODES = require('../exit-codes'); exports.write = (image, drive, options) => { const emitter = new EventEmitter(); - utils.getTemporaryLogFilePath().then((logFile) => { + Bluebird.props({ + logFile: utils.getTemporaryLogFilePath(), + bmapFile: _.attempt(() => { + if (!options.bmapContents) { + return; + } + + return utils.getTemporaryBmapFilePath().tap((bmapFilePath) => { + return fs.writeFileAsync(bmapFilePath, options.bmapContents); + }); + }) + }).then((results) => { const argv = utils.getCLIWriterArguments({ entryPoint: rendererUtils.getApplicationEntryPoint(), - logFile: logFile, + logFile: results.logFile, + bmap: results.bmapFile, image: image, device: drive.device, validateWriteOnSuccess: options.validateWriteOnSuccess, @@ -70,7 +85,7 @@ exports.write = (image, drive, options) => { // Make writer proxy inherit the temporary log file location // while keeping current environment variables intact. - process.env[CONSTANTS.TEMPORARY_LOG_FILE_ENVIRONMENT_VARIABLE] = logFile; + process.env[CONSTANTS.TEMPORARY_LOG_FILE_ENVIRONMENT_VARIABLE] = results.logFile; const child = childProcess.fork(CONSTANTS.WRITER_PROXY_SCRIPT, argv, { silent: true, diff --git a/lib/src/child-writer/utils.js b/lib/src/child-writer/utils.js index 3885759d..93c7615d 100644 --- a/lib/src/child-writer/utils.js +++ b/lib/src/child-writer/utils.js @@ -60,6 +60,7 @@ exports.getBooleanArgumentForm = (argumentName, value) => { * @param {String} options.device - device * @param {String} options.entryPoint - entry point * @param {String} [options.logFile] - log file + * @param {String} [options.bmap] - bmap file * @param {Boolean} [options.validateWriteOnSuccess] - validate write on success * @param {Boolean} [options.unmountOnSuccess] - unmount on success * @returns {String[]} arguments @@ -107,6 +108,10 @@ exports.getCLIWriterArguments = (options) => { argv.push('--log', options.logFile); } + if (options.bmap) { + argv.push('--bmap', options.bmap); + } + return argv; }; @@ -146,3 +151,23 @@ exports.getTemporaryLogFilePath = () => { postfix: '.log' }); }; + +/** + * @summary Get a temporary bmap file path + * @function + * @public + * + * @fulfil {String} - bmap path + * @returns {Promise} + * + * @example + * utils.getTemporaryBmapFilePath().then((bmapFilePath) => { + * console.log(bmapFilePath); + * }); + */ +exports.getTemporaryBmapFilePath = () => { + return tmp.fileAsync({ + prefix: `${packageJSON.name}-`, + postfix: '.bmap' + }); +}; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 6533ab72..29bfad74 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -398,6 +398,43 @@ "from": "bluebird-retry@>=0.7.0 <0.8.0", "resolved": "https://registry.npmjs.org/bluebird-retry/-/bluebird-retry-0.7.0.tgz" }, + "bmapflash": { + "version": "1.1.2", + "from": "bmapflash@>=1.1.2 <2.0.0", + "resolved": "https://registry.npmjs.org/bmapflash/-/bmapflash-1.1.2.tgz", + "dependencies": { + "isarray": { + "version": "1.0.0", + "from": "isarray@~1.0.0", + "resolved": "http://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + }, + "lodash": { + "version": "4.15.0", + "from": "lodash@>=4.14.2 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.15.0.tgz" + }, + "readable-stream": { + "version": "2.0.6", + "from": "readable-stream@~2.0.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz" + }, + "through2": { + "version": "2.0.1", + "from": "through2@^2.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.1.tgz" + }, + "xml2js": { + "version": "0.4.17", + "from": "xml2js@>=0.4.17 <0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.17.tgz" + }, + "xtend": { + "version": "4.0.1", + "from": "xtend@~4.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" + } + } + }, "bn.js": { "version": "4.11.4", "from": "bn.js@>=4.1.1 <5.0.0", @@ -1371,9 +1408,9 @@ } }, "etcher-image-write": { - "version": "6.0.1", - "from": "etcher-image-write@6.0.1", - "resolved": "https://registry.npmjs.org/etcher-image-write/-/etcher-image-write-6.0.1.tgz", + "version": "6.1.1", + "from": "etcher-image-write@6.1.1", + "resolved": "https://registry.npmjs.org/etcher-image-write/-/etcher-image-write-6.1.1.tgz", "dependencies": { "isarray": { "version": "1.0.0", diff --git a/package.json b/package.json index 67c15427..1dc4eeda 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "drivelist": "^3.2.6", "electron-is-running-in-asar": "^1.0.0", "etcher-image-stream": "^3.1.0", - "etcher-image-write": "^6.0.1", + "etcher-image-write": "^6.1.1", "etcher-latest-version": "^1.0.0", "file-tail": "^0.3.0", "flexboxgrid": "^6.3.0", diff --git a/tests/gui/models/flash-state.spec.js b/tests/gui/models/flash-state.spec.js index dd63a4ec..eafc1fb0 100644 --- a/tests/gui/models/flash-state.spec.js +++ b/tests/gui/models/flash-state.spec.js @@ -338,13 +338,13 @@ describe('Browser: FlashStateModel', function() { }).to.throw('Invalid results cancelled: false'); }); - it('should throw if passedValidation is true and sourceChecksum does not exist', function() { + it('should not throw if passedValidation is true and sourceChecksum does not exist', function() { m.chai.expect(function() { FlashStateModel.unsetFlashingFlag({ passedValidation: true, cancelled: false }); - }).to.throw('Missing results sourceChecksum'); + }).to.not.throw(); }); it('should throw if passedValidation is true and sourceChecksum is not a string', function() { diff --git a/tests/gui/models/selection-state.spec.js b/tests/gui/models/selection-state.spec.js index 5047f414..c33e9c3e 100644 --- a/tests/gui/models/selection-state.spec.js +++ b/tests/gui/models/selection-state.spec.js @@ -55,6 +55,10 @@ describe('Browser: SelectionState', function() { m.chai.expect(SelectionStateModel.getImageLogo()).to.be.undefined; }); + it('getImageBmap() should return undefined', function() { + m.chai.expect(SelectionStateModel.getImageBmap()).to.be.undefined; + }); + it('hasDrive() should return false', function() { const hasDrive = SelectionStateModel.hasDrive(); m.chai.expect(hasDrive).to.be.false; @@ -272,7 +276,8 @@ describe('Browser: SelectionState', function() { size: 999999999, url: 'https://www.raspbian.org', name: 'Raspbian', - logo: 'Raspbian' + logo: 'Raspbian', + bmap: 'Foo Bar' }); }); @@ -425,6 +430,15 @@ describe('Browser: SelectionState', function() { }); + describe('.getImageBmap()', function() { + + it('should return the image bmap', function() { + const imageBmap = SelectionStateModel.getImageBmap(); + m.chai.expect(imageBmap).to.equal('Foo Bar'); + }); + + }); + describe('.hasImage()', function() { it('should return true', function() { diff --git a/tests/gui/pages/main.spec.js b/tests/gui/pages/main.spec.js index eaef2934..ecf28aa5 100644 --- a/tests/gui/pages/main.spec.js +++ b/tests/gui/pages/main.spec.js @@ -177,6 +177,22 @@ describe('Browser: MainPage', function() { FlashStateModel.setFlashingFlag(); }); + it('should report 0% if percentage == 0 but speed != 0', function() { + const controller = $controller('FlashController', { + $scope: {} + }); + + FlashStateModel.setProgressState({ + type: 'write', + percentage: 0, + eta: 15, + speed: 100000000000000 + }); + + SettingsModel.set('unmountOnSuccess', true); + m.chai.expect(controller.getProgressButtonLabel()).to.equal('0%'); + }); + it('should handle percentage == 0, type = write, unmountOnSuccess', function() { const controller = $controller('FlashController', { $scope: {} @@ -186,7 +202,7 @@ describe('Browser: MainPage', function() { type: 'write', percentage: 0, eta: 15, - speed: 1000 + speed: 0 }); SettingsModel.set('unmountOnSuccess', true); @@ -202,7 +218,7 @@ describe('Browser: MainPage', function() { type: 'write', percentage: 0, eta: 15, - speed: 1000 + speed: 0 }); SettingsModel.set('unmountOnSuccess', false); @@ -218,7 +234,7 @@ describe('Browser: MainPage', function() { type: 'check', percentage: 0, eta: 15, - speed: 1000 + speed: 0 }); SettingsModel.set('unmountOnSuccess', true); @@ -234,7 +250,7 @@ describe('Browser: MainPage', function() { type: 'check', percentage: 0, eta: 15, - speed: 1000 + speed: 0 }); SettingsModel.set('unmountOnSuccess', false);