diff --git a/package.json b/package.json index 0b1f034d49abf8f3cc3e45b749251fa842fcc501..2c83689a0701c90a49407aeb50954b272fafefab 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ "license": "BSD-3-Clause", "repository": { "type": "git", - "url": "git+https://github.com/parity-js/light.git" + "url": "git+https://github.com/parity-js/fether.git" }, "bugs": { - "url": "https://github.com/parity-js/light/issues" + "url": "https://github.com/parity-js/fether/issues" }, "keywords": [ "Ethereum", @@ -23,7 +23,7 @@ "Light Client", "Parity" ], - "homepage": "https://github.com/parity-js/light", + "homepage": "https://github.com/parity-js/fether", "workspaces": { "packages": [ "packages/*" @@ -49,6 +49,7 @@ "start": "npm-run-all -l -p start-*", "start-electron": "cd packages/fether-electron && yarn start", "start-hoc": "cd packages/light-hoc && yarn start", + "start-parity-electron": "cd packages/parity-electron && yarn start", "start-react": "cd packages/fether-react && yarn start", "start-ui": "cd packages/fether-ui && yarn start", "test": "lerna run test --parallel" diff --git a/packages/fether-electron/package.json b/packages/fether-electron/package.json index 7911506b740ec5c5a47481b7923ccb3f74051faf..aad7601541e60e08d9ac73800c04d234a99fd26f 100644 --- a/packages/fether-electron/package.json +++ b/packages/fether-electron/package.json @@ -12,10 +12,10 @@ "license": "BSD-3-Clause", "repository": { "type": "git", - "url": "git+https://github.com/parity-js/light.git" + "url": "git+https://github.com/parity-js/fether.git" }, "bugs": { - "url": "https://github.com/parity-js/light/issues" + "url": "https://github.com/parity-js/fether/issues" }, "keywords": [ "Ethereum", @@ -23,7 +23,7 @@ "Light Client", "Parity" ], - "homepage": "https://github.com/parity-js/light", + "homepage": "https://github.com/parity-js/fether", "parity": { "channel": "beta" }, @@ -40,17 +40,12 @@ "test": "echo Skipped." }, "dependencies": { - "async-retry": "^1.2.1", - "axios": "^0.18.0", - "checksum": "^0.1.1", - "command-exists": "^1.2.6", + "@parity/electron": "^0.1.0", "commander": "^2.15.1", - "electron-dl": "^1.11.0", "fether-react": "^0.1.0", "menubar": "^5.2.3", "pino": "^4.16.1", "pino-multi-stream": "^3.1.2", - "promise-any": "^0.2.0", "source-map-support": "^0.5.6" }, "devDependencies": { diff --git a/packages/fether-electron/src/main/cli/index.js b/packages/fether-electron/src/main/cli/index.js index 27d2ef5d243098734eef31eb56b9af5be31c4cbc..8709829718897d3880f3230e5d3592a134050015 100644 --- a/packages/fether-electron/src/main/cli/index.js +++ b/packages/fether-electron/src/main/cli/index.js @@ -36,51 +36,4 @@ cli ) .parse(process.argv); -/** - * 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)); - -// Now we must think which arguments passed to cli must be passed down to -// parity. -export const parityArgv = cli.rawArgs - .splice(2) // Remove first 2 arguments which are program path - .filter((item, index, array) => { - const key = camelcase(item.replace('--', '').replace('no-', '')); // Remove '--' and then camelCase - - if (key in cli) { - // If the option is consumed by commander.js, then we don't pass down to parity - return false; - } - - // If it's not consumed by commander.js, and starts with '--', then we keep - // it. - if (item.startsWith('--')) { - return true; - } - - // If it's the 1st argument and did not start with --, then we skip it - if (index === 0) { - return false; - } - - 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; - }); - -export { cli }; +export default cli; diff --git a/packages/fether-electron/src/main/index.js b/packages/fether-electron/src/main/index.js index 2f2e8e4da98e4659608ff1259762d978920ab112..63e6024cf6badf12de429ec44b89c3cfa48daa51 100644 --- a/packages/fether-electron/src/main/index.js +++ b/packages/fether-electron/src/main/index.js @@ -3,18 +3,23 @@ // // SPDX-License-Identifier: BSD-3-Clause +import parityElectron, { + getParityPath, + fetchParity, + runParity, + killParity +} from '@parity/electron'; import electron from 'electron'; import path from 'path'; import url from 'url'; import addMenu from './menu'; -import { doesParityExist } from './operations/doesParityExist'; -import fetchParity from './operations/fetchParity'; -import handleError from './operations/handleError'; +import cli from './cli'; +import handleError from './utils/handleError'; import messages from './messages'; -import { productName } from '../../electron-builder.json'; +import { parity } from '../../package.json'; import Pino from './utils/pino'; -import { runParity, killParity } from './operations/runParity'; +import { productName } from '../../electron-builder.json'; import staticPath from './utils/staticPath'; const { app, BrowserWindow, ipcMain, session } = electron; @@ -29,10 +34,33 @@ function createWindow () { width: 360 }); - doesParityExist() - .catch(() => fetchParity(mainWindow)) // Install parity if not present - .then(() => runParity(mainWindow)) - .catch(handleError); // Errors should be handled before, this is really just in case + // Set options for @parity/electron + parityElectron({ + cli, + logger: namespace => log => Pino({ name: namespace }).info(log) + }); + + // Look if Parity is installed + getParityPath() + .catch(() => + // Install parity if not present + fetchParity(mainWindow, { + onProgress: progress => + // Notify the renderers on download progress + mainWindow.webContents.send('parity-download-progress', progress), + parityChannel: parity.channel + }) + ) + .then(() => + // Run parity when installed + runParity(err => handleError(err, 'An error occured with Parity.')) + ) + .then(() => { + // Notify the renderers + mainWindow.webContents.send('parity-running', true); + global.isParityRunning = true; // Send this variable to renderes via IPC + }) + .catch(handleError); // Opens file:///path/to/build/index.html in prod mode, or whatever is // passed to ELECTRON_START_URL diff --git a/packages/fether-electron/src/main/messages/index.js b/packages/fether-electron/src/main/messages/index.js index a402859401e566247a6a657e54385628fe57e94a..3f1687e383bbdd179c91f52a526c19d4f7f74714 100644 --- a/packages/fether-electron/src/main/messages/index.js +++ b/packages/fether-electron/src/main/messages/index.js @@ -3,15 +3,17 @@ // // SPDX-License-Identifier: BSD-3-Clause -import signerNewToken from '../operations/signerNewToken'; +import { signerNewToken } from '@parity/electron'; /** * Handle all asynchronous messages from renderer to main. */ -export default (event, arg) => { +export default async (event, arg) => { switch (arg) { case 'signer-new-token': { - signerNewToken(event); + const token = await signerNewToken(); + // Send back the token to the renderer process + event.sender.send('asynchronous-reply', token); break; } default: diff --git a/packages/fether-electron/src/main/operations/doesParityExist.js b/packages/fether-electron/src/main/operations/doesParityExist.js deleted file mode 100644 index 1beab61b884e23c827e836aeedb131dd4a2ec4a4..0000000000000000000000000000000000000000 --- a/packages/fether-electron/src/main/operations/doesParityExist.js +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2015-2018 Parity Technologies (UK) Ltd. -// This file is part of Parity. -// -// SPDX-License-Identifier: BSD-3-Clause - -import { app } from 'electron'; -import commandExists from 'command-exists'; -import fs from 'fs'; -import promiseAny from 'promise-any'; -import { promisify } from 'util'; - -import Pino from '../utils/pino'; - -const fsExists = promisify(fs.stat); -const pino = Pino(); - -// The default path to install parity, in case there's no other instance found -// on the machine. -export const defaultParityPath = `${app.getPath('userData')}/parity${ - process.platform === 'win32' ? '.exe' : '' -}`; - -let parityPath; // The real parity path, will be populated after doesParityExist Promise resolves - -// OS locations to test if parity binary exists -const locations = { - linux: ['/bin/parity', '/usr/bin/parity', '/usr/local/bin/parity'], - darwin: ['/Applications/Parity Ethereum.app/Contents/MacOS/parity'], - win32: ['C:\\Program Files\\Parity Technologies\\Parity\\parity.exe'] -}; - -/** - * This function checks if parity has been installed on the local machine: - * - first check if the program is in $PATH, using `command-exists` - * - then check the OS default installation dir if a parity folder exists - * - finally check parity-ui's own userData folder - * This function should run in node env. - * - * @return Promise - Resolves to a string which is the command to run parity. - */ -export const doesParityExist = () => { - return commandExists('parity') // First test if `parity` command exists - .then(() => 'parity') // If yes, return `parity` as command to launch parity - .catch(() => - // Then test if OS-specific locations contain parity - promiseAny( - locations[process.platform].map(location => - fsExists(location).then(() => location) - ) - ) - ) - .catch(() => - // Finally test userData folder - fsExists(defaultParityPath).then(() => defaultParityPath) - ) - .then(path => { - parityPath = path; // Save the final result in module variable - pino.info(`Parity found on machine, can be run with "${path}".`); - return path; - }) - .catch(err => { - pino.info(`Parity not found on machine.`); - throw err; - }); -}; - -export const getParityPath = () => { - return parityPath; -}; diff --git a/packages/fether-electron/src/main/operations/runParity.js b/packages/fether-electron/src/main/operations/runParity.js deleted file mode 100644 index 500353400dd617a52536e1a30973eadeb77104f7..0000000000000000000000000000000000000000 --- a/packages/fether-electron/src/main/operations/runParity.js +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2015-2018 Parity Technologies (UK) Ltd. -// This file is part of Parity. -// -// SPDX-License-Identifier: BSD-3-Clause - -import { app } from 'electron'; -import fs from 'fs'; -import { spawn } from 'child_process'; -import { promisify } from 'util'; - -import { cli, parityArgv } from '../cli'; -import isParityRunning from './isParityRunning'; -import handleError from './handleError'; -import { getParityPath } from './doesParityExist'; -import logCommand from '../utils/logCommand'; -import Pino from '../utils/pino'; - -const fsChmod = promisify(fs.chmod); -const pino = Pino(); -const pinoParity = Pino({ name: 'parity' }); - -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', - 'IO error: While lock file:' -]; - -export const runParity = async mainWindow => { - try { - // Do not run parity with --no-run-parity - if (cli.runParity === false) { - return; - } - - // Do not run parity if there is already another instance running - const isRunning = await isParityRunning(mainWindow); - if (isRunning) { - return; - } - - // Do not run parity if parityPath has not been calculated. Shouldn't - // happen as we always run runParity after doesParityExist resolves. - if (!getParityPath()) { - throw new Error('Attempting to run Parity before parityPath is set.'); - } - - // Some users somehow had no +x on the parity binary after downloading - // it. We try to set it here (no guarantee it will work, we might not - // have rights to do it). - try { - await fsChmod(getParityPath(), '755'); - } catch (e) {} - - let logLastLine; // Always contains last line of the Parity logs - - // Run an instance of parity with the correct args - const args = [...parityArgv, '--light']; - parity = spawn(getParityPath(), args); - pino.info(logCommand(getParityPath(), args)); - - // Save in memory the last line of the log file, for handling error - const callback = data => { - if (data && data.length) { - logLastLine = data.toString(); - } - pinoParity.info(data.toString()); - }; - parity.stdout.on('data', callback); - parity.stderr.on('data', callback); - - parity.on('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 ( - logLastLine && - catchableErrors.some(error => logLastLine.includes(error)) - ) { - pino.warn( - '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 > 0) { - app.exit(1); - } else { - handleError( - new Error(`Exit code ${exitCode}, with signal ${signal}.`), - 'An error occured while running parity.' - ); - } - }); - - // Notify the renderers - mainWindow.webContents.send('parity-running', true); - global.isParityRunning = true; // Send this variable to renderes via IPC - - return Promise.resolve(); - } catch (err) { - handleError(err, 'An error occured while running parity.'); - } -}; - -export const killParity = () => { - if (parity) { - pino.info('Stopping parity.'); - parity.kill(); - parity = null; - } -}; diff --git a/packages/fether-electron/src/main/operations/signerNewToken.js b/packages/fether-electron/src/main/operations/signerNewToken.js deleted file mode 100644 index 633e419770b2a490c9398b13ebcd8fc72bb7c4f7..0000000000000000000000000000000000000000 --- a/packages/fether-electron/src/main/operations/signerNewToken.js +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2015-2018 Parity Technologies (UK) Ltd. -// This file is part of Parity. -// -// SPDX-License-Identifier: BSD-3-Clause - -import { spawn } from 'child_process'; - -import { getParityPath } from './doesParityExist'; -import logCommand from '../utils/logCommand'; -import Pino from '../utils/pino'; - -const pino = Pino(); - -export default event => { - pino.info('Requesting new token.'); - - // Generate a new token - const paritySigner = spawn(getParityPath(), ['signer', 'new-token']); - - pino.info(logCommand(getParityPath(), ['signer', 'new-token'])); - - // Listen to the output of the previous command - paritySigner.stdout.on('data', data => { - // If the output line is xxxx-xxxx-xxxx-xxxx, then it's our token - const match = data - .toString() - .match( - /[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}/ - ); - - if (match) { - const token = match[0]; - - // Send back the token to the renderer process - event.sender.send('asynchronous-reply', token); - paritySigner.kill(); // We don't need the signer anymore - } - }); -}; diff --git a/packages/fether-electron/src/main/operations/handleError.js b/packages/fether-electron/src/main/utils/handleError.js similarity index 75% rename from packages/fether-electron/src/main/operations/handleError.js rename to packages/fether-electron/src/main/utils/handleError.js index 3c73698b9807d30321daecf60c9f71b4044fb63c..8159dfede6f4002f9d1fc6846add2baf2708333b 100644 --- a/packages/fether-electron/src/main/operations/handleError.js +++ b/packages/fether-electron/src/main/utils/handleError.js @@ -6,26 +6,30 @@ import { app, dialog, shell } from 'electron'; import { bugs, name, parity } from '../../../package.json'; -import Pino from '../utils/pino'; +import Pino from './pino'; -const pino = Pino(); const logFile = `${app.getPath('userData')}/${name}.log`; +const pino = Pino(); export default (err, message = 'An error occurred.') => { pino.error(err); dialog.showMessageBox( { buttons: ['OK', 'Open logs'], - defaultId: 0, - detail: `Please attach the following debugging info: + defaultId: 0, // Default button id + detail: `Please file an issue at ${ + bugs.url + }. Please attach the following debugging info: + OS: ${process.platform} Arch: ${process.arch} Channel: ${parity.channel} Error: ${err.message} Please also attach the contents of the following file: -${logFile}`, - message: `${message} Please file an issue at ${bugs.url}.`, +${logFile}. +Click on "Open logs" to open this file.`, + message: `${message}`, title: 'Parity Error', type: 'error' }, diff --git a/packages/fether-electron/src/main/utils/pino.js b/packages/fether-electron/src/main/utils/pino.js index cab0e5f0039e74e1d9ae1d9b730a49ab64137c4e..620844917d434f82cf27ab82f30e42b1d373f51a 100644 --- a/packages/fether-electron/src/main/utils/pino.js +++ b/packages/fether-electron/src/main/utils/pino.js @@ -6,12 +6,12 @@ import { app } from 'electron'; import fs from 'fs'; import { multistream } from 'pino-multi-stream'; -import pino from 'pino'; +import Pino from 'pino'; import { name } from '../../../package.json'; // Pino by default outputs JSON. We prettify that. -const pretty = pino.pretty(); +const pretty = Pino.pretty(); pretty.pipe(process.stdout); // Create userData folder if it doesn't exist @@ -32,14 +32,4 @@ const streams = [ { level: 'info', stream: pretty } ]; -/** - * Create a pino instance - * - * @param {Object} opts - Options to pass to pino. Defaults to { name: 'electron' }. - * @example - * import Pino from './utils/pino'; - * const pino1 = Pino(); - * const pino2 = Pino({ name: 'parity' }); - */ -export default opts => - pino({ name: 'electron', ...opts }, multistream(streams)); +export default opts => Pino({ name, ...opts }, multistream(streams)); diff --git a/packages/fether-react/package.json b/packages/fether-react/package.json index a42cf0225e2a2bc4355578e796522eb023c1e3e4..e5e7b473071b560b11f097adf7fd621962aea209 100644 --- a/packages/fether-react/package.json +++ b/packages/fether-react/package.json @@ -12,10 +12,10 @@ "license": "BSD-3-Clause", "repository": { "type": "git", - "url": "git+https://github.com/parity-js/light.git" + "url": "git+https://github.com/parity-js/fether.git" }, "bugs": { - "url": "https://github.com/parity-js/light/issues" + "url": "https://github.com/parity-js/fether/issues" }, "keywords": [ "Ethereum", diff --git a/packages/fether-react/src/Accounts/AccountsList/AccountsList.js b/packages/fether-react/src/Accounts/AccountsList/AccountsList.js index 331618be4c0d2556e76f9f9a7fdfae303d9603ad..3f1cc479ac074573a3c81d4b687ff0d0a36f44ba 100644 --- a/packages/fether-react/src/Accounts/AccountsList/AccountsList.js +++ b/packages/fether-react/src/Accounts/AccountsList/AccountsList.js @@ -9,6 +9,7 @@ import { accountsInfo$, defaultAccount$ } from '@parity/light.js'; import { inject, observer } from 'mobx-react'; import light from 'light-hoc'; +import debug from '../../utils/debug'; import Health from '../../Health'; @light({ @@ -52,7 +53,7 @@ class AccountsList extends Component { history.push('/tokens', { address }); }) .catch(err => - console.error(`Error while selecting account, ${err.message}.`) + debug('AccountsList')(`Error while selecting account, ${err.message}.`) ); }; diff --git a/packages/fether-react/src/stores/parityStore.js b/packages/fether-react/src/stores/parityStore.js index 453051961aca46682d3ac9bf8dd6fcfc5ec018b9..d8f4ac8f5810d2a0ae21b98bb9ada4f931a97d29 100644 --- a/packages/fether-react/src/stores/parityStore.js +++ b/packages/fether-react/src/stores/parityStore.js @@ -3,14 +3,16 @@ // // SPDX-License-Identifier: BSD-3-Clause -import Api from '@parity/api'; import { action, observable } from 'mobx'; +import Api from '@parity/api'; import isElectron from 'is-electron'; import light from '@parity/light.js'; import store from 'store'; +import Debug from '../utils/debug'; import LS_PREFIX from './utils/lsPrefix'; +const debug = Debug('sendStore'); const electron = isElectron() ? window.require('electron') : null; const LS_KEY = `${LS_PREFIX}::secureToken`; @@ -29,7 +31,7 @@ class ParityStore { } if (!electron) { - console.log( + debug( 'Not in Electron, ParityStore will only have limited capabilities.' ); return; diff --git a/packages/fether-ui/package.json b/packages/fether-ui/package.json index 546bfe2cb3074ab1d93d6f7f60af97486a7e6f1d..9096b9b13bb18f319490c85d30987ec017ea5bc7 100644 --- a/packages/fether-ui/package.json +++ b/packages/fether-ui/package.json @@ -12,10 +12,10 @@ "license": "BSD-3-Clause", "repository": { "type": "git", - "url": "git+https://github.com/parity-js/light.git" + "url": "git+https://github.com/parity-js/fether.git" }, "bugs": { - "url": "https://github.com/parity-js/light/issues" + "url": "https://github.com/parity-js/fether/issues" }, "keywords": [ "Ethereum", @@ -23,7 +23,7 @@ "Light Client", "Parity" ], - "homepage": "https://github.com/parity-js/light", + "homepage": "https://github.com/parity-js/fether", "main": "lib/index.js", "scripts": { "prebuild": "rimraf lib", diff --git a/packages/light-hoc/package.json b/packages/light-hoc/package.json index c100f1c07d696c4e71649a9dc452c9ad91484df6..3f22122cdc21fc31d9c9865a7aea9eacd20d0da1 100644 --- a/packages/light-hoc/package.json +++ b/packages/light-hoc/package.json @@ -12,10 +12,10 @@ "license": "BSD-3-Clause", "repository": { "type": "git", - "url": "git+https://github.com/parity-js/light.git" + "url": "git+https://github.com/parity-js/fether.git" }, "bugs": { - "url": "https://github.com/parity-js/light/issues" + "url": "https://github.com/parity-js/fether/issues" }, "keywords": [ "Ethereum", @@ -23,7 +23,7 @@ "Light Client", "Parity" ], - "homepage": "https://github.com/parity-js/light", + "homepage": "https://github.com/parity-js/fether", "main": "lib/index.js", "scripts": { "prebuild": "rimraf lib", diff --git a/packages/parity-electron/README.md b/packages/parity-electron/README.md new file mode 100644 index 0000000000000000000000000000000000000000..3f6e24e9ca75e364d4f961afb2c4d43924479fbf --- /dev/null +++ b/packages/parity-electron/README.md @@ -0,0 +1,78 @@ +# @parity/electron + +Control the Parity client from electron. + +## Getting Started + +```bash +yarn add @parity/electron +``` + +## Usage + +```javascript +import parityElectron, { isParityRunning } from '@parity/electron'; + +// Optional: override default options +parityElectron({ + cli: myOwnCliObject, + logger: myCustomLoggerFunction, + parityChannel: 'nightly' +}) + +isParityRunning() + .then(() => ...); +``` + +## API + +#### `parityElectron(options: Object)` + +If you don't want to override the default options, there's no need to call this function. Here `options` can have the following fields: + +| Option | Default Value | Description | +| ---------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `options.cli` | `{}` | An object where key/values are --flags passed to the binary. The `cli` object returned by `yargs` or [`commander.js`](https://github.com/tj/commander.js/) would fit here. | +| `options.logger` | `require('debug')` | A function with the same signature as [`debug`](https://github.com/visionmedia/debug). All logs inside `@parity/electron` will then be logged by this function. | + +#### `fetchParity(mainWindow: BrowserWindow, options: Object): Promise` + +Downloads Parity, saves it to Electron's `userData` folder, and returns the path to the downloaded binary once finished. + +| Option | Type | Description | +| ----------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| `mainWindow` | [BrowserWindow](https://github.com/electron/electron/blob/master/docs/api/browser-window.md) | The Electron BrowserWindow in which to download the binary. | +| `options.onProgress` | `Function` | Optional callback that receives a number between 0 and 1 representing the progress of the current download. | +| `options.parityChannel` | `String` | Can be `stable`, `beta` or `nightly`. If downloading Parity is needed, determines which version of Parity to download. Defaults to `beta`. | + +#### `getParityPath(): Promise` + +Returns the path to Parity. It checks (in this order) if Parity is in `$PATH`, in a standard OS location or in Electron's `userData` folder, and returns the first instance of Parity found. The Promise rejects if no Parity instance is found. + +#### `defaultParityPath(): Promise` + +Returns the path to the Parity path inside Electron's `userData` folder, even if that binary doesn't exist. It's the default download location for [`fetchParity`](#fetchParitymainWindow-BrowserWindow-options-Object-PromiseltStringgt0). + +#### `isParityRunning(): Promise` + +Resolves to `true` if Parity is currently running, or to `false` if not. + +#### `runParity(onParityError: Function): Promise` + +Spawns a child process to run Parity. If some `cli` flags are passed into the options in `parityElectron`, then those flags will be passed down to Parity itself. + +| Option | Type | Description | +| --------------- | ---------- | ------------------------------------------------------------------ | +| `onParityError` | `Function` | Callback with `error` as argument when Parity encounters an error. | + +#### `killParity(): Promise` + +If a Parity process has been spawned with [`runParity`](#runParityonParityError-Function-PromiseltNullgt), then it kills this process. The Promise resolves instantly, there's no guarantee that Parity has been cleanly killed. + +#### `deleteParity(): Promise` + +If Parity has been downloaded to Electron's `userData` folder, then it deletes the Parity binary file from that folder. + +#### `signerNewToken(): Promise` + +Runs `parity signer new-token` and resolves with a new secure token to be used in a dapp. Rejects if no token could be extracted. diff --git a/packages/parity-electron/babel.config.js b/packages/parity-electron/babel.config.js new file mode 100644 index 0000000000000000000000000000000000000000..9fae1bf22ee619049cb6aab469aef25705e3afe6 --- /dev/null +++ b/packages/parity-electron/babel.config.js @@ -0,0 +1,19 @@ +module.exports = { + plugins: [ + '@babel/plugin-proposal-class-properties', + [ + '@babel/plugin-transform-runtime', + { + helpers: false, + polyfill: false, + regenerator: true, + moduleName: '@babel/runtime' + } + ] + ], + presets: [ + '@babel/preset-env', + '@babel/preset-react', + ['@babel/preset-stage-0', { decoratorsLegacy: true }] + ] +}; diff --git a/packages/parity-electron/package.json b/packages/parity-electron/package.json new file mode 100644 index 0000000000000000000000000000000000000000..32f6371dc339d40ac2190b97d82ef1850243c7c8 --- /dev/null +++ b/packages/parity-electron/package.json @@ -0,0 +1,49 @@ +{ + "name": "@parity/electron", + "description": "Fether Wallet", + "version": "0.1.0", + "private": true, + "author": "Parity Team ", + "maintainers": [ + "Jaco Greeff", + "Amaury Martiny" + ], + "contributors": [], + "license": "BSD-3-Clause", + "repository": { + "type": "git", + "url": "git+https://github.com/parity-js/fether.git" + }, + "bugs": { + "url": "https://github.com/parity-js/fether/issues" + }, + "keywords": [ + "Ethereum", + "Light", + "Light Client", + "Parity" + ], + "homepage": "https://github.com/parity-js/fether", + "main": "lib/index.js", + "scripts": { + "prebuild": "rimraf lib", + "build": "babel src --out-dir lib", + "lint": "semistandard 'src/**/*.js' --parser babel-eslint", + "start": "yarn build --watch" + }, + "dependencies": { + "async-retry": "^1.2.1", + "axios": "^0.18.0", + "checksum": "^0.1.1", + "command-exists": "^1.2.6", + "debug": "^3.1.0", + "electron-dl": "^1.11.0", + "promise-any": "^0.2.0" + }, + "devDependencies": { + "@babel/plugin-transform-runtime": "^7.0.0-beta.51", + "electron": "^2.0.2", + "semistandard": "^12.0.1" + }, + "peerDependencies": {} +} diff --git a/packages/fether-electron/src/main/operations/fetchParity.js b/packages/parity-electron/src/fetchParity.js similarity index 67% rename from packages/fether-electron/src/main/operations/fetchParity.js rename to packages/parity-electron/src/fetchParity.js index e95cda34339058f04395e1f23f484c7fa54817cd..fbc3da0249c7c73f6353d575d0bea279f13716b1 100644 --- a/packages/fether-electron/src/main/operations/fetchParity.js +++ b/packages/parity-electron/src/fetchParity.js @@ -11,14 +11,13 @@ import fs from 'fs'; import { promisify } from 'util'; import retry from 'async-retry'; -import { defaultParityPath, doesParityExist } from './doesParityExist'; -import handleError from './handleError'; -import { parity } from '../../../package.json'; -import Pino from '../utils/pino'; +import { defaultParityPath, getParityPath } from './getParityPath'; +import logger from './utils/logger'; const checksum = promisify(cs.file); const fsChmod = promisify(fs.chmod); -const pino = Pino(); +const fsStat = promisify(fs.stat); +const fsUnlink = promisify(fs.unlink); const VANITY_URL = 'https://vanity-service.parity.io/parity-binaries'; @@ -56,32 +55,35 @@ const getOs = () => { /** * Remove parity binary in the userData folder */ -const deleteParity = () => { - if (fs.statSync(defaultParityPath)) { - fs.unlinkSync(defaultParityPath); - } +export const deleteParity = async () => { + try { + const parityPath = await defaultParityPath(); + await fsStat(parityPath); + await fsUnlink(parityPath); + } catch (e) {} }; // Fetch parity from https://vanity-service.parity.io/parity-binaries -export default mainWindow => { +export const fetchParity = ( + mainWindow, + { onProgress, parityChannel } = { + parityChannel: 'beta' + } +) => { try { return retry( async (_, attempt) => { if (attempt > 1) { - pino.warn(`Retrying.`); + logger()('@parity/electron:main')('Retrying.'); } + // Delete any old Parity if it exists + await deleteParity(); + // Fetch the metadata of the correct version of parity - pino.info( - `Downloading from ${VANITY_URL}?version=${ - parity.channel - }&os=${getOs()}&architecture=${getArch()}.` - ); - const { data } = await axios.get( - `${VANITY_URL}?version=${ - parity.channel - }&os=${getOs()}&architecture=${getArch()}` - ); + const metadataUrl = `${VANITY_URL}?version=${parityChannel}&os=${getOs()}&architecture=${getArch()}`; + logger()('@parity/electron:main')(`Downloading from ${metadataUrl}.`); + const { data } = await axios.get(metadataUrl); // Get the binary's url const { downloadUrl, checksum: expectedChecksum } = data[0].files.find( @@ -91,9 +93,7 @@ export default mainWindow => { // Start downloading. This will install parity into defaultParityPath. const downloadItem = await download(mainWindow, downloadUrl, { directory: app.getPath('userData'), - onProgress: progress => - // Notify the renderers - mainWindow.webContents.send('parity-download-progress', progress) + onProgress }); const downloadPath = downloadItem.getSavePath(); // Equal to defaultParityPath @@ -113,21 +113,16 @@ export default mainWindow => { await fsChmod(downloadPath, '755'); // Double-check that Parity exists now. - return doesParityExist(); + const parityPath = await getParityPath(); + return parityPath; }, { - onRetry: err => { - pino.warn(err); - - // Everytime we retry, we remove the parity file we just downloaded. - // This needs to be done syncly, since onRetry is sync - deleteParity(); - }, retries: 3 } ); } catch (err) { - deleteParity(); - handleError(err, 'An error occured while fetching parity.'); + return deleteParity().then(() => { + Promise.reject(err); + }); } }; diff --git a/packages/parity-electron/src/getParityPath.js b/packages/parity-electron/src/getParityPath.js new file mode 100644 index 0000000000000000000000000000000000000000..82be6a3ddcab88f29ac2d8d5a0106dfd761485c2 --- /dev/null +++ b/packages/parity-electron/src/getParityPath.js @@ -0,0 +1,107 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import { app } from 'electron'; +import commandExists from 'command-exists'; +import fs from 'fs'; +import promiseAny from 'promise-any'; +import { promisify } from 'util'; + +import logger from './utils/logger'; + +const fsStat = promisify(fs.stat); + +// The default path to install parity, in case there's no other instance found +// on the machine. +export const defaultParityPath = () => + Promise.resolve( + `${app.getPath('userData')}/parity${ + process.platform === 'win32' ? '.exe' : '' + }` + ); + +let parityPath; // The real parity path, will be populated after doesParityExist Promise resolves + +/** + * Test if `parity` command is in $PATH. + */ +const isParityInPath = async () => { + const parityCommandExists = await commandExists('parity'); + if (parityCommandExists) { + // If yes, return `parity` as command to launch parity + return 'parity'; + } +}; + +/** + * Test if Parity is in the common OS locations. + */ +const isParityInOs = async () => { + // OS locations to test if parity binary exists + const locations = { + linux: ['/bin/parity', '/usr/bin/parity', '/usr/local/bin/parity'], + darwin: ['/Applications/Parity Ethereum.app/Contents/MacOS/parity'], + win32: ['C:\\Program Files\\Parity Technologies\\Parity\\parity.exe'] + }; + return promiseAny( + locations[process.platform].map(location => + fsStat(location).then(() => location) + ) + ); +}; + +/** + * Test is Parity is already downloaded in electron app's userData folder. + */ +const isParityInUserData = async () => { + const parityPath = await defaultParityPath(); + await fsStat(parityPath); + return parityPath; +}; + +/** + * This function checks if parity has been installed on the local machine: + * - first check if the program is in $PATH, using `command-exists` + * - then check the OS default installation dir if a parity folder exists + * - finally check parity-ui's own userData folder + * This function should run in node env. + * + * @return Promise - Resolves to a string which is the command to run parity. + */ +const doesParityExist = async () => { + try { + // First test if `parity` command exists + return await isParityInPath(); + } catch (e) {} + + try { + // Then test if parity is in OS + return await isParityInOs(); + } catch (e) {} + + try { + // Finally test userData folder + return await isParityInUserData(); + } catch (e) { + throw new Error('Parity not found.'); + } +}; + +export const getParityPath = async () => { + if (parityPath) { + return parityPath; + } + try { + const path = await doesParityExist(); + parityPath = path; // Save the final result in module variable + logger()('@parity/electron:main')( + `Parity found on machine, can be run with "${path}".` + ); + return path; + } catch (err) { + logger()('@parity/electron:main')(`Parity not found on machine.`); + throw err; + } +}; diff --git a/packages/parity-electron/src/index.js b/packages/parity-electron/src/index.js new file mode 100644 index 0000000000000000000000000000000000000000..bd384f3e6d00200d5a704955369bb7310e659039 --- /dev/null +++ b/packages/parity-electron/src/index.js @@ -0,0 +1,24 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import { setCli } from './utils/cli'; +import { setLogger } from './utils/logger'; + +export * from './getParityPath'; +export * from './fetchParity'; +export * from './isParityRunning'; +export * from './runParity'; +export * from './signerNewToken'; + +// Set default options for @parity/electron +export default opts => { + if (opts.cli) { + setCli(opts.cli); + } + + if (opts.logger) { + setLogger(opts.logger); + } +}; diff --git a/packages/fether-electron/src/main/operations/isParityRunning.js b/packages/parity-electron/src/isParityRunning.js similarity index 76% rename from packages/fether-electron/src/main/operations/isParityRunning.js rename to packages/parity-electron/src/isParityRunning.js index c17cf2e0144abcb16d63718f17e365890ddf9067..9deb4cdb56e60acc2234b6e1e7120cd6ce8b6947 100644 --- a/packages/fether-electron/src/main/operations/isParityRunning.js +++ b/packages/parity-electron/src/isParityRunning.js @@ -6,10 +6,8 @@ import axios from 'axios'; import retry from 'async-retry'; -import { cli } from '../cli'; -import Pino from '../utils/pino'; - -const pino = Pino(); +import { cli } from './utils/cli'; +import logger from './utils/logger'; // Try to ping these hosts const hostsToPing = ['http://127.0.0.1:8545', 'http://127.0.0.1:8546']; @@ -26,20 +24,16 @@ if (cli.wsInterface || cli.wsPort) { * * @return [Promise] - Promise that resolves to true or false. */ -const isParityRunning = async mainWindow => { +export const isParityRunning = async () => { try { // Retry to ping as many times as there are hosts in `hostsToPing` await retry( async (_, attempt) => { const host = hostsToPing[attempt - 1]; // Attempt starts with 1 await axios.get(host); - pino.info( + logger()('@parity/electron:main')( `Another instance of parity is already running on ${host}, skip running local instance.` ); - - // Notify the renderers - mainWindow.webContents.send('parity-running', true); - global.isParityRunning = true; // Send this variable to renderes via IPC }, { retries: hostsToPing.length } ); @@ -49,5 +43,3 @@ const isParityRunning = async mainWindow => { return false; } }; - -export default isParityRunning; diff --git a/packages/parity-electron/src/runParity.js b/packages/parity-electron/src/runParity.js new file mode 100644 index 0000000000000000000000000000000000000000..38c2528cfab3602f93b61bc3298eb063d203765e --- /dev/null +++ b/packages/parity-electron/src/runParity.js @@ -0,0 +1,105 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import { app } from 'electron'; +import fs from 'fs'; +import { spawn } from 'child_process'; +import { promisify } from 'util'; + +import { cli, parityArgv } from './utils/cli'; +import { getParityPath } from './getParityPath'; +import { isParityRunning } from './isParityRunning'; +import logCommand from './utils/logCommand'; +import logger from './utils/logger'; + +const fsChmod = promisify(fs.chmod); + +let parity = null; // Will hold the running parity instance + +// These are errors output by parity, which we should ignore (i.e. don't +// panic). They happen when an instance of parity is already running, and +// parity-electron tries to launch another one. +const catchableErrors = [ + 'is already in use, make sure that another instance of an Ethereum client is not running', + 'IO error: While lock file:' +]; + +export const runParity = async onParityError => { + // Do not run parity with --no-run-parity + if (cli.runParity === false) { + return; + } + + // Do not run parity if there is already another instance running + const isRunning = await isParityRunning(); + if (isRunning) { + return; + } + + const parityPath = await getParityPath(); + + // Some users somehow had no +x on the parity binary after downloading + // it. We try to set it here (no guarantee it will work, we might not + // have rights to do it). + try { + await fsChmod(parityPath, '755'); + } catch (e) {} + + let logLastLine; // Always contains last line of the Parity logs + + // Run an instance of parity with the correct args + const args = [...parityArgv(), '--light']; + parity = spawn(parityPath, args); + logger()('@parity/electron:main')(logCommand(parityPath, args)); + + // Save in memory the last line of the log file, for handling error + const callback = data => { + if (data && data.length) { + logLastLine = data.toString(); + } + logger()('@parity/parity')(data.toString()); + }; + parity.stdout.on('data', callback); + parity.stderr.on('data', callback); + + parity.on('error', err => { + onParityError(err); + }); + 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 ( + logLastLine && + catchableErrors.some(error => logLastLine.includes(error)) + ) { + logger()('@parity/electron:main')( + '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 > 0) { + app.exit(1); + } else { + onParityError(new Error(`Exit code ${exitCode}, with signal ${signal}.`)); + } + }); +}; + +export const killParity = () => { + if (parity) { + logger()('Stopping parity.'); + parity.kill(); + parity = null; + } + return Promise.resolve(); +}; diff --git a/packages/parity-electron/src/signerNewToken.js b/packages/parity-electron/src/signerNewToken.js new file mode 100644 index 0000000000000000000000000000000000000000..fdbc4a0ec6200f316600e333fdd4f9f537f613be --- /dev/null +++ b/packages/parity-electron/src/signerNewToken.js @@ -0,0 +1,48 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import { spawn } from 'child_process'; + +import { getParityPath } from './getParityPath'; +import logCommand from './utils/logCommand'; +import logger from './utils/logger'; + +/** + * Launch a parity instance to get a secure token. + */ +export const signerNewToken = () => + new Promise(async (resolve, reject) => { + logger()('@parity/electron:main')('Requesting new token.'); + + const parityPath = await getParityPath(); + + // Generate a new token + const paritySigner = spawn(parityPath, ['signer', 'new-token']); + logger()('@parity/electron:main')( + logCommand(parityPath, ['signer', 'new-token']) + ); + + // Listen to the output of the previous command + paritySigner.stdout.on('data', data => { + // If the output line is xxxx-xxxx-xxxx-xxxx, then it's our token + const match = data + .toString() + .match( + /[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}/ + ); + + if (match) { + const token = match[0]; + paritySigner.kill(); // We don't need the signer anymore + logger()('@parity/electron:main')('Successfully extracted token.'); + resolve(token); + } + }); + + // If after 2s we still didn't find the token, consider it failed. + setTimeout(() => { + reject(new Error('Error extracting token.')); + }, 2000); + }); diff --git a/packages/parity-electron/src/utils/cli.js b/packages/parity-electron/src/utils/cli.js new file mode 100644 index 0000000000000000000000000000000000000000..f2ca25d620d9154ca394881da5642b580919c566 --- /dev/null +++ b/packages/parity-electron/src/utils/cli.js @@ -0,0 +1,65 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +let cli = {}; + +/** + * Set custom cli options. + * + * @param {*} cliOptions + */ +export const setCli = cliObject => { + cli = cliObject; +}; + +/** + * 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)); + +// Now we must think which arguments passed to cli must be passed down to +// parity. +export const parityArgv = () => + cli.rawArgs + .splice(2) // Remove first 2 arguments which are program path + .filter((item, index, array) => { + const key = camelcase(item.replace('--', '').replace('no-', '')); // Remove '--' and then camelCase + + if (key in cli) { + // If the option is consumed by commander.js, then we don't pass down to parity + return false; + } + + // If it's not consumed by commander.js, and starts with '--', then we keep + // it. + if (item.startsWith('--')) { + return true; + } + + // If it's the 1st argument and did not start with --, then we skip it + if (index === 0) { + return false; + } + + 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; + }); + +export { cli }; diff --git a/packages/fether-electron/src/main/utils/logCommand.js b/packages/parity-electron/src/utils/logCommand.js similarity index 100% rename from packages/fether-electron/src/main/utils/logCommand.js rename to packages/parity-electron/src/utils/logCommand.js diff --git a/packages/parity-electron/src/utils/logger.js b/packages/parity-electron/src/utils/logger.js new file mode 100644 index 0000000000000000000000000000000000000000..acdb56669adc9857c2a4831781892202edf8e150 --- /dev/null +++ b/packages/parity-electron/src/utils/logger.js @@ -0,0 +1,14 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import debug from 'debug'; + +let logger = debug; + +export const setLogger = _logger => { + logger = _logger; +}; + +export default () => logger; diff --git a/yarn.lock b/yarn.lock index 79dcd6800e25356234288bdc58c89e50bc6ca19e..41cf72b7b760ee4d3e2b758b6f598174a7949d01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -706,6 +706,13 @@ dependencies: regenerator-transform "^0.12.4" +"@babel/plugin-transform-runtime@^7.0.0-beta.51": + version "7.0.0-beta.51" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.0.0-beta.51.tgz#0c9cab174f4e3e131659fd65c5ce8e3d73376820" + dependencies: + "@babel/helper-module-imports" "7.0.0-beta.51" + "@babel/helper-plugin-utils" "7.0.0-beta.51" + "@babel/plugin-transform-shorthand-properties@7.0.0-beta.51": version "7.0.0-beta.51" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.0.0-beta.51.tgz#ddbc0b1ae1ddb3bcfe6969f2c968103f11e32bd9"