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 <jv@jviotti.com>
This commit is contained in:
Juan Cruz Viotti 2017-10-25 14:03:30 -04:00 committed by GitHub
parent 433b2734bb
commit daa847d29b

View File

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