Show raspberry pi usbboot update progress in devices list

This commit is contained in:
Alexis Svinartchouk 2018-06-29 14:03:03 +01:00
parent a8a75f22b2
commit d07d535993
11 changed files with 185 additions and 111 deletions

View File

@ -28,8 +28,12 @@ var angular = require('angular')
const electron = require('electron')
const Bluebird = require('bluebird')
const sdk = require('etcher-sdk')
const _ = require('lodash')
const semver = require('semver')
const uuidV4 = require('uuid/v4')
>>>>>>> Show raspberry pi usbboot update progress in devices list
const EXIT_CODES = require('../../shared/exit-codes')
const messages = require('../../shared/messages')
const s3Packages = require('../../shared/s3-packages')
@ -229,35 +233,77 @@ app.run(() => {
})
})
app.run(($timeout) => {
function updateDrives() {
const drives = Array.from(driveScanner.drives)
const BLACKLISTED_DRIVES = settings.has('driveBlacklist')
? settings.get('driveBlacklist').split(',')
: []
app.run(($timeout) => {
const BLACKLISTED_DRIVES = settings.has('driveBlacklist')
? settings.get('driveBlacklist').split(',')
: []
function driveIsAllowed(drive) {
return !(
BLACKLISTED_DRIVES.includes(drive.devicePath) ||
BLACKLISTED_DRIVES.includes(drive.device) ||
BLACKLISTED_DRIVES.includes(drive.raw)
)
}
function prepareDrive(drive) {
if (drive instanceof sdk.sourceDestination.BlockDevice) {
return drive.drive
} else if (drive instanceof sdk.sourceDestination.UsbbootDrive) {
// This is a workaround etcher expecting a device string and a size
drive.device = drive.usbDevice.portId
drive.size = 0
drive.progress = 0
drive.on('progress', (progress) => {
updateDriveProgress(drive, progress)
})
return drive
}
}
function setDrives(drives) {
drives = _.values(drives)
availableDrives.setDrives(drives)
// Safely trigger a digest cycle.
// In some cases, AngularJS doesn't acknowledge that the
// available drives list has changed, and incorrectly
// keeps asking the user to "Connect a drive".
$timeout(() => {
const allowedDrives = drives
.filter((drive) => {
return !(
BLACKLISTED_DRIVES.includes(drive.devicePath) ||
BLACKLISTED_DRIVES.includes(drive.device) ||
BLACKLISTED_DRIVES.includes(drive.raw)
)
})
.map((drive) => {
// TODO: we should be able to use the SourceDestination `drive` directly
return drive.drive
})
availableDrives.setDrives(allowedDrives)
})
$timeout()
}
driveScanner.on('attach', updateDrives)
driveScanner.on('detach', updateDrives)
function getDrives() {
return _.keyBy(availableDrives.getDrives() || [], 'device')
}
function addDrive(drive) {
drive = prepareDrive(drive)
if (!driveIsAllowed(drive)) {
return
}
const drives = getDrives()
drives[drive.device] = drive
setDrives(drives)
}
function removeDrive(drive) {
drive = prepareDrive(drive)
const drives = getDrives()
delete drives[drive.device]
setDrives(drives)
}
function updateDriveProgress(drive, progress) {
const drives = getDrives()
const drive_ = drives[drive.device]
if (drive !== undefined) {
drive.progress = progress
setDrives(drives)
}
}
driveScanner.on('attach', addDrive)
driveScanner.on('detach', removeDrive)
driveScanner.on('error', (error) => {
// Stop the drive scanning loop in case of errors,

View File

@ -174,7 +174,7 @@ class FileSelector extends React.PureComponent {
if (!supportedFormats.isSupportedImage(image.path)) {
const invalidImageError = errors.createUserError({
title: 'Invalid image',
description: messages.error.invalidImage(image)
description: messages.error.invalidImage(image.path)
})
osDialog.showError(invalidImageError)
@ -229,7 +229,7 @@ class FileSelector extends React.PureComponent {
// An easy way so we can quickly identify if we're making use of
// certain features without printing pages of text to DevTools.
image.logo = Boolean(image.logo)
image.bmap = Boolean(image.bmap)
image.blockMap = Boolean(image.blockMap)
analytics.logEvent('Select image', {
image,

View File

@ -211,9 +211,7 @@ exports.getImageSize = () => {
return _.get(store.getState().toJS(), [
'selection',
'image',
'size',
'final',
'value'
'size'
])
}

View File

@ -67,8 +67,7 @@ const flashStateNoNilFields = [
*/
const selectImageNoNilFields = [
'path',
'extension',
'size'
'extension'
]
/**
@ -406,29 +405,11 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
}
}
if (!_.isPlainObject(action.data.size)) {
throw errors.createError({
title: `Invalid image size: ${action.data.size}`
})
}
const MINIMUM_IMAGE_SIZE = 0
if (!_.isInteger(action.data.size.original) || action.data.size.original < MINIMUM_IMAGE_SIZE) {
if ((action.data.size !== undefined) && (action.data.size < MINIMUM_IMAGE_SIZE)) {
throw errors.createError({
title: `Invalid original image size: ${action.data.size.original}`
})
}
if (!_.isInteger(action.data.size.final.value) || action.data.size.final.value < MINIMUM_IMAGE_SIZE) {
throw errors.createError({
title: `Invalid final image size: ${action.data.size.final.value}`
})
}
if (!_.isBoolean(action.data.size.final.estimation)) {
throw errors.createError({
title: `Invalid final image size estimation flag: ${action.data.size.final.estimation}`
title: `Invalid image size: ${action.data.size}`
})
}

View File

@ -20,7 +20,6 @@ const sdk = require('etcher-sdk')
const process = require('process')
const settings = require('../models/settings')
const permissions = require('../../../shared/permissions')
function includeSystemDrives() {
return settings.get('unsafeMode') && !settings.get('disableUnsafeMode')
@ -30,12 +29,11 @@ const adapters = [
new sdk.scanner.adapters.BlockDeviceAdapter(includeSystemDrives)
]
permissions.isElevated()
.then((isElevated) => {
if ((process.platform !== 'linux') || isElevated) {
adapters.push(new sdk.scanner.adapters.UsbbootDeviceAdapter())
}
})
// Can't use permissions.isElevated() here as it returns a promise and we need to set
// module.exports = scanner right now.
if ((process.platform !== 'linux') || (process.geteuid() === 0)) {
adapters.push(new sdk.scanner.adapters.UsbbootDeviceAdapter())
}
const scanner = new sdk.scanner.Scanner(adapters)

View File

@ -188,6 +188,7 @@ module.exports = function (
exceptionReporter.report(error)
}
}).finally(() => {
availableDrives.setDrives([])
driveScanner.start()
unsubscribe()
})

View File

@ -19,10 +19,11 @@
const _ = require('lodash')
const Bluebird = require('bluebird')
const path = require('path')
const sdk = require('etcher-sdk')
const store = require('../../../models/store')
const messages = require('../../../../../shared/messages')
const errors = require('../../../../../shared/errors')
const imageStream = require('../../../../../sdk/image-stream')
const supportedFormats = require('../../../../../shared/supported-formats')
const analytics = require('../../../modules/analytics')
const settings = require('../../../models/settings')
@ -124,7 +125,7 @@ module.exports = function (
// An easy way so we can quickly identify if we're making use of
// certain features without printing pages of text to DevTools.
image.logo = Boolean(image.logo)
image.bmap = Boolean(image.bmap)
image.blockMap = Boolean(image.blockMap)
return analytics.logEvent('Select image', {
image,
@ -145,10 +146,33 @@ module.exports = function (
* ImageSelectionController.selectImageByPath('path/to/image.img');
*/
this.selectImageByPath = (imagePath) => {
imageStream.getImageMetadata(imagePath)
.then((imageMetadata) => {
$timeout(() => {
this.selectImage(imageMetadata)
if (!supportedFormats.isSupportedImage(imagePath)) {
const invalidImageError = errors.createUserError({
title: 'Invalid image',
description: messages.error.invalidImage(imagePath)
})
osDialog.showError(invalidImageError)
analytics.logEvent('Invalid image', { path: imagePath })
return
}
const source = new sdk.sourceDestination.File(imagePath, sdk.sourceDestination.File.OpenFlags.Read)
source.getInnerSource()
.then((innerSource) => {
return innerSource.getMetadata()
.then((metadata) => {
return innerSource.getPartitionTable()
.then((partitionTable) => {
if (partitionTable !== undefined) {
metadata.hasMBR = true
metadata.partitions = partitionTable.partitions
}
$timeout(() => {
metadata.path = imagePath
metadata.extension = path.extname(imagePath).slice(1)
this.selectImage(metadata)
})
})
})
.catch((error) => {
@ -156,10 +180,14 @@ module.exports = function (
title: 'Error opening image',
description: messages.error.openImage(path.basename(imagePath), error.message)
})
osDialog.showError(imageError)
analytics.logException(error)
})
.then(() => {
return innerSource.close()
.catch(() => {})
})
})
}
/**

View File

@ -22,9 +22,6 @@ const ipc = require('node-ipc')
const sdk = require('etcher-sdk')
const EXIT_CODES = require('../../shared/exit-codes')
const errors = require('../../shared/errors')
const ImageWriter = require('../../sdk/writer')
const BlockWriteStream = require('../../sdk/writer/block-write-stream')
const BlockReadStream = require('../../sdk/writer/block-read-stream')
ipc.config.id = process.env.IPC_CLIENT_ID
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT
@ -99,13 +96,14 @@ function runVerifier(verifier, onFail) {
function pipeRegularSourceToDestination(source, destination, verify, onProgress, onFail) {
let checksum
let sparse
let sourceMetadata
let step = 'flashing'
let lastPosition = 0
const errors = new Map() // destination -> error map
const state = {
active: destination.destinations.length,
flashing: destination.destinations.length,
active: destination.destinations.size,
flashing: destination.destinations.size,
verifying: 0,
failed: 0,
successful: 0,
@ -114,7 +112,7 @@ function pipeRegularSourceToDestination(source, destination, verify, onProgress,
function updateState() {
state.type = step
state.failed = errors.size
state.active = destination.destinations.length - state.failed
state.active = destination.destinations.size - state.failed
if (step === 'flashing') {
state.flashing = state.active
state.verifying = 0
@ -127,28 +125,48 @@ function pipeRegularSourceToDestination(source, destination, verify, onProgress,
}
function onProgress2(progressEvent) {
lastPosition = progressEvent.position
progressEvent.percentage = progressEvent.position / sourceMetadata.size * 100
let size
if (sparse && (sourceMetadata.blockMap !== undefined)) {
size = sourceMetadata.blockMap.mappedBlockCount * sourceMetadata.blockMap.blockSize
progressEvent.percentage = progressEvent.bytes / size * 100
} else {
size = sourceMetadata.size
progressEvent.percentage = progressEvent.position / size * 100
}
// NOTE: We need to guard against this becoming Infinity,
// because that value isn't transmitted properly over IPC and becomes `null`
progressEvent.eta = progressEvent.speed ? (sourceMetadata.size - progressEvent.position) / progressEvent.speed : null
progressEvent.eta = progressEvent.speed ? (size - progressEvent.bytes) / progressEvent.speed : null
progressEvent.totalSpeed = progressEvent.speed * state.active
Object.assign(progressEvent, state)
onProgress(progressEvent)
}
return Promise.all([ source.createReadStream(), destination.createWriteStream(), source.getMetadata() ])
return source.canCreateSparseReadStream()
.then((_sparse) => {
sparse = _sparse
let sourceStream
let destinationStream
if (sparse) {
// TODO: calculate checksums in source if needed
sourceStream = source.createSparseReadStream()
destinationStream = destination.createSparseWriteStream()
} else {
sourceStream = source.createReadStream()
destinationStream = destination.createWriteStream()
}
return Promise.all([ sourceStream, destinationStream, source.getMetadata() ])
})
.then(([ sourceStream, destinationStream, metadata ]) => {
destinationStream.on('fail', (error) => {
errors.set(error.destination, error.error)
updateState()
onFail({ device: error.destination.drive, error: error.error }) // TODO: device should be error.destination
onProgress2({ eta: 0, speed: 0, position: lastPosition }) // TODO: this is not needed if a success / error screen is shown
})
sourceMetadata = metadata
return new Promise((resolve, reject) => {
let done = false
sourceStream.on('error', reject)
destinationStream.on('progress', onProgress2)
if (verify) {
if (verify && !sparse) {
const hasher = sdk.sourceDestination.createHasher()
hasher.on('checksum', (cs) => {
checksum = cs
@ -160,7 +178,7 @@ function pipeRegularSourceToDestination(source, destination, verify, onProgress,
}
destinationStream.on('done', () => {
done = true;
if (!verify || (checksum !== undefined)) {
if (sparse || !verify || (checksum !== undefined)) {
resolve()
}
})
@ -176,7 +194,7 @@ function pipeRegularSourceToDestination(source, destination, verify, onProgress,
if (verify) {
step = 'check'
updateState()
const verifier = destination.createVerifier(checksum, sourceMetadata.size)
const verifier = destination.createVerifier(sparse ? sourceMetadata.blockMap : checksum, sourceMetadata.size) // TODO: ensure blockMap exists
verifier.on('progress', onProgress2)
return runVerifier(verifier, onFail)
}
@ -184,7 +202,7 @@ function pipeRegularSourceToDestination(source, destination, verify, onProgress,
.then(() => {
step = 'finished'
updateState()
onProgress2({ speed: 0, position: sourceMetadata.size })
//onProgress2({ speed: 0, position: sourceMetadata.size })
})
.then(() => {
const result = {
@ -281,6 +299,8 @@ ipc.connectTo(IPC_SERVER_ID, () => {
terminate(exitCode)
}
ipc.of[IPC_SERVER_ID].on('cancel', onAbort)
/**
* @summary Error handler
* @param {Error} error - error
@ -306,30 +326,22 @@ ipc.connectTo(IPC_SERVER_ID, () => {
})
}
writer = new ImageWriter({ // TODO: remove
verify: options.validateWriteOnSuccess,
unmountOnSuccess: options.unmountOnSuccess,
checksumAlgorithms: options.checksumAlgorithms || []
})
writer.on('abort', onAbort)
const destinations = _.map(options.destinations, 'drive.device')
const dests = options.destinations.map((destination) => {
return new sdk.sourceDestination.BlockDevice(destination)
return new sdk.sourceDestination.BlockDevice(destination, options.unmountOnSuccess)
})
const source = new sdk.sourceDestination.File(options.imagePath, sdk.sourceDestination.File.OpenFlags.Read)
source.getInnerSource()
.then((innerSource) => {
return Bluebird.using(
sourceDestinationDisposer(innerSource),
sourceDestinationDisposer(new sdk.sourceDestination.MultiDestination(dests)),
(innerSource, destination) => {
destination.on('fail', onFail)
return pipeRegularSourceToDestination(innerSource, destination, options.validateWriteOnSuccess, onProgress, onFail)
}
)
})
Bluebird.using(
sourceDestinationDisposer(new sdk.sourceDestination.File(options.imagePath, sdk.sourceDestination.File.OpenFlags.Read)),
sourceDestinationDisposer(new sdk.sourceDestination.MultiDestination(dests)),
(source, destination) => {
return source.getInnerSource()
.then((innerSource) => {
return Bluebird.using(sourceDestinationDisposer(innerSource), (innerSource) => {
return pipeRegularSourceToDestination(innerSource, destination, options.validateWriteOnSuccess, onProgress, onFail)
})
})
}
)
.then((results) => {
onFinish(results)
})
@ -343,12 +355,6 @@ ipc.connectTo(IPC_SERVER_ID, () => {
log(`Validate on success: ${options.validateWriteOnSuccess}`)
})
ipc.of[IPC_SERVER_ID].on('cancel', () => {
if (writer) {
writer.abort()
}
})
ipc.of[IPC_SERVER_ID].on('connect', () => {
log(`Successfully connected to IPC server: ${IPC_SERVER_ID}, socket root ${ipc.config.socketRoot}`)
ipc.of[IPC_SERVER_ID].emit('ready', {})

View File

@ -182,8 +182,8 @@ module.exports = {
].join(' ')
},
invalidImage: (image) => {
return `${image.path} is not a supported image type.`
invalidImage: (imagePath) => {
return `${imagePath} is not a supported image type.`
},
openImage: (imageBasename, errorMessage) => {

22
npm-shrinkwrap.json generated
View File

@ -58,6 +58,10 @@
"version": "0.0.30",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-0.0.30.tgz"
},
"@types/events": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz"
},
"@types/file-type": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/@types/file-type/-/file-type-5.2.1.tgz"
@ -126,6 +130,10 @@
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.4.tgz"
},
"@types/yauzl": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.0.tgz"
},
"7zip-bin": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-2.4.1.tgz",
@ -661,8 +669,8 @@
"dev": true
},
"aws-sdk": {
"version": "2.260.1",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.260.1.tgz",
"version": "2.263.1",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.263.1.tgz",
"dependencies": {
"ieee754": {
"version": "1.1.8",
@ -3060,7 +3068,7 @@
},
"etcher-sdk": {
"version": "0.0.1",
"resolved": "git://github.com/resin-io-modules/etcher-sdk.git#b12e63b49c4a01305a2809b504859a3940927399",
"resolved": "git://github.com/resin-io-modules/etcher-sdk.git#bda51535715edb3691b783973801434bb3d78b30",
"dependencies": {
"@types/lodash": {
"version": "4.14.110",
@ -3086,6 +3094,10 @@
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz"
},
"fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz"
},
"file-type": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-8.0.0.tgz"
@ -3129,6 +3141,10 @@
"xmlbuilder": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz"
},
"yauzl": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.9.2.tgz"
}
}
},

View File

@ -63,7 +63,7 @@
"debug": "3.1.0",
"drivelist": "6.4.6",
"electron-is-running-in-asar": "1.0.0",
"etcher-sdk": "github:resin-io-modules/etcher-sdk#b12e63b49c4a01305a2809b504859a3940927399",
"etcher-sdk": "github:resin-io-modules/etcher-sdk#bda51535715edb3691b783973801434bb3d78b30",
"file-type": "4.1.0",
"flexboxgrid": "6.3.0",
"gpt": "1.0.0",