guvnor
Version:
A node process manager that isn't spanners all the way down
247 lines (195 loc) • 6.25 kB
JavaScript
var Autowire = require('wantsit').Autowire
var EventEmitter = require('events').EventEmitter
var util = require('util')
var async = require('async')
var shortId = require('shortid')
/**
* Creates a dnode instance that listens on a filesystem socket for
* a message to stop this process.
*
* The socket's read/write access is controlled by filesystem permissions.
*/
var ProcessRPC = function () {
EventEmitter.call(this)
this._userInfo = Autowire
this._parentProcess = Autowire
this._dnode = Autowire
this._fs = Autowire
this._usage = Autowire
this._heapdump = Autowire
this._config = Autowire
this._latencyMonitor = Autowire
this._process = Autowire
this._logger = Autowire
this._heapSnapshots = {}
}
util.inherits(ProcessRPC, EventEmitter)
ProcessRPC.prototype.startDnodeServer = function (callback) {
this.socket = this._config.guvnor.rundir + '/processes/' + process.pid
var api = {}
var nonAPIMethods = [
// ProcessRPC
'afterPropertiesSet', 'getSocket', 'startDnodeServer',
// EventEmitter
'addListener', 'on', 'once', 'removeListener', 'removeAllListeners', 'setMaxListeners', 'listeners', 'emit'
]
for (var method in this) {
// ignore anything that isn't a function, is prefixed with '_' or is in the nonAPIMethods array
if (typeof this[method] !== 'function' ||
method.substring(0, 1) === '_' ||
nonAPIMethods.indexOf(method) !== -1) {
continue
}
api[method] = this[method].bind(this)
}
// publish RPC methods
var dnode = this._dnode(api, {
timeout: this._config.guvnor.rpctimeout
})
async.series([
dnode.listen.bind(dnode, this.socket),
this._fs.chown.bind(this._fs, this.socket, this._userInfo.getUid(), this._userInfo.getGid()),
this._fs.chmod.bind(this._fs, this.socket, parseInt('0770', 8))
], function (error) {
callback(error, this.socket)
}.bind(this))
}
ProcessRPC.prototype.kill = function (callback) {
this._parentProcess.send('process:stopping')
this._fs.unlink(this.socket, function (error) {
if (callback) {
callback(error)
}
this._process.exit(error ? 1 : 0)
}.bind(this))
}
ProcessRPC.prototype.restart = function (callback) {
this._parentProcess.send('process:restarting')
this._fs.unlink(this.socket, function (error) {
if (callback) {
callback(error)
}
this._process.exit(error ? 1 : 0)
}.bind(this))
}
ProcessRPC.prototype.send = function () {
var args = Array.prototype.slice.call(arguments)
var callback = args[args.length - 1]
if (typeof callback === 'function') {
args.length--
} else {
callback = function () {}
}
this._process.emit.apply(process, arguments)
callback()
}
ProcessRPC.prototype.reportStatus = function (callback) {
this._usage.lookup(process.pid, {
keepHistory: true
}, function (error, result) {
if (error) {
return callback(error)
}
var memory = process.memoryUsage()
callback(undefined, {
pid: process.pid,
uid: process.getuid(),
gid: process.getgid(),
user: this._userInfo.getUserName(),
group: this._userInfo.getGroupName(),
name: process.title,
uptime: process.uptime(),
cpu: result ? result.cpu : 0,
heapTotal: memory.heapTotal,
heapUsed: memory.heapUsed,
residentSize: memory.rss,
time: Date.now(),
cwd: process.cwd(),
argv: process.argv,
execArgv: process.execArgv,
latency: this._latencyMonitor.latency
})
}.bind(this))
}
ProcessRPC.prototype.dumpHeap = function (callback) {
this._parentProcess.send('process:heapdump:start')
var here = process.cwd()
async.waterfall([
this._heapdump.writeSnapshot.bind(this._heapdump),
function (fileName, callback) {
var heapSnapshot = {
id: shortId.generate(),
date: Date.now(),
path: here + '/' + fileName
}
// only the filename is passed, not the whole path :(
// https://github.com/bnoordhuis/node-heapdump/issues/42
this._fs.stat(heapSnapshot.path, function (error, stats) {
if (!error) {
heapSnapshot.size = stats.size
}
callback(error, heapSnapshot)
})
}.bind(this)
], function (error, heapSnapshot) {
if (error) {
this._parentProcess.send('process:heapdump:error', error)
} else {
this._heapSnapshots[heapSnapshot.id] = heapSnapshot
this._parentProcess.send('process:heapdump:complete', heapSnapshot)
}
if (callback) {
callback(error, heapSnapshot)
}
}.bind(this))
}
ProcessRPC.prototype.fetchHeapSnapshot = function (id, onReadable, onData, onEnd, callback) {
var heapSnapshot = this._heapSnapshots[id]
if (!heapSnapshot) {
var error = new Error('No snapshot for ' + id + ' available')
error.code = 'ENOENT'
return callback(error)
}
// create a readable stream of the snapshot file
var stream = this._fs.createReadStream(heapSnapshot.path)
stream.on('end', onEnd)
stream.on('readable', onReadable)
var read = function () {
var buf = stream.read()
if (!buf) {
return
}
onData(buf.toString('base64'))
}
callback(undefined, heapSnapshot, read)
}
ProcessRPC.prototype.removeHeapSnapshot = function (id, callback) {
var heapSnapshot = this._heapSnapshots[id]
if (!heapSnapshot) {
this._parentProcess.send('process:heapdump:removed', {
id: id
})
return callback()
}
this._fs.unlink(heapSnapshot.path, function () {
this._parentProcess.send('process:heapdump:removed', heapSnapshot)
callback()
}.bind(this))
}
ProcessRPC.prototype.forceGc = function (callback) {
this._parentProcess.send('process:gc:start')
if (global && typeof global.gc === 'function') {
global.gc()
}
this._parentProcess.send('process:gc:complete')
if (callback) process.nextTick(callback)
}
ProcessRPC.prototype.write = function (string, callback) {
this._parentProcess.send('process:stdin:write', string)
if (callback) process.nextTick(callback)
}
ProcessRPC.prototype.signal = function (signal, callback) {
this._parentProcess.send('process:signal', signal)
if (callback) process.nextTick(callback)
}
module.exports = ProcessRPC