mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-22 18:56:31 +00:00
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:
parent
3147a93ca6
commit
f6a7b2add6
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -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
|
||||
|
@ -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
30
lib/blobs/usbboot/LICENSE
Normal 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.
|
||||
|
5
lib/blobs/usbboot/README.md
Normal file
5
lib/blobs/usbboot/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
usbboot
|
||||
=======
|
||||
|
||||
The files in this directory were taken from
|
||||
https://github.com/raspberrypi/usbboot.
|
BIN
lib/blobs/usbboot/bootcode.bin
Normal file
BIN
lib/blobs/usbboot/bootcode.bin
Normal file
Binary file not shown.
BIN
lib/blobs/usbboot/start.elf
Executable file
BIN
lib/blobs/usbboot/start.elf
Executable file
Binary file not shown.
@ -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)
|
||||
|
@ -25,7 +25,8 @@ const _ = require('lodash')
|
||||
* @constant
|
||||
*/
|
||||
const ADAPTORS = [
|
||||
require('./standard')
|
||||
require('./standard'),
|
||||
require('./usbboot')
|
||||
]
|
||||
|
||||
/**
|
||||
|
387
lib/shared/sdk/usbboot/index.js
Normal file
387
lib/shared/sdk/usbboot/index.js
Normal 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
|
||||
})
|
||||
}
|
381
lib/shared/sdk/usbboot/protocol.js
Normal file
381
lib/shared/sdk/usbboot/protocol.js
Normal 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
|
||||
}
|
||||
}
|
161
lib/shared/sdk/usbboot/usb.js
Normal file
161
lib/shared/sdk/usbboot/usb.js
Normal 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}`
|
||||
})
|
||||
}
|
@ -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}`
|
||||
|
@ -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
133
npm-shrinkwrap.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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 \
|
||||
|
@ -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 \
|
||||
|
@ -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 \
|
||||
|
@ -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 = [
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user