Merge pull request #2080 from resin-io/sdk-write-prepare

feat(sdk): Move CLI writer logic into SDK writer
This commit is contained in:
Jonas Hermsmeier 2018-02-23 10:26:36 -08:00 committed by GitHub
commit d233558b44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 287 additions and 118 deletions

View File

@ -16,24 +16,8 @@
'use strict'
const _ = require('lodash')
const Bluebird = require('bluebird')
const fs = Bluebird.promisifyAll(require('fs'))
const mountutils = Bluebird.promisifyAll(require('mountutils'))
const drivelist = Bluebird.promisifyAll(require('drivelist'))
const os = require('os')
const imageStream = require('../sdk/image-stream')
const errors = require('../shared/errors')
const constraints = require('../shared/drive-constraints')
const ImageWriter = require('../sdk/writer')
const diskpart = require('./diskpart')
/**
* @summary Timeout, in milliseconds, to wait before unmounting on success
* @constant
* @type {Number}
*/
const UNMOUNT_ON_SUCCESS_TIMEOUT_MS = 2000
/**
* @summary Write an image to a disk drive
@ -61,85 +45,18 @@ const UNMOUNT_ON_SUCCESS_TIMEOUT_MS = 2000
* });
*/
exports.writeImage = (imagePath, drive, options, onProgress) => {
return drivelist.listAsync().then((drives) => {
const selectedDrive = _.find(drives, {
device: drive
})
const writer = new ImageWriter({
path: drive,
imagePath,
verify: options.validateWriteOnSuccess,
checksumAlgorithms: [ 'crc32' ],
unmountOnSuccess: options.unmountOnSuccess
})
if (!selectedDrive) {
throw errors.createUserError({
title: 'The selected drive was not found',
description: `We can't find ${drive} in your system. Did you unplug the drive?`,
code: 'EUNPLUGGED'
})
}
return selectedDrive
}).then((driveObject) => {
return Bluebird.try(() => {
// Unmounting a drive in Windows means we can't write to it anymore
if (os.platform() === 'win32') {
return Bluebird.resolve()
}
return mountutils.unmountDiskAsync(driveObject.device)
}).then(() => {
return diskpart.clean(driveObject.device)
}).then(() => {
/* eslint-disable no-bitwise */
const flags = fs.constants.O_RDWR |
fs.constants.O_NONBLOCK |
fs.constants.O_SYNC
/* eslint-enable no-bitwise */
return fs.openAsync(driveObject.raw, flags)
}).then((driveFileDescriptor) => {
return imageStream.getFromFilePath(imagePath).then((image) => {
if (!constraints.isDriveLargeEnough(driveObject, image)) {
throw errors.createUserError({
title: 'The image you selected is too big for this drive',
description: 'Please connect a bigger drive and try again'
})
}
const writer = new ImageWriter({
image,
fd: driveFileDescriptor,
path: driveObject.raw,
verify: options.validateWriteOnSuccess,
checksumAlgorithms: [ 'crc32' ]
})
return writer.write()
}).then((writer) => {
return new Bluebird((resolve, reject) => {
writer.on('progress', onProgress)
writer.on('error', reject)
writer.on('finish', (results) => {
results.drive = driveObject
resolve(results)
})
})
}).tap(() => {
// Make sure the device stream file descriptor is closed
// before returning control the the caller. Not closing
// the file descriptor (and waiting for it) results in
// `EBUSY` errors when attempting to unmount the drive
// right afterwards in some Windows 7 systems.
return fs.closeAsync(driveFileDescriptor).then(() => {
if (!options.unmountOnSuccess) {
return Bluebird.resolve()
}
// Closing a file descriptor on a drive containing mountable
// partitions causes macOS to mount the drive. If we try to
// unmount to quickly, then the drive might get re-mounted
// right afterwards.
return Bluebird.delay(UNMOUNT_ON_SUCCESS_TIMEOUT_MS)
.return(driveObject.device)
.then(mountutils.unmountDiskAsync)
})
})
})
return new Bluebird((resolve, reject) => {
writer.flash()
.on('error', reject)
.on('progress', onProgress)
.on('finish', resolve)
})
}

View File

@ -16,6 +16,11 @@
'use strict'
const os = require('os')
const fs = require('fs')
const EventEmitter = require('events').EventEmitter
const mountutils = require('mountutils')
const drivelist = require('drivelist')
const stream = require('readable-stream')
const Pipage = require('pipage')
const BlockMap = require('blockmap')
@ -24,10 +29,54 @@ const BlockWriteStream = require('./block-write-stream')
const BlockReadStream = require('./block-read-stream')
const ChecksumStream = require('./checksum-stream')
const ProgressStream = require('./progress-stream')
const imageStream = require('../image-stream')
const diskpart = require('../../cli/diskpart')
const constraints = require('../../shared/drive-constraints')
const errors = require('../../shared/errors')
const debug = require('debug')('etcher:writer')
const EventEmitter = require('events').EventEmitter
const _ = require('lodash')
/* eslint-disable prefer-reflect */
/**
* @summary Timeout, in milliseconds, to wait before unmounting on success
* @constant
* @type {Number}
*/
const UNMOUNT_ON_SUCCESS_TIMEOUT_MS = 2000
/**
* @summary Helper function to run a set of async tasks in sequence
* @private
* @param {Array<Function>} tasks - set of tasks
* @param {Function} callback - callback(error)
* @example
* runSeries([
* (next) => first(next),
* (next) => second(next),
* ], (error) => {
* // ...
* })
*/
const runSeries = (tasks, callback) => {
/**
* @summary Task runner
* @param {Error} [error] - error
* @example
* run()
*/
const run = (error) => {
const task = tasks.shift()
if (error || task == null) {
callback(error)
return
}
task(run)
}
run()
}
/**
* @summary ImageWriter class
* @class
@ -36,6 +85,11 @@ class ImageWriter extends EventEmitter {
/**
* @summary ImageWriter constructor
* @param {Object} options - options
* @param {String} options.imagePath - disk image path
* @param {String} options.path - dest path
* @param {Boolean} options.verify - whether to verify the dest
* @param {Boolean} options.unmountOnSuccess - whether to unmount the dest after flashing
* @param {Array<String>} options.checksumAlgorithms - checksums to calculate
* @example
* new ImageWriter(options)
*/
@ -48,13 +102,187 @@ class ImageWriter extends EventEmitter {
this.pipeline = null
this.target = null
this.hadError = false
this.bytesRead = 0
this.bytesWritten = 0
this.checksum = {}
}
/**
* @summary Verify that the selected destination device exists
* @param {Function} callback - callback(error)
* @private
* @example
* writer.checkSelectedDevice((error) => {
* // ...
* })
*/
checkSelectedDevice (callback) {
debug('state:device-select', this.options.path)
this.destinationDevice = null
drivelist.list((error, drives) => {
debug('state:device-select', this.options.path, error ? 'NOT OK' : 'OK')
if (error) {
callback.call(this, error)
return
}
const selectedDrive = _.find(drives, {
device: this.options.path
})
if (!selectedDrive) {
const selectionError = errors.createUserError({
title: 'The selected drive was not found',
description: `We can't find ${this.options.path} in your system. Did you unplug the drive?`,
code: 'EUNPLUGGED'
})
debug('state:device-select', this.options.path, 'NOT OK')
callback.call(this, selectionError)
return
}
this.destinationDevice = selectedDrive
callback.call(this)
})
}
/**
* @summary Unmount the destination device
* @param {Function} callback - callback(error)
* @private
* @example
* writer.unmountDevice((error) => {
* // ...
* })
*/
unmountDevice (callback) {
if (os.platform() === 'win32') {
callback.call(this)
return
}
debug('state:unmount', this.destinationDevice.device)
mountutils.unmountDisk(this.destinationDevice.device, (error) => {
debug('state:unmount', this.destinationDevice.device, error ? 'NOT OK' : 'OK')
callback.call(this, error)
})
}
/**
* @summary Clean a device's partition table
* @param {Function} callback - callback(error)
* @private
* @example
* writer.removePartitionTable((error) => {
* // ...
* })
*/
removePartitionTable (callback) {
if (os.platform() !== 'win32') {
callback.call(this)
return
}
debug('state:clean', this.destinationDevice.device)
diskpart.clean(this.destinationDevice.device).asCallback((error) => {
debug('state:clean', this.destinationDevice.device, error ? 'NOT OK' : 'OK')
callback.call(this, error)
})
}
/**
* @summary Open the source for reading
* @param {Function} callback - callback(error)
* @private
* @example
* writer.openSource((error) => {
* // ...
* })
*/
openSource (callback) {
debug('state:source-open', this.options.imagePath)
imageStream.getFromFilePath(this.options.imagePath).asCallback((error, image) => {
debug('state:source-open', this.options.imagePath, error ? 'NOT OK' : 'OK')
if (error) {
callback.call(this, error)
return
}
if (!constraints.isDriveLargeEnough(this.destinationDevice, image)) {
const driveError = errors.createUserError({
title: 'The image you selected is too big for this drive',
description: 'Please connect a bigger drive and try again'
})
debug('state:source-open', this.options.imagePath, 'NOT OK')
callback.call(this, driveError)
return
}
this.options.image = image
callback.call(this)
})
}
/**
* @summary Open the destination for writing
* @param {Function} callback - callback(error)
* @private
* @example
* writer.openDestination((error) => {
* // ...
* })
*/
openDestination (callback) {
debug('state:destination-open', this.destinationDevice.raw)
/* eslint-disable no-bitwise */
const flags = fs.constants.O_RDWR |
fs.constants.O_NONBLOCK |
fs.constants.O_SYNC
/* eslint-enable no-bitwise */
fs.open(this.destinationDevice.raw, flags, (error, fd) => {
debug('state:destination-open', this.destinationDevice.raw, error ? 'NOT OK' : 'OK')
this.options.fd = fd
callback.call(this, error)
})
}
/**
* @summary Start the flashing process
* @returns {ImageWriter} imageWriter
* @example
* imageWriter.flash()
* .on('error', reject)
* .on('progress', onProgress)
* .on('finish', resolve)
*/
flash () {
const tasks = [
(next) => { this.checkSelectedDevice(next) },
(next) => { this.unmountDevice(next) },
(next) => { this.removePartitionTable(next) },
(next) => { this.openSource(next) },
(next) => { this.openDestination(next) }
]
runSeries(tasks, (error) => {
if (error) {
this.emit('error')
return
}
this.write()
})
return this
}
/**
* @summary Start the writing process
* @returns {ImageWriter} imageWriter
@ -62,15 +290,12 @@ class ImageWriter extends EventEmitter {
* 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)
})
@ -80,7 +305,7 @@ class ImageWriter extends EventEmitter {
if (this.options.verify) {
this.verify()
} else {
this._emitFinish()
this._finish()
}
})
@ -96,7 +321,6 @@ class ImageWriter extends EventEmitter {
verify () {
this._createVerifyPipeline(this.options)
.on('error', (error) => {
this.hadError = true
this.emit('error', error)
})
.on('checksum', (checksum) => {
@ -106,13 +330,13 @@ class ImageWriter extends EventEmitter {
error.code = 'EVALIDATION'
this.emit('error', error)
}
this._emitFinish()
this._finish()
})
.on('finish', () => {
debug('verify:end')
// NOTE: As the 'checksum' event only happens after
// the 'finish' event, we `._emitFinish()` there instead of here
// the 'finish' event, we `._finish()` there instead of here
})
return this
@ -125,22 +349,56 @@ class ImageWriter extends EventEmitter {
*/
abort () {
if (this.source) {
this.emit('abort')
this.source.destroy()
}
this.emit('abort')
}
/**
* @summary Cleanup after writing; close file descriptors & unmount
* @param {Function} callback - callback(error)
* @private
* @example
* writer._cleanup((error) => {
* // ...
* })
*/
_cleanup (callback) {
debug('state:cleanup')
fs.close(this.options.fd, (closeError) => {
debug('state:cleanup', closeError ? 'NOT OK' : 'OK')
if (!this.options.unmountOnSuccess) {
callback.call(this, closeError)
return
}
// Closing a file descriptor on a drive containing mountable
// partitions causes macOS to mount the drive. If we try to
// unmount too quickly, then the drive might get re-mounted
// right afterwards.
setTimeout(() => {
mountutils.unmountDisk(this.destinationDevice.device, (error) => {
debug('state:cleanup', error ? 'NOT OK' : 'OK')
callback.call(this, error)
})
}, UNMOUNT_ON_SUCCESS_TIMEOUT_MS)
})
}
/**
* @summary Emits the `finish` event with state metadata
* @private
* @example
* this._emitFinish()
* this._finish()
*/
_emitFinish () {
this.emit('finish', {
bytesRead: this.bytesRead,
bytesWritten: this.bytesWritten,
checksum: this.checksum
_finish () {
this._cleanup(() => {
this.emit('finish', {
drive: this.destinationDevice,
bytesRead: this.bytesRead,
bytesWritten: this.bytesWritten,
checksum: this.checksum
})
})
}
@ -216,9 +474,6 @@ class ImageWriter extends EventEmitter {
const target = new BlockWriteStream({
fd: options.fd,
path: options.path,
flags: options.flags,
mode: options.mode,
autoClose: false
})
@ -296,9 +551,6 @@ class ImageWriter extends EventEmitter {
const source = new BlockReadStream({
fd: options.fd,
path: options.path,
flags: options.flags,
mode: options.mode,
autoClose: false,
start: 0,
end: size