mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-23 19:26:33 +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 debug = require('debug')('sdk:usbboot')
|
||||||
const usb = require('./usb')
|
const usb = require('./usb')
|
||||||
const protocol = require('./protocol')
|
const protocol = require('./protocol')
|
||||||
|
const utils = require('../../utils')
|
||||||
|
|
||||||
debug.enabled = true
|
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
|
* @summary Convert a USB id (e.g. product/vendor) to a string
|
||||||
* @function
|
* @function
|
||||||
@ -171,6 +187,9 @@ class USBBootAdapter extends EventEmitter {
|
|||||||
|
|
||||||
/** @type {Object} Blob cache */
|
/** @type {Object} Blob cache */
|
||||||
this.blobCache = {}
|
this.blobCache = {}
|
||||||
|
|
||||||
|
/** @type {Object} Progress hash */
|
||||||
|
this.progress = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -228,23 +247,97 @@ class USBBootAdapter extends EventEmitter {
|
|||||||
/* eslint-enable lodash/prefer-lodash-method */
|
/* eslint-enable lodash/prefer-lodash-method */
|
||||||
|
|
||||||
// This is the only way we can unique identify devices
|
// 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'
|
const result = {
|
||||||
device.description = 'Compute Module'
|
device: device.raw,
|
||||||
device.raw = device.device
|
raw: device.raw,
|
||||||
device.size = null
|
displayName: 'Initializing device',
|
||||||
device.mountpoints = []
|
description: 'Compute Module',
|
||||||
device.protected = false
|
size: null,
|
||||||
device.system = false
|
mountpoints: [],
|
||||||
device.disabled = true
|
protected: false,
|
||||||
device.icon = 'loading'
|
system: false,
|
||||||
device.vendor = usbIdToString(device.deviceDescriptor.idVendor)
|
disabled: true,
|
||||||
device.product = usbIdToString(device.deviceDescriptor.idProduct)
|
icon: 'loading',
|
||||||
device.adaptor = exports.name
|
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
|
// 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()
|
device.open()
|
||||||
|
|
||||||
// Ensures we don't wait forever if an issue occurs
|
// Ensures we don't wait forever if an issue occurs
|
||||||
@ -279,33 +372,45 @@ class USBBootAdapter extends EventEmitter {
|
|||||||
|
|
||||||
const endpoint = deviceInterface.endpoint(addresses.endpoint)
|
const endpoint = deviceInterface.endpoint(addresses.endpoint)
|
||||||
|
|
||||||
return Bluebird.try(() => {
|
if (serialNumberIndex === USB_DESCRIPTOR_NULL_INDEX) {
|
||||||
const serialNumberIndex = device.deviceDescriptor.iSerialNumber
|
return this.queryBlobFromCache(USBBOOT_BOOTCODE_FILE_NAME, options.readFile).then((bootcode) => {
|
||||||
debug(`Serial number index: ${serialNumberIndex}`)
|
return USBBootAdapter.writeBootCode(device, endpoint, bootcode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (serialNumberIndex === USB_DESCRIPTOR_NULL_INDEX) {
|
debug('Starting file server')
|
||||||
return USBBootAdapter.writeBootCode(device, endpoint, _.get(options.files, [
|
|
||||||
USBBOOT_BOOTCODE_FILE_NAME
|
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)
|
||||||
}
|
}
|
||||||
|
}).tap(() => {
|
||||||
debug('Starting file server')
|
setProgress(utils.PERCENTAGE_MAXIMUM)
|
||||||
return this.startFileServer(device, endpoint, options.files)
|
|
||||||
}).return(device).finally(() => {
|
|
||||||
device.close()
|
|
||||||
})
|
})
|
||||||
|
}).return(device).catch({
|
||||||
// See http://bluebirdjs.com/docs/api/promise.map.html
|
message: 'LIBUSB_TRANSFER_CANCELLED'
|
||||||
}, {
|
}, {
|
||||||
concurrency: 5
|
message: 'LIBUSB_ERROR_NO_DEVICE'
|
||||||
}).catch((error) => {
|
}, _.constant(null)).tap((result) => {
|
||||||
callback(error)
|
if (result) {
|
||||||
}).then((devices) => {
|
result.close()
|
||||||
this.emit('devices', devices)
|
}
|
||||||
callback(null, devices)
|
}).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} device - node-usb device
|
||||||
* @param {Object} endpoint - node-usb endpoint
|
* @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}
|
* @returns {Promise}
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
@ -374,14 +482,20 @@ class USBBootAdapter extends EventEmitter {
|
|||||||
* const usb = require('usb')
|
* const usb = require('usb')
|
||||||
* const device = usb.findByIds(0x0a5c, 0x2763)
|
* const device = usb.findByIds(0x0a5c, 0x2763)
|
||||||
*
|
*
|
||||||
* adapter.startFileServer(device, device.interfaces(0).endpoint(1), (name) => {
|
* adapter.startFileServer(device, device.interfaces(0).endpoint(1), {
|
||||||
* return fs.readFileAsync(name)
|
* readFile: (name) => {
|
||||||
|
* return fs.readFileAsync(name)
|
||||||
|
* },
|
||||||
|
* progress: (step) => {
|
||||||
|
* console.log(`Currently on step ${step}`)
|
||||||
|
* }
|
||||||
* }).then(() => {
|
* }).then(() => {
|
||||||
* console.log('Done!')
|
* console.log('Done!')
|
||||||
* })
|
* })
|
||||||
*/
|
*/
|
||||||
startFileServer (device, endpoint, readFile) {
|
startFileServer (device, endpoint, options, step = DEFAULT_FILE_SERVER_STEP) {
|
||||||
debug('Listening for file messages')
|
debug(`Listening for file messages (step ${step})`)
|
||||||
|
options.progress(step)
|
||||||
return protocol
|
return protocol
|
||||||
.read(device, protocol.FILE_MESSAGE_SIZE)
|
.read(device, protocol.FILE_MESSAGE_SIZE)
|
||||||
.then(protocol.parseFileMessageBuffer)
|
.then(protocol.parseFileMessageBuffer)
|
||||||
@ -411,7 +525,7 @@ class USBBootAdapter extends EventEmitter {
|
|||||||
if (fileMessage.command === protocol.FILE_MESSAGE_COMMANDS.GET_FILE_SIZE) {
|
if (fileMessage.command === protocol.FILE_MESSAGE_COMMANDS.GET_FILE_SIZE) {
|
||||||
debug(`Getting the size of ${fileMessage.fileName}`)
|
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
|
const fileSize = fileBuffer.length
|
||||||
debug(`Sending size: ${fileSize}`)
|
debug(`Sending size: ${fileSize}`)
|
||||||
return protocol.sendBufferSize(device, fileSize)
|
return protocol.sendBufferSize(device, fileSize)
|
||||||
@ -427,7 +541,7 @@ class USBBootAdapter extends EventEmitter {
|
|||||||
if (fileMessage.command === protocol.FILE_MESSAGE_COMMANDS.READ_FILE) {
|
if (fileMessage.command === protocol.FILE_MESSAGE_COMMANDS.READ_FILE) {
|
||||||
debug(`Reading ${fileMessage.fileName}`)
|
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)
|
return protocol.write(device, endpoint, fileBuffer)
|
||||||
}).catch({
|
}).catch({
|
||||||
code: 'ENOENT'
|
code: 'ENOENT'
|
||||||
@ -441,7 +555,8 @@ class USBBootAdapter extends EventEmitter {
|
|||||||
return Bluebird.reject(new Error(`Unrecognized command: ${fileMessage.command}`))
|
return Bluebird.reject(new Error(`Unrecognized command: ${fileMessage.command}`))
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
debug('Starting again')
|
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