etcher/lib/cli/writer.js
Juan Cruz Viotti 2291321b46 refactor(GUI): remove the intermediate child writer proxy process (#1910)
Etcher currently elevates a child writer proxy that itself spawns the
Etcher CLI in robot mode, parses the output, and proxies those messages
to the GUI application over IPC.

After these set of changes, Etcher elevates a single child writer
process that directly communicates back with the GUI using IPC. The main
purpose behind these changes is to simplify the overall architecture and
fix various issues caused by the current complex child process tree.

Here's a summary of the changes:

- Stop wrapping the Etcher CLI to perform writing
- Remove the robot option from the Etcher CLI (along with related
  documentation)
- Elevate a new `child-write.js` standalone executable
- Move the relevant bits of `lib/child-writer` to the `image-writer` GUI
  module
- Remove the `lib/child-writer` directory
- Add a new "Child died unexpectedly" Mixpanel event
- Floor state percentage in the flash state model

The above changes made is possible to tackle all the remaining issues
where the writer process would remain alive even if the parent died.

Change-Type: patch
Changelog-Entry: Ensure the writer process dies when the GUI application is killed.
See: https://github.com/resin-io/etcher/pull/1873
See: https://github.com/resin-io/etcher/pull/1843
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
2018-01-04 20:46:09 +01:00

148 lines
4.9 KiB
JavaScript

/*
* Copyright 2016 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')
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('../image-stream')
const errors = require('../shared/errors')
const constraints = require('../shared/drive-constraints')
const ImageWriter = require('../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
* @function
* @public
*
* @param {String} imagePath - path to image
* @param {String} drive - drive
* @param {Object} options - options
* @param {Boolean} [options.unmountOnSuccess=false] - unmount on success
* @param {Boolean} [options.validateWriteOnSuccess=false] - validate write on success
* @param {Function} onProgress - on progress callback (state)
*
* @fulfil {Boolean} - whether the operation was successful
* @returns {Promise}
*
* @example
* writer.writeImage('path/to/image.img', '/dev/disk2', {
* unmountOnSuccess: true,
* validateWriteOnSuccess: true
* }, (state) => {
* console.log(state.percentage);
* }).then(() => {
* console.log('Done!');
* });
*/
exports.writeImage = (imagePath, drive, options, onProgress) => {
return drivelist.listAsync().then((drives) => {
const selectedDrive = _.find(drives, {
device: drive
})
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_EXCL |
fs.constants.O_NONBLOCK |
fs.constants.O_SYNC |
fs.constants.O_DIRECT
/* 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)
})
})
})
})
}