feat: implement usbboot adapter (#1686)

This commit installs `node-usb` v1.3.0 from GitHub, since that version
was never published to NPM, and is the only one that works with Visual
Studio 2015 (see https://github.com/tessel/node-usb/issues/109).

The usbboot communicates with a Raspberry Pi / Amber through USB and
eventually mounts it as a block device we can write to.

This feature bundles bootcode.bin and start.elf from the original
usbboot implementation.

The flow is the following:

- On each scan, the usbboot scanner will try to get a usbboot compatible
  USB device to the next "phase", until they are all transformed to
  block devices the user can flash to as usual

Change-Type: minor
Changelog-Entry: Integrate Raspberry Pi's usbboot technology.
Fixes: https://github.com/resin-io/etcher/issues/1541
See: https://github.com/raspberrypi/usbboot
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
This commit is contained in:
Juan Cruz Viotti 2017-10-06 14:19:35 +01:00 committed by GitHub
parent 3147a93ca6
commit f6a7b2add6
19 changed files with 1111 additions and 66 deletions

2
.gitattributes vendored
View File

@ -35,6 +35,8 @@ Makefile text
*.img binary diff=hex
*.iso binary diff=hex
*.png binary diff=hex
*.bin binary diff=hex
*.elf binary diff=hex
*.xz binary diff=hex
*.zip binary diff=hex
*.dmg binary diff=hex

View File

@ -58,7 +58,7 @@ before_install:
fi
install:
- travis_wait ./scripts/ci/install.sh -o $HOST_OS -r $TARGET_ARCH
- ./scripts/ci/install.sh -o $HOST_OS -r $TARGET_ARCH
script:
- ./scripts/ci/test.sh -o $HOST_OS -r $TARGET_ARCH

30
lib/blobs/usbboot/LICENSE Normal file
View File

@ -0,0 +1,30 @@
Copyright (c) 2006, Broadcom Corporation.
Copyright (c) 2015, Raspberry Pi (Trading) Ltd
All rights reserved.
Redistribution. Redistribution and use in binary form, without
modification, are permitted provided that the following conditions are
met:
* This software may only be used for the purposes of developing for,
running or using a Raspberry Pi device.
* Redistributions must reproduce the above copyright notice and the
following disclaimer in the documentation and/or other materials
provided with the distribution.
* Neither the name of Broadcom Corporation nor the names of its suppliers
may be used to endorse or promote products derived from this software
without specific prior written permission.
DISCLAIMER. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.

View File

@ -0,0 +1,5 @@
usbboot
=======
The files in this directory were taken from
https://github.com/raspberrypi/usbboot.

Binary file not shown.

BIN
lib/blobs/usbboot/start.elf Executable file

Binary file not shown.

View File

@ -18,6 +18,8 @@
const EventEmitter = require('events').EventEmitter
const Bluebird = require('bluebird')
const fs = require('fs')
const path = require('path')
const settings = require('../models/settings')
const sdk = require('../../shared/sdk')
@ -26,7 +28,7 @@ const sdk = require('../../shared/sdk')
* @type {Number}
* @constant
*/
const DRIVE_SCANNER_INTERVAL_MS = 1000
const DRIVE_SCANNER_INTERVAL_MS = 2000
/**
* @summary Scanner event emitter singleton instance
@ -54,6 +56,29 @@ const emitter = new EventEmitter()
* ```
*/
/**
* @summary The Etcher "blobs" directory path
* @type {String}
* @constant
*/
const BLOBS_DIRECTORY = path.join(__dirname, '..', '..', 'blobs')
/**
* @summary The usbboot "bootcode.bin" buffer
* @type {Buffer}
* @constant
*/
const USBBOOT_BOOTCODE_BIN_BUFFER = fs.readFileSync(
path.join(BLOBS_DIRECTORY, 'usbboot', 'bootcode.bin'))
/**
* @summary The usbboot "start.elf" buffer
* @type {Buffer}
* @constant
*/
const USBBOOT_START_ELF_BUFFER = fs.readFileSync(
path.join(BLOBS_DIRECTORY, 'usbboot', 'start.elf'))
/**
* @summary Flag to control scanning status
* @type {Boolean}
@ -83,6 +108,12 @@ const scan = () => {
return sdk.scan({
standard: {
includeSystemDrives: settings.get('unsafeMode')
},
usbboot: {
files: {
'bootcode.bin': USBBOOT_BOOTCODE_BIN_BUFFER,
'start.elf': USBBOOT_START_ELF_BUFFER
}
}
}).then((drives) => {
emitter.emit('drives', drives)

View File

@ -25,7 +25,8 @@ const _ = require('lodash')
* @constant
*/
const ADAPTORS = [
require('./standard')
require('./standard'),
require('./usbboot')
]
/**

View File

@ -0,0 +1,387 @@
/*
* Copyright 2017 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* This work is heavily based on https://github.com/raspberrypi/usbboot
* Copyright 2016 Raspberry Pi Foundation
*/
'use strict'
const _ = require('lodash')
const Bluebird = require('bluebird')
const debug = require('debug')('sdk:usbboot')
const usb = require('./usb')
const protocol = require('./protocol')
/**
* @summary The name of this adaptor
* @public
* @type {String}
* @constant
*/
exports.name = 'usbboot'
/**
* @summary Vendor ID of "Broadcom Corporation"
* @type {Number}
* @constant
*/
const USB_VENDOR_ID_BROADCOM_CORPORATION = 0x0a5c
/**
* @summary Product ID of BCM2708
* @type {Number}
* @constant
*/
const USB_PRODUCT_ID_BCM2708_BOOT = 0x2763
/**
* @summary Product ID of BCM2710
* @type {Number}
* @constant
*/
const USB_PRODUCT_ID_BCM2710_BOOT = 0x2764
/**
* @summary List of usbboot capable devices
* @type {Object[]}
* @constant
*/
const USBBOOT_CAPABLE_USB_DEVICES = [
// BCM2835
{
vendorID: USB_VENDOR_ID_BROADCOM_CORPORATION,
productID: USB_PRODUCT_ID_BCM2708_BOOT
},
// BCM2837
{
vendorID: USB_VENDOR_ID_BROADCOM_CORPORATION,
productID: USB_PRODUCT_ID_BCM2710_BOOT
}
]
/**
* @summary The timeout for USB device operations
* @type {Number}
* @constant
*/
const USB_OPERATION_TIMEOUT_MS = 1000
/**
* @summary The number of USB endpoint interfaces in devices with a BCM2835 SoC
* @type {Number}
* @constant
*/
const USB_ENDPOINT_INTERFACES_SOC_BCM2835 = 1
/**
* @summary The USB device descriptor index of an empty property
* @type {Number}
* @constant
*/
const USB_DESCRIPTOR_NULL_INDEX = 0
/**
* @summary usbboot bootcode file name
* @type {String}
* @constant
*/
const USBBOOT_BOOTCODE_FILE_NAME = 'bootcode.bin'
/**
* @summary Check if a USB device object is usbboot-capable
* @function
* @private
*
* @param {Object} device - device
* @returns {Boolean} whether the device is usbboot-capable
*
* @example
* if (isUsbBootCapableUSBDevice({ ... })) {
* console.log('We can use usbboot on this device')
* }
*/
const isUsbBootCapableUSBDevice = (device) => {
return _.some(USBBOOT_CAPABLE_USB_DEVICES, {
vendorID: device.deviceDescriptor.idVendor,
productID: device.deviceDescriptor.idProduct
})
}
/**
* @summary The radix used by USB ID numbers
* @type {Number}
* @constant
*/
const USB_ID_RADIX = 16
/**
* @summary The expected length of a USB ID number
* @type {Number}
* @constant
*/
const USB_ID_LENGTH = 4
/**
* @summary Convert a USB id (e.g. product/vendor) to a string
* @function
* @private
*
* @param {Number} id - USB id
* @returns {String} string id
*
* @example
* console.log(usbIdToString(2652))
* > '0x0a5c'
*/
const usbIdToString = (id) => {
return `0x${_.padStart(id.toString(USB_ID_RADIX), USB_ID_LENGTH, '0')}`
}
/**
* @summary Write bootcode to USB device (usbboot first stage)
* @function
* @private
*
* @description
* After this stage is run, the USB will be re-mounted as 0x0a5c:0x2764.
*
* @param {Object} device - node-usb device
* @param {Object} endpoint - node-usb endpoint
* @param {Buffer} bootCodeBuffer - bootcode buffer
* @returns {Promise}
*
* @example
* const usb = require('usb')
* const device = usb.findByIds(0x0a5c, 0x2763)
* const bootcode = fs.readFileSync('./bootcode.bin')
*
* writeBootCode(device, device.interfaces(0).endpoint(1), bootcode).then(() => {
* console.log('Done!')
* })
*/
const writeBootCode = (device, endpoint, bootCodeBuffer) => {
debug('Writing bootcode')
debug(`Bootcode buffer length: ${bootCodeBuffer.length}`)
const bootMessageBuffer = protocol.createBootMessageBuffer(bootCodeBuffer.length)
debug('Writing boot message buffer to out endpoint')
return protocol.write(device, endpoint, bootMessageBuffer).then(() => {
debug('Writing boot code buffer to out endpoint')
return protocol.write(device, endpoint, bootCodeBuffer)
}).then(() => {
debug('Reading return code from device')
return protocol.read(device, protocol.RETURN_CODE_LENGTH)
}).then((data) => {
const returnCode = data.readInt32LE()
debug(`Received return code: ${returnCode}`)
if (returnCode !== protocol.RETURN_CODE_SUCCESS) {
throw new Error(`Couldn't write the bootcode, got return code ${returnCode} from device`)
}
})
}
/**
* @summary Mount a USB device as a block device (usbboot second stage)
* @function
* @private
*
* @description
* The possible files you can pass here are:
*
* - autoboot.txt
* - config.txt
* - recovery.elf
* - start.elf
* - fixup.dat
*
* @param {Object} device - node-usb device
* @param {Object} endpoint - node-usb endpoint
* @param {Object} files - a set of file buffers
* @returns {Promise}
*
* @example
* const usb = require('usb')
* const device = usb.findByIds(0x0a5c, 0x2763)
*
* startFileServer(device, device.interfaces(0).endpoint(1), {
* 'start.elf': fs.readFileSync('./start.elf')
* }).then(() => {
* console.log('Done!')
* })
*/
const startFileServer = (device, endpoint, files) => {
debug('Listening for file messages')
return protocol
.read(device, protocol.FILE_MESSAGE_SIZE)
.then(protocol.parseFileMessageBuffer)
// We get these error messages when reading a command
// from the device when the communication has ended
.catch({
message: 'LIBUSB_TRANSFER_STALL'
}, {
message: 'LIBUSB_TRANSFER_ERROR'
}, (error) => {
debug(`Got ${error.message} when reading a command, assuming everything is done`)
return {
command: protocol.FILE_MESSAGE_COMMAND_DONE
}
})
.then((fileMessage) => {
debug(`Received message: ${fileMessage.command} -> ${fileMessage.fileName}`)
if (fileMessage.command === protocol.FILE_MESSAGE_COMMAND_DONE) {
debug('Done')
return Bluebird.resolve()
}
return Bluebird.try(() => {
if (fileMessage.command === protocol.FILE_MESSAGE_COMMAND_GET_FILE_SIZE) {
debug(`Getting the size of ${fileMessage.fileName}`)
const fileSize = _.get(files, [ fileMessage.fileName, 'length' ])
if (_.isNil(fileSize)) {
debug(`Couldn't find ${fileMessage.fileName}`)
debug('Sending error signal')
return protocol.sendErrorSignal(device)
}
debug(`Sending size: ${fileSize}`)
return protocol.sendBufferSize(device, fileSize)
}
if (fileMessage.command === protocol.FILE_MESSAGE_COMMAND_READ_FILE) {
debug(`Reading ${fileMessage.fileName}`)
const fileBuffer = _.get(files, [ fileMessage.fileName ])
if (_.isNil(fileBuffer)) {
debug(`Couldn't find ${fileMessage.fileName}`)
debug('Sending error signal')
return protocol.sendErrorSignal(device)
}
return protocol.write(device, endpoint, fileBuffer)
}
return Bluebird.reject(new Error(`Unrecognized command: ${fileMessage.command}`))
}).then(() => {
debug('Starting again')
return startFileServer(device, endpoint, files)
})
})
}
/**
* @summary Scan for usbboot capable USB devices
* @function
* @public
*
* @description
* You should at the very least pass a file named `bootcode.bin`.
*
* @param {Object} options - options
* @param {Object} options.files - files buffers
* @fulfil {Object[]} - USB devices
* @returns {Promise}
*
* @example
* usbboot.scan({
* files: {
* 'bootcode.bin': fs.readFileSync('./msd/bootcode.bin'),
* 'start.elf': fs.readFileSync('./msd/start.elf')
* }
* }).each((device) => {
* console.log(device)
* })
*/
exports.scan = (options) => {
/* eslint-disable lodash/prefer-lodash-method */
return usb.listDevices().filter(isUsbBootCapableUSBDevice).map((device) => {
/* eslint-enable lodash/prefer-lodash-method */
const idPair = _.join([
usbIdToString(device.deviceDescriptor.idVendor),
usbIdToString(device.deviceDescriptor.idProduct)
], ':')
device.device = idPair
device.displayName = idPair
device.raw = idPair
device.size = null
device.mountpoints = []
device.protected = false
device.system = false
device.pending = true
device.adaptor = exports.name
// We need to open the device in order to access _configDescriptor
debug(`Opening device: ${device.name}`)
device.open()
// Ensures we don't wait forever if an issue occurs
device.timeout = USB_OPERATION_TIMEOUT_MS
// Handle 2837 where it can start with two interfaces, the first
// is mass storage the second is the vendor interface for programming
const addresses = {}
/* eslint-disable no-underscore-dangle */
if (device._configDescriptor.bNumInterfaces === USB_ENDPOINT_INTERFACES_SOC_BCM2835) {
/* eslint-enable no-underscore-dangle */
addresses.interface = 0
addresses.endpoint = 1
} else {
addresses.interface = 1
addresses.endpoint = 3
}
const deviceInterface = device.interface(addresses.interface)
debug(`Claiming interface: ${addresses.interface}`)
deviceInterface.claim()
const endpoint = deviceInterface.endpoint(addresses.endpoint)
return usb.getDeviceName(device).then((deviceName) => {
device.description = `${deviceName.manufacturer} ${deviceName.product}`
}).then(() => {
const serialNumberIndex = device.deviceDescriptor.iSerialNumber
debug(`Serial number index: ${serialNumberIndex}`)
if (serialNumberIndex === USB_DESCRIPTOR_NULL_INDEX) {
return writeBootCode(device, endpoint, _.get(options.files, [
USBBOOT_BOOTCODE_FILE_NAME
]))
}
debug('Starting file server')
return startFileServer(device, endpoint, options.files)
}).return(device).finally(() => {
device.close()
})
// See http://bluebirdjs.com/docs/api/promise.map.html
}, {
concurrency: 5
})
}

View File

@ -0,0 +1,381 @@
/*
* Copyright 2017 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* This work is heavily based on https://github.com/raspberrypi/usbboot
* Copyright 2016 Raspberry Pi Foundation
*/
'use strict'
const _ = require('lodash')
const Bluebird = require('bluebird')
const usb = require('./usb')
// The equivalent of a NULL buffer, given that node-usb complains
// if the data argument is not an instance of Buffer
const NULL_BUFFER_SIZE = 0
const NULL_BUFFER = Buffer.alloc(NULL_BUFFER_SIZE)
/**
* @summary The size of the boot message bootcode length section
* @type {Number}
* @constant
*/
const BOOT_MESSAGE_BOOTCODE_LENGTH_SIZE = 4
/**
* @summary The offset of the boot message bootcode length section
* @type {Number}
* @constant
*/
const BOOT_MESSAGE_BOOTCODE_LENGTH_OFFSET = 0
/**
* @summary The size of the boot message signature section
* @type {Number}
* @constant
*/
const BOOT_MESSAGE_SIGNATURE_SIZE = 20
/**
* @summary The offset of the file message command section
* @type {Number}
* @constant
*/
const FILE_MESSAGE_COMMAND_OFFSET = 0
/**
* @summary The size of the file message command section
* @type {Number}
* @constant
*/
const FILE_MESSAGE_COMMAND_SIZE = 4
/**
* @summary The offset of the file message file name section
* @type {Number}
* @constant
*/
const FILE_MESSAGE_FILE_NAME_OFFSET = FILE_MESSAGE_COMMAND_SIZE
/**
* @summary The size of the file message file name section
* @type {Number}
* @constant
*/
const FILE_MESSAGE_FILE_NAME_SIZE = 256
/**
* @summary The GET_STATUS usb control transfer request code
* @type {Number}
* @constant
* @description
* See http://www.jungo.com/st/support/documentation/windriver/811/wdusb_man_mhtml/node55.html#usb_standard_dev_req_codes
*/
const USB_REQUEST_CODE_GET_STATUS = 0
/**
* @summary The maximum buffer length of a usbboot message
* @type {Number}
* @constant
*/
const USBBOOT_MESSAGE_MAX_BUFFER_LENGTH = 0xffff
/**
* @summary The delay to wait between each USB read/write operation
* @type {Number}
* @constant
* @description
* The USB bus seems to hang if we execute many operations at
* the same time.
*/
const USB_REQUEST_DELAY_MS = 1000
/**
* @summary The timeout for USB bulk transfers, in milliseconds
* @type {Number}
* @constant
*/
const USB_BULK_TRANSFER_TIMEOUT_MS = 1000
/**
* @summary The amount of bits to shift to the right on a control transfer index
* @type {Number}
* @constant
*/
const CONTROL_TRANSFER_INDEX_RIGHT_BIT_SHIFT = 16
/**
* @summary The size of the usbboot file message
* @type {Number}
* @constant
*/
exports.FILE_MESSAGE_SIZE = FILE_MESSAGE_COMMAND_SIZE + FILE_MESSAGE_FILE_NAME_SIZE
/**
* @summary File message command display names
* @namespace FILE_MESSAGE_COMMANDS
* @public
*/
exports.FILE_MESSAGE_COMMANDS = {
/**
* @property {String}
* @memberof FILE_MESSAGE_COMMANDS
*
* @description
* The "get file size" file message command name.
*/
GET_FILE_SIZE: 'GetFileSize',
/**
* @property {String}
* @memberof FILE_MESSAGE_COMMANDS
*
* @description
* The "read file" file message command name.
*/
READ_FILE: 'ReadFile',
/**
* @property {String}
* @memberof FILE_MESSAGE_COMMANDS
*
* @description
* The "done" file message command name.
*/
DONE: 'Done'
}
/**
* @summary The usbboot return code that represents success
* @type {Number}
* @constant
*/
exports.RETURN_CODE_SUCCESS = 0
/**
* @summary The buffer length of the return code message
* @type {Number}
* @constant
*/
exports.RETURN_CODE_LENGTH = 4
/**
* @summary Send a buffer size to a device as a control transfer
* @function
* @public
*
* @param {Object} device - node-usb device
* @param {Number} size - buffer size
* @returns {Promise}
*
* @example
* const usb = require('usb')
* const device = usb.findByIds(0x0a5c, 0x2763)
*
* protocol.sendBufferSize(device, 512).then(() => {
* console.log('Done!')
* })
*/
exports.sendBufferSize = (device, size) => {
return usb.performControlTransfer(device, {
bmRequestType: usb.LIBUSB_REQUEST_TYPE_VENDOR,
bRequest: USB_REQUEST_CODE_GET_STATUS,
data: NULL_BUFFER,
/* eslint-disable no-bitwise */
wValue: size & USBBOOT_MESSAGE_MAX_BUFFER_LENGTH,
wIndex: size >> CONTROL_TRANSFER_INDEX_RIGHT_BIT_SHIFT
/* eslint-enable no-bitwise */
})
}
/**
* @summary Write a buffer to an OUT endpoint
* @function
* @private
*
* @param {Object} device - device
* @param {Object} endpoint - endpoint
* @param {Buffer} buffer - buffer
* @returns {Promise}
*
* @example
* const usb = require('usb')
* const device = usb.findByIds(0x0a5c, 0x2763)
* return protocol.write(device, device.interface(0).endpoint(1), Buffer.alloc(1)).then(() => {
* console.log('Done!')
* })
*/
exports.write = (device, endpoint, buffer) => {
return exports.sendBufferSize(device, buffer.length)
// We get LIBUSB_TRANSFER_STALL sometimes
// in future bulk transfers without this
.delay(USB_REQUEST_DELAY_MS)
.then(() => {
return Bluebird.fromCallback((callback) => {
endpoint.timeout = USB_BULK_TRANSFER_TIMEOUT_MS
endpoint.transfer(buffer, callback)
})
})
}
/**
* @summary Send an error signal to a device
* @function
* @public
*
* @param {Object} device - node-usb device
* @returns {Promise}
*
* @example
* const usb = require('usb')
* const device = usb.findByIds(0x0a5c, 0x2763)
*
* protocol.sendErrorSignal(device).then(() => {
* console.log('Done!')
* })
*/
exports.sendErrorSignal = (device) => {
return exports.sendBufferSize(device, NULL_BUFFER_SIZE)
}
/**
* @summary Read a buffer from a device
* @function
* @private
*
* @param {Object} device - device
* @param {Number} bytesToRead - bytes to read
* @fulfil {Buffer} - data
* @returns {Promise}
*
* @example
* const usb = require('usb')
* const device = usb.findByIds(0x0a5c, 0x2763)
* protocol.read(device, 4).then((data) => {
* console.log(data.readInt32BE())
* })
*/
exports.read = (device, bytesToRead) => {
return usb.performControlTransfer(device, {
/* eslint-disable no-bitwise */
bmRequestType: usb.LIBUSB_REQUEST_TYPE_VENDOR | usb.LIBUSB_ENDPOINT_IN,
wValue: bytesToRead & USBBOOT_MESSAGE_MAX_BUFFER_LENGTH,
wIndex: bytesToRead >> CONTROL_TRANSFER_INDEX_RIGHT_BIT_SHIFT,
/* eslint-enable no-bitwise */
bRequest: USB_REQUEST_CODE_GET_STATUS,
length: bytesToRead
})
}
/**
* @summary Create a boot message buffer
* @function
* @private
*
* @description
* This is based on the following data structure:
*
* typedef struct MESSAGE_S {
* int length;
* unsigned char signature[20];
* } boot_message_t;
*
* This needs to be sent to the out endpoint of the USB device
* as a 24 bytes big-endian buffer where:
*
* - The first 4 bytes contain the size of the bootcode.bin buffer
* - The remaining 20 bytes contain the boot signature, which
* we don't make use of in this implementation
*
* @param {Number} bootCodeBufferLength - bootcode.bin buffer length
* @returns {Buffer} boot message buffer
*
* @example
* const bootMessageBuffer = protocol.createBootMessageBuffer(50216)
*/
exports.createBootMessageBuffer = (bootCodeBufferLength) => {
const bootMessageBufferSize = BOOT_MESSAGE_BOOTCODE_LENGTH_SIZE + BOOT_MESSAGE_SIGNATURE_SIZE
// Buffers are automatically filled with zero bytes
const bootMessageBuffer = Buffer.alloc(bootMessageBufferSize)
// The bootcode length should be stored in 4 big-endian bytes
bootMessageBuffer.writeInt32BE(bootCodeBufferLength, BOOT_MESSAGE_BOOTCODE_LENGTH_OFFSET)
return bootMessageBuffer
}
/**
* @summary Parse a file message buffer from a device
* @function
* @public
*
* @param {Buffer} fileMessageBuffer - file message buffer
* @returns {Object} parsed file message
*
* @example
* const usb = require('usb')
* const device = usb.findByIds(0x0a5c, 0x2763)
*
* return protocol.read(device, protocol.FILE_MESSAGE_SIZE).then((fileMessageBuffer) => {
* return protocol.parseFileMessageBuffer(fileMessageBuffer)
* }).then((fileMessage) => {
* console.log(fileMessage.command)
* console.log(fileMessage.fileName)
* })
*/
exports.parseFileMessageBuffer = (fileMessageBuffer) => {
const commandCode = fileMessageBuffer.readInt32LE(FILE_MESSAGE_COMMAND_OFFSET)
const command = _.nth([
exports.FILE_MESSAGE_COMMANDS.GET_FILE_SIZE,
exports.FILE_MESSAGE_COMMANDS.READ_FILE,
exports.FILE_MESSAGE_COMMANDS.DONE
], commandCode)
if (_.isNil(command)) {
throw new Error(`Invalid file message command code: ${commandCode}`)
}
const fileName = _.chain(fileMessageBuffer.toString('ascii', FILE_MESSAGE_FILE_NAME_OFFSET))
// The parsed string will likely contain tons of trailing
// null bytes that we should get rid of for convenience
// See https://github.com/nodejs/node/issues/4775
.takeWhile((character) => {
return character !== '\0'
})
.join('')
.value()
// A blank file name can also mean "done"
if (_.isEmpty(fileName)) {
return {
command: exports.FILE_MESSAGE_COMMANDS.DONE
}
}
return {
command,
fileName
}
}

View File

@ -0,0 +1,161 @@
/*
* Copyright 2017 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict'
const _ = require('lodash')
const Bluebird = require('bluebird')
// The USB module calls `libusb_init`, which will fail
// if the device we're running in has no USB controller
// plugged in (e.g. in certain CI services).
// In order to workaround that, we need to return a
// stub if such error occurs.
const usb = (() => {
try {
return require('usb')
} catch (error) {
return {
getDeviceList: _.constant([])
}
}
})()
// Re-expose some `usb` constants
_.each([
'LIBUSB_REQUEST_TYPE_VENDOR',
'LIBUSB_ENDPOINT_IN',
'LIBUSB_TRANSFER_TYPE_BULK',
'LIBUSB_ERROR_NO_DEVICE',
'LIBUSB_ERROR_IO'
], (constant) => {
exports[constant] = usb[constant]
})
/**
* @summary List the available USB devices
* @function
* @public
*
* @fulfil {Object[]} - usb devices
* @returns {Promise}
*
* @example
* usb.listDevices().each((device) => {
* console.log(device)
* })
*/
exports.listDevices = () => {
return Bluebird.resolve(usb.getDeviceList())
}
/**
* @summary Get a USB device string from an index
* @function
* @public
*
* @param {Object} device - device
* @param {Number} index - string index
* @fulfil {String} - string
* @returns {Promise}
*
* @example
* usb.getDeviceStringFromIndex({ ... }, 5).then((string) => {
* console.log(string)
* })
*/
exports.getDeviceStringFromIndex = (device, index) => {
return Bluebird.fromCallback((callback) => {
device.getStringDescriptor(index, callback)
})
}
/**
* @summary Perform a USB control transfer
* @function
* @public
*
* @description
* See http://libusb.sourceforge.net/api-1.0/group__syncio.html
*
* @param {Object} device - usb device
* @param {Object} options - options
* @param {Number} options.bmRequestType - the request type field for the setup packet
* @param {Number} options.bRequest - the request field for the setup packet
* @param {Number} options.wValue - the value field for the setup packet
* @param {Number} options.wIndex - the index field for the setup packet
* @param {Buffer} [options.data] - output data buffer (for OUT transfers)
* @param {Number} [options.length] - input data size (for IN transfers)
* @fulfil {(Buffer|Undefined)} - result
* @returns {Promise}
*
* @example
* const buffer = Buffer.alloc(512)
*
* usb.performControlTransfer({ ... }, {
* bmRequestType: usb.LIBUSB_REQUEST_TYPE_VENDOR
* bRequest: 0,
* wValue: buffer.length & 0xffff,
* wIndex: buffer.length >> 16,
* data: Buffer.alloc(256)
* })
*/
exports.performControlTransfer = (device, options) => {
if (_.isNil(options.data) && _.isNil(options.length)) {
return Bluebird.reject(new Error('You must define either data or length'))
}
if (!_.isNil(options.data) && !_.isNil(options.length)) {
return Bluebird.reject(new Error('You can define either data or length, but not both'))
}
return Bluebird.fromCallback((callback) => {
device.controlTransfer(
options.bmRequestType,
options.bRequest,
options.wValue,
options.wIndex,
options.data || options.length,
callback
)
})
}
/**
* @summary Get a human friendly name for a USB device
* @function
* @public
*
* @description
* This function assumes the device is open.
*
* @param {Object} device - usb device
* @fulfil {String} - device name
* @returns {Promise}
*
* @example
* usb.getDeviceName({ ... }).then((name) => {
* console.log(name)
* })
*/
exports.getDeviceName = (device) => {
return Bluebird.props({
manufacturer: exports.getDeviceStringFromIndex(device, device.deviceDescriptor.iManufacturer),
product: exports.getDeviceStringFromIndex(device, device.deviceDescriptor.iProduct)
}).tap((properties) => {
return `${properties.manufacturer} ${properties.product}`
})
}

View File

@ -120,6 +120,9 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
})
}
// Convert object instances to plain objects
action.data = JSON.parse(JSON.stringify(action.data))
if (!_.isArray(action.data) || !_.every(action.data, _.isPlainObject)) {
throw errors.createError({
title: `Invalid drives: ${action.data}`

View File

@ -16,6 +16,8 @@
'use strict'
process.env.DEBUG = `sdk:usbboot,${process.env.DEBUG}`
// See http://electron.atom.io/docs/v0.37.7/api/environment-variables/#electronrunasnode
//
// Notice that if running electron with `ELECTRON_RUN_AS_NODE`, the binary

133
npm-shrinkwrap.json generated
View File

@ -56,8 +56,7 @@
"abbrev": {
"version": "1.1.0",
"from": "abbrev@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz"
},
"acorn": {
"version": "4.0.11",
@ -229,8 +228,7 @@
"aproba": {
"version": "1.1.1",
"from": "aproba@>=1.0.3 <2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.1.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.1.tgz"
},
"arch": {
"version": "2.1.0",
@ -240,8 +238,7 @@
"are-we-there-yet": {
"version": "1.1.2",
"from": "are-we-there-yet@>=1.1.2 <1.2.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz"
},
"argparse": {
"version": "1.0.7",
@ -447,8 +444,7 @@
"balanced-match": {
"version": "0.4.2",
"from": "balanced-match@>=0.4.1 <0.5.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz"
},
"base64-js": {
"version": "0.0.8",
@ -489,8 +485,7 @@
"block-stream": {
"version": "0.0.9",
"from": "block-stream@*",
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz"
},
"bloodline": {
"version": "1.0.1",
@ -599,8 +594,7 @@
"brace-expansion": {
"version": "1.1.6",
"from": "brace-expansion@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz"
},
"braces": {
"version": "1.8.5",
@ -913,8 +907,7 @@
"concat-map": {
"version": "0.0.1",
"from": "concat-map@0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
},
"concat-stream": {
"version": "1.5.2",
@ -951,8 +944,7 @@
"console-control-strings": {
"version": "1.1.0",
"from": "console-control-strings@>=1.1.0 <1.2.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz"
},
"contains-path": {
"version": "0.1.0",
@ -1142,8 +1134,7 @@
"deep-extend": {
"version": "0.4.1",
"from": "deep-extend@>=0.4.1 <0.5.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz"
},
"deep-is": {
"version": "0.1.3",
@ -1254,8 +1245,7 @@
"delegates": {
"version": "1.0.0",
"from": "delegates@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz"
},
"detect-node": {
"version": "2.0.3",
@ -2593,14 +2583,17 @@
"fs.realpath": {
"version": "1.0.0",
"from": "fs.realpath@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
},
"fstream": {
"version": "1.0.11",
"from": "fstream@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz"
},
"fstream-ignore": {
"version": "1.0.5",
"from": "fstream-ignore@>=1.0.5 <2.0.0",
"resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz"
},
"ftp": {
"version": "0.3.10",
@ -2625,8 +2618,7 @@
"gauge": {
"version": "2.7.3",
"from": "gauge@>=2.7.1 <2.8.0",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.3.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.3.tgz"
},
"gaze": {
"version": "1.1.2",
@ -2721,8 +2713,7 @@
"glob": {
"version": "7.1.1",
"from": "glob@>=7.1.0 <8.0.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz"
},
"glob-base": {
"version": "0.3.0",
@ -2836,8 +2827,7 @@
"graceful-fs": {
"version": "4.1.11",
"from": "graceful-fs@>=4.1.2 <5.0.0",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz"
},
"graceful-readlink": {
"version": "1.0.1",
@ -2914,8 +2904,7 @@
"has-unicode": {
"version": "2.0.1",
"from": "has-unicode@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz"
},
"has-values": {
"version": "0.1.4",
@ -3086,8 +3075,7 @@
"inflight": {
"version": "1.0.6",
"from": "inflight@>=1.0.4 <2.0.0",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz"
},
"inherits": {
"version": "2.0.1",
@ -3097,8 +3085,7 @@
"ini": {
"version": "1.3.4",
"from": "ini@>=1.3.0 <1.4.0",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz"
},
"inquirer": {
"version": "0.11.4",
@ -4554,8 +4541,7 @@
"minimatch": {
"version": "3.0.3",
"from": "minimatch@>=3.0.2 <4.0.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz"
},
"minimist": {
"version": "1.2.0",
@ -4590,13 +4576,11 @@
"version": "0.5.1",
"from": "mkdirp@>=0.5.0 <0.6.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"dev": true,
"dependencies": {
"minimist": {
"version": "0.0.8",
"from": "minimist@0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
}
}
},
@ -4900,6 +4884,23 @@
"from": "node-ipc@8.9.2",
"resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-8.9.2.tgz"
},
"node-pre-gyp": {
"version": "0.6.36",
"from": "node-pre-gyp@>=0.6.30 <0.7.0",
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz",
"dependencies": {
"nopt": {
"version": "4.0.1",
"from": "nopt@>=4.0.1 <5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz"
},
"semver": {
"version": "5.4.1",
"from": "semver@>=5.3.0 <6.0.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz"
}
}
},
"node-sass": {
"version": "4.5.3",
"from": "node-sass@4.5.3",
@ -4958,13 +4959,11 @@
"version": "4.0.2",
"from": "npmlog@>=4.0.0 <5.0.0",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.0.2.tgz",
"dev": true,
"dependencies": {
"set-blocking": {
"version": "2.0.0",
"from": "set-blocking@~2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz"
}
}
},
@ -8206,8 +8205,7 @@
"os-homedir": {
"version": "1.0.2",
"from": "os-homedir@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz"
},
"os-locale": {
"version": "1.4.0",
@ -8222,8 +8220,7 @@
"osenv": {
"version": "0.1.4",
"from": "osenv@>=0.0.0 <1.0.0",
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz"
},
"p-finally": {
"version": "1.0.0",
@ -8337,8 +8334,7 @@
"path-is-absolute": {
"version": "1.0.1",
"from": "path-is-absolute@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz"
},
"path-is-inside": {
"version": "1.0.2",
@ -8622,8 +8618,7 @@
"rc": {
"version": "1.1.7",
"from": "rc@>=1.1.2 <2.0.0",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.1.7.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/rc/-/rc-1.1.7.tgz"
},
"react": {
"version": "15.5.4",
@ -8987,8 +8982,7 @@
"rimraf": {
"version": "2.6.1",
"from": "rimraf@>=2.5.2 <3.0.0",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz"
},
"run-async": {
"version": "0.1.0",
@ -9495,8 +9489,7 @@
"strip-json-comments": {
"version": "2.0.1",
"from": "strip-json-comments@>=2.0.1 <2.1.0",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz"
},
"striptags": {
"version": "2.2.1",
@ -9617,8 +9610,12 @@
"tar": {
"version": "2.2.1",
"from": "tar@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz"
},
"tar-pack": {
"version": "3.4.0",
"from": "tar-pack@>=3.4.0 <4.0.0",
"resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.0.tgz"
},
"tempfile": {
"version": "1.1.1",
@ -9896,6 +9893,11 @@
"dev": true,
"optional": true
},
"uid-number": {
"version": "0.0.6",
"from": "uid-number@>=0.0.6 <0.0.7",
"resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz"
},
"uid2": {
"version": "0.0.3",
"from": "uid2@0.0.3",
@ -9974,6 +9976,18 @@
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz",
"dev": true
},
"usb": {
"version": "1.3.0",
"from": "tessel/node-usb#1.3.0",
"resolved": "git://github.com/tessel/node-usb.git#38cc9cc75759e74f3d3ee8c79ca852395c3529b0",
"dependencies": {
"nan": {
"version": "2.6.2",
"from": "nan@>=2.4.0 <3.0.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz"
}
}
},
"user-home": {
"version": "2.0.0",
"from": "user-home@>=2.0.0 <3.0.0",
@ -10099,8 +10113,7 @@
"wide-align": {
"version": "1.1.0",
"from": "wide-align@>=1.1.0 <2.0.0",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.0.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.0.tgz"
},
"widest-line": {
"version": "1.0.0",

View File

@ -51,6 +51,7 @@
"bootstrap-sass": "3.3.6",
"chalk": "1.1.3",
"command-join": "2.0.0",
"debug": "2.6.0",
"drivelist": "5.1.8",
"electron-is-running-in-asar": "1.0.0",
"etcher-image-write": "9.1.3",
@ -82,6 +83,7 @@
"trackjs": "2.3.1",
"udif": "0.10.0",
"unbzip2-stream": "1.0.11",
"usb": "github:tessel/node-usb#1.3.0",
"uuid": "3.0.1",
"xml2js": "0.4.17",
"yargs": "4.7.1",

View File

@ -21,8 +21,10 @@ RUN apt-get update \
libasound2 \
libgconf-2-4 \
libgtk2.0-0 \
libx11-xcb1 \
libudev-dev \
libusb-1.0-0-dev \
libnss3 \
libx11-xcb1 \
libxss1 \
libxtst6 \
libyaml-dev \

View File

@ -20,8 +20,10 @@ RUN apt-get update \
libasound2 \
libgconf-2-4 \
libgtk2.0-0 \
libx11-xcb1 \
libudev-dev \
libusb-1.0-0-dev \
libnss3 \
libx11-xcb1 \
libxss1 \
libxtst6 \
libyaml-dev \

View File

@ -25,8 +25,10 @@ RUN apt-get update \
libasound2 \
libgconf-2-4 \
libgtk2.0-0 \
libx11-xcb1 \
libudev-dev \
libusb-1.0-0-dev \
libnss3 \
libx11-xcb1 \
libxss1 \
libxtst6 \
libyaml-dev \

View File

@ -74,6 +74,27 @@ describe('Model: availableDrives', function () {
m.chai.expect(availableDrives.getDrives()).to.deep.equal(drives)
})
it('should be able to set non-plain drive objects', function () {
class Device {
constructor () {
this.device = '/dev/sdb'
this.description = 'Foo'
this.mountpoint = '/mnt/foo'
this.system = false
}
}
availableDrives.setDrives([ new Device() ])
m.chai.expect(availableDrives.getDrives()).to.deep.equal([
{
device: '/dev/sdb',
description: 'Foo',
mountpoint: '/mnt/foo',
system: false
}
])
})
it('should be able to set drives with extra properties', function () {
const drives = [
{