File indexing completed on 2024-04-14 04:50:22

0001 /*
0002     This file is part of Choqok, the KDE micro-blogging client
0003 
0004     SPDX-FileCopyrightText: 2008-2012 Mehrdad Momeny <mehrdad.momeny@gmail.com>
0005 
0006     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0007 */
0008 
0009 #include "pluginmanager.h"
0010 
0011 #include <QRegExp>
0012 #include <QTimer>
0013 #include <QStack>
0014 #include <QCoreApplication>
0015 
0016 #include <KConfigGroup>
0017 #include <KSharedConfig>
0018 #include <KPluginMetaData>
0019 #include <KPluginFactory>
0020 
0021 #include "accountmanager.h"
0022 #include "libchoqokdebug.h"
0023 
0024 namespace Choqok
0025 {
0026 
0027 class PluginManagerPrivate
0028 {
0029 public:
0030     PluginManagerPrivate() : shutdownMode(StartingUp), isAllPluginsLoaded(false)
0031     {
0032         plugins = KPluginMetaData::findPlugins(QStringLiteral("choqok_plugins"));
0033     }
0034 
0035     ~PluginManagerPrivate()
0036     {
0037         if (shutdownMode != DoneShutdown) {
0038             qCWarning(CHOQOK) << "Destructing plugin manager without going through the shutdown process!" << endl;
0039         }
0040 
0041         // Clean up loadedPlugins manually, because PluginManager can't access our global
0042         // static once this destructor has started.
0043         for (const KPluginMetaData &p: loadedPlugins.keys()) {
0044             Plugin *plugin = loadedPlugins.value(p);
0045             qCWarning(CHOQOK) << "Deleting stale plugin '" << plugin->pluginId() << "'";
0046             plugin->disconnect(&instance, SLOT(slotPluginDestroyed(QObject*)));
0047             plugin->deleteLater();;
0048             loadedPlugins.remove(p);
0049         }
0050     }
0051 
0052     // All available plugins, regardless of category, and loaded or not
0053     QVector<KPluginMetaData> plugins;
0054 
0055     // Dict of all currently loaded plugins, mapping the KPluginMetaData to
0056     // a plugin
0057     typedef QHash<KPluginMetaData, Plugin *> MetaDataToPluginMap;
0058     MetaDataToPluginMap loadedPlugins;
0059 
0060     // The plugin manager's mode. The mode is StartingUp until loadAllPlugins()
0061     // has finished loading the plugins, after which it is set to Running.
0062     // ShuttingDown and DoneShutdown are used during Choqok shutdown by the
0063     // async unloading of plugins.
0064     enum ShutdownMode { StartingUp, Running, ShuttingDown, DoneShutdown };
0065     ShutdownMode shutdownMode;
0066 
0067     // Plugins pending for loading
0068     QStack<QString> pluginsToLoad;
0069 
0070     bool isAllPluginsLoaded;
0071     PluginManager instance;
0072 };
0073 
0074 Q_GLOBAL_STATIC(PluginManagerPrivate, _kpmp)
0075 
0076 PluginManager *PluginManager::self()
0077 {
0078     return &_kpmp->instance;
0079 }
0080 
0081 PluginManager::PluginManager() : QObject()
0082 {
0083     connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit,
0084             this, &PluginManager::slotAboutToQuit);
0085 }
0086 
0087 PluginManager::~PluginManager()
0088 {
0089 }
0090 
0091 QVector<KPluginMetaData> PluginManager::availablePlugins(const QString &category) const
0092 {
0093     if (category.isEmpty()) {
0094         return _kpmp->plugins;
0095     }
0096 
0097     QVector<KPluginMetaData> result;
0098     for (const KPluginMetaData &p: _kpmp->plugins) {
0099         if (p.category().compare(category) == 0) {
0100             result.append(p);
0101         }
0102     }
0103 
0104     return result;
0105 }
0106 
0107 PluginList PluginManager::loadedPlugins(const QString &category) const
0108 {
0109     PluginList result;
0110 
0111     for (const KPluginMetaData &p: _kpmp->loadedPlugins.keys()) {
0112         if (category.isEmpty() || p.category().compare(category) == 0) {
0113             result.append(_kpmp->loadedPlugins.value(p));
0114         }
0115     }
0116 
0117     return result;
0118 }
0119 
0120 KPluginMetaData PluginManager::pluginMetaData(const Plugin *plugin) const
0121 {
0122     for (const KPluginMetaData &p: _kpmp->loadedPlugins.keys()) {
0123         if (_kpmp->loadedPlugins.value(p) == plugin) {
0124             return p;
0125         }
0126     }
0127 
0128     return KPluginMetaData();
0129 }
0130 
0131 void PluginManager::shutdown()
0132 {
0133     qCDebug(CHOQOK);
0134     if (_kpmp->shutdownMode != PluginManagerPrivate::Running) {
0135         qCDebug(CHOQOK) << "called when not running.  / state =" << _kpmp->shutdownMode;
0136         return;
0137     }
0138 
0139     _kpmp->shutdownMode = PluginManagerPrivate::ShuttingDown;
0140 
0141     // Remove any pending plugins to load, we're shutting down now :)
0142     _kpmp->pluginsToLoad.clear();
0143 
0144     // Ask all plugins to unload
0145     for (PluginManagerPrivate::MetaDataToPluginMap::ConstIterator it = _kpmp->loadedPlugins.constBegin();
0146             it != _kpmp->loadedPlugins.constEnd(); /* EMPTY */) {
0147         // Plugins could emit their ready for unload signal directly in response to this,
0148         // which would invalidate the current iterator. Therefore, we copy the iterator
0149         // and increment it beforehand.
0150         PluginManagerPrivate::MetaDataToPluginMap::ConstIterator current(it);
0151         ++it;
0152         // FIXME: a much cleaner approach would be to just delete the plugin now. if it needs
0153         //  to do some async processing, it can grab a reference to the app itself and create
0154         //  another object to do it.
0155         current.value()->aboutToUnload();
0156     }
0157 
0158     // When running under valgrind, don't enable the timer because it will almost
0159     // certainly fire due to valgrind's much slower processing
0160 #if defined(HAVE_VALGRIND_H) && !defined(NDEBUG) && defined(__i386__)
0161     if (RUNNING_ON_VALGRIND) {
0162         qCDebug(CHOQOK) << "Running under valgrind, disabling plugin unload timeout guard";
0163     } else
0164 #endif
0165         QTimer::singleShot(3000, this, SLOT(slotShutdownTimeout()));
0166 }
0167 
0168 void PluginManager::slotPluginReadyForUnload()
0169 {
0170     qCDebug(CHOQOK);
0171     // Using QObject::sender() is on purpose here, because otherwise all
0172     // plugins would have to pass 'this' as parameter, which makes the API
0173     // less clean for plugin authors
0174     // FIXME: I don't buy the above argument. Add a Choqok::Plugin::emitReadyForUnload(void),
0175     //        and make readyForUnload be passed a plugin. - Richard
0176     Plugin *plugin = dynamic_cast<Plugin *>(const_cast<QObject *>(sender()));
0177     if (!plugin) {
0178         qCWarning(CHOQOK) << "Calling object is not a plugin!";
0179         return;
0180     }
0181     qCDebug(CHOQOK) << plugin->pluginId() << "ready for unload";
0182     _kpmp->loadedPlugins.remove(_kpmp->loadedPlugins.key(plugin));
0183     plugin->deleteLater();
0184     plugin = nullptr;
0185     if (_kpmp->loadedPlugins.count() < 1) {
0186         slotShutdownDone();
0187     }
0188 }
0189 
0190 void PluginManager::slotShutdownTimeout()
0191 {
0192     qCDebug(CHOQOK);
0193     // When we were already done the timer might still fire.
0194     // Do nothing in that case.
0195     if (_kpmp->shutdownMode == PluginManagerPrivate::DoneShutdown) {
0196         return;
0197     }
0198 
0199     QStringList remaining;
0200     for (Plugin *p: _kpmp->loadedPlugins.values()) {
0201         remaining.append(p->pluginId());
0202     }
0203 
0204     qCWarning(CHOQOK) << "Some plugins didn't shutdown in time!" << endl
0205                       << "Remaining plugins:" << remaining << endl
0206                       << "Forcing Choqok shutdown now." << endl;
0207 
0208     slotShutdownDone();
0209 }
0210 
0211 void PluginManager::slotShutdownDone()
0212 {
0213     qCDebug(CHOQOK) ;
0214     _kpmp->shutdownMode = PluginManagerPrivate::DoneShutdown;
0215 }
0216 
0217 void PluginManager::loadAllPlugins()
0218 {
0219     qCDebug(CHOQOK);
0220     KSharedConfig::Ptr config = KSharedConfig::openConfig();
0221     if (config->hasGroup(QLatin1String("Plugins"))) {
0222         QMap<QString, bool> pluginsMap;
0223 
0224         const QMap<QString, QString> entries = config->entryMap(QLatin1String("Plugins"));
0225         for (const QString &key: entries.keys()) {
0226             if (key.endsWith(QLatin1String("Enabled"))) {
0227                 pluginsMap.insert(key.left(key.length() - 7), (entries.value(key).compare(QLatin1String("true")) == 0));
0228             }
0229         }
0230 
0231         for (const KPluginMetaData &p: availablePlugins(QString())) {
0232             if ((p.category().compare(QLatin1String("MicroBlogs")) == 0) ||
0233                     (p.category().compare(QLatin1String("Shorteners")) == 0))
0234             {
0235                 continue;
0236             }
0237 
0238             const QString pluginName = p.pluginId();
0239             if (pluginsMap.value(pluginName, p.isEnabledByDefault())) {
0240                 if (!plugin(pluginName)) {
0241                     _kpmp->pluginsToLoad.push(pluginName);
0242                 }
0243             } else {
0244                 //This happens if the user unloaded plugins with the config plugin page.
0245                 // No real need to be assync because the user usually unload few plugins
0246                 // compared tto the number of plugin to load in a cold start. - Olivier
0247                 if (plugin(pluginName)) {
0248                     unloadPlugin(pluginName);
0249                 }
0250             }
0251         }
0252     } else {
0253         // we had no config, so we load any plugins that should be loaded by default.
0254         for (const KPluginMetaData &p: availablePlugins(QString())) {
0255             if ((p.category().compare(QLatin1String("MicroBlogs")) == 0) ||
0256                     (p.category().compare(QLatin1String("Shorteners")) == 0))
0257             {
0258                 continue;
0259             }
0260 
0261             if (p.isEnabledByDefault()) {
0262                 _kpmp->pluginsToLoad.push(p.pluginId());
0263             }
0264         }
0265     }
0266     // Schedule the plugins to load
0267     QTimer::singleShot(0, this, SLOT(slotLoadNextPlugin()));
0268 }
0269 
0270 void PluginManager::slotLoadNextPlugin()
0271 {
0272     qCDebug(CHOQOK);
0273     if (_kpmp->pluginsToLoad.isEmpty()) {
0274         if (_kpmp->shutdownMode == PluginManagerPrivate::StartingUp) {
0275             _kpmp->shutdownMode = PluginManagerPrivate::Running;
0276             _kpmp->isAllPluginsLoaded = true;
0277             qCDebug(CHOQOK) << "All plugins loaded...";
0278             Q_EMIT allPluginsLoaded();
0279         }
0280         return;
0281     }
0282 
0283     QString key = _kpmp->pluginsToLoad.pop();
0284     loadPluginInternal(key);
0285 
0286     // Schedule the next run unconditionally to avoid code duplication on the
0287     // allPluginsLoaded() signal's handling. This has the added benefit that
0288     // the signal is delayed one event loop, so the accounts are more likely
0289     // to be instantiated.
0290     QTimer::singleShot(0, this, SLOT(slotLoadNextPlugin()));
0291 }
0292 
0293 Plugin *PluginManager::loadPlugin(const QString &_pluginId, PluginLoadMode mode /* = LoadSync */)
0294 {
0295     QString pluginId = _pluginId;
0296 
0297     // Try to find legacy code
0298     // FIXME: Find any cases causing this, remove them, and remove this too - Richard
0299     if (pluginId.endsWith(QLatin1String(".desktop"))) {
0300         qCWarning(CHOQOK) << "Trying to use old-style API!" << endl;
0301         pluginId = pluginId.remove(QRegExp(QLatin1String(".desktop$")));
0302     }
0303 
0304     if (mode == LoadSync) {
0305         return loadPluginInternal(pluginId);
0306     } else {
0307         _kpmp->pluginsToLoad.push(pluginId);
0308         QTimer::singleShot(0, this, SLOT(slotLoadNextPlugin()));
0309         return nullptr;
0310     }
0311 }
0312 
0313 Plugin *PluginManager::loadPluginInternal(const QString &pluginId)
0314 {
0315     qCDebug(CHOQOK) << "Loading Plugin:" << pluginId;
0316 
0317     KPluginMetaData metaData = metaDataForPluginId(pluginId);
0318 
0319     if (_kpmp->loadedPlugins.contains(metaData)) {
0320         return _kpmp->loadedPlugins[ metaData ];
0321     }
0322 
0323     auto pluginMetaData = KPluginMetaData::findPluginById(QStringLiteral("choqok_plugins"), pluginId);
0324     if (!pluginMetaData.isValid()) {
0325         return nullptr;
0326     }
0327 
0328     auto pluginResult = KPluginFactory::instantiatePlugin<Plugin>(pluginMetaData);
0329     if (auto plugin = pluginResult.plugin) {
0330         _kpmp->loadedPlugins.insert(metaData, plugin);
0331 
0332         connect(plugin, &Plugin::destroyed, this, &PluginManager::slotPluginDestroyed);
0333         connect(plugin, &Plugin::readyForUnload, this, &PluginManager::slotPluginReadyForUnload);
0334 
0335         qCDebug(CHOQOK) << "Successfully loaded plugin '" << pluginId << "'";
0336 
0337         if (plugin->pluginMetaData().category() != QLatin1String("MicroBlogs") && plugin->pluginMetaData().category() != QLatin1String("Shorteners")) {
0338             qCDebug(CHOQOK) << "Emitting pluginLoaded()";
0339             Q_EMIT pluginLoaded(plugin);
0340         }
0341         return plugin;
0342     } else {
0343         qCDebug(CHOQOK) << "Loading plugin" << pluginId << "failed:" << pluginResult.errorString << pluginResult.errorText;
0344     }
0345     return nullptr;
0346 }
0347 
0348 bool PluginManager::unloadPlugin(const QString &spec)
0349 {
0350     qCDebug(CHOQOK) << spec;
0351     if (Plugin *thePlugin = plugin(spec)) {
0352         qCDebug(CHOQOK) << "Unloading" << spec;
0353         thePlugin->aboutToUnload();
0354         return true;
0355     } else {
0356         return false;
0357     }
0358 }
0359 
0360 void PluginManager::slotPluginDestroyed(QObject *plugin)
0361 {
0362     qCDebug(CHOQOK);
0363     for (const KPluginMetaData &p: _kpmp->loadedPlugins.keys()) {
0364         if (_kpmp->loadedPlugins.value(p) == plugin) {
0365             const QString pluginName = p.name();
0366             _kpmp->loadedPlugins.remove(p);
0367             Q_EMIT pluginUnloaded(pluginName);
0368             break;
0369         }
0370     }
0371 
0372     if (_kpmp->shutdownMode == PluginManagerPrivate::ShuttingDown && _kpmp->loadedPlugins.isEmpty()) {
0373         // Use a timer to make sure any pending deleteLater() calls have
0374         // been handled first
0375         QTimer::singleShot(0, this, SLOT(slotShutdownDone()));
0376     }
0377 }
0378 
0379 Plugin *PluginManager::plugin(const QString &_pluginId) const
0380 {
0381     // Hack for compatibility with Plugin::pluginId(), which returns
0382     // classname() instead of the internal name. Changing that is not easy
0383     // as it invalidates the config file, the contact list, and most likely
0384     // other code as well.
0385     // For now, just transform FooProtocol to choqok_foo.
0386     // FIXME: In the future we'll need to change this nevertheless to unify
0387     //        the handling - Martijn
0388     QString pluginId = _pluginId;
0389     if (pluginId.endsWith(QLatin1String("Protocol"))) {
0390         pluginId = QLatin1String("choqok_") + _pluginId.toLower().remove(QLatin1String("protocol"));
0391     }
0392     // End hack
0393 
0394     KPluginMetaData metaData = metaDataForPluginId(pluginId);
0395     if (!metaData.isValid()) {
0396         return nullptr;
0397     }
0398 
0399     if (_kpmp->loadedPlugins.contains(metaData)) {
0400         return _kpmp->loadedPlugins[ metaData ];
0401     } else {
0402         return nullptr;
0403     }
0404 }
0405 
0406 KPluginMetaData PluginManager::metaDataForPluginId(const QString &pluginId) const
0407 {
0408     for (const KPluginMetaData &p: _kpmp->plugins) {
0409         if (p.pluginId().compare(pluginId) == 0) {
0410             return p;
0411         }
0412     }
0413 
0414     return KPluginMetaData();
0415 }
0416 
0417 bool PluginManager::setPluginEnabled(const QString &_pluginId, bool enabled /* = true */)
0418 {
0419     QString pluginId = _pluginId;
0420 
0421     KConfigGroup config(KSharedConfig::openConfig(), "Plugins");
0422 
0423     // FIXME: What is this for? This sort of thing is kconf_update's job - Richard
0424     if (!pluginId.startsWith(QLatin1String("choqok_"))) {
0425         pluginId.prepend(QLatin1String("choqok_"));
0426     }
0427 
0428     if (!metaDataForPluginId(pluginId).isValid()) {
0429         return false;
0430     }
0431 
0432     config.writeEntry(pluginId + QLatin1String("Enabled"), enabled);
0433     config.sync();
0434 
0435     return true;
0436 }
0437 
0438 bool PluginManager::isAllPluginsLoaded() const
0439 {
0440     return _kpmp->isAllPluginsLoaded;
0441 }
0442 
0443 void PluginManager::slotAboutToQuit()
0444 {
0445     shutdown();
0446 }
0447 
0448 } //END namespace Choqok
0449 
0450 #include "moc_pluginmanager.cpp"