separate plugin manaager into separatge files with better defined responsibility

This commit is contained in:
Iuri Matias
2026-03-27 11:17:47 -04:00
parent 860910cdcc
commit f4854404ee
11 changed files with 707 additions and 512 deletions
+6
View File
@@ -92,6 +92,12 @@ set(LOGOS_CORE_SOURCES
logos_core/logos_core.h
logos_core/app_lifecycle.cpp
logos_core/app_lifecycle.h
logos_core/plugin_registry.cpp
logos_core/plugin_registry.h
logos_core/dependency_resolver.cpp
logos_core/dependency_resolver.h
logos_core/plugin_loader.cpp
logos_core/plugin_loader.h
logos_core/plugin_manager.cpp
logos_core/plugin_manager.h
logos_core/qt/qt_app_context.cpp
+105
View File
@@ -0,0 +1,105 @@
#include "dependency_resolver.h"
#include <QDebug>
#include <QJsonArray>
#include <QSet>
#include <QHash>
namespace DependencyResolver {
QStringList resolve(const QStringList& requested,
IsKnownFn isKnown,
GetMetadataFn getMetadata) {
qDebug() << "Resolving dependencies for modules:" << requested;
QSet<QString> modulesToLoad;
QStringList queue = requested;
QStringList missingDependencies;
while (!queue.isEmpty()) {
QString moduleName = queue.takeFirst();
if (modulesToLoad.contains(moduleName))
continue;
if (!isKnown(moduleName)) {
qWarning() << "Module not found in known plugins:" << moduleName;
missingDependencies.append(moduleName);
continue;
}
modulesToLoad.insert(moduleName);
QJsonObject metadata = getMetadata(moduleName);
if (!metadata.isEmpty()) {
QJsonArray deps = metadata.value("dependencies").toArray();
for (const QJsonValue& dep : deps) {
QString depName = dep.toString();
if (!depName.isEmpty() && !modulesToLoad.contains(depName)) {
queue.append(depName);
}
}
}
}
if (!missingDependencies.isEmpty()) {
qWarning() << "Missing dependencies detected:" << missingDependencies;
}
// Topological sort (Kahn's algorithm)
QHash<QString, QStringList> dependents;
QHash<QString, int> inDegree;
for (const QString& moduleName : modulesToLoad) {
if (!inDegree.contains(moduleName)) {
inDegree[moduleName] = 0;
}
QJsonObject metadata = getMetadata(moduleName);
if (!metadata.isEmpty()) {
QJsonArray deps = metadata.value("dependencies").toArray();
for (const QJsonValue& dep : deps) {
QString depName = dep.toString();
if (!depName.isEmpty() && modulesToLoad.contains(depName)) {
inDegree[moduleName]++;
dependents[depName].append(moduleName);
}
}
}
}
QStringList result;
QStringList zeroInDegree;
for (const QString& moduleName : modulesToLoad) {
if (inDegree.value(moduleName, 0) == 0) {
zeroInDegree.append(moduleName);
}
}
while (!zeroInDegree.isEmpty()) {
QString moduleName = zeroInDegree.takeFirst();
result.append(moduleName);
for (const QString& dependent : dependents.value(moduleName)) {
inDegree[dependent]--;
if (inDegree[dependent] == 0) {
zeroInDegree.append(dependent);
}
}
}
if (result.size() < modulesToLoad.size()) {
QStringList cycleModules;
for (const QString& moduleName : modulesToLoad) {
if (!result.contains(moduleName)) {
cycleModules.append(moduleName);
}
}
qCritical() << "Circular dependency detected involving modules:" << cycleModules;
}
qDebug() << "Resolved load order:" << result;
return result;
}
}
+19
View File
@@ -0,0 +1,19 @@
#ifndef DEPENDENCY_RESOLVER_H
#define DEPENDENCY_RESOLVER_H
#include <QString>
#include <QStringList>
#include <QJsonObject>
#include <functional>
namespace DependencyResolver {
using IsKnownFn = std::function<bool(const QString&)>;
using GetMetadataFn = std::function<QJsonObject(const QString&)>;
QStringList resolve(const QStringList& requested,
IsKnownFn isKnown,
GetMetadataFn getMetadata);
}
#endif // DEPENDENCY_RESOLVER_H
+252
View File
@@ -0,0 +1,252 @@
#include "plugin_loader.h"
#include "plugin_registry.h"
#include "dependency_resolver.h"
#include "qt/qt_process_manager.h"
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QUuid>
#include <QCoreApplication>
#include "logos_api.h"
#include "logos_api_client.h"
#include "token_manager.h"
PluginLoader::PluginLoader(PluginRegistry& registry)
: m_registry(registry)
{
}
QString PluginLoader::resolveLogosHostPath() {
QString logosHostPath;
QByteArray envPathBytes = qgetenv("LOGOS_HOST_PATH");
if (!envPathBytes.isEmpty()) {
logosHostPath = QString::fromUtf8(envPathBytes);
}
if (logosHostPath.isEmpty()) {
logosHostPath = QDir::cleanPath(QCoreApplication::applicationDirPath() + "/logos_host");
}
if (!QFile::exists(logosHostPath)) {
QStringList dirs = m_registry.pluginsDirs();
if (!dirs.isEmpty()) {
QDir pluginsDirCandidate(dirs.first());
QString candidate = QDir::cleanPath(pluginsDirCandidate.absoluteFilePath("../bin/logos_host"));
if (QFile::exists(candidate)) {
logosHostPath = candidate;
}
}
}
qDebug() << "Logos host path (resolved):" << logosHostPath;
if (!QFile::exists(logosHostPath)) {
qCritical() << "logos_host executable not found at:" << logosHostPath;
qCritical() << "Set environment variable LOGOS_HOST_PATH to the absolute path of logos_host or ensure it is next to the Electron executable or under ../bin from the plugins directory.";
return QString();
}
return logosHostPath;
}
void PluginLoader::notifyCapabilityModule(const QString& name, const QString& token) {
if (!m_registry.isLoaded("capability_module")) {
qDebug() << "Capability module not loaded, skipping token notification";
return;
}
qDebug() << "Informing capability module about new module token for:" << name;
TokenManager& tokenManager = TokenManager::instance();
QString capabilityModuleToken = tokenManager.getToken("capability_module");
qDebug() << "Capability module token:" << capabilityModuleToken;
static LogosAPI* s_coreApi = nullptr;
if (!s_coreApi)
s_coreApi = new LogosAPI("core");
LogosAPIClient* client = s_coreApi->getClient("capability_module");
bool success = client->informModuleToken(capabilityModuleToken, name, token);
if (success) {
qDebug() << "Successfully informed capability module about token for:" << name;
} else {
qWarning() << "Failed to inform capability module about token for:" << name;
}
}
bool PluginLoader::loadPlugin(const QString& name) {
qDebug() << "Attempting to load plugin by name:" << name;
if (!m_registry.isKnown(name)) {
qWarning() << "Plugin not found among known plugins:" << name;
return false;
}
QString pluginPath = m_registry.pluginPath(name);
qDebug() << "Loading plugin:" << name << "from path:" << pluginPath << "in separate process";
if (m_registry.isLoaded(name)) {
qWarning() << "Plugin already loaded:" << name;
return false;
}
QString logosHostPath = resolveLogosHostPath();
if (logosHostPath.isEmpty())
return false;
std::vector<std::string> arguments = {
"--name", name.toStdString(),
"--path", pluginPath.toStdString()
};
qDebug() << "Starting logos_host with arguments:" << name << pluginPath;
PluginRegistry* registry = &m_registry;
QtProcessManager::ProcessCallbacks callbacks;
callbacks.onFinished = [registry](const std::string& pluginName, int exitCode, bool crashed) {
Q_UNUSED(exitCode);
QString qName = QString::fromStdString(pluginName);
if (crashed) {
qCritical() << "Plugin process crashed:" << qName << "- terminating core with error";
exit(1);
}
registry->markUnloaded(qName);
};
callbacks.onError = [](const std::string& pluginName, bool crashed) {
if (crashed) {
qCritical() << "Plugin process crashed:" << QString::fromStdString(pluginName) << "- terminating core with error";
exit(1);
}
};
callbacks.onOutput = [](const std::string& pluginName, const std::string& line, bool isStderr) {
QString qName = QString::fromStdString(pluginName);
QString qLine = QString::fromStdString(line);
if (isStderr) {
qCritical() << "[LOGOS_HOST" << qName << "] STDERR:" << qLine;
} else if (qLine.contains("qrc:") || qLine.contains("Warning:") || qLine.contains("WARNING:")) {
qWarning() << "[LOGOS_HOST" << qName << "]:" << qLine;
} else if (qLine.contains("Critical:") || qLine.contains("FAILED:") || qLine.contains("ERROR:")) {
qCritical() << "[LOGOS_HOST" << qName << "]:" << qLine;
} else {
qDebug() << "[LOGOS_HOST" << qName << "]:" << qLine;
}
};
if (!QtProcessManager::startProcess(name.toStdString(), logosHostPath.toStdString(), arguments, callbacks)) {
return false;
}
QUuid authToken = QUuid::createUuid();
QString authTokenString = authToken.toString(QUuid::WithoutBraces);
qDebug() << "Generated auth token:" << authTokenString;
if (!QtProcessManager::sendToken(name.toStdString(), authTokenString.toStdString())) {
return false;
}
qDebug() << "Auth token sent securely to plugin:" << name;
m_registry.markLoaded(name);
TokenManager& tokenManager = TokenManager::instance();
tokenManager.saveToken(name, authTokenString);
notifyCapabilityModule(name, authTokenString);
qDebug() << "Plugin" << name << "is now running in separate process";
qDebug() << "Remote registry URL for this plugin: local:logos_" << name;
return true;
}
bool PluginLoader::loadPluginWithDependencies(const QString& name) {
QStringList requested;
requested.append(name);
QStringList resolved = DependencyResolver::resolve(
requested,
[this](const QString& n) { return m_registry.isKnown(n); },
[this](const QString& n) { return m_registry.pluginMetadata(n); }
);
if (resolved.isEmpty() || !resolved.contains(name)) {
qWarning() << "Cannot load plugin: plugin not found:" << name;
return false;
}
bool allSucceeded = true;
for (const QString& moduleName : resolved) {
if (m_registry.isLoaded(moduleName)) {
qDebug() << "Plugin already loaded, skipping:" << moduleName;
continue;
}
if (!loadPlugin(moduleName)) {
qWarning() << "Failed to load module:" << moduleName;
allSucceeded = false;
}
}
return allSucceeded;
}
bool PluginLoader::initializeCapabilityModule() {
qDebug() << "\n=== Initializing Capability Module ===";
if (!m_registry.isKnown("capability_module")) {
qDebug() << "Capability module not found in known plugins, skipping initialization";
return false;
}
qDebug() << "Capability module found, attempting to load...";
bool success = loadPlugin("capability_module");
if (!success) {
qDebug() << "Failed to load capability module";
return false;
}
qDebug() << "Capability module loaded successfully";
return true;
}
bool PluginLoader::unloadPlugin(const QString& name) {
qDebug() << "Attempting to unload plugin by name:" << name;
if (!m_registry.isLoaded(name)) {
qWarning() << "Plugin not loaded, cannot unload:" << name;
qDebug() << "Loaded plugins:" << m_registry.loadedPluginNames();
return false;
}
if (!QtProcessManager::hasProcess(name.toStdString())) {
qWarning() << "No process found for plugin:" << name;
return false;
}
QtProcessManager::terminateProcess(name.toStdString());
m_registry.markUnloaded(name);
qDebug() << "Successfully unloaded plugin:" << name;
return true;
}
void PluginLoader::terminateAll() {
QtProcessManager::terminateAll();
m_registry.clearLoaded();
}
QHash<QString, qint64> PluginLoader::getPluginProcessIds() {
auto stdMap = QtProcessManager::getAllProcessIds();
QHash<QString, qint64> result;
for (const auto& [name, pid] : stdMap) {
result.insert(QString::fromStdString(name), pid);
}
return result;
}
+27
View File
@@ -0,0 +1,27 @@
#ifndef PLUGIN_LOADER_H
#define PLUGIN_LOADER_H
#include <QString>
#include <QHash>
class PluginRegistry;
class PluginLoader {
public:
explicit PluginLoader(PluginRegistry& registry);
bool loadPlugin(const QString& name);
bool loadPluginWithDependencies(const QString& name);
bool initializeCapabilityModule();
bool unloadPlugin(const QString& name);
void terminateAll();
QHash<QString, qint64> getPluginProcessIds();
private:
QString resolveLogosHostPath();
void notifyCapabilityModule(const QString& name, const QString& token);
PluginRegistry& m_registry;
};
#endif // PLUGIN_LOADER_H
+63 -422
View File
@@ -1,115 +1,70 @@
#include "plugin_manager.h"
#include "qt/qt_process_manager.h"
#include "plugin_registry.h"
#include "plugin_loader.h"
#include "dependency_resolver.h"
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QJsonDocument>
#include <QJsonArray>
#include <QUuid>
#include <QCoreApplication>
#include <cstring>
#include <cassert>
#include "logos_api.h"
#include "logos_api_client.h"
#include "token_manager.h"
#include "logos_mode.h"
#include <module_lib/module_lib.h>
#include <package_manager_lib.h>
#include <cstring>
using namespace ModuleLib;
namespace {
PluginRegistry& registryInstance() {
static PluginRegistry instance;
return instance;
}
PluginLoader& loaderInstance() {
static PluginLoader instance(registryInstance());
return instance;
}
char** toNullTerminatedArray(const QStringList& list) {
int count = list.size();
if (count == 0) {
char** result = new char*[1];
result[0] = nullptr;
return result;
}
char** result = new char*[count + 1];
for (int i = 0; i < count; ++i) {
QByteArray utf8Data = list[i].toUtf8();
result[i] = new char[utf8Data.size() + 1];
strcpy(result[i], utf8Data.constData());
}
result[count] = nullptr;
return result;
}
}
namespace PluginManager {
QStringList s_plugins_dirs;
QStringList s_loaded_plugins;
QHash<QString, QString> s_known_plugins;
QHash<QString, QJsonObject> s_plugin_metadata;
PluginRegistry& registry() {
return registryInstance();
}
void setPluginsDir(const char* plugins_dir) {
assert(plugins_dir != nullptr);
s_plugins_dirs.clear();
s_plugins_dirs.append(QString(plugins_dir));
qInfo() << "Custom plugins directory set to:" << s_plugins_dirs.first();
registryInstance().setPluginsDir(QString(plugins_dir));
}
void addPluginsDir(const char* plugins_dir) {
assert(plugins_dir != nullptr);
QString dir = QString(plugins_dir);
if (s_plugins_dirs.contains(dir)) return;
s_plugins_dirs.append(dir);
qDebug() << "Added plugins directory:" << dir;
}
// Delegate to PackageManagerLib for platform variant selection and plugin scanning
static PackageManagerLib& packageManagerInstance() {
static PackageManagerLib instance;
return instance;
registryInstance().addPluginsDir(QString(plugins_dir));
}
void discoverInstalledModules() {
// Configure the package manager with the directories set via the C API
PackageManagerLib& pm = packageManagerInstance();
if (!s_plugins_dirs.isEmpty()) {
pm.setEmbeddedModulesDirectory(s_plugins_dirs.first().toStdString());
for (int i = 1; i < s_plugins_dirs.size(); ++i) {
pm.setUserModulesDirectory(s_plugins_dirs[i].toStdString());
}
}
std::string jsonStr = pm.getInstalledModules();
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(jsonStr));
QJsonArray modules = doc.array();
for (const QJsonValue& val : modules) {
QJsonObject mod = val.toObject();
QString name = mod.value("name").toString();
QString mainFilePath = mod.value("mainFilePath").toString();
if (name.isEmpty() || mainFilePath.isEmpty())
continue;
// Process plugin binary to extract Qt metadata and register
QString pluginName = processPlugin(mainFilePath);
if (pluginName.isEmpty()) {
qWarning() << "Failed to process plugin (no metadata or invalid):" << mainFilePath;
} else {
qDebug() << "Discovered module:" << pluginName << "at" << mainFilePath;
}
}
qDebug() << "Total known plugins after discovery:" << s_known_plugins.size();
qDebug() << "Known plugin names:" << s_known_plugins.keys();
registryInstance().discoverInstalledModules();
}
void terminateAll() {
QtProcessManager::terminateAll();
s_loaded_plugins.clear();
}
QString processPlugin(const QString &pluginPath) {
auto metadataOpt = LogosModule::extractMetadata(pluginPath);
if (!metadataOpt) {
qWarning() << "No metadata found for plugin:" << pluginPath;
return QString();
}
const ModuleMetadata& metadata = *metadataOpt;
if (!metadata.isValid()) {
qWarning() << "Plugin name not specified in metadata for:" << pluginPath;
return QString();
}
s_known_plugins.insert(metadata.name, pluginPath);
s_plugin_metadata.insert(metadata.name, metadata.rawMetadata);
return metadata.name;
QString processPlugin(const QString& pluginPath) {
return registryInstance().processPlugin(pluginPath);
}
char* processPluginCStr(const char* pluginPath) {
QString path = QString::fromUtf8(pluginPath);
qDebug() << "Processing plugin file:" << path;
QString pluginName = processPlugin(path);
QString pluginName = registryInstance().processPlugin(path);
if (pluginName.isEmpty()) {
qWarning() << "Failed to process plugin file:" << path;
return nullptr;
@@ -122,365 +77,51 @@ namespace PluginManager {
}
bool loadPlugin(const char* pluginName) {
const QString name = QString::fromUtf8(pluginName);
qDebug() << "Attempting to load plugin by name:" << name;
if (!s_known_plugins.contains(name)) {
qWarning() << "Plugin not found among known plugins:" << name;
return false;
}
QString pluginPath = s_known_plugins.value(name);
qDebug() << "Loading plugin:" << name << "from path:" << pluginPath << "in separate process";
if (isPluginLoaded(name)) {
qWarning() << "Plugin already loaded:" << name;
return false;
}
// Find the logos_host executable with multiple strategies
QString logosHostPath;
// 1) Environment override
QByteArray envPathBytes = qgetenv("LOGOS_HOST_PATH");
if (!envPathBytes.isEmpty()) {
logosHostPath = QString::fromUtf8(envPathBytes);
}
// 2) Default next to Electron/host executable
if (logosHostPath.isEmpty()) {
logosHostPath = QDir::cleanPath(QCoreApplication::applicationDirPath() + "/logos_host");
}
// 3) Fallback relative to plugins directory (../../logos-liblogos/build/bin/logos_host)
if (!QFile::exists(logosHostPath)) {
if (!s_plugins_dirs.isEmpty()) {
QDir pluginsDirCandidate(s_plugins_dirs.first());
QString candidate = QDir::cleanPath(pluginsDirCandidate.absoluteFilePath("../bin/logos_host"));
if (QFile::exists(candidate)) {
logosHostPath = candidate;
}
}
}
qDebug() << "Logos host path (resolved):" << logosHostPath;
if (!QFile::exists(logosHostPath)) {
qCritical() << "logos_host executable not found at:" << logosHostPath;
qCritical() << "Set environment variable LOGOS_HOST_PATH to the absolute path of logos_host or ensure it is next to the Electron executable or under ../bin from the plugins directory.";
return false;
}
std::vector<std::string> arguments = {
"--name", name.toStdString(),
"--path", pluginPath.toStdString()
};
qDebug() << "Starting logos_host with arguments:" << name << pluginPath;
QtProcessManager::ProcessCallbacks callbacks;
callbacks.onFinished = [](const std::string& pluginName, int exitCode, bool crashed) {
Q_UNUSED(exitCode);
QString qName = QString::fromStdString(pluginName);
if (crashed) {
qCritical() << "Plugin process crashed:" << qName << "- terminating core with error";
exit(1);
}
s_loaded_plugins.removeAll(qName);
};
callbacks.onError = [](const std::string& pluginName, bool crashed) {
if (crashed) {
qCritical() << "Plugin process crashed:" << QString::fromStdString(pluginName) << "- terminating core with error";
exit(1);
}
};
callbacks.onOutput = [](const std::string& pluginName, const std::string& line, bool isStderr) {
QString qName = QString::fromStdString(pluginName);
QString qLine = QString::fromStdString(line);
if (isStderr) {
qCritical() << "[LOGOS_HOST" << qName << "] STDERR:" << qLine;
} else if (qLine.contains("qrc:") || qLine.contains("Warning:") || qLine.contains("WARNING:")) {
qWarning() << "[LOGOS_HOST" << qName << "]:" << qLine;
} else if (qLine.contains("Critical:") || qLine.contains("FAILED:") || qLine.contains("ERROR:")) {
qCritical() << "[LOGOS_HOST" << qName << "]:" << qLine;
} else {
qDebug() << "[LOGOS_HOST" << qName << "]:" << qLine;
}
};
if (!QtProcessManager::startProcess(name.toStdString(), logosHostPath.toStdString(), arguments, callbacks)) {
return false;
}
// Generate auth token and send via IPC
QUuid authToken = QUuid::createUuid();
QString authTokenString = authToken.toString(QUuid::WithoutBraces);
qDebug() << "Generated auth token:" << authTokenString;
if (!QtProcessManager::sendToken(name.toStdString(), authTokenString.toStdString())) {
return false;
}
qDebug() << "Auth token sent securely to plugin:" << name;
s_loaded_plugins.append(name);
TokenManager& tokenManager = TokenManager::instance();
tokenManager.saveToken(name, authTokenString);
// Inform capability module about the new module token
if (s_loaded_plugins.contains("capability_module")) {
qDebug() << "Informing capability module about new module token for:" << name;
QString capabilityModuleToken = tokenManager.getToken("capability_module");
qDebug() << "Capability module token:" << capabilityModuleToken;
static LogosAPI* s_coreApi = nullptr;
if (!s_coreApi)
s_coreApi = new LogosAPI("core");
LogosAPIClient* client = s_coreApi->getClient("capability_module");
bool success = client->informModuleToken(capabilityModuleToken, name, authTokenString);
if (success) {
qDebug() << "Successfully informed capability module about token for:" << name;
} else {
qWarning() << "Failed to inform capability module about token for:" << name;
}
} else {
qDebug() << "Capability module not loaded, skipping token notification";
}
qDebug() << "Plugin" << name << "is now running in separate process";
qDebug() << "Remote registry URL for this plugin: local:logos_" << name;
return true;
return loaderInstance().loadPlugin(QString::fromUtf8(pluginName));
}
bool loadPluginWithDependencies(const char* pluginName) {
const QString name = QString::fromUtf8(pluginName);
QStringList requestedModules;
requestedModules.append(name);
QStringList resolvedModules = resolveDependencies(requestedModules);
if (resolvedModules.isEmpty() || !resolvedModules.contains(name)) {
qWarning() << "Cannot load plugin: plugin not found:" << name;
return false;
}
bool allSucceeded = true;
for (const QString& moduleName : resolvedModules) {
if (isPluginLoaded(moduleName)) {
qDebug() << "Plugin already loaded, skipping:" << moduleName;
continue;
}
if (!loadPlugin(moduleName.toUtf8().constData())) {
qWarning() << "Failed to load module:" << moduleName;
allSucceeded = false;
}
}
return allSucceeded;
return loaderInstance().loadPluginWithDependencies(QString::fromUtf8(pluginName));
}
bool initializeCapabilityModule() {
qDebug() << "\n=== Initializing Capability Module ===";
if (!s_known_plugins.contains("capability_module")) {
qDebug() << "Capability module not found in known plugins, skipping initialization";
return false;
}
qDebug() << "Capability module found, attempting to load...";
bool success = loadPlugin("capability_module");
if (!success) {
qDebug() << "Failed to load capability module";
return false;
}
qDebug() << "Capability module loaded successfully";
return true;
return loaderInstance().initializeCapabilityModule();
}
bool unloadPlugin(const char* pluginName) {
const QString name = QString::fromUtf8(pluginName);
qDebug() << "Attempting to unload plugin by name:" << name;
return loaderInstance().unloadPlugin(QString::fromUtf8(pluginName));
}
if (!s_loaded_plugins.contains(name)) {
qWarning() << "Plugin not loaded, cannot unload:" << name;
qDebug() << "Loaded plugins:" << s_loaded_plugins;
return false;
}
if (!QtProcessManager::hasProcess(name.toStdString())) {
qWarning() << "No process found for plugin:" << name;
return false;
}
QtProcessManager::terminateProcess(name.toStdString());
s_loaded_plugins.removeAll(name);
qDebug() << "Successfully unloaded plugin:" << name;
return true;
void terminateAll() {
loaderInstance().terminateAll();
}
char** getLoadedPluginsCStr() {
int count = s_loaded_plugins.size();
if (count == 0) {
char** result = new char*[1];
result[0] = nullptr;
return result;
}
char** result = new char*[count + 1];
for (int i = 0; i < count; ++i) {
QByteArray utf8Data = s_loaded_plugins[i].toUtf8();
result[i] = new char[utf8Data.size() + 1];
strcpy(result[i], utf8Data.constData());
}
result[count] = nullptr;
return result;
return toNullTerminatedArray(registryInstance().loadedPluginNames());
}
char** getKnownPluginsCStr() {
QStringList knownPlugins = s_known_plugins.keys();
int count = knownPlugins.size();
if (count == 0) {
QStringList known = registryInstance().knownPluginNames();
if (known.isEmpty()) {
qWarning() << "No known plugins to return";
char** result = new char*[1];
result[0] = nullptr;
return result;
}
char** result = new char*[count + 1];
for (int i = 0; i < count; ++i) {
QByteArray utf8Data = knownPlugins[i].toUtf8();
result[i] = new char[utf8Data.size() + 1];
strcpy(result[i], utf8Data.constData());
}
result[count] = nullptr;
return result;
return toNullTerminatedArray(known);
}
bool isPluginLoaded(const QString& name) {
return s_loaded_plugins.contains(name);
}
QStringList resolveDependencies(const QStringList& requestedModules) {
qDebug() << "Resolving dependencies for modules:" << requestedModules;
QSet<QString> modulesToLoad;
QStringList queue = requestedModules;
QStringList missingDependencies;
while (!queue.isEmpty()) {
QString moduleName = queue.takeFirst();
if (modulesToLoad.contains(moduleName)) {
continue;
}
if (!s_known_plugins.contains(moduleName)) {
qWarning() << "Module not found in known plugins:" << moduleName;
missingDependencies.append(moduleName);
continue;
}
modulesToLoad.insert(moduleName);
if (s_plugin_metadata.contains(moduleName)) {
QJsonObject metadata = s_plugin_metadata.value(moduleName);
QJsonArray deps = metadata.value("dependencies").toArray();
for (const QJsonValue& dep : deps) {
QString depName = dep.toString();
if (!depName.isEmpty() && !modulesToLoad.contains(depName)) {
queue.append(depName);
}
}
}
}
if (!missingDependencies.isEmpty()) {
qWarning() << "Missing dependencies detected:" << missingDependencies;
}
QHash<QString, QStringList> dependents;
QHash<QString, int> inDegree;
for (const QString& moduleName : modulesToLoad) {
if (!inDegree.contains(moduleName)) {
inDegree[moduleName] = 0;
}
if (s_plugin_metadata.contains(moduleName)) {
QJsonObject metadata = s_plugin_metadata.value(moduleName);
QJsonArray deps = metadata.value("dependencies").toArray();
for (const QJsonValue& dep : deps) {
QString depName = dep.toString();
if (!depName.isEmpty() && modulesToLoad.contains(depName)) {
inDegree[moduleName]++;
dependents[depName].append(moduleName);
}
}
}
}
QStringList result;
QStringList zeroInDegree;
for (const QString& moduleName : modulesToLoad) {
if (inDegree.value(moduleName, 0) == 0) {
zeroInDegree.append(moduleName);
}
}
while (!zeroInDegree.isEmpty()) {
QString moduleName = zeroInDegree.takeFirst();
result.append(moduleName);
for (const QString& dependent : dependents.value(moduleName)) {
inDegree[dependent]--;
if (inDegree[dependent] == 0) {
zeroInDegree.append(dependent);
}
}
}
if (result.size() < modulesToLoad.size()) {
QStringList cycleModules;
for (const QString& moduleName : modulesToLoad) {
if (!result.contains(moduleName)) {
cycleModules.append(moduleName);
}
}
qCritical() << "Circular dependency detected involving modules:" << cycleModules;
}
qDebug() << "Resolved load order:" << result;
return result;
return registryInstance().isLoaded(name);
}
QHash<QString, qint64> getPluginProcessIds() {
auto stdMap = QtProcessManager::getAllProcessIds();
QHash<QString, qint64> result;
for (const auto& [name, pid] : stdMap) {
result.insert(QString::fromStdString(name), pid);
}
return result;
return loaderInstance().getPluginProcessIds();
}
QStringList resolveDependencies(const QStringList& requestedModules) {
return DependencyResolver::resolve(
requestedModules,
[](const QString& name) { return registryInstance().isKnown(name); },
[](const QString& name) { return registryInstance().pluginMetadata(name); }
);
}
}
+4
View File
@@ -6,7 +6,11 @@
#include <QHash>
#include <QJsonObject>
class PluginRegistry;
namespace PluginManager {
PluginRegistry& registry();
void setPluginsDir(const char* plugins_dir);
void addPluginsDir(const char* plugins_dir);
+134
View File
@@ -0,0 +1,134 @@
#include "plugin_registry.h"
#include <QDebug>
#include <QDir>
#include <QJsonDocument>
#include <QJsonArray>
#include <cassert>
#include <module_lib/module_lib.h>
#include <package_manager_lib.h>
using namespace ModuleLib;
static PackageManagerLib& packageManagerInstance() {
static PackageManagerLib instance;
return instance;
}
void PluginRegistry::setPluginsDir(const QString& dir) {
m_pluginsDirs.clear();
m_pluginsDirs.append(dir);
qInfo() << "Custom plugins directory set to:" << m_pluginsDirs.first();
}
void PluginRegistry::addPluginsDir(const QString& dir) {
if (m_pluginsDirs.contains(dir)) return;
m_pluginsDirs.append(dir);
qDebug() << "Added plugins directory:" << dir;
}
QStringList PluginRegistry::pluginsDirs() const {
return m_pluginsDirs;
}
void PluginRegistry::discoverInstalledModules() {
PackageManagerLib& pm = packageManagerInstance();
if (!m_pluginsDirs.isEmpty()) {
pm.setEmbeddedModulesDirectory(m_pluginsDirs.first().toStdString());
for (int i = 1; i < m_pluginsDirs.size(); ++i) {
pm.setUserModulesDirectory(m_pluginsDirs[i].toStdString());
}
}
std::string jsonStr = pm.getInstalledModules();
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(jsonStr));
QJsonArray modules = doc.array();
for (const QJsonValue& val : modules) {
QJsonObject mod = val.toObject();
QString name = mod.value("name").toString();
QString mainFilePath = mod.value("mainFilePath").toString();
if (name.isEmpty() || mainFilePath.isEmpty())
continue;
QString pluginName = processPlugin(mainFilePath);
if (pluginName.isEmpty()) {
qWarning() << "Failed to process plugin (no metadata or invalid):" << mainFilePath;
} else {
qDebug() << "Discovered module:" << pluginName << "at" << mainFilePath;
}
}
qDebug() << "Total known plugins after discovery:" << m_knownPlugins.size();
qDebug() << "Known plugin names:" << m_knownPlugins.keys();
}
QString PluginRegistry::processPlugin(const QString& pluginPath) {
auto metadataOpt = LogosModule::extractMetadata(pluginPath);
if (!metadataOpt) {
qWarning() << "No metadata found for plugin:" << pluginPath;
return QString();
}
const ModuleMetadata& metadata = *metadataOpt;
if (!metadata.isValid()) {
qWarning() << "Plugin name not specified in metadata for:" << pluginPath;
return QString();
}
m_knownPlugins.insert(metadata.name, pluginPath);
m_pluginMetadata.insert(metadata.name, metadata.rawMetadata);
return metadata.name;
}
bool PluginRegistry::isKnown(const QString& name) const {
return m_knownPlugins.contains(name);
}
QString PluginRegistry::pluginPath(const QString& name) const {
return m_knownPlugins.value(name);
}
QJsonObject PluginRegistry::pluginMetadata(const QString& name) const {
return m_pluginMetadata.value(name);
}
QStringList PluginRegistry::knownPluginNames() const {
return m_knownPlugins.keys();
}
void PluginRegistry::registerPlugin(const QString& name, const QString& path) {
m_knownPlugins.insert(name, path);
}
void PluginRegistry::registerMetadata(const QString& name, const QJsonObject& metadata) {
m_pluginMetadata.insert(name, metadata);
}
bool PluginRegistry::isLoaded(const QString& name) const {
return m_loadedPlugins.contains(name);
}
void PluginRegistry::markLoaded(const QString& name) {
m_loadedPlugins.append(name);
}
void PluginRegistry::markUnloaded(const QString& name) {
m_loadedPlugins.removeAll(name);
}
QStringList PluginRegistry::loadedPluginNames() const {
return m_loadedPlugins;
}
void PluginRegistry::clearLoaded() {
m_loadedPlugins.clear();
}
void PluginRegistry::clear() {
m_pluginsDirs.clear();
m_knownPlugins.clear();
m_pluginMetadata.clear();
m_loadedPlugins.clear();
}
+40
View File
@@ -0,0 +1,40 @@
#ifndef PLUGIN_REGISTRY_H
#define PLUGIN_REGISTRY_H
#include <QString>
#include <QStringList>
#include <QHash>
#include <QJsonObject>
class PluginRegistry {
public:
void setPluginsDir(const QString& dir);
void addPluginsDir(const QString& dir);
QStringList pluginsDirs() const;
void discoverInstalledModules();
QString processPlugin(const QString& pluginPath);
bool isKnown(const QString& name) const;
QString pluginPath(const QString& name) const;
QJsonObject pluginMetadata(const QString& name) const;
QStringList knownPluginNames() const;
void registerPlugin(const QString& name, const QString& path);
void registerMetadata(const QString& name, const QJsonObject& metadata);
bool isLoaded(const QString& name) const;
void markLoaded(const QString& name);
void markUnloaded(const QString& name);
QStringList loadedPluginNames() const;
void clearLoaded();
void clear();
private:
QStringList m_pluginsDirs;
QHash<QString, QString> m_knownPlugins;
QHash<QString, QJsonObject> m_pluginMetadata;
QStringList m_loadedPlugins;
};
#endif // PLUGIN_REGISTRY_H
+16 -26
View File
@@ -1,21 +1,13 @@
#include <gtest/gtest.h>
#include "app_lifecycle.h"
#include "plugin_manager.h"
#include "plugin_registry.h"
#include <QCoreApplication>
#include <QDebug>
namespace PluginManager {
extern QStringList s_plugins_dirs;
extern QStringList s_loaded_plugins;
extern QHash<QString, QString> s_known_plugins;
extern QHash<QString, QJsonObject> s_plugin_metadata;
}
static void clearPluginState() {
PluginManager::terminateAll();
PluginManager::s_plugins_dirs.clear();
PluginManager::s_known_plugins.clear();
PluginManager::s_plugin_metadata.clear();
PluginManager::registry().clear();
}
int main(int argc, char** argv) {
@@ -70,35 +62,35 @@ TEST_F(AppLifecycleTest, SetPluginsDir_SetsDirectory) {
PluginManager::setPluginsDir(testDir);
ASSERT_EQ(PluginManager::s_plugins_dirs.size(), 1);
EXPECT_EQ(PluginManager::s_plugins_dirs[0].toStdString(), testDir);
ASSERT_EQ(PluginManager::registry().pluginsDirs().size(), 1);
EXPECT_EQ(PluginManager::registry().pluginsDirs()[0].toStdString(), testDir);
}
TEST_F(AppLifecycleTest, SetPluginsDir_ClearsExisting) {
PluginManager::addPluginsDir("/dir1");
PluginManager::addPluginsDir("/dir2");
ASSERT_EQ(PluginManager::s_plugins_dirs.size(), 2);
ASSERT_EQ(PluginManager::registry().pluginsDirs().size(), 2);
PluginManager::setPluginsDir("/new_dir");
ASSERT_EQ(PluginManager::s_plugins_dirs.size(), 1);
EXPECT_EQ(PluginManager::s_plugins_dirs[0].toStdString(), "/new_dir");
ASSERT_EQ(PluginManager::registry().pluginsDirs().size(), 1);
EXPECT_EQ(PluginManager::registry().pluginsDirs()[0].toStdString(), "/new_dir");
}
TEST_F(AppLifecycleTest, AddPluginsDir_AppendsDirectory) {
PluginManager::addPluginsDir("/dir1");
PluginManager::addPluginsDir("/dir2");
ASSERT_EQ(PluginManager::s_plugins_dirs.size(), 2);
EXPECT_EQ(PluginManager::s_plugins_dirs[0].toStdString(), "/dir1");
EXPECT_EQ(PluginManager::s_plugins_dirs[1].toStdString(), "/dir2");
ASSERT_EQ(PluginManager::registry().pluginsDirs().size(), 2);
EXPECT_EQ(PluginManager::registry().pluginsDirs()[0].toStdString(), "/dir1");
EXPECT_EQ(PluginManager::registry().pluginsDirs()[1].toStdString(), "/dir2");
}
TEST_F(AppLifecycleTest, AddPluginsDir_NoDuplicates) {
PluginManager::addPluginsDir("/test");
PluginManager::addPluginsDir("/test");
EXPECT_EQ(PluginManager::s_plugins_dirs.size(), 1);
EXPECT_EQ(PluginManager::registry().pluginsDirs().size(), 1);
}
// =============================================================================
@@ -106,13 +98,13 @@ TEST_F(AppLifecycleTest, AddPluginsDir_NoDuplicates) {
// =============================================================================
TEST_F(AppLifecycleTest, Cleanup_ClearsGlobals) {
PluginManager::s_known_plugins.insert("test", "/path/to/test");
PluginManager::registry().registerPlugin("test", "/path/to/test");
PluginManager::addPluginsDir("/test");
clearPluginState();
EXPECT_TRUE(PluginManager::s_loaded_plugins.isEmpty());
EXPECT_TRUE(PluginManager::s_known_plugins.isEmpty());
EXPECT_TRUE(PluginManager::registry().loadedPluginNames().isEmpty());
EXPECT_TRUE(PluginManager::registry().knownPluginNames().isEmpty());
}
TEST_F(AppLifecycleTest, Cleanup_DeletesOwnedApp) {
@@ -135,8 +127,6 @@ TEST_F(AppLifecycleTest, Cleanup_PreservesExternalApp) {
// =============================================================================
TEST_F(AppLifecycleTest, ProcessEvents_HandlesNoApp) {
// cleanup() resets app state to null (won't delete the test runner's app
// since init() detected it as external and didn't claim ownership)
char* argv[] = {(char*)"test"};
AppLifecycle::init(1, argv);
AppLifecycle::cleanup();
@@ -163,8 +153,8 @@ TEST_F(AppLifecycleTest, Start_UsesCustomPluginsDirs) {
AppLifecycle::start();
ASSERT_EQ(PluginManager::s_plugins_dirs.size(), 1);
EXPECT_EQ(PluginManager::s_plugins_dirs[0].toStdString(), "/custom/plugins");
ASSERT_EQ(PluginManager::registry().pluginsDirs().size(), 1);
EXPECT_EQ(PluginManager::registry().pluginsDirs()[0].toStdString(), "/custom/plugins");
}
// =============================================================================
+41 -64
View File
@@ -1,5 +1,6 @@
#include <gtest/gtest.h>
#include "plugin_manager.h"
#include "plugin_registry.h"
#include "logos_core.h"
#include <QCoreApplication>
#include <QJsonArray>
@@ -10,18 +11,9 @@
#include <cstring>
#include <fstream>
namespace PluginManager {
extern QStringList s_plugins_dirs;
extern QStringList s_loaded_plugins;
extern QHash<QString, QString> s_known_plugins;
extern QHash<QString, QJsonObject> s_plugin_metadata;
}
static void clearPluginState() {
PluginManager::terminateAll();
PluginManager::s_plugins_dirs.clear();
PluginManager::s_known_plugins.clear();
PluginManager::s_plugin_metadata.clear();
PluginManager::registry().clear();
}
class PluginManagerTest : public ::testing::Test {
@@ -40,20 +32,20 @@ protected:
// =============================================================================
TEST_F(PluginManagerTest, GetLoadedPlugins_ReturnsEmptyList) {
EXPECT_TRUE(PluginManager::s_loaded_plugins.isEmpty());
EXPECT_TRUE(PluginManager::registry().loadedPluginNames().isEmpty());
}
TEST_F(PluginManagerTest, GetKnownPlugins_ReturnsEmptyHash) {
EXPECT_TRUE(PluginManager::s_known_plugins.isEmpty());
EXPECT_TRUE(PluginManager::registry().knownPluginNames().isEmpty());
}
TEST_F(PluginManagerTest, GetKnownPlugins_ReturnsCorrectHash) {
PluginManager::s_known_plugins.insert("plugin1", "/path/to/plugin1.dylib");
PluginManager::s_known_plugins.insert("plugin2", "/path/to/plugin2.dylib");
PluginManager::registry().registerPlugin("plugin1", "/path/to/plugin1.dylib");
PluginManager::registry().registerPlugin("plugin2", "/path/to/plugin2.dylib");
ASSERT_EQ(PluginManager::s_known_plugins.size(), 2);
EXPECT_EQ(PluginManager::s_known_plugins.value("plugin1").toStdString(), "/path/to/plugin1.dylib");
EXPECT_EQ(PluginManager::s_known_plugins.value("plugin2").toStdString(), "/path/to/plugin2.dylib");
ASSERT_EQ(PluginManager::registry().knownPluginNames().size(), 2);
EXPECT_EQ(PluginManager::registry().pluginPath("plugin1").toStdString(), "/path/to/plugin1.dylib");
EXPECT_EQ(PluginManager::registry().pluginPath("plugin2").toStdString(), "/path/to/plugin2.dylib");
}
TEST_F(PluginManagerTest, IsPluginLoaded_ReturnsFalseForUnloaded) {
@@ -61,13 +53,13 @@ TEST_F(PluginManagerTest, IsPluginLoaded_ReturnsFalseForUnloaded) {
}
TEST_F(PluginManagerTest, IsPluginKnown_ReturnsFalseForUnknown) {
EXPECT_FALSE(PluginManager::s_known_plugins.contains("nonexistent_plugin"));
EXPECT_FALSE(PluginManager::registry().isKnown("nonexistent_plugin"));
}
TEST_F(PluginManagerTest, IsPluginKnown_ReturnsTrueForKnown) {
PluginManager::s_known_plugins.insert("test_plugin", "/path/to/plugin");
PluginManager::registry().registerPlugin("test_plugin", "/path/to/plugin");
EXPECT_TRUE(PluginManager::s_known_plugins.contains("test_plugin"));
EXPECT_TRUE(PluginManager::registry().isKnown("test_plugin"));
}
// =============================================================================
@@ -93,8 +85,8 @@ TEST_F(PluginManagerTest, GetKnownPluginsCStr_ReturnsNullTerminatedArrayWhenEmpt
}
TEST_F(PluginManagerTest, GetKnownPluginsCStr_ReturnsCorrectArray) {
PluginManager::s_known_plugins.insert("plugin1", "/path/to/plugin1");
PluginManager::s_known_plugins.insert("plugin2", "/path/to/plugin2");
PluginManager::registry().registerPlugin("plugin1", "/path/to/plugin1");
PluginManager::registry().registerPlugin("plugin2", "/path/to/plugin2");
char** result = PluginManager::getKnownPluginsCStr();
@@ -151,11 +143,11 @@ TEST_F(PluginManagerTest, ResolveDependencies_ReturnsEmptyForUnknownPlugin) {
}
TEST_F(PluginManagerTest, ResolveDependencies_ReturnsSinglePluginWithNoDeps) {
PluginManager::s_known_plugins.insert("plugin_a", "/path/to/plugin_a");
PluginManager::registry().registerPlugin("plugin_a", "/path/to/plugin_a");
QJsonObject metadata;
metadata["name"] = "plugin_a";
metadata["dependencies"] = QJsonArray();
PluginManager::s_plugin_metadata.insert("plugin_a", metadata);
PluginManager::registry().registerMetadata("plugin_a", metadata);
QStringList requested;
requested.append("plugin_a");
@@ -167,20 +159,20 @@ TEST_F(PluginManagerTest, ResolveDependencies_ReturnsSinglePluginWithNoDeps) {
}
TEST_F(PluginManagerTest, ResolveDependencies_ReturnsCorrectOrder) {
PluginManager::s_known_plugins.insert("plugin_a", "/path/to/plugin_a");
PluginManager::s_known_plugins.insert("plugin_b", "/path/to/plugin_b");
PluginManager::registry().registerPlugin("plugin_a", "/path/to/plugin_a");
PluginManager::registry().registerPlugin("plugin_b", "/path/to/plugin_b");
QJsonObject metadataA;
metadataA["name"] = "plugin_a";
QJsonArray depsA;
depsA.append("plugin_b");
metadataA["dependencies"] = depsA;
PluginManager::s_plugin_metadata.insert("plugin_a", metadataA);
PluginManager::registry().registerMetadata("plugin_a", metadataA);
QJsonObject metadataB;
metadataB["name"] = "plugin_b";
metadataB["dependencies"] = QJsonArray();
PluginManager::s_plugin_metadata.insert("plugin_b", metadataB);
PluginManager::registry().registerMetadata("plugin_b", metadataB);
QStringList requested;
requested.append("plugin_a");
@@ -193,28 +185,28 @@ TEST_F(PluginManagerTest, ResolveDependencies_ReturnsCorrectOrder) {
}
TEST_F(PluginManagerTest, ResolveDependencies_HandlesTransitiveDeps) {
PluginManager::s_known_plugins.insert("plugin_a", "/path/to/plugin_a");
PluginManager::s_known_plugins.insert("plugin_b", "/path/to/plugin_b");
PluginManager::s_known_plugins.insert("plugin_c", "/path/to/plugin_c");
PluginManager::registry().registerPlugin("plugin_a", "/path/to/plugin_a");
PluginManager::registry().registerPlugin("plugin_b", "/path/to/plugin_b");
PluginManager::registry().registerPlugin("plugin_c", "/path/to/plugin_c");
QJsonObject metadataA;
metadataA["name"] = "plugin_a";
QJsonArray depsA;
depsA.append("plugin_b");
metadataA["dependencies"] = depsA;
PluginManager::s_plugin_metadata.insert("plugin_a", metadataA);
PluginManager::registry().registerMetadata("plugin_a", metadataA);
QJsonObject metadataB;
metadataB["name"] = "plugin_b";
QJsonArray depsB;
depsB.append("plugin_c");
metadataB["dependencies"] = depsB;
PluginManager::s_plugin_metadata.insert("plugin_b", metadataB);
PluginManager::registry().registerMetadata("plugin_b", metadataB);
QJsonObject metadataC;
metadataC["name"] = "plugin_c";
metadataC["dependencies"] = QJsonArray();
PluginManager::s_plugin_metadata.insert("plugin_c", metadataC);
PluginManager::registry().registerMetadata("plugin_c", metadataC);
QStringList requested;
requested.append("plugin_a");
@@ -246,8 +238,8 @@ TEST_F(PluginManagerTest, LoadPluginWithDependencies_ReturnsZeroForUnknown) {
TEST_F(PluginManagerTest, SetPluginsDir_SetsFirstDirectory) {
PluginManager::setPluginsDir("/tmp/test_plugins");
ASSERT_EQ(PluginManager::s_plugins_dirs.size(), 1);
EXPECT_EQ(PluginManager::s_plugins_dirs[0].toStdString(), "/tmp/test_plugins");
ASSERT_EQ(PluginManager::registry().pluginsDirs().size(), 1);
EXPECT_EQ(PluginManager::registry().pluginsDirs()[0].toStdString(), "/tmp/test_plugins");
}
TEST_F(PluginManagerTest, AddPluginsDir_AppendsDirectory) {
@@ -255,25 +247,22 @@ TEST_F(PluginManagerTest, AddPluginsDir_AppendsDirectory) {
PluginManager::addPluginsDir("/tmp/dir2");
PluginManager::addPluginsDir("/tmp/dir3");
ASSERT_EQ(PluginManager::s_plugins_dirs.size(), 3);
EXPECT_EQ(PluginManager::s_plugins_dirs[0].toStdString(), "/tmp/dir1");
EXPECT_EQ(PluginManager::s_plugins_dirs[1].toStdString(), "/tmp/dir2");
EXPECT_EQ(PluginManager::s_plugins_dirs[2].toStdString(), "/tmp/dir3");
ASSERT_EQ(PluginManager::registry().pluginsDirs().size(), 3);
EXPECT_EQ(PluginManager::registry().pluginsDirs()[0].toStdString(), "/tmp/dir1");
EXPECT_EQ(PluginManager::registry().pluginsDirs()[1].toStdString(), "/tmp/dir2");
EXPECT_EQ(PluginManager::registry().pluginsDirs()[2].toStdString(), "/tmp/dir3");
}
TEST_F(PluginManagerTest, GetPluginsDirs_ReturnsEmptyAfterClear) {
PluginManager::setPluginsDir("/tmp/dir1");
clearPluginState();
EXPECT_TRUE(PluginManager::s_plugins_dirs.isEmpty());
EXPECT_TRUE(PluginManager::registry().pluginsDirs().isEmpty());
}
// =============================================================================
// Discovery Tests — fake installed modules
// =============================================================================
// Helper to create a fake installed module directory structure:
// <parentDir>/<moduleName>/manifest.json
// <parentDir>/<moduleName>/<mainFile> (empty fake binary)
static void createFakeModule(const QString& parentDir,
const QString& moduleName,
const QString& mainFile,
@@ -281,7 +270,6 @@ static void createFakeModule(const QString& parentDir,
QDir dir(parentDir);
dir.mkpath(moduleName);
// Write manifest.json
QJsonObject manifest;
manifest["name"] = moduleName;
manifest["version"] = "1.0.0";
@@ -295,7 +283,6 @@ static void createFakeModule(const QString& parentDir,
mf << doc.toJson().toStdString();
mf.close();
// Create a fake binary file (processPlugin will fail on this, which is expected)
QString binaryPath = dir.filePath(moduleName + "/" + mainFile);
std::ofstream bf(binaryPath.toStdString());
bf << "fake";
@@ -309,15 +296,14 @@ TEST_F(PluginManagerTest, DiscoverInstalledModules_DoesNotCrashWithEmptyDir) {
PluginManager::setPluginsDir(tmpDir.path().toUtf8().constData());
PluginManager::discoverInstalledModules();
// No modules to find — known plugins should remain empty
EXPECT_TRUE(PluginManager::s_known_plugins.isEmpty());
EXPECT_TRUE(PluginManager::registry().knownPluginNames().isEmpty());
}
TEST_F(PluginManagerTest, DiscoverInstalledModules_DoesNotCrashWithNonexistentDir) {
PluginManager::setPluginsDir("/tmp/nonexistent_dir_12345");
PluginManager::discoverInstalledModules();
EXPECT_TRUE(PluginManager::s_known_plugins.isEmpty());
EXPECT_TRUE(PluginManager::registry().knownPluginNames().isEmpty());
}
TEST_F(PluginManagerTest, DiscoverInstalledModules_FindsFakeModulesWithoutCrash) {
@@ -330,17 +316,13 @@ TEST_F(PluginManagerTest, DiscoverInstalledModules_FindsFakeModulesWithoutCrash)
PluginManager::setPluginsDir(tmpDir.path().toUtf8().constData());
PluginManager::discoverInstalledModules();
// processPlugin fails for fake binaries, so they won't be in known plugins.
// The important thing is that discovery ran without crashing and the
// scanning pipeline found the manifest.json files.
EXPECT_TRUE(PluginManager::s_known_plugins.isEmpty());
EXPECT_TRUE(PluginManager::registry().knownPluginNames().isEmpty());
}
TEST_F(PluginManagerTest, DiscoverInstalledModules_IgnoresModulesWithoutManifest) {
QTemporaryDir tmpDir;
ASSERT_TRUE(tmpDir.isValid());
// Create a module directory without manifest.json
QDir dir(tmpDir.path());
dir.mkpath("no_manifest_module");
QString binaryPath = dir.filePath("no_manifest_module/plugin.so");
@@ -351,21 +333,19 @@ TEST_F(PluginManagerTest, DiscoverInstalledModules_IgnoresModulesWithoutManifest
PluginManager::setPluginsDir(tmpDir.path().toUtf8().constData());
PluginManager::discoverInstalledModules();
EXPECT_TRUE(PluginManager::s_known_plugins.isEmpty());
EXPECT_TRUE(PluginManager::registry().knownPluginNames().isEmpty());
}
TEST_F(PluginManagerTest, DiscoverInstalledModules_IgnoresUiTypeModules) {
QTemporaryDir tmpDir;
ASSERT_TRUE(tmpDir.isValid());
// Create a UI module — getInstalledModules() only scans for "core" type
createFakeModule(tmpDir.path(), "ui_module", "ui_module_plugin.so", "ui");
PluginManager::setPluginsDir(tmpDir.path().toUtf8().constData());
PluginManager::discoverInstalledModules();
// UI modules are not discovered by getInstalledModules (which filters by "core")
EXPECT_TRUE(PluginManager::s_known_plugins.isEmpty());
EXPECT_TRUE(PluginManager::registry().knownPluginNames().isEmpty());
}
TEST_F(PluginManagerTest, DiscoverInstalledModules_MultipleDirectories) {
@@ -380,20 +360,17 @@ TEST_F(PluginManagerTest, DiscoverInstalledModules_MultipleDirectories) {
PluginManager::setPluginsDir(tmpDir1.path().toUtf8().constData());
PluginManager::addPluginsDir(tmpDir2.path().toUtf8().constData());
ASSERT_EQ(PluginManager::s_plugins_dirs.size(), 2);
ASSERT_EQ(PluginManager::registry().pluginsDirs().size(), 2);
PluginManager::discoverInstalledModules();
// Both directories should be scanned without crash
// (processPlugin still fails for fake binaries)
EXPECT_TRUE(PluginManager::s_known_plugins.isEmpty());
EXPECT_TRUE(PluginManager::registry().knownPluginNames().isEmpty());
}
TEST_F(PluginManagerTest, DiscoverInstalledModules_InvalidManifestJson) {
QTemporaryDir tmpDir;
ASSERT_TRUE(tmpDir.isValid());
// Create a module with invalid JSON in manifest
QDir dir(tmpDir.path());
dir.mkpath("bad_manifest_module");
QString manifestPath = dir.filePath("bad_manifest_module/manifest.json");
@@ -404,5 +381,5 @@ TEST_F(PluginManagerTest, DiscoverInstalledModules_InvalidManifestJson) {
PluginManager::setPluginsDir(tmpDir.path().toUtf8().constData());
PluginManager::discoverInstalledModules();
EXPECT_TRUE(PluginManager::s_known_plugins.isEmpty());
EXPECT_TRUE(PluginManager::registry().knownPluginNames().isEmpty());
}