Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
0.1.0 master process and ipc
  • Loading branch information
haraldrudell committed Dec 22, 2012
1 parent 6b0ed5e commit be9ec49
Show file tree
Hide file tree
Showing 11 changed files with 449 additions and 81 deletions.
96 changes: 28 additions & 68 deletions app.js
@@ -1,73 +1,33 @@
// Node God
// keeps node apps running
// master.js
// master process for nodegod
// © Harald Rudell 2012

var defaults = require('haraldops').init({appName: 'Node God',
appFolder: __dirname,
sessionSecret: 'veryGreat',
PORT: 1111 })
var establish = require('./lib/master/establish')
var uimanager = require('./lib/master/uimanager')
// http://nodejs.org/api/path.html
var path = require('path')

// https://github.com/visionmedia/express
var express = require('express')
var apprunner = require('apprunner')
//apprunner.enableAnomalyMail(false)
var cbc = apprunner.getCbCounter(/*{callback: initAppResult}*/)
var theSignal = 'SIGUSR2'
var marker = path.basename(__filename, path.extname(__filename))
var fileId = marker + ':' + process.pid

// get app and start error listener
var app = module.exports = express.createServer()
apprunner.initApp(defaults, app, cbc.add(initAppResult))
var godcontrol = require('./lib/godcontrol')
var godview = require('./routes/godview')
// determine if this process should launch ui
console.log(fileId, 'starting')
process.on(theSignal, uimanager.signalHandler)
process.on('uncaughtException', processException)
establish.establish(marker, theSignal, masterResult)

// Configuration
godview.setTitle(defaults.init.appName)
godcontrol.init(app, defaults)
app.configure(function(){
app.set('views', __dirname + '/views')
app.set('view engine', 'ejs')
//3 app.use(express.favicon())
app.use(express.bodyParser())
//3 app.use(express.methodOverride())
//3 app.use(express.cookieParser(defaults.sessionSecret))
app.use(express.cookieParser())
//3 app.use(express.session())
app.use(express.session({ secret: defaults.sessionSecret }))
app.use(express.methodOverride())
app.use(app.router)
app.use(express.static(__dirname + '/public'))
})
/*
require('ejsinbrowser').writeScript({
folder: app.settings.views,
ext: app.settings['view engine'],
jsGlobalVariable: 'NODEGOD',
templates: 'partials',
filename: __dirname + '/public/javascripts/templates.js'})
*/
app.configure('development', function(){
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }))
})
app.configure('production', function(){
app.use(express.errorHandler())
})

// Routes
app.get('/', godview.index)

/*3
app.listen(defaults.PORT, function(){
console.log("Express server listening on port %d in %s mode",
defaults.PORT,
app.settings.env)
})
*/

