Source: pluginManager/pluginManager.js

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;