gardener
Version:
A node process manager for couchapps that depend on npm modules, or have npm modules bundled with them.
659 lines (565 loc) • 20.4 kB
JavaScript
exports.start = start;
exports.install = install;
exports.list = list;
exports.uninstall = uninstall;
var fs = require('fs'),
url = require('url'),
path = require('path'),
async = require('async'),
semver = require('semver'),
_ = require('underscore'),
forever = require('forever-monitor'),
request = require('request'),
safeUrl = require('safe-url'),
rimraf = require('rimraf'),
logger = require('./logger'),
npm_manager = require('./npm'),
process_manager = require('./processes'),
utils = require('./utils'),
options = require('./options'),
dashboard = require('./dashboard'),
working_dir = 'working_dir',
cache_name = '.details',
work_queue,
running_processes = {},
dashboard_db,
gardener_http,
couch_root_url;
/**
* @name terminate_children:
* Terminate all child processes in response to a signal.
*/
function terminate_children (_signal_name, _terminate_self) {
process.stderr.write(
"Caught " + (_terminate_self ? "fatal" : '') +
" signal " + _signal_name + "; terminating child processes...\n"
);
_.each(running_processes, function (_v, _k) {
var pid = _v.child.pid;
process.stderr.write("Terminating module `" + _k + "`\n");
try {
process.kill(pid, 'SIGTERM');
} catch (_e) {
/* Ignore ESRCH */
}
process.stderr.write("Sent SIGTERM to process ID " + pid + "\n");
});
if (_terminate_self) {
process.stderr.write("Gardener is terminating.\n");
process.kill(process.pid, 'SIGKILL');
}
};
function start(couch_root, dash_db, http, callback) {
couch_root_url = couch_root;
dashboard_db = dash_db;
gardener_http = http;
process.on('SIGHUP', function () {
terminate_children('SIGHUP');
});
process.on('SIGTERM', function () {
terminate_children('SIGTERM', true);
});
work_queue = async.queue(queue_processor, 1);
async.waterfall([
gather_modules,
start_forever
], function(err) {
callback(err);
});
}
function install(details) {
logger.info('adding install task to the queue. ' );
work_queue.push({
type: 'install',
details: details
});
}
function list(callback) {
var localnames = _.keys(running_processes);
async.map(localnames, load_cache_details, callback);
}
function uninstall(local_name, details) {
logger.info('adding uninstall task to the queue. ' );
work_queue.push({
type: 'uninstall',
local_name: local_name,
details: details
});
}
function queue_processor(task, callback) {
logger.info(
'pulling ' + task.type + ' task from the queue. ' +
work_queue.length() + ' remaining'
);
if (task.type === 'install') {
return queue_install(task.details, callback);
}
if (task.type === 'uninstall') {
return queue_uninstall(task.local_name, task.details, callback);
}
}
function queue_install(details, callback) {
var ddoc_url = details.ddoc_url,
pre_details = details;
_fill_missing_install_details(ddoc_url, pre_details, function(err, all_details) {
if (err) return callback(err);
is_installed(all_details, function(err, installed) {
if (!installed && (!all_details.dashboard_managed || pre_details.force_install)) {
_install(all_details, callback);
} else if (!is_running(all_details) && all_details.start_immediate) {
start_module(all_details.local_name, callback);
} else {
return callback(null);
}
});
});
}
function queue_uninstall(local_name, details, callback) {
if (!details) {
load_cache_details(local_name, function(err, dets) {
dets.local_name = local_name;
_uninstall(err, dets, callback);
});
} else {
_uninstall(null, details, callback);
}
}
function _fill_missing_install_details(ddoc_url, other, callback) {
var re = /-([0-9]+\.[0-9]+\.[0-9]+(-[0-9]+-?)?([a-zA-Z-+][a-zA-Z0-9-\.:]*)?)\.tgz$/;
var data = {};
module_name(ddoc_url, other, data, function(err, module_name){
/*
* Not much to do if we don't have module name.
*/
if (err) {
return callback(err);
}
data.module_name = module_name;
async.parallel([
function start_immediate(cb) {
data.start_immediate = other.start_immediate;
cb();
},
function is_remote_package(cb) {
data.is_remote_package = !utils.is_tgz(data.module_name);
cb();
},
function is_dashboard_managed(cb) {
dashboard.is_dashboard_managed(couch_root_url, dashboard_db, other.db_name, function(err, managed) {
data.dashboard_managed = managed;
cb(err);
});
},
function package_version(cb) {
var parts = data.module_name.match(re);
if (parts && parts.length > 0) {
data.version = data.module_name.match(re)[1];
return cb();
} else {
// this is just a npm name
var nv = data.module_name.split("@"),
name = nv[0],
version = nv[1];
if (version) {
data.version = version;
return cb();
}
// no version spcified, check npm
npm_manager.current_details(data.module_name, function(err, current_details){
data.version = current_details.version;
cb();
});
}
},
function local_name(cb) {
if (other.local_name) data.local_name = other.local_name;
else data.local_name = utils.local_name(ddoc_url, module_name);
cb();
},
function db_name(cb) {
if (other.db_name) {
data.db_name = other.db_name;
return cb();
}
utils.get_db_name(ddoc_url, function(err, db_name){
data.db_name = db_name;
cb();
});
},
function db_url(cb) {
if (other.db_url) {
data.db_url = other.db_url;
return cb();
}
utils.get_db_url(ddoc_url, function(err, db_url){
data.db_url = db_url;
cb();
});
}
], function(err) {
data.ddoc_url = ddoc_url;
callback(err, data);
});
});
}
function module_name(ddoc_url, other, data, cb) {
if (other.module_name) {
if (other.module_digest) {
data.module_digest = other.module_digest;
}
return cb(null, other.module_name);
}
return cb('No module details');
}
var truncate_message = function(str) {
var max_length = 41;
var joiner = '...';
var half_size = (max_length - joiner.length) / 2;
if (!str || str.length < max_length) {
return str;
}
return str.substr(0, half_size) + joiner + str.substr(str.length - half_size);
};
function progress(message, percent, details) {
logger.info(message);
if (!dashboard_db) return;
var dash_db = url.resolve(couch_root_url, dashboard_db);
var doc = {
type: 'gardener_progress',
time: new Date().getTime(),
path: url.parse(details.ddoc_url).path,
module: details.module_name,
percent: percent,
msg: truncate_message(message)
};
request({
url: dash_db,
method: 'POST',
json: doc
}, function(err){
//console.log(err);
});
}
function _install(details, callback) {
var safe = {
ddoc_url: safeUrl(details.ddoc_url),
db_url: safeUrl(details.db_url)
};
logger.info('installing ' + module_log_name(details), safe);
progress('Installing ' + module_log_name(details), 10, details);
async.auto({
stop_module: function(cb) {
logger.info('Checking if module '+ module_log_name(details) +' needs to be stopped');
if (is_running(details)) {
progress('Stopping module ' + module_log_name(details), 15, details);
stop(details.local_name, cb);
} else {
cb();
}
},
install_package: ['stop_module', function(cb) {
var package_url = url.resolve(details.ddoc_url + '/', details.module_name);
if (details.is_remote_package) {
package_url = details.module_name;
}
var working_dir = module_working_dir(details.local_name);
var indeterminant = 30;
var on_npm_update = function(msg) {
if (indeterminant < 80) indeterminant = indeterminant + 0.5;
progress(msg, indeterminant, details);
};
progress('Installing module ' + module_log_name(details), 30, details);
npm_manager.install(package_url, working_dir, on_npm_update, cb);
}],
cache_details: ['install_package', function(cb, data) {
details.install_package = data.install_package;
cache_details(details.local_name, details, cb);
}],
run: ['cache_details', function(cb, data){
if (!details.start_immediate ) return cb();
progress('Running module ' + module_log_name(details), 90, details);
if (is_running(details)) {
restart(details.local_name, cb);
} else if (details.start_immediate) {
start_module(details.local_name, cb);
}
}],
report: ['run', function(cb) {
if (!details.start_immediate ) return cb();
progress('Module is ready', 100, details);
cb();
}]
}, function(err, results) {
/* Try to clean up temporary directory:
Some versions of npm before v1.4.15 will use a temporary
directory inside of the current working directory; some
of those versions also fail to clean up after themselves.
Later versions use `/tmp` and clean things up just fine.
Try to remove the local `tmp`, and avoid exhausting disk
space in cases where we're using an older version of npm. */
rimraf('tmp', function () {
callback(err); /* Ignore errors */
});
});
}
function _uninstall(err, details, callback) {
if (err) {
return callback(err);
}
if (!is_running(details)) {
return callback(null);
}
logger.info('uninstalling ' + details.module_name);
progress('Uninstalling ' + details.module_name, 10, details);
async.auto({
stop: function(cb){
progress('Stopping module' + details.module_name, 15, details);
logger.info('Stopping module ' + module_log_name(details));
stop(details.local_name, cb);
},
deleting: ['stop', function(cb){
var working_dir = module_working_dir(details.local_name);
progress('Removing module' + details.module_name, 80, details);
logger.info('Removing module ' + module_log_name(details));
rimraf(working_dir, cb);
}],
clean: ['deleting', function(cb){
delete running_processes[details.local_name];
progress('Uninstalled' + details.module_name, 100, details);
logger.info('Uninstalled ' + module_log_name(details));
cb(null);
}]
}, callback);
}
function start_module(local_name, callback) {
async.auto({
cache: function(cb) { load_cache_details(local_name, cb); },
start_forever: ['cache', function(cb, data) {
start_forever([data.cache], callback);
}]
});
}
function restart(local_name, callback) {
var child = running_processes[local_name];
if (!child) {
return callback('No process found to restart');
}
child.restart();
callback(null);
}
function stop(local_name, callback) {
var child = running_processes[local_name];
if (!child) {
return callback('No process found to stop');
}
// possibly signal the child to shutdown.
logger.info('Preparing to stop module: [' + local_name +']');
var returned = false;
setTimeout(function(){
// safeguard to return
if (!returned) {
running_processes[local_name] = null;
returned = true;
callback();
}
}, 2000);
child.once('stop', function () {
if (!returned) {
running_processes[local_name] = null;
returned = true;
callback();
}
});
if (child.send) child.send({restart: true, time: 100});
setTimeout(function(){
child.stop();
}, 100);
}
function module_working_dir(local_name) {
return path.join(working_dir, local_name);
}
function is_running(details) {
var process = running_processes[details.local_name];
return process && process.data && process.data.running;
}
function is_installed(details, callback) {
load_cache_details(details.local_name, function(err, cache) {
if (err) {
// check that is just not found...
return callback(null, false);
}
if (cache.module_name !== details.module_name) {
return callback(null, false);
}
// lastly, if there is a module_digest, compare those for changes
if (cache.module_digest && details.module_digest) {
if (cache.module_digest !== details.module_digest) {
return callback(null, false);
}
}
return callback(null, true);
});
}
function cache_details(local_name, details, callback) {
init_working_dirs([details], function(err) {
if (err) return callback(err);
var folder = module_working_dir(local_name),
cache = path.join(folder, cache_name),
options = { mode: 0600, encoding: 'utf8' };
fs.writeFile(cache, JSON.stringify(details, 3), options, callback);
});
}
function load_cache_details(local_name, callback) {
var folder = module_working_dir(local_name),
cache = path.join(folder, cache_name);
fs.readFile(cache, function(err, data) {
if (err) {
return callback(err);
}
return callback(null, JSON.parse(data));
});
}
function gather_modules(callback) {
if (!fs.existsSync(working_dir)){
fs.mkdirSync(working_dir);
}
fs.readdir(working_dir, function(err, files) {
if (err && err.code !== 'ENOENT') {
return callback(err);
}
if (err) {
return callback(null, []);
}
var gathered_modules = [];
async.each(files, function(local_name, cb) {
load_cache_details(local_name, function(err, details){
if (err) {
logger.error('Cant read module cache: ' + local_name);
return cb(); // just ignore, no error
}
gathered_modules.push(details);
cb();
});
}, function(err){
callback(err, gathered_modules);
});
});
}
function init_working_dirs(module_localnames, callback) {
if (!fs.existsSync(working_dir)){
fs.mkdirSync(working_dir);
}
async.forEach(module_localnames, function(module, cb) {
var module_working_dir = get_working_dir(module);
if (!fs.existsSync(module_working_dir)){
return fs.mkdir(module_working_dir, cb);
}
return cb(null, module_working_dir, module);
}, function(err) {
callback(err, module_localnames);
});
}
function get_working_dir(module_detail) {
return path.join(working_dir, module_detail.local_name);
}
function get_package_dir(module_detail) {
return module_detail.install_package.install_dir;
}
function get_start_script(package_dir) {
var ss = path.join(package_dir, 'server.js');
if (fs.existsSync(ss)) return ss;
ss = path.join(package_dir, 'index.js');
if (fs.existsSync(ss)) return ss;
}
function start_forever(module_details, callback) {
async.forEach(module_details, function(module_detail, cb) {
if (is_running(module_detail)) {
logger.log('WARNING: module ', module_detail.local_name, ' is already running');
cb(null);
}
var working_dir = get_working_dir(module_detail),
package_dir = get_package_dir(module_detail),
start_script = path.resolve(get_start_script(package_dir)),
opts = getForeverOptions(working_dir, module_detail),
route = (url.parse(module_detail.ddoc_url).path),
logname = [module_detail.db_name, utils.get_ddoc_name(module_detail.ddoc_url), module_detail.install_package.package_name].join('_'),
process = forever_bind(logname, start_script, opts, route);
running_processes[module_detail.local_name] = process;
cb(null);
}, callback);
}
function forever_bind(logname, start_script, opts, route) {
logger.info('starting [' + logname + '] with script ' + start_script);
var child = new (forever.Monitor)(start_script, opts);
child.once('error', function(err) { process_error(logname, err); });
child.once('start', function(process, data) { process_start(logname, process, data, opts); });
child.on('stop', function(process) { process_stop(logname, process); });
child.on('restart', function() { process_restart(logname); });
child.once('exit', function() { process_exit(logname); });
child.on('stdout', function(data) { process_stdout(logname, data); });
child.on('stderr', function(data) { process_stderr(logname, data); });
child.on('message', function(data) {
if (gardener_http && data.port) {
logger.info('process [' + logname + '] has bound to port ' + data.port);
gardener_http.add_app(route, data.port);
}
});
child.start();
return child;
}
function process_error(package, err) {
logger.info('Error [' + package + ']', err);
}
function process_start(package, process, data, opts) {
logger.info('starting package [' + package + '] in ' + opts.cwd );
}
function process_stop(package, process) {
logger.info('stopping package [' + package + ']');
}
function process_restart(package) {
logger.info('restart package [' + package + ']');
}
function process_exit(package) {
logger.info('exit package [' + package + ']');
}
function process_stdout(package, data) {
logger.custom(package, 'info', data.toString());
}
function process_stderr(package, data) {
logger.custom(package, 'error', data.toString());
}
function getForeverOptions(working_dir, module_details) {
var couch_url = manage_auth_in_url(module_details.db_url);
var couch_server = manage_auth_in_url(couch_root_url);
return {
fork: true,
silent: true,
cwd: working_dir,
env: {
COUCH_URL: couch_url,
COUCH_DB: module_details.db_name,
COUCH_SERVER: couch_server
},
stdio: [ 'ipc', 'pipe', 'pipe' ],
killTree: true,
killTTL: 0,
minUptime: 2000,
spinSleepTime: 2000
};
}
function manage_auth_in_url(uri) {
var user = options.get_options_value('user'),
pass = options.get_options_value('pass');
if (user && pass) {
var temp = url.parse(uri);
temp.auth = user + ':' + pass;
return url.format(temp);
}
return uri;
}
function module_log_name(details) {
return [
details.db_name, '_design',
utils.get_ddoc_name(details.ddoc_url), details.module_name
].join('/');
}