File indexing completed on 2024-04-28 12:40:55
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"