tellki-node-windows
Version:
Support for Tellki Agent Windows service.
614 lines (557 loc) • 17.9 kB
JavaScript
/**
* @class nodewindows.Service
* This utility can be used to manage node.js scripts as Windows services.
*
* **Please note that like all Windows services, creating one requires administrative privileges**.
*
* To create a service with node-windows, prepare a script like:
*
* var Service = require('node-windows').Service;
*
* // Create a new service object
* var svc = new Service({
* name:'Hello World',
* description: 'The nodejs.org example web server.',
* script: 'C:\\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();
*
* The code above creates a new `Service` object, providing a pretty name and description.
* The `script` attribute identifies the Node.js script that should run as a service. Upon running
* this, the script will be visible from the Windows Services utility.
*
* 
*/
var exec = require('child_process').exec,
path = require('path'),
fs = require('fs'),
PermError = 'Permission Denied. Requires administrative privileges.',
wincmd = require('./binaries'),
Logger = require('./eventlog'),
daemonDir = 'daemon',
wrapper = path.resolve(path.join(__dirname,'./wrapper.js'));
// BEGIN SUPER AWFUL HACK TO GET AROUND WINSW.EXE ISSUE! REPLACE ASAP!!!!
// winsw.exe immediately responds with nothing, indicating success, even though
// it continues processing with the "install" method.
var sleep = function(period){
var st = new Date().getTime();
while(new Date().getTime() <= st+(period*1000)){}
return;
};
// The daemon class
var daemon = function(config){
/**
* @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: 'C:\\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: 'C:\\path\\to\\helloworld.js',
* env: [{
* name: "HOME",
* value: process.env["USERPROFILE"] // Access the user home directory
* },{
* name: "NODE_ENV",
* value: "production"
* }]
* });
*/
Object.defineProperties(this,{
_name: {
enumerable: false,
writable: true,
configurable: false,
value: config.name || null
},
_eventlog:{
enumerable: false,
writable: true,
configurable: false,
value: null
},
_xml: {
enumerable: false,
get: function(){
return require('./winsw').generateXml({
name: this.name,
id: this._exe,
script: '"'+wrapper+'" -f "'+this.script+'" -l "'+this.name +'"',
description: this.description,
logpath: this.logpath,
env: config.env,
flags: config.flags
});
}
},
_exe: {
enumerable: false,
get: function(){
return this.id+'.exe';
}
},
_directory: {
enumerable: false,
writable: true,
configurable: false,
value: config.script !== null ? path.dirname(config.script) : null
},
/**
* Resolves the directory where the script is saved.
*/
directory: {
enumerable: false,
writable: false,
configurable: false,
value: function(dir){
if (this.script == null || this.name == null){
throw Error('Script and Name are required but were not provided.');
}
if (dir){
this._directory = path.resolve(dir);
}
return path.resolve(path.join(this._directory,daemonDir));
}
},
/**
* @property {String} root
* The root directory where the process files are stored.
*/
root: {
enumerable: true,
get: function(){ return this.directory();}
},
// Generates the primary logging utility
log: {
enumerable: false,
get: function(){
if (this._eventlog !== null)
return this._eventlog;
if (this.name == null)
throw 'No name was specified for the service';
this._eventlog = new Logger(this.name+' Monitor');
return this._eventlog;
}
},
// The path where log files should be stored
logpath: {
enumerable: true,
writable: false,
configurable: false,
value: config.logpath || null
},
// The log mode. Options are the same as winsw#generateXml
logmode: {
enumerable: true,
writable: false,
configurable: false,
value: config.logmode || 'rotate'
},
// The name of the process
name: {
enumerable: false,
get: function(){return this._name;},
set: function(value){this._name = value;}
},
// The ID for the process
id: {
enumerable: true,
get: function(){
return this.name.replace(/[^\w]/gi,'').toLowerCase();
}
},
// Description of the service
description: {
enumerable: true,
writable: false,
configurable: false,
value: config.description || ''
},
/**
* @property {Object} [user]
* If you need to specify a specific user or particular credentials to manage a service, the following
* attributes may be helpful.
*
* The `user` attribute is an object with three keys: `domain`,`account`, and `password`.
* This can be used to identify which user the service library should use to perform system commands.
* By default, the domain is set to the local computer name, but it can be overridden with an Active Directory
* or LDAP domain. For example:
*
* **app.js**
*
* var Service = require('node-windows').Service;
*
* // Create a new service object
* var svc = new Service({
* name:'Hello World',
* script: require('path').join(__dirname,'helloworld.js')
* });
*
* svc.user.domain = 'mydomain.local';
* svc.user.account = 'username';
* svc.user.password = 'password';
* ...
*
* Both the account and password must be explicitly defined if you want the service module to
* run commands as a specific user. By default, it will run using the user account that launched
* the process (i.e. who launched `node app.js`).
*/
user: {
enumerable: false,
writable: true,
configurable: false,
value: {
account: null,
password: null,
domain: process.env.COMPUTERNAME
}
},
// Optionally provide a sudo password.
sudo: {
enumerable:false,
writable: true,
configurable: false,
value: {
password: null
}
},
/**
* @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
},
/**
* @method install
* Install the script as a process.
* @param {String} [dir=root of script]
* The directory where the process files will be saved. Defaults to #script path.
* @param {Function} [callback]
* The callback to fire when the installation completes.
*/
/**
* @event install
* Fired when the installation process is complete.
*/
/**
* @event alreadyinstalled
* Fired if the script is already known to be a service.
*/
/**
* @event invalidinstallation
* Fired if an installation is detected but missing required files.
*/
/**
* @event error
* Fired in some instances when an error occurs.
*/
install: {
enumerable: true,
writable: false,
configurable: false,
value: function(dir){
if (this.script == null || this.name == null){
throw Error('Script and Name are required but were not provided.');
}
if (this.exists){
var missing = false;
if (!fs.existsSync(path.join(this.root,this._exe))){
this.log.warn('The main executable is missing or cannot be found ('+path.join(this.root,this._exe)+')');
missing = true;
}
if (!fs.existsSync(path.join(this.root,this.id+'.xml'))){
this.log.warn('The primary configuration file is missing or cannot be found ('+path.join(this.root,this.id+'.xml')+')');
missing = true;
}
if (missing.length > 0){
this.emit('invalidinstallation');
return;
}
this.log.warn('The process cannot be installed again because it already exists.');
this.emit('alreadyinstalled');
return;
}
var winsw = require('./winsw'), me = this;
if (typeof dir === 'function'){
callback = dir;
dir = null;
}
dir = this.directory(dir);
// If the output directory does not exist, create it.
fs.exists(dir,function(exists){
if (!exists){
fs.mkdirSync(dir);
}
// Write the configuration file
fs.writeFile(path.resolve(dir,me.id+'.xml'),me._xml,function(){
// Write the exe file
winsw.createExe(me.id,dir,function(){
me.execute('"'+path.resolve(dir,me._exe)+'" install',function(){
sleep(2);
me.emit('install');
});
});
});
});
}
},
/**
* @method uninstall
* Uninstall the service.
* @param {Number} [waitTime]
* Seconds to wait until winsw.exe finish processing the uninstall command.
*
* var Service = require('node-windows').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();
*/
/**
* @event uninstall
* Fired when the uninstall is complete.
*/
uninstall: {
enumerable: true,
writable: false,
value: function(waitTime){
var me = this;
waitTime = waitTime || 2; // The same wait time as in the install method
if (!this.exists){
console.log('Uninstall was skipped because process does not exist or could not be found.');
return;
}
var uninstaller = function(){
// Uninstall the process
me.execute('"'+path.resolve(me.root,me._exe)+'" uninstall',function(error,stdout,stderr){
if (error){
me.checkPermError(error);
} else if (stderr.trim().length > 0){
console.log('Error: ',stderr);
} else {
sleep(waitTime); // Wait for uninstall to fully finish
var rm = function(file){
if (fs.existsSync(path.join(me.root,file))){
fs.unlinkSync(path.join(me.root,file));
}
};
// Remove the daemon files individually to prevent security warnings.
rm(me.id+'.xml');
// Remove known wrappers
rm(me.id+'.wrapper.log');
rm(me.id+'.out.log');
rm(me.id+'.err.log');
//removes exe config file
rm(me.id+'.exe.config');
// Remove the executable
rm(me.id+'.exe');
// Remove all other files
var _files = fs.readdirSync(me.root);
_files.forEach(function(f){
rm(f);
});
if (me.root !== path.dirname(me.script)){
fs.rmdir(me.root,function(){
sleep(1);
me.emit('uninstall');
});
} else {
me.emit('uninstall');
}
}
});
};
this.once('stop',uninstaller);
this.once('alreadystopped',uninstaller);
this.stop();
}
},
/**
* @method start
* Start an existing method.
*/
/**
* @event start
* Fired when the event has started.
*/
start: {
enumerable: true,
writable: false,
configurable: false,
value: function(){
var me = this;
if (this.name == null){
throw "A name for the service is required.";
}
if (!this.exists){
throw Error('The service "'+this.name+'" does not exist or could not be found.');
}
this.execute('NET START "'+me.name+'"',function(err,stdout,stderr){
if (err){
if (err.code == 2){
if (err.message.indexOf('already been started') >= 0 && err.message.indexOf('service name is invalid') < 0){
me.log.warn('An attempt to start the service failed because the service is already running. The process should be stopped before starting, or the restart method should be used.');
me.emit('error',err);
return;
} else if (err.message.indexOf('service name is invalid') < 0){
me.checkPermError(err);
console.log(err);
me.emit('error',err);
return;
}
} else {
me.log.error(err.toString());
}
} else {
me.emit('start');
}
})
}
},
/**
* @method stop
* Stop the service.
*/
/**
* @event stop
* Fired when the service is stopped.
*/
stop: {
enumerable: true,
writable: false,
value: function(){
var me = this;
me.execute('NET STOP "'+me.name+'"',function(err,stdout,stderr){
if (err){
if (err.code == 2){
me.log.warn('An attempt to stop the service failed because the service is/was not running.');
//callback(err);
me.emit('alreadystopped');
} else {
me.checkPermError(err);
}
} else {
me.log.info(stdout);
sleep(7); // Wait for stop to complete.
me.emit('stop');
}
});
}
},
/**
* @method restart
* Restart an existing service
*/
restart: {
enumerable: true,
writable: false,
value: function(callback){
var me = this;
this.once('stop',me.start);
this.stop();
}
},
/**
* @property {Boolean} exists
* Determine whether the service exists.
*/
exists: {
enumerable: true,
get: function(){
if (this.script == null || this.name == null){
throw Error('Script and name are required but were not specified.');
}
return fs.existsSync(path.join(this.directory(),this.id+'.exe')) && fs.existsSync(path.join(this.directory(),this.id+'.xml')) ;
}
},
// Execute commands with elevated privileges.
execute: {
enumerable: false,
writable: false,
configurable: false,
value: function(cmd,options,callback) {
var me = this;
callback = callback || function(){};
options = options || {};
wincmd.isAdminUser(function(isAdmin){
if (isAdmin) {
if (typeof options === 'function'){
callback = options;
options = {};
}
if (me.user.account !== null && me.user.password !== null){
_cmd = "runas /profile /user:"+me.user.domain+"\\"+me.user.account+" "+cmd;
exec(cmd,options,callback);
} else if (me.sudo.password !== null){
// If the user is not an admin, but a sudo password is provided for admin,
// attempt to launch using sudo.
wincmd.sudo(cmd,me.sudo.password||'',options,callback);
} else {
//wincmd.elevate(cmd,options,callback)
exec(cmd,options,callback);
}
} else {
console.log(PermError);
throw PermError;
}
});
}
},
// Check for permission errors
checkPermError: {
enumerable: false,
writable: false,
configurable: false,
value: function(error){
if (error.message.indexOf('Administrator access') >= 0 || error.message.indexOf('Access is denied') >= 0){
try {this.log.error(PermError);} catch(e){console.log(PermError);}
} else {
try {this.log.error(error.toString());} catch(e) {console.log(error.toString());}
}
process.exit(1);
}
}
});
};
var util = require('util'),
EventEmitter = require('events').EventEmitter;
// Inherit Events
util.inherits(daemon,EventEmitter);
// Export functionality for the module.
module.exports = daemon;