diff --git a/cli/main.js b/cli/main.js index 33e18c74..04506701 100644 --- a/cli/main.js +++ b/cli/main.js @@ -5,12 +5,14 @@ var path = require('path'); var bitcorenode = require('..'); function main() { + /* jshint maxstatements: 100 */ - // local commands var version = bitcorenode.version; var create = bitcorenode.scaffold.create; var add = bitcorenode.scaffold.add; var start = bitcorenode.scaffold.start; + var remove = bitcorenode.scaffold.remove; + var callMethod = bitcorenode.scaffold.callMethod; var findConfig = bitcorenode.scaffold.findConfig; var defaultConfig = bitcorenode.scaffold.defaultConfig; @@ -69,7 +71,7 @@ function main() { modules: modules }; add(opts, function() { - console.log('Successfully added modules: ', modules.join(', ')); + console.log('Successfully added module(s):', modules.join(', ')); }); }).on('--help', function() { console.log(' Examples:'); @@ -79,6 +81,51 @@ function main() { console.log(); }); + program + .command('remove ') + .alias('uninstall') + .description('Uninstall a module for the current node') + .action(function(modules){ + var configInfo = findConfig(process.cwd()); + if (!configInfo) { + throw new Error('Could not find configuration, see `bitcore-node create --help`'); + } + var opts = { + path: configInfo.path, + modules: modules + }; + remove(opts, function() { + console.log('Successfully removed module(s):', modules.join(', ')); + }); + }).on('--help', function() { + console.log(' Examples:'); + console.log(); + console.log(' $ bitcore-node remove wallet-service'); + console.log(' $ bitcore-node remove insight-api'); + console.log(); + }); + + program + .command('call [params...]') + .description('Call an API method') + .action(function(method, params) { + var configInfo = findConfig(process.cwd()); + if (!configInfo) { + configInfo = defaultConfig(); + } + var options = { + protocol: 'http', + host: 'localhost', + port: configInfo.config.port + }; + callMethod(options, method, params, function(err, data) { + if (err) { + throw err; + } + console.log(JSON.stringify(data, null, 2)); + }); + }); + program.parse(process.argv); if (process.argv.length === 2) { diff --git a/index.js b/index.js index ec446e0c..7225237e 100644 --- a/index.js +++ b/index.js @@ -15,7 +15,9 @@ module.exports.modules.AddressModule = require('./lib/modules/address'); module.exports.scaffold = {}; module.exports.scaffold.create = require('./lib/scaffold/create'); module.exports.scaffold.add = require('./lib/scaffold/add'); +module.exports.scaffold.remove = require('./lib/scaffold/remove'); module.exports.scaffold.start = require('./lib/scaffold/start'); +module.exports.scaffold.callMethod = require('./lib/scaffold/call-method'); module.exports.scaffold.findConfig = require('./lib/scaffold/find-config'); module.exports.scaffold.defaultConfig = require('./lib/scaffold/default-config'); diff --git a/lib/db.js b/lib/db.js index a3ac5059..99bdb43e 100644 --- a/lib/db.js +++ b/lib/db.js @@ -81,6 +81,14 @@ DB.prototype.stop = function(callback) { setImmediate(callback); }; +DB.prototype.getInfo = function(callback) { + var self = this; + setImmediate(function() { + var info = self.node.bitcoind.getInfo(); + callback(null, info); + }); +}; + DB.prototype.getBlock = function(hash, callback) { var self = this; @@ -289,6 +297,7 @@ DB.prototype.blockHandler = function(block, add, callback) { DB.prototype.getAPIMethods = function() { var methods = [ + ['getInfo', this, this.getInfo, 0], ['getBlock', this, this.getBlock, 1], ['getTransaction', this, this.getTransaction, 2], ['sendTransaction', this, this.sendTransaction, 1], diff --git a/lib/scaffold/call-method.js b/lib/scaffold/call-method.js new file mode 100644 index 00000000..b5ca9ad6 --- /dev/null +++ b/lib/scaffold/call-method.js @@ -0,0 +1,43 @@ +'use strict'; + +var socketClient = require('socket.io-client'); + +/** + * Calls a remote node with a method and params + * @param {Object} options + * @param {String} method - The name of the method to call + * @param {Array} params - An array of the params for the method + * @param {Function} done - The callback function + */ +function callMethod(options, method, params, done) { + + var host = options.host; + var protocol = options.protocol; + var port = options.port; + var url = protocol + '://' + host + ':' + port; + var socketOptions = { + reconnection: false, + connect_timeout: 5000 + }; + var socket = socketClient(url, socketOptions); + + socket.on('connect', function(){ + socket.send({ + method: method, + params: params, + }, function(response) { + if (response.error) { + return done(new Error(response.error.message)); + } + socket.close(); + done(null, response.result); + }); + }); + + socket.on('connect_error', done); + + return socket; + +} + +module.exports = callMethod; diff --git a/lib/scaffold/remove.js b/lib/scaffold/remove.js new file mode 100644 index 00000000..4a513ee9 --- /dev/null +++ b/lib/scaffold/remove.js @@ -0,0 +1,144 @@ +'use strict'; + +var async = require('async'); +var fs = require('fs'); +var npm = require('npm'); +var path = require('path'); +var spawn = require('child_process').spawn; +var bitcore = require('bitcore'); +var $ = bitcore.util.preconditions; +var _ = bitcore.deps._; + +/** + * Will remove a module from bitcore-node.json + * @param {String} configFilePath - The absolute path to the configuration file + * @param {String} module - The name of the module + * @param {Function} done + */ +function removeConfig(configFilePath, module, done) { + $.checkArgument(path.isAbsolute(configFilePath), 'An absolute path is expected'); + fs.readFile(configFilePath, function(err, data) { + if (err) { + return done(err); + } + var config = JSON.parse(data); + $.checkState( + Array.isArray(config.modules), + 'Configuration file is expected to have a modules array.' + ); + // remove the module from the configuration + for (var i = 0; i < config.modules.length; i++) { + if (config.modules[i] === module) { + config.modules.splice(i, 1); + } + } + config.modules = _.unique(config.modules); + config.modules.sort(function(a, b) { + return a > b; + }); + fs.writeFile(configFilePath, JSON.stringify(config, null, 2), done); + }); +} + +/** + * Will uninstall a Node.js module and remove from package.json. + * @param {String} configDir - The absolute configuration directory path + * @param {String} module - The name of the module + * @param {Function} done + */ +function uninstallModule(configDir, module, done) { + $.checkArgument(path.isAbsolute(configDir), 'An absolute path is expected'); + $.checkArgument(_.isString(module), 'A string is expected for the module argument'); + + var child = spawn('npm', ['uninstall', module, '--save'], {cwd: configDir}); + + child.stdout.on('data', function(data) { + process.stdout.write(data); + }); + + child.stderr.on('data', function(data) { + process.stderr.write(data); + }); + + child.on('close', function(code) { + if (code !== 0) { + return done(new Error('There was an error uninstalling module: ' + module)); + } else { + return done(); + } + }); +} + +/** + * Will remove a Node.js module if it is installed. + * @param {String} configDir - The absolute configuration directory path + * @param {String} module - The name of the module + * @param {Function} done + */ +function removeModule(configDir, module, done) { + $.checkArgument(path.isAbsolute(configDir), 'An absolute path is expected'); + $.checkArgument(_.isString(module), 'A string is expected for the module argument'); + + // check if the module is installed + npm.load(function(err) { + if (err) { + return done(err); + } + npm.commands.ls([module], true /*silent*/, function(err, data, lite) { + if (err) { + return done(err); + } + if (lite.dependencies) { + uninstallModule(configDir, module, done); + } else { + done(); + } + }); + }); + +} + +/** + * Will remove the Node.js module and from the bitcore-node configuration. + * @param {String} options.cwd - The current working directory + * @param {String} options.dirname - The bitcore-node configuration directory + * @param {Array} options.modules - An array of strings of module names + * @param {Function} done - A callback function called when finished + */ +function remove(options, done) { + $.checkArgument(_.isObject(options)); + $.checkArgument(_.isFunction(done)); + $.checkArgument( + _.isString(options.path) && path.isAbsolute(options.path), + 'An absolute path is expected' + ); + $.checkArgument(Array.isArray(options.modules)); + + var configPath = options.path; + var modules = options.modules; + + var bitcoreConfigPath = path.resolve(configPath, 'bitcore-node.json'); + var packagePath = path.resolve(configPath, 'package.json'); + + if (!fs.existsSync(bitcoreConfigPath) || !fs.existsSync(packagePath)) { + return done( + new Error('Directory does not have a bitcore-node.json and/or package.json file.') + ); + } + + async.eachSeries( + modules, + function(module, next) { + // if the module is installed remove it + removeModule(configPath, module, function(err) { + if (err) { + return next(err); + } + // remove module to bitcore-node.json + removeConfig(bitcoreConfigPath, module, next); + }); + }, done + ); +} + +module.exports = remove; diff --git a/package.json b/package.json index 389c0bf0..8e30d982 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,10 @@ "memdown": "^1.0.0", "mkdirp": "0.5.0", "nan": "1.3.0", + "npm": "^2.14.1", "semver": "^5.0.1", - "socket.io": "^1.3.6" + "socket.io": "^1.3.6", + "socket.io-client": "^1.3.6" }, "devDependencies": { "aws-sdk": "~2.0.0-rc.15", diff --git a/test/db.unit.js b/test/db.unit.js index 2d111dd5..a7a7c4c8 100644 --- a/test/db.unit.js +++ b/test/db.unit.js @@ -369,7 +369,7 @@ describe('Bitcoin DB', function() { db.node = {}; db.node.modules = {}; var methods = db.getAPIMethods(); - methods.length.should.equal(4); + methods.length.should.equal(5); }); }); diff --git a/test/scaffold/call-method.unit.js b/test/scaffold/call-method.unit.js new file mode 100644 index 00000000..cf2291be --- /dev/null +++ b/test/scaffold/call-method.unit.js @@ -0,0 +1,93 @@ +'use strict'; + +var should = require('chai').should(); +var sinon = require('sinon'); +var proxyquire = require('proxyquire'); +var EventEmitter = require('events').EventEmitter; + +describe('#callMethod', function() { + + var expectedUrl = 'http://localhost:3001'; + var expectedOptions = { + reconnection: false, + connect_timeout: 5000 + }; + + var callOptions = { + host: 'localhost', + port: 3001, + protocol: 'http' + }; + + var callMethod; + + before(function() { + callMethod = proxyquire('../../lib/scaffold/call-method', { + 'socket.io-client': function(url, options) { + url.should.equal(expectedUrl); + options.should.deep.equal(expectedOptions); + return new EventEmitter(); + } + }); + }); + + it('handle a connection error', function(done) { + var socket = callMethod(callOptions, 'getInfo', null, function(err) { + should.exist(err); + err.message.should.equal('connect'); + done(); + }); + socket.emit('connect_error', new Error('connect')); + }); + + it('give an error response', function(done) { + var socket = callMethod(callOptions, 'getInfo', null, function(err) { + should.exist(err); + err.message.should.equal('response'); + done(); + }); + socket.send = function(opts, callback) { + opts.method.should.equal('getInfo'); + should.equal(opts.params, null); + var response = { + error: { + message: 'response' + } + }; + callback(response); + }; + socket.emit('connect'); + }); + + it('give result and close socket', function(done) { + var expectedData = { + version: 110000, + protocolversion: 70002, + blocks: 258614, + timeoffset: -2, + connections: 8, + difficulty: 112628548.66634709, + testnet: false, + relayfee: 1000, + errors: '' + }; + var socket = callMethod(callOptions, 'getInfo', null, function(err, data) { + should.not.exist(err); + data.should.deep.equal(expectedData); + socket.close.callCount.should.equal(1); + done(); + }); + socket.close = sinon.stub(); + socket.send = function(opts, callback) { + opts.method.should.equal('getInfo'); + should.equal(opts.params, null); + var response = { + error: null, + result: expectedData + }; + callback(response); + }; + socket.emit('connect'); + }); + +}); diff --git a/test/scaffold/remove.integration.js b/test/scaffold/remove.integration.js new file mode 100644 index 00000000..4228e18c --- /dev/null +++ b/test/scaffold/remove.integration.js @@ -0,0 +1,138 @@ +'use strict'; + +var should = require('chai').should(); +var sinon = require('sinon'); +var path = require('path'); +var fs = require('fs'); +var proxyquire = require('proxyquire'); +var mkdirp = require('mkdirp'); +var rimraf = require('rimraf'); +var remove = require('../../lib/scaffold/remove'); + +describe('#remove', function() { + + var basePath = path.resolve(__dirname, '..'); + var testDir = path.resolve(basePath, 'temporary-test-data'); + var startConfig = { + name: 'My Node', + modules: ['a', 'b', 'c'] + }; + var startPackage = {}; + + before(function(done) { + mkdirp(testDir + '/s0/s1', function(err) { + if (err) { + throw err; + } + fs.writeFile( + testDir + '/s0/s1/bitcore-node.json', + JSON.stringify(startConfig), + function(err) { + if (err) { + throw err; + } + fs.writeFile( + testDir + '/s0/s1/package.json', + JSON.stringify(startPackage), + done + ); + } + ); + }); + }); + + after(function(done) { + // cleanup testing directories + rimraf(testDir, function(err) { + if (err) { + throw err; + } + done(); + }); + }); + + describe('will modify scaffold files', function() { + + it('will give an error if expected files do not exist', function(done) { + remove({ + path: path.resolve(testDir, 's0'), + modules: ['b'] + }, function(err) { + should.exist(err); + err.message.match(/^Invalid state/); + done(); + }); + }); + + it('will update bitcore-node.json modules', function(done) { + var spawn = sinon.stub().returns({ + stdout: { + on: sinon.stub() + }, + stderr: { + on: sinon.stub() + }, + on: sinon.stub().callsArgWith(1, 0) + }); + var removetest = proxyquire('../../lib/scaffold/remove', { + 'child_process': { + spawn: spawn + }, + 'npm': { + load: sinon.stub().callsArg(0), + commands: { + ls: sinon.stub().callsArgWith(2, null, {}, { + dependencies: {} + }) + } + } + }); + removetest({ + path: path.resolve(testDir, 's0/s1/'), + modules: ['b'] + }, function(err) { + should.not.exist(err); + var configPath = path.resolve(testDir, 's0/s1/bitcore-node.json'); + var config = JSON.parse(fs.readFileSync(configPath)); + config.modules.should.deep.equal(['a', 'c']); + done(); + }); + }); + + it('will receive error from `npm uninstall`', function(done) { + var spawn = sinon.stub().returns({ + stdout: { + on: sinon.stub() + }, + stderr: { + on: sinon.stub() + }, + on: sinon.stub().callsArgWith(1, 1) + }); + var removetest = proxyquire('../../lib/scaffold/remove', { + 'child_process': { + spawn: spawn + }, + 'npm': { + load: sinon.stub().callsArg(0), + commands: { + ls: sinon.stub().callsArgWith(2, null, {}, { + dependencies: {} + }) + } + } + }); + + removetest({ + path: path.resolve(testDir, 's0/s1/'), + modules: ['b'] + }, function(err) { + should.exist(err); + err.message.should.equal('There was an error uninstalling module: b'); + done(); + }); + }); + + }); + +});