clientBinaryManager.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. const _ = require('./utils/underscore.js');
  2. const Q = require('bluebird');
  3. const fs = require('fs');
  4. const { app, dialog } = require('electron');
  5. const got = require('got');
  6. const path = require('path');
  7. const Settings = require('./settings');
  8. const Windows = require('./windows');
  9. const ClientBinaryManager = require('ethereum-client-binaries').Manager;
  10. const EventEmitter = require('events').EventEmitter;
  11. const log = require('./utils/logger').create('ClientBinaryManager');
  12. // should be 'https://raw.githubusercontent.com/ethereum/mist/master/clientBinaries.json'
  13. const BINARY_URL =
  14. 'https://raw.githubusercontent.com/ethereum/mist/master/clientBinaries.json';
  15. const ALLOWED_DOWNLOAD_URLS_REGEX = /^https:\/\/(?:(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)?ethereum\.org\/|gethstore\.blob\.core\.windows\.net\/|bintray\.com\/artifact\/download\/karalabe\/ethereum\/)(?:.+)/; // eslint-disable-line max-len
  16. class Manager extends EventEmitter {
  17. constructor() {
  18. super();
  19. this._availableClients = {};
  20. }
  21. init(restart) {
  22. log.info('Initializing...');
  23. // check every hour
  24. setInterval(() => this._checkForNewConfig(true), 1000 * 60 * 60);
  25. return this._checkForNewConfig(restart);
  26. }
  27. getClient(clientId) {
  28. return this._availableClients[clientId.toLowerCase()];
  29. }
  30. _writeLocalConfig(json) {
  31. log.info('Write new client binaries local config to disk ...');
  32. fs.writeFileSync(
  33. path.join(Settings.userDataPath, 'clientBinaries.json'),
  34. JSON.stringify(json, null, 2)
  35. );
  36. }
  37. _checkForNewConfig(restart) {
  38. const nodeType = 'Geth';
  39. let binariesDownloaded = false;
  40. let nodeInfo;
  41. log.info(`Checking for new client binaries config from: ${BINARY_URL}`);
  42. this._emit('loadConfig', 'Fetching remote client config');
  43. // fetch config
  44. return got(BINARY_URL, {
  45. timeout: 3000,
  46. json: true
  47. })
  48. .then(res => {
  49. if (!res || _.isEmpty(res.body)) {
  50. throw new Error('Invalid fetch result');
  51. } else {
  52. return res.body;
  53. }
  54. })
  55. .catch(err => {
  56. log.warn('Error fetching client binaries config from repo', err);
  57. })
  58. .then(latestConfig => {
  59. if (!latestConfig) return;
  60. let localConfig;
  61. let skipedVersion;
  62. const nodeVersion = latestConfig.clients[nodeType].version;
  63. this._emit('loadConfig', 'Fetching local config');
  64. try {
  65. // now load the local json
  66. localConfig = JSON.parse(
  67. fs
  68. .readFileSync(
  69. path.join(Settings.userDataPath, 'clientBinaries.json')
  70. )
  71. .toString()
  72. );
  73. } catch (err) {
  74. log.warn(
  75. `Error loading local config - assuming this is a first run: ${err}`
  76. );
  77. if (latestConfig) {
  78. localConfig = latestConfig;
  79. this._writeLocalConfig(localConfig);
  80. } else {
  81. throw new Error(
  82. 'Unable to load local or remote config, cannot proceed!'
  83. );
  84. }
  85. }
  86. try {
  87. skipedVersion = fs
  88. .readFileSync(
  89. path.join(Settings.userDataPath, 'skippedNodeVersion.json')
  90. )
  91. .toString();
  92. } catch (err) {
  93. log.info('No "skippedNodeVersion.json" found.');
  94. }
  95. // prepare node info
  96. const platform = process.platform
  97. .replace('darwin', 'mac')
  98. .replace('win32', 'win')
  99. .replace('freebsd', 'linux')
  100. .replace('sunos', 'linux');
  101. const binaryVersion =
  102. latestConfig.clients[nodeType].platforms[platform][process.arch];
  103. const checksums = _.pick(binaryVersion.download, 'sha256', 'md5');
  104. const algorithm = _.keys(checksums)[0].toUpperCase();
  105. const hash = _.values(checksums)[0];
  106. // get the node data, to be able to pass it to a possible error
  107. nodeInfo = {
  108. type: nodeType,
  109. version: nodeVersion,
  110. checksum: hash,
  111. algorithm
  112. };
  113. // if new config version available then ask user if they wish to update
  114. if (
  115. latestConfig &&
  116. JSON.stringify(localConfig) !== JSON.stringify(latestConfig) &&
  117. nodeVersion !== skipedVersion
  118. ) {
  119. return new Q(resolve => {
  120. log.debug(
  121. 'New client binaries config found, asking user if they wish to update...'
  122. );
  123. const wnd = Windows.createPopup(
  124. 'clientUpdateAvailable',
  125. {
  126. sendData: {
  127. uiAction_sendData: {
  128. name: nodeType,
  129. version: nodeVersion,
  130. checksum: `${algorithm}: ${hash}`,
  131. downloadUrl: binaryVersion.download.url,
  132. restart
  133. }
  134. }
  135. },
  136. update => {
  137. // update
  138. if (update === 'update') {
  139. this._writeLocalConfig(latestConfig);
  140. resolve(latestConfig);
  141. // skip
  142. } else if (update === 'skip') {
  143. fs.writeFileSync(
  144. path.join(Settings.userDataPath, 'skippedNodeVersion.json'),
  145. nodeVersion
  146. );
  147. resolve(localConfig);
  148. }
  149. wnd.close();
  150. }
  151. );
  152. // if the window is closed, simply continue and as again next time
  153. wnd.on('close', () => {
  154. resolve(localConfig);
  155. });
  156. });
  157. }
  158. return localConfig;
  159. })
  160. .then(localConfig => {
  161. if (!localConfig) {
  162. log.info(
  163. 'No config for the ClientBinaryManager could be loaded, using local clientBinaries.json.'
  164. );
  165. const localConfigPath = path.join(
  166. Settings.userDataPath,
  167. 'clientBinaries.json'
  168. );
  169. localConfig = fs.existsSync(localConfigPath)
  170. ? require(localConfigPath)
  171. : require('../clientBinaries.json'); // eslint-disable-line no-param-reassign, global-require, import/no-dynamic-require, import/no-unresolved
  172. }
  173. // scan for node
  174. const mgr = new ClientBinaryManager(localConfig);
  175. mgr.logger = log;
  176. this._emit('scanning', 'Scanning for binaries');
  177. return mgr
  178. .init({
  179. folders: [
  180. path.join(Settings.userDataPath, 'binaries', 'Geth', 'unpacked'),
  181. path.join(Settings.userDataPath, 'binaries', 'Eth', 'unpacked')
  182. ]
  183. })
  184. .then(() => {
  185. const clients = mgr.clients;
  186. this._availableClients = {};
  187. const available = _.filter(clients, c => !!c.state.available);
  188. if (!available.length) {
  189. if (_.isEmpty(clients)) {
  190. throw new Error(
  191. 'No client binaries available for this system!'
  192. );
  193. }
  194. this._emit('downloading', 'Downloading binaries');
  195. return Q.map(_.values(clients), c => {
  196. binariesDownloaded = true;
  197. return mgr.download(c.id, {
  198. downloadFolder: path.join(Settings.userDataPath, 'binaries'),
  199. urlRegex: ALLOWED_DOWNLOAD_URLS_REGEX
  200. });
  201. });
  202. }
  203. })
  204. .then(() => {
  205. this._emit('filtering', 'Filtering available clients');
  206. _.each(mgr.clients, client => {
  207. if (client.state.available) {
  208. const idlcase = client.id.toLowerCase();
  209. this._availableClients[idlcase] = {
  210. binPath:
  211. Settings[`${idlcase}Path`] || client.activeCli.fullPath,
  212. version: client.version
  213. };
  214. }
  215. });
  216. // restart if it downloaded while running
  217. if (restart && binariesDownloaded) {
  218. log.info('Restarting app ...');
  219. app.relaunch();
  220. app.quit();
  221. }
  222. this._emit('done');
  223. });
  224. })
  225. .catch(err => {
  226. log.error(err);
  227. this._emit('error', err.message);
  228. // show error
  229. if (err.message.indexOf('Hash mismatch') !== -1) {
  230. // show hash mismatch error
  231. dialog.showMessageBox(
  232. {
  233. type: 'warning',
  234. buttons: ['OK'],
  235. message: global.i18n.t('mist.errors.nodeChecksumMismatch.title'),
  236. detail: global.i18n.t(
  237. 'mist.errors.nodeChecksumMismatch.description',
  238. {
  239. type: nodeInfo.type,
  240. version: nodeInfo.version,
  241. algorithm: nodeInfo.algorithm,
  242. hash: nodeInfo.checksum
  243. }
  244. )
  245. },
  246. () => {
  247. app.quit();
  248. }
  249. );
  250. // throw so the main.js can catch it
  251. throw err;
  252. }
  253. });
  254. }
  255. _emit(status, msg) {
  256. log.debug(`Status: ${status} - ${msg}`);
  257. this.emit('status', status, msg);
  258. }
  259. _resolveEthBinPath() {
  260. log.info('Resolving path to Eth client binary ...');
  261. let platform = process.platform;
  262. // "win32" -> "win" (because nodes are bundled by electron-builder)
  263. if (platform.indexOf('win') === 0) {
  264. platform = 'win';
  265. } else if (platform.indexOf('darwin') === 0) {
  266. platform = 'mac';
  267. }
  268. log.debug(`Platform: ${platform}`);
  269. let binPath = path.join(
  270. __dirname,
  271. '..',
  272. 'nodes',
  273. 'eth',
  274. `${platform}-${process.arch}`
  275. );
  276. if (Settings.inProductionMode) {
  277. // get out of the ASAR
  278. binPath = binPath.replace('nodes', path.join('..', '..', 'nodes'));
  279. }
  280. binPath = path.join(path.resolve(binPath), 'eth');
  281. if (platform === 'win') {
  282. binPath += '.exe';
  283. }
  284. log.info(`Eth client binary path: ${binPath}`);
  285. this._availableClients.eth = {
  286. binPath,
  287. version: '1.3.0'
  288. };
  289. }
  290. }
  291. module.exports = new Manager();