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: ''
+ logo: '',
+ 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);