fix(GUI): emit progress back to parent using node-ipc (#774)

This PR makes use of `node-ipc` to emit progress information from the
child CLI to the GUI process rather than the tailing approach we've
been doing until now.

This change was motivated by the following Electron fix which landed in
v1.4.4: https://github.com/electron/electron/pull/7578. Before such fix,
Electron would not output anything to stdout/stderr if the Electron
process was running with `ELECTRON_RUN_AS_NODE` under Windows, which
forced us to implement `--log` option in the Etcher CLI to output state
information to a file.

Since this issue is fixed, we can consume the Etcher CLI output from
within `child_process.spawn`, which opens more interesting possibilities
for sharing information between both processes.

This coindentally fixes a Windows issue where the tailing module would
receive malformed JSON, causing Etcher to crash at `JSON.parse`. The
reason of this problem was a bug in the tailing module we were using.

Fixes: https://github.com/resin-io/etcher/issues/642
Change-Type: patch
Changelog-Entry: Fix "Unexpected end of JSON" error in Windows.
Signed-off-by: Juan Cruz Viotti <jviotti@openmailbox.org>
This commit is contained in:
Juan Cruz Viotti 2016-10-25 23:56:39 -04:00 committed by GitHub
parent 4d3eab4915
commit d9822faf2f
12 changed files with 188 additions and 300 deletions

View File

@ -38,7 +38,6 @@ Options
--drive, -d drive
--check, -c validate write
--robot, -r parse-able output without interactivity
--log, -l output log file
--yes, -y confirm non-interactively
--unmount, -u unmount on success
```

View File

@ -80,14 +80,6 @@ module.exports = yargs
return true;
})
.check((argv) => {
if (argv.log && !argv.robot) {
throw new Error('The `--log` option requires `--robot`');
}
return true;
})
.options({
help: {
describe: 'show help',
@ -115,11 +107,6 @@ module.exports = yargs
boolean: true,
alias: 'r'
},
log: {
describe: 'output log file',
string: true,
alias: 'l'
},
yes: {
describe: 'confirm non-interactively',
boolean: true,

View File

@ -24,7 +24,6 @@ const drivelist = Bluebird.promisifyAll(require('drivelist'));
const writer = require('./writer');
const utils = require('./utils');
const options = require('./cli');
const log = require('./log');
const EXIT_CODES = require('../src/exit-codes');
form.run([
@ -74,7 +73,7 @@ form.run([
}, (state) => {
if (options.robot) {
log.toStdout(JSON.stringify({
console.log(JSON.stringify({
command: 'progress',
data: {
type: state.type,
@ -93,7 +92,7 @@ form.run([
return Bluebird.try(() => {
if (options.robot) {
return log.toStdout(JSON.stringify({
return console.log(JSON.stringify({
command: 'done',
data: {
sourceChecksum: results.sourceChecksum
@ -115,11 +114,12 @@ form.run([
return Bluebird.try(() => {
if (options.robot) {
return log.toStderr(JSON.stringify({
return console.error(JSON.stringify({
command: 'error',
data: {
message: error.message,
description: error.description,
stacktrace: error.stack,
code: error.code
}
}));
@ -134,4 +134,4 @@ form.run([
process.exit(EXIT_CODES.GENERAL_ERROR);
});
}).finally(log.close);
});

View File

@ -1,106 +0,0 @@
/*
* 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 Bluebird = require('bluebird');
const fs = require('fs');
const options = require('./cli');
const logStream = options.log ? fs.createWriteStream(options.log) : null;
const STDOUT_STREAM = logStream || process.stdout;
const STDERR_STREAM = logStream || process.stderr;
/**
* The purpose of this module is to workaround an Electron Windows issue
* where an Electron process with `ELECTRON_RUN_AS_NODE` enabled will
* not attach `stdout`/`stderr` to the process, therefore not allowing
* us to redirect output to a file from `child_process` or console pipes.
*
* A temporary solution is to implement logic that redirects output
* to a log file in the Etcher CLI.
*
* TODO: Delete this file, and the corresponding `--log` option once
* this issue is fixed in Electron.
* See: https://github.com/electron/electron/issues/5715
*/
/**
* @summary Write a line to stdout
* @function
* @public
*
* @description
* If the `--log` option was passed, this function writes the line
* to it, otherwise to `process.stdout`.
*
* @param {String} line - line
* @returns {Promise}
*
* @example
* log.toStdout('Hello world!');
*/
exports.toStdout = (line) => {
return new Bluebird((resolve) => {
STDOUT_STREAM.write(line + '\n', () => {
return resolve();
});
});
};
/**
* @summary Write a line to stderr
* @function
* @public
*
* @description
* If the `--log` option was passed, this function writes the line
* to it, otherwise to `process.stderr`.
*
* @param {String} line - line
* @returns {Promise}
*
* @example
* log.toStderr('Hello world!');
*/
exports.toStderr = (line) => {
return new Bluebird((resolve) => {
STDERR_STREAM.write(line + '\n', () => {
return resolve();
});
});
};
/**
* @summary Close any used streams, if needed
* @function
* @public
*
* @returns {Promise}
*
* @example
* log.close();
*/
exports.close = () => {
return new Bluebird((resolve) => {
if (!logStream) {
return resolve();
}
logStream.close(() => {
return resolve();
});
});
};

View File

@ -63,6 +63,20 @@ const app = angular.module('Etcher', [
require('./utils/manifest-bind/manifest-bind')
]);
app.run(() => {
console.log([
' _____ _ _',
'| ___| | | |',
'| |__ | |_ ___| |__ ___ _ __',
'| __|| __/ __| \'_ \\ / _ \\ \'__|',
'| |___| || (__| | | | __/ |',
'\\____/ \\__\\___|_| |_|\\___|_|',
'',
'Interested in joining the Etcher team?',
'Drop us a line at jobs@resin.io'
].join('\n'));
});
app.run((AnalyticsService, UpdateNotifierService, SelectionStateModel) => {
AnalyticsService.logEvent('Application start');

View File

@ -1,35 +1,69 @@
# Etcher Child Writer
Etcher Child Writer
===================
This module is in charge of dealing with the gory details of elevating and managing the child writer process. As a word of warning, it contains tons of workarounds and "hacks" to deal with platform differences, packaging, and inter-process communication. This empowers us to write this small guide to explain how it works in a more high level manner, hoping to make it easier to grok for contributors.
This module is in charge of dealing with the gory details of elevating and
managing the child writer process. As a word of warning, it contains tons of
workarounds and "hacks" to deal with platform differences, packaging, and
inter-process communication. This empowers us to write this small guide to
explain how it works in a more high level manner, hoping to make it easier to
grok for contributors.
## The problem
The problem
-----------
Elevating a forked process is an easy task. Thanks to the widely available NPM modules to display nice GUI prompt dialogs, elevation is just a matter of executing the process with one of those modules instead of with `child_process` directly.
Elevating a forked process is an easy task. Thanks to the widely available NPM
modules to display nice GUI prompt dialogs, elevation is just a matter of
executing the process with one of those modules instead of with `child_process`
directly.
The main problems we faced are:
- The modules that implement elevation provide "execution" support, but don't allow us to fork/spawn the process and consume its `stdout` and `stderr` in a stream fashion. This also means that we can't use the nice `process.send` IPC communication channel directly that `child_process.fork` gives us to send messages back to the parent.
- The modules that implement elevation provide "execution" support, but don't
allow us to fork/spawn the process and consume its `stdout` and `stderr` in a
stream fashion. This also means that we can't use the nice `process.send` IPC
communication channel directly that `child_process.fork` gives us to send
messages back to the parent.
- Since we can't assume anything from the environment Etcher is running on, we must make use of the same application entry point to execute both the GUI and the CLI code, which starts to get messy once we throw `asar` packaging into the mix.
- Since we can't assume anything from the environment Etcher is running on, we
must make use of the same application entry point to execute both the GUI and
the CLI code, which starts to get messy once we throw `asar` packaging into
the mix.
- Each elevation mechanism has its quirks, mainly on GNU/Linux. Making sure that the forked process was elevated correctly and could work without issues required various workarounds targeting `pkexec` or `kdesudo`.
- Each elevation mechanism has its quirks, mainly on GNU/Linux. Making sure
that the forked process was elevated correctly and could work without issues
required various workarounds targeting `pkexec` or `kdesudo`.
## How it works
How it works
------------
The Etcher binary runs in CLI or GUI mode depending on an environment variable called `ELECTRON_RUN_AS_NODE`. When this variable is set, it instructs Electron to run as a normal NodeJS process (without Chromium, etc), but still keep any patches applied by Electron, like `asar` support.
The Etcher binary runs in CLI or GUI mode depending on an environment variable
called `ELECTRON_RUN_AS_NODE`. When this variable is set, it instructs Electron
to run as a normal NodeJS process (without Chromium, etc), but still keep any
patches applied by Electron, like `asar` support.
When the Etcher GUI is ran, and the user presses the "Flash!" button, it passes all the required information, like the image path, the device path, current settings, etc; to the child writer module, which based on this information computes an array of arguments, including an option called `--robot`, which can be passed to the Etcher CLI to perform the writing as the user instructed. The `--robot` option basically tells the Etcher CLI to output state information in a way that can be very easily parsed by the parent process.
When the Etcher GUI is ran, and the user presses the "Flash!" button, the GUI
creates an IPC server, and forks a process called the "writer proxy", passing
it all the required information to perform the flashing, such as the image
path, the device path, the current settings, etc.
Afterwards, the child writer entry point *forks* yet another script passing all the command line arguments collected so far and blindly proxies all IPC messages from its child to the parent, the GUI process.
The writer proxy then checks if its currently elevated, and if not, prompts the
user for elevation and re-spawns itself.
Once the new script starts, it creates a temporary log file, starts tailing it, parses what it gets, and sends any data to its parent using IPC messages (which are proxied to the GUI). After all is set, the script checks if its currently elevated, and if not, prompts the user for his password and re-executes itself, while keeping the original process alive and tailing.
Once the writer proxy has enough permissions to directly access devices, it
spawns the Etcher CLI passing the `--robot` option along with all the
information gathered before. The `--robot` option basically tells the Etcher
CLI to output state information in a way that can be very easily parsed by the
parent process.
Since elevation is done by executing, not forking, we lose IPC communication at this point. Since the non-elevated version of the script is alive, we can go ahead and spawn the Etcher CLI with all the arguments computed before, and redirect `stdout` to the temporary log file.
The output of the Etcher CLI is then sent to the IPC server that was opened by
the GUI, which nicely displays them in the progress bar the user sees.
The non-elevated process receives the `--robot` output by tailing the temporary file, parses each line and sends back the results to the GUI, which nicely displays them in the progress bar the user sees.
Summary
-------
## Summary
There are lots of details we're omitting for the sake of clarity. Feel free to
dive in inside the child writer code, which is heavily commented to explain the
reasons behind each decision or workaround.
There are lots of details we're omitting for the sake of clarity. Feel free to dive in inside the child writer code, which is heavily commented to explain the reasons behind each decision or workaround.
Don't hesitate in getting in touch if you have any suggestion, or just want to know more!
Don't hesitate in getting in touch if you have any suggestion, or just want to
know more!

View File

@ -35,12 +35,6 @@ module.exports = {
* @property {String} WRITER_PROXY_SCRIPT
* @memberof CONSTANTS
*/
WRITER_PROXY_SCRIPT: path.join(__dirname, 'writer-proxy.js'),
/**
* @property {String} TEMPORARY_LOG_FILE_ENVIRONMENT_VARIABLE
* @memberof CONSTANTS
*/
TEMPORARY_LOG_FILE_ENVIRONMENT_VARIABLE: 'ETCHER_INTERNAL_LOG_FILE'
WRITER_PROXY_SCRIPT: path.join(__dirname, 'writer-proxy.js')
};

View File

@ -18,6 +18,7 @@
const EventEmitter = require('events').EventEmitter;
const childProcess = require('child_process');
const ipc = require('node-ipc');
const rendererUtils = require('./renderer-utils');
const utils = require('./utils');
const CONSTANTS = require('./constants');
@ -56,20 +57,55 @@ const EXIT_CODES = require('../exit-codes');
exports.write = (image, drive, options) => {
const emitter = new EventEmitter();
utils.getTemporaryLogFilePath().then((logFile) => {
const argv = utils.getCLIWriterArguments({
entryPoint: rendererUtils.getApplicationEntryPoint(),
logFile: logFile,
image: image,
device: drive.device,
validateWriteOnSuccess: options.validateWriteOnSuccess,
unmountOnSuccess: options.unmountOnSuccess
});
const argv = utils.getCLIWriterArguments({
entryPoint: rendererUtils.getApplicationEntryPoint(),
image: image,
device: drive.device,
validateWriteOnSuccess: options.validateWriteOnSuccess,
unmountOnSuccess: options.unmountOnSuccess
});
// Make writer proxy inherit the temporary log file location
// while keeping current environment variables intact.
process.env[CONSTANTS.TEMPORARY_LOG_FILE_ENVIRONMENT_VARIABLE] = logFile;
// There might be multiple Etcher instances running at
// the same time, therefore we must ensure each IPC
// server/client has a different name.
process.env.IPC_SERVER_ID = `etcher-server-${process.pid}`;
process.env.IPC_CLIENT_ID = `etcher-client-${process.pid}`;
ipc.config.id = process.env.IPC_SERVER_ID;
ipc.config.silent = true;
ipc.serve();
ipc.server.on('error', (error) => {
emitter.emit('error', error);
});
ipc.server.on('message', (data) => {
let message;
try {
message = JSON.parse(data);
} catch (error) {
return emitter.emit('error', new Error(`Invalid message: ${data}`));
}
if (!message.command || !message.data) {
return emitter.emit('error', new Error(`Invalid message: ${data}`));
}
// The error object is decomposed by the CLI for serialisation
// purposes. We compose it back to an `Error` here in order
// to provide better encapsulation.
if (message.command === 'error') {
const error = new Error(message.data.message);
error.code = message.data.code;
error.description = message.data.description;
error.stack = message.data.stacktrace;
return emitter.emit('error', error);
}
emitter.emit(message.command, message.data);
});
ipc.server.on('start', () => {
const child = childProcess.fork(CONSTANTS.WRITER_PROXY_SCRIPT, argv, {
silent: true,
env: process.env
@ -83,21 +119,6 @@ exports.write = (image, drive, options) => {
emitter.emit('error', new Error(data.toString()));
});
child.on('message', (message) => {
// The error object is decomposed by the CLI for serialisation
// purposes. We compose it back to an `Error` here in order
// to provide better encapsulation.
if (message.command === 'error') {
const error = new Error(message.data.message);
error.code = message.data.code;
error.description = message.data.description;
return emitter.emit('error', error);
}
emitter.emit(message.command, message.data);
});
child.on('error', (error) => {
emitter.emit('error', error);
});
@ -115,5 +136,7 @@ exports.write = (image, drive, options) => {
});
});
ipc.server.start();
return emitter;
};

View File

@ -18,9 +18,6 @@
const _ = require('lodash');
const os = require('os');
const Bluebird = require('bluebird');
const tmp = Bluebird.promisifyAll(require('tmp'));
const packageJSON = require('../../../package.json');
/**
* @summary Get the explicit boolean form of an argument
@ -59,7 +56,6 @@ exports.getBooleanArgumentForm = (argumentName, value) => {
* @param {String} options.image - image
* @param {String} options.device - device
* @param {String} options.entryPoint - entry point
* @param {String} [options.logFile] - log file
* @param {Boolean} [options.validateWriteOnSuccess] - validate write on success
* @param {Boolean} [options.unmountOnSuccess] - unmount on success
* @returns {String[]} arguments
@ -100,13 +96,6 @@ exports.getCLIWriterArguments = (options) => {
];
// This temporarily workarounds a Windows Electron issue where
// we can't pipe `stdout`/`stderr` with system pipes.
// See: https://github.com/electron/electron/issues/5715
if (options.logFile) {
argv.push('--log', options.logFile);
}
return argv;
};
@ -126,23 +115,3 @@ exports.escapeWhiteSpacesFromArguments = (argv) => {
return argument.replace(/\s/g, '\\ ');
});
};
/**
* @summary Get a temporary log file path
* @function
* @public
*
* @fulfil {String} - file path
* @returns {Promise}
*
* @example
* utils.getTemporaryLogFilePath().then((filePath) => {
* console.log(filePath);
* });
*/
exports.getTemporaryLogFilePath = () => {
return tmp.fileAsync({
prefix: `${packageJSON.name}-`,
postfix: '.log'
});
};

View File

@ -19,14 +19,12 @@
const Bluebird = require('bluebird');
const childProcess = require('child_process');
const isElevated = Bluebird.promisify(require('is-elevated'));
const ipc = require('node-ipc');
const _ = require('lodash');
const os = require('os');
const Tail = require('tail').Tail;
const fileTail = require('file-tail');
const sudoPrompt = Bluebird.promisifyAll(require('sudo-prompt'));
const EXIT_CODES = require('../exit-codes');
const packageJSON = require('../../../package.json');
const CONSTANTS = require('./constants');
const utils = require('./utils');
// This script is in charge of spawning the writer process and
@ -35,47 +33,13 @@ const utils = require('./utils');
// modules don't work in a spawn/fork fashion.
//
// This script spawns the writer process and redirects its `stdout`
// to a temporary 'log file', which is tailed by this same script.
// The output is then parsed, and sent as IPC messages to the
// parent process, taking care of the writer elevation as needed.
// and `stderr` to the parent process using IPC communication,
// taking care of the writer elevation as needed.
const EXECUTABLE = process.argv[0];
const ETCHER_ARGUMENTS = process.argv.slice(2);
return isElevated().then((elevated) => {
const logFile = process.env[CONSTANTS.TEMPORARY_LOG_FILE_ENVIRONMENT_VARIABLE];
if (process.send) {
console.log(`Tailing ${logFile}`);
// Sadly, `fs.createReadStream()` won't work since
// the stream that function returns gets closed
// when it initially reaches EOF, instead of waiting
// for the file to receive more data.
const tail = _.attempt(() => {
if (os.platform() === 'win32') {
// This tail module overcomes various Windows
// issues by not using `fs.watch()`, but doesn't
// work 100% as expected on other operating systems,
// therefore we only use it on Windows.
return fileTail.startTailing(logFile);
}
return new Tail(logFile);
});
tail.on('error', (error) => {
console.error(error);
process.exit(1);
});
tail.on('line', (line) => {
process.send(JSON.parse(line));
});
}
if (!elevated) {
console.log('Attempting to elevate');
@ -88,7 +52,10 @@ return isElevated().then((elevated) => {
'ELECTRON_RUN_AS_NODE=1',
'&&',
'set',
`${CONSTANTS.TEMPORARY_LOG_FILE_ENVIRONMENT_VARIABLE}=${logFile}`,
`IPC_SERVER_ID=${process.env.IPC_SERVER_ID}`,
'&&',
'set',
`IPC_CLIENT_ID=${process.env.IPC_CLIENT_ID}`,
'&&',
// This is a trick to make the binary afterwards catch
@ -116,7 +83,8 @@ return isElevated().then((elevated) => {
// in are manually inherited.
'env',
'ELECTRON_RUN_AS_NODE=1',
`${CONSTANTS.TEMPORARY_LOG_FILE_ENVIRONMENT_VARIABLE}=${logFile}`
`IPC_SERVER_ID=${process.env.IPC_SERVER_ID}`,
`IPC_CLIENT_ID=${process.env.IPC_CLIENT_ID}`
];
@ -197,18 +165,23 @@ return isElevated().then((elevated) => {
console.log('Re-spawning with elevation');
return new Bluebird((resolve, reject) => {
const child = childProcess.spawn(EXECUTABLE, ETCHER_ARGUMENTS);
ipc.config.id = process.env.IPC_CLIENT_ID;
ipc.config.silent = true;
ipc.connectTo(process.env.IPC_SERVER_ID, () => {
ipc.of[process.env.IPC_SERVER_ID].on('error', reject);
ipc.of[process.env.IPC_SERVER_ID].on('connect', () => {
const child = childProcess.spawn(EXECUTABLE, ETCHER_ARGUMENTS);
child.on('error', reject);
child.on('close', resolve);
child.stdout.on('data', (data) => {
console.log(data.toString());
const emitMessage = (data) => {
ipc.of[process.env.IPC_SERVER_ID].emit('message', data.toString());
};
child.stdout.on('data', emitMessage);
child.stderr.on('data', emitMessage);
});
});
child.stderr.on('data', (data) => {
console.error(data.toString());
});
child.on('error', reject);
child.on('close', resolve);
}).then((exitCode) => {
process.exit(exitCode);
});

79
npm-shrinkwrap.json generated
View File

@ -579,6 +579,11 @@
"from": "builtin-status-codes@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-2.0.0.tgz"
},
"cached-path-relative": {
"version": "1.0.0",
"from": "cached-path-relative@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.0.tgz"
},
"caller-path": {
"version": "0.1.0",
"from": "caller-path@>=0.1.0 <0.2.0",
@ -1338,6 +1343,11 @@
"from": "es6-map@>=0.1.3 <0.2.0",
"resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.4.tgz"
},
"es6-promise": {
"version": "3.3.1",
"from": "es6-promise@>=3.2.1 <4.0.0",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz"
},
"es6-set": {
"version": "0.1.4",
"from": "es6-set@>=0.1.3 <0.2.0",
@ -1458,6 +1468,11 @@
"from": "event-emitter@>=0.3.4 <0.4.0",
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.4.tgz"
},
"event-pubsub": {
"version": "4.2.3",
"from": "event-pubsub@4.2.3",
"resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.2.3.tgz"
},
"events": {
"version": "1.1.1",
"from": "events@>=1.1.0 <1.2.0",
@ -2517,6 +2532,16 @@
"from": "jodid25519@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz"
},
"js-message": {
"version": "1.0.5",
"from": "js-message@>=1.0.5",
"resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.5.tgz"
},
"js-queue": {
"version": "1.0.0",
"from": "js-queue@>=1.0.0",
"resolved": "https://registry.npmjs.org/js-queue/-/js-queue-1.0.0.tgz"
},
"js-tokens": {
"version": "1.0.3",
"from": "js-tokens@>=1.0.1 <2.0.0",
@ -3543,40 +3568,6 @@
}
}
},
"module-deps": {
"version": "4.0.7",
"from": "module-deps@>=4.0.2 <5.0.0",
"resolved": "https://registry.npmjs.org/module-deps/-/module-deps-4.0.7.tgz",
"dependencies": {
"isarray": {
"version": "1.0.0",
"from": "isarray@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
},
"readable-stream": {
"version": "2.1.4",
"from": "readable-stream@>=2.0.2 <3.0.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.4.tgz"
},
"through2": {
"version": "2.0.1",
"from": "through2@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.1.tgz",
"dependencies": {
"readable-stream": {
"version": "2.0.6",
"from": "readable-stream@>=2.0.0 <2.1.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz"
}
}
},
"xtend": {
"version": "4.0.1",
"from": "xtend@>=4.0.0 <5.0.0",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz"
}
}
},
"moment": {
"version": "2.13.0",
"from": "moment@>=2.8.0 <3.0.0",
@ -3629,6 +3620,11 @@
"from": "nested-error-stacks@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-1.0.2.tgz"
},
"node-cmd": {
"version": "1.1.1",
"from": "node-cmd@>=1.1.1",
"resolved": "https://registry.npmjs.org/node-cmd/-/node-cmd-1.1.1.tgz"
},
"node-gyp": {
"version": "3.4.0",
"from": "node-gyp@>=3.3.1 <4.0.0",
@ -3646,6 +3642,11 @@
"from": "node-int64@>=0.4.0 <0.5.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz"
},
"node-ipc": {
"version": "8.9.2",
"from": "node-ipc@latest",
"resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-8.9.2.tgz"
},
"node-stream-zip": {
"version": "1.3.4",
"from": "node-stream-zip@>=1.3.4 <2.0.0",
@ -4782,6 +4783,11 @@
"from": "sudo-prompt@6.1.0",
"resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-6.1.0.tgz"
},
"sumchecker": {
"version": "1.2.0",
"from": "sumchecker@>=1.2.0 <2.0.0",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-1.2.0.tgz"
},
"supports-color": {
"version": "2.0.0",
"from": "supports-color@>=2.0.0 <3.0.0",
@ -4900,11 +4906,6 @@
"from": "timers-browserify@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz"
},
"tmp": {
"version": "0.0.28",
"from": "tmp@0.0.28",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.28.tgz"
},
"tn1150": {
"version": "0.1.0",
"from": "tn1150@>=0.1.0 <0.2.0",

View File

@ -76,6 +76,7 @@
"immutable": "^3.8.1",
"is-elevated": "^1.0.0",
"lodash": "^4.5.1",
"node-ipc": "^8.9.2",
"redux": "^3.5.2",
"redux-localstorage": "^0.4.1",
"removedrive": "^1.1.1",
@ -86,7 +87,6 @@
"semver": "^5.1.0",
"sudo-prompt": "^6.1.0",
"tail": "^1.1.0",
"tmp": "0.0.28",
"trackjs": "^2.1.16",
"umount": "^1.1.3",
"username": "^2.1.0",
@ -100,7 +100,7 @@
"electron-mocha": "^1.2.2",
"electron-osx-sign": "^0.3.0",
"electron-packager": "^7.0.1",
"electron-prebuilt": "1.2.6",
"electron-prebuilt": "1.4.4",
"eslint": "^2.13.1",
"jsonfile": "^2.3.1",
"mochainon": "^1.0.0",