mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 15:27:17 +00:00
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:
parent
433b2734bb
commit
daa847d29b
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user