From abfcaea92380adefd5e2e5a51881f1cedb2258c5 Mon Sep 17 00:00:00 2001 From: Corey Butler Date: Sat, 25 May 2013 20:32:33 -0500 Subject: [PATCH] Initial code. --- .npmignore | 6 + LICENSE | 22 ++ README.md | 5 +- example/helloworld.js | 15 ++ example/install.js | 35 +++ example/uninstall.js | 16 ++ lib/daemon.js | 486 ++++++++++++++++++++++++++++++++++++++++++ lib/init.js | 142 ++++++++++++ lib/node-linux.js | 23 ++ lib/templates/debian | 262 +++++++++++++++++++++++ lib/templates/redhat | 65 ++++++ lib/wrapper.js | 174 +++++++++++++++ package.json | 33 +++ 13 files changed, 1282 insertions(+), 2 deletions(-) create mode 100644 .npmignore create mode 100644 LICENSE create mode 100644 example/helloworld.js create mode 100644 example/install.js create mode 100644 example/uninstall.js create mode 100644 lib/daemon.js create mode 100644 lib/init.js create mode 100644 lib/node-linux.js create mode 100644 lib/templates/debian create mode 100644 lib/templates/redhat create mode 100644 lib/wrapper.js create mode 100644 package.json diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..d19ead6 --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +support +test +example +docs +Gruntfile.js +*.sock diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9475a60 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2013 Corey Butler + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 1c4d7ae..196bcd3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -node-linux -========== +# node-linux Create native background daemons on Linux systems. + +Currently being built. \ No newline at end of file diff --git a/example/helloworld.js b/example/helloworld.js new file mode 100644 index 0000000..5bf4522 --- /dev/null +++ b/example/helloworld.js @@ -0,0 +1,15 @@ +var http = require('http'); +var server = http.createServer(function (req, res) { + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.end(JSON.stringify(process.env)); + //res.end('Hello World\n'); +}); + +server.listen(3000); +console.log('Server running at http://127.0.0.1:3000/'); + +// Force the process to close after 15 seconds +setTimeout(function(){ + throw 'A test Error' + //process.exit(); +},25000); diff --git a/example/install.js b/example/install.js new file mode 100644 index 0000000..50d7031 --- /dev/null +++ b/example/install.js @@ -0,0 +1,35 @@ +var Service = require('../').Service; + +// Create a new service object +var svc = new Service({ + name:'Hello World', + description: 'The nodejs.org example web server.', + script: require('path').join(__dirname,'helloworld.js'), + env:{ + name: "NODE_ENV", + value: "production" + } +}); + +// Listen for the "install" event, which indicates the +// process is available as a service. +svc.on('install',function(){ + console.log('\nInstallation Complete\n---------------------'); + svc.start(); +}); + +// Just in case this file is run twice. +svc.on('alreadyinstalled',function(){ + console.log('This service is already installed.'); + console.log('Attempting to start it.'); + svc.start(); +}); + +// Listen for the "start" event and let us know when the +// process has actually started working. +svc.on('start',function(){ + console.log(svc.name+' started!\nVisit http://127.0.0.1:3000 to see it in action.\n'); +}); + +// Install the script as a service. +svc.install(); \ No newline at end of file diff --git a/example/uninstall.js b/example/uninstall.js new file mode 100644 index 0000000..2754193 --- /dev/null +++ b/example/uninstall.js @@ -0,0 +1,16 @@ +var Service = require('../').Service; + +// Create a new service object +var svc = new Service({ + name:'Hello World', + script: require('path').join(__dirname,'helloworld.js') +}); + +// Listen for the "uninstall" event so we know when it's done. +svc.on('uninstall',function(){ + console.log('Uninstall complete.'); + console.log('The service exists: ',svc.exists); +}); + +// Uninstall the service. +svc.uninstall(); \ No newline at end of file diff --git a/lib/daemon.js b/lib/daemon.js new file mode 100644 index 0000000..9e083dc --- /dev/null +++ b/lib/daemon.js @@ -0,0 +1,486 @@ +/** + * @class nodelinux.Daemon + * Manage node.js scripts as native Linux daemons. + * var Service = require('node-mac').Service; + * + * // Create a new service object + * var svc = new Service({ + * name:'Hello World', + * description: 'The nodejs.org example web server.', + * script: '/path/to/helloworld.js') + * }); + * + * // Listen for the "install" event, which indicates the + * // process is available as a service. + * svc.on('install',function(){ + * svc.start(); + * }); + * + * svc.install(); + * @author Corey Butler + */ +var plist = require('plist'), + fs = require('fs'), + p = require('path'), + exec = require('child_process').exec, + wrapper = p.resolve(p.join(__dirname,'./wrapper.js')); + +var daemon = function(config) { + + Object.defineProperties(this,{ + + /** + * @cfg {String} name + * The descriptive name of the process, i.e. `My Process`. + */ + _name: { + enumerable: false, + writable: true, + configurable: false, + value: config.name || null + }, + + /** + * @property {String} name + * The name of the process. + */ + name: { + enumerable: true, + get: function(){return this._name;}, + set: function(value){this._name = value;} + }, + + label: { + enumerable: false, + get: function(){ + return this.name.replace(/[^a-zA-Z]+/gi,'').toLowerCase() + } + }, + + plist: { + enumerable: false, + get: function(){ + return p.join(this.root,this.label+'.plist'); + } + }, + + outlog: { + enumerable: false, + get: function(){ + return p.join(this.logpath,this.label+'.log'); + } + }, + + errlog: { + enumerable: false, + get: function(){ + return p.join(this.logpath,this.label+'_error.log'); + } + }, + + /** + * @property {Boolean} exists + * Indicates that the service exists. + * @readonly + */ + exists: { + enumerable: true, + get: function(){ + return fs.existsSync(this.plist); + } + }, + + /** + * @property {String} id + * The ID for the process. + * @readonly + */ + id: { + enumerable: true, + get: function(){ + return this.name.replace(/[^\w]/gi,'').toLowerCase(); + } + }, + + /** + * @cfg {String} [description=''] + * Description of the service. + */ + description: { + enumerable: true, + writable: false, + configurable: false, + value: config.description || '' + }, + + /** + * @cfg {String} [cwd] + * The absolute path of the current working directory. Defaults to the base directory of #script. + */ + cwd: { + enumerable: false, + writable: true, + configurable: false, + value: config.cwd || p.dirname(this.script) + }, + + /** + * @cfg {Array|Object} [env] + * An optional array or object used to pass environment variables to the node.js script. + * You can do this by setting environment variables in the service config, as shown below: + * + * var svc = new Service({ + * name:'Hello World', + * description: 'The nodejs.org example web server.', + * script: '/path/to/helloworld.js', + * env: { + * name: "NODE_ENV", + * value: "production" + * } + * }); + * + * You can also supply an array to set multiple environment variables: + * + * var svc = new Service({ + * name:'Hello World', + * description: 'The nodejs.org example web server.', + * script: '/path/to/helloworld.js', + * env: [{ + * name: "HOME", + * value: process.env["USERPROFILE"] // Access the user home directory + * },{ + * name: "NODE_ENV", + * value: "production" + * }] + * }); + */ + _ev: { + enumerable: false, + writable: true, + configurable: false, + value: config.env || [] + }, + + EnvironmentVariables: { + enumerable: false, + get: function(){ + var ev = [], tmp = {}; + if (Object.prototype.toString.call(this._ev) === '[object Array]'){ + this._ev.forEach(function(item){ + tmp = {}; + tmp[this._ev[i].name] = this._ev[i].value; + ev.push(tmp); + }); + } else { + tmp[this._ev.name] = this._ev.value; + ev.push(tmp); + } + return ev; + } + }, + + /** + * @cfg {String} script + * The absolute path of the script to launch as a service. + * @required + */ + script: { + enumerable: true, + writable: true, + configurable: false, + value: config.script !== undefined ? require('path').resolve(config.script) : null + }, + + root: { + enumerable: false, + writable: true, + configurable: false, + value: '/Library/LaunchDaemons' + }, + + /** + * @cfg {String} [logpath=/Library/Logs/node-scripts] + * The root directory where the log will be stored. + */ + logpath: { + enumerable: true, + writable: true, + configurable: false, + value: config.logpath || '/Library/Logs/'+(this.name || config.name || 'node-scripts') + }, + + /** + * @cfg {Number} [maxRetries=null] + * The maximum number of restart attempts to make before the service is considered non-responsive/faulty. + * Ignored by default. + */ + maxRetries: { + enumerable: true, + writable: false, + configurable: false, + value: config.maxRetries || null + }, + + /** + * @cfg {Number} [maxRestarts=3] + * The maximum number of restarts within a 60 second period before haulting the process. + * This cannot be _disabled_, but it can be rendered ineffective by setting a value of `0`. + */ + maxRestarts: { + enumerable: true, + writable: false, + configurable: false, + value: config.maxRestarts || 3 + }, + + /** + * @cfg {Boolean} [abortOnError=false] + * Setting this to `true` will force the process to exit if it encounters an error that stops the node.js script from running. + * This does not mean the process will stop if the script throws an error. It will only abort if the + * script throws an error causing the process to exit (i.e. `process.exit(1)`). + */ + abortOnError: { + enumerable: true, + writable: false, + configurable: false, + value: config.abortOnError instanceof Boolean ? config.abortOnError : false + }, + + /** + * @cfg {Number} [wait=1] + * The initial number of seconds to wait before attempting a restart (after the script stops). + */ + wait: { + enumerable: true, + writable: false, + configurable: false, + value: config.wait || 1 + }, + + /** + * @cfg {Number} [grow=.25] + * A number between 0-1 representing the percentage growth rate for the #wait interval. + * Setting this to anything other than `0` allows the process to increase it's wait period + * on every restart attempt. If a process dies fatally, this will prevent the server from + * restarting the process too rapidly (and too strenuously). + */ + grow: { + enumerable: true, + writable: false, + configurable: false, + value: config.grow || .25 + }, + + /** + * @method install + * Install the script as a background process/daemon. + * @param {Function} [callback] + */ + install: { + enumerable: true, + writable: true, + configurable: false, + value: function(callback){ + + var me = this; + + if (!fs.existsSync(this.logpath)){ + fs.mkdirSync(this.logpath); + } + + // Create the log file if it doesn't exist. + fs.exists(this.outlog,function(exists){ + if (!exists){ + fs.appendFileSync(me.outlog,'# '+me.name); + } + }); + + // Create the error file if it doesn't exist. + fs.exists(this.errlog,function(exists){ + if (!exists){ + fs.appendFileSync(me.errlog,'# '+me.name+' Errors'); + } + }); + + // Create the plist file if it doesn't exist. + fs.exists(this.plist,function(exists){ + if (!exists){ + + // Make sure a node.js file is specified. + if (!me.script){ + console.log(me.script); + throw new Error('No file specified. Cannot start.'); + } + // Build the plist file + var args = [ + process.execPath,'--harmony',wrapper, + '-f',me.script, + '-l',me.outlog, + '-e',me.errlog, + '-t',me.name, + '-g',me.grow.toString(), + '-w',me.wait.toString(), + '-r',me.maxRestarts.toString(), + '-a',(me.abortOnError==true?'y':'n') + ]; + if (me.maxRetries!==null){ + args.push('-m'); + args.push(me.maxRetries.toString()); + } + // Add environment variables + for (var i=0;i= 0; + })[0]; + + switch(_os){ + // Use RedHat for CentOS + case 'centos': + case 'redhat': + _os = 'redhat'; + break; + + // Use debian for Ubuntu & default + case 'ubuntu': + case 'debian': + default: + _os = 'debian'; + break; + } + + /** + * @cfg {String} name required + * The title of the process + */ + /** + * @cfg {String} description + * A description of what the service does. + */ + /** + * @cfg {String} [author='Unknown'] + * The author of the process. + */ + /** + * @cfg {String} [user='root'] + * The user account under which the process should run. + */ + /** + * @cfg {String} [group='root'] + * The user group under which the process should run. + */ + /** + * @cfg {String} [pidroot='/var/run'] + * The root directory where the PID file will be created (if applicable to the OS environment). + */ + /** + * @cfg {String} [logroot='/var/log'] + * The root directory where the log file will be created. + */ + /** + * @cfg {Object} [env] + * A key/value object containing environment variables that should be passed to the process. + */ + + opt = { + label: config.name.replace(/[^a-zA-Z0-9]/,'').toLowerCase(), + servicesummary: config.name, + servicedescription: config.description || config.name, + author: config.author || 'Unknown', + script: p.join(__dirname,'..','example','helloworld.js'), + description: config.description, + user: config.user || 'root', + group: config.group || 'root', + pidroot: config.pidroot || '/var/run', + logroot: config.logroot || '/var/log', + wrappercode: '-w 3', + env: '', + created: new Date(), + execpath: process.execPath, + }; + mu.compile(p.join(me.templateRoot,_os),function(err,tpl){ + var stream = mu.render(tpl,opt), + chunk = ""; + stream.on('data',function(data){ + chunk += data; + }); + stream.on('end',function(){ + callback(chunk); + }); + }); + }); + } + } + + }); + +}; +var x = new init({ + name: 'Hello World', + description: 'Node.JS: Say hello to the world', + author: 'Corey Butler - corey@coreybutler.com', + pidroot: '/var/run' +}); +x.generate(function(script){ + fs.writeFile('myapp',script); +}); +module.exports = init; \ No newline at end of file diff --git a/lib/node-linux.js b/lib/node-linux.js new file mode 100644 index 0000000..19add0e --- /dev/null +++ b/lib/node-linux.js @@ -0,0 +1,23 @@ +/** + * @class nodelinux + * This is a standalone module, originally designed for internal use in [NGN](http://github.com/thinkfirst/NGN). + * However; it is capable of providing the same features for Node.JS scripts + * independently of NGN. + * + * ### Getting node-linux + * + * `npm install node-linux` + * + * ### Using node-linux + * + * `var nm = require('node-linux');` + * + * @singleton + * @author Corey Butler + */ +if (require('os').platform().indexOf('linux') < 0){ + throw 'node-linux is only supported on Linux.'; +} + +// Add daemon management capabilities +module.exports.Service = require('./daemon'); \ No newline at end of file diff --git a/lib/templates/debian b/lib/templates/debian new file mode 100644 index 0000000..86e4d47 --- /dev/null +++ b/lib/templates/debian @@ -0,0 +1,262 @@ +#!/bin/bash + +# Adapted from https://gist.github.com/peterhost/715255 +# INIT SCRIPT http://www.debian.org/doc/debian-policy/ch-opersys.html#s-sysvinit +# INIT-INFO RULES http://wiki.debian.org/LSBInitScripts +# INSTALL/REMOVE http://www.debian-administration.org/articles/28 +# ------------------------------------------------------------------------------ +# 3) copy the renamed/modified script(s) to /etc/init.d +# chmod 755, +# +# 4) if you wish the Daemon to be lauched at boot / stopped at shutdown : +# INSTALL : update-rc.d scriptname defaults +# (UNINSTALL : update-rc.d -f scriptname remove) +# +# Provides: {{label}} +# Required-Start: $remote_fs $named $syslog +# Required-Stop: $remote_fs $named $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: {{servicesummary}} +# Description: {{servicedescription}} +# Author: {{author}} +# Created: {{created}} + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin # modify if needed + +DAEMON_ARGS="{{script}}" # path to your node.js server/app + # NB: don't use ~/ in path + +DESC="{{description}}" # whatever fancy description you like + +NODEUSER={{user}}:{{group}} # USER who OWNS the daemon process (no matter whoever runs the init script) + # user:group (if no group is specified, the primary GID for that user is used) + +LOCAL_VAR_RUN=/usr/local/var/run # In case the init script is run by non-root user, you need to + # indicate a directory writeable by $NODEUSER to store the PID file + +DAEMON={{execpath}} # this SHOULD POINT TO where your node executable is + +LABEL={{label}} +LOGFILE={{logroot}}/$LABEL.log # Logfile path +ERRFILE={{logroot}}/$LABEL-error.log # Error file path + +# ______________________________________________________________________________ + +# Do NOT "set -e" + +# Create the log file if it does not exist already +if [ ! -a "{{logroot}}" ];then + mkdir -p {{logroot}}; +fi + +if [ ! -a $LOGFILE ];then + touch $LOGFILE; +fi + +# Create the error log file if it does not exist already +if [ ! -a $ERRFILE ];then + touch $ERRFILE; +fi + +[ $UID -eq "0" ] && LOCAL_VAR_RUN=/var/run # in case this script is run by root, override user setting +THIS_ARG=$0 +INIT_SCRIPT_NAME=`basename $THIS_ARG` +[ -h $THIS_ARG ] && INIT_SCRIPT_NAME=`basename $(readlink $THIS_ARG)` # in case of symlink +INIT_SCRIPT_NAME_NOEXT=${INIT_SCRIPT_NAME%.*) +#PIDFILE="$LOCAL_VAR_RUN/$INIT_SCRIPT_NAME_NOEXT.pid" +SCRIPTNAME=/etc/init.d/$INIT_SCRIPT_NAME + +# Exit if the package is not installed +[ -x "$DAEMON" ] || { echo "can't find Node.js ($DAEMON)" >&2; exit 0; } + +# Exit if the 'run' folder is not present +[ -d "$LOCAL_VAR_RUN" ] || { echo "Directory $LOCAL_VAR_RUN does not exist. Modify the '$INIT_SCRIPT_NAME_NOEXT' init.d script ($THIS_ARG) accordingly" >&2; exit 0; } + +# Read configuration variable file if it is present +[ -r /etc/default/$INIT_SCRIPT_NAME ] && . /etc/default/$INIT_SCRIPT_NAME + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. +. /lib/lsb/init-functions + +# uncomment to override system setting +VERBOSE=yes + +# Constructed Variables +USER=`id -n -u` +PIDFILE={{pidroot}}/$INIT_SCRIPT_NAME_NOEXT.pid + +# +# Function that starts the daemon/service +# +do_start() +{ + # Return 1 if daemon was already running + start-stop-daemon --start --quiet --pidfile $PIDFILE --chuid $NODEUSER --background --exec $DAEMON --name $LABEL --test > /dev/null \ + || { [ "$VERBOSE" != no ] && log_daemon_msg " ---> Daemon already running $DESC" "$INIT_SCRIPT_NAME_NOEXT"; return 1; } + + # Return 2 if daemon could not be started + { start-stop-daemon --start --quiet --chuid $NODEUSER --make-pidfile --pidfile $PIDFILE --background --name $LABEL --exec $DAEMON -- \ + $DAEMON_ARGS {{wrappercode}} >> "$LOGFILE" 2>&1 could not be start $DESC" "$INIT_SCRIPT_NAME_NOEXT"; return 2; } + + # Add code here, if necessary, that waits for the process to be ready + # to handle requests from services started subsequently which depend + # on this one. As a last resort, sleep for some time. + + #[ "$VERBOSE" != no ] && log_daemon_msg " ---> started $INIT_SCRIPT_NAME_NOEXT" + # Return 0 when daemon has been started + return 0; +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + + start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --chuid $NODEUSER --name $DAEMON + RETVAL="$?" + + #[ "$VERBOSE" != no ] && [ "$RETVAL" = 1 ] && log_daemon_msg " ---> SIGKILL failed => hardkill $DESC" "$INIT_SCRIPT_NAME_NOEXT" + [ "$RETVAL" = 2 ] && return 2 + + # Wait for children to finish too if this is a daemon that forks + # and if the daemon is only ever run from this initscript. + # If the above conditions are not satisfied then add some other code + # that waits for the process to drop all resources that could be + # needed by services started subsequently. A last resort is to + # sleep for some time. + start-stop-daemon --stop --quiet --oknodo --retry=0/3/KILL/5 --pidfile $PIDFILE --chuid $NODEUSER --exec $DAEMON -- $DAEMON_ARGS {{wrappercode}} + [ "$?" = 2 ] && return 2 + + # Many daemons don't delete their pidfiles when they exit. + rm -f $PIDFILE + + #[ "$VERBOSE" != no ] && [ "$RETVAL" = 1 ] && log_daemon_msg " ---> $DESC not running" "$INIT_SCRIPT_NAME_NOEXT" + #[ "$VERBOSE" != no -a "$RETVAL" = 0 ] && log_daemon_msg " ---> $DESC stopped" "$INIT_SCRIPT_NAME_NOEXT" + return "$RETVAL" +} + +# +# Function that sends a SIGHUP to the daemon/service +# +do_reload() { + # + # If the daemon can reload its configuration without + # restarting (for example, when it is sent a SIGHUP), + # then implement that here. + # + start-stop-daemon --stop --quiet --signal 1 --pidfile $PIDFILE --chuid $NODEUSER --name $LABEL --exec node + return 0 +} + +# +# Function that returns the daemon +# +do_status() { + # + # http://refspecs.freestandards.org/LSB_3.1.1/LSB-Core-generic/LSB-Core-generic/iniscrptact.html + # 0 program is running or service is OK + # 1 program is dead and /var/run pid file exists + # (2 program is dead and /var/lock lock file exists) (not used here) + # 3 program is not running + # 4 program or service status is unknown + RUNNING=$(running) + + ispidactive=$(pidof $NAME | grep `cat $PIDFILE 2>&1` >/dev/null 2>&1) + ISPIDACTIVE=$? + + if [ -n "$RUNNING" ]; then + if [ $ISPIDACTIVE ]; then + log_success_msg "$INIT_SCRIPT_NAME_NOEXT running (launched by $USER, --chuid $NODEUSER)." + exit 0 + fi + else + if [ -f $PIDFILE ]; then + rm -f $PIDFILE + log_success_msg "$INIT_SCRIPT_NAME_NOEXT is not running. Phantom pidfile, $PIDFILE, removed." + exit 1 + else + log_success_msg "$INIT_SCRIPT_NAME_NOEXT is not running." + exit 3 + fi + fi +} + +running() { + RUNSTAT=$(start-stop-daemon --start --quiet --pidfile $PIDFILE --chuid $NODEUSER --background --exec $DAEMON --test > /dev/null) + if [ "$?" = 1 ]; then + echo y + fi +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $INIT_SCRIPT_NAME_NOEXT" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $INIT_SCRIPT_NAME_NOEXT" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + #reload|force-reload) + # + # If do_reload() is not implemented then leave this commented out + # and leave 'force-reload' as an alias for 'restart'. + # + #log_daemon_msg "Reloading $DESC" "$NAME" + #do_reload + #log_end_msg $? + #;; + restart|force-reload) + # + # If the "reload" option is implemented then remove the + # 'force-reload' alias + # + log_daemon_msg "Restarting $INIT_SCRIPT_NAME_NOEXT" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + status) + do_status + ;; + *) + #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 + echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2 + exit 3 + ;; +esac + +exit 0 diff --git a/lib/templates/redhat b/lib/templates/redhat new file mode 100644 index 0000000..06756ef --- /dev/null +++ b/lib/templates/redhat @@ -0,0 +1,65 @@ +#!/bin/sh +# +# chkconfig: 35 99 99 +# description: {{description}} +# +# Author: {{author}} +# Created: {{created}} + +. /etc/rc.d/init.d/functions + +PIDFILE="{{pidroot}}/{{label}}.pid" +ps -fe | grep "{{label}}" | head -n1 | cut -d" " -f 6 > ${PIDFILE} + +USER="{{user}}" +DAEMON="{{execpath}}" +SCRIPT="{{script}}" +LABEL="{{label}}" +LOG_FILE="{{logroot}}/$LABEL.log" +LOCK_FILE="/var/lock/subsys/node-daemon-$LABEL" +ENV="{{env}}" + +do_start() +{ + if [ ! -f "$LOCK_FILE" ] ; then + echo -n $"Starting $LABEL: " + runuser -l "$USER" -c "$ENV $DAEMON $SCRIPT >> $LOG_FILE &" && echo_success || echo_failure + RETVAL=$? + echo + [ $RETVAL -eq 0 ] && touch $LOCK_FILE + else + echo "$SCRIPT is locked." + RETVAL=1 + fi +} + +do_stop() +{ + echo -n $"Stopping $LABEL: " + PID=`pgrep -f "$SCRIPT"` + kill -9 $PID > /dev/null 2>&1 && echo_success || echo_failure + if [ -f ${PIDFILE} ]; then + rm ${PIDFILE} + fi + RETVAL=$? + echo + [ $RETVAL -eq 0 ] && rm -f $LOCK_FILE +} + +case "$1" in + start) + do_start + ;; + stop) + do_stop + ;; + restart) + do_stop + do_start + ;; + *) + echo "Usage: $0 {start|stop|restart}" + RETVAL=1 +esac + +exit $RETVAL diff --git a/lib/wrapper.js b/lib/wrapper.js new file mode 100644 index 0000000..70e775a --- /dev/null +++ b/lib/wrapper.js @@ -0,0 +1,174 @@ +// Handle input parameters +var optimist = require('optimist'), + fs = require('fs'), + max = 60, + p = require('path'), + argv = optimist + .demand('file') + .alias('f','file') + .describe('file','The absolute path of the script to be run as a process.') + .check(function(argv){ + require('fs').existsSync(p.resolve(argv.f),function(exists){ + return exists; + }); + }) + .demand('log') + .alias('l','log') + .describe('log','The absolute path of the log file.') + .demand('errorlog') + .alias('e','errorlog') + .describe('errorlog','The absolute path of the error log file.') + .demand('title') + .alias('t','title') + .describe('title','The name/title of the process.') + .default('maxretries',-1) + .alias('m','maxretries') + .describe('maxretries','The maximim number of times the process will be auto-restarted.') + .default('maxrestarts',5) + .alias('r','maxrestarts') + .describe('maxrestarts','The maximim number of times the process should be restarted within a '+max+' second period shutting down.') + .default('wait',1) + .alias('w','wait') + .describe('wait','The number of seconds between each restart attempt.') + .check(function(argv){ + return argv.w >= 0; + }) + .default('grow',.25) + .alias('g','grow') + .describe('grow','A percentage growth rate at which the wait time is increased.') + .check(function(argv){ + return (argv.g >= 0 && argv.g <= 1); + }) + .default('abortonerror','no') + .alias('a','abortonerror') + .describe('abortonerror','Do not attempt to restart the process if it fails with an error,') + .check(function(argv){ + return ['y','n','yes','no'].indexOf(argv.a.trim().toLowerCase()) >= 0; + }) + .argv, + //log = new Logger(argv.e == undefined ? argv.l : {source:argv.l,eventlog:argv.e}), + fork = require('child_process').fork, + script = p.resolve(argv.f), + wait = argv.w*1000, + grow = argv.g+1, + attempts = 0, + startTime = null, + starts = 0, + child = null; + +process.title = argv.t || 'Node.JS Script'; + +// Log Formatting - Standard Output Hook +console.log = function (data) { + fs.appendFileSync(argv.log,'\n'+new Date().toLocaleString()+' - '+data); +}; + +console.error = function (data) { + fs.appendFileSync(argv.errorlog,'\n'+new Date().toLocaleString()+' - '+data); +}; + +console.warn = function (data) { + fs.appendFileSync(argv.log,'\n'+new Date().toLocaleString()+' - WARNING: '+data); +}; + +console.log(''); + +if (argv.env){ + if (Object.prototype.toString.call(argv.env) === '[object Array]'){ + for(var i=0;i= argv.r){ + if (new Date().getTime()-(max*1000) <= startTime.getTime()){ + console.error('Too many restarts within the last '+max+' seconds. Please check the script.'); + process.exit(); + } + } + + setTimeout(function(){ + wait = wait * grow; + attempts += 1; + if (attempts > argv.m && argv.m >= 0){ + console.error('Too many restarts. '+argv.f+' will not be restarted because the maximum number of total restarts has been exceeded.'); + process.exit(); + } else { + launch(); + } + },wait); + } else { + attempts = 0; + wait = argv.w * 1000; + } +}; + + +/** + * @method launch + * A method to start a process. + */ +var launch = function(){ + //log.info('Starting '+argv.f); + + // Set the start time if it's null + if (startTime == null) { + startTime = startTime || new Date(); + setTimeout(function(){ + startTime = null; + starts = 0; + },(max*1000)+1); + } + starts += 1; + + // Fork the child process + child = fork(script,{env:process.env}); + + // When the child dies, attempt to restart based on configuration + child.on('exit',function(code){ + console.warn(argv.f+' stopped running.'); + + // If an error is thrown and the process is configured to exit, then kill the parent. + if (code !== 0 && argv.a == "yes"){ + console.error(argv.f+' exited with error code '+code); + process.exit(); + server.unref(); + } + + delete child.pid; + + // Monitor the process + monitor(); + }); +}; + +process.on('exit',function(){ + if (child.pid){ + process.kill(child.pid) + }; + process.exit(); +}); + +// Launch the process +launch(); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..0926ab8 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "node-linux", + "version": "0.1.0", + "description": "Support daemon creation and management on Linux.", + "keywords": [ + "ngn", + "linux", + "daemon", + "service", + "centos", + "redhat", + "debian", + "ubuntu" + ], + "author": "Corey Butler ", + "devDependencies": { + "mocha": "*" + }, + "main": "lib/node-linux.js", + "dependencies": { + "optimist": "~0.4.0", + "mu2": "~0.5.17" + }, + "readmeFilename": "README.md", + "scripts": { + "test": "mocha" + }, + "repository": { + "type": "git", + "url": "git://github.com/coreybutler/node-linux.git" + }, + "license": "MIT" +}