mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-20 05:17:18 +00:00

As we've actually been displaying the read-speed in various forms during the flashing process, this is a venture into displaying the actual write-speed from the end of the pipeline. Change-Type: minor Changelog-Entry: Display actual write speed
326 lines
8.5 KiB
JavaScript
326 lines
8.5 KiB
JavaScript
/*
|
|
* 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 stream = require('readable-stream')
|
|
const Pipage = require('pipage')
|
|
const BlockMap = require('blockmap')
|
|
const BlockStream = require('./block-stream')
|
|
const BlockWriteStream = require('./block-write-stream')
|
|
const BlockReadStream = require('./block-read-stream')
|
|
const ChecksumStream = require('./checksum-stream')
|
|
const ProgressStream = require('./progress-stream')
|
|
const debug = require('debug')('image-writer')
|
|
const EventEmitter = require('events').EventEmitter
|
|
const _ = require('lodash')
|
|
|
|
/**
|
|
* @summary ImageWriter class
|
|
* @class
|
|
*/
|
|
class ImageWriter extends EventEmitter {
|
|
/**
|
|
* @summary ImageWriter constructor
|
|
* @param {Object} options - options
|
|
* @example
|
|
* new ImageWriter(options)
|
|
*/
|
|
constructor (options) {
|
|
super()
|
|
|
|
this.options = options
|
|
|
|
this.source = null
|
|
this.pipeline = null
|
|
this.target = null
|
|
|
|
this.hadError = false
|
|
|
|
this.bytesRead = 0
|
|
this.bytesWritten = 0
|
|
this.checksum = {}
|
|
}
|
|
|
|
/**
|
|
* @summary Start the writing process
|
|
* @returns {ImageWriter} imageWriter
|
|
* @example
|
|
* imageWriter.write()
|
|
*/
|
|
write () {
|
|
this.hadError = false
|
|
|
|
this._createWritePipeline(this.options)
|
|
.on('checksum', (checksum) => {
|
|
debug('write:checksum', checksum)
|
|
this.checksum = checksum
|
|
})
|
|
.on('error', (error) => {
|
|
this.hadError = true
|
|
this.emit('error', error)
|
|
})
|
|
|
|
this.target.on('finish', () => {
|
|
this.bytesRead = this.source.bytesRead
|
|
this.bytesWritten = this.target.bytesWritten
|
|
if (this.options.verify) {
|
|
this.verify()
|
|
} else {
|
|
this._emitFinish()
|
|
}
|
|
})
|
|
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* @summary Start the writing process
|
|
* @returns {ImageWriter} imageWriter
|
|
* @example
|
|
* imageWriter.verify()
|
|
*/
|
|
verify () {
|
|
this._createVerifyPipeline(this.options)
|
|
.on('error', (error) => {
|
|
this.hadError = true
|
|
this.emit('error', error)
|
|
})
|
|
.on('checksum', (checksum) => {
|
|
debug('verify:checksum', this.checksum, '==', checksum)
|
|
if (!_.isEqual(this.checksum, checksum)) {
|
|
const error = new Error(`Verification failed: ${JSON.stringify(this.checksum)} != ${JSON.stringify(checksum)}`)
|
|
error.code = 'EVALIDATION'
|
|
this.emit('error', error)
|
|
}
|
|
this._emitFinish()
|
|
})
|
|
.on('finish', () => {
|
|
debug('verify:end')
|
|
|
|
// NOTE: As the 'checksum' event only happens after
|
|
// the 'finish' event, we `._emitFinish()` there instead of here
|
|
})
|
|
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* @summary Abort the flashing process
|
|
* @example
|
|
* imageWriter.abort()
|
|
*/
|
|
abort () {
|
|
if (this.source) {
|
|
this.emit('abort')
|
|
this.source.destroy()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @summary Emits the `finish` event with state metadata
|
|
* @private
|
|
* @example
|
|
* this._emitFinish()
|
|
*/
|
|
_emitFinish () {
|
|
this.emit('finish', {
|
|
bytesRead: this.bytesRead,
|
|
bytesWritten: this.bytesWritten,
|
|
checksum: this.checksum
|
|
})
|
|
}
|
|
|
|
/**
|
|
* @summary Creates a write pipeline from given options
|
|
* @private
|
|
* @param {Object} options - options
|
|
* @param {Object} options.image - source image
|
|
* @param {Number} [options.fd] - destination file descriptor
|
|
* @param {String} [options.path] - destination file path
|
|
* @param {String} [options.flags] - destination file open flags
|
|
* @param {String} [options.mode] - destination file mode
|
|
* @returns {Pipage} pipeline
|
|
* @example
|
|
* this._createWritePipeline({
|
|
* image: sourceImage,
|
|
* path: '/dev/rdisk2'
|
|
* })
|
|
*/
|
|
_createWritePipeline (options) {
|
|
const pipeline = new Pipage({
|
|
readableObjectMode: true
|
|
})
|
|
|
|
const image = options.image
|
|
const source = image.stream
|
|
const progressOptions = {
|
|
length: image.size.original,
|
|
time: 500
|
|
}
|
|
|
|
let progressStream = null
|
|
|
|
// If the final size is an estimation,
|
|
// use the original source size for progress metering
|
|
if (image.size.final.estimation) {
|
|
progressStream = new ProgressStream(progressOptions)
|
|
pipeline.append(progressStream)
|
|
}
|
|
|
|
const isPassThrough = image.transform instanceof stream.PassThrough
|
|
|
|
// If the image transform is a pass-through,
|
|
// ignore it to save on the overhead
|
|
if (image.transform && !isPassThrough) {
|
|
pipeline.append(image.transform)
|
|
}
|
|
|
|
// If the final size is known precisely and we're not
|
|
// using block maps, then use the final size for progress
|
|
if (!image.size.final.estimation && !image.bmap) {
|
|
progressOptions.length = image.size.final.value
|
|
progressStream = new ProgressStream(progressOptions)
|
|
pipeline.append(progressStream)
|
|
}
|
|
|
|
if (image.bmap) {
|
|
const blockMap = BlockMap.parse(image.bmap)
|
|
debug('write:bmap', blockMap)
|
|
progressStream = new ProgressStream(progressOptions)
|
|
pipeline.append(progressStream)
|
|
pipeline.append(new BlockMap.FilterStream(blockMap))
|
|
} else {
|
|
debug('write:blockstream')
|
|
const checksumStream = new ChecksumStream({
|
|
objectMode: true,
|
|
algorithms: options.checksumAlgorithms
|
|
})
|
|
pipeline.append(new BlockStream())
|
|
pipeline.append(checksumStream)
|
|
pipeline.bind(checksumStream, 'checksum')
|
|
}
|
|
|
|
const target = new BlockWriteStream({
|
|
fd: options.fd,
|
|
path: options.path,
|
|
flags: options.flags,
|
|
mode: options.mode,
|
|
autoClose: false
|
|
})
|
|
|
|
// Pipeline.bind(progressStream, 'progress');
|
|
progressStream.on('progress', (state) => {
|
|
state.device = options.path
|
|
state.type = 'write'
|
|
state.speed = target.speed
|
|
this.emit('progress', state)
|
|
})
|
|
|
|
pipeline.bind(source, 'error')
|
|
pipeline.bind(target, 'error')
|
|
|
|
source.pipe(pipeline)
|
|
.pipe(target)
|
|
|
|
this.source = source
|
|
this.pipeline = pipeline
|
|
this.target = target
|
|
|
|
return pipeline
|
|
}
|
|
|
|
/**
|
|
* @summary Creates a verification pipeline from given options
|
|
* @private
|
|
* @param {Object} options - options
|
|
* @param {Object} options.image - image
|
|
* @param {Number} [options.fd] - file descriptor
|
|
* @param {String} [options.path] - file path
|
|
* @param {String} [options.flags] - file open flags
|
|
* @param {String} [options.mode] - file mode
|
|
* @returns {Pipage} pipeline
|
|
* @example
|
|
* this._createVerifyPipeline({
|
|
* path: '/dev/rdisk2'
|
|
* })
|
|
*/
|
|
_createVerifyPipeline (options) {
|
|
const pipeline = new Pipage()
|
|
|
|
let size = this.bytesWritten
|
|
|
|
if (!options.image.size.final.estimation) {
|
|
size = Math.max(this.bytesWritten, options.image.size.final.value)
|
|
}
|
|
|
|
const progressStream = new ProgressStream({
|
|
length: size,
|
|
time: 500
|
|
})
|
|
|
|
pipeline.append(progressStream)
|
|
|
|
if (options.image.bmap) {
|
|
debug('verify:bmap')
|
|
const blockMap = BlockMap.parse(options.image.bmap)
|
|
const blockMapStream = new BlockMap.FilterStream(blockMap)
|
|
pipeline.append(blockMapStream)
|
|
|
|
// NOTE: Because the blockMapStream checksums each range,
|
|
// and doesn't emit a final "checksum" event, we artificially
|
|
// raise one once the stream finishes
|
|
blockMapStream.once('finish', () => {
|
|
pipeline.emit('checksum', {})
|
|
})
|
|
} else {
|
|
const checksumStream = new ChecksumStream({
|
|
algorithms: options.checksumAlgorithms
|
|
})
|
|
pipeline.append(checksumStream)
|
|
pipeline.bind(checksumStream, 'checksum')
|
|
}
|
|
|
|
const source = new BlockReadStream({
|
|
fd: options.fd,
|
|
path: options.path,
|
|
flags: options.flags,
|
|
mode: options.mode,
|
|
autoClose: false,
|
|
start: 0,
|
|
end: size
|
|
})
|
|
|
|
pipeline.bind(source, 'error')
|
|
|
|
progressStream.on('progress', (state) => {
|
|
state.device = options.path
|
|
state.type = 'check'
|
|
this.emit('progress', state)
|
|
})
|
|
|
|
this.target = null
|
|
this.source = source
|
|
this.pipeline = pipeline
|
|
|
|
source.pipe(pipeline).resume()
|
|
|
|
return pipeline
|
|
}
|
|
}
|
|
|
|
module.exports = ImageWriter
|