diff --git a/interface/i18n/mist.en.i18n.json b/interface/i18n/mist.en.i18n.json index c09ea0ff4..faddbab36 100644 --- a/interface/i18n/mist.en.i18n.json +++ b/interface/i18n/mist.en.i18n.json @@ -59,6 +59,8 @@ "runTests": "Run tests", "logFiles": "Show log file", "ethereumNode": "Ethereum Node", + "starting": "starting", + "externalNode": "using external node", "network": "Network", "mainNetwork": "Main Network", "startMining": "⛏ Start Mining (Testnet only)", diff --git a/modules/ethereumNode.js b/modules/ethereumNode.js index d565fd5ae..28e48833e 100644 --- a/modules/ethereumNode.js +++ b/modules/ethereumNode.js @@ -14,6 +14,7 @@ const Q = require('bluebird'); const getNodePath = require('./getNodePath.js'); const EventEmitter = require('events').EventEmitter; const Sockets = require('./sockets'); +const SocketsBase = require('./sockets/base.js'); const Settings = require('./settings'); const DEFAULT_NODE_TYPE = 'geth'; @@ -327,13 +328,15 @@ class EthereumNode extends EventEmitter { this._network = network; this._type = nodeType; - const binPath = getNodePath(nodeType); - - log.debug(`Start node using ${binPath}`); - return new Q((resolve, reject) => { - this.__startProcess(nodeType, network, binPath) - .then(resolve, reject); + Q.all(SocketsBase.getNodePathProm) + .then(() => { + const binPath = Object.keys(getNodePath.query()[nodeType])[0]; + log.debug(`Start node using ${binPath}`); + + this.__startProcess(nodeType, network, binPath) + .then(resolve, reject); + }); }); } diff --git a/modules/getNodePath.js b/modules/getNodePath.js index c0f58ac68..13cb47abd 100644 --- a/modules/getNodePath.js +++ b/modules/getNodePath.js @@ -6,53 +6,172 @@ Gets the right Node path @module getNodePath */ +const fs = require('fs'); const path = require('path'); +const Q = require('bluebird'); +const exec = require('child_process').exec; +const cmpVer = require('semver-compare'); const binaryPath = path.resolve(__dirname + '/../nodes'); const log = require('./utils/logger').create('getNodePath'); const Settings = require('./settings'); // cache -const resolvedPaths = {}; +const paths = {}, // all versions (system + bundled): { type: { path: version } } + resolvedPaths = {}; // only latest version per type -module.exports = function(type) { - if (resolvedPaths[type]) { - return resolvedPaths[type]; - } - - let ret = ''; +/** + * Get path of system node + * + * @param {String} type the type of node (i.e. 'geth', 'eth') + */ +function getSystemPath(type) { + return new Q((resolve, reject) => { + var proc = exec('type ' + type); + proc.stdout.on('data', resolve); + proc.stderr.on('data', reject); + }); +} - // global override? - let globallySetType = Settings[`${type}Path`]; - - if (globallySetType) { - resolvedPaths[type] = globallySetType; - } else { - var binPath = (Settings.inProductionMode) - ? binaryPath.replace('nodes','node') + '/'+ type +'/'+ type - : binaryPath + '/'+ type +'/'+ process.platform +'-'+ process.arch + '/'+ type; - if(Settings.inProductionMode) { - binPath = binPath.replace('app.asar/','').replace('app.asar\\',''); - - if(process.platform === 'darwin') { - binPath = path.resolve(binPath.replace('/node/', '/../Frameworks/node/')); - } +/** + * Get versions of node (system and bundled) + * + * @param {String} type the type of node (i.e. 'geth', 'eth') + * @param {String} path the path of the node instance + */ +function getVersion(type, path) { + return new Q((resolve, reject) => { + switch (type) { + case 'geth': + var command = path + ' version'; + break; + case 'eth': + case 'parity': + var command = path + ' --version'; + break; } + var proc = exec(command); + proc.stdout.on('data', resolve); + }); +} - if(process.platform === 'win32') { - binPath = binPath.replace(/\/+/,'\\'); - binPath += '.exe'; +/** + * Compare versions of system and bundled nodes + */ +function compareNodes() { + return new Q((resolve, reject) => { + for (let type in paths) { + var entries = Object.keys(paths[type]); + var preferredPath = entries[0]; + if (Object.keys(paths[type]).length > 1) + var preferredPath = (cmpVer(paths[type][entries[0]], paths[type][entries[1]])) + ? entries[1] : entries[0] + var version = paths[type][preferredPath]; + resolvedPaths[type] = {}; + resolvedPaths[type][preferredPath] = version; } - - resolvedPaths[type] = binPath; + }); +} + + +module.exports = { + /** + * Returns path of node + * linux and mac only: returns system or bundled path depending on the latest version + * + * @param {String} type the type of node (i.e. 'geth', 'eth') + */ + query: () => { + return resolvedPaths; + }, + /** + * Evaluates node paths + * - Enumerates bundled nodes + * linux and mac only: + * - probes for system installation of nodes + * - compares the versions of bundled and system nodes (preferres latest version) + */ + probe: () => { + return new Q((resolve, reject) => { + if(Settings.inProductionMode) { + var binPath = binaryPath.replace('nodes','node').replace('app.asar/','').replace('app.asar\\',''); + + if(process.platform === 'darwin') { + binPath = path.resolve(binPath.replace('/node', '/../Frameworks/node')); + } + + fs.readdirSync(binPath).forEach((type) => { + var nodePath = binPath + '/' + type + '/' + type; + + if(process.platform === 'win32') { + nodePath = nodePath.replace(/\/+/,'\\'); + nodePath += '.exe'; + } + + paths[type] = {}; + paths[type][nodePath] = null; + }); + } else { + fs.readdirSync('nodes/').forEach((type) => { + if (fs.statSync('nodes/' + type).isDirectory()) { + var nodePath = path.resolve('nodes/' + type + '/' + process.platform +'-'+ process.arch + '/' + type); + paths[type] = {}; + paths[type][nodePath] = null; + } + }); + } + + if (process.platform === 'linux' || process.platform === 'darwin') { + var getPathProms = [], + getVersionProms = []; + + for (let type in paths) { + getPathProms.push(getSystemPath(type) + .then((data) => { + paths[type][data.match(/(\/\w+)+/)[0]] = null; + }) + .catch((data) => { + log.trace(data); + }) + ); + } + + Q.all(getPathProms).then(() => { + for (let type in paths) { + for (let path in paths[type]) { + getVersionProms.push(getVersion(type, path) + .then((data) => { + var version = data.match(/[\d.]+/)[0]; + paths[type][path] = version; + })); + } + } + }).then(() => { + Q.all(getVersionProms).then(() => { + compareNodes(); + log.debug('Available backends:'); + Object.keys(paths).forEach((type) => { + let i = 0; + for (let path in paths[type]) { + var name = (0 === i++) ? type + ' -' : type.replace(/./g, ' ') + ' ' + log.debug(name, paths[type][path], '=>', path); + } + }); + }) + .then(resolve, reject); + }) + } else { + for (let type in paths) { + for (let path in paths[type]) { + resolvedPaths[type] = path; + } + } + log.debug('Available backends: %j', paths); + resolve(); + } + }); } - - log.debug(`Resolved path for ${type}: ${resolvedPaths[type]}`); - - return resolvedPaths[type]; -}; - - +} diff --git a/modules/menuItems.js b/modules/menuItems.js index c455506f2..536300de0 100644 --- a/modules/menuItems.js +++ b/modules/menuItems.js @@ -7,6 +7,7 @@ const shell = electron.shell; const log = require('./utils/logger').create('menuItems'); const ipc = electron.ipcMain; const ethereumNode = require('./ethereumNode.js'); +const getNodePath = require('./getNodePath.js'); const Windows = require('./windows'); const updateChecker = require('./updateChecker'); const Settings = require('./settings'); @@ -342,46 +343,48 @@ var menuTempl = function(webviews) { } ]; - - - - // add node switching menu devToolsMenu.push({ type: 'separator' }); // add node switch - if(process.platform === 'darwin' || process.platform === 'win32') { - devToolsMenu.push({ - label: i18n.t('mist.applicationMenu.develop.ethereumNode'), - submenu: [ - { - label: 'Geth 1.4.10 (Go)', - checked: ethereumNode.isOwnNode && ethereumNode.isGeth, + var resolvedPaths = getNodePath.query(); + if (Object.keys(resolvedPaths).length > 1) { + var nodes = []; + for (let type in resolvedPaths) + nodes.push({ + label: type.charAt(0).toUpperCase() + type.slice(1) + + ' (' + resolvedPaths[type][Object.keys(resolvedPaths[type])[0]] + ')', + checked: ethereumNode.isOwnNode && ethereumNode.type === type, enabled: ethereumNode.isOwnNode, type: 'checkbox', click: function(){ - restartNode('geth'); - } - }, - { - label: 'Eth 1.2.9 (C++) [no hardfork support!]', - /*checked: ethereumNode.isOwnNode && ethereumNode.isEth, - enabled: ethereumNode.isOwnNode,*/ - enabled: false, - type: 'checkbox', - click: function(){ - restartNode('eth'); + restartNode(type); } - } - ]}); + }); + var submenu = { 'label': i18n.t('mist.applicationMenu.develop.ethereumNode') }; + if (ethereumNode.state === undefined) { + submenu.label += ' (' + i18n.t('mist.applicationMenu.develop.starting') + ')'; + submenu.enabled = false; + } else if (ethereumNode.isExternalNode) { + submenu.label += ' (' + i18n.t('mist.applicationMenu.develop.externalNode') + ')'; + submenu.enabled = false; + } else if (nodes.length > 0) { + submenu.submenu = nodes; + } + devToolsMenu.push(submenu); } // add network switch - devToolsMenu.push({ - label: i18n.t('mist.applicationMenu.develop.network'), - submenu: [ - { + var submenu = { label: i18n.t('mist.applicationMenu.develop.network') }; + if (ethereumNode.state === undefined) { + submenu.label += ' (' + i18n.t('mist.applicationMenu.develop.starting') + ')'; + submenu.enabled = false; + } else if (ethereumNode.isExternalNode) { + submenu.label += ' (' + i18n.t('mist.applicationMenu.develop.externalNode') + ')'; + submenu.enabled = false; + } else { + submenu.submenu = [{ label: i18n.t('mist.applicationMenu.develop.mainNetwork'), accelerator: 'CommandOrControl+Shift+1', checked: ethereumNode.isOwnNode && ethereumNode.isMainNetwork, @@ -390,8 +393,8 @@ var menuTempl = function(webviews) { click: function(){ restartNode(ethereumNode.type, 'main'); } - }, - { + }, + { label: 'Testnet (Morden)', accelerator: 'CommandOrControl+Shift+2', checked: ethereumNode.isOwnNode && ethereumNode.isTestNetwork, @@ -400,8 +403,9 @@ var menuTempl = function(webviews) { click: function(){ restartNode(ethereumNode.type, 'test'); } - } - ]}); + }]; + } + devToolsMenu.push(submenu); // add fork support devToolsMenu.push({ diff --git a/modules/sockets/base.js b/modules/sockets/base.js index 8a112ba23..64718ae28 100644 --- a/modules/sockets/base.js +++ b/modules/sockets/base.js @@ -5,12 +5,14 @@ const EventEmitter = require('events').EventEmitter; const _ = global._; const log = require('../utils/logger').create('Sockets'); +const getNodePath = require('../getNodePath.js'); const CONNECT_INTERVAL_MS = 1000; const CONNECT_TIMEOUT_MS = 3000; +var getNodePathProm = []; /** * Socket connecting to Ethereum Node. @@ -83,6 +85,9 @@ class Socket extends EventEmitter { if (STATE.CONNECTING === this._state) { this._log.warn(`Connection failed, retrying after ${CONNECT_INTERVAL_MS}ms...`); + if (Object.keys(getNodePath.query()).length == 0) + getNodePathProm.push(getNodePath.probe()); + connectTimerId = setTimeout(() => { this._socket.connect(connectConfig); }, CONNECT_INTERVAL_MS); @@ -196,6 +201,7 @@ class Socket extends EventEmitter { exports.Socket = Socket; +exports.getNodePathProm = getNodePathProm; const STATE = exports.STATE = Socket.STATE = { diff --git a/package.json b/package.json index fc7a10c44..6d9d3de27 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "numeral": "^1.5.3", "os-timesync": "^1.0.6", "semver": "^5.1.0", + "semver-compare": "^1.0.0", "solc": "^0.3.1-1", "underscore": "^1.8.3", "uuid": "^2.0.2",