UNPKG

webgme-engine

Version:

WebGME server and Client API without a GUI

435 lines (381 loc) 18.1 kB
/*globals requirejs, define*/ /*eslint-env node, browser*/ /** * @author pmeijer / https://github.com/pmeijer */ define([ 'common/core/coreQ', 'plugin/PluginResult', 'plugin/PluginMessage', 'plugin/util', 'common/storage/project/interface', 'common/storage/util', 'common/util/tarjan', 'q', ], function (Core, PluginResult, PluginMessage, pluginUtil, ProjectInterface, storageUtil, Tarjan, Q) { 'use strict'; /** * * @param blobClient * @param [project] * @param mainLogger * @param gmeConfig * @constructor */ function PluginManagerBase(blobClient, project, mainLogger, gmeConfig) { var self = this; this.logger = mainLogger.fork('PluginManagerBase'); this.notificationHandlers = []; function getPluginInstance(pluginIdOrClass, callback) { var deferred = Q.defer(), pluginPath; function instantiatePlugin(PluginClass) { var plugin; if (!PluginClass) { // This should not happen, but just in case.. deferred.reject(new Error('Loading plugin "' + pluginIdOrClass + '" with requirejs return undefined.')); return; } plugin = new PluginClass(); if (self.serverSide && plugin.pluginMetadata.disableServerSideExecution) { deferred.reject(new Error(pluginIdOrClass + ' cannot be invoked on server.')); } else if (self.browserSide && plugin.pluginMetadata.disableBrowserSideExecution) { deferred.reject(new Error(pluginIdOrClass + ' cannot be invoked in browser.')); } else { deferred.resolve(plugin); } } if (typeof pluginIdOrClass === 'function') { self.logger.debug('plugin class was passed wont load it with requirejs'); instantiatePlugin(pluginIdOrClass); } else { pluginPath = 'plugin/' + pluginIdOrClass + '/' + pluginIdOrClass + '/' + pluginIdOrClass; self.logger.debug('requirejs plugin from path: ' + pluginPath); requirejs([pluginPath], instantiatePlugin, function (err) { deferred.reject(err); } ); } return deferred.promise.nodeify(callback); } function checkDependencies(plugin, tarjan, callback) { return Q.all(plugin.getPluginDependencies() .map(function (pluginId) { if (tarjan.addVertex(pluginId) === false) { // Dependency already added, just account for the connection tarjan.connectVertices(plugin.getId(), pluginId); return Q.resolve(); } else { tarjan.connectVertices(plugin.getId(), pluginId); return getPluginInstance(pluginId) .then(function (depPluginInstance) { return checkDependencies(depPluginInstance, tarjan); }); } })) .nodeify(callback); } /** * These are used to determine if the user is allowed to execute a plugin based on * the project access level. It also determines if the user is allowed to modify certain config * parameters of the plugin. * N.B. When reading or writing to the project from the plugin the access level is always checked * by the storage. * @type {{read: boolean, write: boolean, delete: boolean}} */ this.projectAccess = { read: true, write: true, delete: true }; /** * */ this.blobClient = blobClient; /** * * @param {string} pluginIdOrClass * @param {object} pluginConfig - configuration for the plugin. * @param {object} context * @param {string} [context.branchName] - name of branch that should be updated * @param {string} [context.commitHash=<%brashHash%>] - commit from which to start the plugin. * @param {ProjectInterface} [context.project=project] - project instance if different from the one passed in. * @param {string} [context.activeNode=''] - path to active node * @param {string[]} [context.activeSelection=[]] - paths to selected nodes. * @param {string} [context.namespace=''] - used namespace during execution ('' represents all namespaces). * @param {function} callback */ this.executePlugin = function (pluginIdOrClass, pluginConfig, context, callback) { var pluginId = typeof pluginIdOrClass === 'string' ? pluginIdOrClass : pluginIdOrClass.metadata.id, plugin; self.initializePlugin(pluginIdOrClass) .then(function (plugin_) { plugin = plugin_; return self.configurePlugin(plugin, pluginConfig, context); }) .then(function () { self.runPluginMain(plugin, callback); }) .catch(function (err) { var pluginResult = self.getPluginErrorResult(pluginId, pluginId, 'Exception was raised, err: ' + err.stack, plugin && plugin.projectId); self.logger.error(err.stack); callback(err.message, pluginResult); }); }; /** * Retrieves plugin script files and creates instance. * @param {string} - pluginId * @param {function} callback * @returns {promise} */ this.initializePlugin = function (pluginId, callback) { var plugin, tarjan; if (!self.serverSide && !self.browserSide) { self.logger.debug('Running as CLI - does not respect gmeConfig.plugin.allowServerExecution..'); } else { if (self.serverSide && !gmeConfig.plugin.allowServerExecution) { throw new Error('Plugin execution on server side is disabled from gmeConfig.'); } else if (self.browserSide && !gmeConfig.plugin.allowBrowserExecution) { throw new Error('Plugin execution on server side is disabled from gmeConfig.'); } } return getPluginInstance(pluginId) .then(function (plugin_) { tarjan = new Tarjan(); plugin = plugin_; tarjan.addVertex(pluginId); return checkDependencies(plugin, tarjan); }) .then(function () { if (tarjan.hasLoops()) { throw new Error('The dependencies of ' + pluginId + ' forms a circular loop..'); } var pluginLogger = self.logger.fork('plugin:' + pluginId); plugin.initialize(pluginLogger, self.blobClient, gmeConfig); plugin.notificationHandlers = self.notificationHandlers; return plugin; }) .nodeify(callback); }; /** * * @param {object} plugin * @param {object} pluginConfig - configuration for the plugin. * @param {object} context * @param {string} context.branchName - name of branch that should be updated * @param {string} [context.commitHash=<%brashHash%>] - commit from which to start the plugin. * @param {ProjectInterface} [context.project=project] - project instance if different from the one passed in. * @param {string} [context.activeNode=''] - path to active node * @param {string[]} [context.activeSelection=[]] - paths to selected nodes. * @param {string} [context.namespace=''] - used namespace during execution ('' represents all namespaces). * @param {function} callback * @returns {promise} */ this.configurePlugin = function (plugin, pluginConfig, context, callback) { var deferred = Q.defer(), self = this, defaultConfig = plugin.getDefaultConfig(), writeAccessKeys = {}, readOnlyKeys = {}, faultyKeys = [], key; context.project = context.project || project; if (context.project instanceof ProjectInterface === false) { deferred.reject(new Error('project is not an instance of ProjectInterface, ' + 'pass it via context or set it in the constructor of PluginManagerBase.')); } else if (plugin.pluginMetadata.writeAccessRequired === true && self.projectAccess.write === false) { deferred.reject(new Error('Plugin requires write access to the project for execution!')); } else { plugin.pluginMetadata.configStructure.forEach(function (configStructure) { if (configStructure.writeAccessRequired === true && self.projectAccess.write === false) { writeAccessKeys[configStructure.name] = true; } if (configStructure.readOnly === true) { readOnlyKeys[configStructure.name] = true; } }); pluginConfig = pluginConfig || {}; for (key in pluginConfig) { if (readOnlyKeys[key] || writeAccessKeys[key]) { // Parameter is not allowed to be modified, check if it was. if (Object.hasOwn(pluginConfig, key) && pluginConfig[key] !== defaultConfig[key]) { faultyKeys.push(key); } } // We do allow extra config-parameters that aren't specified in the default config. defaultConfig[key] = pluginConfig[key]; } if (faultyKeys.length > 0) { deferred.reject(new Error('User not allowed to modify configuration parameter(s): "' + faultyKeys + '".')); } else { plugin.setCurrentConfig(defaultConfig); self.loadContext(context) .then(function (pluginContext) { plugin.configure(pluginContext); deferred.resolve(); }) .catch(deferred.reject); } } return deferred.promise.nodeify(callback); }; /** * * @param plugin * @param callback */ this.runPluginMain = async function (plugin, callback) { var startTime = (new Date()).toISOString(), mainCallbackCalls = 0, multiCallbackHandled = false; self.logger.debug('plugin configured, invoking main'); if (plugin.isConfigured === false) { callback('Plugin is not configured.', self.getPluginErrorResult(plugin.getId(), plugin.getName(), 'Plugin is not configured.', project && project.projectId)); return; } let result, err = null; const expectsCallback = plugin.main.length > 0; const checkMultiCallbacks = () => { mainCallbackCalls += 1; if (mainCallbackCalls > 1) { const stackTrace = new Error().stack; self.logger.error('The main callback is being called more than once!', {metadata: stackTrace}); result.setError('The main callback is being called more than once!'); if (multiCallbackHandled === true) { plugin.createMessage(null, stackTrace); return; } multiCallbackHandled = true; result.setSuccess(false); plugin.createMessage(null, 'The main callback is being called more than once.'); plugin.createMessage(null, stackTrace); callback('The main callback is being called more than once!', result); } }; try { if (expectsCallback) { result = await new Promise((resolve, reject) => plugin.main(function (err, res) { result = res || result; if (err) { reject(err); } resolve(); setTimeout(checkMultiCallbacks); }) ); } else { result = await plugin.main(); } } catch (e) { err = e; } result = result || plugin.result; self.logger.debug('plugin main callback called', {result: result.serialize()}); // set common information (meta info) about the plugin and measured execution times result.setFinishTime((new Date()).toISOString()); result.setStartTime(startTime); result.setPluginName(plugin.getName()); result.setPluginId(plugin.getId()); result.setError(err); plugin.notificationHandlers = []; callback(typeof err === 'string' ? new Error(err) : err, result); }; this.getPluginErrorResult = function (pluginId, pluginName, message, projectId) { var pluginResult = new PluginResult(), pluginMessage = new PluginMessage(); pluginMessage.severity = 'error'; pluginMessage.message = message; pluginResult.setSuccess(false); pluginResult.setPluginName(pluginName); pluginResult.setPluginId(pluginId); pluginResult.setProjectId(projectId || 'N/A'); pluginResult.addMessage(pluginMessage); pluginResult.setStartTime((new Date()).toISOString()); pluginResult.setFinishTime((new Date()).toISOString()); pluginResult.setError(pluginMessage.message); return pluginResult; }; function getBranchHash(project, branchName) { if (branchName) { return project.getBranchHash(branchName); } else { return Q(null); } } /** * * @param {object} context * @param {object} context.project - project form where to load the context. * @param {string} [context.branchName] - name of branch that should be updated * @param {string} [context.commitHash=<%branchHash%>] - commit from which to start the plugin. * @param {string} [context.activeNode=''] - path to active node * @param {string[]} [context.activeSelection=[]] - paths to selected nodes. * @param {string} [context.namespace=''] - used namespace during execution ('' represents all namespaces). * @param {object} pluginLogger - logger for the plugin. */ this.loadContext = function (context) { var deferred = Q.defer(), pluginContext = { branchName: context.branchName, commitHash: context.commitHash || context.commit, rootNode: null, activeNode: null, activeSelection: null, META: {}, namespace: context.namespace || '', project: context.project, projectId: context.project.projectId, projectName: storageUtil.getProjectNameFromProjectId(context.project.projectId), core: new Core(context.project, { globConf: gmeConfig, logger: self.logger.fork('core') }) }; self.logger.debug('loading context'); getBranchHash(pluginContext.project, pluginContext.branchName) .then(function (branchHash) { pluginContext.commitHash = context.commitHash || branchHash; if (!pluginContext.commitHash) { throw new Error('Neither commitHash nor branchHash from branch was obtained, branchName: [' + context.branchName + ']'); } return pluginUtil.loadNodesAtCommitHash( pluginContext.project, pluginContext.core, pluginContext.commitHash, self.logger, context); }) .then(function (result) { pluginContext.rootNode = result.rootNode; pluginContext.activeNode = result.activeNode; pluginContext.activeSelection = result.activeSelection; pluginContext.META = result.META; deferred.resolve(pluginContext); }) .catch(function (err) { deferred.reject(err); }); return deferred.promise; }; } return PluginManagerBase; });