config: update configuration options for exec path

- config options for bitcoind to specify exec path of bitcoind
- config options to connect to multiple bitcoind processes
- systemd and upstart preferred methods to daemonize
This commit is contained in:
Braydon Fuller 2016-04-06 18:02:22 -04:00
parent 5932b34a1f
commit 0f24dd5f49
6 changed files with 291 additions and 183 deletions

View File

@ -28,7 +28,6 @@ var errors = require('./errors');
*
* @param {Object} config - The configuration of the node
* @param {Array} config.services - The array of services
* @param {String} config.datadir - The directory for data (e.g. bitcoind datadir)
* @param {Number} config.port - The HTTP port for services
* @param {Boolean} config.https - Enable https
* @param {Object} config.httpsOptions - Options for https
@ -52,8 +51,6 @@ function Node(config) {
$.checkArgument(Array.isArray(config.services));
this._unloadedServices = config.services;
}
$.checkState(config.datadir, 'Node config expects "datadir"');
this.datadir = config.datadir;
this.port = config.port;
this.https = config.https;
this.httpsOptions = config.httpsOptions;

View File

@ -15,10 +15,15 @@ function getDefaultBaseConfig(options) {
return {
path: process.cwd(),
config: {
datadir: options.datadir || path.resolve(process.env.HOME, '.bitcoin'),
network: options.network || 'livenet',
port: 3001,
services: ['bitcoind', 'db', 'address', 'web']
services: ['bitcoind', 'web'],
servicesConfig: {
bitcoind: {
datadir: options.datadir || path.resolve(process.env.HOME, '.bitcoin'),
exec: path.resolve(__dirname, '../../dist/bitcoind')
}
}
}
};
}

View File

@ -24,17 +24,22 @@ function getDefaultConfig(options) {
mkdirp.sync(defaultPath);
}
var defaultServices = ['bitcoind', 'db', 'address', 'web'];
var defaultServices = ['bitcoind', 'web'];
if (options.additionalServices) {
defaultServices = defaultServices.concat(options.additionalServices);
}
if (!fs.existsSync(defaultConfigFile)) {
var defaultConfig = {
datadir: path.resolve(defaultPath, './data'),
network: 'livenet',
port: 3001,
services: defaultServices
services: defaultServices,
servicesConfig: {
bitcoind: {
datadir: path.resolve(defaultPath, './data'),
exec: path.resolve(__dirname, '../../dist/bitcoind')
}
}
};
fs.writeFileSync(defaultConfigFile, JSON.stringify(defaultConfig, null, 2));
}

View File

@ -201,11 +201,6 @@ function start(options) {
}
fullConfig.services = start.setupServices(require, servicesPath, options.config);
fullConfig.datadir = path.resolve(options.path, options.config.datadir);
if (fullConfig.daemon) {
start.spawnChildProcess(fullConfig.datadir, process);
}
var node = new BitcoreNode(fullConfig);
@ -237,46 +232,9 @@ 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.exitHandler = exitHandler;
module.exports.registerSyncHandlers = registerSyncHandlers;
module.exports.setupServices = setupServices;
module.exports.spawnChildProcess = spawnChildProcess;
module.exports.cleanShutdown = cleanShutdown;

View File

