iccicci/daemon-control

View on GitHub
index.js

Summary

Maintainability
C
7 hrs
Test Coverage
/*jslint evil: true */
"use strict";

var child_process = require("child_process");
var EventEmitter  = require("events");
var fs            = require("fs");
var path          = require("path");
var utils         = require("./utils");

function DaemonControl(daemon, filename, options) {
    if(! (this instanceof DaemonControl))
        return new DaemonControl(daemon, filename, options);

    EventEmitter.call(this);

    this.daemon   = daemon;
    this.filename = filename;
    this.options  = options;

    process.nextTick(this._main.bind(this));
}

DaemonControl.prototype = new EventEmitter();

module.exports = DaemonControl;

var commands = utils.commands;
var hooks    = utils.hooks;

DaemonControl.prototype._init         = utils._init;
DaemonControl.prototype._init_options = utils._init_options;
DaemonControl.prototype._init_spawn   = utils._init_spawn;
DaemonControl.prototype._write        = utils._write;

DaemonControl.prototype._cmdline = function(callback) {
    if(process.argv.length < 3)
        return callback();

    if(! (process.argv[2] in commands))
        return callback();

    if(process.argv[2] === "reload" && ! this.reload)
        return callback();

    if(this.hooks.argv)
        return this.hooks.argv(callback, process.argv[2], process.argv.slice(3));

    callback(process.argv[2], process.argv.slice(3));
};

DaemonControl.prototype._help = function() {
    var self = this;
    var done = function(verbose) {
        if(! verbose)
            return;

        self._write(self._syntax() + "\n");
        self._write("help      prints this screen\n");
        self._write("nodaemon  launches the application without detaching it from console\n");
        self._write("status    checks id daemon is running\n");
        self._write("start     launches the daemon\n");
        self._write("stop      stops the daemon\n");
        self._write("restart   stops than starts the daemon\n");
        if(self.reload)
        self._write("reload    makes daemon to reload configuration\n"); // eslint-disable-line indent
    };

    if(this.hooks.help)
        return this.hooks.help(done);

    done(true);
};

DaemonControl.prototype._kill = function(pid, callback) {
    var self = this;
    var done = function(verbose) {
        if(verbose)
            self._write("Sending SIGKILL to daemon.");

        process.kill(pid, "SIGKILL");
        self._wait(pid, callback, true);
    };

    if(self.hooks.kill)
        return self.hooks.kill(done, pid);

    done(true);
};

DaemonControl.prototype._main = function() {
    try {
        this._init();
    }
    catch(e) {
        return this.emit("error", e);
    }

    if(process.env.__daemon_control)
        return this.daemon(true);

    var self = this;

    this._cmdline(function(cmd, argv) {
        if(! cmd) {
            var done = function(verbose) {
                if(verbose)
                    self._write(self._syntax());
            };

            if(self.hooks.syntax)
                return self.hooks.syntax(done);

            return done(true);
        }

        self.argv = argv;
        eval("self._" + cmd + "();"); // eslint-disable-line no-eval
    });
};

DaemonControl.prototype._nodaemon = function() {
    var self = this;

    process.nextTick(function() {
        try {
            self._write("Starting in console.\n");
            self.daemon(false);
        }
        catch(e) {
            self.emit("error", e);
        }
    });
};

DaemonControl.prototype._reload = function() {
    var self = this;

    this._status(function(pid) {
        var done = function(verbose) {
            if(pid)
                process.kill(pid, "SIGHUP");

            if(! verbose)
                return;

            if(pid)
                return self._write("Sending SIGHUP to daemon.\n");

            self._write("Use start command\n");
        };

        if(self.hooks.reload)
            return self.hooks.reload(done, pid);

        done(true);
    });
};

DaemonControl.prototype._restart = function() {
    this._stop(this._start.bind(this, true));
};

DaemonControl.prototype._start = function(checked) {
    var self = this;
    var done = function(pid) {
        if(pid) {
            done = function(verbose) {
                if(verbose)
                    self._write("Stop the daemon before starting it, or use restart command\n");
            };

            if(self.hooks.running)
                return self.hooks.running(done, pid);

            done(true);
        }
        else {
            done = function(verbose, options) {
                var child;

                done = function(verbose) {
                    if(! verbose)
                        return;

                    if(child.pid)
                        self._write("Daemon started with pid: " + child.pid + "\n");
                    else
                        self._write("Daemon not started\n");
                };

                if(verbose)
                    self._write("Starting daemon...\n");

                options.env.__daemon_control = "true";
                self.argv.unshift(process.argv[1]);
                child = child_process.spawn(process.argv[0], self.argv, options);
                child.on("error", self.emit.bind(self, "error"));
                child.unref();

                fs.writeFile(self.filename, child.pid, function(err) {
                    if(err)
                        self.emit("error", err);

                    if(self.hooks.start)
                        return self.hooks.start(done, child);

                    done(true);
                });
            };

            if(self.hooks.starting)
                return self.hooks.starting(done, self.options);

            done(true, self.options);
        }
    };

    if(checked)
        return done(null);

    this._status(done);
};

DaemonControl.prototype._status = function(callback) {
    var self = this;
    var done = function(pid) {
        done = function() {
            done = function(verbose, pid) {
                if(verbose) {
                    if(! pid)
                        self._write("Daemon is not running\n");
                    else
                        self._write("Daemon is running with pid: " + pid + "\n");
                }

                if(callback)
                    return callback(pid);
            };

            if(self.hooks.status)
                return self.hooks.status(done, pid);

            done(true, pid);
        };

        if(pid)
            return done();

        fs.unlink(self.filename, done);
    };

    fs.readFile(this.filename, function(err, data) {
        if(err && err.code !== "ENOENT")
            return self.emit("error", err);

        if(err)
            return done(null);

        var pid = parseInt(data, 10);

        if(isNaN(pid))
            return done(null);

        try { process.kill(pid, 0); }
        catch(e) { return done(null); }

        done(pid);
    });
};

DaemonControl.prototype._stop = function(callback) {
    var self = this;

    this._status(function(pid) {
        if(! pid) {
            if(! callback)
                return;

            return callback();
        }

        var done = function(verbose) {
            if(verbose)
                self._write("Sending SIGTERM to daemon.");

            process.kill(pid, "SIGTERM");
            self._wait(pid, callback);
        };

        if(self.hooks.term)
            return self.hooks.term(done);

        done(true);
    });
};

DaemonControl.prototype._stopped = function(callback) {
    var self = this;

    var done = function(verbose) {
        if(verbose)
            self._write("\nDaemon stopped\n");

        if(callback)
            return callback();
    };

    if(self.hooks.stop)
        return self.hooks.stop(done);

    done(true);
};

DaemonControl.prototype._syntax = function() {
    return "Usage:\nnode " + path.basename(process.argv[1]) + " {start|stop|restart|status|nodaemon|help" + (this.reload ? "|reload" : "") + "} [...]\n";
};

DaemonControl.prototype._wait = function(pid, callback, killed, count) {
    var self = this;

    if(! count)
        count = 1;

    setTimeout(function() {
        var done = function(verbose) {
            if(verbose)
                self._write(".");

            try { process.kill(pid, 0); }
            catch(e) {
                return self._stopped(callback);
            }

            if(count === self.timeout && ! killed)
                return self._kill(pid, callback);

            self._wait(pid, callback, killed, count + 1);
        };

        if(self.hooks.wait)
            return self.hooks.wait(done);

        done(true);
    }, 1000);
};