diff --git a/electron/cli/index.js b/electron/cli/index.js index 41caeb0b63d18287696f8d2adc30eaae01990fe7..498a5a394e64940ca534e073cd8873fe6fdf7cf7 100644 --- a/electron/cli/index.js +++ b/electron/cli/index.js @@ -3,79 +3,83 @@ // // SPDX-License-Identifier: MIT -/* eslint-disable */ -const dynamicRequire = - typeof __non_webpack_require__ === 'undefined' - ? require - : __non_webpack_require__; // Dynamic require https://github.com/yargs/yargs/issues/781 -/* eslint-enable */ +const cli = require('commander'); -const { app } = require('electron'); -const fs = require('fs'); -const omit = require('lodash/omit'); -const { spawn } = require('child_process'); - -const argv = dynamicRequire('yargs').argv; -const parityPath = require('../utils/parityPath'); +const { productName } = require('../config.json'); const { version } = require('../../package.json'); -let parityArgv = null; // Args to pass to `parity` command - /** - * Show output of `parity` command with args. The args are supposed to make - * parity stop, so that the output can be immediately shown on the terminal. + * Process.argv arguments length is different in electron mode and in packaged + * mode. This small line is to harmonize the behavior for consistent parsing. * - * @param {Array} args - The arguments to pass to `parity`. + * @see https://github.com/tj/commander.js/issues/512 + * @see https://github.com/electron/electron/issues/4690#issuecomment-217435222 */ -const showParityOutput = args => { - if (fs.existsSync(parityPath())) { - const parityHelp = spawn(parityPath(), args); - - parityHelp.stdout.on('data', data => console.log(data.toString())); - parityHelp.on('close', () => app.quit()); - } else { - console.log( - 'Please run Parity Light Wallet once to install Parity Light Client. This help message will then show all available commands.' - ); - app.quit(); - } - - return false; -}; +if (process.defaultApp !== true) { + process.argv.unshift(''); +} -module.exports = () => { - if (argv.help || argv.h) { - return showParityOutput(['--help']); - } +cli + .version(version) + .allowUnknownOption() + .option( + '--no-run-parity', + `${productName} will not attempt to run the locally installed parity.` + ) + .option( + '--ui-dev', + `${productName} will load http://localhost:3000. WARNING: Only use this is you plan on developing on ${productName}.` + ) + .option( + '--ws-interface ', + `Specify the hostname portion of the WebSockets server ${productName} will connect to. IP should be an interface's IP address. (default: 127.0.0.1)` + ) + .option( + '--ws-port ', + `Specify the port portion of the WebSockets server ${productName} will connect to. (default: 8546)` + ) + .parse(process.argv); - if (argv.version || argv.v) { - console.log(`Parity UI version ${version}.`); - return showParityOutput(['--version']); - } +/** + * Camel-case the given `flag` + * + * @param {String} flag + * @return {String} + * @see https://github.com/tj/commander.js/blob/dcddf698c5463795401ad3d6382f5ec5ec060478/index.js#L1160-L1172 + */ +const camelcase = flag => + flag + .split('-') + .reduce((str, word) => str + word[0].toUpperCase() + word.slice(1)); - // Used cached value if it exists - if (parityArgv) { - return [argv, parityArgv]; - } +// Now we must think which arguments passed to cli must be passed down to +// parity. +const parityArgv = cli.rawArgs + .splice(Math.max(cli.rawArgs.findIndex(item => item.startsWith('--')), 0)) // Remove all arguments until one --option + .filter((item, index, array) => { + const key = camelcase(item.replace('--', '').replace('no-', '')); // Remove first 2 '--' and then camelCase - // Args to pass to `parity` command - parityArgv = omit(argv, '_', '$0', 'help', 'version'); + if (key in cli) { + // If the option is consumed by commander.js, then we skip it + return false; + } - // Sanitize args to be easily used by parity - Object.keys(parityArgv).forEach(key => { - // Delete all keys starting with --ui* from parityArgv. - // They will be handled directly by the UI. - if (key.startsWith('ui')) { - delete parityArgv[key]; + // If it's not consumed by commander.js, and starts with '--', then we keep + // it. This step is optional, used for optimization only. + if (item.startsWith('--')) { + return true; } - // yargs create camelCase keys for each arg, e.g. "--ws-origins all" will - // create { wsOrigins: 'all' }. For parity, we remove all those that have - // a capital letter - if (/[A-Z]/.test(key)) { - delete parityArgv[key]; + const previousKey = camelcase( + array[index - 1].replace('--', '').replace('no-', '') + ); + if (cli[previousKey] === item) { + // If it's an argument of an option consumed by commander.js, then we + // skip it too + return false; } + + return true; }); - return [argv, parityArgv]; -}; +module.exports = { cli, parityArgv }; diff --git a/electron/index.js b/electron/index.js index d559aa304811055c8584502a095372d23dc6c39b..c35eb361ca0a9e47b3bff7160f8c32f298323ffd 100644 --- a/electron/index.js +++ b/electron/index.js @@ -18,16 +18,7 @@ const { runParity, killParity } = require('./operations/runParity'); const { app, BrowserWindow, ipcMain, session } = electron; let mainWindow; -// Get arguments from cli -const [argv] = cli(); - function createWindow () { - // If cli() returns false, then it means that the arguments are stopping the - // app (e.g. --help or --version). We don't do anything more in this case. - if (!argv) { - return; - } - mainWindow = new BrowserWindow({ height: 800, width: 1200 @@ -38,7 +29,7 @@ function createWindow () { .then(() => runParity(mainWindow)) .catch(handleError); // Errors should be handled before, this is really just in case - if (argv['ui-dev'] === true) { + if (cli.uiDev === true) { // Opens http://127.0.0.1:3000 in --ui-dev mode mainWindow.loadURL('http://127.0.0.1:3000'); mainWindow.webContents.openDevTools(); diff --git a/electron/operations/runParity.js b/electron/operations/runParity.js index 8e07674910346a1690aa93ffef01318c53190b93..ff6796c426eb495b07c734464d210b4d24c1488e 100644 --- a/electron/operations/runParity.js +++ b/electron/operations/runParity.js @@ -3,62 +3,95 @@ // // SPDX-License-Identifier: MIT -const flatten = require('lodash/flatten'); +const { app } = require('electron'); const fs = require('fs'); +const noop = require('lodash/noop'); const { spawn } = require('child_process'); const util = require('util'); -const cli = require('../cli'); +const { cli, parityArgv } = require('../cli'); const handleError = require('./handleError'); const parityPath = require('../utils/parityPath'); -const [, parityArgv] = cli(); +const fsChmod = util.promisify(fs.chmod); const fsExists = util.promisify(fs.stat); -const fsReadFile = util.promisify(fs.readFile); const fsUnlink = util.promisify(fs.unlink); let parity = null; // Will hold the running parity instance +// These are errors output by parity, which Parity UI ignores (i.e. doesn't +// panic). They happen when an instance of parity is already running, and +// parity-ui tries to launch another one. +const catchableErrors = [ + 'is already in use, make sure that another instance of an Ethereum client is not running or change the address using the --ws-port and --ws-interface options.', + 'Error(Msg("IO error: While lock file:' +]; + module.exports = { runParity (mainWindow) { + // Do not run parity with --no-run-parity + if (cli.runParity === false) { + return; + } + // Create a logStream to save logs const logFile = `${parityPath()}.log`; fsExists(logFile) - .then(() => fsUnlink(logFile)) - .catch(() => {}) + .then(() => fsUnlink(logFile)) // Delete logFile and create a fresh one on each launch + .catch(noop) + .then(() => fsChmod(parityPath(), '755')) // Should already be 755 after download, just to be sure .then(() => { - var logStream = fs.createWriteStream(logFile, { flags: 'a' }); + const logStream = fs.createWriteStream(logFile, { flags: 'a' }); + let logLastLine; // Always contains last line of the logFile - // Run an instance of parity if we receive the `run-parity` message - parity = spawn( - parityPath(), - flatten( - Object.keys(parityArgv).map(key => [`--${key}`, parityArgv[key]]) // Transform {arg: value} into [--arg, value] - ) - .concat('--light') - .filter(value => value !== true) // --arg true is equivalent to --arg - ); + // Run an instance of parity with the correct args + parity = spawn(parityPath(), parityArgv); + // Pipe all parity command output into the logFile parity.stdout.pipe(logStream); parity.stderr.pipe(logStream); + + // Save in memory the last line of the log file, for handling error + const callback = data => { + if (data && data.length) { + logLastLine = data.toString(); + } + }; + parity.stdout.on('data', callback); + parity.stderr.on('data', callback); + parity.on('error', err => { - throw new Error(err); + handleError(err, 'An error occured while running parity.'); }); parity.on('close', (exitCode, signal) => { if (exitCode === 0) { return; } + // When there's already an instance of parity running, then the log + // is logging a particular line, see below. In this case, we just + // silently ignore our local instance, and let the 1st parity + // instance be the main one. + if (catchableErrors.some(error => logLastLine.includes(error))) { + console.log( + 'Another instance of parity is running, closing local instance.' + ); + return; + } + // If the exit code is not 0, then we show some error message - if (Object.keys(parityArgv).length) { + if (Object.keys(parityArgv).length > 0) { // If parity has been launched with some args, then most likely the // args are wrong, so we show the output of parity. - return fsReadFile(logFile).then(data => - console.log(data.toString()) - ); + const log = fs.readFileSync(logFile); + console.log(log.toString()); + app.exit(1); } else { - throw new Error(`Exit code ${exitCode}, with signal ${signal}.`); + handleError( + new Error(`Exit code ${exitCode}, with signal ${signal}.`), + 'An error occured while running parity.' + ); } }); }) diff --git a/package.json b/package.json index 71d545db7b7dbaeee14dbc911481eac3ba1b201e..f96d9dd7569fe06560be4d934f01fff8e6898e78 100644 --- a/package.json +++ b/package.json @@ -31,11 +31,11 @@ "build-css": "node-sass-chokidar src/ -o src/", "build-electron": "webpack --config electron/webpack.config.js", "build-js": "react-app-rewired build", - "electron": "npm run build && electron build/electron.js", + "electron": "npm run build && electron build/electron.js --light", "lint": "semistandard 'src/**/*.js' 'electron/**/*.js' --parser babel-eslint", "prebuild": "rimraf build/", "start": "npm-run-all -p watch-css start-js", - "start-electron": "electron electron/ --ui-dev --ws-origins all", + "start-electron": "electron electron/ --ui-dev --light --ws-origins all", "start-js": "react-app-rewired start", "test": "echo Skipped.", "watch-css": "npm run build-css -- --watch --recursive" @@ -44,6 +44,7 @@ "@parity/api": "^2.1.22", "@parity/light.js": "https://github.com/parity-js/light.js#c531c68b5268a1bf7aba1f43424f440b2bde7828", "axios": "^0.18.0", + "commander": "^2.15.1", "electron": "^2.0.0", "electron-dl": "^1.11.0", "is-electron": "^2.1.0", @@ -55,8 +56,7 @@ "react-dom": "^16.3.2", "react-router-dom": "^4.2.2", "react-scripts": "1.1.4", - "rxjs": "^6.1.0", - "yargs": "^11.0.0" + "rxjs": "^6.1.0" }, "devDependencies": { "babel-eslint": "^8.2.3", diff --git a/yarn.lock b/yarn.lock index 1c2a7152be7f380edf31b4577dd6ff9daa3c01e7..c3d682be32d8cd94eecb311e63b5d02bd8c1245d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2018,7 +2018,7 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" -commander@2.15.x, commander@^2.11.0, commander@^2.9.0, commander@~2.15.0: +commander@2.15.x, commander@^2.11.0, commander@^2.15.1, commander@^2.9.0, commander@~2.15.0: version "2.15.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" @@ -9487,23 +9487,6 @@ yargs-parser@^9.0.2: dependencies: camelcase "^4.1.0" -yargs@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.0.0.tgz#c052931006c5eee74610e5fc0354bedfd08a201b" - dependencies: - cliui "^4.0.0" - decamelize "^1.1.1" - find-up "^2.1.0" - get-caller-file "^1.0.1" - os-locale "^2.0.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1" - yargs-parser "^9.0.2" - yargs@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.1.0.tgz#90b869934ed6e871115ea2ff58b03f4724ed2d77"