UNPKG

node-mac

Version:

Support daemon creation and management on Mac.

493 lines (448 loc) 12.6 kB
/** * @class nodemac.Daemon * Manage node.js scripts as native Mac 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) { config.runAsAgent = config.hasOwnProperty('runAsAgent') ? config.runAsAgent: false; config.logOnAsUser = config.hasOwnProperty('logOnAsUser') ? config.logOnAsUser: false; var homedir = config.logOnAsUser ? require('os').homedir() : ''; 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.resolve(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 === undefined ) || ( this.script === null ) ) ? '' : this.script.toString() ) }, /** * @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[item.name] = item.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: config.runAsAgent ? homedir + '/Library/LaunchAgents' : '/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 || homedir + '/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, { recursive: true }); } // 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<me.EnvironmentVariables.length;i++){ args.push('--env'); for (var el in me.EnvironmentVariables[i]){ args.push(el+'='+me.EnvironmentVariables[i][el]); } } var tpl = { Title: me.label, Label: me.label, ProgramArguments: args, RunAtLoad: true, //StartInterval: 3600, KeepAlive: false, WorkingDirectory: me.cwd, StandardOutPath: me.outlog, StandardErrorPath: me.errlog }; var data = plist.build(tpl).toString(); if (!fs.existsSync(p.dirname(me.plist))){ fs.mkdirSync(p.dirname(me.plist), {recursive: true}); } // Write the plist file to disk. fs.writeFile(me.plist,data,function(err){ if (err) throw err; // Load the deamon //exec('launchctl load '+me.plist,{},function(){ me.emit('install'); callback && callback(); //}); }); } else { me.emit('alreadyinstalled'); } }); } }, /** * @method uninstall * Uninstall an existing background process/daemon. * @param {Function} [callback] * Executed when the process is uninstalled. */ uninstall: { enumerable: true, writable: true, configurable: false, value: function(callback){ var me = this; this.stop(function(){ exec('rm '+me.plist,{},function(){ var dne = !me.exists; if (fs.existsSync(me.outlog)){ fs.unlinkSync(me.outlog); } if (fs.existsSync(me.errlog)){ fs.unlinkSync(me.errlog); } if (fs.existsSync(me.logpath)){ if (me.logpath !== '/Library/Logs'){ var dir = fs.readdirSync(me.logpath); if (dir.length == 0){ try { fs.unlinkSync(me.logpath); } catch (e) { exec('rm -r '+me.logpath.replace(/\s/gi,'\\ '),function(err){ if (err){ console.log(err); } }); } } } } if (dne) { /** * @event doesnotexist * Fired when an attempt to uninstall the service fails because it does not exist. */ me.emit('doesnotexist'); } me.emit('uninstall'); callback && callback(); }); }); } }, /** * @method start * Start and/or create a daemon. * @param {Function} [callback] */ start:{ enumerable: true, writable: false, configurable: false, value: function(callback){ var me = this; exec('launchctl load '+this.plist,{},function(){ fs.appendFileSync(me.outlog,'\n'+new Date().toLocaleString()+' - '+me.name+' Started'); me.emit('start'); callback && callback(); }); } }, /** * @method stop * Stop the process if it is currently running. * @param {Function} [callback] */ stop: { enumerable: true, writable: false, configurable: false, value: function(callback){ var me = this; exec('launchctl unload '+this.plist,{},function(){ fs.appendFileSync(me.outlog,'\n'+new Date().toLocaleString()+' - '+me.name+' Stopped'); me.emit('stop'); callback && callback(); }); } }, /** * @method restart * @param {Function} [callback] */ restart: { enumerable: true, writable: true, configurable: false, value: function(callback){ var me = this; this.stop(function(){ me.start(callback); }); } } }); }; var util = require('util'), EventEmitter = require('events').EventEmitter; // Inherit Events util.inherits(daemon,EventEmitter); module.exports = daemon;