diff --git a/cli/main.js b/cli/main.js index dc0b98a9..8eb36763 100644 --- a/cli/main.js +++ b/cli/main.js @@ -46,6 +46,7 @@ function main() { .command('start') .description('Start the current node') .option('-c, --config ', 'Specify the directory with Bitcore Node configuration') + .option('-d, --daemon', 'Make bitcore-node a daemon (running in the background)') .action(function(cmd){ if (cmd.config) { cmd.config = path.resolve(process.cwd(), cmd.config); @@ -54,6 +55,9 @@ function main() { if (!configInfo) { configInfo = defaultConfig(); } + if(cmd.daemon) { + configInfo.config.daemon = true; + } start(configInfo); }); diff --git a/lib/scaffold/start.js b/lib/scaffold/start.js index bae01a23..a459a7a4 100644 --- a/lib/scaffold/start.js +++ b/lib/scaffold/start.js @@ -7,6 +7,9 @@ var bitcore = require('bitcore'); var _ = bitcore.deps._; var $ = bitcore.util.preconditions; var log = index.log; +var child_process = require('child_process'); +var fs = require('fs'); + log.debug = function() {}; /** @@ -111,10 +114,10 @@ function registerSyncHandlers(node, delay) { /** * Will register event handlers to stop the node for `process` events * `uncaughtException` and `SIGINT`. - * @param {Object} proc - The Node.js process + * @param {Object} _process - The Node.js process * @param {Node} node */ -function registerExitHandlers(proc, node) { +function registerExitHandlers(_process, node) { function exitHandler(options, err) { if (err) { @@ -126,27 +129,27 @@ function registerExitHandlers(proc, node) { if(err) { log.error('Failed to stop services: ' + err); } - proc.exit(-1); + _process.exit(-1); }); } if (options.sigint) { node.stop(function(err) { if(err) { log.error('Failed to stop services: ' + err); - return proc.exit(1); + return _process.exit(1); } log.info('Halted'); - proc.exit(0); + _process.exit(0); }); } } //catches uncaught exceptions - proc.on('uncaughtException', exitHandler.bind(null, {exit:true})); + _process.on('uncaughtException', exitHandler.bind(null, {exit:true})); //catches ctrl+c event - proc.on('SIGINT', exitHandler.bind(null, {sigint:true})); + _process.on('SIGINT', exitHandler.bind(null, {sigint:true})); } /** @@ -164,16 +167,20 @@ function registerExitHandlers(proc, node) { function start(options) { var fullConfig = _.clone(options.config); - fullConfig.services = setupServices(require, options.path, options.config); + fullConfig.services = start.setupServices(require, options.path, options.config); fullConfig.datadir = path.resolve(options.path, options.config.datadir); + if (fullConfig.daemon) { + start.spawnChildProcess(fullConfig.datadir, process); + } + var node = new BitcoreNode(fullConfig); // set up the event handlers for logging sync information - registerSyncHandlers(node); + start.registerSyncHandlers(node); // setup handlers for uncaught exceptions and ctrl+c - registerExitHandlers(process, node); + start.registerExitHandlers(process, node); node.on('ready', function() { log.info('Bitcore Node ready'); @@ -187,7 +194,44 @@ function start(options) { } +/** + * This function will fork the passed in process and exit the parent process + * in order to daemonize the process. If there is already a daemon for this pid (process), + * then the function just returns. Stdout and stderr both append to one file, 'bitcore-node.log' + * located in the datadir. + * @param {String} datadir - The data directory where the bitcoin blockchain and config live. + * @param {Object} _process - The process that needs to fork a child and then, itself, exit. + */ +function spawnChildProcess(datadir, _process) { + + if (_process.env.__bitcore_node) { + return _process.pid; + } + + var args = [].concat(_process.argv); + args.shift(); + var script = args.shift(); + var env = _process.env; + var cwd = _process.cwd(); + env.__bitcore_node = true; + + var stderr = fs.openSync(datadir + '/bitcore-node.log', 'a+'); + var stdout = stderr; + + var cp_opt = { + stdio: ['ignore', stdout, stderr], + env: env, + cwd: cwd, + detached: true + }; + + var child = child_process.spawn(_process.execPath, [script].concat(args), cp_opt); + child.unref(); + return _process.exit(); +} + module.exports = start; module.exports.registerExitHandlers = registerExitHandlers; module.exports.registerSyncHandlers = registerSyncHandlers; module.exports.setupServices = setupServices; +module.exports.spawnChildProcess = spawnChildProcess; diff --git a/test/scaffold/start.unit.js b/test/scaffold/start.unit.js index 2a592045..1b507db0 100644 --- a/test/scaffold/start.unit.js +++ b/test/scaffold/start.unit.js @@ -163,4 +163,104 @@ describe('#start', function() { }); }); }); + describe('#spawnChildProcess', function() { + + it('should build the appropriate arguments to spawn a child process', function() { + var child = { + unref: function() {} + }; + var _process = { + exit: function() {}, + env: { + __bitcore_node: false + }, + argv: [ + 'node', + 'bitcore-node' + ], + cwd: function(){return ''}, + pid: 999, + execPath: '/tmp' + }; + var fd = {}; + var spawn = sinon.stub().returns(child); + var openSync = sinon.stub().returns(fd); + var spawnChildProcess = proxyquire('../../lib/scaffold/start', { + fs: { + openSync: openSync + }, + child_process: { + spawn: spawn + } + }).spawnChildProcess; + + spawnChildProcess('/tmp', _process); + + spawn.callCount.should.equal(1); + spawn.args[0][0].should.equal(_process.execPath); + var expected = [].concat(_process.argv); + expected.shift(); + spawn.args[0][1].should.deep.equal(expected); + var cp_opt = { + stdio: ['ignore', fd, fd], + env: _process.env, + cwd: '', + detached: true + }; + spawn.args[0][2].should.deep.equal(cp_opt); + openSync.callCount.should.equal(1); + openSync.args[0][0].should.equal('/tmp/bitcore-node.log'); + openSync.args[0][1].should.equal('a+'); + }); + it('should not spawn a new child process if there is already a daemon running', function() { + var _process = { + exit: function() {}, + env: { + __bitcore_node: true + }, + argv: [ + 'node', + 'bitcore-node' + ], + cwd: 'cwd', + pid: 999, + execPath: '/tmp' + }; + var spawnChildProcess = proxyquire('../../lib/scaffold/start', {}).spawnChildProcess; + spawnChildProcess('/tmp', _process).should.equal(999); + }); + }); + describe('daemon', function() { + var sandbox; + var spawn; + var setup; + var registerSync; + var registerExit; + var start = require('../../lib/scaffold/start'); + var options = { + config: { + datadir: '/tmp', + daemon: true + } + } + beforeEach(function() { + sandbox = sinon.sandbox.create(); + spawn = sandbox.stub(start, 'spawnChildProcess', function() {}); + setup = sandbox.stub(start, 'setupServices', function() {}); + registerSync = sandbox.stub(start, 'registerSyncHandlers', function() {}); + registerExit = sandbox.stub(start, 'registerExitHandlers', function() {}); + }); + afterEach(function() { + sandbox.restore(); + }); + it('call spawnChildProcess if there is a config option to do so', function() { + start(options); + spawn.callCount.should.equal(1); + }); + it('not call spawnChildProcess if there is not an option to do so', function() { + options.config.daemon = false; + start(options); + spawn.callCount.should.equal(0); + }); + }); });