mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 07:17:18 +00:00
Merge pull request #2080 from resin-io/sdk-write-prepare
feat(sdk): Move CLI writer logic into SDK writer
This commit is contained in:
commit
d233558b44
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user