feat(GUI): display a nice alert ribbon if drive runs out of space (#588)

We try our best to check that the images the user select are too big for
the selected drive as early as possible, but this probes to be
problematic with certain compressed formats, like bzip2, which doesn't
store any information about the uncompressed size, requiring a ~50s
intensive computation as a minimum to find it out.

For these kinds of formats, we don't perform an early check, but instead
gracefully handle the case where the drive doesn't have any more space.

This PR handles an `ENOSPC` error by displaying the alert orange ribbon,
and prompting the user to retry with a larger drive. This is a huge
improvement over the cryptic `EIO` error what was thrown before, and
over having Etcher freeze at a certain percentage point.

Change-Type: minor
Changelog-Entry: Display a nice alert ribbon if drive runs out of space.
See: https://github.com/resin-io/etcher/issues/571
Signed-off-by: Juan Cruz Viotti <jviottidc@gmail.com>
This commit is contained in:
Juan Cruz Viotti 2016-07-24 15:32:00 -04:00 committed by GitHub
parent 5f943e98be
commit b7d6d3d9a1
10 changed files with 92 additions and 14 deletions

View File

@ -65,7 +65,7 @@ form.run([
});
if (!selectedDrive) {
throw new Error(`Drive not found: ${selectedDrive}`);
throw new Error(`Drive not found: ${answers.drive}`);
}
return writer.writeImage(options._[0], selectedDrive, {
@ -120,7 +120,8 @@ form.run([
log.toStderr(JSON.stringify({
command: 'error',
data: {
message: error.message
message: error.message,
code: error.code
}
}));
} else {

View File

@ -58,9 +58,8 @@ exports.writeImage = (imagePath, drive, options, onProgress) => {
return umount.umountAsync(drive.device).then(() => {
return imageStream.getFromFilePath(imagePath);
}).then((image) => {
return imageWrite.write(drive.device, image.stream, {
return imageWrite.write(drive, image, {
check: options.validateWriteOnSuccess,
size: image.size,
transform: image.transform
});
}).then((writer) => {

View File

@ -156,6 +156,14 @@ app.controller('AppController', function(
this.tooltipModal = TooltipModalService;
this.handleError = (error) => {
// This particular error is handled by the alert ribbon
// on the main application page.
if (error.code === 'ENOSPC') {
AnalyticsService.logEvent('Drive ran out of space');
return;
}
OSDialogService.showError(error);
// Also throw it so it gets displayed in DevTools

View File

@ -206,6 +206,10 @@ const storeReducer = (state, action) => {
throw new Error(`Invalid results sourceChecksum: ${action.data.sourceChecksum}`);
}
if (action.data.errorCode && !_.isString(action.data.errorCode)) {
throw new Error(`Invalid results errorCode: ${action.data.errorCode}`);
}
return state
.set('isFlashing', false)
.set('flashResults', Immutable.fromJS(action.data))

View File

@ -252,7 +252,8 @@ imageWriter.service('ImageWriterService', function($q, $timeout, SettingsModel)
}).catch((error) => {
this.unsetFlashingFlag({
cancelled: false,
passedValidation: false
passedValidation: false,
errorCode: error.code
});
return $q.reject(error);

View File

@ -98,10 +98,13 @@
<div class="alert-ribbon alert-warning" ng-class="{ 'alert-ribbon--open': !app.wasLastFlashSuccessful() }">
<span class="glyphicon glyphicon-warning-sign"></span>
<span ng-show="app.settings.get('validateWriteOnSuccess')">
<span ng-show="app.writer.getFlashResults().errorCode === 'ENOSPC'">
Not enough space on the drive.<br>Please insert larger one and <button class="btn btn-link" ng-click="app.restartAfterFailure()">try again</button>
</span>
<span ng-show="app.writer.getFlashResults().errorCode !== 'ENOSPC' && app.settings.get('validateWriteOnSuccess')">
Your removable drive did not pass validation check.<br>Please insert another one and <button class="btn btn-link" ng-click="app.restartAfterFailure()">try again</button>
</span>
<span ng-hide="app.settings.get('validateWriteOnSuccess')">
<span ng-show="app.writer.getFlashResults().errorCode !== 'ENOSPC' && !app.settings.get('validateWriteOnSuccess')">
Oops, seems something went wrong. Click <button class="btn btn-link" ng-click="app.restartAfterFailure()">here</button> to retry
</span>
</div>

View File

@ -86,6 +86,16 @@ exports.write = (image, drive, options) => {
});
child.on('message', (message) => {
// The error object is decomposed by the CLI for serialisation
// purposes. We compose it back to an `Error` here in order
// to provide better encapsulation.
if (message.command === 'error') {
const error = new Error(message.data.message);
error.code = message.data.code;
return emitter.emit('error', error);
}
emitter.emit(message.command, message.data);
});

42
npm-shrinkwrap.json generated
View File

@ -307,9 +307,9 @@
"resolved": "https://registry.npmjs.org/astw/-/astw-2.0.0.tgz"
},
"async": {
"version": "2.0.0-rc.6",
"version": "2.0.0",
"from": "async@>=2.0.0-rc.2 <3.0.0",
"resolved": "https://registry.npmjs.org/async/-/async-2.0.0-rc.6.tgz"
"resolved": "https://registry.npmjs.org/async/-/async-2.0.0.tgz"
},
"async-foreach": {
"version": "0.1.3",
@ -1278,9 +1278,31 @@
"resolved": "https://registry.npmjs.org/etcher-image-stream/-/etcher-image-stream-2.5.2.tgz"
},
"etcher-image-write": {
"version": "5.0.3",
"from": "etcher-image-write@>=5.0.3 <6.0.0",
"resolved": "https://registry.npmjs.org/etcher-image-write/-/etcher-image-write-5.0.3.tgz"
"version": "6.0.0",
"from": "etcher-image-write@6.0.0",
"resolved": "https://registry.npmjs.org/etcher-image-write/-/etcher-image-write-6.0.0.tgz",
"dependencies": {
"isarray": {
"version": "1.0.0",
"from": "isarray@~1.0.0",
"resolved": "http://registry.npmjs.org/isarray/-/isarray-1.0.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 <3.0.0",
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.1.tgz"
},
"xtend": {
"version": "4.0.1",
"from": "xtend@~4.0.0",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz"
}
}
},
"etcher-latest-version": {
"version": "1.0.0",
@ -1615,6 +1637,11 @@
"from": "glob2base@>=0.0.12 <0.0.13",
"resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz"
},
"globals": {
"version": "9.9.0",
"from": "globals@>=9.2.0 <10.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-9.9.0.tgz"
},
"globby": {
"version": "4.1.0",
"from": "globby@>=4.0.0 <5.0.0",
@ -4364,6 +4391,11 @@
"from": "shell-quote@>=1.4.3 <2.0.0",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz"
},
"shelljs": {
"version": "0.6.0",
"from": "shelljs@>=0.6.0 <0.7.0",
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.6.0.tgz"
},
"sigmund": {
"version": "1.0.1",
"from": "sigmund@>=1.0.0 <1.1.0",

View File

@ -65,7 +65,7 @@
"drivelist": "^3.2.2",
"electron-is-running-in-asar": "^1.0.0",
"etcher-image-stream": "^2.5.2",
"etcher-image-write": "^5.0.3",
"etcher-image-write": "^6.0.0",
"etcher-latest-version": "^1.0.0",
"file-tail": "^0.3.0",
"flexboxgrid": "^6.3.0",

View File

@ -241,6 +241,17 @@ describe('Browser: ImageWriter', function() {
}).to.throw('Missing results');
});
it('should throw if errorCode is defined but it is not a number', function() {
m.chai.expect(function() {
ImageWriterService.unsetFlashingFlag({
passedValidation: true,
cancelled: false,
sourceChecksum: '1234',
errorCode: 123
});
}).to.throw('Invalid results errorCode: 123');
});
it('should throw if no passedValidation', function() {
m.chai.expect(function() {
ImageWriterService.unsetFlashingFlag({
@ -445,7 +456,9 @@ describe('Browser: ImageWriter', function() {
beforeEach(function() {
this.performWriteStub = m.sinon.stub(ImageWriterService, 'performWrite');
this.performWriteStub.returns($q.reject(new Error('write error')));
this.error = new Error('write error');
this.error.code = 'FOO';
this.performWriteStub.returns($q.reject(this.error));
});
afterEach(function() {
@ -458,6 +471,13 @@ describe('Browser: ImageWriter', function() {
m.chai.expect(ImageWriterService.isFlashing()).to.be.false;
});
it('should set the error code in the flash results', function() {
ImageWriterService.flash('foo.img', '/dev/disk2');
$rootScope.$apply();
const flashResults = ImageWriterService.getFlashResults();
m.chai.expect(flashResults.errorCode).to.equal('FOO');
});
it('should be rejected with the error', function() {
ImageWriterService.unsetFlashingFlag({
passedValidation: true,