feat: integrate bmap support (#635)

This PR integrates `.bmap` support recently added to
`etcher-image-write` into Etcher itself.

It does it in the following way:

- It adds a `--bmap` option to the Etcher CLI.
- It saves a potential `bmap` file contents to the
  `SelectionStateModel`.
- In the GUI, at the time of writing, if there is a `bmap` file content
  in `SelectionStateModel`, it gets written to a temporary file and such
  path is passed as the `--bmap` option to the CLI.

Since validation checksums don't make sense anymore, the finish screen
doesn't show the checksum box in this case.

Change-Type: minor
Changelog-Entry: Add `.bmap` support.
Fixes: https://github.com/resin-io/etcher/issues/171
Signed-off-by: Juan Cruz Viotti <jviottidc@gmail.com>
Signed-off-by: Juan Cruz Viotti <jviotti@openmailbox.org>
This commit is contained in:
Juan Cruz Viotti 2016-08-22 09:50:56 -04:00 committed by GitHub
parent b69d03a58b
commit a5a195e8fb
18 changed files with 176 additions and 28 deletions

View File

@ -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
```

View File

@ -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,

View File

@ -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) {

View File

@ -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) => {

View File

@ -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

View File

@ -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}`);
}

View File

@ -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);

View File

@ -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
});

View File

@ -28,7 +28,8 @@
</div>
</div>
<span class="label label-big label-default">CRC32 CHECKSUM : <b class="monospace">{{ ::finish.checksum }}</b></span>
<span class="label label-big label-default"
ng-if="finish.checksum">CRC32 CHECKSUM : <b class="monospace">{{ ::finish.checksum }}</b></span>
</div>
</div>
</div>

View File

@ -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')) {

View File

@ -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);
};
/**

View File

@ -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,

View File

@ -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'
});
};

43
npm-shrinkwrap.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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() {

View File

@ -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: '<svg><text fill="red">Raspbian</text></svg>'
logo: '<svg><text fill="red">Raspbian</text></svg>',
bmap: '<Range>Foo Bar</Range>'
});
});
@ -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('<Range>Foo Bar</Range>');
});
});
describe('.hasImage()', function() {
it('should return true', function() {

View File

@ -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);