diff --git a/integration/regtest-node.js b/integration/regtest-node.js index bc4909bb..6a2cb942 100644 --- a/integration/regtest-node.js +++ b/integration/regtest-node.js @@ -117,6 +117,12 @@ describe('Node Functionality', function() { }); }); + node.start(function(err) { + if (err) { + throw err; + } + }); + }); }); diff --git a/lib/node.js b/lib/node.js index a6d2c152..ef8a90aa 100644 --- a/lib/node.js +++ b/lib/node.js @@ -10,7 +10,6 @@ var _ = bitcore.deps._; var index = require('./'); var log = index.log; var Bus = require('./bus'); -var BaseService = require('./service'); var errors = require('./errors'); function Node(config) { @@ -18,8 +17,6 @@ function Node(config) { return new Node(config); } - var self = this; - this.errors = errors; // So services can use errors without having to have bitcore-node as a dependency this.log = log; this.network = null; @@ -38,13 +35,6 @@ function Node(config) { this._setNetwork(config); - this.start(function(err) { - if(err) { - return self.emit('error', err); - } - self.emit('ready'); - }); - } util.inherits(Node, EventEmitter); @@ -137,35 +127,53 @@ Node.prototype.getServiceOrder = function() { return stack; }; -Node.prototype._instantiateService = function(service) { +Node.prototype._startService = function(serviceInfo, callback) { var self = this; - $.checkState(_.isObject(service.config)); - $.checkState(!service.config.node); + $.checkState(_.isObject(serviceInfo.config)); + $.checkState(!serviceInfo.config.node); + $.checkState(!serviceInfo.config.name); - var config = service.config; + log.info('Starting ' + serviceInfo.name); + + var config = serviceInfo.config; config.node = this; - config.name = service.name; - var mod = new service.module(config); + config.name = serviceInfo.name; + var service = new serviceInfo.module(config); - // include in loaded services - this.services[service.name] = mod; - - // add API methods - var methodData = mod.getAPIMethods(); - methodData.forEach(function(data) { - var name = data[0]; - var instance = data[1]; - var method = data[2]; - - if (self[name]) { - throw new Error('Existing API method exists: ' + name); - } else { - self[name] = function() { - return method.apply(instance, arguments); - }; + service.start(function(err) { + if (err) { + return callback(err); } + + // include in loaded services + self.services[serviceInfo.name] = service; + + // add API methods + var methodData = service.getAPIMethods(); + var methodNameConflicts = []; + methodData.forEach(function(data) { + var name = data[0]; + var instance = data[1]; + var method = data[2]; + + if (self[name]) { + methodNameConflicts.push(name); + } else { + self[name] = function() { + return method.apply(instance, arguments); + }; + } + }); + + if (methodNameConflicts.length > 0) { + return callback(new Error('Existing API method(s) exists: ' + methodNameConflicts.join(', '))); + } + + callback(); + }); + }; Node.prototype.start = function(callback) { @@ -175,15 +183,15 @@ Node.prototype.start = function(callback) { async.eachSeries( servicesOrder, function(service, next) { - log.info('Starting ' + service.name); - try { - self._instantiateService(service); - } catch(err) { + self._startService(service, next); + }, + function(err) { + if (err) { return callback(err); } - self.services[service.name].start(next); - }, - callback + self.emit('ready'); + callback(); + } ); }; @@ -198,8 +206,13 @@ Node.prototype.stop = function(callback) { async.eachSeries( services, function(service, next) { - log.info('Stopping ' + service.name); - self.services[service.name].stop(next); + if (self.services[service.name]) { + log.info('Stopping ' + service.name); + self.services[service.name].stop(next); + } else { + log.info('Stopping ' + service.name + ' (not started)'); + setImmediate(next); + } }, callback ); diff --git a/lib/scaffold/start.js b/lib/scaffold/start.js index a459a7a4..e4528324 100644 --- a/lib/scaffold/start.js +++ b/lib/scaffold/start.js @@ -83,27 +83,31 @@ function registerSyncHandlers(node, delay) { function logSyncStatus() { log.info( - 'Sync Status: Tip:', node.services.db.tip.hash, + 'Database Sync Status: Tip:', node.services.db.tip.hash, 'Height:', node.services.db.tip.__height, 'Rate:', count/10, 'blocks per second' ); } - node.on('synced', function() { - clearInterval(interval); - }); - node.on('ready', function() { - node.services.db.on('addblock', function(block) { - count++; - // Initialize logging if not already instantiated - if (!interval) { - interval = setInterval(function() { - logSyncStatus(); - count = 0; - }, delay); - } - }); + + if (node.services.db) { + node.on('synced', function() { + clearInterval(interval); + logSyncStatus(); + }); + node.services.db.on('addblock', function(block) { + count++; + // Initialize logging if not already instantiated + if (!interval) { + interval = setInterval(function() { + logSyncStatus(); + count = 0; + }, delay); + } + }); + } + }); node.on('stopping', function() { @@ -111,6 +115,22 @@ function registerSyncHandlers(node, delay) { }); } +/** + * Will shutdown a node and then the process + * @param {Object} _process - The Node.js process object + * @param {Node} node - The Bitcore Node instance + */ +function cleanShutdown(_process, node) { + node.stop(function(err) { + if(err) { + log.error('Failed to stop services: ' + err); + return _process.exit(1); + } + log.info('Halted'); + _process.exit(0); + }); +} + /** * Will register event handlers to stop the node for `process` events * `uncaughtException` and `SIGINT`. @@ -133,15 +153,7 @@ function registerExitHandlers(_process, node) { }); } if (options.sigint) { - node.stop(function(err) { - if(err) { - log.error('Failed to stop services: ' + err); - return _process.exit(1); - } - - log.info('Halted'); - _process.exit(0); - }); + cleanShutdown(_process, node); } } @@ -190,6 +202,16 @@ function start(options) { log.error(err); }); + node.start(function(err) { + if(err) { + log.error('Failed to start services'); + if (err.stack) { + log.error(err.stack); + } + start.cleanShutdown(process, node); + } + }); + return node; } @@ -235,3 +257,4 @@ module.exports.registerExitHandlers = registerExitHandlers; module.exports.registerSyncHandlers = registerSyncHandlers; module.exports.setupServices = setupServices; module.exports.spawnChildProcess = spawnChildProcess; +module.exports.cleanShutdown = cleanShutdown; diff --git a/lib/services/db.js b/lib/services/db.js index 45537fa6..042c7bab 100644 --- a/lib/services/db.js +++ b/lib/services/db.js @@ -75,6 +75,7 @@ DB.prototype._setDataPath = function() { }; DB.prototype.start = function(callback) { + var self = this; if (!fs.existsSync(this.dataPath)) { mkdirp.sync(this.dataPath); diff --git a/lib/services/web.js b/lib/services/web.js index 0684b387..b5b2bca5 100644 --- a/lib/services/web.js +++ b/lib/services/web.js @@ -48,7 +48,7 @@ WebService.prototype.stop = function(callback) { } callback(); - }) + }); }; WebService.prototype.setupAllRoutes = function() { @@ -60,7 +60,7 @@ WebService.prototype.setupAllRoutes = function() { this.app.use('/' + this.node.services[key].getRoutePrefix(), subApp); this.node.services[key].setupRoutes(subApp, express); } else { - log.info('Not setting up routes for ' + service.name); + log.debug('No routes defined for: ' + key); } } }; @@ -186,4 +186,4 @@ WebService.prototype.socketMessageHandler = function(message, socketCallback) { } }; -module.exports = WebService; \ No newline at end of file +module.exports = WebService; diff --git a/test/node.unit.js b/test/node.unit.js index 19e1f2e6..6593f7b5 100644 --- a/test/node.unit.js +++ b/test/node.unit.js @@ -65,7 +65,6 @@ describe('Bitcore Node', function() { var TestNode = proxyquire('../lib/node', {}); TestNode.prototype.start = sinon.spy(); var node = new TestNode(config); - TestNode.prototype.start.callCount.should.equal(1); node._unloadedServices.length.should.equal(1); node._unloadedServices[0].name.should.equal('test1'); node._unloadedServices[0].module.should.equal(TestService); @@ -105,29 +104,6 @@ describe('Bitcore Node', function() { should.exist(regtest); node.network.should.equal(regtest); }); - it('should emit error if an error occurred starting services', function(done) { - var config = { - datadir: 'testdir', - services: [ - { - name: 'test1', - module: TestService - } - ], - }; - var TestNode = proxyquire('../lib/node', {}); - TestNode.prototype.start = function(callback) { - setImmediate(function() { - callback(new Error('error')); - }); - }; - var node = new TestNode(config); - node.once('error', function(err) { - should.exist(err); - err.message.should.equal('error'); - done(); - }); - }); }); describe('#openBus', function() { @@ -214,11 +190,12 @@ describe('Bitcore Node', function() { }); }); - describe('#_instantiateService', function() { + describe('#_startService', function() { it('will instantiate an instance and load api methods', function() { var node = new Node(baseConfig); function TestService() {} util.inherits(TestService, BaseService); + TestService.prototype.start = sinon.stub().callsArg(0); TestService.prototype.getData = function() {}; TestService.prototype.getAPIMethods = function() { return [ @@ -230,9 +207,28 @@ describe('Bitcore Node', function() { module: TestService, config: {} }; - node._instantiateService(service); - should.exist(node.services.testservice); - should.exist(node.getData); + node._startService(service, function(err) { + if (err) { + throw err; + } + TestService.prototype.start.callCount.should.equal(1); + should.exist(node.services.testservice); + should.exist(node.getData); + }); + }); + it('will give an error from start', function() { + var node = new Node(baseConfig); + function TestService() {} + util.inherits(TestService, BaseService); + TestService.prototype.start = sinon.stub().callsArgWith(0, new Error('test')); + var service = { + name: 'testservice', + module: TestService, + config: {} + }; + node._startService(service, function(err) { + err.message.should.equal('test'); + }); }); }); @@ -318,7 +314,7 @@ describe('Bitcore Node', function() { node.start(function(err) { should.exist(err); - err.message.should.match(/^Existing API method exists/); + err.message.should.match(/^Existing API method\(s\) exists\:/); done(); }); diff --git a/test/scaffold/start.integration.js b/test/scaffold/start.integration.js index e6985068..2374d163 100644 --- a/test/scaffold/start.integration.js +++ b/test/scaffold/start.integration.js @@ -18,6 +18,7 @@ describe('#start', function() { config: {} }); }; + TestNode.prototype.start = sinon.stub().callsArg(0); TestNode.prototype.on = sinon.stub(); TestNode.prototype.chain = { on: sinon.stub() @@ -39,7 +40,29 @@ describe('#start', function() { node.should.be.instanceof(TestNode); done(); }); - + it('shutdown with an error from start', function(done) { + var TestNode = proxyquire('../../lib/node', {}); + TestNode.prototype.start = function(callback) { + setImmediate(function() { + callback(new Error('error')); + }); + }; + var starttest = proxyquire('../../lib/scaffold/start', { + '../node': TestNode + }); + starttest.cleanShutdown = sinon.stub(); + starttest({ + path: __dirname, + config: { + services: [], + datadir: './testdir' + } + }); + setImmediate(function() { + starttest.cleanShutdown.callCount.should.equal(1); + done(); + }); + }); it('require each bitcore-node service with explicit config', function(done) { var node; var TestNode = function(options) { @@ -51,6 +74,7 @@ describe('#start', function() { } }); }; + TestNode.prototype.start = sinon.stub().callsArg(0); TestNode.prototype.on = sinon.stub(); TestNode.prototype.chain = { on: sinon.stub() diff --git a/test/scaffold/start.unit.js b/test/scaffold/start.unit.js index 1b507db0..3cd8c9de 100644 --- a/test/scaffold/start.unit.js +++ b/test/scaffold/start.unit.js @@ -124,6 +124,55 @@ describe('#start', function() { }, 35); }); }); + describe('#cleanShutdown', function() { + it('will call node stop and process exit', function() { + var log = { + info: sinon.stub(), + error: sinon.stub() + }; + var cleanShutdown = proxyquire('../../lib/scaffold/start', { + '../': { + log: log + } + }).cleanShutdown; + var node = { + stop: sinon.stub().callsArg(0) + }; + var _process = { + exit: sinon.stub() + }; + cleanShutdown(_process, node); + setImmediate(function() { + node.stop.callCount.should.equal(1); + _process.exit.callCount.should.equal(1); + _process.exit.args[0][0].should.equal(0); + }); + }); + it('will log error during shutdown and exit with status 1', function() { + var log = { + info: sinon.stub(), + error: sinon.stub() + }; + var cleanShutdown = proxyquire('../../lib/scaffold/start', { + '../': { + log: log + } + }).cleanShutdown; + var node = { + stop: sinon.stub().callsArgWith(0, new Error('test')) + }; + var _process = { + exit: sinon.stub() + }; + cleanShutdown(_process, node); + setImmediate(function() { + node.stop.callCount.should.equal(1); + log.error.callCount.should.equal(1); + _process.exit.callCount.should.equal(1); + _process.exit.args[0][0].should.equal(1); + }); + }); + }); describe('#registerExitHandlers', function() { var log = { info: sinon.stub(),