Commit 8bea07b5 authored by Amaury Martiny's avatar Amaury Martiny
Browse files

Create separate package for @parity/electron

parent f4f79858
......@@ -46,10 +46,12 @@
"checksum": "^0.1.1",
"command-exists": "^1.2.6",
"commander": "^2.15.1",
"debug": "^3.1.0",
"electron-dl": "^1.11.0",
"fether-react": "^0.1.0",
"menubar": "^5.2.3",
"pino": "^4.16.1",
"pino-debug": "^1.1.1",
"pino-multi-stream": "^3.1.2",
"promise-any": "^0.2.0",
"source-map-support": "^0.5.6"
......
......@@ -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,23 +3,27 @@
//
// 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 { parity } from '../../package.json';
import pino from './utils/pino';
import { productName } from '../../electron-builder.json';
import Pino from './utils/pino';
import { runParity, killParity } from './operations/runParity';
import staticPath from './utils/staticPath';
const { app, BrowserWindow, ipcMain, session } = electron;
let mainWindow;
const pino = Pino();
function createWindow () {
pino.info(`Starting ${productName}...`);
......@@ -29,10 +33,16 @@ function createWindow () {
width: 360
});
doesParityExist()
// Set options for @parity/electron
parityElectron({
cli,
parityChannel: parity.channel
});
getParityPath()
.catch(() => fetchParity(mainWindow)) // Install parity if not present
.then(() => runParity(mainWindow))
.catch(handleError); // Errors should be handled before, this is really just in case
.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,9 +6,8 @@
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`;
export default (err, message = 'An error occurred.') => {
......@@ -16,7 +15,7 @@ export default (err, message = 'An error occurred.') => {
dialog.showMessageBox(
{
buttons: ['OK', 'Open logs'],
defaultId: 0,
defaultId: 0, // Default button id
detail: `Please attach the following debugging info:
OS: ${process.platform}
Arch: ${process.arch}
......
......@@ -4,14 +4,16 @@
// SPDX-License-Identifier: BSD-3-Clause
import { app } from 'electron';
import debug from 'debug';
import fs from 'fs';
import { multistream } from 'pino-multi-stream';
import pino from 'pino';
import Pino from 'pino';
import pinoDebug from 'pino-debug';
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 +34,15 @@ 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));
const pino = Pino({ name }, multistream(streams));
// @parity/electron's debug logs are in namespace "@parity/log", we enable
// them by default.
debug.enable('@parity/electron');
pinoDebug(pino, {
map: {
'@parity/electron': 'debug'
}
});
export default pino;
# @parity/electron
Control the Parity client from electron.
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 }]
]
};
{
"name": "@parity/electron",
"description": "Fether Wallet",
"version": "0.1.0",
"private": true,
"author": "Parity Team <admin@parity.io>",
"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",
"commander": "^2.15.1",
"debug": "^3.1.0",
"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": {
"@babel/plugin-transform-runtime": "^7.0.0-beta.51",
"babel-eslint": "^8.2.3",
"copyfiles": "^2.0.0",
"cross-env": "^5.2.0",
"electron": "^2.0.2",
"electron-builder": "^20.15.1",
"electron-webpack": "^2.1.2",
"semistandard": "^12.0.1",
"webpack": "^4.7.0"
}
}
......@@ -6,19 +6,21 @@
import { app } from 'electron';
import axios from 'axios';
import cs from 'checksum';
import debug from 'debug';
import { download } from 'electron-dl';
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 { name } from '../package.json';
import parityChannel from './utils/parityChannel';
const checksum = promisify(cs.file);
const logger = debug(`${name}:main`);
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 +58,27 @@ 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 => {
try {
return retry(
async (_, attempt) => {
if (attempt > 1) {
pino.warn(`Retrying.`);
logger('Retrying.');
}
// 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(`Downloading from ${metadataUrl}.`);
const { data } = await axios.get(metadataUrl);
// Get the binary's url
const { downloadUrl, checksum: expectedChecksum } = data[0].files.find(
......@@ -113,21 +110,27 @@ export default mainWindow => {
await fsChmod(downloadPath, '755');
// Double-check that Parity exists now.
return doesParityExist();
console.log('fetchParity');
const parityPath = await getParityPath();
return parityPath;
},
{
onRetry: err => {
pino.warn(err);
onRetry: async err => {
console.log('onRetry');
debug(err);
// Everytime we retry, we remove the parity file we just downloaded.
// This needs to be done syncly, since onRetry is sync
deleteParity();
// This needs to be done syncly normally, since onRetry is sync
// https://github.com/zeit/async-retry/issues/43
return deleteParity();
},
retries: 3
}
);
} catch (err) {
deleteParity();
handleError(err, 'An error occured while fetching parity.');
console.log('error in fetchParity');
return deleteParity().then(() => {
Promise.reject(err);
});
}
};
......@@ -5,28 +5,62 @@
import { app } from 'electron';
import commandExists from 'command-exists';
import debug from 'debug';
import fs from 'fs';
import promiseAny from 'promise-any';
import { promisify } from 'util';
import Pino from '../utils/pino';
import { name } from '../package.json';
const fsExists = promisify(fs.stat);
const pino = Pino();
const logger = debug(`${name}:main`);
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 = `${app.getPath('userData')}/parity${
process.platform === 'win32' ? '.exe' : ''
}`;
export const defaultParityPath = () =>