├── test ├── bin │ ├── maga │ ├── rada │ └── folder2 │ │ └── rada ├── archives │ ├── maga2-bad.zip │ ├── no-maga2.tgz │ ├── maga2-good.zip │ └── maga2-good-rename.zip ├── config.test.js ├── logger.test.js ├── _base.js ├── checkAvailability.test.js └── download.test.js ├── .npmignore ├── .gitignore ├── .travis.yml ├── .editorconfig ├── CONTRIBUTING.md ├── LICENSE.md ├── package.json ├── src ├── config.json └── index.js └── README.md /test/bin/maga: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "good:$1" -------------------------------------------------------------------------------- /test/bin/rada: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "bad" -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | .gitignore 3 | .editorconfig 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | coverage 4 | .node* -------------------------------------------------------------------------------- /test/bin/folder2/rada: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "good" 4 | echo "good again" 5 | echo "$1" -------------------------------------------------------------------------------- /test/archives/maga2-bad.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/ethereum-client-binaries/HEAD/test/archives/maga2-bad.zip -------------------------------------------------------------------------------- /test/archives/no-maga2.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/ethereum-client-binaries/HEAD/test/archives/no-maga2.tgz -------------------------------------------------------------------------------- /test/archives/maga2-good.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/ethereum-client-binaries/HEAD/test/archives/maga2-good.zip -------------------------------------------------------------------------------- /test/archives/maga2-good-rename.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/ethereum-client-binaries/HEAD/test/archives/maga2-good-rename.zip -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | language: node_js 3 | 4 | node_js: 5 | - "6" 6 | - "8" 7 | 8 | script: 9 | - "npm run test-coverage" 10 | 11 | notifications: 12 | email: 13 | - ram@hiddentao.com 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true # doesn't work yet 9 | insert_final_newline = true # doesn't work yet -------------------------------------------------------------------------------- /test/config.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const test = require('./_base')(module); 4 | 5 | 6 | 7 | test['DefaultConfig'] = function*() { 8 | this.DefaultConfig.should.eql(require('../src/config.json')); 9 | }; 10 | 11 | 12 | 13 | test['no config given'] = function*() { 14 | this.mgr = new this.Manager(); 15 | 16 | this.mgr.config.should.eql(this.DefaultConfig); 17 | }; 18 | 19 | 20 | 21 | test['config override'] = function*() { 22 | this.mgr = new this.Manager({ 23 | foo: 'bar' 24 | }); 25 | 26 | this.mgr.config.should.eql({ foo: 'bar' }); 27 | }; 28 | -------------------------------------------------------------------------------- /test/logger.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const test = require('./_base')(module); 4 | 5 | 6 | test.before = function*() { 7 | this.mgr = new this.Manager(); 8 | }; 9 | 10 | 11 | test['nothing by default'] = function*() { 12 | let spy = this.mocker.spy(console, 'info'); 13 | 14 | this.mgr._logger.info('test'); 15 | 16 | spy.should.not.have.been.called; 17 | }; 18 | 19 | 20 | test['turn on and off'] = function*() { 21 | let spy = this.mocker.spy(console, 'info'); 22 | 23 | this.mgr.logger = { 24 | info: spy, 25 | }; 26 | this.mgr._logger.info('test logging'); 27 | 28 | const callCount = spy.callCount; 29 | callCount.should.eql(1); 30 | 31 | this.mgr.logger = null; 32 | 33 | this.mgr._logger.info('test logging'); 34 | 35 | spy.callCount.should.eql(callCount); 36 | }; 37 | 38 | 39 | 40 | test['must be valid logger'] = function*() { 41 | let spy = this.mocker.spy(); 42 | 43 | this.mgr.logger = 'blah'; 44 | 45 | this.mgr._logger.info('test'); 46 | 47 | spy.should.not.have.been.called; 48 | }; 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to ethereum-client-binaries 2 | 3 | This guide guidelines for those wishing to contribute. 4 | 5 | ## Contributor license agreement 6 | 7 | By submitting code as an individual or as an entity you agree that your code is licensed the same as `ethereum-client-binaries`. 8 | 9 | ## Issues and pull requests 10 | 11 | Issues and merge requests should be in English and contain appropriate language for audiences of all ages. 12 | 13 | We will only accept a merge requests which meets the following criteria: 14 | 15 | * Includes proper tests and all tests pass (unless it contains a test exposing a bug in existing code) 16 | * Can be merged without problems (if not please use: `git rebase master`) 17 | * Does not break any existing functionality 18 | * Fixes one specific issue or implements one specific feature (do not combine things, send separate merge requests if needed) 19 | * Keeps the code base clean and well structured 20 | * Contains functionality we think other users will benefit from too 21 | * Doesn't add unnecessary configuration options since they complicate future changes 22 | 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 [Ramesh Nair](http://www.hiddentao.com/) 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethereum-client-binaries", 3 | "version": "1.6.4", 4 | "description": "Download Ethereum client binaries for your OS", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "mocha --timeout 180000 --ui exports --reporter spec test/*.test.js", 8 | "test-coverage": "istanbul cover _mocha -- --timeout 180000 --ui exports --reporter spec test/*.test.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/ethereum/ethereum-client-binaries.git" 13 | }, 14 | "keywords": [ 15 | "ethereum", 16 | "blockchain", 17 | "client", 18 | "geth", 19 | "parity", 20 | "ethereumj", 21 | "ethereumjs", 22 | "pyethereum" 23 | ], 24 | "author": "Ramesh Nair ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/ethereum/ethereum-client-binaries/issues" 28 | }, 29 | "homepage": "https://github.com/ethereum/ethereum-client-binaries#readme", 30 | "dependencies": { 31 | "buffered-spawn": "^3.3.2", 32 | "got": "^6.5.0", 33 | "lodash.get": "^4.4.2", 34 | "lodash.isempty": "^4.4.0", 35 | "lodash.values": "^4.3.0", 36 | "mkdirp": "^0.5.1", 37 | "node-unzip-2": "^0.2.7", 38 | "tmp": "0.0.29" 39 | }, 40 | "devDependencies": { 41 | "bluebird": "^3.4.6", 42 | "chai": "^3.5.0", 43 | "co-mocha": "^1.1.3", 44 | "genomatic": "^1.0.0", 45 | "istanbul": "^0.4.5", 46 | "live-server": "^1.1.0", 47 | "lodash.frompairs": "^4.0.1", 48 | "md5-file": "^3.1.1", 49 | "mocha": "^3.0.2", 50 | "sinon": "^1.17.6", 51 | "sinon-chai": "^2.8.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/_base.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // ensure we're in testing mode 4 | process.env.NODE_ENV = 'test'; 5 | 6 | require('co-mocha'); // monkey-patch mocha 7 | 8 | const _ = { 9 | fromPairs: require('lodash.frompairs') 10 | }; 11 | 12 | const path = require('path'), 13 | liveServer = require("live-server"), 14 | fs = require('fs'), 15 | Q = require('bluebird'), 16 | genomatic = require('genomatic'), 17 | chai = require('chai'), 18 | sinon = require('sinon'); 19 | 20 | 21 | chai.use(require('sinon-chai')); 22 | 23 | // pkg 24 | const EthereumClients = require('../src'); 25 | 26 | 27 | module.exports = function(_module) { 28 | const tools = { 29 | buildPlatformConfig: function(platform, arch, cfg) { 30 | if (platform === 'darwin') { 31 | platform = 'mac'; 32 | } else if (platform === 'win32') { 33 | platform = 'win'; 34 | } 35 | 36 | const p = {}; 37 | p[`${platform}`] = {}; 38 | p[`${platform}`][`${arch}`] = cfg; 39 | return p; 40 | }, 41 | startServer: function*() { 42 | liveServer.start({ 43 | port: 38081, // Set the server port. Defaults to 8080. 44 | root: path.join(__dirname, 'archives'), // Set root directory that's being served. Defaults to cwd. 45 | open: false, // When false, it won't load your browser by default. 46 | logLevel: 0, // 0 = errors only, 1 = some, 2 = lots 47 | }); 48 | 49 | yield Q.delay(1000); 50 | 51 | this.archiveTestHost = 'http://localhost:38081'; 52 | }, 53 | stopServer: function*() { 54 | liveServer.shutdown(); 55 | 56 | yield Q.delay(1000); 57 | }, 58 | }; 59 | 60 | const test = { 61 | before: function*() { 62 | this.assert = chai.assert; 63 | this.expect = chai.expect; 64 | this.should = chai.should(); 65 | 66 | this.Manager = EthereumClients.Manager; 67 | this.DefaultConfig = EthereumClients.DefaultConfig; 68 | 69 | for (let k in tools) { 70 | this[k] = genomatic.bind(tools[k], this); 71 | } 72 | 73 | // test help 74 | console.debug = console.log.bind(console); 75 | process.env.PATH += `:${path.join(__dirname, 'bin')}`; 76 | }, 77 | beforeEach: function*() { 78 | this.mocker = sinon.sandbox.create(); 79 | }, 80 | afterEach: function*() { 81 | this.mocker.restore(); 82 | }, 83 | tests: {}, 84 | }; 85 | 86 | _module.exports[path.basename(_module.filename)] = test; 87 | 88 | return test.tests; 89 | }; 90 | -------------------------------------------------------------------------------- /src/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "clients": { 3 | "Geth": { 4 | "version": "1.5.5", 5 | "platforms": { 6 | "linux": { 7 | "x64": { 8 | "download": { 9 | "url": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.5.5-ff07d548.tar.gz", 10 | "type": "tar", 11 | "sha256": "4f36d6df25c37eb33829407d8bf2cea209cc412b3443319e2430db18581c7c01", 12 | "bin": "geth-linux-amd64-1.5.5-ff07d548/geth" 13 | }, 14 | "bin": "geth", 15 | "commands": { 16 | "sanity": { 17 | "args": ["version"], 18 | "output": [ "Geth", "1.5.5" ] 19 | } 20 | } 21 | }, 22 | "ia32": { 23 | "download": { 24 | "url": "https://gethstore.blob.core.windows.net/builds/geth-linux-386-1.5.5-ff07d548.tar.gz", 25 | "type": "tar", 26 | "sha256": "6767651e4e5b34acaa6c53079d66a9047acb74e80fd25f570bf63da87d0ce863", 27 | "bin": "geth-linux-386-1.5.5-ff07d548/geth" 28 | }, 29 | "bin": "geth", 30 | "commands": { 31 | "sanity": { 32 | "args": ["version"], 33 | "output": [ "Geth", "1.5.5" ] 34 | } 35 | } 36 | } 37 | }, 38 | "mac": { 39 | "x64": { 40 | "download": { 41 | "url": "https://gethstore.blob.core.windows.net/builds/geth-darwin-amd64-1.5.5-ff07d548.tar.gz", 42 | "type": "tar", 43 | "sha256": "a5b3ae5b7e9d91a0ca42ca24b079631578cdccce036cc5b1f0035cd0d9706b53", 44 | "bin": "geth-darwin-amd64-1.5.5-ff07d548/geth" 45 | }, 46 | "bin": "geth", 47 | "commands": { 48 | "sanity": { 49 | "args": ["version"], 50 | "output": [ "Geth", "1.5.5" ] 51 | } 52 | } 53 | } 54 | }, 55 | "win": { 56 | "x64": { 57 | "download": { 58 | "url": "https://gethstore.blob.core.windows.net/mist/geth-windows-amd64-1.5.5-ff07d548-mist-fix.zip", 59 | "type": "zip", 60 | "sha256": "6b9e65ccac8a07535fbfd003662cdd4f69289f93947689c715d10c6486e703d7", 61 | "bin": "geth-windows-amd64-1.5.5-ff07d548\\geth.exe" 62 | }, 63 | "bin": "geth.exe", 64 | "commands": { 65 | "sanity": { 66 | "args": ["version"], 67 | "output": [ "Geth", "1.5.5" ] 68 | } 69 | } 70 | }, 71 | "ia32": { 72 | "download": { 73 | "url": "https://gethstore.blob.core.windows.net/mist/geth-windows-386-1.5.5-ff07d548-mist-fix.zip", 74 | "type": "zip", 75 | "sha256": "74ef8372ae7748c1016a8fcfe2d49574b52a2780913081cf0184fb197f26f01c", 76 | "bin": "geth-windows-386-1.5.5-ff07d548\\geth.exe" 77 | }, 78 | "bin": "geth.exe", 79 | "commands": { 80 | "sanity": { 81 | "args": ["version"], 82 | "output": [ "Geth", "1.5.5" ] 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test/checkAvailability.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require('path'); 4 | const _values = require('lodash.values'); 5 | const test = require('./_base')(module); 6 | 7 | 8 | test['no clients'] = function*() { 9 | let mgr = new this.Manager({ 10 | "clients": {} 11 | }); 12 | 13 | // mgr.logger = console; 14 | yield mgr.init(); 15 | 16 | Object.keys(mgr.clients).length.should.eql(0); 17 | }; 18 | 19 | 20 | test['client not supported on architecture'] = function*() { 21 | const platforms = this.buildPlatformConfig(process.platform, 'invalid', { 22 | "url": "http://badgerbadgerbadger.com", 23 | "bin": "maga", 24 | "commands": { 25 | "sanity": { 26 | "args": ['test'], 27 | "output": [ "good:test" ] 28 | } 29 | }, 30 | }); 31 | 32 | let mgr = new this.Manager({ 33 | clients: { 34 | "Maga": { 35 | "homepage": "http://badgerbadgerbadger.com", 36 | "version": "1.0.0", 37 | "foo": "bar", 38 | "versionNotes": "http://badgerbadgerbadger.com", 39 | "platforms": platforms, 40 | } 41 | } 42 | }); 43 | 44 | // mgr.logger = console; 45 | yield mgr.init(); 46 | 47 | Object.keys(mgr.clients).length.should.eql(0); 48 | }; 49 | 50 | 51 | 52 | test['client not supported on platform'] = function*() { 53 | const platforms = this.buildPlatformConfig('invalid', process.arch, { 54 | "url": "http://badgerbadgerbadger.com", 55 | "bin": "maga", 56 | "commands": { 57 | "sanity": { 58 | "args": ['test'], 59 | "output": [ "good:test" ] 60 | } 61 | }, 62 | }); 63 | 64 | let mgr = new this.Manager({ 65 | clients: { 66 | "Maga": { 67 | "homepage": "http://badgerbadgerbadger.com", 68 | "version": "1.0.0", 69 | "foo": "bar", 70 | "versionNotes": "http://badgerbadgerbadger.com", 71 | "platforms": platforms, 72 | } 73 | } 74 | }); 75 | 76 | // mgr.logger = console; 77 | yield mgr.init(); 78 | 79 | Object.keys(mgr.clients).length.should.eql(0); 80 | }; 81 | 82 | 83 | 84 | test['unable to resolve binary'] = function*() { 85 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 86 | "url": "http://badgerbadgerbadger.com", 87 | "bin": "invalid", 88 | "commands": { 89 | "sanity": { 90 | "args": ['test'], 91 | "output": [ "good:test" ] 92 | } 93 | }, 94 | }); 95 | 96 | let mgr = new this.Manager({ 97 | clients: { 98 | "Maga": { 99 | "homepage": "http://badgerbadgerbadger.com", 100 | "version": "1.0.0", 101 | "foo": "bar", 102 | "versionNotes": "http://badgerbadgerbadger.com", 103 | "platforms": platforms, 104 | } 105 | } 106 | }); 107 | 108 | // mgr.logger = console; 109 | yield mgr.init(); 110 | 111 | Object.keys(mgr.clients).length.should.eql(1); 112 | 113 | const client = _values(mgr.clients).pop(); 114 | 115 | client.state.available.should.be.false; 116 | client.state.failReason.should.eql('notFound'); 117 | }; 118 | 119 | 120 | 121 | test['sanity check failed'] = function*() { 122 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 123 | "url": "http://badgerbadgerbadger.com", 124 | "bin": "maga", 125 | "commands": { 126 | "sanity": { 127 | "args": ['test'], 128 | "output": [ "invalid" ] 129 | } 130 | }, 131 | }); 132 | 133 | let mgr = new this.Manager({ 134 | clients: { 135 | "Maga": { 136 | "homepage": "http://badgerbadgerbadger.com", 137 | "version": "1.0.0", 138 | "foo": "bar", 139 | "versionNotes": "http://badgerbadgerbadger.com", 140 | "platforms": platforms, 141 | } 142 | } 143 | }); 144 | 145 | // mgr.logger = console; 146 | yield mgr.init(); 147 | 148 | Object.keys(mgr.clients).length.should.eql(1); 149 | 150 | const client = _values(mgr.clients).pop(); 151 | 152 | client.state.available.should.be.false; 153 | client.state.failReason.should.eql('sanityCheckFail'); 154 | }; 155 | 156 | 157 | test['sanity check passed'] = function*() { 158 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 159 | "url": "http://badgerbadgerbadger.com", 160 | "bin": "maga", 161 | "commands": { 162 | "sanity": { 163 | "args": ['test'], 164 | "output": [ "good:test" ] 165 | } 166 | }, 167 | }); 168 | 169 | let mgr = new this.Manager({ 170 | clients: { 171 | "Maga": { 172 | "homepage": "http://badgerbadgerbadger.com", 173 | "version": "1.0.0", 174 | "foo": "bar", 175 | "versionNotes": "http://badgerbadgerbadger.com", 176 | "platforms": platforms, 177 | } 178 | } 179 | }); 180 | 181 | // mgr.logger = console; 182 | yield mgr.init(); 183 | 184 | Object.keys(mgr.clients).length.should.eql(1); 185 | 186 | const client = _values(mgr.clients).pop(); 187 | 188 | client.state.available.should.be.true; 189 | }; 190 | 191 | 192 | 193 | test['sanity check is mandatory'] = function*() { 194 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 195 | "url": "http://badgerbadgerbadger.com", 196 | "bin": "maga" 197 | }); 198 | 199 | let mgr = new this.Manager({ 200 | clients: { 201 | "Maga": { 202 | "homepage": "http://badgerbadgerbadger.com", 203 | "version": "1.0.0", 204 | "foo": "bar", 205 | "versionNotes": "http://badgerbadgerbadger.com", 206 | "platforms": platforms, 207 | } 208 | } 209 | }); 210 | 211 | // mgr.logger = console; 212 | yield mgr.init(); 213 | 214 | Object.keys(mgr.clients).length.should.eql(1); 215 | 216 | const client = _values(mgr.clients).pop(); 217 | 218 | client.state.available.should.be.false; 219 | client.state.failReason.should.eql('sanityCheckFail'); 220 | }; 221 | 222 | 223 | 224 | test['client config returned'] = function*() { 225 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 226 | "url": "http://badgerbadgerbadger.com", 227 | "bin": "maga", 228 | "commands": { 229 | "sanity": { 230 | "args": ['test'], 231 | "output": [ "good:test" ] 232 | } 233 | }, 234 | }); 235 | 236 | const config = { 237 | clients: { 238 | "Maga": { 239 | "homepage": "http://badgerbadgerbadger.com", 240 | "version": "1.0.0", 241 | "foo": "bar", 242 | "versionNotes": "http://badgerbadgerbadger.com", 243 | "platforms": platforms, 244 | } 245 | } 246 | }; 247 | 248 | let mgr = new this.Manager(config); 249 | 250 | // mgr.logger = console; 251 | yield mgr.init(); 252 | 253 | const client = _values(mgr.clients).pop(); 254 | 255 | client.should.eql(Object.assign({}, config.clients.Maga, { 256 | id: 'Maga', 257 | state: { 258 | available: true, 259 | }, 260 | activeCli: { 261 | url: 'http://badgerbadgerbadger.com', 262 | bin: 'maga', 263 | fullPath: path.join(__dirname, 'bin', 'maga'), 264 | "commands": { 265 | "sanity": { 266 | "args": ['test'], 267 | "output": [ "good:test" ] 268 | } 269 | }, 270 | } 271 | })); 272 | }; 273 | 274 | 275 | 276 | test['search additional folders'] = function*() { 277 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 278 | "url": "http://badgerbadgerbadger.com", 279 | "bin": "rada", 280 | "commands": { 281 | "sanity": { 282 | "args": ['test'], 283 | "output": [ "good", "test" ] 284 | } 285 | }, 286 | }); 287 | 288 | const config = { 289 | clients: { 290 | "Rada": { 291 | "homepage": "http://badgerbadgerbadger.com", 292 | "version": "1.0.0", 293 | "foo": "bar", 294 | "versionNotes": "http://badgerbadgerbadger.com", 295 | "platforms": platforms, 296 | } 297 | } 298 | }; 299 | 300 | let mgr = new this.Manager(config); 301 | 302 | // mgr.logger = console; 303 | yield mgr.init({ 304 | folders: [ 305 | path.join(__dirname, 'bin', 'folder2') 306 | ] 307 | }); 308 | 309 | const client = _values(mgr.clients).pop(); 310 | 311 | client.should.eql(Object.assign({}, config.clients.Rada, { 312 | id: 'Rada', 313 | state: { 314 | available: true, 315 | }, 316 | activeCli: { 317 | url: 'http://badgerbadgerbadger.com', 318 | bin: 'rada', 319 | fullPath: path.join(__dirname, 'bin', 'folder2', 'rada'), 320 | commands: { 321 | "sanity": { 322 | "args": ['test'], 323 | "output": [ "good", "test" ] 324 | } 325 | } 326 | } 327 | })); 328 | }; 329 | 330 | 331 | 332 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Archival Notice 2 | This repository is an archive. Please see https://ethereum.org/ and specifically the https://ethereum.org/en/developers/docs/nodes-and-clients/ for actual information on clients. 3 | 4 | # Original README.md 5 | # ethereum-client-binaries 6 | 7 | [![Build Status](https://secure.travis-ci.org/ethereum/ethereum-client-binaries.svg?branch=master)](http://travis-ci.org/ethereum/ethereum-client-binaries) [![NPM module](https://badge.fury.io/js/ethereum-client-binaries.svg)](https://badge.fury.io/js/ethereum-client-binaries) 8 | 9 | Download Ethereum client binaries for your OS. 10 | 11 | When you wish to run a local Ethereum client node it would be beneficial to first 12 | scan for existing node client binaries on the machine and then download 13 | appropriate client binaries if none found. **This package does both.** 14 | 15 | It is structured so that it can be optionally be used in conjunction with a UI, 16 | e.g. if one wishes to allow a user to select the client software they wish to 17 | download. 18 | 19 | Features: 20 | * Configurable client types (Geth, Eth, Parity, etc) 21 | * Security: Binary *sanity* checks, URL regex checks, SHA256 hash checks 22 | * Can scan and download to specific folders 23 | * Logging can be toggled on/off at runtime 24 | * Can be integrated into Electron.js apps 25 | 26 | ## Installation 27 | 28 | ```shell 29 | npm install --save ethereum-client-binaries 30 | ``` 31 | 32 | ## Usage 33 | 34 | ### Config object 35 | 36 | First a config object needs to be defined. This specifies the possible clients 37 | and the platforms they support. 38 | 39 | For example, a config object which specifies the [Geth client](https://github.com/ethereum/go-ethereum) for only 64-bit Linux platforms and the [Parity client](https://github.com/ethcore/parity) for only 32-bit Windows platforms might be: 40 | 41 | ```js 42 | const config = { 43 | "clients": { 44 | "Geth": { 45 | "platforms": { 46 | "linux": { 47 | "x64": { 48 | "download": { 49 | "url": "https://geth.com/latest.tgz", 50 | "type": "tar", 51 | "bin": "geth-linux-x64", 52 | "sha256": "8359e8e647b168dbd053ec56438ab4cea8d76bd5153d681d001c5ce1a390401c", 53 | }, 54 | "bin": "geth", 55 | "commands": { 56 | "sanity": { 57 | "args": ["version"], 58 | "output": [ "Geth", "1.4.12" ] 59 | } 60 | } 61 | }, 62 | } 63 | } 64 | }, 65 | "Parity": { 66 | "platforms": { 67 | "win": { 68 | "ia32": { 69 | "download": { 70 | "url": "https://parity.com/latest.zip", 71 | "type": "zip" 72 | }, 73 | "bin": "parity", 74 | "commands": { 75 | "sanity": { 76 | "args": ["version"], 77 | "output": [ "Parity", "11.0" ] 78 | } 79 | } 80 | }, 81 | } 82 | } 83 | } 84 | } 85 | } 86 | ``` 87 | 88 | Every client must specify one or more platforms, each of which must specify 89 | one or more architectures. Supported platforms are as documented for Node's [process.platform](https://nodejs.org/dist/latest-v6.x/docs/api/process.html#process_process_platform) except that `mac` is used instead of `darwin` and `win` is used instead of `win32`. Supported architectures are as documented for Node's [process.arch](https://nodejs.org/dist/latest-v6.x/docs/api/process.html#process_process_arch). 90 | 91 | Each *platform-arch* entry needs to specify a `bin` key which holds the name of the executable on the system, a `download` key which holds info on where the binary can be downloaded from if needed, and a `commands` key which holds information on different kinds of commands that can be run against the binary. 92 | 93 | The `download` key holds the download `url`, the `type` of archive being downloaded, and - optionally - the filename of the binary (`bin`) inside the archive in case it differs from the expected filename of the binary. As a security measure, a `sha256` key equalling the SHA256 hash calculation of the downloadable file may be provided, in which the downloaded file's hash is tested 94 | for equality with this value. 95 | 96 | The `sanity` command is mandatory and is a way to check a found binary to ensure that is is actually a valid client binary and not something else. In the above config the `sanity` command denotes that running `geth version` should return output containing *both* `Geth` and `1.4.12`. 97 | 98 | Now we can construct a `Manager` with this config: 99 | 100 | ```js 101 | const Manager = require('ethereum-client-binaries').Manager; 102 | 103 | // construct 104 | const mgr = new Manager(config); 105 | ``` 106 | 107 | **Note:** If no config is provided then the default config ([src/config.json](https://github.com/ethereum/ethereum-client-binaries/blob/master/src/config.json)) gets used. 108 | 109 | ### Scanning for binaries 110 | 111 | Initialising a *manager* tells it to scan the system for available binaries: 112 | 113 | ```js 114 | // initialise (scan for existing binaries on system) 115 | mgr.init() 116 | .then(() => { 117 | console.log( 'Client config: ', mgr.clients ); 118 | }) 119 | .catch(process.exit); 120 | ``` 121 | 122 | Let's say the current platform is `linux` with an `x64` architecture, and that `geth` has been resolved successfully to `/usr/local/bin/geth`, the `mgr.clients` property will look like: 123 | 124 | ```js 125 | /* 126 | [ 127 | { 128 | id: 'Geth', 129 | state: { 130 | available: true, 131 | }, 132 | platforms: { .... same as original ... } 133 | activeCli: { 134 | "download": { 135 | "url": "https://geth.com/latest.tgz", 136 | "type": "tar" 137 | }, 138 | "bin": "geth", 139 | "commands": { 140 | "sanity": { 141 | "args": ["version"], 142 | "output": [ "Geth", "1.4.12" ] 143 | } 144 | }, 145 | fullPath: '/usr/local/bin/geth' 146 | } 147 | } 148 | ] 149 | */ 150 | ``` 151 | 152 | The `state.available` property is the key property to check. If `false` then `state.failReason` will also be set. There are currently two possible values for `state.failReason`: 153 | 154 | 1. `notFound` - a binary with matching name (`geth` in above example) could not be found. 155 | 2. `sanityCheckFail` - a binary with matching name was found, but it failed the sanity check when executed. 156 | 157 | The `activeCli.fullPath` property denotes the full path to the resolved client binary - this is only valid if `state.available` is `true`. 158 | 159 | **NOTE:** The Parity client isn't present in `mgr.clients` shown above because there is no linux-x64 entry specified in the Parity config shown earlier. Thus, only *possible* clients (as per the original config) will be present in `mgr.clients`. 160 | 161 | ### Scan additional folders 162 | 163 | By default the manager only scan the system `PATH` for available binaries, i.e. it doesn't do a full-disk scan. You can specify additional folders to scan using the `folders` option: 164 | 165 | ```js 166 | mgr.init({ 167 | folders: [ 168 | '/path/to/my/folder/1', 169 | '/path/to/my/folder/2' 170 | ] 171 | }) 172 | .then(...) 173 | .catch(...) 174 | ``` 175 | 176 | This features is useful if you have previously downloaded the client binaries elsewhere or you already know that client binaries will be located within specific folders. 177 | 178 | ### Download client binaries 179 | 180 | Client binaries can be downloaded whether already available on the system or not. The downloading mechanism supports downloading and unpacking ZIP and TAR files. 181 | 182 | The initial config object specifies where a package can be downloaded from, e.g: 183 | 184 | ```js 185 | "download": { 186 | "url": "https://geth.com/latest.tgz", 187 | "type": "tar" 188 | }, 189 | ``` 190 | 191 | To perform the download, specify the client id: 192 | 193 | ```js 194 | mgr.download("Geth") 195 | .then(console.log) 196 | .catch(console.error); 197 | ``` 198 | 199 | The returned result will be an object which looks like: 200 | 201 | ```js 202 | { 203 | downloadFolder: /* where archive got downloaded */, 204 | downloadFile: /* the downloaded archive file */, 205 | unpackFolder: /* folder archive was unpacked to */, 206 | client: { 207 | id: 'Geth', 208 | state: {...}, 209 | platforms: {...}, 210 | activeCli: {...}, 211 | } 212 | } 213 | ``` 214 | 215 | The `client` entry in the returned info will be the same as is present for the given client within the `mgr.clients` property (see above). 216 | 217 | After downloading and unpacking the client binary the sanity check is run against it to check that it is indeed the required binary, which means that the client's `state.available` and `state.failReason` keys will be updated with the results. 218 | 219 | ### Download to specific folder 220 | 221 | By default the client binary archive will be downloaded to a temporarily created folder. But you can override this using the `downloadFolder` option: 222 | 223 | ```js 224 | mgr.download("Geth", { 225 | downloadFolder: '/path/to/my/folder' 226 | }) 227 | .then(...) 228 | .catch(...) 229 | ``` 230 | 231 | If download and unpacking is successful the returned object will look something like: 232 | 233 | ```js 234 | { 235 | downloadFolder: '/path/to/my/folder', 236 | downloadFile: '/path/to/my/folder/archive.tgz', 237 | unpackFolder: '/path/to/my/folder/unpacked', 238 | } 239 | ``` 240 | 241 | The next time you initialise the manager you can pass in `/path/to/my/folder/unpacked` as an additional folder to scan for binaries in: 242 | 243 | ```js 244 | mgr.init({ 245 | folders: [ 246 | `/path/to/my/folder/unpacked` 247 | ] 248 | }); 249 | ``` 250 | 251 | ### URL regular expression (regex) check 252 | 253 | Even though you can check the SHA 256 hash of the downloaded package (as shown 254 | above) you may additionally wish to ensure that the download URL points to 255 | a domain you control. This is important if for example you are obtaining the 256 | initial JSON config object from a remote server. 257 | 258 | This is how you use it: 259 | 260 | ```js 261 | mgr.download("Geth", { 262 | urlRegex: /^https:\/\/ethereum.org\/.+$/ 263 | }) 264 | .then(...) 265 | .catch(...) 266 | ``` 267 | 268 | The above regex states that ONLY download URLs beginning with 269 | `https://ethereum.org/` are valid and allowed. 270 | 271 | 272 | ### Logging 273 | 274 | By default internal logging is silent. But you can turn on logging at any time by setting the logger property: 275 | 276 | ```js 277 | mgr.logger = console; /* log everything to console */ 278 | ``` 279 | 280 | The supplied logger object must have 3 methods: info, warn and error. If any one of these methods isn't provided then the built-in method (i.e. silent method) get used. For example: 281 | 282 | ```js 283 | // let's output only the error messages 284 | mgr.logger = { 285 | error: console.error.bind(console) 286 | } 287 | ``` 288 | 289 | 290 | ## Development 291 | 292 | To build and run the tests: 293 | 294 | ```shell 295 | $ npm install 296 | $ npm test 297 | ``` 298 | 299 | ## Contributions 300 | 301 | Contributions welcome - see [CONTRIBUTING.md](CONTRIBUTING.md) 302 | 303 | ## License 304 | 305 | MIT - see [LICENSE.md](LICENSE.md) 306 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const got = require('got'), 4 | fs = require('fs'), 5 | crypto = require('crypto'), 6 | path = require('path'), 7 | tmp = require('tmp'), 8 | mkdirp = require('mkdirp'), 9 | unzip = require('node-unzip-2'), 10 | spawn = require('buffered-spawn'); 11 | 12 | const _ = { 13 | isEmpty: require('lodash.isempty'), 14 | get: require('lodash.get'), 15 | values: require('lodash.values'), 16 | }; 17 | 18 | 19 | function copyFile(src, dst) { 20 | return new Promise((resolve, reject) => { 21 | var rd = fs.createReadStream(src); 22 | 23 | rd.on("error", (err) => { 24 | reject(err); 25 | }); 26 | 27 | var wr = fs.createWriteStream(dst); 28 | wr.on("error", (err) => { 29 | reject(err); 30 | }); 31 | wr.on("close", (ex) => { 32 | resolve(); 33 | }); 34 | 35 | rd.pipe(wr); 36 | }); 37 | } 38 | 39 | 40 | function checksum(filePath, algorithm) { 41 | return new Promise((resolve, reject) => { 42 | const checksum = crypto.createHash(algorithm); 43 | 44 | const stream = fs.ReadStream(filePath); 45 | 46 | stream.on('data', (d) => checksum.update(d)); 47 | 48 | stream.on('end', () => { 49 | resolve(checksum.digest('hex')); 50 | }); 51 | 52 | stream.on('error', reject); 53 | }); 54 | } 55 | 56 | 57 | const DUMMY_LOGGER = { 58 | debug: function() {}, 59 | info: function() {}, 60 | warn: function() {}, 61 | error: function() {} 62 | }; 63 | 64 | 65 | const DefaultConfig = exports.DefaultConfig = require('./config.json'); 66 | 67 | 68 | class Manager { 69 | /** 70 | * Construct a new instance. 71 | * 72 | * @param {Object} [config] The configuraton to use. If ommitted then the 73 | * default configuration (`DefaultConfig`) will be used. 74 | */ 75 | constructor (config) { 76 | this._config = config || DefaultConfig; 77 | 78 | this._logger = DUMMY_LOGGER; 79 | } 80 | 81 | /** 82 | * Get configuration. 83 | * @return {Object} 84 | */ 85 | get config () { 86 | return this._config; 87 | } 88 | 89 | 90 | /** 91 | * Set the logger. 92 | * @param {Object} val Should have same methods as global `console` object. 93 | */ 94 | set logger (val) { 95 | this._logger = {}; 96 | 97 | for (let key in DUMMY_LOGGER) { 98 | this._logger[key] = (val && typeof val[key] === 'function') 99 | ? val[key].bind(val) 100 | : DUMMY_LOGGER[key] 101 | ; 102 | } 103 | } 104 | 105 | 106 | /** 107 | * Get info on available clients. 108 | * 109 | * This will return an object, each item having the structure: 110 | * 111 | * "client name": { 112 | * id: "client name" 113 | * homepage: "client homepage url" 114 | * version: "client version" 115 | * versionNotes: "client version notes url" 116 | * cli: {... info on all available platforms...}, 117 | * activeCli: { 118 | * ...info for this platform... 119 | * } 120 | * status: { 121 | "available": true OR false (depending on status) 122 | "failReason": why it is not available (`sanityCheckFail`, `notFound`, etc) 123 | * } 124 | * } 125 | * 126 | * @return {Object} 127 | */ 128 | get clients () { 129 | return this._clients; 130 | } 131 | 132 | 133 | /** 134 | * Initialize the manager. 135 | * 136 | * This will scan for clients. 137 | * Upon completion `this.clients` will have all the info you need. 138 | * 139 | * @param {Object} [options] Additional options. 140 | * @param {Array} [options.folders] Additional folders to search in for client binaries. 141 | * 142 | * @return {Promise} 143 | */ 144 | init(options) { 145 | this._logger.info('Initializing...'); 146 | 147 | this._resolvePlatform(); 148 | 149 | return this._scan(options); 150 | } 151 | 152 | 153 | /** 154 | * Download a particular client. 155 | * 156 | * If client supports this platform then 157 | * it will be downloaded from the download URL, whether it is already available 158 | * on the system or not. 159 | * 160 | * If client doesn't support this platform then the promise will be rejected. 161 | * 162 | * Upon completion the `clients` property will have been updated with the new 163 | * availability status of this client. In addition the following info will 164 | * be returned from the promise: 165 | * 166 | * ``` 167 | * { 168 | * downloadFolder: ...where archive got downloaded... 169 | * downloadFile: ...location of downloaded file... 170 | * unpackFolder: ...where archive was unpacked to... 171 | * client: ...updated client object (contains availability info and full binary path)... 172 | * } 173 | * ``` 174 | * 175 | * @param {Object} [options] Options. 176 | * @param {Object} [options.downloadFolder] Folder to download client to, and to unzip it in. 177 | * @param {Function} [options.unpackHandler] Custom download archive unpack handling function. 178 | * @param {RegExp} [options.urlRegex] Regex to check the download URL against (this is a security measure). 179 | * 180 | * @return {Promise} 181 | */ 182 | download (clientId, options) { 183 | options = Object.assign({ 184 | downloadFolder: null, 185 | unpackHandler: null, 186 | urlRegex: null, 187 | }, options); 188 | 189 | this._logger.info(`Download binary for ${clientId} ...`); 190 | 191 | const client = _.get(this._clients, clientId); 192 | 193 | const activeCli = _.get(client, `activeCli`), 194 | downloadCfg = _.get(activeCli, `download`); 195 | 196 | return Promise.resolve() 197 | .then(() => { 198 | // not for this machine? 199 | if (!client) { 200 | throw new Error(`${clientId} missing configuration for this platform.`); 201 | } 202 | 203 | if (!_.get(downloadCfg, 'url') || !_.get(downloadCfg, 'type')) { 204 | throw new Error(`Download info not available for ${clientId}`); 205 | } 206 | 207 | if (options.urlRegex) { 208 | this._logger.debug('Checking download URL against regex ...'); 209 | 210 | if (!options.urlRegex.test(downloadCfg.url)) { 211 | throw new Error(`Download URL failed regex check`); 212 | } 213 | } 214 | 215 | let resolve, reject; 216 | const promise = new Promise((_resolve, _reject) => { 217 | resolve = _resolve; 218 | reject = _reject; 219 | }); 220 | 221 | this._logger.debug('Generating download folder path ...'); 222 | 223 | const downloadFolder = path.join( 224 | options.downloadFolder || tmp.dirSync().name, 225 | client.id 226 | ); 227 | 228 | this._logger.debug(`Ensure download folder ${downloadFolder} exists ...`); 229 | 230 | mkdirp.sync(downloadFolder); 231 | 232 | const downloadFile = path.join(downloadFolder, `archive.${downloadCfg.type}`); 233 | 234 | this._logger.info(`Downloading package from ${downloadCfg.url} to ${downloadFile} ...`); 235 | 236 | const writeStream = fs.createWriteStream(downloadFile); 237 | 238 | const stream = got.stream(downloadCfg.url); 239 | 240 | // stream.pipe(progress({ 241 | // time: 100 242 | // })); 243 | 244 | stream.pipe(writeStream); 245 | 246 | // stream.on('progress', (info) => ); 247 | 248 | stream.on('error', (err) => { 249 | this._logger.error(err); 250 | 251 | reject(new Error(`Error downloading package for ${clientId}: ${err.message}`)); 252 | }) 253 | 254 | stream.on('end', () => { 255 | this._logger.debug(`Downloaded ${downloadCfg.url} to ${downloadFile}`); 256 | 257 | // quick sanity check 258 | try { 259 | fs.accessSync(downloadFile, fs.F_OK | fs.R_OK); 260 | 261 | resolve({ 262 | downloadFolder: downloadFolder, 263 | downloadFile: downloadFile, 264 | }); 265 | } catch (err) { 266 | reject(new Error(`Error downloading package for ${clientId}: ${err.message}`)); 267 | } 268 | }); 269 | 270 | return promise; 271 | }) 272 | .then((dInfo) => { 273 | const downloadFolder = dInfo.downloadFolder, 274 | downloadFile = dInfo.downloadFile; 275 | 276 | // test checksum 277 | let value, algorithm, expectedHash; 278 | 279 | if (value = _.get(downloadCfg, 'md5')) { 280 | expectedHash = value; 281 | algorithm = 'md5'; 282 | } else if (value = _.get(downloadCfg, 'sha256')) { 283 | expectedHash = value; 284 | algorithm = 'sha256'; 285 | } 286 | 287 | if (algorithm) { 288 | return checksum(dInfo.downloadFile, algorithm) 289 | .then((hash) => { 290 | if (expectedHash !== hash) { 291 | throw new Error(`Hash mismatch (using ${algorithm}): expected ${expectedHash}; got ${hash}`); 292 | } 293 | return dInfo; 294 | }); 295 | } else { 296 | return dInfo; 297 | } 298 | }) 299 | .then((dInfo) => { 300 | const downloadFolder = dInfo.downloadFolder, 301 | downloadFile = dInfo.downloadFile; 302 | 303 | const unpackFolder = path.join(downloadFolder, 'unpacked'); 304 | 305 | this._logger.debug(`Ensure unpack folder ${unpackFolder} exists ...`); 306 | 307 | mkdirp.sync(unpackFolder); 308 | 309 | this._logger.debug(`Unzipping ${downloadFile} to ${unpackFolder} ...`); 310 | 311 | let promise; 312 | 313 | if (options.unpackHandler) { 314 | this._logger.debug(`Invoking custom unpack handler ...`); 315 | 316 | promise = options.unpackHandler(downloadFile, unpackFolder); 317 | } else { 318 | switch (downloadCfg.type) { 319 | case 'zip': 320 | this._logger.debug(`Using unzip ...`); 321 | 322 | promise = new Promise((resolve, reject) => { 323 | try { 324 | fs.createReadStream(downloadFile) 325 | .pipe( 326 | unzip.Extract({ path: unpackFolder }) 327 | .on('close', resolve) 328 | .on('error', reject) 329 | ) 330 | .on('error', reject); 331 | } catch (err) { 332 | reject(err); 333 | } 334 | }); 335 | break; 336 | case 'tar': 337 | this._logger.debug(`Using tar ...`); 338 | 339 | promise = this._spawn('tar', ['-xf', downloadFile, '-C', unpackFolder]); 340 | break; 341 | default: 342 | throw new Error(`Unsupported archive type: ${downloadCfg.type}`); 343 | } 344 | } 345 | 346 | return promise.then(() => { 347 | this._logger.debug(`Unzipped ${downloadFile} to ${unpackFolder}`); 348 | 349 | const linkPath = path.join(unpackFolder, activeCli.bin); 350 | 351 | // need to rename binary? 352 | if (downloadCfg.bin) { 353 | let realPath = path.join(unpackFolder, downloadCfg.bin); 354 | 355 | try { 356 | fs.accessSync(linkPath, fs.R_OK); 357 | fs.unlinkSync(linkPath); 358 | } catch (e) { 359 | if (e.code !== 'ENOENT') 360 | this._logger.warn(e); 361 | } 362 | 363 | return copyFile(realPath, linkPath).then(() => linkPath) 364 | } else { 365 | return Promise.resolve(linkPath); 366 | } 367 | }) 368 | .then((binPath) => { 369 | // make binary executable 370 | try { 371 | fs.chmodSync(binPath, '755'); 372 | } catch (e) { 373 | this._logger.warn(e); 374 | } 375 | 376 | return { 377 | downloadFolder: downloadFolder, 378 | downloadFile: downloadFile, 379 | unpackFolder: unpackFolder, 380 | }; 381 | }); 382 | }) 383 | .then((info) => { 384 | return this._verifyClientStatus(client, { 385 | folders: [info.unpackFolder], 386 | }) 387 | .then(() => { 388 | info.client = client; 389 | 390 | return info; 391 | }); 392 | }); 393 | } 394 | 395 | 396 | 397 | _resolvePlatform () { 398 | this._logger.info('Resolving platform...'); 399 | 400 | // platform 401 | switch (process.platform) { 402 | case 'win32': 403 | this._os = 'win'; 404 | break; 405 | case 'darwin': 406 | this._os = 'mac'; 407 | break; 408 | default: 409 | this._os = process.platform; 410 | } 411 | 412 | // architecture 413 | this._arch = process.arch; 414 | 415 | return Promise.resolve(); 416 | } 417 | 418 | 419 | /** 420 | * Scan the local machine for client software, as defined in the configuration. 421 | * 422 | * Upon completion `this._clients` will be set. 423 | * 424 | * @param {Object} [options] Additional options. 425 | * @param {Array} [options.folders] Additional folders to search in for client binaries. 426 | * 427 | * @return {Promise} 428 | */ 429 | _scan (options) { 430 | this._clients = {}; 431 | 432 | return this._calculatePossibleClients() 433 | .then((clients) => { 434 | this._clients = clients; 435 | 436 | const count = Object.keys(this._clients).length; 437 | 438 | this._logger.info(`${count} possible clients.`); 439 | 440 | if (_.isEmpty(this._clients)) { 441 | return; 442 | } 443 | 444 | this._logger.info(`Verifying status of all ${count} possible clients...`); 445 | 446 | return Promise.all(_.values(this._clients).map( 447 | (client) => this._verifyClientStatus(client, options) 448 | )); 449 | }); 450 | } 451 | 452 | 453 | /** 454 | * Calculate possible clients for this machine by searching for binaries. 455 | * @return {Promise} 456 | */ 457 | _calculatePossibleClients () { 458 | return Promise.resolve() 459 | .then(() => { 460 | // get possible clients 461 | this._logger.info('Calculating possible clients...'); 462 | 463 | const possibleClients = {}; 464 | 465 | for (let clientName in _.get(this._config, 'clients', {})) { 466 | let client = this._config.clients[clientName]; 467 | 468 | if (_.get(client, `platforms.${this._os}.${this._arch}`)) { 469 | possibleClients[clientName] = 470 | Object.assign({}, client, { 471 | id: clientName, 472 | activeCli: client.platforms[this._os][this._arch] 473 | }); 474 | } 475 | } 476 | 477 | return possibleClients; 478 | }); 479 | } 480 | 481 | 482 | /** 483 | * This will modify the passed-in `client` item according to check results. 484 | * 485 | * @param {Object} [options] Additional options. 486 | * @param {Array} [options.folders] Additional folders to search in for client binaries. 487 | * 488 | * @return {Promise} 489 | */ 490 | _verifyClientStatus (client, options) { 491 | options = Object.assign({ 492 | folders: [] 493 | }, options); 494 | 495 | this._logger.info(`Verify ${client.id} status ...`); 496 | 497 | return Promise.resolve().then(() => { 498 | const binName = client.activeCli.bin; 499 | 500 | // reset state 501 | client.state = {}; 502 | delete client.activeCli.binPath; 503 | 504 | this._logger.debug(`${client.id} binary name: ${binName}`); 505 | 506 | const binPaths = []; 507 | let command; 508 | let args = []; 509 | 510 | if (process.platform === 'win32') { 511 | command = 'where'; 512 | } else { 513 | command = 'command'; 514 | args.push('-v'); 515 | } 516 | args.push(binName); 517 | 518 | return this._spawn(command, args) 519 | .then((output) => { 520 | const systemPath = _.get(output, 'stdout', '').trim(); 521 | 522 | if (_.get(systemPath, 'length')) { 523 | this._logger.debug(`Got PATH binary for ${client.id}: ${systemPath}`); 524 | 525 | binPaths.push(systemPath); 526 | } 527 | }, (err) => { 528 | this._logger.debug(`Command ${binName} not found in path.`); 529 | }) 530 | .then(() => { 531 | // now let's search additional folders 532 | if (_.get(options, 'folders.length')) { 533 | options.folders.forEach((folder) => { 534 | this._logger.debug(`Checking for ${client.id} binary in ${folder} ...`); 535 | 536 | const fullPath = path.join(folder, binName); 537 | 538 | try { 539 | fs.accessSync(fullPath, fs.F_OK | fs.X_OK); 540 | 541 | this._logger.debug(`Got optional folder binary for ${client.id}: ${fullPath}`); 542 | 543 | binPaths.push(fullPath); 544 | } catch (err) { 545 | /* do nothing */ 546 | } 547 | }); 548 | } 549 | }) 550 | .then(() => { 551 | if (!binPaths.length) { 552 | throw new Error(`No binaries found for ${client.id}`); 553 | } 554 | }) 555 | .catch((err) => { 556 | this._logger.error(`Unable to resolve ${client.id} executable: ${binName}`); 557 | 558 | client.state.available = false; 559 | client.state.failReason = 'notFound'; 560 | 561 | throw err; 562 | }) 563 | .then(() => { 564 | // sanity check each available binary until a good one is found 565 | return Promise.all(binPaths.map((binPath) => { 566 | this._logger.debug(`Running ${client.id} sanity check for binary: ${binPath} ...`); 567 | 568 | return this._runSanityCheck(client, binPath) 569 | .catch((err) => { 570 | this._logger.debug(`Sanity check failed for: ${binPath}`); 571 | }); 572 | })) 573 | .then(() => { 574 | // if one succeeded then we're good 575 | if (client.activeCli.fullPath) { 576 | return; 577 | } 578 | 579 | client.state.available = false; 580 | client.state.failReason = 'sanityCheckFail'; 581 | 582 | throw new Error('All sanity checks failed'); 583 | }); 584 | }) 585 | .then(() => { 586 | client.state.available = true; 587 | }) 588 | .catch((err) => { 589 | this._logger.debug(`${client.id} deemed unavailable`); 590 | 591 | client.state.available = false; 592 | }) 593 | }); 594 | } 595 | 596 | 597 | /** 598 | * Run sanity check for client. 599 | 600 | * @param {Object} client Client config info. 601 | * @param {String} binPath Path to binary (to sanity-check). 602 | * 603 | * @return {Promise} 604 | */ 605 | _runSanityCheck (client, binPath) { 606 | this._logger.debug(`${client.id} binary path: ${binPath}`); 607 | 608 | this._logger.info(`Checking for ${client.id} sanity check ...`); 609 | 610 | const sanityCheck = _.get(client, 'activeCli.commands.sanity'); 611 | 612 | return Promise.resolve() 613 | .then(() => { 614 | if (!sanityCheck) { 615 | throw new Error(`No ${client.id} sanity check found.`); 616 | } 617 | }) 618 | .then(() => { 619 | this._logger.info(`Checking sanity for ${client.id} ...`) 620 | 621 | return this._spawn(binPath, sanityCheck.args); 622 | }) 623 | .then((output) => { 624 | const haystack = output.stdout + output.stderr; 625 | 626 | this._logger.debug(`Sanity check output: ${haystack}`); 627 | 628 | const needles = sanityCheck.output || []; 629 | 630 | for (let needle of needles) { 631 | if (0 > haystack.indexOf(needle)) { 632 | throw new Error(`Unable to find "${needle}" in ${client.id} output`); 633 | } 634 | } 635 | 636 | this._logger.debug(`Sanity check passed for ${binPath}`); 637 | 638 | // set it! 639 | client.activeCli.fullPath = binPath; 640 | }) 641 | .catch((err) => { 642 | this._logger.error(`Sanity check failed for ${client.id}`, err); 643 | 644 | throw err; 645 | }); 646 | } 647 | 648 | 649 | /** 650 | * @return {Promise} Resolves to { stdout, stderr } object 651 | */ 652 | _spawn(cmd, args) { 653 | args = args || []; 654 | 655 | this._logger.debug(`Exec: "${cmd} ${args.join(' ')}"`); 656 | 657 | return spawn(cmd, args); 658 | } 659 | } 660 | 661 | 662 | exports.Manager = Manager; 663 | -------------------------------------------------------------------------------- /test/download.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const _get = require('lodash.get'), 4 | fs = require('fs'), 5 | md5File = require('md5-file'), 6 | path = require('path'); 7 | 8 | const test = require('./_base')(module); 9 | 10 | 11 | test.before = function*() { 12 | yield this.startServer(); 13 | }; 14 | 15 | 16 | test.after = function*() { 17 | yield this.stopServer(); 18 | }; 19 | 20 | 21 | 22 | test['no clients'] = function*() { 23 | let mgr = new this.Manager({ 24 | "clients": {} 25 | }); 26 | 27 | try { 28 | // mgr.logger = console; 29 | yield mgr.download('Maga'); 30 | throw -1; 31 | } catch (err) { 32 | err.message.should.eql('Maga missing configuration for this platform.'); 33 | } 34 | }; 35 | 36 | 37 | test['client not supported on architecture'] = function*() { 38 | const platforms = this.buildPlatformConfig(process.platform, 'invalid', { 39 | "url": "http://badgerbadgerbadger.com", 40 | "bin": "maga", 41 | "commands": { 42 | "sanity": { 43 | "args": ['test'], 44 | "output": [ "good:test" ] 45 | } 46 | }, 47 | }); 48 | 49 | let mgr = new this.Manager({ 50 | clients: { 51 | "Maga": { 52 | "homepage": "http://badgerbadgerbadger.com", 53 | "version": "1.0.0", 54 | "foo": "bar", 55 | "versionNotes": "http://badgerbadgerbadger.com", 56 | "platforms": platforms, 57 | } 58 | } 59 | }); 60 | 61 | // mgr.logger = console; 62 | yield mgr.init(); 63 | 64 | try { 65 | yield mgr.download('Maga'); 66 | throw -1; 67 | } catch (err) { 68 | err.message.should.eql(`Maga missing configuration for this platform.`); 69 | } 70 | }; 71 | 72 | 73 | 74 | test['client not supported on platform'] = function*() { 75 | const platforms = this.buildPlatformConfig('invalid', process.arch, { 76 | "url": "http://badgerbadgerbadger.com", 77 | "bin": "maga", 78 | "commands": { 79 | "sanity": { 80 | "args": ['test'], 81 | "output": [ "good:test" ] 82 | } 83 | }, 84 | }); 85 | 86 | let mgr = new this.Manager({ 87 | clients: { 88 | "Maga": { 89 | "homepage": "http://badgerbadgerbadger.com", 90 | "version": "1.0.0", 91 | "foo": "bar", 92 | "versionNotes": "http://badgerbadgerbadger.com", 93 | "platforms": platforms, 94 | } 95 | } 96 | }); 97 | 98 | // mgr.logger = console; 99 | yield mgr.init(); 100 | 101 | try { 102 | yield mgr.download('Maga'); 103 | throw -1; 104 | } catch (err) { 105 | err.message.should.eql(`Maga missing configuration for this platform.`); 106 | } 107 | }; 108 | 109 | 110 | 111 | test['download info not available'] = function*() { 112 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 113 | "bin": "maga", 114 | "commands": { 115 | "sanity": { 116 | "args": ['test'], 117 | "output": [ "good:test" ] 118 | } 119 | }, 120 | }); 121 | 122 | let mgr = new this.Manager({ 123 | clients: { 124 | "Maga": { 125 | "homepage": "http://badgerbadgerbadger.com", 126 | "version": "1.0.0", 127 | "foo": "bar", 128 | "versionNotes": "http://badgerbadgerbadger.com", 129 | "platforms": platforms, 130 | } 131 | } 132 | }); 133 | 134 | // mgr.logger = console; 135 | yield mgr.init(); 136 | 137 | try { 138 | yield mgr.download('Maga'); 139 | throw -1; 140 | } catch (err) { 141 | err.message.should.eql(`Download info not available for Maga`); 142 | } 143 | }; 144 | 145 | 146 | 147 | test['download url not available'] = function*() { 148 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 149 | download: { 150 | type: 'blah' 151 | }, 152 | "bin": "maga", 153 | "commands": { 154 | "sanity": { 155 | "args": ['test'], 156 | "output": [ "good:test" ] 157 | } 158 | }, 159 | }); 160 | 161 | let mgr = new this.Manager({ 162 | clients: { 163 | "Maga": { 164 | "homepage": "http://badgerbadgerbadger.com", 165 | "version": "1.0.0", 166 | "foo": "bar", 167 | "versionNotes": "http://badgerbadgerbadger.com", 168 | "platforms": platforms, 169 | } 170 | } 171 | }); 172 | 173 | // mgr.logger = console; 174 | yield mgr.init(); 175 | 176 | try { 177 | yield mgr.download('Maga'); 178 | throw -1; 179 | } catch (err) { 180 | err.message.should.eql(`Download info not available for Maga`); 181 | } 182 | }; 183 | 184 | 185 | test['download unpack command not available'] = function*() { 186 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 187 | download: { 188 | url: 'http://adsfasd.com' 189 | }, 190 | "bin": "maga", 191 | "commands": { 192 | "sanity": { 193 | "args": ['test'], 194 | "output": [ "good:test" ] 195 | } 196 | }, 197 | }); 198 | 199 | let mgr = new this.Manager({ 200 | clients: { 201 | "Maga": { 202 | "homepage": "http://badgerbadgerbadger.com", 203 | "version": "1.0.0", 204 | "foo": "bar", 205 | "versionNotes": "http://badgerbadgerbadger.com", 206 | "platforms": platforms, 207 | } 208 | } 209 | }); 210 | 211 | // mgr.logger = console; 212 | yield mgr.init(); 213 | 214 | try { 215 | yield mgr.download('Maga'); 216 | throw -1; 217 | } catch (err) { 218 | err.message.should.eql(`Download info not available for Maga`); 219 | } 220 | }; 221 | 222 | 223 | 224 | test['download fails'] = function*() { 225 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 226 | download: { 227 | url: `${this.archiveTestHost}/invalid.zip`, 228 | type: 'zip' 229 | }, 230 | "bin": "maga", 231 | "commands": { 232 | "sanity": { 233 | "args": ['test'], 234 | "output": [ "good:test" ] 235 | } 236 | }, 237 | }); 238 | 239 | let mgr = new this.Manager({ 240 | clients: { 241 | "Maga": { 242 | "homepage": "http://badgerbadgerbadger.com", 243 | "version": "1.0.0", 244 | "foo": "bar", 245 | "versionNotes": "http://badgerbadgerbadger.com", 246 | "platforms": platforms, 247 | } 248 | } 249 | }); 250 | 251 | // mgr.logger = console; 252 | yield mgr.init(); 253 | 254 | try { 255 | yield mgr.download('Maga'); 256 | throw -1; 257 | } catch (err) { 258 | err.message.should.contain(`Error downloading package for Maga`); 259 | } 260 | }; 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | test['unsupported archive type'] = function*() { 269 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 270 | download: { 271 | url: `${this.archiveTestHost}/maga2-good.zip`, 272 | type: 'blah' 273 | }, 274 | "bin": "maga2", 275 | "commands": { 276 | "sanity": { 277 | "args": ['test'], 278 | "output": [ "good:test" ] 279 | } 280 | }, 281 | }); 282 | 283 | let mgr = new this.Manager({ 284 | clients: { 285 | "Maga2": { 286 | "homepage": "http://badgerbadgerbadger.com", 287 | "version": "1.0.0", 288 | "foo": "bar", 289 | "versionNotes": "http://badgerbadgerbadger.com", 290 | "platforms": platforms, 291 | } 292 | } 293 | }); 294 | 295 | // mgr.logger = console; 296 | yield mgr.init(); 297 | 298 | try { 299 | yield mgr.download('Maga2'); 300 | throw -1; 301 | } catch (err) { 302 | err.message.should.contain(`Unsupported archive type: blah`); 303 | } 304 | }; 305 | 306 | 307 | 308 | test['hash sha256 mismatch'] = function*() { 309 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 310 | download: { 311 | url: `${this.archiveTestHost}/maga2-good.zip`, 312 | type: 'blah', 313 | sha256: 'blahblahblah' 314 | }, 315 | "bin": "maga2", 316 | "commands": { 317 | "sanity": { 318 | "args": ['test'], 319 | "output": [ "good:test" ] 320 | } 321 | }, 322 | }); 323 | 324 | let mgr = new this.Manager({ 325 | clients: { 326 | "Maga2": { 327 | "homepage": "http://badgerbadgerbadger.com", 328 | "version": "1.0.0", 329 | "foo": "bar", 330 | "versionNotes": "http://badgerbadgerbadger.com", 331 | "platforms": platforms, 332 | } 333 | } 334 | }); 335 | 336 | // mgr.logger = console; 337 | yield mgr.init(); 338 | 339 | try { 340 | yield mgr.download('Maga2'); 341 | throw -1; 342 | } catch (err) { 343 | err.message.should.contain(`Hash mismatch (using sha256): expected blahblahblah; got e7781ccd95e2db9246dbe8c1deaf9238ab4428a713d08080689834fd68a25652`); 344 | } 345 | }; 346 | 347 | 348 | 349 | test['hash md5 mismatch'] = function*() { 350 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 351 | download: { 352 | url: `${this.archiveTestHost}/maga2-good.zip`, 353 | type: 'blah', 354 | md5: 'blahblahblah' 355 | }, 356 | "bin": "maga2", 357 | "commands": { 358 | "sanity": { 359 | "args": ['test'], 360 | "output": [ "good:test" ] 361 | } 362 | }, 363 | }); 364 | 365 | let mgr = new this.Manager({ 366 | clients: { 367 | "Maga2": { 368 | "homepage": "http://badgerbadgerbadger.com", 369 | "version": "1.0.0", 370 | "foo": "bar", 371 | "versionNotes": "http://badgerbadgerbadger.com", 372 | "platforms": platforms, 373 | } 374 | } 375 | }); 376 | 377 | // mgr.logger = console; 378 | yield mgr.init(); 379 | 380 | try { 381 | yield mgr.download('Maga2'); 382 | throw -1; 383 | } catch (err) { 384 | err.message.should.contain(`Hash mismatch (using md5): expected blahblahblah; got dff641865ffb9b44d53f1f9def74f2e6`); 385 | } 386 | }; 387 | 388 | 389 | 390 | test['url regex mismatch'] = function*() { 391 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 392 | download: { 393 | url: `${this.archiveTestHost}/maga2-good.zip`, 394 | type: 'zip' 395 | }, 396 | "bin": "maga2", 397 | "commands": { 398 | "sanity": { 399 | "args": ['test'], 400 | "output": [ "good:test" ] 401 | } 402 | }, 403 | }); 404 | 405 | let mgr = new this.Manager({ 406 | clients: { 407 | "Maga2": { 408 | "homepage": "http://badgerbadgerbadger.com", 409 | "version": "1.0.0", 410 | "foo": "bar", 411 | "versionNotes": "http://badgerbadgerbadger.com", 412 | "platforms": platforms, 413 | } 414 | } 415 | }); 416 | 417 | // mgr.logger = console; 418 | yield mgr.init(); 419 | 420 | try { 421 | yield mgr.download('Maga2', { 422 | urlRegex: /blahblah/i 423 | }); 424 | throw -1; 425 | } catch (err) { 426 | err.message.should.contain(`Download URL failed regex check`); 427 | } 428 | }; 429 | 430 | 431 | 432 | test['custom unpack handler'] = { 433 | before: function*() { 434 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 435 | download: { 436 | url: `${this.archiveTestHost}/maga2-good.zip`, 437 | type: 'invalid' 438 | }, 439 | "bin": "maga2", 440 | "commands": { 441 | "sanity": { 442 | "args": ['test'], 443 | "output": [ "good:test" ] 444 | } 445 | }, 446 | }); 447 | 448 | let mgr = new this.Manager({ 449 | clients: { 450 | "Maga2": { 451 | "homepage": "http://badgerbadgerbadger.com", 452 | "version": "1.0.0", 453 | "foo": "bar", 454 | "versionNotes": "http://badgerbadgerbadger.com", 455 | "platforms": platforms, 456 | } 457 | } 458 | }); 459 | 460 | // mgr.logger = console; 461 | yield mgr.init(); 462 | 463 | this.mgr = mgr; 464 | }, 465 | 466 | success: function*() { 467 | let spy = this.mocker.spy(() => Promise.resolve()); 468 | 469 | yield this.mgr.download('Maga2', { 470 | unpackHandler: spy 471 | }); 472 | 473 | spy.should.have.been.calledOnce; 474 | spy.getCall(0).args.length.should.eql(2); 475 | }, 476 | 477 | fail: function*() { 478 | try { 479 | yield this.mgr.download('Maga2', { 480 | unpackHandler: () => Promise.reject(new Error('foo!')) 481 | }); 482 | throw -1; 483 | } catch (err) { 484 | err.message.should.contain(`foo!`); 485 | } 486 | } 487 | }; 488 | 489 | 490 | 491 | 492 | 493 | test['unpacks and verifies ok'] = function*() { 494 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 495 | download: { 496 | url: `${this.archiveTestHost}/maga2-good.zip`, 497 | type: 'zip' 498 | }, 499 | "bin": "maga2", 500 | "commands": { 501 | "sanity": { 502 | "args": ['test'], 503 | "output": [ "good:test" ] 504 | } 505 | }, 506 | }); 507 | 508 | let mgr = new this.Manager({ 509 | clients: { 510 | "Maga2": { 511 | "homepage": "http://badgerbadgerbadger.com", 512 | "version": "1.0.0", 513 | "foo": "bar", 514 | "versionNotes": "http://badgerbadgerbadger.com", 515 | "platforms": platforms, 516 | } 517 | } 518 | }); 519 | 520 | // mgr.logger = console; 521 | yield mgr.init(); 522 | 523 | let ret = yield mgr.download('Maga2', { 524 | urlRegex: /localhost/ 525 | }); 526 | 527 | const downloadFolder = _get(ret, 'downloadFolder', ''); 528 | _get(ret, 'downloadFile', '').should.eql(path.join(downloadFolder, `archive.zip`)); 529 | _get(ret, 'unpackFolder', '').should.eql(path.join(downloadFolder, `unpacked`)); 530 | 531 | _get(ret, 'client.state.available', '').should.be.true; 532 | _get(ret, 'client.activeCli.fullPath', '').should.eql(path.join(downloadFolder, `unpacked`, 'maga2')); 533 | 534 | mgr.clients['Maga2'].should.eql(ret.client); 535 | }; 536 | 537 | 538 | 539 | test['unpacked but no binary found'] = function*() { 540 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 541 | download: { 542 | url: `${this.archiveTestHost}/no-maga2.tgz`, 543 | type: 'tar' 544 | }, 545 | "bin": "maga2", 546 | "commands": { 547 | "sanity": { 548 | "args": ['test'], 549 | "output": [ "good:test" ] 550 | } 551 | }, 552 | }); 553 | 554 | let mgr = new this.Manager({ 555 | clients: { 556 | "Maga2": { 557 | "homepage": "http://badgerbadgerbadger.com", 558 | "version": "1.0.0", 559 | "foo": "bar", 560 | "versionNotes": "http://badgerbadgerbadger.com", 561 | "platforms": platforms, 562 | } 563 | } 564 | }); 565 | 566 | // mgr.logger = console; 567 | yield mgr.init(); 568 | 569 | let ret = yield mgr.download('Maga2'); 570 | 571 | _get(ret, 'client.state.available', '').should.be.false; 572 | _get(ret, 'client.state.failReason', '').should.eql('notFound'); 573 | _get(ret, 'client.activeCli.fullPath', '').should.eql(''); 574 | }; 575 | 576 | 577 | test['unpacked but sanity check failed'] = function*() { 578 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 579 | download: { 580 | url: `${this.archiveTestHost}/maga2-bad.zip`, 581 | type: 'zip' 582 | }, 583 | "bin": "maga2", 584 | "commands": { 585 | "sanity": { 586 | "args": ['test'], 587 | "output": [ "good:test" ] 588 | } 589 | }, 590 | }); 591 | 592 | let mgr = new this.Manager({ 593 | clients: { 594 | "Maga2": { 595 | "homepage": "http://badgerbadgerbadger.com", 596 | "version": "1.0.0", 597 | "foo": "bar", 598 | "versionNotes": "http://badgerbadgerbadger.com", 599 | "platforms": platforms, 600 | } 601 | } 602 | }); 603 | 604 | // mgr.logger = console; 605 | yield mgr.init(); 606 | 607 | let ret = yield mgr.download('Maga2'); 608 | 609 | _get(ret, 'client.state.available', '').should.be.false; 610 | _get(ret, 'client.state.failReason', '').should.eql('sanityCheckFail'); 611 | _get(ret, 'client.activeCli.fullPath', '').should.eql(''); 612 | }; 613 | 614 | 615 | 616 | test['unpacked and set to required name'] = function*() { 617 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 618 | download: { 619 | url: `${this.archiveTestHost}/maga2-good-rename.zip`, 620 | type: 'zip', 621 | bin: 'maga2-special' 622 | }, 623 | "bin": "maga2", 624 | "commands": { 625 | "sanity": { 626 | "args": ['test'], 627 | "output": [ "good:test" ] 628 | } 629 | }, 630 | }); 631 | 632 | let mgr = new this.Manager({ 633 | clients: { 634 | "Maga2": { 635 | "homepage": "http://badgerbadgerbadger.com", 636 | "version": "1.0.0", 637 | "foo": "bar", 638 | "versionNotes": "http://badgerbadgerbadger.com", 639 | "platforms": platforms, 640 | } 641 | } 642 | }); 643 | 644 | // mgr.logger = console; 645 | yield mgr.init(); 646 | 647 | let ret = yield mgr.download('Maga2'); 648 | 649 | _get(ret, 'client.state.available', '').should.be.true; 650 | }; 651 | 652 | 653 | test['unpacked updated version and copied over old version'] = function*(){ 654 | var downloadOpts = { 655 | download: { 656 | url: `${this.archiveTestHost}/maga2-good.zip`, 657 | type: 'zip', 658 | bin: 'maga2' 659 | }, 660 | "bin": "maga3", 661 | "commands": { 662 | "sanity": { 663 | "args": ['test'], 664 | "output": [ "good:test" ] 665 | } 666 | }, 667 | }; 668 | 669 | let buildMgrOpts = function(scope, downloadOpts){ 670 | return { 671 | clients: { 672 | "Maga2": { 673 | "platforms": scope.buildPlatformConfig(process.platform, process.arch, downloadOpts), 674 | } 675 | } 676 | } 677 | }; 678 | 679 | let mgr = new this.Manager(buildMgrOpts(this, downloadOpts)); 680 | // mgr.logger = console; 681 | 682 | yield mgr.init(); 683 | 684 | let ret = yield mgr.download('Maga2'); 685 | 686 | const downloadFolder = _get(ret, 'downloadFolder', ''); 687 | 688 | _get(ret, 'client.activeCli.fullPath', '').should.eql(path.join(downloadFolder, 'unpacked', 'maga3')); 689 | 690 | // Settings params for 2nd download 691 | downloadOpts.download = { 692 | url: `${this.archiveTestHost}/maga2-good-rename.zip`, 693 | type: 'zip', 694 | bin: 'maga2-special' 695 | }; 696 | 697 | let mgr2 = new this.Manager(buildMgrOpts(this, downloadOpts)); 698 | // mgr2.logger = console; 699 | 700 | yield mgr2.init(); 701 | 702 | let ret2 = yield mgr2.download('Maga2', {downloadFolder: path.join(downloadFolder, '..')}); 703 | 704 | _get(ret2, 'client.activeCli.fullPath', '').should.eql(path.join(downloadFolder, 'unpacked', 'maga3')); 705 | 706 | // check that maga3 === maga2-special 707 | const hash1 = md5File.sync(path.join(downloadFolder, 'unpacked', 'maga3')); 708 | const hash2 = md5File.sync(path.join(downloadFolder, 'unpacked', 'maga2-special')); 709 | 710 | hash1.should.eql(hash2); 711 | } 712 | 713 | 714 | 715 | 716 | test['hash sha256 match'] = function*() { 717 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 718 | download: { 719 | url: `${this.archiveTestHost}/maga2-good.zip`, 720 | type: 'zip', 721 | sha256: 'e7781ccd95e2db9246dbe8c1deaf9238ab4428a713d08080689834fd68a25652' 722 | }, 723 | "bin": "maga2", 724 | "commands": { 725 | "sanity": { 726 | "args": ['test'], 727 | "output": [ "good:test" ] 728 | } 729 | }, 730 | }); 731 | 732 | let mgr = new this.Manager({ 733 | clients: { 734 | "Maga2": { 735 | "homepage": "http://badgerbadgerbadger.com", 736 | "version": "1.0.0", 737 | "foo": "bar", 738 | "versionNotes": "http://badgerbadgerbadger.com", 739 | "platforms": platforms, 740 | } 741 | } 742 | }); 743 | 744 | // mgr.logger = console; 745 | yield mgr.init(); 746 | 747 | let ret = yield mgr.download('Maga2'); 748 | 749 | mgr.clients['Maga2'].should.eql(ret.client); 750 | }; 751 | 752 | 753 | 754 | 755 | test['hash md5 match'] = function*() { 756 | const platforms = this.buildPlatformConfig(process.platform, process.arch, { 757 | download: { 758 | url: `${this.archiveTestHost}/maga2-good.zip`, 759 | type: 'zip', 760 | md5: 'dff641865ffb9b44d53f1f9def74f2e6' 761 | }, 762 | "bin": "maga2", 763 | "commands": { 764 | "sanity": { 765 | "args": ['test'], 766 | "output": [ "good:test" ] 767 | } 768 | }, 769 | }); 770 | 771 | let mgr = new this.Manager({ 772 | clients: { 773 | "Maga2": { 774 | "homepage": "http://badgerbadgerbadger.com", 775 | "version": "1.0.0", 776 | "foo": "bar", 777 | "versionNotes": "http://badgerbadgerbadger.com", 778 | "platforms": platforms, 779 | } 780 | } 781 | }); 782 | 783 | // mgr.logger = console; 784 | yield mgr.init(); 785 | 786 | let ret = yield mgr.download('Maga2'); 787 | 788 | mgr.clients['Maga2'].should.eql(ret.client); 789 | }; 790 | --------------------------------------------------------------------------------