UNPKG

ngn-idk-core

Version:
1,025 lines (928 loc) 29.7 kB
/** * @class NGN.core.Process * @singleton * A process enables a globally accessible NGN#configuration, * which is designed to store custom data for use throughout applications. * This class optionally connects with [NGN Mechanic](#!/guide/mechanic), * making it accessible through Mechanic. A process can also send events * to Mechanic, making it possible for Mechanic to respond to application- * specific events. * * When using a process with Mechanic, a secure (TLS) socket connection is * established between the process and Mechanic. If Mechanic is non-responsive * (i.e. temporarily down, network outage, etc), the process will attempt to * reconnect automatically on an adjustable time interval. * * Once a connection to Mechanic is established, an auth handshake is exchanged. * Upon successful authentication and authorization, a communication channel is * opened between the process and Mechanic. * * ## NGN Mechanic Benefits * * The complete list of benefits is available in the [guide](#!/guide/mechanic). * The highlights include system monitoring, remote process control, pooled processing, * OS-specific logging, shell access, and the ability to use native OS daemons/services * for managing the process. * @private */ var fs = require('fs'), os = require('os'), path = require('path'); var Class = NGN.Class.extend({ constructor: function(config,callback){ if (process.hasOwnProperty('mechanic')){ throw new Error('Only one NGN process can run per node process.'); } var me = this; process.ngn = this; callback = callback || function(){}; config = config || {}; if (typeof config == 'function'){ callback = config; config = {}; } // NGN Mechanic Configuration Defaults if (NGN.coalesce(config.enableMechanic,true)){ process.mechanic = {}; Object.defineProperties(process.mechanic,{ /** * @cfg {Number} [mechanicPort=55555] * The port on which [NGN Mechanic](#!/guide/mechanic) is running. */ mechanicPort: { enumerable: false, writable: false, value: config.mechanicPort || 55555 }, /** * @cfg {String} [mechanicHost=localhost] * The host IP address, domain, or URI where [NGN Mechanic](#!/guide/mechanic) is running. */ mechanicHost: { enumerable: false, writable: false, value: config.mechanicHost || 'localhost' }, /** * @cfg {Boolean} [enableMechanic=true] * [NGN Mechanic](#!/guide/mechanic) provides process management and monitoring for all * connected processes on an NGN server. * * If the process cannot connect to Mechanic, it will periodically poll the specified * Mechanic server (default is `localhost`) until a connection is established. If * your process does not need Mechanic, then this attribute should be set to `false`. */ enableMechanic: { enumerable: true, writable: true, value: true }, /** * @cfg {Boolean} [remote=false] * When using [NGN Mechanic](#!/guide/mechanic), this can * be set to force Mechanic to recognize the process as * a remotely hosted process. */ remote: { enumerable: false, get: function(){ if (['127.0.0.1','localhost'].indexOf(process.mechanic.server) < 0){ return NGN.coalesce(config.remote,false); } return true; } }, /** * @cfg {Boolean} system * Indicates this is a system process running on the same * server as [NGN Mechanic](#!/guide/mechanic). Local system * processes are granted a higher level of access in Mechanic * (i.e. they can be used to supplement Mechanic services). */ // Boolean indicator that NGN Mechanic is hosted on the same server internal: { enumerable: false, get: function(){ if (process.mechanic.remote){ return false; } return NGN.coalesce(config.system,false); } }, /** * @cfg {String} [mechanicSecret=null] * (Optional) The shared secret defined in the [NGN Mechanic Configuration](#!/guide/mechanic). */ key: { enumerable: false, writable: true, value: config.mechanicSecret || null }, /** * @cfg {Number} [healthcheckFrequency=5] * The interval, in seconds, between health checks. A * health check is a message sent to [NGN Mechanic](#!/guide/mechanic) containing * data about the utilization of the server on which the process runs. */ healthcheckFrequency: { enumerable: false, writable: false, value: config.healthCheckFrequency || 5 }, // This is the socket client that connects to Mechanic client: { enumerable: false, writable: true, configurable: false, value: new NGN.core.TcpClient({ autoConnect: false, autoReconnect: true, connectionType: 'tls', port: config.mechanicPort || 55555, host: config.mechanicHost || 'localhost' }) }, send:{ enumerable: true, writable: true, value: function(eventName,meta){ if (process.mechanic.client.connected){ var _data = typeof meta == 'object' ? meta : {}; if (typeof meta !== 'object' && meta !== undefined){ _data.data = meta; } process.mechanic.client.send(eventName,_data); } else { console.log((eventName.bold+' failed to fire.').yellow); /** * @event eventFailure * Fired when the process unsuccessfully sends an event to [NGN Mechanic](#!/guide/mechanic). * This event is only fired if a connection to an NGN Mechanic process exists, but cannot be * completed. The most common use case is when an NGN process has established a connection * but has not yet registered/authenticated with the NGN Mechanic service. * @returns {Object} * The resulting object contains two attributes: * - *name*: The event name that failed. * - *meta*: Any metadata fired by the event. */ me.emit('eventFailure',{ name: eventName, meta: meta || null }); } } } }); this._socketHandler(process.mechanic.client); } Object.defineProperties(this,{ /** * @property {Object} [DSN=Object] * Store **data service names** associated with the application. * Each DSN key represents a database connection. * * var userDB = NGN.system.getDatasource('users'); * * *OR* * * var userDB = NGN.system.DSN['users']; * @protected */ DSN: { value: {}, enumerable: true, writable: true }, /** * @property {Object} * Data Manager: Map NGN.model.Model objects to a persistence/storage object (NGN.model.data.Manager). * @protected */ DM: { value: {}, enumerable: true, writable: true }, /** * @property {Object} [SERVER=Object] * Stores servers used in the application. * @protected */ SERVER: { value: {}, enumerable: true, writable: true }, /** * @property {Boolean} [connected=false] * @readonly * Indicates the process is connected to a managing agent (i.e. [NGN Mechanic](!#/guide/mechainc)). */ connected: { enumerable: true, get: function(){ return NGN.coalesce(process.mechanic.client.connected,false); } }, _monitor: { enumerable: false, writable: true, configurable: false, value: null }, /** * @property {Boolean} [initialized=false] * Indicates the process has been initialized with [NGN Mechanic](#!/guide/mechanic). * @readonly */ initialized: { enumerable: false, writable: true, value: false }, /** * @method initialize * Initialize the configuration after it has been identified. * @param {Function} callback * Receives the mechanic connection as the only callback argument. * @protected */ initialize: { enumerable: false, writable: true, value: function(callback){ if (this.initialized){ callback(this); return; } Object.defineProperties(this,{ /** * @cfg {String} [name=NGN_Application] * The name/title of the process. */ name: { enumerable: true, get: function(){ return process.title || 'NGN_Application'; }, set: function(value){ process.title = value || 'NGN_Application'; } }, /** * @cfg {String} [description] * A description of the process. */ description: { enumerable: true, writable: true, value: config.description || this.name }, /** * @cfg {Array/String} administrators * An email address or array of email addresses representing * administrative contacts who will receive alerts about this * process. */ administrators: { enumerable: true, writable: true, value: config.administrators || [] }, __elements: { value: [], enumerable: false, writable: true } }); // Set the process name. if(config.name) this.name = config.name; // Create an array of admins this.administrators = typeof this.administrators == 'string' ? this.administrators.split(',') : this.administrators; // Load elements for (var el in this){ if (this.hasOwnProperty(el)) this.__elements.push(el); } // Self reference var me = this; // Create a global reference to configuration Object.defineProperties(NGN,{ configuration: { enumerable: false, get: function() { return me; } }, config: { enumerable: false, get: function(){ return NGN.configuration; } }, cfg: { enumerable: false, get: function(){ return NGN.configuration; } } }); this.initialized = true; callback && callback(this); } } }); Class.super.constructor.call(this,config); // Execute the callback if defined if (NGN.coalesce(config.enableMechanic,true)){ process.mechanic.client.connect(function(){ me.initialize(); }); callback && callback(me); } else { callback && callback(this); } }, /* * The socket handler manages communication with NGN Mechanic. */ _socketHandler: function(socket){ // Only works if mechanic is enabled. if (!process.hasOwnProperty('mechanic')){ return null; } // NGN Mechanic Processes var mechProcs = process.mechanic.client == undefined ? [] : [ 'register', 'authorizationRequest', 'adminAuthorizationRequest', 'identificationDataSent', 'identificationRequest', 'credentialDataSent', 'authorized', 'authenticated', 'heartbeat', 'serverConfigurationRequest', 'unauthorized', 'authFailure', 'adminAccessGranted', 'adminAccessRejected', 'heartbeatRequest', 'presumeDeadConnection', 'ready' ], genProcs = [ 'connect', 'connecting', 'disconnect', 'reconnect', 'newCustomEvent', 'error', 'reconnecting' ]; var procs = genProcs.concat(mechProcs), me = this; // Support for NGN Mechanic processes mechProcs.forEach(function(proc){ if (proc !== null){ socket.on(proc,function(){ if (mechProcs.indexOf(proc) < 0) { me.emit(proc,arguments); } else { if (me['onMechanic'+NGN.string.capitalize(proc)] == undefined) { me.fireWarning('onMechanic'+NGN.string.capitalize(proc)+' is not defined. Processing "'+proc+'" command aborted.'); } else { me['onMechanic'+NGN.string.capitalize(proc)](arguments[0]); } } }); } }); // Support for custom events socket.on('newCustomEvent',function(event,args){ var obj = {name:event.trim()}; if (args){ obj.args = args; } if (!socket._emitter._events.hasOwnProperty(obj.name)){ socket._emitter._events[obj.name] = function(){ me.emit(obj.name,arguments[0]); } } }); // Generic Events socket.on('disconnect',function(){ me.onDisconnect(); }); socket.on('connecting',function(){ me.onConnecting(); }); socket.on('connect',function(socket){ me.onConnect(); }); socket.on('reconnect',function(){ me.onReconnect(); }); socket.on('reconnecting',function(){ me.onReconnecting(); }); socket.on('serverFault',function(){ me.onServerFault(); }); socket.on('error',function(e){ me.fireError(e); }); socket.on('ready',function(){ me.onReady(); }) }, /** * @method getRegistrationData * Gets registration data for [NGN Mechanic](#!/guide/mechanic). * @private */ getRegistrationData: function(){ return { internal: process.mechanic.remote == true ? false : process.mechanic.internal, remote: process.mechanic.remote, name: this.name, description: this.description, admin: this.administrators || [], // List of administrator objects pid: process.pid, uptime: process.uptime(), memory: process.memoryUsage() }; }, /** * @method monitor * Sends a heartbeat notice to [NGN Mechanic](#!/guide/mechanic) with system load details. * @param {Boolean} [forceRestart=false] * Set this to `true` when forcing a restart. * @private */ monitor: function(restart){ var me = this; restart = NGN.coalesce(restart,false); if (!this.connected){ this._monitor = null; return; } if (restart){ clearTimeout(this._monitor); this._monitor = null; } var hb = function(){ if (!me.connected){ return; } var msg = { uptime: process.uptime(), memory: process.memoryUsage(), i: process.mechanic.client.bytesRead, o: process.mechanic.client._bytesDispatched }; if (me.remote){ var server = me.getHostUtilization(); for (var i in server){ msg[i] = server[i]; } } /** * @event heartbeat * Fired when the heartbeat/healthcheck is sent to [NGN Mechanic](#!/guide/mechanic). */ me.emit('heartbeat',msg); process.mechanic.send('heartbeat',msg); me._monitor = setTimeout(hb,process.mechanic.healthcheckFrequency*1000); }; if (this._monitor == null){ hb(); } }, /** * @method getHostDetails * Get the details of the host machine on which the process is running. * @returns {Object} */ getHostDetails: function(){ return { platform: process.platform, arch: process.arch, os: { type: os.type(), release: os.release() }, cpu: os.cpus(), nic: os.networkInterfaces(), hostname: os.hostname(), node: process.versions }; }, /** * @method getHostUtilization * Get the host utilization levels (uptime, loadavg, memory usage). * @returns {Object} */ getHostUtilization: function(){ return { uptime: os.uptime(), loadavg: os.loadavg(), memory: { total: os.totalmem(), free: os.freemem() } }; }, /** * @method on * Listens for a specific event. * @param {String} eventName * The name of the event to listen for. * @param {Function} callback * Optional callback. */ on: function(eventName, callback){ this._emitter.on(eventName,callback); }, /** * @method fireEvent * Fires the specified event. * @param {String} eventName * The name of the event to fire. * @param {Object} [metadata] * Optional JSON metadata. */ fireEvent: function( eventName, metadata ) { this._emitter.emit( eventName, metadata || null ); }, /** * @method bubbleEvent * Fires the specified event and bubbles it up to the [NGN Mechanic](#!/guide/mechanic). * @param {String} eventName * The name of the event to bubble. * @param {Object} [metadata] * JSON data to bubble with the event. */ bubbleEvent: function( eventName, metadata ) { this.fireEvent( eventName, metadata || null ); process.mechanic.send(eventName, metadata || null); }, /** * @method fireError * Fires the specified error. * @param {Object} [metadata] * JSON data of the error. */ fireError: function( err ) { this.fireEvent( 'error', err || null ); }, /** * @method * Get a server instance by it's registered name. * @param {String} name * The name of the server. */ getServer: function(name){ return NGN.getServer(name); }, /** * @method * Get servers by a specific type. This returns an object with each attribute of the * object being the name of a server and each value being a reference to the server object. * @param {String} type * The type of server. * @returns {Object} */ getServersByType: function(type){ return NGN.getServersByType(type); }, /** * @method * Create and register a #DSN. * @param {String} name * The name by which the #DSN is referenced. * @param {NGN.datasource.Connection} connection * A connection to a specific database or data store. */ createDatasource: function( name, connection ){ this.onBeforeCreateDSN(name, connection); this.DSN[name] = connection; this.onCreateDSN(name, connection); }, /** * @method * Returns the specified datasource connection. * @param {String} name * The reference name of the #DSN to return. * @returns NGN.datasource.Connection */ getDatasource: function( name ){ return this.DSN[name]; }, /** * @method * Shortcut. Equivalent to #getDatasource. * @param {String} name * The reference name of the #DSN to return. * @returns NGN.datasource.Connection */ getDSN: function( name ){ return this.getDatasource(name); }, /** * @method * Removes a datasource connection. * @param {String} name * The reference name of the #DSN to remove. */ removeDatasource: function( name ){ this.onBeforeremoveDatasurce(name, this.DSN[name]); delete this.DSN[name]; this.onremoveDatasource(name); }, /** * @method * Shortcut. Equivalent to #removeDatasource. * @param {String} name * The reference name of the #DSN to remove. */ removeDSN: function( name ){ this.removeDatasource(name); }, /** * @method * Register a server * @param {NGN.core.Server} server * The server instance */ registerServer: function(server){ //if (this.SERVER['__'+server.type] == undefined) // this.SERVER['__'+server.type] = {}; var ct = 0; while (this.SERVER[server.id] !== undefined){ ct++; server.id = server.id + ct.toString(); } Object.defineProperty(this.SERVER,server.id,{ enumerable: true, get: function(){ return server; } }); this.onregisterServer(server); }, /** * @method * Unregister a server. This will remove the instance from the application. */ unregisterServer: function(name) { this.onunregisterServer(this.SERVER[name]); delete this.SERVER[name]; }, /** * This event is fired just prior to the creation of a #DSN. * @event beforecreateDatasource * @param {String} name * The reference name of the new datasource. * @param {NGN.datasource.Connection} connection * The connection about to be created. */ onBeforeCreateDSN: function( name, connection ) { this.fireEvent('beforecreateDatasource', name, connection || null); }, /** * This event is fired just after the creation of a #DSN. * @event createDatasource * @param {String} name * The reference name of the new datasource. * @param {NGN.datasource.Connection} connection * The connection object just created. */ onCreateDSN: function( connection ) { this.fireEvent('createDatasource',connection); }, /** * This event is fired just prior to removeing a #DSN. * @event beforeRemoveDatasource * @param {String} name * The reference name of the datasource. * @param {NGN.datasource.Connection} connection * The connection object about to be removeed. */ onbeforeRemoveDatasource: function( name ) { this.fireEvent('beforeRemoveDatasource', name, connection); }, /** * This event is fired just after the destruction of a #DSN. * @event removeDatasource * @param {String} name * The reference name of the new datasource. */ onRemoveDatasource: function( name ) { this.fireEvent('removeDatasource',name); }, /** * @event registerServer * Fired when a server is registered * @returns {NGN.core.Server/null} */ onregisterServer: function(server){ this.emit('serverUnregistered',server||null); }, /** * @event unregisterServer * Fired when a server is unregistered/removed * @returns {NGN.core.Server/null} */ onUnregisterServer: function(server){ this.fireEvent('unregisterServer',server||null); }, /** * @event ready * Fired when the application is ready. */ onReady: function(){ console.info(this.name+' application is running.'); this.fireEvent('ready'); }, /** * @event disconnect * Fired when the process drops its connection to [NGN Mechanic](#!/guide/mechanic). */ onDisconnect: function(){ this.emit('disconnect'); }, /** * @event connect * Fired when the process establishes a connection to [NGN Mechanic](#!/guide/mechanic). */ onConnect: function(){ this.emit('connect'); }, /** * @event connecting * Fired when the process is attempting to connect to [NGN Mechanic](#!/guide/mechanic). */ onConnecting: function(){ this.emit('connecting'); }, /** * @event reconnect * Fired when the process re-establishes a connection to [NGN Mechanic](#!/guide/mechanic). */ onReconnect: function(){ this.emit('reconnect'); }, /** * @event reconnecting * Fired when the process is attempting to reconnect to [NGN Mechanic](#!/guide/mechanic). */ onReconnecting: function(){ this.emit('reconnecting'); }, /** * @event serverFault * Fired when [NGN Mechanic](#!/guide/mechanic) is unreachable and has never been reachable. */ onServerFault: function(){ this.emit('serverFault'); }, /** * @event mechanicReady * Fired when [NGN Mechanic](#!/guide/mechanic) is ready for i/o. * This is fired after authentication/authorization. */ onMechanicReady: function(){ this.emit('mechanicReady'); this.monitor(); }, /** * @event authFailure * Fired when the authentication data is rejected. * @param {String} reason * The reason for failure. */ onMechanicAuthFailure: function(reason){ this.emit('authFailure',reason.message||reason||'Unknown'); }, /** * @event authorizationDataSent * Fired when authorization data is sent to [NGN Mechanic](#!/guide/mechanic). */ /** * @event authorizationRequest * Fired when [NGN Mechanic](#!/guide/mechanic) requests identification. */ onMechanicAuthorizationRequest: function(){ this.emit('authorizationRequest'); process.mechanic.send('authorizationToken',process.mechanic.key||''); }, /** * @event serverConfigurationRequest * Fired when [NGN Mechanic](#!/guide/mechanic) requests the * hardware details of the server on which the process is currently * running. */ onMechanicServerConfigurationRequest: function(){ this.emit('serverConfigurationRequest'); var msg = this.getHostDetails(), utl = this.getHostUtilization(); for (var u in utl){ msg[u] = utl[u]; } this.remote = true; process.mechanic.send('serverConfigurationData',msg); }, /** * @event identificationSent * Fired when the process sends identification data * to [NGN Mechanic](#!/guide/mechanic). This is * usually a response to the #identifiactionRequest event. */ /** * @event identificationRequest * Fired when [NGN Mechanic](#!/guide/mechanic) requests * the process identification information (i.e. registration). */ onMechanicIdentificationRequest: function(){ this.emit('identificationRequest'); var me = this; process.mechanic.send('identification',this.getRegistrationData(),function(){ me.emit('identificationSent'); }); }, /** * @event adminAuthorizationSent * Fired when the administrative access token is * retrieved from the local system and sent to * [NGN Mechanic](#!/guide/mechanic). * This is a response to #adminAuthorizationRequest. */ /** * @event adminAuthorizationRequest * Fired when [NGN Mechanic](#!/guide/mechanic) identifies the * process as a #system process and requests more stringent * authorization. */ onMechanicAdminAuthorizationRequest: function(file){ this.emit('adminAuthorizationRequest'); var me = this; // Read the file and exchange the token fs.readFile(path.join(util.tempDir,file),'utf8',function(err,token){ if (err){me.fireError(err);} process.mechanic.send('adminAuthorizationData',token.toString().trim()); me.emit('adminAuthorizationSent'); }); }, /** * @event authorized * Fired when [NGN Mechanic](#!/guide/mechanic) has authorized * the connection. */ onMechanicAuthorized: function(){ this.emit('authorized'); }, /** * @event unauthorized * Fired when [NGN Mechanic](#!/guide/mechanic) responds * to a request with an "unauthorized" code. */ onMechanicUnauthorized: function(){ this.emit('unauthorized'); }, /** * @event adminAccessGranted * Fired when [NGN Mechanic](#!/guide/mechanic) grants * the process administrative privileges as a local system process. */ onMechanicAdminAccessGranted: function(){ this.emit('adminAccessGranted'); }, /** * @event adminAccessRejected * Fired when [NGN Mechanic](#!/guide/mechanic) grants * the process administrative privileges as a local system process. */ onMechanicAdminAccessRejected: function(){ this.emit('adminAccessGranted'); }, /** * @event heartbeatRequest * Fired when [NGN Mechanic](#!/guide/mechanic) requests a heart monitor. * In the event Mechanic cannot hear the heartbeat produced by this process, * it may request the heartbeat be started. By default, the process will * immediately begin sending a heartbeat to Mechanic. */ onMechanicHeartbeatRequest: function(){ this.emit('heartbeatRequest'); this.monitor(true); }, /** * @event presumeDeadConnection * Fired when [NGN Mechanic](#!/guide/mechanic) believes the connection to * this process is dead. This only happens when Mechanic requests a heartbeat * and does not receive one repeatedly. */ onMechanicPresumeDeadConnection: function(){ this.emit('presumeDeadConnection'); } }); module.exports = Class;