const fs = require('fs');
const EventEmitter = require('events');
// Generic Plugin-Object
const Plugin = require('./plugin');
// Objects used and distibuted by PluginManager
const FCMTokens = require('../models/fctmtokens');
/**
* Generic ALARMiator Plugin Manager Class
*
* author: Jens Dinstühler
* vesion: 1.0.0
*
* @class
*/
class ALARMiatorPluginManager extends EventEmitter {
/**
* @constructor
*/
constructor() {
super();
// General settings for Plugin Manager
this.inboundPluginsFolder = global.appRoot + '/plugins/inbound';
this.outboundPluginsFolder = global.appRoot + '/plugins/outbound';
this.pluginList = [];
this.plugins = [];
this.requireList = [];
this.instances = [];
this.pluginObjects = [];
this.configStore = [];
// Global Sqlite3 Storage
this.coreDb = global.coreDb;
// Logger for Plugin Ecosystem
this.startLogger();
this.logger.info('PLUGINMANAGER | Start ----------------------------');
process.on('uncaughtException', function (error) {
console.log(error);
});
}
/**
* Starts Logging which is provided to all plugins
*/
startLogger() {
// create a rolling file logger based on date/time that fires process events
const opts = {
logDirectory: global.appRoot + '/logs/plgmanager', // NOTE: folder must exist and be writable...
fileNamePattern: 'ALARMiator-PluginManager-<DATE>.log',
dateFormat: 'YYYY.MM.DD'
};
this.logmanager = require('simple-node-logger');
this.logger = this.logmanager.createRollingFileLogger(opts);
this.logger.setLevel('all');
}
/**
* Reads the inbound plugins
*/
readInboundPluginList() {
fs.readdirSync(this.inboundPluginsFolder).forEach(file => {
if (this.isValidPlugin(this.inboundPluginsFolder + '/' + file)) {
var manifest = this.readPluginManifest(this.inboundPluginsFolder + '/' + file);
this.logger.debug('PLUGINMANAGER | read Inbound Plugins | valid Plugin found | ' + manifest.name + ' : ' + this.inboundPluginsFolder + '/' + file);
this.pluginList.push(manifest);
} else {
this.logger.debug('PLUGINMANAGER | read Inbound Plugins | invalid Plugin found: ' + this.inboundPluginsFolder + '/' + file);
};
});
}
/**
* Reads the outbound plugins
*/
readOutboundPluginList() {
fs.readdirSync(this.outboundPluginsFolder).forEach(file => {
if (this.isValidPlugin(this.outboundPluginsFolder + '/' + file)) {
var manifest = this.readPluginManifest(this.outboundPluginsFolder + '/' + file);
this.logger.debug('PLUGINMANAGER | read outbound Plugins | valid outbound Plugin found | ' + manifest.name + ' : ' + this.outboundPluginsFolder + '/' + file);
this.pluginList.push(manifest);
} else {
this.logger.debug('PLUGINMANAGER | read outbound Plugins | invalid outbound Plugin found: ' + this.outboundPluginsFolder + '/' + file);
};
});
}
/**
* Registers Plugin in Core Plugin Store
* @param {object} manifest manifest object
*/
registerPluginsInStore(pluginList) {
pluginList.forEach((manifest) => {
// check if entry exists for plugin
var sql = "SELECT * FROM pluginstore WHERE namespace = '" + manifest.namespace + "'";
this.coreDb.all(sql, [], (err, rows) => {
if (err) {
this.logger.error('PLUGINMANAGER | ' + err);
}
//this.logger.debug('PLUGINMANAGER | ' + rows.length + ' configurations in database found for Plugin ' + manifest.name)
if (rows.length > 0) {
// existing entry
// Check if version of installed plugin is different to the manifest version
if (manifest.version !== rows[0].version) {
// manifest has differen version than plugin config in database
// if manifest offers update-method, call this method
this.logger.debug('PLUGINMANAGER | Plugin ' + manifest.name + ' show different version (' + manifest.version + ') than registered in pluginstore (' + rows[0].version + ')');
if (manifest.updateMethod !== null) {
if (typeof manifest.updateMethod !== 'undefined') {
if (manifest.updateMethod.length !== 0) {
// call update method of plugin
this.logger.debug('PLUGINMANAGER | Calling Update-function for Plugin ' + manifest.name);
// TBD
}
}
}
this.logger.debug('PLUGINMANAGER | Updating version information for plugin ' + manifest.name + ' in config store');
this.coreDb.run(`UPDATE pluginstore SET version = ? WHERE namespace = ?`,
[
manifest.version,
manifest.namespace,
],
function (err) {
if (err) {
//this.logger.error('PLUGINMANAGER | Error updating configuratio for plugin ' + manifest.name + ' in config store: ' + err.message);
} else {
// get the last insert id
//this.logger.info(`PLUGINMANAGER | Configuration for plugin ` + manifest.name + ` successfully updated in config store`);
}
});
}
} else {
console.log(manifest);
// new plugin, no entry in plugin store
this.coreDb.run(`INSERT INTO pluginstore (namespace, version, state, configstore, plgtype, alarmingByUser, fcmtokenconsumer) VALUES(?, ?, ?, ?, ?, ?, ?)`,
[
manifest.namespace,
manifest.version,
manifest.state,
JSON.stringify(manifest.configstore),
manifest.plgtype,
manifest.alarmingByUser,
manifest.fcmtokenconsumer
],
function (err) {
if (err) {
console.log('PLUGINMANAGER | Error inserting new plugin to config store: ' + err.message);
} else {
//this.logger.info('PLUGINMANAGER | successfully inserted plugin into config store');
// get the last insert id
// check if plugin manfiest offers a setupMethod which needs to be called for the plugin
if (manifest.setupMethod !== null) {
if (typeof manifest.setupMethod !== 'undefined') {
if (manifest.setupMethod.length !== 0) {
//this.logger.info(`PLUGINMANAGER | Executing Setup method for plugin ` + manifest.name);
// There is a setup method definded
// Call this method now
// TBD
}
}
}
}
});
}
});
// if not -> create one
})
}
/**
* Initializes all Plugins in PluginList which are active
*/
initializePlugins() {
this.pluginList.forEach((plugin) => {
if (this.isPluginActive(plugin.namespace)) {
this.logger.info('PLUGINMANAGER | Initializing plugin ' + plugin.pluginpath);
var instance = {
namespace: plugin.namespace,
classPath: plugin.pluginpath + '/base.js'
}
this.requireList.push(instance);
//this.plugins.push(require(plugin.pluginpath + '/' + plugin.main));
} else {
this.logger.info('PLUGINMANAGER | Ignoring plugin ' + plugin.pluginpath + ' (not activated)');
}
});
// Now instanziate plugins
this.requireList.forEach((instance, index) => {
this.loadClass(instance.classPath, instance.namespace);
});
this.logger.info('PLUGINMANAGER | Finished initialization of plugins.');
}
/**
* instanziates a new class object for given namespace
* @param {string} Path filepath to js-file containing the class
* @param {*} namespace namespace the new class instance shall be registered for
*/
loadClass(Path, namespace) {
this.logger.info(`PLUGINMANAGER | initialising ${Path} for namespace ${namespace}`);
var requiredClass = require(Path);
this.pluginObjects[namespace] = new requiredClass;
}
/**
* Checks if plugin is valid (has needed files in path)
* @param {string} path base path of plugin directory to be checked
*/
isValidPlugin(path) {
var retVal = false;
try {
if (fs.existsSync(path + '/manifest.json')) {
retVal = true;
}
} catch (err) {
retVal = false;
}
return retVal;
}
/**
* Reads plugin manifest and return its content as an object.
* @param {strign} path base path of plugin directory to be checked
*/
readPluginManifest(path) {
var manifestObj = null;
try {
var manifest = JSON.parse(fs.readFileSync(path + '/manifest.json', 'utf8'));
var manifestObj = new Plugin();
manifestObj.name = manifest.name;
manifestObj.namespace = manifest.namespace;
manifestObj.logidentifier = manifest.logidentifier;
manifestObj.version = manifest.version;
manifestObj.description = manifest.description;
manifestObj.main = manifest.main;
manifestObj.author = manifest.author;
manifestObj.plgtype = manifest.plgtype;
manifestObj.state = 0;
manifestObj.pluginpath = path;
manifestObj.isService = manifest.isService;
manifestObj.configstore = manifest.configstore;
manifestObj.setupMethod = manifest.setupMethod;
manifestObj.updateMethod = manifest.updateMethod;
if (typeof manifest.alarmingByUser !== 'undefined') {
manifestObj.alarmingByUser = manifest.alarmingByUser;
}
if (typeof manifest.fcmtokenconsumer !== 'undefined') {
manifestObj.fcmtokenconsumer = manifest.fcmtokenconsumer;
}
} catch (err) {
this.logger.error('PLUGINMANAGER | Error reading Manifest for Plugin ' + path);
this.logger.error('PLUGINMANAGER | ' + err.message);
}
return manifestObj;
}
/**
* Resets Plugin in Core Plugin Store
* @param {object} manifest manifest object
*/
resetPluginInStore(manifest) {
// Query for reseting plugin in pluginStore back to manifest settings
var query = `UPDATE 'pluginstore' SET `+
`'version' = ?, ` +
`'state' = ?, ` +
`'configstore' = ?, ` +
`'plgtype' = ? `+
"WHERE `pluginstore`.`namespace` = ?";
this.coreDb.run(query,
[
manifest.version,
manifest.state,
JSON.stringify(manifest.configstore),
manifest.plgtype,
manifest.namespace
],
function (err) {
if (err) {
this.logger.error('PLUGINMANAGER | Error reseting plugin in plugin store: ' + err.message);
} else {
this.logger.debug('PLUGINMANAGER | Reseted plugin [' + manifest.namespace + '] in plugin store');
}
}.bind(this));
}
/**
* resets Manifest for given namespace
* @param {string} namespace namespace the manifest should be reseted for
*/
resetManifestForNamespace(namespace) {
// get path for namespace
var refreshedPluginList = [];
var bolUpdated = false;
this.pluginList.forEach((manifest) => {
if (manifest.namespace === namespace) {
// we've found the manifest for given namespace
// now extract path and call readPluginManifest
var plgPath = manifest.pluginpath;
var updatedManifest = this.readPluginManifest(plgPath);
this.resetPluginInStore(updatedManifest);
refreshedPluginList.push(updatedManifest);
bolUpdated = true;
} else
refreshedPluginList.push(manifest);
});
if (bolUpdated === true) {
// a new manifest has been read
this.pluginList = refreshedPluginList;
return true;
} else {
return false;
}
};
/**
* loads config json from configstore and represents it as an object
* @param {string} namespace
*/
loadConfigFromStore(namespace, callback) {
var sql = "SELECT configstore FROM pluginstore WHERE namespace = '" + namespace + "'";
this.coreDb.all(sql, [], (err, rows) => {
if (err) {
this.logger.error('PLUGINMANAGER | ' + err);
callback(null);
}
if (rows.length > 0) {
this.logger.debug('PLUGINMANAGER | Anzahl Einträge für Plugin [' + namespace + '] in ConfigStore: ' + rows.length);
callback(rows);
} else {
this.logger.debug('PLUGINMANAGER | Keine Einträge für Plugin [' + namespace + '] im Config Store');
}
});
}
/**
* Loads the plugins configuration from core database into class member configStore
* @param {function} callback success (true or false)
*/
initializeConfigFromStore(callback) {
let query = "SELECT * FROM pluginstore";
global.coreDb.all(query, [], (err, rows) => {
if (err) {
this.logger.error('PLUGINMANAGER | Error loading plugin configurations from core database into memory: ' + err.message);
this.configStore = [];
return callback(false);
}
this.logger.debug('PLUGINMANAGER | loaded plugin configurations from core database into memory');
this.configStore = rows;
return callback(true);
});
}
/**
* returns boolean if plugin is configured as active in config store in core database
* Check is based on configStore information pulled by initializeConfigFromStore.
* @param {string} namespace
*/
isPluginActive(namespace) {
var retVal = false;
if (Array.isArray(this.configStore)) {
this.configStore.forEach((config) => {
if (config.namespace === namespace) {
if (config.state === 1) {
retVal = true;
}
}
})
}
return retVal;
}
/**
* Returns number of active inbound plugins
* @returns integer
*/
getCountActivePlugingsInbound() {
var count = 0;
if (Array.isArray(this.configStore)) {
this.configStore.forEach((config) => {
if (config.state === 1) {
if (config.plgtype === 'inbound') {
count++;
}
}
})
}
return count;
}
/**
* Returns number of active outbound plugins
* @returns integer
*/
getCountActivePlugingsOutbound() {
var count = 0;
if (Array.isArray(this.configStore)) {
this.configStore.forEach((config) => {
if (config.state === 1) {
if (config.plgtype === 'outbound') {
count++;
}
}
})
}
return count;
}
/**
* Returns the file path for the plugin identified with namespace
* @param {string} namespace namespace of plugin the pluginpath shall be returned for
*/
getPluginPath(namespace) {
console.log('identifying pluginpath for plugin with namespace: ' + namespace);
var retVal = null;
this.pluginList.forEach((plugin) => {
if (plugin.namespace === namespace) {
retVal = plugin.pluginpath;
}
})
return retVal;
}
/**
* Returns Array with Plugins which offer alarming which can be configured seperately by user
*/
getPluginsWithAlarmingByUser() {
var arrPlugins = [];
if (Array.isArray(this.configStore)) {
this.configStore.forEach((config) => {
if (config.alarmingByUser === 1) {
arrPlugins.push(config);
}
})
}
return arrPlugins;
}
/**
*
* ------------ Event Handling --------------------
*
* inbound Events, plugin Manager offers to plugins
*/
/**
* Sends a notification to a plugin addressed by namespace
* @param {string} namespace namespace of plugin the message shall be forwarded to
* @param {string} message payload for the plugin addressed with namespace
*/
event_plugin_notification(namespace, message) {
this.logger.info('PLUGINMANAGER | EVENT | BROADCAST | event_plugin_notification');
this.emit('event_plugin_notification', namespace, message);
}
/**
* the configuration for a plugin with namespace has been changed.
* The plugin shall itself reload the configuration and restart itself and existing services.
* @param {string} namespace namespace of the plugin whose config has been changed
*/
event_pluginConfig_refreshed(namespace) {
this.logger.info('PLUGINMANAGER | EVENT | BROADCAST | event_pluginConfig_refreshed for ' + namespace);
this.emit('event_pluginConfig_refreshed', namespace);
}
/**
* a new alarm took place
* @param {object} alarmInfo alarm object
*/
event_new_alarm(alarmInfo) {
this.logger.info('PLUGINMANAGER | EVENT | BROADCAST | event_new_alarm');
this.logger.debug(alarmInfo);
this.emit('event_new_alarm', alarmInfo);
}
/**
* a new testalarm shall be sent
* @param {object} alarmInfo alarm object
*/
event_new_test_alarm(basedataId, pluginNamespace, alarmInfo) {
this.logger.info('PLUGINMANAGER | EVENT | BROADCAST | event_new_test_alarm | namespace --> ' + pluginNamespace + ' | basedataId: ' + basedataId);
this.emit('event_new_test_alarm', basedataId, pluginNamespace, alarmInfo);
}
/**
* A new notification to admin shall be sent
* @param {object} notificationInfo notification object
*/
event_new_admin_notification(notificationInfo) {
this.logger.info('PLUGINMANAGER | EVENT | BROADCAST | event_new_admin_notification');
this.emit('event_new_admin_notification', notificationInfo);
}
/**
* A new eMail shall be sent
* @param {object} emailInfo email info object
*/
event_send_email(emailInfo) {
this.logger.info('PLUGINMANAGER | EVENT | BROADCAST | event_send_email');
this.logger.debug(emailInfo);
this.emit('event_send_email', emailInfo);
}
/**
* a new state has been sent
* @param {object} stateInfo State object
*/
event_new_state(stateInfo) {
this.logger.info('PLUGINMANAGER | EVENT | BROADCAST | event_new_state');
this.logger.debug(stateInfo);
this.emit('event_new_state', stateInfo);
}
/**
* a new alarm via zvei-code took place
* @param {object} zveiInfo zvei object
*/
event_new_zveialarm(zveiInfo) {
this.logger.info('PLUGINMANAGER | EVENT | BROADCAST | event_new_zveialarm');
this.logger.debug(zveiInfo);
this.emit('event_new_zvei_alarm', zveiInfo);
}
/**
* external ip address has changed
* @param {string} ipInfo
*/
event_refresh_extip(ipInfo) {
this.logger.info('PLUGINMANAGER | EVENT | BROADCAST | event_refresh_extip');
this.emit('event_refresh_extip', ipInfo);
}
/**
* a pdf document waits for printing to the default printer
* @param {string} pathToPDF
*/
event_print_pdf(pathToPDF) {
this.logger.info('PLUGINMANAGER | EVENT | BROADCAST | event_print_pdf ');
this.emit('event_print_pdf', pathToPDF);
}
/**
* a new feedback response has been received
* @param {string} operationUUID uuid of operation, this feedback belongs to
* @param {string} basedataUUID uuid of basedata sending the new state
* @param {integer} stateId id of feedback state from table
*/
event_new_feedback_received(operationUUID, basedataUUID, stateId) {
this.logger.info('PLUGINMANAGER | EVENT | BROADCAST | event_new_feedback_received');
this.emit('event_new_feedback_received', operationUUID, basedataUUID, stateId);
}
}
module.exports = ALARMiatorPluginManager;