function initAppResult(err) {
if (err) throw err
function masterResult(isMaster) {
if (isMaster) {
console.log(fileId, 'launching ui process')
uimanager.launchUi(fileId, __dirname)
} else console.log(fileId, 'notified the master process')
}
app.listen(defaults.PORT, defaults.appInterface)
//app.listen(defaults.worldPort)
console.log('Application %s on node %s available on port %s in %s mode',
defaults.init.appName,
process.version,
defaults.PORT,
app.settings.env)

function processException() {
console.log(marker, 'uncaughtException')
Array.prototype.slice.call(arguments).forEach(function (value, index) {
console.log(index + ': ', value)
if (value instanceof Error && value.stack) console.log(value.stack)
})
}
2 changes: 1 addition & 1 deletion lib/appentity.js
Expand Up @@ -32,7 +32,7 @@ minSecondsCrashToCrash = 3
function AppEntity(conf) {
var crashCount = 0
var exitCode
var appState = appstate.appState(childCallback, conf.start)
var appState = appstate.appState(conf.id, childCallback, conf.start)
var lastLaunch
var lastCrash
var pid
Expand Down
31 changes: 25 additions & 6 deletions lib/appstate.js
@@ -1,6 +1,8 @@
// appstate.js
// manage one app through the master process
// © Harald Rudell 2012

var childmanager = require('./childmanager')
var masterlink = require('./masterlink')

module.exports = {
appState: appState,
Expand All @@ -9,11 +11,17 @@ module.exports = {
var states = [ 'debug', 'run', 'stop', 'crash', 'stopping' ]
var commands = [ 'run', 'stop', 'debug', 'restart', 'nodebug' ]

function appState(childCallback, launchCommand) {
var childManager = childmanager.childManager(childCallback, stateChange)
appLinks = {}
var master = masterlink.getMaster().on('message', receiver)

function appState(appId, childCallback, launchCommand) {
var commandQueue = [] // commands affect state
var state = 'stop'
var lastCommand
appLinks[appId] = {
sc: stateChange,
cb: childCallback,
}

return {
addValidCommands: addValidCommands,
Expand Down Expand Up @@ -67,7 +75,7 @@ function appState(childCallback, launchCommand) {
// stop or crash states
// we will now launch this app
lastCommand = command
childManager.launch(launchCommand) // launch process
master.write({app: appId, launch: launchCommand}) // launch process
break
}
// put run back onto the command queue
Expand All @@ -77,7 +85,7 @@ function appState(childCallback, launchCommand) {
if (state == 'stop' ||
state == 'crash') continue
// run or debug states
childManager.kill() // once child dies, we will continue
master.write({app: appId, kill: true}) // once child dies, we will continue
break
case 'debug':
if (state == 'debug') continue
Expand All @@ -87,7 +95,7 @@ function appState(childCallback, launchCommand) {
}
// stop or crash
lastCommand = command
childManager.launch(launchCommand, true) // launch process
master.write({app: appId, launch: launchCommand, debug: true}) // launch process
break
case 'restart':
if (state == 'stop' ||
Expand Down Expand Up @@ -115,4 +123,15 @@ function appState(childCallback, launchCommand) {
childCallback('state', state)
doNextCommand()
}
}

function receiver(message) {
var ok
var proc = message.app && appLinks[message.app]
if (proc) {
if (message.state && typeof proc.sc == 'function') proc.sc(ok = message.state)
if (message.exit != null && typeof proc.cb == 'function') proc.cb(ok = 'exit', message.exit)
if (message.pid != null && typeof proc.cb == 'function') proc.cb(ok = 'pid', message.pid, message.debug)
}
if (!ok) console.log('message:', message)
}
95 changes: 95 additions & 0 deletions lib/master/establish.js
@@ -0,0 +1,95 @@
// establish.js
// establish a nodegod parent process without any dependencies
// © Harald Rudell 2012

exports.establish = establish

// http://nodejs.org/api/path.html
var path = require('path')
// http://nodejs.org/api/fs.html
var fs = require('fs')

var pidFile
/*
Establish if this is the master process
marker: string: the id for pid file
cb(boolean): function:
- true: this is the master process
- false: the master process has been notified to restart the user interface
*/
function establish(marker, theSignal, cb) {
if (!pidFile) pidFile = getFileName(marker)
fs.readFile(pidFile, 'utf-8', pidFromFile)

function pidFromFile(err, data) {
var pid
if (!err && data) {
var num = parseInt(data)
if (num > 0) { // there was a pid
try { // signal to see if that process still exists
process.kill(num, theSignal)
pid = num
} catch (e) {
// if process is missing, it's ok
if (e.errno != 'ESRCH') throw e
}
}
}

if (!pid) { // we are taking over
fs.writeFile(pidFile, String(process.pid), writeResult)
} else cb(false) // we're not master exit
}

function writeResult(err) {
if (err) console.log('writing pid:', err.toString)
cb(true) // we're master exit
}
}

// helper functions

function getFileName(marker) {
var f = path.basename(__filename, path.extname(__filename))
return path.join(getTmpFolder(), f + '.pid')
}

function getTmpFolder() {
folder = path.join(getHomeFolder(), 'tmp')
if (getType(folder) !== 1) {
folder = process.env.TEMP
if (!folder || getType(folder) !== 1) {
folder = '/tmp'
if (getType(folder) !== 1) throw Error('no tmp folder found')
}
}

return folder
}

function getHomeFolder() {
return process.env[
process.platform == 'win32' ?
'USERPROFILE' :
'HOME']
}

function getType(path1) {
var result
var stats
try {
stats = fs.statSync(path1)
} catch (e) {
var bad = true
if (e instanceof Error && e.code == 'ENOENT') bad = false
if (bad) {
console.error('Exception for:', typeof path1, path1, path1 != null && path1.length)
throw e
}
}
if (stats) {
if (stats.isFile()) result = true
if (stats.isDirectory()) result = 1
}
return result
}
107 changes: 107 additions & 0 deletions lib/master/manager.js
@@ -0,0 +1,107 @@
// manager.js
// manage a child process for the ui
// © Harald Rudell 2012

exports.receiver = receiver
exports.setSend = setSend

var send
var marker = 'master'
setSend()

var childMap = {}

// http://nodejs.org/api/child_process.html
var child_process = require('child_process')

function launch(proc, start, debug) {
var args = Array.isArray(start) ? start : start.split(' ')
if (debug) args.unshift('--debug-brk')
proc.didKill = false
proc.child = child_process.spawn('node', args)
var thisPid = proc.child.pid
send({app: proc.app, pid: proc.child.pid, debug: debug})
proc.child.stdout.on('data', log)
proc.child.stderr.on('data', log)
proc.child.on('exit', function (code) {
var isCurrent = !proc.child || proc.child.pid == thisPid
if (isCurrent) {
proc.child = null
proc.pendingKill = false
// code is number
send({app: proc.app, exit: code, state: (proc.didKill ? 'stop' : 'crash')})
}
})
proc.child.stderr.setEncoding('utf-8')
proc.child.stdout.setEncoding('utf-8')
send({app: proc.app, state: (debug ? 'debug' : 'run')})

function log(string) {
console.log(proc.app, string.slice(0, -1))
}
}

function kill(proc) {
var t
if (proc.child && proc.child.pid && !proc.didKill) {
proc.didKill = true
proc.pendingKill = true
send({app: proc.app, state: 'stopping'})
t = Date.now()
setTimeout(sigResult, 3000)
// try the nice sigint first (ctrl-break)
process.kill(proc.child.pid, 'SIGINT')
}

function sigResult() {
proc.killTimer = null
if (proc.pendingKill) {
console.log(marker, proc.app, 'Force kill at: ' + (Date.now() - t) / 1e3)
// now the evil sigterm
process.kill(proc.child.pid)
}
}
}

function sendMap() {
var data = {}
for (var i in childMap) {
var pid
var child = childMap[i].child
if (child && child.pid) pid = child.pid
data[i] = pid
}
send({map: data})
}

function receiver(message) {
var ok
if (message.app) {
var proc = childMap[message.app]
if (!proc) {
childMap[message.app] = proc = {app: message.app}
}
if (message.launch) {
launch(proc, message.launch, message.debug)
ok = true
} else if (message.kill) {
kill(proc)
ok = true
} else if (message.getMap) {
sendMap()
ok = true
} else if (message.del) {
delete childMap[message.app]
}
}
if (!ok) console.log(marker, 'Bad message', message)
}

function setSend(f, marker0) {
send = typeof f == 'function' ? f : badSend
if (marker0) marker = marker0

function badSend() {
console.log(marker, 'No ipc available:', Array.prototype.slice.call(arguments))
}
}

0 comments on commit be9ec49

Please sign in to comment.