Unverified Commit 7e7ff1f5 authored by Amaury Martiny's avatar Amaury Martiny Committed by GitHub
Browse files

Merge pull request #116 from parity-js/am-parity-electron

Extract parity-electron and put it in a separate package
parents bcb66127 55d6a16f
......@@ -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"
......
......@@ -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": {
......
......@@ -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;
......@@ -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
......
......@@ -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:
......
// 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;
}
};
// 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
}
});
};
......@@ -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'
},
......
......@@ -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));
......@@ -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",
......
......@@ -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}.`)
);
};
......
......@@ -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;
......
......@@ -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",
......
......@@ -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",
......
# @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<String>`
Downloads Parity, saves it to Electron's `userData` folder, and returns the path to the downloaded binary once finished.
| Option | Type | Description |
| ----------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
<