refactor(GUI): remove angular dependency from drive scanner (#1512)

Remove the Angular dependency from DriveScanner and with it the service,
exposing it through the module directly.
This commit is contained in:
Benedict Aas 2017-06-15 16:26:03 +01:00 committed by Juan Cruz Viotti
parent 9c1f9a54b1
commit 0f600c3cc2
5 changed files with 330 additions and 359 deletions

View File

@ -41,6 +41,7 @@ const analytics = require('./modules/analytics');
const updateNotifier = require('./components/update-notifier'); const updateNotifier = require('./components/update-notifier');
const availableDrives = require('./models/available-drives'); const availableDrives = require('./models/available-drives');
const selectionState = require('./models/selection-state'); const selectionState = require('./models/selection-state');
const driveScanner = require('./modules/drive-scanner');
const Store = require('./models/store'); const Store = require('./models/store');
@ -51,7 +52,6 @@ const app = angular.module('Etcher', [
// Etcher modules // Etcher modules
require('./modules/error'), require('./modules/error'),
require('./modules/drive-scanner'),
// Components // Components
require('./components/svg-icon'), require('./components/svg-icon'),
@ -176,8 +176,8 @@ app.run(() => {
}); });
}); });
app.run(($timeout, DriveScannerService, ErrorService) => { app.run(($timeout, ErrorService) => {
DriveScannerService.on('drives', (drives) => { driveScanner.on('drives', (drives) => {
// Safely trigger a digest cycle. // Safely trigger a digest cycle.
// In some cases, AngularJS doesn't acknowledge that the // In some cases, AngularJS doesn't acknowledge that the
@ -188,18 +188,18 @@ app.run(($timeout, DriveScannerService, ErrorService) => {
}); });
}); });
DriveScannerService.on('error', (error) => { driveScanner.on('error', (error) => {
// Stop the drive scanning loop in case of errors, // Stop the drive scanning loop in case of errors,
// otherwise we risk presenting the same error over // otherwise we risk presenting the same error over
// and over again to the user, while also heavily // and over again to the user, while also heavily
// spamming our error reporting service. // spamming our error reporting service.
DriveScannerService.stop(); driveScanner.stop();
return ErrorService.reportException(error); return ErrorService.reportException(error);
}); });
DriveScannerService.start(); driveScanner.start();
}); });
app.run(($window, WarningModalService, ErrorService, OSDialogService) => { app.run(($window, WarningModalService, ErrorService, OSDialogService) => {

View File

@ -16,112 +16,100 @@
'use strict'; 'use strict';
/**
* @module Etcher.Modules.DriveScanner
*/
const Rx = require('rx'); const Rx = require('rx');
const os = require('os'); const os = require('os');
const _ = require('lodash'); const _ = require('lodash');
const angular = require('angular');
const EventEmitter = require('events').EventEmitter; const EventEmitter = require('events').EventEmitter;
const drivelist = require('drivelist'); const drivelist = require('drivelist');
const settings = require('../models/settings'); const settings = require('../models/settings');
const MODULE_NAME = 'Etcher.Modules.DriveScanner'; const DRIVE_SCANNER_INTERVAL_MS = 2000;
const driveScanner = angular.module(MODULE_NAME, []); const DRIVE_SCANNER_FIRST_SCAN_DELAY_MS = 0;
const emitter = new EventEmitter();
driveScanner.factory('DriveScannerService', () => { /* eslint-disable lodash/prefer-lodash-method */
const DRIVE_SCANNER_INTERVAL_MS = 2000;
const DRIVE_SCANNER_FIRST_SCAN_DELAY_MS = 0;
const emitter = new EventEmitter();
/* eslint-disable lodash/prefer-lodash-method */ const availableDrives = Rx.Observable.timer(
DRIVE_SCANNER_FIRST_SCAN_DELAY_MS,
DRIVE_SCANNER_INTERVAL_MS
)
const availableDrives = Rx.Observable.timer( /* eslint-enable lodash/prefer-lodash-method */
DRIVE_SCANNER_FIRST_SCAN_DELAY_MS,
DRIVE_SCANNER_INTERVAL_MS
)
/* eslint-enable lodash/prefer-lodash-method */ .flatMap(() => {
return Rx.Observable.fromNodeCallback(drivelist.list)();
})
.flatMap(() => { // Build human friendly "description"
return Rx.Observable.fromNodeCallback(drivelist.list)(); .map((drives) => {
}) return _.map(drives, (drive) => {
drive.name = drive.device;
// Build human friendly "description" if (os.platform() === 'win32' && !_.isEmpty(drive.mountpoints)) {
.map((drives) => { drive.name = _.join(_.map(drive.mountpoints, 'path'), ', ');
return _.map(drives, (drive) => {
drive.name = drive.device;
if (os.platform() === 'win32' && !_.isEmpty(drive.mountpoints)) {
drive.name = _.join(_.map(drive.mountpoints, 'path'), ', ');
}
return drive;
});
})
.map((drives) => {
if (settings.get('unsafeMode')) {
return drives;
} }
return _.reject(drives, { return drive;
system: true });
}); })
})
.pausable(new Rx.Subject());
/* .map((drives) => {
* This service emits the following events: if (settings.get('unsafeMode')) {
* return drives;
* - `drives (Object[])` }
* - `error (Error)`
*
* For example:
*
* ```
* DriveScannerService.on('drives', (drives) => {
* console.log(drives);
* });
*
* DriveScannerService.on('error', (error) => {
* throw error;
* });
* ```
*/
availableDrives.subscribe((drives) => {
emitter.emit('drives', drives);
}, (error) => {
emitter.emit('error', error);
});
/** return _.reject(drives, {
* @summary Start scanning drives system: true
* @function });
* @public })
* .pausable(new Rx.Subject());
* @example
* DriveScannerService.start();
*/
emitter.start = () => {
availableDrives.resume();
};
/** /*
* @summary Stop scanning drives * This service emits the following events:
* @function *
* @public * - `drives (Object[])`
* * - `error (Error)`
* @example *
* DriveScannerService.stop(); * For example:
*/ *
emitter.stop = () => { * ```
availableDrives.pause(); * driveScanner.on('drives', (drives) => {
}; * console.log(drives);
* });
return emitter; *
* driveScanner.on('error', (error) => {
* throw error;
* });
* ```
*/
availableDrives.subscribe((drives) => {
emitter.emit('drives', drives);
}, (error) => {
emitter.emit('error', error);
}); });
module.exports = MODULE_NAME; /**
* @summary Start scanning drives
* @function
* @public
*
* @example
* driveScanner.start();
*/
emitter.start = () => {
availableDrives.resume();
};
/**
* @summary Stop scanning drives
* @function
* @public
*
* @example
* driveScanner.stop();
*/
emitter.stop = () => {
availableDrives.pause();
};
module.exports = emitter;

View File

@ -19,13 +19,13 @@
const messages = require('../../../../shared/messages'); const messages = require('../../../../shared/messages');
const settings = require('../../../models/settings'); const settings = require('../../../models/settings');
const flashState = require('../../../models/flash-state'); const flashState = require('../../../models/flash-state');
const driveScanner = require('../../../modules/drive-scanner');
const utils = require('../../../../shared/utils'); const utils = require('../../../../shared/utils');
const notification = require('../../../os/notification'); const notification = require('../../../os/notification');
const path = require('path'); const path = require('path');
module.exports = function( module.exports = function(
$state, $state,
DriveScannerService,
ImageWriterService, ImageWriterService,
FlashErrorModalService, FlashErrorModalService,
ErrorService ErrorService
@ -58,7 +58,7 @@ module.exports = function(
// Stop scanning drives when flashing // Stop scanning drives when flashing
// otherwise Windows throws EPERM // otherwise Windows throws EPERM
DriveScannerService.stop(); driveScanner.stop();
const iconPath = '../../assets/icon.png'; const iconPath = '../../assets/icon.png';
@ -101,7 +101,7 @@ module.exports = function(
}) })
.finally(() => { .finally(() => {
DriveScannerService.start(); driveScanner.start();
}); });
}; };

View File

@ -43,7 +43,6 @@ const MainPage = angular.module(MODULE_NAME, [
require('../../os/open-external/open-external'), require('../../os/open-external/open-external'),
require('../../os/dropzone/dropzone'), require('../../os/dropzone/dropzone'),
require('../../modules/drive-scanner'),
require('../../modules/image-writer'), require('../../modules/image-writer'),
require('../../modules/error'), require('../../modules/error'),

View File

@ -2,52 +2,83 @@
const m = require('mochainon'); const m = require('mochainon');
const os = require('os'); const os = require('os');
const angular = require('angular');
const drivelist = require('drivelist'); const drivelist = require('drivelist');
require('angular-mocks'); const driveScanner = require('../../../lib/gui/modules/drive-scanner');
describe('Browser: DriveScanner', function() { describe('Browser: driveScanner', function() {
beforeEach(angular.mock.module( describe('given no available drives', function() {
require('../../../lib/gui/modules/drive-scanner')
));
describe('DriveScannerService', function() {
let DriveScannerService;
beforeEach(angular.mock.inject(function(_DriveScannerService_) {
DriveScannerService = _DriveScannerService_;
}));
describe('given no available drives', function() {
beforeEach(function() {
this.drivesListStub = m.sinon.stub(drivelist, 'list');
this.drivesListStub.yields(null, []);
});
afterEach(function() {
this.drivesListStub.restore();
});
it('should emit an empty array', function(done) {
DriveScannerService.on('drives', function(drives) {
m.chai.expect(drives).to.deep.equal([]);
DriveScannerService.stop();
done();
});
DriveScannerService.start();
});
beforeEach(function() {
this.drivelistStub = m.sinon.stub(drivelist, 'list');
this.drivelistStub.yields(null, []);
}); });
describe('given only system available drives', function() { afterEach(function() {
this.drivelistStub.restore();
});
it('should emit an empty array', function(done) {
driveScanner.once('drives', function(drives) {
m.chai.expect(drives).to.deep.equal([]);
driveScanner.stop();
done();
});
driveScanner.start();
});
});
describe('given only system available drives', function() {
beforeEach(function() {
this.drivelistStub = m.sinon.stub(drivelist, 'list');
this.drivelistStub.yields(null, [ {
device: '/dev/sda',
description: 'WDC WD10JPVX-75J',
size: '931.5G',
mountpoints: [
{
path: '/'
}
],
system: true
} ]);
});
afterEach(function() {
this.drivelistStub.restore();
});
it('should emit an empty array', function(done) {
driveScanner.once('drives', function(drives) {
m.chai.expect(drives).to.deep.equal([]);
driveScanner.stop();
done();
});
driveScanner.start();
});
});
describe('given linux', function() {
beforeEach(function() {
this.osPlatformStub = m.sinon.stub(os, 'platform');
this.osPlatformStub.returns('linux');
});
afterEach(function() {
this.osPlatformStub.restore();
});
describe('given available drives', function() {
beforeEach(function() { beforeEach(function() {
this.drivesListStub = m.sinon.stub(drivelist, 'list'); this.drivelistStub = m.sinon.stub(drivelist, 'list');
this.drivesListStub.yields(null, [ this.drivelistStub.yields(null, [
{ {
device: '/dev/sda', device: '/dev/sda',
description: 'WDC WD10JPVX-75J', description: 'WDC WD10JPVX-75J',
@ -58,55 +89,42 @@ describe('Browser: DriveScanner', function() {
} }
], ],
system: true system: true
},
{
device: '/dev/sdb',
description: 'Foo',
size: '14G',
mountpoints: [
{
path: '/mnt/foo'
}
],
system: false
},
{
device: '/dev/sdc',
description: 'Bar',
size: '14G',
mountpoints: [
{
path: '/mnt/bar'
}
],
system: false
} }
]); ]);
}); });
afterEach(function() { afterEach(function() {
this.drivesListStub.restore(); this.drivelistStub.restore();
}); });
it('should emit an empty array', function(done) { it('should emit the non removable drives', function(done) {
DriveScannerService.on('drives', function(drives) { driveScanner.once('drives', function(drives) {
m.chai.expect(drives).to.deep.equal([]); m.chai.expect(drives).to.deep.equal([
DriveScannerService.stop();
done();
});
DriveScannerService.start();
});
});
describe('given linux', function() {
beforeEach(function() {
this.osPlatformStub = m.sinon.stub(os, 'platform');
this.osPlatformStub.returns('linux');
});
afterEach(function() {
this.osPlatformStub.restore();
});
describe('given available drives', function() {
beforeEach(function() {
this.drivesListStub = m.sinon.stub(drivelist, 'list');
this.drivesListStub.yields(null, [
{
device: '/dev/sda',
description: 'WDC WD10JPVX-75J',
size: '931.5G',
mountpoints: [
{
path: '/'
}
],
system: true
},
{ {
device: '/dev/sdb', device: '/dev/sdb',
name: '/dev/sdb',
description: 'Foo', description: 'Foo',
size: '14G', size: '14G',
mountpoints: [ mountpoints: [
@ -118,6 +136,7 @@ describe('Browser: DriveScanner', function() {
}, },
{ {
device: '/dev/sdc', device: '/dev/sdc',
name: '/dev/sdc',
description: 'Bar', description: 'Bar',
size: '14G', size: '14G',
mountpoints: [ mountpoints: [
@ -128,81 +147,76 @@ describe('Browser: DriveScanner', function() {
system: false system: false
} }
]); ]);
driveScanner.stop();
done();
}); });
afterEach(function() { driveScanner.start();
this.drivesListStub.restore();
});
it('should emit the non removable drives', function(done) {
DriveScannerService.on('drives', function(drives) {
m.chai.expect(drives).to.deep.equal([
{
device: '/dev/sdb',
name: '/dev/sdb',
description: 'Foo',
size: '14G',
mountpoints: [
{
path: '/mnt/foo'
}
],
system: false
},
{
device: '/dev/sdc',
name: '/dev/sdc',
description: 'Bar',
size: '14G',
mountpoints: [
{
path: '/mnt/bar'
}
],
system: false
}
]);
DriveScannerService.stop();
done();
});
DriveScannerService.start();
});
}); });
}); });
describe('given windows', function() { });
describe('given windows', function() {
beforeEach(function() {
this.osPlatformStub = m.sinon.stub(os, 'platform');
this.osPlatformStub.returns('win32');
});
afterEach(function() {
this.osPlatformStub.restore();
});
describe('given available drives', function() {
beforeEach(function() { beforeEach(function() {
this.osPlatformStub = m.sinon.stub(os, 'platform'); this.drivelistStub = m.sinon.stub(drivelist, 'list');
this.osPlatformStub.returns('win32'); this.drivelistStub.yields(null, [
{
device: '\\\\.\\PHYSICALDRIVE1',
description: 'WDC WD10JPVX-75J',
size: '931.5G',
mountpoints: [
{
path: 'C:'
}
],
system: true
},
{
device: '\\\\.\\PHYSICALDRIVE2',
description: 'Foo',
size: '14G',
mountpoints: [],
system: false
},
{
device: '\\\\.\\PHYSICALDRIVE3',
description: 'Bar',
size: '14G',
mountpoints: [
{
path: 'F:'
}
],
system: false
}
]);
}); });
afterEach(function() { afterEach(function() {
this.osPlatformStub.restore(); this.drivelistStub.restore();
}); });
describe('given available drives', function() { it('should emit the non removable drives', function(done) {
driveScanner.once('drives', function(drives) {
beforeEach(function() { m.chai.expect(drives).to.deep.equal([
this.drivesListStub = m.sinon.stub(drivelist, 'list');
this.drivesListStub.yields(null, [
{
device: '\\\\.\\PHYSICALDRIVE1',
description: 'WDC WD10JPVX-75J',
size: '931.5G',
mountpoints: [
{
path: 'C:'
}
],
system: true
},
{ {
device: '\\\\.\\PHYSICALDRIVE2', device: '\\\\.\\PHYSICALDRIVE2',
name: '\\\\.\\PHYSICALDRIVE2',
description: 'Foo', description: 'Foo',
size: '14G', size: '14G',
mountpoints: [], mountpoints: [],
@ -210,6 +224,7 @@ describe('Browser: DriveScanner', function() {
}, },
{ {
device: '\\\\.\\PHYSICALDRIVE3', device: '\\\\.\\PHYSICALDRIVE3',
name: 'F:',
description: 'Bar', description: 'Bar',
size: '14G', size: '14G',
mountpoints: [ mountpoints: [
@ -220,149 +235,118 @@ describe('Browser: DriveScanner', function() {
system: false system: false
} }
]); ]);
driveScanner.stop();
done();
}); });
afterEach(function() { driveScanner.start();
this.drivesListStub.restore();
});
it('should emit the non removable drives', function(done) {
DriveScannerService.on('drives', function(drives) {
m.chai.expect(drives).to.deep.equal([
{
device: '\\\\.\\PHYSICALDRIVE2',
name: '\\\\.\\PHYSICALDRIVE2',
description: 'Foo',
size: '14G',
mountpoints: [],
system: false
},
{
device: '\\\\.\\PHYSICALDRIVE3',
name: 'F:',
description: 'Bar',
size: '14G',
mountpoints: [
{
path: 'F:'
}
],
system: false
}
]);
DriveScannerService.stop();
done();
});
DriveScannerService.start();
});
});
describe('given a drive with a single drive letters', function() {
beforeEach(function() {
this.drivesListStub = m.sinon.stub(drivelist, 'list');
this.drivesListStub.yields(null, [
{
device: '\\\\.\\PHYSICALDRIVE3',
description: 'Bar',
size: '14G',
mountpoints: [
{
path: 'F:'
}
],
system: false
}
]);
});
afterEach(function() {
this.drivesListStub.restore();
});
it('should use the drive letter as the name', function(done) {
DriveScannerService.on('drives', function(drives) {
m.chai.expect(drives).to.have.length(1);
m.chai.expect(drives[0].name).to.equal('F:');
DriveScannerService.stop();
done();
});
DriveScannerService.start();
});
});
describe('given a drive with multiple drive letters', function() {
beforeEach(function() {
this.drivesListStub = m.sinon.stub(drivelist, 'list');
this.drivesListStub.yields(null, [
{
device: '\\\\.\\PHYSICALDRIVE3',
description: 'Bar',
size: '14G',
mountpoints: [
{
path: 'F:'
},
{
path: 'G:'
},
{
path: 'H:'
}
],
system: false
}
]);
});
afterEach(function() {
this.drivesListStub.restore();
});
it('should join all the mountpoints in `name`', function(done) {
DriveScannerService.on('drives', function(drives) {
m.chai.expect(drives).to.have.length(1);
m.chai.expect(drives[0].name).to.equal('F:, G:, H:');
DriveScannerService.stop();
done();
});
DriveScannerService.start();
});
}); });
}); });
describe('given an error when listing the drives', function() { describe('given a drive with a single drive letters', function() {
beforeEach(function() {
this.drivelistStub = m.sinon.stub(drivelist, 'list');
this.drivelistStub.yields(null, [
{
device: '\\\\.\\PHYSICALDRIVE3',
description: 'Bar',
size: '14G',
mountpoints: [
{
path: 'F:'
}
],
system: false
}
]);
});
afterEach(function() {
this.drivelistStub.restore();
});
it('should use the drive letter as the name', function(done) {
driveScanner.once('drives', function(drives) {
m.chai.expect(drives).to.have.length(1);
m.chai.expect(drives[0].name).to.equal('F:');
driveScanner.stop();
done();
});
driveScanner.start();
});
});
describe('given a drive with multiple drive letters', function() {
beforeEach(function() { beforeEach(function() {
this.drivesListStub = m.sinon.stub(drivelist, 'list'); this.drivesListStub = m.sinon.stub(drivelist, 'list');
this.drivesListStub.yields(new Error('scan error')); this.drivesListStub.yields(null, [
{
device: '\\\\.\\PHYSICALDRIVE3',
description: 'Bar',
size: '14G',
mountpoints: [
{
path: 'F:'
},
{
path: 'G:'
},
{
path: 'H:'
}
],
system: false
}
]);
}); });
afterEach(function() { afterEach(function() {
this.drivesListStub.restore(); this.drivesListStub.restore();
}); });
it('should emit the error', function(done) { it('should join all the mountpoints in `name`', function(done) {
DriveScannerService.on('error', function(error) { driveScanner.once('drives', function(drives) {
m.chai.expect(error).to.be.an.instanceof(Error); m.chai.expect(drives).to.have.length(1);
m.chai.expect(error.message).to.equal('scan error'); m.chai.expect(drives[0].name).to.equal('F:, G:, H:');
DriveScannerService.stop(); driveScanner.stop();
done(); done();
}); });
DriveScannerService.start(); driveScanner.start();
}); });
}); });
}); });
describe('given an error when listing the drives', function() {
beforeEach(function() {
this.drivesListStub = m.sinon.stub(drivelist, 'list');
this.drivesListStub.yields(new Error('scan error'));
});
afterEach(function() {
this.drivesListStub.restore();
});
it('should emit the error', function(done) {
driveScanner.on('error', function(error) {
m.chai.expect(error).to.be.an.instanceof(Error);
m.chai.expect(error.message).to.equal('scan error');
driveScanner.stop();
done();
});
driveScanner.start();
});
});
}); });