mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-20 09:46:31 +00:00
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:
parent
5f943e98be
commit
b7d6d3d9a1
@ -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 {
|
||||
|
@ -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) => {
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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
42
npm-shrinkwrap.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user