@ -6,7 +6,6 @@ var spawn = require('child_process').spawn;
var util = require('util');
var mkdirp = require('mkdirp');
var bitcore = require('bitcore-lib');
var Address = bitcore.Address;
var zmq = require('zmq');
var async = require('async');
var LRU = require('lru-cache');
@ -27,12 +26,11 @@ var Transaction = require('../transaction');
* @param {Node} options.node - A reference to the node
*/
function Bitcoin(options) {
/* jshint maxstatements: 20 */
if (!(this instanceof Bitcoin)) {
return new Bitcoin(options);
}
this._reindex = false;
this._reindexWait = 1000;
Service.call(this, options);
// caches valid until there is a new block
@ -41,15 +39,32 @@ function Bitcoin(options) {
this.balanceCache = LRU(50000);
this.summaryCache = LRU(50000);
// caches valid indefinetly
// caches valid indefinitely
this.transactionCache = LRU(100000);
this.transactionInfoCache = LRU(100000);
this.blockCache = LRU(144);
this.blockHeaderCache = LRU(288);
this.zmqKnownTransactions = LRU(50);
this.options = options;
// bitcoind child process
this.spawn = false;
// available bitcoind nodes
this.nodes = [];
this.nodesIndex = 0;
Object.defineProperty(this, 'client', {
get: function() {
var client = this.nodes[this.nodesIndex].client;
this.nodesIndex = (this.nodesIndex + 1) % this.nodes.length;
return client;
},
enumerable: true,
configurable: false
});
$.checkState(this.node.datadir, 'Node is missing datadir property');
}
util.inherits(Bitcoin, Service);
Bitcoin.dependencies = [];
@ -77,15 +92,24 @@ Bitcoin.prototype.getAPIMethods = function() {
return methods;
};
Bitcoin.prototype._loadConfiguration = function() {
Bitcoin.prototype._loadSpawnConfiguration = function(node) {
/* jshint maxstatements: 25 */
$.checkArgument(this.node.datadir, 'Please specify "datadir" in configuration options');
var configPath = this.node.datadir + '/bitcoin.conf';
this.configuration = {};
$.checkArgument(this.options.spawn, 'Please specify "spawn" in bitcoind config options');
$.checkArgument(this.options.spawn.datadir, 'Please specify "spawn.datadir" in bitcoind config options');
$.checkArgument(this.options.spawn.exec, 'Please specify "spawn.exec" in bitcoind config options');
if (!fs.existsSync(this.node.datadir)) {
mkdirp.sync(this.node.datadir);
var spawnOptions = this.options.spawn;
var configPath = spawnOptions.datadir + '/bitcoin.conf';
this.spawn = {};
this.spawn.datadir = this.options.spawn.datadir;
this.spawn.exec = this.options.spawn.exec;
this.spawn.configPath = configPath;
this.spawn.config = {};
if (!fs.existsSync(spawnOptions.datadir)) {
mkdirp.sync(spawnOptions.datadir);
}
var file = fs.readFileSync(configPath);
@ -100,48 +124,50 @@ Bitcoin.prototype._loadConfiguration = function() {
} else {
value = option[1];
}
this.configuration[option[0]] = value;
this.spawn.config[option[0]] = value;
}
}
var spawnConfig = this.spawn.config;
$.checkState(
this.configuration.txindex && this.configuration.txindex === 1,
spawnConfig.txindex && spawnConfig.txindex === 1,
'"txindex" option is required in order to use transaction query features of bitcore-node. ' +
'Please add "txindex=1" to your configuration and reindex an existing database if ' +
'necessary with reindex=1'
);
$.checkState(
this.configuration.addressindex && this.configuration.addressindex === 1,
spawnConfig.addressindex && spawnConfig.addressindex === 1,
'"addressindex" option is required in order to use address query features of bitcore-node. ' +
'Please add "addressindex=1" to your configuration and reindex an existing database if ' +
'necessary with reindex=1'
);
$.checkState(
this.configuration.server && this.configuration.server === 1,
spawnConfig.server && spawnConfig.server === 1,
'"server" option is required to communicate to bitcoind from bitcore. ' +
'Please add "server=1" to your configuration and restart'
);
$.checkState(
this.configuration.zmqpubrawtx,
spawnConfig.zmqpubrawtx,
'"zmqpubrawtx" option is required to get event updates from bitcoind. ' +
'Please add "zmqpubrawtx=tcp://127.0.0.1:<port>" to your configuration and restart'
);
$.checkState(
this.configuration.zmqpubhashblock,
spawnConfig.zmqpubhashblock,
'"zmqpubhashblock" option is required to get event updates from bitcoind. ' +
'Please add "zmqpubhashblock=tcp://127.0.0.1:<port>" to your configuration and restart'
);
if (this.configuration.reindex && this.configuration.reindex === 1) {
if (spawnConfig.reindex && spawnConfig.reindex === 1) {
log.warn('Reindex option is currently enabled. This means that bitcoind is undergoing a reindex. ' +
'The reindex flag will start the index from beginning every time the node is started, so it ' +
'should be removed after the reindex has been initiated. Once the reindex is complete, the rest ' +
'of bitcore-node services will start.');
this._reindex = true;
'The reindex flag will start the index from beginning every time the node is started, so it ' +
'should be removed after the reindex has been initiated. Once the reindex is complete, the rest ' +
'of bitcore-node services will start.');
node._reindex = true;
}
};
@ -153,67 +179,37 @@ Bitcoin.prototype._resetCaches = function() {
this.summaryCache.reset();
};
Bitcoin.prototype._registerEventHandlers = function() {
Bitcoin.prototype._initChain = function(callback) {
var self = this;
this.zmqSubSocket.subscribe('hashblock');
this.zmqSubSocket.subscribe('rawtx');
this.zmqSubSocket.on('message', function(topic, message) {
var topicString = topic.toString('utf8');
if (topicString === 'rawtx') {
self.emit('tx', message);
} else if (topicString === 'hashblock') {
self._resetCaches();
self.tiphash = message.toString('hex');
self.client.getBlock(self.tiphash, function(err, response) {
if (err) {
return log.error(err);
}
self.height = response.result.height;
$.checkState(self.height >= 0);
self.emit('tip', self.height);
});
if(!self.node.stopping) {
self.syncPercentage(function(err, percentage) {
if (err) {
return log.error(err);
}
log.info('Bitcoin Height:', self.height, 'Percentage:', percentage.toFixed(2));
});
}
}
});
};
Bitcoin.prototype._onReady = function(result, callback) {
var self = this;
self._registerEventHandlers();
self.client.getInfo(function(err, response) {
self.client.getBestBlockHash(function(err, response) {
if (err) {
return callback(err);
}
self.height = response.result.blocks;
self.client.getBlockHash(0, function(err, response) {
self.client.getBlock(response.result, function(err, response) {
if (err) {
return callback(err);
}
var blockhash = response.result;
self.getBlock(blockhash, function(err, block) {
self.height = response.result.height;
self.client.getBlockHash(0, function(err, response) {
if (err) {
return callback(err);
}
self.tiphash = block.hash;
self.genesisBuffer = block.toBuffer();
self.emit('ready', result);
log.info('Bitcoin Daemon Ready');
callback();
var blockhash = response.result;
self.getBlock(blockhash, function(err, block) {
if (err) {
return callback(err);
}
self.genesisBuffer = block.toBuffer();
self.emit('ready');
log.info('Bitcoin Daemon Ready');
callback();
});
});
});
});
};
@ -229,27 +225,139 @@ Bitcoin.prototype._getNetworkOption = function() {
return networkOption;
};
/**
* Called by Node to start the service
* @param {Function} callback
*/
Bitcoin.prototype.start = function(callback) {
Bitcoin.prototype._zmqBlockHandler = function(node, message) {
var self = this;
var hex = message.toString('hex');
if (hex !== self.tiphash) {
self._resetCaches();
self.tiphash = message.toString('hex');
node.client.getBlock(self.tiphash, function(err, response) {
if (err) {
return log.error(err);
}
self.height = response.result.height;
$.checkState(self.height >= 0);
self.emit('tip', self.height);
});
if(!self.node.stopping) {
self.syncPercentage(function(err, percentage) {
if (err) {
return log.error(err);
}
log.info('Bitcoin Height:', self.height, 'Percentage:', percentage.toFixed(2));
});
}
}
};
Bitcoin.prototype._zmqTransactionHandler = function(node, message) {
var self = this;
var id = message.toString('binary');
if (!self.zmqKnownTransactions[id]) {
self.zmqKnownTransactions[id] = true;
self.emit('tx', message);
}
};
Bitcoin.prototype._subscribeZmqEvents = function(node) {
var self = this;
node.zmqSubSocket.subscribe('hashblock');
node.zmqSubSocket.subscribe('rawtx');
node.zmqSubSocket.on('message', function(topic, message) {
var topicString = topic.toString('utf8');
if (topicString === 'rawtx') {
self._zmqTransactionHandler(node, message);
} else if (topicString === 'hashblock') {
self._zmqBlockHandler(node, message);
}
});
};
Bitcoin.prototype._initZmqSubSocket = function(node, zmqUrl) {
var self = this;
node.zmqSubSocket = zmq.socket('sub');
node.zmqSubSocket.on('monitor_error', function(err) {
log.error('Error in monitoring: %s, will restart monitoring in 5 seconds', err);
setTimeout(function() {
self.zmqSubSocket.monitor(500, 0);
}, 5000);
});
node.zmqSubSocket.monitor(500, 0);
node.zmqSubSocket.connect(zmqUrl);
};
Bitcoin.prototype._checkReindex = function(node, callback) {
var self = this;
if (node._reindex) {
var interval = setInterval(function() {
node.client.syncPercentage(function(err, percentSynced) {
if (err) {
return log.error(err);
}
log.info('Bitcoin Core Daemon Reindex Percentage: ' + percentSynced.toFixed(2));
if (Math.round(percentSynced) >= 100) {
node._reindex = false;
self._subscribeZmqEvents(node);
callback();
clearInterval(interval);
}
});
}, self._reindexWait);
} else {
self._subscribeZmqEvents(node);
callback();
}
};
Bitcoin.prototype._loadTipFromNode = function(node, callback) {
var self = this;
node.client.getBestBlockHash(function(err, response) {
if (err) {
if (!(err instanceof Error)) {
log.warn(err.message);
}
return callback(new Error('Could not connect to bitcoind RPC'));
}
node.client.getBlock(response.result, function(err, response) {
if (err) {
return done(err);
}
self.height = response.result.height;
$.checkState(self.height >= 0);
self.emit('tip', self.height);
callback();
});
});
};
Bitcoin.prototype._spawnChildProcess = function(callback) {
var self = this;
self._loadConfiguration();
var node = {};
node._reindex = false;
node._reindexWait = 1000;
try {
self._loadSpawnConfiguration(node);
} catch(e) {
return callback(e);
}
var options = [
'--conf=' + path.resolve(this.node.datadir, './bitcoin.conf'),
'--datadir=' + this.node.datadir,
'--conf=' + path.resolve(this.spawn.configPath),
'--datadir=' + this.spawn.datadir,
];
if (self._getNetworkOption()) {
options.push(self._getNetworkOption());
}
self.spawn.process = spawn(this.spawn.exec, options, {stdio: 'inherit'});
self.process = spawn('bitcoind', options, {stdio: 'inherit'});
self.process.on('error', function(err) {
self.spawn.process.on('error', function(err) {
log.error(err);
});
@ -258,67 +366,104 @@ Bitcoin.prototype.start = function(callback) {
return done(new Error('Stopping while trying to connect to bitcoind.'));
}
self.client = new BitcoinRPC({
node.client = new BitcoinRPC({
protocol: 'http',
host: '127.0.0.1',
port: self.configuration.rpcport,
user: self.configuration.rpcuser,
pass: self.configuration.rpcpassword
port: self.spawn.config.rpcport,
user: self.spawn.config.rpcuser,
pass: self.spawn.config.rpcpassword
});
self.client.getBestBlockHash(function(err, response) {
if (err) {
if (!(err instanceof Error)) {
log.warn(err.message);
}
return done(new Error('Could not connect to bitcoind RPC'));
}
self.client.getBlock(response.result, function(err, response) {
if (err) {
return done(err);
}
self.height = response.result.height;
$.checkState(self.height >= 0);
self.emit('tip', self.height);
done();
});
});
self._loadTipFromNode(node, done);
}, function ready(err, result) {
}, function(err) {
if (err) {
return callback(err);
}
self.zmqSubSocket = zmq.socket('sub');
self._initZmqSubSocket(node, self.spawn.config.zmqpubrawtx);
self.zmqSubSocket.on('monitor_error', function(err) {
log.error('Error in monitoring: %s, will restart monitoring in 5 seconds', err);
setTimeout(function() {
self.zmqSubSocket.monitor(500, 0);
}, 5000);
self._checkReindex(node, function() {
if (err) {
return callback(err);
}
callback(null, node);
});
self.zmqSubSocket.monitor(500, 0);
self.zmqSubSocket.connect(self.configuration.zmqpubrawtx);
});
};
if (self._reindex) {
var interval = setInterval(function() {
self.syncPercentage(function(err, percentSynced) {
if (err) {
return log.error(err);
}
log.info('Bitcoin Core Daemon Reindex Percentage: ' + percentSynced.toFixed(2));
if (Math.round(percentSynced) >= 100) {
self._reindex = false;
self._onReady(result, callback);
clearInterval(interval);
}
});
}, self._reindexWait);
Bitcoin.prototype._connectProcess = function(config, callback) {
var self = this;
var node = {};
} else {
self._onReady(result, callback);
async.retry({times: 60, interval: 5000}, function(done) {
if (self.node.stopping) {
return done(new Error('Stopping while trying to connect to bitcoind.'));
}
node.client = new BitcoinRPC({
protocol: config.rpcprotocol || 'http',
host: config.rpchost || '127.0.0.1',
port: config.rpcport,
user: config.rpcuser,
pass: config.rpcpassword
});
self._loadTipFromNode(node, done);
}, function(err) {
if (err) {
return callback(err);
}
self._initZmqSubSocket(node, config.zmqpubrawtx);
callback(null, node);
});
};
/**
* Called by Node to start the service
* @param {Function} callback
*/
Bitcoin.prototype.start = function(callback) {
var self = this;
async.series([
function(next) {
if (self.options.spawn) {
self._spawnChildProcess(function(err, node) {
if (err) {
return next(err);
}
self.nodes.push(node);
next();
});
} else {
next();
}
},
function(next) {
if (self.options.connect) {
async.map(self.options.connect, self._connectProcess.bind(self), function(err, nodes) {
if (err) {
return callback(err);
}
for(var i = 0; i < nodes.length; i++) {
self.nodes.push(nodes[i]);
}
next();
});
} else {
next();
}
}
], function(err) {
if (err) {
return callback(err);
}
self._initChain(callback);
});
};
@ -950,15 +1095,15 @@ Bitcoin.prototype.getInfo = function(callback) {
* @param {Function} callback
*/
Bitcoin.prototype.stop = function(callback) {
if (this.process) {
this.process.once('exit', function(err, status) {
if (this.spawn && this.spawn.process) {
this.spawn.process.once('exit', function(err, status) {
if (err) {
return callback(err);
} else {
return callback();
}
});
this.process.kill('SIGHUP');
this.spawn.process.kill('SIGHUP');
} else {
callback();
}

View File

@ -40,7 +40,6 @@
],
"dependencies": {
"async": "^1.3.0",
"bindings": "^1.2.1",
"bitcoind-rpc": "^0.3.0",
"bitcore-lib": "^0.13.13",
"body-parser": "^1.13.3",
@ -50,7 +49,6 @@
"express": "^4.13.3",
"liftoff": "^2.2.0",
"lru-cache": "^4.0.1",
"memdown": "^1.0.0",
"mkdirp": "0.5.0",
"npm": "^2.14.1",
"semver": "^5.0.1",