the-shepherd
Version:
Control a herd of wild processes.
416 lines (370 loc) • 12.6 kB
JavaScript
// Generated by CoffeeScript 1.8.0
(function() {
var $, Child, Handlebars, Helpers, Http, Opts, Os, Process, Server, Shell, Worker, log, verbose,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
$ = require('bling');
Os = require("os");
Shell = require('shelljs');
Handlebars = require("handlebars");
Process = require('./process');
Helpers = require('./helpers');
Http = require('./http');
Opts = require('./opts');
log = $.logger("[child]");
verbose = function() {
var err, _ref;
try {
if (Opts.verbose) {
return log.apply(null, arguments);
}
} catch (_error) {
err = _error;
return log("verbose error:", (_ref = err.stack) != null ? _ref : err);
}
};
Child = (function() {
var toString;
function Child(opts, index) {
$.extend(this, {
opts: opts,
index: index,
process: null,
started: $.extend($.Promise(), {
attempts: 0,
timeout: null
})
});
this.log = $.logger(this.toString());
this.log.verbose = (function(_this) {
return function() {
var err, _ref;
try {
if (Opts.verbose) {
return _this.log.apply(null, arguments);
}
} catch (_error) {
err = _error;
return _this.log("verbose error:", (_ref = err.stack) != null ? _ref : err);
}
};
})(this);
}
Child.prototype.start = function() {
var cmd, fail, on_data;
try {
return this.started;
} finally {
fail = (function(_this) {
return function(msg) {
return _this.started.reject(msg);
};
})(this);
if (++this.started.attempts > this.opts.restart.maxAttempts) {
fail("too many attempts");
} else {
clearTimeout(this.started.timeout);
this.started.timeout = setTimeout(((function(_this) {
return function() {
return _this.started.attempts = 0;
};
})(this)), this.opts.restart.maxInterval);
log("shell >", cmd = "env " + (this.env()) + " bash -c 'cd " + this.opts.cd + " && " + this.opts.command + "'");
this.process = Shell.exec(cmd, {
silent: true,
async: true
}, $.identity);
this.process.on("exit", (function(_this) {
return function(err, signal) {
return _this.onExit(err, signal);
};
})(this));
on_data = (function(_this) {
return function(prefix) {
if (prefix == null) {
prefix = "";
}
return function(data) {
var line, _i, _len, _ref, _results;
_ref = String(data).split(/\n/);
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
line = _ref[_i];
if (line.length) {
_results.push(_this.log(prefix + line));
}
}
return _results;
};
};
})(this);
this.process.stdout.on("data", on_data(""));
this.process.stderr.on("data", on_data("(stderr) "));
if (!this.process.pid) {
fail("no pid");
}
}
}
};
Child.prototype.stop = function(signal) {
var err, p, _ref;
try {
return p = $.Promise();
} finally {
this.started.attempts = Infinity;
this.expectedExit = true;
if (this.process) {
try {
Process.killTree(this.process.pid, signal).then(p.resolve, p.reject);
} catch (_error) {
err = _error;
log("Error calling killTree:", (_ref = err.stack) != null ? _ref : err);
}
} else {
p.resolve();
}
}
};
Child.prototype.restart = function() {
var p, restart;
try {
return p = $.Promise();
} finally {
if (this.process == null) {
log("Starting fresh child (no existing process)");
this.start().then(p.resolve, p.reject);
} else {
restart = (function(_this) {
return function() {
var err, _ref;
try {
log("Restarting child...");
_this.process = null;
_this.started.reset();
_this.started.attempts = 0;
return _this.start().then(p.resolve, p.reject);
} catch (_error) {
err = _error;
log("restart error:", (_ref = err.stack) != null ? _ref : err);
return p.reject(err);
}
};
})(this);
log("Killing existing process", this.process.pid);
this.expectedExit = true;
Process.killTree(this.process.pid, "SIGTERM").wait(this.opts.restart.gracePeriod, function(err) {
var _ref;
try {
if (err === "timeout") {
log("Child failed to die within " + this.opts.restart.gracePeriod + "ms, using SIGKILL");
return Process.killTree(this.process.pid, "SIGKILL").then(restart, p.reject);
} else if (err) {
return p.reject(err);
} else {
return restart();
}
} catch (_error) {
err = _error;
log("restart error during kill tree:", (_ref = err.stack) != null ? _ref : err);
return p.reject(err);
}
});
}
}
};
Child.prototype.onExit = function(code, signal) {
var err, _ref;
try {
signal = $.is('number', code) ? code - 128 : Process.getSignalNumber(signal);
this.log("Child exited (signal=" + signal + ")", this.expectedExit ? "(expected)" : "");
if (!this.expectedExit) {
this.restart();
}
return this.expectedExit = false;
} catch (_error) {
err = _error;
return this.log("child.onExit error:", (_ref = err.stack) != null ? _ref : err);
}
};
Child.prototype.toString = toString = function() {
var err, _ref;
try {
return "" + this.constructor.name + "[" + this.index + "]";
} catch (_error) {
err = _error;
return log("toString error:", (_ref = err.stack) != null ? _ref : err);
}
};
Child.prototype.inspect = toString;
Child.prototype.env = function() {
var err, key, val, _ref;
try {
return ((function() {
var _ref, _results;
_ref = this.opts.env;
_results = [];
for (key in _ref) {
val = _ref[key];
if (val != null) {
_results.push("" + key + "=\"" + val + "\"");
}
}
return _results;
}).call(this)).join(" ");
} catch (_error) {
err = _error;
return log("env error:", (_ref = err.stack) != null ? _ref : err);
}
};
Child.defaults = function(opts) {
opts = $.extend(Object.create(null), {
cd: ".",
command: "node index.js",
count: -1,
env: {}
}, opts);
opts.count = parseInt(opts.count, 10);
while (opts.count < 0) {
opts.count += Os.cpus().length;
}
opts.count || (opts.count = 1);
opts.restart = $.extend(Object.create(null), {
maxAttempts: 5,
maxInterval: 10000,
gracePeriod: 3000,
timeout: 10000
}, opts.restart);
opts.git = $.extend(Object.create(null), {
enabled: false,
cd: ".",
remote: "origin",
branch: "master",
command: "git pull {{remote}} {{branch}} || git merge --abort"
}, opts.git);
opts.git.command = Handlebars.compile(opts.git.command);
opts.git.command.inspect = function(level) {
return '"' + opts.git.command({
remote: "{{remote}}",
branch: "{{branch}}"
}) + '"';
};
return opts;
};
return Child;
})();
Worker = (function(_super) {
var workers;
__extends(Worker, _super);
Http.get("/workers", function(req, res) {
var worker;
return res.pass("[" + (((function() {
var _i, _len, _ref, _ref1, _results;
_results = [];
for (_i = 0, _len = workers.length; _i < _len; _i++) {
worker = workers[_i];
_results.push("[" + ((_ref = (_ref1 = worker.process) != null ? _ref1.pid : void 0) != null ? _ref : "DEAD") + ", " + worker.port + "]");
}
return _results;
})()).join(",\n")) + "]");
});
Http.get("/workers/restart", function(req, res) {
var worker, _i, _len;
for (_i = 0, _len = workers.length; _i < _len; _i++) {
worker = workers[_i];
worker.restart();
}
return res.redirect(302, "/workers?restarting");
});
workers = [];
function Worker(opts, index) {
Child.apply(this, [opts = Worker.defaults(opts), index]);
workers.push(this);
this.log = $.logger(this.toString());
}
Worker.prototype.start = function() {
Worker.__super__.start.call(this);
return this.started.resolve();
};
Worker.defaults = Child.defaults;
return Worker;
})(Child);
Server = (function(_super) {
var servers;
__extends(Server, _super);
Http.get("/servers", function(req, res) {
var port, ret, s, v;
ret = "[";
for (port in servers) {
v = servers[port];
ret += ((function() {
var _i, _len, _ref, _ref1, _results;
_results = [];
for (_i = 0, _len = v.length; _i < _len; _i++) {
s = v[_i];
_results.push("[" + ((_ref = (_ref1 = s.process) != null ? _ref1.pid : void 0) != null ? _ref : "DEAD") + ", " + s.port + "]");
}
return _results;
})()).join(",\n");
}
return res.pass(ret + "]");
});
Http.get("/servers/restart", function(req, res) {
$.valuesOf(servers).flatten().select('restart').call();
return res.redirect(302, "/servers?restarting");
});
servers = {};
function Server(opts, index) {
var _name;
Child.apply(this, [opts = Server.defaults(opts), index]);
this.port = opts.port + index;
this.log = $.logger("(" + this.opts.cd + "):" + this.port);
(servers[_name = opts.port] != null ? servers[_name] : servers[_name] = []).push(this);
}
Server.prototype.start = function() {
try {
return this.started;
} finally {
Process.clearCache().findOne({
ports: this.port
}).then((function(_this) {
return function(owner) {
if (owner != null) {
_this.log("Killing previous owner of", _this.port, "PID:", owner.pid);
return Process.killTree(owner, "SIGKILL").then(function() {
return _this.start();
});
} else {
Server.__super__.start.call(_this);
if (!_this.process) {
return _this.started.reject("no process");
} else {
verbose("Waiting for port", _this.port, "to be owned by", _this.process.pid);
return Helpers.portIsOwned(_this.process.pid, _this.port, _this.opts.restart.timeout).then((function() {
verbose("Port " + _this.port + " is successfully owned.");
return _this.started.resolve();
}), _this.started.reject);
}
}
};
})(this));
}
};
Server.prototype.env = function() {
return Server.__super__.env.call(this) + ("" + this.opts.portVariable + "=\"" + this.port + "\"");
};
Server.defaults = function(opts) {
opts = $.extend({
port: 8001,
portVariable: "PORT",
poolName: "shepherd_pool"
}, Child.defaults(opts));
opts.port = parseInt(opts.port, 10);
return opts;
};
return Server;
})(Child);
$.extend(module.exports, {
Server: Server,
Worker: Worker
});
}).call(this);