Files
qt5-legatus-legacy/Lts/Common/LtsPluginManager/lts_plugin_loader.cpp
T
2026-08-13 18:33:10 +02:00

461 lines
14 KiB
C++
Executable File

/**
* @RDE_HEADER@
* plugin loader helper function
* $Id$
* $HeadURL$
* $Author$
*
* Created by Christian Ehrlicher, SLSE, 2016-07-07
*/
#include "lts_plugin_loader.h"
#include <NGLLogger/NGLLoggerBootstrap.h>
#include <LtsAppDescriptor/lts_app_descriptor.h>
#include <QCoreApplication>
#include <QDirIterator>
#include <QFileInfo>
#include <QJsonObject>
#include <QPluginLoader>
#include <QMutableListIterator>
#include <QDebug>
namespace LtsPluginManager
{
struct LtsPluginLoaderHandler
{
LtsPluginLoader ltsPluginLoader;
static NGL::INGLLogger &logger() { return NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger("LtsPluginLoader"); }
static void cleanupPluginLoader();
};
Q_GLOBAL_STATIC(LtsPluginLoaderHandler, s_ltsPluginLoader)
void LtsPluginLoaderHandler::cleanupPluginLoader()
{
if (s_ltsPluginLoader.exists())
s_ltsPluginLoader->ltsPluginLoader.unloadAllPlugins();
}
LtsPluginLoader *LtsPluginLoader::getInstance()
{
if (!s_ltsPluginLoader.exists())
qAddPostRoutine(&LtsPluginLoaderHandler::cleanupPluginLoader);
return &s_ltsPluginLoader->ltsPluginLoader;
}
LtsPluginLoader::~LtsPluginLoader()
{
unloadAllPlugins();
}
void LtsPluginLoader::unloadAllPlugins()
{
for (auto &pi : qAsConst(m_loadedPlugins))
{
QPluginLoader *pluginLoader = pi.pluginLoader;
delete pluginLoader->instance();
if (!pluginLoader->unload())
qWarning() << "~LtsPluginLoader: could not unload" << pi.pluginName << pluginLoader->errorString();
delete pluginLoader;
}
m_loadedPlugins.clear();
}
void LtsPluginLoader::checkPlugins(const QVector<LtsAppDescriptor::AppDescription> &appVector,
QStringList &errors /* out */)
{
NGL::INGLLogger &logger = LtsPluginLoaderHandler::logger();
if (appVector.isEmpty())
{
errors += QStringLiteral("No Apps available");
return;
}
QVector<PluginInstance> dataPlugins;
QVector<PluginInstance> guiPlugins;
auto addDataPluginFn = [&](const PluginInstance &pi)
{
dataPlugins.append(pi);
return true;
};
auto addGuiPluginFn = [&](const PluginInstance &pi)
{
guiPlugins.append(pi);
return true;
};
if (!loadPlugins(PluginType::DATA, addDataPluginFn))
{
errors += QStringLiteral("failed to load data plugins");
return;
}
if (!loadPlugins(PluginType::GUI, addGuiPluginFn))
{
errors += QStringLiteral("failed to load gui plugins");
return;
}
// check for duplicate iids per plugin type
auto checkDuplicateIidsFn = [&](const QVector<PluginInstance> &plugins, const QString &pluginType)
{
typedef QMap<QString /*iid*/, QString /*filename*/> IidMap;
IidMap iids;
for (auto i3 = plugins.constBegin(); i3 != plugins.constEnd(); ++i3)
{
const PluginInstance &pin = *i3;
auto iidFound = iids.constFind(pin.pluginName);
if (iidFound != iids.constEnd())
errors += QStringLiteral("Iid duplicate for %1 plugin '%2'").arg(pluginType, pin.pluginName);
else
iids.insert(pin.pluginName, pin.fullFileName);
}
};
checkDuplicateIidsFn(dataPlugins, QStringLiteral("data"));
checkDuplicateIidsFn(guiPlugins, QStringLiteral("gui"));
QVector<PluginInstance> plugins(dataPlugins + guiPlugins);
// check if all in apps.json requested plugins are available
for (auto i1 = appVector.constBegin(); i1 != appVector.constEnd(); ++i1) // Apps
{
const LtsAppDescriptor::AppDescription &app = *i1;
//WARNING_LOG(logger) << "Checking app '"<<app.appName<<"'";
const QVector<LtsAppDescriptor::PluginInfo> &piVector = app.pluginInfos;
for (auto i2 = piVector.constBegin(); i2 != piVector.constEnd(); i2++) // App's Required Plugins
{
const LtsAppDescriptor::PluginInfo &pi = *i2;
bool found = false;
for (auto i3 = plugins.constBegin(); i3 != plugins.constEnd(); ++i3) // Available Plugins
{
const PluginInstance &pin = *i3;
if (pi.iid == pin.pluginName)
{
found = true;
break;
}
}
if (!found)
{
ERROR_LOG(logger) << "Plugin with iid '" << pi.iid << "' was requested but was not loaded!";
errors += QStringLiteral("App '%1' requires plugin '%2' that could not be found.").arg(app.appName, pi.iid);
}
}
}
return;
}
bool LtsPluginLoader::loadPlugins(PluginType requestedPluginType,
const std::function<bool(const PluginInstance &)> &loadPlugin,
const QStringList &requiredPlugins,
const QString &specialTopicTypeFilter,
const QStringList &optionalPlugins)
{
NGL::INGLLogger &logger = LtsPluginLoaderHandler::logger();
Q_ASSERT(requestedPluginType != PluginType::UNKNOWN); // do not allow to look for unknown
QStringList requiredPluginsList = requiredPlugins; // copy ...
QStringList optionalPluginsList = optionalPlugins; // copy ...
bool checkPluginsLists = (!requiredPluginsList.isEmpty()) || (!optionalPluginsList.isEmpty());
if (checkPluginsLists && !m_loadedPlugins.isEmpty()) // only lookup of there is something to lookup TODO: faster to NOT do that check?
{
{
QMutableListIterator<QString> i1(requiredPluginsList); // delete all plugins from the to-load list that were cached
while (i1.hasNext())
{
const QString pluginName = i1.next().toLower();
QMap<PluginKey, PluginInstance>::iterator i2 = m_loadedPlugins.find(PluginKey(requestedPluginType, pluginName));
if (i2 != m_loadedPlugins.end())
{
DEBUG_LOG(logger) << "Plugin " << pluginName << " already loaded, using that instance";
if (loadPlugin)
loadPlugin(i2.value());
i1.remove();
}
}
}
{
QMutableListIterator<QString> i1(optionalPluginsList); // delete all plugins from the to-load list that were cached
while (i1.hasNext())
{
const QString pluginName = i1.next().toLower();
QMap<PluginKey, PluginInstance>::iterator i2 = m_loadedPlugins.find(PluginKey(requestedPluginType, pluginName));
if (i2 != m_loadedPlugins.end())
{
DEBUG_LOG(logger) << "Plugin " << pluginName << " already loaded, using that instance";
if (loadPlugin)
loadPlugin(i2.value());
i1.remove();
}
}
}
}
QFileInfo libPathInfo(QCoreApplication::applicationDirPath() + QStringLiteral("/../lib/"));
QString libPath(libPathInfo.absolutePath());
if (!libPathInfo.exists())
{
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* In case of windows systems all libs are located in application directory. But can't
* use QCoreApplication::applicationDirPath here, because it points some times to the
* wrong directory.
* 2016/10/18 JT
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
libPathInfo.setFile(QCoreApplication::applicationFilePath());
libPath = libPathInfo.absolutePath();
}
#ifdef Q_OS_WIN
const QStringList nameFilter = QStringList(QStringLiteral("*Plugin*.dll"));
#else
const QStringList nameFilter = QStringList(QStringLiteral("*Plugin*.so"));
#endif
static const QStringList pluginTypes({QStringLiteral("data"),
QStringLiteral("gui"),
QStringLiteral("topic_type"),
QStringLiteral("project"),
QStringLiteral("style"),
QStringLiteral("special")}); // #warning TODO: recordreplay is a data plugin ?
QDirIterator pluginDir(libPath, nameFilter, QDir::Files);
while (pluginDir.hasNext())
{
pluginDir.next();
const QString filename = pluginDir.fileName();
if (filename.contains(QStringLiteral("PluginInterface"), Qt::CaseSensitive))
continue;
const QString absoluteFilePath = pluginDir.filePath();
DEBUG_LOG(logger) << "File Path: " << absoluteFilePath;
// if (!filename.contains(type))
// {
// pluginInfo.resultType = ResultType::WRONG_TYPE;
// pluginInfos.append(pluginInfo);
// continue;
// }
QScopedPointer<QPluginLoader> pluginLoader(new QPluginLoader);
pluginLoader->setFileName(absoluteFilePath);
const QJsonObject jObj = pluginLoader->metaData();
QJsonObject::const_iterator iid = jObj.find(QStringLiteral("IID"));
if (iid == jObj.end())
{
DEBUG_LOG(logger) << "Plugin '" << filename << "' has no IID !";
continue;
}
const QString jsonIid = iid.value().toString().toLower(); // LOWERCASE ! (pun intended)
const QVector<QStringRef> iidParameters = jsonIid.splitRef(QLatin1Char(':'));
DEBUG_LOG(logger) << "Plugin " << absoluteFilePath << " provides iid '" << jsonIid;
if (iidParameters.count() != 3)
{
WARNING_LOG(logger) << "Invalid iid "<<jsonIid<<" for plugin " << filename;
continue;
}
bool ok = false;
int pluginLoaderVersion = iidParameters.at(1).toInt(&ok);
if (!ok || pluginLoaderVersion != PLUGIN_LOADER_VERSION)
{
WARNING_LOG(logger) << "Invalid plugin loader version" << pluginLoaderVersion << "for plugin:" << filename;
continue;
}
PluginType pluginType = PluginType::UNKNOWN;
const QString pluginName = iidParameters.at(2).toString();
const QString pluginTypeString = iidParameters.at(0).toString();
switch (pluginTypes.indexOf(pluginTypeString))
{
case 0: // data
pluginType = PluginType::DATA;
break;
case 1: // gui
pluginType = PluginType::GUI;
break;
case 2: // topic_type
pluginType = PluginType::TOPIC_TYPE;
break;
case 3: // project
pluginType = PluginType::PROJECT;
break;
case 4: // style
pluginType = PluginType::STYLE;
break;
case 5: // special
pluginType = PluginType::SPECIAL;
break;
default:
break;
}
if (pluginType != requestedPluginType)
{
DEBUG_LOG(logger) << "Plugin provides not the requested type " << (int)requestedPluginType << " is: " << (int)pluginType;
continue;
}
if (pluginType == PluginType::SPECIAL && !specialTopicTypeFilter.isEmpty())
{
if (!pluginName.startsWith(specialTopicTypeFilter))
{
DEBUG_LOG(logger) << "Special plugin, but filter does not match: " << pluginName << " - " << specialTopicTypeFilter;
continue;
}
}
if (checkPluginsLists)
{
bool found = false;
{
QMutableListIterator<QString> i (requiredPluginsList);
while (i.hasNext())
{
const QString requiredPlugin = i.next().toLower();
if (pluginName == requiredPlugin)
{
found = true;
i.remove();
break; // no need to look further, just a waste of time, like writing this long unnecessary explanation
}
}
}
if (!found)
{
for (auto i = optionalPluginsList.constBegin(); i != optionalPluginsList.constEnd(); i++)
{
const QString optionalPlugin = (*i).toLower();
if (pluginName == optionalPlugin)
{
found = true;
break; // no need to look further, just a waste of time, like writing this long unnecessary explanation
}
}
}
if (!found)
{
DEBUG_LOG(logger) << "Plugin " << pluginName << " was not requested";
continue; // plugin was not requested to be loaded
}
}
// before we create a new instance of a plugin, which is not needed as one instance is perfectly fine to use for many apps, look it up in the loaded plugins list
if (loadPlugin && !m_loadedPlugins.isEmpty()) // only lookup if there is something to lookup TODO: faster to NOT do that check?
{
QMap<PluginKey, PluginInstance>::iterator i = m_loadedPlugins.find(PluginKey(requestedPluginType, pluginName));
if (i != m_loadedPlugins.end())
{
DEBUG_LOG(logger) << "Plugin " << pluginName << " already loaded, using that instance ("<<i.value().plugin->metaObject()->className()<<")";
loadPlugin(i.value());
continue; // found, next plugin!
}
}
// plugin not already loaded:
QObject *plugin = pluginLoader->instance();
if (!plugin)
{
ERROR_LOG(logger) << "Failed to load " << filename << " - " << pluginLoader->errorString();
continue;
}
PluginInstance pi;
pi.pluginName = pluginName;
pi.fileName = filename;
pi.fullFileName = absoluteFilePath;
pi.plugin = plugin;
pi.pluginLoader = pluginLoader.data();
if (loadPlugin && !loadPlugin(pi)) // does the dev want this plugin? if no, unload it! TODO: really unload? or still cache it??
{
DEBUG_LOG(logger) << "Plugin " << pluginName << " unloaded";
delete pluginLoader->instance();
pluginLoader->unload();
continue; // next plugin
}
pi.pluginLoader = pluginLoader.take();
// the dev decided to use that plugin, so cache it
m_loadedPlugins.insert(PluginKey(requestedPluginType, pluginName), pi);
// if (m_loadedPlugins.count() > 100)
// WARNING_LOG(logger) << "There are more than 100 plugins cached!";
DEBUG_LOG(logger) << "Loaded " << jsonIid << " - File: " << filename;
} // end of >> pluginDir.hasNext();
if (checkPluginsLists && !requiredPluginsList.isEmpty())
{
WARNING_LOG(logger) << "These plugins could not be found for Plugin Type " << (int)requestedPluginType << ": " << requiredPluginsList.join(QStringLiteral(", "));
return false;
}
return true;
}
} // namespace LtsPluginManager