#include "plugincontainer.h" #include "organizercore.h" #include "organizerproxy.h" #include "report.h" #include #include "iuserinterface.h" #include #include "shared/appconfig.h" #include #include #include #include #include #include #include #include #include using namespace MOBase; using namespace MOShared; namespace bf = boost::fusion; template struct PluginTypeName; template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Plugin"); } }; template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Diagnose"); } }; template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Game"); } }; template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Installer"); } }; template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Mod Page"); } }; template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Preview"); } }; template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Tool"); } }; template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Proxy"); } }; template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("File Mapper"); } }; QStringList PluginContainer::pluginInterfaces() { // Find all the names: QStringList names; boost::mp11::mp_for_each([&names](const auto* p) { using plugin_type = std::decay_t; auto name = PluginTypeName::value(); if (!name.isEmpty()) { names.append(name); } }); return names; } // PluginRequirementProxy PluginRequirements::PluginRequirements( PluginContainer* pluginContainer, MOBase::IPlugin* plugin, IOrganizer* proxy, MOBase::IPluginProxy* pluginProxy) : m_PluginContainer(pluginContainer) , m_Plugin(plugin) , m_PluginProxy(pluginProxy) , m_Master(nullptr) , m_Organizer(proxy) { // There are a lots of things we cannot set here (e.g. m_Master) because we do not // know the order plugins are loaded. } void PluginRequirements::fetchRequirements() { for (auto* requirement : m_Plugin->requirements()) { m_Requirements.emplace_back(requirement); } } IPluginProxy* PluginRequirements::proxy() const { return m_PluginProxy; } std::vector PluginRequirements::proxied() const { std::vector children; if (dynamic_cast(m_Plugin)) { for (auto* obj : m_PluginContainer->plugins()) { auto* plugin = qobject_cast(obj); if (plugin && m_PluginContainer->requirements(plugin).proxy() == m_Plugin) { children.push_back(plugin); } } } return children; } IPlugin* PluginRequirements::master() const { // If we have a m_Master, it was forced and thus override the default master(). if (m_Master) { return m_Master; } if (m_Plugin->master().isEmpty()) { return nullptr; } return m_PluginContainer->plugin(m_Plugin->master()); } void PluginRequirements::setMaster(IPlugin* master) { m_Master = master; } std::vector PluginRequirements::children() const { std::vector children; for (auto* obj : m_PluginContainer->plugins()) { auto* plugin = qobject_cast(obj); // Not checking master() but requirements().master() due to "hidden" // masters. // If the master has the same name as the plugin, this is a "hidden" // master, we do not had it here. if (plugin && m_PluginContainer->requirements(plugin).master() == m_Plugin && plugin->name() != m_Plugin->name()) { children.push_back(plugin); } } return children; } std::vector PluginRequirements::problems() const { std::vector result; for (auto& requirement : m_Requirements) { if (auto p = requirement->check(m_Organizer)) { result.push_back(*p); } } return result; } bool PluginRequirements::canEnable() const { return problems().empty(); } bool PluginRequirements::hasRequirements() const { return !m_Requirements.empty(); } QStringList PluginRequirements::requiredGames() const { // We look for a "GameDependencyRequirement" - There can be only one since otherwise // it'd mean that the plugin requires two games at once. for (auto& requirement : m_Requirements) { if (auto* gdep = dynamic_cast(requirement.get())) { return gdep->gameNames(); } } return {}; } std::vector PluginRequirements::requiredFor() const { std::vector required; std::set visited; requiredFor(required, visited); return required; } void PluginRequirements::requiredFor(std::vector &required, std::set& visited) const { // Handle cyclic dependencies. if (visited.contains(m_Plugin)) { return; } visited.insert(m_Plugin); for (auto& [plugin, requirements] : m_PluginContainer->m_Requirements) { // If the plugin is not enabled, discard: if (!m_PluginContainer->isEnabled(plugin)) { continue; } // Check the requirements: for (auto& requirement : requirements.m_Requirements) { // We check for plugin dependency. Game dependency are not checked this way. if (auto* pdep = dynamic_cast(requirement.get())) { // Check if at least one of the plugin in the requirements is enabled (except this // one): bool oneEnabled = false; for (auto& pluginName : pdep->pluginNames()) { if (pluginName != m_Plugin->name() && m_PluginContainer->isEnabled(pluginName)) { oneEnabled = true; break; } } // No plugin enabled found, so the plugin requires this plugin: if (!oneEnabled) { required.push_back(plugin); requirements.requiredFor(required, visited); break; } } } } } // PluginContainer PluginContainer::PluginContainer(OrganizerCore *organizer) : m_Organizer(organizer) , m_UserInterface(nullptr) , m_PreviewGenerator(this) { } PluginContainer::~PluginContainer() { m_Organizer = nullptr; unloadPlugins(); } void PluginContainer::setUserInterface(IUserInterface *userInterface, QWidget *widget) { for (IPluginProxy *proxy : bf::at_key(m_Plugins)) { proxy->setParentWidget(widget); } if (userInterface != nullptr) { for (IPluginModPage *modPage : bf::at_key(m_Plugins)) { userInterface->registerModPage(modPage); } } m_UserInterface = userInterface; } QStringList PluginContainer::implementedInterfaces(IPlugin* plugin) const { // We need a QObject to be able to qobject_cast<> to the plugin types: QObject* oPlugin = as_qobject(plugin); if (!oPlugin) { return {}; } return implementedInterfaces(oPlugin); } QStringList PluginContainer::implementedInterfaces(QObject * oPlugin) const { // Find all the names: QStringList names; boost::mp11::mp_for_each([oPlugin, &names](const auto* p) { using plugin_type = std::decay_t; if (qobject_cast(oPlugin)) { auto name = PluginTypeName::value(); if (!name.isEmpty()) { names.append(name); } } }); // If the plugin implements at least one interface other than IPlugin, remove IPlugin: if (names.size() > 1) { names.removeAll(PluginTypeName::value()); } return names; } QString PluginContainer::topImplementedInterface(IPlugin* plugin) const { auto interfaces = implementedInterfaces(plugin); return interfaces.isEmpty() ? "" : interfaces[0]; } bool PluginContainer::isBetterInterface(QObject* lhs, QObject* rhs) const { int count = 0, lhsIdx = -1, rhsIdx = -1; boost::mp11::mp_for_each([&](const auto* p) { using plugin_type = std::decay_t; if (lhsIdx < 0 && qobject_cast(lhs)) { lhsIdx = count; } if (rhsIdx < 0 && qobject_cast(rhs)) { rhsIdx = count; } ++count; }); return lhsIdx < rhsIdx; } QStringList PluginContainer::pluginFileNames() const { QStringList result; for (QPluginLoader *loader : m_PluginLoaders) { result.append(loader->fileName()); } std::vector proxyList = bf::at_key(m_Plugins); for (IPluginProxy *proxy : proxyList) { QStringList proxiedPlugins = proxy->pluginList( QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath())); result.append(proxiedPlugins); } return result; } QObject* PluginContainer::as_qobject(MOBase::IPlugin* plugin) const { // Find the correspond QObject - Can this be done safely with a cast? auto& objects = bf::at_key(m_Plugins); auto it = std::find_if(std::begin(objects), std::end(objects), [plugin](QObject* obj) { return qobject_cast(obj) == plugin; }); if (it == std::end(objects)) { return nullptr; } return *it; } bool PluginContainer::initPlugin(IPlugin *plugin, IPluginProxy *pluginProxy) { // when MO has no instance loaded, init() is not called on plugins, except // for proxy plugins, where init() is called with a null IOrganizer // // after proxies are initialized, instantiate() is called for all the plugins // they've discovered, but as for regular plugins, init() won't be // called on them if m_OrganizerCore is null if (plugin == nullptr) { return false; } OrganizerProxy* proxy = nullptr; if (m_Organizer) { proxy = new OrganizerProxy(m_Organizer, this, plugin); } // Check if it is a proxy plugin: bool isProxy = dynamic_cast(plugin); if (!m_Organizer && !isProxy) { return true; } auto [it, bl] = m_Requirements.emplace(plugin, PluginRequirements(this, plugin, proxy, pluginProxy)); if (!plugin->init(proxy)) { log::warn("plugin failed to initialize"); return false; } // Update requirements: it->second.fetchRequirements(); return true; } void PluginContainer::registerGame(IPluginGame *game) { m_SupportedGames.insert({ game->gameName(), game }); } IPlugin* PluginContainer::registerPlugin(QObject *plugin, const QString &fileName, MOBase::IPluginProxy* pluginProxy) { // generic treatment for all plugins IPlugin *pluginObj = qobject_cast(plugin); if (pluginObj == nullptr) { log::debug("PluginContainer::registerPlugin() called with a non IPlugin QObject."); return nullptr; } // If we already a plugin with this name: auto& mapNames = bf::at_key(m_AccessPlugins); if (mapNames.contains(pluginObj->name())) { IPlugin* other = mapNames[pluginObj->name()]; // If both plugins are from the same proxy and the same file, this is usually // ok (in theory some one could write two different classes from the same Python file/module): if (pluginProxy && m_Requirements.at(other).proxy() == pluginProxy && as_qobject(other)->property("filename") == fileName) { if (isBetterInterface(plugin, as_qobject(other))) { log::debug("replacing plugin '{}' with interfaces [{}] by one with interfaces [{}]", pluginObj->name(), implementedInterfaces(other).join(", "), implementedInterfaces(plugin).join(", ")); bf::at_key(m_AccessPlugins)[pluginObj->name()] = pluginObj; } } else { log::warn("Trying to register two plugins with the name '{}', the second one will not be registered.", pluginObj->name()); } } else { bf::at_key(m_AccessPlugins)[pluginObj->name()] = pluginObj; } // Storing the original QObject* is a bit of a hack as I couldn't figure out any // way to cast directly between IPlugin* and IPluginDiagnose* bf::at_key(m_Plugins).push_back(plugin); plugin->setProperty("filename", fileName); if (m_Organizer) { m_Organizer->settings().plugins().registerPlugin(pluginObj); } { // diagnosis plugin IPluginDiagnose *diagnose = qobject_cast(plugin); if (diagnose != nullptr) { bf::at_key(m_Plugins).push_back(diagnose); bf::at_key(m_AccessPlugins)[diagnose] = pluginObj; m_DiagnosisConnections.push_back( diagnose->onInvalidated([&] () { emit diagnosisUpdate(); }) ); } } { // file mapper plugin IPluginFileMapper *mapper = qobject_cast(plugin); if (mapper != nullptr) { bf::at_key(m_Plugins).push_back(mapper); bf::at_key(m_AccessPlugins)[mapper] = pluginObj; } } { // mod page plugin IPluginModPage *modPage = qobject_cast(plugin); if (initPlugin(modPage, pluginProxy)) { bf::at_key(m_Plugins).push_back(modPage); return modPage; } } { // game plugin IPluginGame *game = qobject_cast(plugin); if (game) { game->detectGame(); if (initPlugin(game, pluginProxy)) { bf::at_key(m_Plugins).push_back(game); registerGame(game); return game; } } } { // tool plugins IPluginTool *tool = qobject_cast(plugin); if (initPlugin(tool, pluginProxy)) { bf::at_key(m_Plugins).push_back(tool); return tool; } } { // installer plugins IPluginInstaller *installer = qobject_cast(plugin); if (initPlugin(installer, pluginProxy)) { bf::at_key(m_Plugins).push_back(installer); if (m_Organizer) { m_Organizer->installationManager()->registerInstaller(installer); } return installer; } } { // preview plugins IPluginPreview *preview = qobject_cast(plugin); if (initPlugin(preview, pluginProxy)) { bf::at_key(m_Plugins).push_back(preview); m_PreviewGenerator.registerPlugin(preview); return preview; } } { // proxy plugins IPluginProxy *proxy = qobject_cast(plugin); if (initPlugin(proxy, pluginProxy)) { bf::at_key(m_Plugins).push_back(proxy); QStringList pluginNames = proxy->pluginList( QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath())); for (const QString &pluginName : pluginNames) { try { // We get a list of matching plugins as proxies can return multiple plugins // per file and do not have a good way of supporting multiple inheritance. QList matchingPlugins = proxy->instantiate(pluginName); // We are going to group plugin by names and "fix" them later: std::map> proxiedByNames; for (QObject *proxiedPlugin : matchingPlugins) { if (proxiedPlugin != nullptr) { if (IPlugin* proxied = registerPlugin(proxiedPlugin, pluginName, proxy); proxied) { log::debug("loaded plugin '{}' from '{}' - [{}]", proxied->name(), QFileInfo(pluginName).fileName(), implementedInterfaces(proxied).join(", ")); // Store the plugin for later: proxiedByNames[proxied->name()].push_back(proxied); } else { log::warn( "plugin \"{}\" failed to load. If this plugin is for an older version of MO " "you have to update it or delete it if no update exists.", pluginName); } } } // Fake masters: for (auto& [name, proxiedPlugins] : proxiedByNames) { if (proxiedPlugins.size() > 1) { auto it = std::min_element(std::begin(proxiedPlugins), std::end(proxiedPlugins), [&](auto const& lhs, auto const& rhs) { return isBetterInterface(as_qobject(lhs), as_qobject(rhs)); }); for (auto& proxiedPlugin : proxiedPlugins) { if (proxiedPlugin != *it) { m_Requirements.at(proxiedPlugin).setMaster(*it); } } } } } catch (const std::exception &e) { reportError(QObject::tr("failed to initialize plugin %1: %2").arg(pluginName).arg(e.what())); } } return proxy; } } { // dummy plugins // only initialize these, no processing otherwise IPlugin *dummy = qobject_cast(plugin); if (initPlugin(dummy, pluginProxy)) { bf::at_key(m_Plugins).push_back(dummy); return dummy; } } log::debug("no matching plugin interface"); return nullptr; } void PluginContainer::unloadPlugins() { if (m_UserInterface != nullptr) { m_UserInterface->disconnectPlugins(); } // disconnect all slots before unloading plugins so plugins don't have to take care of that if (m_Organizer) { m_Organizer->disconnectPlugins(); } bf::for_each(m_Plugins, [](auto& t) { t.second.clear(); }); for (const boost::signals2::connection &connection : m_DiagnosisConnections) { connection.disconnect(); } m_DiagnosisConnections.clear(); while (!m_PluginLoaders.empty()) { QPluginLoader *loader = m_PluginLoaders.back(); m_PluginLoaders.pop_back(); if ((loader != nullptr) && !loader->unload()) { log::debug("failed to unload {}: {}", loader->fileName(), loader->errorString()); } delete loader; } } IPlugin* PluginContainer::managedGame() const { // TODO: This const_cast is safe but ugly. Most methods require a IPlugin*, so // returning a const-version if painful. This should be fixed by making methods accept // a const IPlugin* instead, but there are a few tricks with qobject_cast and const. return const_cast(m_Organizer->managedGame()); } bool PluginContainer::isEnabled(IPlugin* plugin) const { // Check if it's a game plugin: if (implementInterface(plugin)) { return plugin == m_Organizer->managedGame(); } // Check the master, if any: auto& requirements = m_Requirements.at(plugin); if (requirements.master()) { return isEnabled(requirements.master()); } // Check if the plugin is enabled: if (!m_Organizer->persistent(plugin->name(), "enabled", true).toBool()) { return false; } // Check the requirements: return m_Requirements.at(plugin).canEnable(); } void PluginContainer::setEnabled(MOBase::IPlugin* plugin, bool enable, bool dependencies) { // If required, disable dependencies: if (!enable && dependencies) { for (auto* p : requirements(plugin).requiredFor()) { setEnabled(p, false, false); // No need to "recurse" here since requiredFor already does it. } } // Always disable/enable child plugins: for (auto* p : requirements(plugin).children()) { // "Child" plugin should have no dependencies. setEnabled(p, enable, false); } m_Organizer->setPersistent(plugin->name(), "enabled", enable, true); if (enable) { emit pluginEnabled(plugin); } else { emit pluginDisabled(plugin); } } MOBase::IPlugin* PluginContainer::plugin(QString const& pluginName) const { auto& map = bf::at_key(m_AccessPlugins); auto it = map.find(pluginName); if (it == std::end(map)) { return nullptr; } return it->second; } MOBase::IPlugin* PluginContainer::plugin(MOBase::IPluginDiagnose* diagnose) const { auto& map = bf::at_key(m_AccessPlugins); auto it = map.find(diagnose); if (it == std::end(map)) { return nullptr; } return it->second; } MOBase::IPlugin* PluginContainer::plugin(MOBase::IPluginFileMapper* mapper) const { auto& map = bf::at_key(m_AccessPlugins); auto it = map.find(mapper); if (it == std::end(map)) { return nullptr; } return it->second; } bool PluginContainer::isEnabled(QString const& pluginName) const { IPlugin* p = plugin(pluginName); return p ? isEnabled(p) : false; } bool PluginContainer::isEnabled(MOBase::IPluginDiagnose* diagnose) const { IPlugin* p = plugin(diagnose); return p ? isEnabled(p) : false; } bool PluginContainer::isEnabled(MOBase::IPluginFileMapper* mapper) const { IPlugin* p = plugin(mapper); return p ? isEnabled(p) : false; } const PluginRequirements& PluginContainer::requirements(IPlugin* plugin) const { return m_Requirements.at(plugin); } IPluginGame *PluginContainer::managedGame(const QString &name) const { auto iter = m_SupportedGames.find(name); if (iter != m_SupportedGames.end()) { return iter->second; } else { return nullptr; } } const PreviewGenerator &PluginContainer::previewGenerator() const { return m_PreviewGenerator; } void PluginContainer::loadPlugins() { TimeThis tt("PluginContainer::loadPlugins()"); unloadPlugins(); for (QObject *plugin : QPluginLoader::staticInstances()) { registerPlugin(plugin, "", nullptr); } QFile loadCheck; QString skipPlugin; if (m_Organizer) { loadCheck.setFileName(qApp->property("dataPath").toString() + "/plugin_loadcheck.tmp"); if (loadCheck.exists() && loadCheck.open(QIODevice::ReadOnly)) { // oh, there was a failed plugin load last time. Find out which plugin was loaded last QString fileName; while (!loadCheck.atEnd()) { fileName = QString::fromUtf8(loadCheck.readLine().constData()).trimmed(); } log::warn("loadcheck file found for plugin '{}'", fileName); MOBase::TaskDialog dlg; const auto Skip = QMessageBox::Ignore; const auto Blacklist = QMessageBox::Cancel; const auto Load = QMessageBox::Ok; const auto r = dlg .title(tr("Plugin error")) .main(tr( "Mod Organizer failed to load the plugin '%1' last time it was started.") .arg(fileName)) .content(tr( "The plugin can be skipped for this session, blacklisted, " "or loaded normally, in which case it might fail again. Blacklisted " "plugins can be re-enabled later in the settings.")) .icon(QMessageBox::Warning) .button({tr("Skip this plugin"), Skip}) .button({tr("Blacklist this plugin"), Blacklist}) .button({tr("Load this plugin"), Load}) .exec(); switch (r) { case Skip: log::warn("user wants to skip plugin '{}'", fileName); skipPlugin = fileName; break; case Blacklist: log::warn("user wants to blacklist plugin '{}'", fileName); m_Organizer->settings().plugins().addBlacklist(fileName); break; case Load: log::warn("user wants to load plugin '{}' anyway", fileName); break; } loadCheck.close(); } loadCheck.open(QIODevice::WriteOnly); } QString pluginPath = qApp->applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()); log::debug("looking for plugins in {}", QDir::toNativeSeparators(pluginPath)); QDirIterator iter(pluginPath, QDir::Files | QDir::NoDotAndDotDot); while (iter.hasNext()) { iter.next(); if (skipPlugin == iter.fileName()) { log::debug("plugin \"{}\" skipped for this session", iter.fileName()); continue; } if (m_Organizer) { if (m_Organizer->settings().plugins().blacklisted(iter.fileName())) { log::debug("plugin \"{}\" blacklisted", iter.fileName()); continue; } } if (loadCheck.isOpen()) { loadCheck.write(iter.fileName().toUtf8()); loadCheck.write("\n"); loadCheck.flush(); } QString pluginName = iter.filePath(); if (QLibrary::isLibrary(pluginName)) { std::unique_ptr pluginLoader(new QPluginLoader(pluginName, this)); if (pluginLoader->instance() == nullptr) { m_FailedPlugins.push_back(pluginName); log::error( "failed to load plugin {}: {}", pluginName, pluginLoader->errorString()); } else { if (IPlugin* plugin = registerPlugin(pluginLoader->instance(), pluginName, nullptr); plugin) { log::debug("loaded plugin '{}' from '{}' - [{}]", plugin->name(), QFileInfo(pluginName).fileName(), implementedInterfaces(plugin).join(", ")); m_PluginLoaders.push_back(pluginLoader.release()); } else { m_FailedPlugins.push_back(pluginName); log::warn("plugin \"{}\" failed to load (may be outdated)", pluginName); } } } } if (skipPlugin.isEmpty()) { // remove the load check file on success if (loadCheck.isOpen()) { loadCheck.remove(); } } else { // remember the plugin for next time if (loadCheck.isOpen()) { loadCheck.close(); } log::warn("user skipped plugin '{}', remembering in loadcheck", skipPlugin); loadCheck.open(QIODevice::WriteOnly); loadCheck.write(skipPlugin.toUtf8()); loadCheck.write("\n"); loadCheck.flush(); } bf::at_key(m_Plugins).push_back(this); if (m_Organizer) { bf::at_key(m_Plugins).push_back(m_Organizer); m_Organizer->connectPlugins(this); } } std::vector PluginContainer::activeProblems() const { std::vector problems; if (m_FailedPlugins.size()) { problems.push_back(PROBLEM_PLUGINSNOTLOADED); } return problems; } QString PluginContainer::shortDescription(unsigned int key) const { switch (key) { case PROBLEM_PLUGINSNOTLOADED: { return tr("Some plugins could not be loaded"); } break; default: { return tr("Description missing"); } break; } } QString PluginContainer::fullDescription(unsigned int key) const { switch (key) { case PROBLEM_PLUGINSNOTLOADED: { QString result = tr("The following plugins could not be loaded. The reason may be missing " "dependencies (i.e. python) or an outdated version:") + "
    "; for (const QString &plugin : m_FailedPlugins) { result += "
  • " + plugin + "
  • "; } result += "
      "; return result; } break; default: { return tr("Description missing"); } break; } } bool PluginContainer::hasGuidedFix(unsigned int) const { return false; } void PluginContainer::startGuidedFix(unsigned int) const { }