diff --git a/app.js b/app.js index 3683a12..077248b 100644 --- a/app.js +++ b/app.js @@ -1,10 +1,11 @@ var express = require('express.io'); var path = require('path'); var fs = require('fs'); +var nodeModel = require('./lib/node'); var config; var app = express(); -app.http().io(); +app.io(); if(fs.existsSync('./config.js')){ config = require('./config'); @@ -12,37 +13,27 @@ if(fs.existsSync('./config.js')){ config = require('./config.default'); } -var node = new require('./lib/node')(config); +var node = new nodeModel(config); -// catch 404 and forward to error handler -app.use(function(req, res, next) { - var err = new Error('Not Found'); - err.status = 404; - next(err); -}); +console.log(node.stats); -// error handlers -// development error handler -// will print stacktrace -if (app.get('env') === 'development') { - app.use(function(err, req, res, next) { - res.status(err.status || 500); - res.render('error', { - message: err.message, - error: err - }); - }); +var gracefulShutdown = function() { + console.log("Received kill signal, shutting down gracefully."); + + node.stop(); + console.log("Closed node watcher"); + + setTimeout(function(){ + console.log("Closed out remaining connections."); + process.exit() + }, 2*1000); } -// production error handler -// no stacktraces leaked to user -app.use(function(err, req, res, next) { - res.status(err.status || 500); - res.render('error', { - message: err.message, - error: {} - }); -}); +// listen for TERM signal .e.g. kill +process.on('SIGTERM', gracefulShutdown); + +// listen for INT signal e.g. Ctrl-C +process.on('SIGINT', gracefulShutdown); module.exports = app; diff --git a/bin/www b/bin/www index e77df43..bc3849c 100755 --- a/bin/www +++ b/bin/www @@ -1,9 +1,3 @@ #!/usr/bin/env node -var debug = require('debug')('eth-netstats'); -var app = require('../app'); - -app.set('port', process.env.PORT || 3000); - -var server = app.listen(app.get('port'), function() { - debug('Express server listening on port ' + server.address().port); -}); +var debug = require('debug')('eth-netstatsservice'); +var app = require('../app'); \ No newline at end of file diff --git a/config.default.js b/config.default.js index d5427c8..75bc7fa 100644 --- a/config.default.js +++ b/config.default.js @@ -1,7 +1,6 @@ var config = { name: 'Node', type: 'C++', - os: 'linux', rpcHost: 'localhost', rpcPort: '8080', serverHost: 'localhost', diff --git a/lib/node.js b/lib/node.js index 12ad086..a1ef3a4 100644 --- a/lib/node.js +++ b/lib/node.js @@ -1,65 +1,254 @@ -var Node = function Node(options) +var web3 = require('ethereum.js'); +var _ = require('underscore'); +var os = require('os'); + +var MAX_BLOCKS_HISTORY = 12, + LOWEST_TIMESTAMP = 0; + +function Node(options) { this.options = options; this.info = { name: options.name, ip: options.rpcHost, type: options.type, - os: options.os + os: os.platform(), + os_v: os.release() }; this.info.id = this.info.ip; - this.info.stats = { + this.stats = { active: false, - peers: 0, + listening: false, mining: false, - block: { - number: 0, - hash: '?', - timestamp: 0 - }, + peers: 0, + pending: 0, + gasPrice: 0, + block: {}, + blocks: [], + difficulty: [], uptime: { down: 0, inc: 0, total: 0 - } + }, + errors: [] } - this.web3 = require('ethereum.js'); + this.pendingWatch = false; + this.chainWatch = false; + this.updateInterval = false; - var sock = new this.web3.providers.HttpSyncProvider('http://' + this.options.rpcHost + ':' + this.options.rpcPort); - this.web3.setProvider(sock); + var sock = new web3.providers.HttpSyncProvider('http://' + this.options.rpcHost + ':' + this.options.rpcPort); + web3.setProvider(sock); + + this.init(); return this; } -Node.prototype.update = function() +Node.prototype.isActive = function() { - var eth = this.web3.eth; + this.stats.uptime.inc++; + this.stats.errors = []; try { - this.info.stats.peers = eth.peerCount; + this.stats.peers = web3.eth.peerCount; + this.stats.active = true; + + return true; } catch (err) { - this.info.stats.peers = null; - } + this.stats.active = false; + this.stats.listening = false; + this.stats.mining = false; + this.stats.peers = 0; + this.stats.uptime.down++; - if(this.info.stats.peers != null) { - this.info.stats.block = eth.block(parseInt(eth.number)); - if(this.info.stats.block.hash != '?'){ - this.info.stats.block.difficulty = this.web3.toDecimal(this.info.stats.block.difficulty); + this.stats.errors.push({ + code: '1', + msg: err + }); + + return false; + } +} + +Node.prototype.getBlock = function(number) +{ + var block = { + number: 0, + hash: '?', + difficulty: 0, + timestamp: 0 + }; + + if(typeof number === 'undefined'){ + try { + number = parseInt(web3.eth.number); + + if(number === this.stats.block.number + 1) + return this.stats.block; + } + catch (err) { + this.stats.errors.push({ + code: '3', + msg: err + }); } - this.info.stats.mining = eth.mining; - this.info.stats.active = true; - } else { - this.info.stats.active = false; - this.info.stats.uptime.down++; } - this.info.stats.uptime.inc++; - this.info.stats.uptime.total = ((this.info.stats.uptime.inc - this.info.stats.uptime.down) / this.info.stats.uptime.inc) * 100; + try { + block = web3.eth.block(number); - return this.info; + if(block.hash != '?' && typeof block.difficulty !== 'undefined') + { + block.difficulty = web3.toDecimal(block.difficulty); + } + } + catch (err) { + this.stats.errors.push({ + code: '2', + msg: err + }); + } + + return block; +} + +Node.prototype.getLatestBlocks = function() +{ + var bestBlock = this.stats.block.number; + var maxIterations = MAX_BLOCKS_HISTORY; + var minBlock = 0; + + if(this.stats.blocks.length > 0) + { + maxIterations = Math.min(bestBlock - this.stats.blocks[0].number, MAX_BLOCKS_HISTORY); + } + + minBlock = Math.max(0, parseInt(bestBlock) - maxIterations); + + for (var i = minBlock; i < bestBlock; i++) + { + this.addBlockHistory(this.getBlock(i)); + }; + + this.addBlockHistory(this.stats.block); + + this.calculateBlockTimes(); + this.stats.blocktimeAvg = this.blockTimesAvg(); + this.stats.difficulty = this.difficultyChart(); +} + +Node.prototype.addBlockHistory = function(block) +{ + if(this.stats.blocks.length === MAX_BLOCKS_HISTORY) + { + LOWEST_TIMESTAMP = this.stats.blocks[MAX_BLOCKS_HISTORY - 1].timestamp; + this.stats.blocks.pop(); + } + + this.stats.blocks.unshift(block); +} + +Node.prototype.calculateBlockTimes = function() +{ + var self = this; + + var blockTimes = _.map(this.stats.blocks, function(block, key, list) + { + var diff = block.timestamp - (key < list.length - 1 ? list[key + 1].timestamp : LOWEST_TIMESTAMP); + + self.stats.blocks[key].blocktime = diff; + + return diff; + }); + + return blockTimes; +} + +Node.prototype.blockTimesAvg = function() +{ + var sum = _.reduce(this.stats.blocks, function(memo, block) { return memo + block.blocktime;}, 0); + + return sum/this.stats.blocks.length; +} + +Node.prototype.difficultyChart = function() +{ + return difficulty = _.map(this.stats.blocks, function(block) + { + return block.difficulty; + }); +} + +Node.prototype.uptime = function() +{ + this.stats.uptime.total = ((this.stats.uptime.inc - this.stats.uptime.down) / this.stats.uptime.inc) * 100; +} + +Node.prototype.getStats = function() +{ + if(this.isActive()) + { + this.stats.block = this.getBlock(); + + // Get last MAX_BLOCKS_HISTORY blocks for calculations + if(this.stats.block.number > 0) + this.getLatestBlocks(); + + this.stats.mining = web3.eth.mining; + this.stats.gasPrice = web3.toDecimal(web3.eth.gasPrice); + this.stats.listening = web3.eth.listening; + } + + this.uptime(); +} + +Node.prototype.update = function() +{ + this.getStats(); + + return this.stats; }; +Node.prototype.setWatches = function() +{ + var self = this; + this.pendingWatch = web3.eth.watch('pending'); + this.pendingWatch.changed(function(log) { + console.log('pending changed'); + self.stats.pending = parseInt(log.number); + }); + + this.chainWatch = web3.eth.watch('chain'); + this.chainWatch.messages(function(log) { + console.log('block changed'); + self.update(); + }); + + this.updateInterval = setInterval(function(){ + self.update(); + }, 1000); +} + +Node.prototype.init = function() +{ + this.update(); + this.setWatches(); +} + +Node.prototype.stop = function() +{ + if(this.updateInterval) + clearInterval(this.updateInterval); + + if(this.pendingWatch) + this.pendingWatch.uninstall(); + + if(this.chainWatch) + this.chainWatch.uninstall(); +} + module.exports = Node; \ No newline at end of file diff --git a/package.json b/package.json index a97bd51..3d09715 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dependencies": { "debug": "~2.0.0", "ethereum.js": "*", - "express.io": "^1.1.13" + "express.io": "^1.1.13", + "underscore": "^1.7.0" } }