/* This file is part of the KDE project Copyright 2004, 2007 Alexander Dymo Copyright 2006 Matt Rogers Based on code from Kopete Copyright (c) 2002-2003 Martijn Klingens This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "plugincontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "mainwindow.h" #include "core.h" #include "shellextension.h" #include "runcontroller.h" #include "debugcontroller.h" #include "documentationcontroller.h" #include "sourceformattercontroller.h" #include "projectcontroller.h" namespace { // TODO kf5: use QStringLiteral const QString KEY_Plugins = "Plugins"; const QString KEY_LoadMode = "X-KDevelop-LoadMode"; const QString KEY_Category = "X-KDevelop-Category"; const QString KEY_Mode = "X-KDevelop-Mode"; const QString KEY_Version = "X-KDevelop-Version"; const QString KEY_Interfaces = "X-KDevelop-Interfaces"; const QString KEY_Required = "X-KDevelop-IRequired"; const QString KEY_Optional = "X-KDevelop-IOptional"; const QString KEY_Global = "Global"; const QString KEY_Project = "Project"; const QString KEY_Gui = "GUI"; const QString KEY_AlwaysOn = "AlwaysOn"; const QString KEY_UserSelectable = "UserSelectable"; bool isUserSelectable( const KPluginInfo& info ) { QString loadMode = info.property( KEY_LoadMode ).toString(); return loadMode.isEmpty() || loadMode == KEY_UserSelectable; } bool isGlobalPlugin( const KPluginInfo& info ) { return info.property( KEY_Category ).toString() == KEY_Global; } bool hasMandatoryProperties( const KPluginInfo& info ) { QVariant mode = info.property( KEY_Mode ); QVariant version = info.property( KEY_Version ); return mode.isValid() && mode.canConvert( QVariant::String ) && version.isValid() && version.canConvert( QVariant::String ); } bool constraintsMatch( const KPluginInfo& info, const QVariantMap& constraints) { for (auto it = constraints.begin(); it != constraints.end(); ++it) { const auto property = info.property(it.key()); if (!property.isValid()) { return false; } else if (property.canConvert()) { QSet values = property.toStringList().toSet(); QSet expected = it.value().toStringList().toSet(); if (!values.contains(expected)) { return false; } } else if (it.value() != property) { return false; } } return true; } struct Dependency { Dependency(const QString &dependency) : interface(dependency) { if (dependency.contains('@')) { const auto list = dependency.split('@', QString::SkipEmptyParts); if (list.size() == 2) { interface = list.at(0); pluginName = list.at(1); } } } QString interface; QString pluginName; }; } namespace KDevelop { class PluginControllerPrivate { public: QList plugins; //map plugin infos to currently loaded plugins typedef QMap InfoToPluginMap; InfoToPluginMap loadedPlugins; // The plugin manager's mode. The mode is StartingUp until loadAllPlugins() // has finished loading the plugins, after which it is set to Running. // ShuttingDown and DoneShutdown are used during shutdown by the // async unloading of plugins. enum CleanupMode { Running /**< the plugin manager is running */, CleaningUp /**< the plugin manager is cleaning up for shutdown */, CleanupDone /**< the plugin manager has finished cleaning up */ }; CleanupMode cleanupMode; bool canUnload( const KPluginInfo& plugin ) { kDebug() << "checking can unload for:" << plugin.name() << plugin.property(KEY_LoadMode); if( plugin.property( KEY_LoadMode ).toString() == KEY_AlwaysOn ) { return false; } QStringList interfaces = plugin.property( KEY_Interfaces ).toStringList(); kDebug() << "checking dependencies:" << interfaces; foreach( const KPluginInfo& info, loadedPlugins.keys() ) { if( info.pluginName() != plugin.pluginName() ) { QStringList dependencies = info.property( KEY_Required ).toStringList(); dependencies += info.property( KEY_Optional ).toStringList(); foreach( const QString& dep, dependencies ) { Dependency dependency(dep); if (!dependency.pluginName.isEmpty() && dependency.pluginName != plugin.pluginName()) { continue; } if (interfaces.contains(dependency.interface) && !canUnload(info)) { return false; } } } } return true; } KPluginInfo infoForId( const QString& id ) const { foreach( const KPluginInfo& info, plugins ) { if( info.pluginName() == id ) { return info; } } return KPluginInfo(); } /** * Iterate over all cached plugin infos, and call the functor for every enabled plugin. * * If an extension and/or pluginName is given, the functor will only be called for * those plugins matching this information. * * The functor should return false when the iteration can be stopped, and true if it * should be continued. */ template void foreachEnabledPlugin(F func, const QString &extension = {}, const QVariantMap& constraints = {}, const QString &pluginName = {}) { foreach (const auto& info, plugins) { if( !isEnabled(info) ) { continue; } if ((pluginName.isEmpty() || info.pluginName() == pluginName) && (extension.isEmpty() || info.property(KEY_Interfaces).toStringList().contains(extension)) && constraintsMatch(info, constraints)) { if (!func(info)) { break; } } } } bool isEnabled(const KPluginInfo& info) const { static const QStringList disabledPlugins = QString(qgetenv("KDEV_DISABLE_PLUGINS")).split(';'); if (disabledPlugins.contains(info.pluginName())) { return false; } KConfigGroup grp = Core::self()->activeSession()->config()->group( KEY_Plugins ); bool isEnabled = grp.readEntry( info.pluginName()+"Enabled", ShellExtension::getInstance()->defaultPlugins().isEmpty() || ShellExtension::getInstance()->defaultPlugins().contains( info.pluginName() ) ); //kDebug() << "read config:" << isEnabled << "is global plugin:" << isGlobalPlugin( info ) << "default:" << ShellExtension::getInstance()->defaultPlugins().isEmpty() << ShellExtension::getInstance()->defaultPlugins().contains( info.pluginName() ); return !isGlobalPlugin( info ) || !isUserSelectable( info ) || isEnabled; } Core *core; }; PluginController::PluginController(Core *core) : IPluginController(), d(new PluginControllerPrivate) { setObjectName("PluginController"); d->core = core; //kDebug() << "Fetching plugin info which matches:" << QString( "[X-KDevelop-Version] == %1" ).arg(KDEVELOP_PLUGIN_VERSION); d->plugins = KPluginInfo::fromServices( KServiceTypeTrader::self()->query( QLatin1String( "KDevelop/Plugin" ), QString( "[X-KDevelop-Version] == %1" ).arg(KDEVELOP_PLUGIN_VERSION) ) ); d->cleanupMode = PluginControllerPrivate::Running; // Register the KDevelop::IPlugin* metatype so we can properly unload it qRegisterMetaType( "KDevelop::IPlugin*" ); } PluginController::~PluginController() { if ( d->cleanupMode != PluginControllerPrivate::CleanupDone ) { kWarning(9501) << "Destructing plugin controller without going through the shutdown process! Backtrace is: " << endl << kBacktrace() << endl; } delete d; } KPluginInfo PluginController::pluginInfo( const IPlugin* plugin ) const { return d->loadedPlugins.key(const_cast(plugin)); } void PluginController::cleanup() { if(d->cleanupMode != PluginControllerPrivate::Running) { //kDebug() << "called when not running. state =" << d->cleanupMode; return; } d->cleanupMode = PluginControllerPrivate::CleaningUp; // Ask all plugins to unload while ( !d->loadedPlugins.isEmpty() ) { //Let the plugin do some stuff before unloading unloadPlugin(d->loadedPlugins.begin().value(), Now); } d->cleanupMode = PluginControllerPrivate::CleanupDone; } IPlugin* PluginController::loadPlugin( const QString& pluginName ) { return loadPluginInternal( pluginName ); } bool PluginController::isEnabled( const KPluginInfo& info ) { return d->isEnabled(info); } void PluginController::initialize() { QMap pluginMap; if( ShellExtension::getInstance()->defaultPlugins().isEmpty() ) { foreach( const KPluginInfo& pi, d->plugins ) { pluginMap.insert( pi.pluginName(), true ); } } else { // Get the default from the ShellExtension foreach( const QString& s, ShellExtension::getInstance()->defaultPlugins() ) { pluginMap.insert( s, true ); } } KConfigGroup grp = Core::self()->activeSession()->config()->group( KEY_Plugins ); QMap entries = grp.entryMap(); QMap::Iterator it; for ( it = entries.begin(); it != entries.end(); ++it ) { QString key = it.key(); if ( key.endsWith( QLatin1String( "Enabled" ) ) ) { QString pluginid = key.left( key.length() - 7 ); bool defValue; QMap::const_iterator entry = pluginMap.constFind( pluginid ); if( entry != pluginMap.constEnd() ) { defValue = entry.value(); } else { defValue = false; } pluginMap.insert( key.left(key.length() - 7), grp.readEntry(key,defValue) ); } } foreach( const KPluginInfo& pi, d->plugins ) { if( isGlobalPlugin( pi ) ) { QMap::const_iterator it = pluginMap.constFind( pi.pluginName() ); if( it != pluginMap.constEnd() && ( it.value() || !isUserSelectable( pi ) ) ) { // Plugin is mentioned in pluginmap and the value is true, so try to load it loadPluginInternal( pi.pluginName() ); if( !grp.hasKey( pi.pluginName() + "Enabled" ) ) { if( isUserSelectable( pi ) ) { // If plugin isn't listed yet, add it with true now grp.writeEntry( pi.pluginName()+"Enabled", true ); } } else if( grp.hasKey( pi.pluginName() + "Disabled" ) && !isUserSelectable( pi ) ) { // Remove now-obsolete entries grp.deleteEntry( pi.pluginName() + "Disabled" ); } } } } // Synchronize so we're writing out to the file. grp.sync(); } QList PluginController::loadedPlugins() const { return d->loadedPlugins.values(); } bool PluginController::unloadPlugin( const QString & pluginId ) { IPlugin *thePlugin = plugin( pluginId ); bool canUnload = d->canUnload( d->infoForId( pluginId ) ); kDebug() << "Unloading plugin:" << pluginId << "?" << thePlugin << canUnload; if( thePlugin && canUnload ) { return unloadPlugin(thePlugin, Later); } return (canUnload && thePlugin); } bool PluginController::unloadPlugin(IPlugin* plugin, PluginDeletion deletion) { kDebug() << "unloading plugin:" << plugin << pluginInfo( plugin ).name(); emit unloadingPlugin(plugin); plugin->unload(); emit pluginUnloaded(plugin); //Remove the plugin from our list of plugins so we create a new //instance when we're asked for it again. //This is important to do right here, not later when the plugin really //vanishes. For example project re-opening might try to reload the plugin //and then would get the "old" pointer which will be deleted in the next //event loop run and thus causing crashes. for ( PluginControllerPrivate::InfoToPluginMap::Iterator it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); ++it ) { if ( it.value() == plugin ) { d->loadedPlugins.erase( it ); break; } } if (deletion == Later) plugin->deleteLater(); else delete plugin; return true; } KPluginInfo PluginController::infoForPluginId( const QString &pluginId ) const { QList::ConstIterator it; for ( it = d->plugins.constBegin(); it != d->plugins.constEnd(); ++it ) { if ( it->pluginName() == pluginId ) return *it; } return KPluginInfo(); } IPlugin *PluginController::loadPluginInternal( const QString &pluginId ) { KPluginInfo info = infoForPluginId( pluginId ); if ( !info.isValid() ) { kWarning(9501) << "Unable to find a plugin named '" << pluginId << "'!" ; return 0L; } if ( d->loadedPlugins.contains( info ) ) return d->loadedPlugins[ info ]; if( !isEnabled( info ) ) { // Do not load disabled plugins kWarning() << "Not loading plugin named" << pluginId << "because its been disabled!"; return 0; } if( !hasMandatoryProperties( info ) ) { kWarning() << "Unable to load plugin named " << pluginId << "! Doesn't have all mandatory properties set"; return 0; } if( info.property(KEY_Mode) == KEY_Gui && Core::self()->setupFlags() == Core::NoUi ) { kDebug() << "Not loading plugin named" << pluginId << ". Running in No-Ui mode, but the plugin says it needs a GUI"; return 0; } kDebug() << "Attempting to load '" << pluginId << "'"; emit loadingPlugin( info.pluginName() ); IPlugin *plugin = 0; QStringList missingInterfaces; kDebug() << "Checking... " << info.name(); if ( checkForDependencies( info, missingInterfaces ) ) { kDebug() << "Checked... starting to load:" << info.name(); QString failedDependency; if( !loadDependencies( info, failedDependency ) ) { kWarning() << "Could not load a required dependency:" << failedDependency; return 0; } loadOptionalDependencies( info ); KPluginLoader loader(*info.service()); auto factory = loader.factory(); if (factory) { plugin = factory->create(d->core); } else { kWarning() << "Could not obtain factory to load plugin" << pluginId << loader.errorString(); } } if ( plugin ) { if ( plugin->hasError() ) { kWarning() << i18n("Plugin '%1' could not be loaded correctly and was disabled.\nReason: %2.", info.name(), plugin->errorDescription()); info.setPluginEnabled(false); info.save(Core::self()->activeSession()->config()->group(KEY_Plugins)); unloadPlugin(pluginId); return 0; } d->loadedPlugins.insert( info, plugin ); info.setPluginEnabled( true ); kDebug() << "Successfully loaded plugin '" << pluginId << "'"; emit pluginLoaded( plugin ); } else { if( !missingInterfaces.isEmpty() ) { kWarning() << "Can't load plugin '" << pluginId << "' some of its required dependencies could not be fulfilled:" << endl << missingInterfaces.join(",") << endl; } else { kWarning() << "Loading plugin '" << pluginId << "' failed."; } } return plugin; } IPlugin* PluginController::plugin( const QString& pluginId ) { KPluginInfo info = infoForPluginId( pluginId ); if ( !info.isValid() ) return 0L; if ( d->loadedPlugins.contains( info ) ) return d->loadedPlugins[ info ]; else return 0L; } bool PluginController::checkForDependencies( const KPluginInfo& info, QStringList& missing ) const { QVariant prop = info.property( KEY_Required ); if( prop.canConvert() ) { QSet required = prop.toStringList().toSet(); if (!required.isEmpty()) { d->foreachEnabledPlugin([&required] (const KPluginInfo& plugin) -> bool { foreach(const QString& iface, plugin.property( KEY_Interfaces ).toStringList()) { required.remove(iface); required.remove(iface + '@' + plugin.pluginName()); } return !required.isEmpty(); }); } if (!required.isEmpty()) { missing = required.toList(); return false; } } return true; } void PluginController::loadOptionalDependencies( const KPluginInfo& info ) { QVariant prop = info.property( KEY_Optional ); if( prop.canConvert() ) { foreach( const QString &dep, prop.toStringList() ) { Dependency dependency(dep); if (!pluginForExtension(dependency.interface, dependency.pluginName)) { kDebug() << "Couldn't load optional dependency:" << dep << info.pluginName(); } } } } bool PluginController::loadDependencies( const KPluginInfo& info, QString& failedDependency ) { QVariant prop = info.property( KEY_Required ); if( prop.canConvert() ) { foreach( const QString &value, prop.toStringList() ) { Dependency dependency(value); if (!pluginForExtension(dependency.interface, dependency.pluginName)) { failedDependency = value; return false; } } } return true; } IPlugin *PluginController::pluginForExtension(const QString &extension, const QString &pluginName, const QVariantMap& constraints) { //kDebug() << "Finding Plugin for Extension:" << extension << "|" << constraints; IPlugin* plugin = nullptr; d->foreachEnabledPlugin([this, &plugin] (const KPluginInfo& info) -> bool { if( d->loadedPlugins.contains( info ) ) { plugin = d->loadedPlugins[ info ]; } else { plugin = loadPluginInternal( info.pluginName() ); } return !plugin; }, extension, constraints, pluginName); return plugin; } QList PluginController::allPluginsForExtension(const QString &extension, const QVariantMap& constraints) { //kDebug() << "Finding all Plugins for Extension:" << extension << "|" << constraints; QList plugins; d->foreachEnabledPlugin([this, &plugins] (const KPluginInfo& info) -> bool { if( d->loadedPlugins.contains( info ) ) { plugins << d->loadedPlugins[ info ]; } else if (auto plugin = loadPluginInternal( info.pluginName() )) { plugins << plugin; } return true; }, extension, constraints); return plugins; } KPluginInfo::List PluginController::queryExtensionPlugins(const QString& extension, const QVariantMap& constraints) const { KPluginInfo::List plugins; d->foreachEnabledPlugin([&plugins] (const KPluginInfo& info) -> bool { plugins << info; return true; }, extension, constraints); return plugins; } QStringList PluginController::allPluginNames() { QStringList names; Q_FOREACH( const KPluginInfo& info , d->plugins ) { names << info.pluginName(); } return names; } QList PluginController::queryPluginsForContextMenuExtensions( KDevelop::Context* context ) const { QList exts; Q_FOREACH( const KPluginInfo& info, d->loadedPlugins.keys() ) { IPlugin* plug = d->loadedPlugins[info]; exts << plug->contextMenuExtension( context ); } exts << Core::self()->debugControllerInternal()->contextMenuExtension( context ); exts << Core::self()->documentationControllerInternal()->contextMenuExtension( context ); exts << Core::self()->sourceFormatterControllerInternal()->contextMenuExtension( context ); exts << Core::self()->runControllerInternal()->contextMenuExtension( context ); exts << Core::self()->projectControllerInternal()->contextMenuExtension( context ); return exts; } QStringList PluginController::projectPlugins() { QStringList names; Q_FOREACH( const KPluginInfo& info , d->plugins ) { if( info.property(KEY_Category).toString() == KEY_Project ) names << info.pluginName(); } return names; } void PluginController::loadProjectPlugins() { Q_FOREACH( const QString& name, projectPlugins() ) { loadPluginInternal( name ); } } void PluginController::unloadProjectPlugins() { Q_FOREACH( const QString& name, projectPlugins() ) { unloadPlugin( name ); } } QList PluginController::allPluginInfos() const { return d->plugins; } void PluginController::updateLoadedPlugins() { QStringList defaultPlugins = ShellExtension::getInstance()->defaultPlugins(); KConfigGroup grp = Core::self()->activeSession()->config()->group( KEY_Plugins ); foreach( const KPluginInfo& info, d->plugins ) { if( isGlobalPlugin( info ) ) { bool enabled = grp.readEntry( info.pluginName()+"Enabled", ( defaultPlugins.isEmpty() || defaultPlugins.contains( info.pluginName() ) ) ) || !isUserSelectable( info ); if( d->loadedPlugins.contains( info ) && !enabled ) { kDebug() << "unloading" << info.pluginName(); if( !unloadPlugin( info.pluginName() ) ) { grp.writeEntry( info.pluginName()+"Enabled", false ); } } else if( !d->loadedPlugins.contains( info ) && enabled ) { loadPluginInternal( info.pluginName() ); } } } } void PluginController::resetToDefaults() { KSharedConfig::Ptr cfg = Core::self()->activeSession()->config(); cfg->deleteGroup( KEY_Plugins ); cfg->sync(); KConfigGroup grp = cfg->group( KEY_Plugins ); QStringList plugins = ShellExtension::getInstance()->defaultPlugins(); if( plugins.isEmpty() ) { foreach( const KPluginInfo& info, d->plugins ) { plugins << info.pluginName(); } } foreach( const QString& s, plugins ) { grp.writeEntry( s+"Enabled", true ); } grp.sync(); } } #include "moc_plugincontroller.cpp"