From daa847d29b642895ef0b57009c215b6383769627 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Wed, 25 Oct 2017 14:03:30 -0400 Subject: [PATCH] feat(usbboot): add progress property to usbboot scanned drives (#1803) This commit re-architects the usbboot adapter to prepare the drives in the background, while emitting scan results every 2s, where each drive has a `progress` percentage property. Change-Type: minor Signed-off-by: Juan Cruz Viotti --- lib/shared/sdk/usbboot/index.js | 203 +++++++++++++++++++++++++------- 1 file changed, 159 insertions(+), 44 deletions(-) diff --git a/lib/shared/sdk/usbboot/index.js b/lib/shared/sdk/usbboot/index.js index 3751eb5e..dfb32efe 100644 --- a/lib/shared/sdk/usbboot/index.js +++ b/lib/shared/sdk/usbboot/index.js @@ -27,6 +27,7 @@ const Bluebird = require('bluebird') const debug = require('debug')('sdk:usbboot') const usb = require('./usb') const protocol = require('./protocol') +const utils = require('../../utils') debug.enabled = true @@ -116,6 +117,21 @@ const USBBOOT_CAPABLE_USB_DEVICES = [ ] +/** + * @summary Estimated device reboot delay + * @type {Number} + * @constant + */ +const DEVICE_REBOOT_DELAY = 5000 + +/** + * @summary The initial step of the file server usbboot phase + * @constant + * @type {Number} + * @private + */ +const DEFAULT_FILE_SERVER_STEP = 1 + /** * @summary Convert a USB id (e.g. product/vendor) to a string * @function @@ -171,6 +187,9 @@ class USBBootAdapter extends EventEmitter { /** @type {Object} Blob cache */ this.blobCache = {} + + /** @type {Object} Progress hash */ + this.progress = {} } /** @@ -228,23 +247,97 @@ class USBBootAdapter extends EventEmitter { /* eslint-enable lodash/prefer-lodash-method */ // This is the only way we can unique identify devices - device.device = `${device.busNumber}:${device.deviceAddress}` + device.raw = `${device.busNumber}:${device.deviceAddress}` - device.displayName = 'Initializing device' - device.description = 'Compute Module' - device.raw = device.device - device.size = null - device.mountpoints = [] - device.protected = false - device.system = false - device.disabled = true - device.icon = 'loading' - device.vendor = usbIdToString(device.deviceDescriptor.idVendor) - device.product = usbIdToString(device.deviceDescriptor.idProduct) - device.adaptor = exports.name + const result = { + device: device.raw, + raw: device.raw, + displayName: 'Initializing device', + description: 'Compute Module', + size: null, + mountpoints: [], + protected: false, + system: false, + disabled: true, + icon: 'loading', + vendor: usbIdToString(device.deviceDescriptor.idVendor), + product: usbIdToString(device.deviceDescriptor.idProduct), + adaptor: exports.name + } + if (_.isNil(this.progress[result.raw])) { + // TODO: Emit an error event if this fails + this.prepare(device, { + readFile: options.readFile + }) + } + + result.progress = this.progress[result.raw] + return result + + // See http://bluebirdjs.com/docs/api/promise.map.html + }, { + concurrency: 5 + }).catch((error) => { + callback(error) + }).then((devices) => { + this.emit('devices', devices) + callback(null, devices) + }) + + return this + } + + /** + * @summary Prepare a usbboot device + * @function + * @private + * + * @param {Object} device - node-usb device + * @param {Object} options - options + * @param {Function} options.readFile - read file function + * @returns {Promise} + * + * @example + * const fs = Bluebird.promisifyAll(require('fs')) + * const usb = require('usb') + * const device = usb.findByIds(0x0a5c, 0x2763) + * + * adapter.prepare(device, (name) => { + * return fs.readFileAsync(name) + * }).then(() => { + * console.log('Done!') + * }) + */ + prepare (device, options) { + /** + * @summary Set device progress + * @function + * @private + * + * @param {Number} percentage - percentage + * + * @example + * setProgress(90) + */ + const setProgress = (percentage) => { + debug(`%c[${device.raw}] -> ${Math.floor(percentage)}%%`, 'color:red;') + this.progress[device.raw] = percentage + } + + const serialNumberIndex = device.deviceDescriptor.iSerialNumber + debug(`Serial number index: ${serialNumberIndex}`) + if (serialNumberIndex === USB_DESCRIPTOR_NULL_INDEX) { + // eslint-disable-next-line no-magic-numbers + setProgress(10) + } else { + // eslint-disable-next-line no-magic-numbers + setProgress(15) + } + + return Bluebird.try(() => { // We need to open the device in order to access _configDescriptor - debug(`Opening device: ${device.device} (${device.vendor}:${device.product})`) + debug(`Opening device: ${device.raw}`) device.open() // Ensures we don't wait forever if an issue occurs @@ -279,33 +372,45 @@ class USBBootAdapter extends EventEmitter { const endpoint = deviceInterface.endpoint(addresses.endpoint) - return Bluebird.try(() => { - const serialNumberIndex = device.deviceDescriptor.iSerialNumber - debug(`Serial number index: ${serialNumberIndex}`) + if (serialNumberIndex === USB_DESCRIPTOR_NULL_INDEX) { + return this.queryBlobFromCache(USBBOOT_BOOTCODE_FILE_NAME, options.readFile).then((bootcode) => { + return USBBootAdapter.writeBootCode(device, endpoint, bootcode) + }) + } - if (serialNumberIndex === USB_DESCRIPTOR_NULL_INDEX) { - return USBBootAdapter.writeBootCode(device, endpoint, _.get(options.files, [ - USBBOOT_BOOTCODE_FILE_NAME - ])) + debug('Starting file server') + + const PERCENTAGE_START = 20 + const PERCENTAGE_TOTAL = 95 + + // TODO: Find a way to not hardcode these values, and instead + // figure out the correct number for each board on the fly. + // This might be possible once we implement proper device + // auto-discovery. For now, we assume the worst case scenario. + // eslint-disable-next-line no-magic-numbers + const STEPS_TOTAL = 38 + + return this.startFileServer(device, endpoint, { + readFile: options.readFile, + progress: (step) => { + setProgress((step * (PERCENTAGE_TOTAL - PERCENTAGE_START) / STEPS_TOTAL) + PERCENTAGE_START) } - - debug('Starting file server') - return this.startFileServer(device, endpoint, options.files) - }).return(device).finally(() => { - device.close() + }).tap(() => { + setProgress(utils.PERCENTAGE_MAXIMUM) }) - - // See http://bluebirdjs.com/docs/api/promise.map.html + }).return(device).catch({ + message: 'LIBUSB_TRANSFER_CANCELLED' }, { - concurrency: 5 - }).catch((error) => { - callback(error) - }).then((devices) => { - this.emit('devices', devices) - callback(null, devices) + message: 'LIBUSB_ERROR_NO_DEVICE' + }, _.constant(null)).tap((result) => { + if (result) { + result.close() + } + }).finally(() => { + return Bluebird.delay(DEVICE_REBOOT_DELAY).then(() => { + Reflect.deleteProperty(this.progress, device.raw) + }) }) - - return this } /** @@ -366,7 +471,10 @@ class USBBootAdapter extends EventEmitter { * * @param {Object} device - node-usb device * @param {Object} endpoint - node-usb endpoint - * @param {Function} readFile - read file function + * @param {Object} options - options + * @param {Function} options.readFile - read file function + * @param {Function} options.progress - progress function (step) + * @param {Number} [step] - current step (used internally) * @returns {Promise} * * @example @@ -374,14 +482,20 @@ class USBBootAdapter extends EventEmitter { * const usb = require('usb') * const device = usb.findByIds(0x0a5c, 0x2763) * - * adapter.startFileServer(device, device.interfaces(0).endpoint(1), (name) => { - * return fs.readFileAsync(name) + * adapter.startFileServer(device, device.interfaces(0).endpoint(1), { + * readFile: (name) => { + * return fs.readFileAsync(name) + * }, + * progress: (step) => { + * console.log(`Currently on step ${step}`) + * } * }).then(() => { * console.log('Done!') * }) */ - startFileServer (device, endpoint, readFile) { - debug('Listening for file messages') + startFileServer (device, endpoint, options, step = DEFAULT_FILE_SERVER_STEP) { + debug(`Listening for file messages (step ${step})`) + options.progress(step) return protocol .read(device, protocol.FILE_MESSAGE_SIZE) .then(protocol.parseFileMessageBuffer) @@ -411,7 +525,7 @@ class USBBootAdapter extends EventEmitter { if (fileMessage.command === protocol.FILE_MESSAGE_COMMANDS.GET_FILE_SIZE) { debug(`Getting the size of ${fileMessage.fileName}`) - return this.queryBlobFromCache(fileMessage.fileName, readFile).then((fileBuffer) => { + return this.queryBlobFromCache(fileMessage.fileName, options.readFile).then((fileBuffer) => { const fileSize = fileBuffer.length debug(`Sending size: ${fileSize}`) return protocol.sendBufferSize(device, fileSize) @@ -427,7 +541,7 @@ class USBBootAdapter extends EventEmitter { if (fileMessage.command === protocol.FILE_MESSAGE_COMMANDS.READ_FILE) { debug(`Reading ${fileMessage.fileName}`) - return this.queryBlobFromCache(fileMessage.fileName, readFile).then((fileBuffer) => { + return this.queryBlobFromCache(fileMessage.fileName, options.readFile).then((fileBuffer) => { return protocol.write(device, endpoint, fileBuffer) }).catch({ code: 'ENOENT' @@ -441,7 +555,8 @@ class USBBootAdapter extends EventEmitter { return Bluebird.reject(new Error(`Unrecognized command: ${fileMessage.command}`)) }).then(() => { debug('Starting again') - return this.startFileServer(device, endpoint, readFile) + const STEP_INCREMENT = 1 + return this.startFileServer(device, endpoint, options, step + STEP_INCREMENT) }) }) }