mushroom
Version:
Mushroom sprouts a never-ending set of child processes which can report statistics back to the host. Like multi-process forever.
274 lines (235 loc) • 6.15 kB
JavaScript
module.exports = (function()
{
var childProcess = require("child_process");
var http = require("http");
var url = require("url");
var EventEmitter = require("events").EventEmitter;
//a more powerful _.extend
function extend(obj)
{
var recurse = arguments.callee;
Array.prototype.slice.call(arguments, 1).forEach(function(source)
{
for(var prop in source)
{
if(source[prop] instanceof Array)
obj[prop] = ((obj[prop] instanceof Array)? obj[prop] : []).concat(source[prop]);
else if((typeof(obj[prop]) == "object") && (typeof(source[prop]) == "object"))
recurse(obj[prop], source[prop]);
else
obj[prop] = source[prop];
}
});
return(obj);
};
var sporeSingleton;
Spore.prototype = extend(new EventEmitter(),
{
port: null,
__tally: null,
__heartbeat: null,
tally: function(channel, count, metadata)
{
channel = channel || "general";
var t = (this.__tally[channel] || (this.__tally[channel] = {count: 0, meta: []}));
t.count += count;
if(metadata) t.meta.push({time: Date.now(), meta: metadata});
}
});
function Spore()
{
var ths = (this instanceof arguments.callee)? this : new arguments.callee(); //force ctor call
ths.port = process.env["MUSHROOM_PORT"];
ths.serverPort = process.env["MUSHROOM_SERVER_PORT"];
ths.__tally = {};
ths.__heartbeat = setInterval(function()
{
http.request(
{
method: "PUT",
host: "localhost",
port: ths.serverPort,
path: "/checkin/" + ths.port
}, function(response)
{
//if(response.statusCode != 200)
//console.log("Unexpect server status: ", response.statusCode);
var json = "";
response.setEncoding("utf8");
response.on("data", function(d)
{
json += d;
}).on("end", function()
{
try
{
json = JSON.parse(json);
if(json.cmd)
console.log(json);
}
catch(e)
{
//console.log("Malformed JSON: >>" + json + "<<");
}
});
}).on("error", function(err)
{
//console.log("Heartbeat failed: ", err);
}).end(JSON.stringify(
{
//send these things to the master
time: Date.now(),
tally: ths.__tally,
mem: process.memoryUsage()
}));
ths.__tally = {};
}, 3000);
return(ths);
}
SporeHandle.prototype =
{
__restartWhenDied: null,
__restartDelay: null,
__spore: null,
stderr: null,
restart: function()
{
if(this.__spore)
{
this.__spore.kill();
this.__spore = undefined;
}
},
stop: function()
{
if(this.__spore)
{
this.__restartWhenDied = false;
this.__spore.kill();
this.__spore = undefined;
}
}
};
function SporeHandle(spore)
{
this.stderr = "";
this.__spore = spore;
this.__restartWhenDied = true;
this.__restartDelay = 1000;
}
var globalSporeHandles = [];
var globalExceptionListener = false;
Shroom.prototype = extend(new EventEmitter(),
{
__serverPort: null,
__options: null,
__spres: null,
spawn: function(processArgs, port)
{
this.__ensureServer();
var ths = this;
var spore = childProcess.spawn(process.execPath, processArgs,
{
env:
{
"MUSHROOM_SERVER_PORT": this.__serverPort,
"MUSHROOM_PORT": port
}
});
var sporeHandle = this.__spores[port] || (this.__spores[port] = new SporeHandle(spore));
spore.stdout.setEncoding("utf8");
spore.stderr.setEncoding("utf8");
spore.stdout.on("data", function(d)
{
//console.log("stdout: ", d);
ths.emit("stdout", {source: sporeHandle, port: port, data: d});
});
spore.stderr.on("data", function(d)
{
//console.log("stderr: ", d);
sporeHandle.stderr = (sporeHandle.stderr + d).substr(-((this.__options && this.__options.scrollbackLength) || 4000));
ths.emit("stderr", {source: sporeHandle, port: port, data: d});
});
spore.on("exit", function(code)
{
//console.log("Spore " + port + " exited with: ", code);
ths.emit("exit", {source: sporeHandle, port: port, code: code});
if(sporeHandle.__restartWhenDied)
{
console.log("Respawning spore " + port + " in a moment...");
setTimeout(function(){ths.spawn(processArgs, port)}, sporeHandle.__restartDelay); //@@respawn timeout
}
});
globalSporeHandles.push(sporeHandle);
return(sporeHandle);
},
__ensureServer: function()
{
var ths = this;
if(!this.__server)
{
this.__server = http.createServer(function(request, response)
{
var args = url.parse(request.url, true);
request.method = request.method.toLowerCase();
request.args = {query: args.query};
var urlParts = args.pathname.split("/").filter(function(p){return(p != "");});
var finish = function(result, httpCode, headers)
{
response.writeHead(httpCode || 200);
response.end(JSON.stringify(result));
};
switch(urlParts[0])
{
case "checkin":
ths.sporeCheckin(request, response, {url: {sporeID: urlParts[1]}}, finish);
break;
default:
finish({}, 404);
break;
}
});
this.__server.listen(this.__serverPort);
}
},
sporeCheckin: function(request, response, args, callback)
{
var ths = this;
var data = "";
request.setEncoding("utf8");
request.on("data", function(d)
{
data += d;
}).on("end", function()
{
//console.log("RX from " + args.url.sporeID + ": ", data);
callback({});
var message;
try{message = JSON.parse(json);} catch(e){}
var source = ths.__spores[args.url.sporeID];
ths.emit("data", {source: source, port: args.url.sporeID, message: message});
});
}
});
function Shroom(options)
{
this.__options = options;
this.__serverPort = (this.__options && this.__options.port) || 1985;
this.__spores = {};
if(!globalExceptionListener)
{
globalExceptionListener = true;
process.on("uncaughtException", function(e)
{
for(var i = 0; i < globalSporeHandles.length; i++)
globalSporeHandles[i].stop();
throw e;
});
}
}
return(
{
spore: function(){return(sporeSingleton || (sporeSingleton = new Spore()));},
shroom: Shroom
});
})();