diff --git a/.gitattributes b/.gitattributes index 0d117874..d18e7e1b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/.travis.yml b/.travis.yml index aa97b715..c7d97645 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/lib/blobs/usbboot/LICENSE b/lib/blobs/usbboot/LICENSE new file mode 100644 index 00000000..89b5c0c0 --- /dev/null +++ b/lib/blobs/usbboot/LICENSE @@ -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. + diff --git a/lib/blobs/usbboot/README.md b/lib/blobs/usbboot/README.md new file mode 100644 index 00000000..16aeb9b9 --- /dev/null +++ b/lib/blobs/usbboot/README.md @@ -0,0 +1,5 @@ +usbboot +======= + +The files in this directory were taken from +https://github.com/raspberrypi/usbboot. diff --git a/lib/blobs/usbboot/bootcode.bin b/lib/blobs/usbboot/bootcode.bin new file mode 100644 index 00000000..e731673b Binary files /dev/null and b/lib/blobs/usbboot/bootcode.bin differ diff --git a/lib/blobs/usbboot/start.elf b/lib/blobs/usbboot/start.elf new file mode 100755 index 00000000..aedee326 Binary files /dev/null and b/lib/blobs/usbboot/start.elf differ diff --git a/lib/gui/modules/drive-scanner.js b/lib/gui/modules/drive-scanner.js index 7254c5a1..782efe3e 100644 --- a/lib/gui/modules/drive-scanner.js +++ b/lib/gui/modules/drive-scanner.js @@ -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) diff --git a/lib/shared/sdk/index.js b/lib/shared/sdk/index.js index 10e1f0b9..4690d3bf 100644 --- a/lib/shared/sdk/index.js +++ b/lib/shared/sdk/index.js @@ -25,7 +25,8 @@ const _ = require('lodash') * @constant */ const ADAPTORS = [ - require('./standard') + require('./standard'), + require('./usbboot') ] /** diff --git a/lib/shared/sdk/usbboot/index.js b/lib/shared/sdk/usbboot/index.js new file mode 100644 index 00000000..02fab743 --- /dev/null +++ b/lib/shared/sdk/usbboot/index.js @@ -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 + }) +} diff --git a/lib/shared/sdk/usbboot/protocol.js b/lib/shared/sdk/usbboot/protocol.js new file mode 100644 index 00000000..acd8303d --- /dev/null +++ b/lib/shared/sdk/usbboot/protocol.js @@ -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 + } +} diff --git a/lib/shared/sdk/usbboot/usb.js b/lib/shared/sdk/usbboot/usb.js new file mode 100644 index 00000000..6ae844a9 --- /dev/null +++ b/lib/shared/sdk/usbboot/usb.js @@ -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}` + }) +} diff --git a/lib/shared/store.js b/lib/shared/store.js index 6141ea0c..b244f8d3 100644 --- a/lib/shared/store.js +++ b/lib/shared/store.js @@ -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}` diff --git a/lib/start.js b/lib/start.js index 4462945e..75e9d4d8 100644 --- a/lib/start.js +++ b/lib/start.js @@ -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 diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index eaa2b5b0..d34f27b9 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -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", diff --git a/package.json b/package.json index 3f9cc884..29df491f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/build/docker/Dockerfile-i686 b/scripts/build/docker/Dockerfile-i686 index 8adfc84e..c6c35703 100644 --- a/scripts/build/docker/Dockerfile-i686 +++ b/scripts/build/docker/Dockerfile-i686 @@ -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 \ diff --git a/scripts/build/docker/Dockerfile-x86_64 b/scripts/build/docker/Dockerfile-x86_64 index d4803409..d26b3d04 100644 --- a/scripts/build/docker/Dockerfile-x86_64 +++ b/scripts/build/docker/Dockerfile-x86_64 @@ -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 \ diff --git a/scripts/build/docker/Dockerfile.template b/scripts/build/docker/Dockerfile.template index 327b6de0..e18e6dc7 100644 --- a/scripts/build/docker/Dockerfile.template +++ b/scripts/build/docker/Dockerfile.template @@ -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 \ diff --git a/tests/shared/models/available-drives.spec.js b/tests/shared/models/available-drives.spec.js index 84d57e54..27f2c17c 100644 --- a/tests/shared/models/available-drives.spec.js +++ b/tests/shared/models/available-drives.spec.js @@ -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 = [ {