diff --git a/lib/cli/unmount.js b/lib/cli/unmount.js new file mode 100644 index 00000000..7dcb0b6e --- /dev/null +++ b/lib/cli/unmount.js @@ -0,0 +1,105 @@ +/* + * 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 childProcess = Bluebird.promisifyAll(require('child_process')); +const os = require('os'); + +/** + * @summary Unmount command templates + * @namespace COMMAND_TEMPLATES + * @private + * + * We make sure that the commands declared here exit + * successfully even if the drive is not mounted. + */ +const COMMAND_TEMPLATES = { + + /** + * @property {String} darwin + * @memberof COMMAND_TEMPLATES + */ + darwin: '/usr/sbin/diskutil unmountDisk force <%= device %>', + + /** + * @property {String} linux + * @memberof COMMAND_TEMPLATES + * + * @description + * If trying to unmount the raw device in Linux, we get: + * > umount: /dev/sdN: not mounted + * Therefore we use the ?* glob to make sure umount processes + * the partitions of sdN independently (even if they contain multiple digits) + * but not the raw device. + * We also redirect stderr to /dev/null to ignore warnings + * if a device is already unmounted. + * Finally, we also wrap the command in a boolean expression + * that always evaluates to true to ignore the return code, + * which is non zero when a device was already unmounted. + */ + linux: 'umount <%= device %>?* 2>/dev/null || /bin/true' + +}; + +/** + * @summary Get UNIX unmount command + * @function + * @public + * + * @param {String} operatingSystem - operating system slug + * @param {Object} drive - drive object + * @returns {String} command + * + * @example + * const drivelist = require('drivelist'); + * const os = require('os'); + * + * drivelist.list((drives) => { + * const command = unmount.getUNIXUnmountCommand(os.platform(), drives[0]); + * }); + */ +exports.getUNIXUnmountCommand = (operatingSystem, drive) => { + return _.template(COMMAND_TEMPLATES[operatingSystem])(drive); +}; + +/** + * @summary Unmount drive + * @function + * @public + * + * @param {Object} drive - drive object + * @returns {Promise} + * + * @example + * const Bluebird = require('bluebird'); + * const drivelist = Bluebird.promisifyAll(require('drivelist')); + * + * drivelist.listAsync().each(unmount.unmountDrive); + */ +exports.unmountDrive = (drive) => { + const platform = os.platform(); + + if (platform === 'win32') { + const removedrive = Bluebird.promisifyAll(require('removedrive')); + return removedrive.ejectAsync(drive.mountpoint); + } + + const command = exports.getUNIXUnmountCommand(platform, drive); + return childProcess.execAsync(command); +}; diff --git a/lib/cli/writer.js b/lib/cli/writer.js index 59959a84..1dde1fe8 100644 --- a/lib/cli/writer.js +++ b/lib/cli/writer.js @@ -20,9 +20,8 @@ const imageWrite = require('etcher-image-write'); const imageStream = require('etcher-image-stream'); const Bluebird = require('bluebird'); const fs = Bluebird.promisifyAll(require('fs')); -const umount = Bluebird.promisifyAll(require('umount')); const os = require('os'); -const isWindows = os.platform() === 'win32'; +const unmount = require('./unmount'); /** * @summary Write an image to a disk drive @@ -56,7 +55,15 @@ const isWindows = os.platform() === 'win32'; * }); */ exports.writeImage = (imagePath, drive, options, onProgress) => { - return umount.umountAsync(drive.device).then(() => { + return Bluebird.try(() => { + + // Unmounting a drive in Windows means we can't write to it anymore + if (os.platform() === 'win32') { + return; + } + + return unmount.unmountDrive(drive); + }).then(() => { return Bluebird.props({ image: imageStream.getFromFilePath(imagePath), driveFileDescriptor: fs.openAsync(drive.raw, 'rs+') @@ -83,14 +90,6 @@ exports.writeImage = (imagePath, drive, options, onProgress) => { return; } - if (isWindows && drive.mountpoint) { - - // The `can-ignore` annotation is EncloseJS (http://enclosejs.com) specific. - const removedrive = Bluebird.promisifyAll(require('removedrive', 'can-ignore')); - - return removedrive.ejectAsync(drive.mountpoint); - } - - return umount.umountAsync(drive.device); + return unmount.unmountDrive(drive); }); }; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 600f35e6..d9327b64 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -5146,18 +5146,6 @@ "from": "umd@>=3.0.0 <4.0.0", "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.1.tgz" }, - "umount": { - "version": "1.1.5", - "from": "umount@1.1.5", - "resolved": "https://registry.npmjs.org/umount/-/umount-1.1.5.tgz", - "dependencies": { - "lodash": { - "version": "3.10.1", - "from": "lodash@>=3.7.0 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz" - } - } - }, "unbzip2-stream": { "version": "1.0.10", "from": "unbzip2-stream@>=1.0.10 <2.0.0", diff --git a/package.json b/package.json index 5db28bb1..859ce937 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "url": "git@github.com:resin-io/etcher.git" }, "scripts": { - "test": "npm run lint && electron-mocha --recursive --renderer tests/gui -R min", + "test": "npm run lint && electron-mocha --recursive --renderer tests/gui -R min && electron-mocha --recursive tests/cli -R min", "sass": "node-sass ./lib/gui/scss/main.scss > build/css/main.css", "jslint": "eslint lib tests scripts bin versionist.conf.js", "scsslint": "scss-lint lib/gui/scss", @@ -88,7 +88,6 @@ "sudo-prompt": "^6.1.0", "tail": "^1.1.0", "trackjs": "^2.1.16", - "umount": "^1.1.5", "username": "^2.1.0", "yargs": "^4.6.0" }, diff --git a/tests/cli/unmount.spec.js b/tests/cli/unmount.spec.js new file mode 100644 index 00000000..c02065e2 --- /dev/null +++ b/tests/cli/unmount.spec.js @@ -0,0 +1,64 @@ +/* + * 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 m = require('mochainon'); +const unmount = require('../../lib/cli/unmount'); + +describe('CLI: Unmount', function() { + + describe('.getUNIXUnmountCommand()', function() { + + it('should return the correct command for OS X', function() { + const command = unmount.getUNIXUnmountCommand('darwin', { + device: '/dev/disk2', + description: 'DataTraveler 2.0', + size: 7823458304, + mountpoints: [ + { + path: '/Volumes/UNTITLED' + } + ], + raw: '/dev/rdisk2', + protected: false, + system: false + }); + + m.chai.expect(command).to.equal('/usr/sbin/diskutil unmountDisk force /dev/disk2'); + }); + + it('should return the correct command for GNU/Linux', function() { + const command = unmount.getUNIXUnmountCommand('linux', { + device: '/dev/sda', + description: 'DataTraveler 2.0', + size: 7823458304, + mountpoints: [ + { + path: '/media/UNTITLED' + } + ], + raw: '/dev/sda', + protected: false, + system: false + }); + + m.chai.expect(command).to.equal('umount /dev/sda?* 2>/dev/null || /bin/true'); + }); + + }); + +});