From f4854404eef49a425d482ff98ed93f8928a7e4cc Mon Sep 17 00:00:00 2001 From: Iuri Matias Date: Fri, 27 Mar 2026 11:17:47 -0400 Subject: [PATCH] separate plugin manaager into separatge files with better defined responsibility --- src/CMakeLists.txt | 6 + src/logos_core/dependency_resolver.cpp | 105 ++++++ src/logos_core/dependency_resolver.h | 19 + src/logos_core/plugin_loader.cpp | 252 +++++++++++++ src/logos_core/plugin_loader.h | 27 ++ src/logos_core/plugin_manager.cpp | 485 ++++--------------------- src/logos_core/plugin_manager.h | 4 + src/logos_core/plugin_registry.cpp | 134 +++++++ src/logos_core/plugin_registry.h | 40 ++ tests/test_app_lifecycle.cpp | 42 +-- tests/test_plugin_manager.cpp | 105 +++--- 11 files changed, 707 insertions(+), 512 deletions(-) create mode 100644 src/logos_core/dependency_resolver.cpp create mode 100644 src/logos_core/dependency_resolver.h create mode 100644 src/logos_core/plugin_loader.cpp create mode 100644 src/logos_core/plugin_loader.h create mode 100644 src/logos_core/plugin_registry.cpp create mode 100644 src/logos_core/plugin_registry.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2387a3a..8ee1ae5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/logos_core/dependency_resolver.cpp b/src/logos_core/dependency_resolver.cpp new file mode 100644 index 0000000..31b62cd --- /dev/null +++ b/src/logos_core/dependency_resolver.cpp @@ -0,0 +1,105 @@ +#include "dependency_resolver.h" +#include +#include +#include +#include + +namespace DependencyResolver { + + QStringList resolve(const QStringList& requested, + IsKnownFn isKnown, + GetMetadataFn getMetadata) { + qDebug() << "Resolving dependencies for modules:" << requested; + + QSet 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 dependents; + QHash 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; + } + +} diff --git a/src/logos_core/dependency_resolver.h b/src/logos_core/dependency_resolver.h new file mode 100644 index 0000000..8edcbe0 --- /dev/null +++ b/src/logos_core/dependency_resolver.h @@ -0,0 +1,19 @@ +#ifndef DEPENDENCY_RESOLVER_H +#define DEPENDENCY_RESOLVER_H + +#include +#include +#include +#include + +namespace DependencyResolver { + + using IsKnownFn = std::function; + using GetMetadataFn = std::function; + + QStringList resolve(const QStringList& requested, + IsKnownFn isKnown, + GetMetadataFn getMetadata); +} + +#endif // DEPENDENCY_RESOLVER_H diff --git a/src/logos_core/plugin_loader.cpp b/src/logos_core/plugin_loader.cpp new file mode 100644 index 0000000..7c86e8a --- /dev/null +++ b/src/logos_core/plugin_loader.cpp @@ -0,0 +1,252 @@ +#include "plugin_loader.h" +#include "plugin_registry.h" +#include "dependency_resolver.h" +#include "qt/qt_process_manager.h" +#include +#include +#include +#include +#include +#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 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 PluginLoader::getPluginProcessIds() { + auto stdMap = QtProcessManager::getAllProcessIds(); + QHash result; + for (const auto& [name, pid] : stdMap) { + result.insert(QString::fromStdString(name), pid); + } + return result; +} diff --git a/src/logos_core/plugin_loader.h b/src/logos_core/plugin_loader.h new file mode 100644 index 0000000..9f3967e --- /dev/null +++ b/src/logos_core/plugin_loader.h @@ -0,0 +1,27 @@ +#ifndef PLUGIN_LOADER_H +#define PLUGIN_LOADER_H + +#include +#include + +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 getPluginProcessIds(); + +private: + QString resolveLogosHostPath(); + void notifyCapabilityModule(const QString& name, const QString& token); + + PluginRegistry& m_registry; +}; + +#endif // PLUGIN_LOADER_H diff --git a/src/logos_core/plugin_manager.cpp b/src/logos_core/plugin_manager.cpp index 7af389c..efeb597 100644 --- a/src/logos_core/plugin_manager.cpp +++ b/src/logos_core/plugin_manager.cpp @@ -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 -#include -#include -#include -#include -#include -#include -#include #include -#include "logos_api.h" -#include "logos_api_client.h" -#include "token_manager.h" -#include "logos_mode.h" -#include -#include +#include -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 s_known_plugins; - QHash 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 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 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 dependents; - QHash 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 getPluginProcessIds() { - auto stdMap = QtProcessManager::getAllProcessIds(); - QHash 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); } + ); } } diff --git a/src/logos_core/plugin_manager.h b/src/logos_core/plugin_manager.h index a2948c6..4d8af93 100644 --- a/src/logos_core/plugin_manager.h +++ b/src/logos_core/plugin_manager.h @@ -6,7 +6,11 @@ #include #include +class PluginRegistry; + namespace PluginManager { + PluginRegistry& registry(); + void setPluginsDir(const char* plugins_dir); void addPluginsDir(const char* plugins_dir); diff --git a/src/logos_core/plugin_registry.cpp b/src/logos_core/plugin_registry.cpp new file mode 100644 index 0000000..f8a82ce --- /dev/null +++ b/src/logos_core/plugin_registry.cpp @@ -0,0 +1,134 @@ +#include "plugin_registry.h" +#include +#include +#include +#include +#include +#include +#include + +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(); +} diff --git a/src/logos_core/plugin_registry.h b/src/logos_core/plugin_registry.h new file mode 100644 index 0000000..af04fad --- /dev/null +++ b/src/logos_core/plugin_registry.h @@ -0,0 +1,40 @@ +#ifndef PLUGIN_REGISTRY_H +#define PLUGIN_REGISTRY_H + +#include +#include +#include +#include + +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 m_knownPlugins; + QHash m_pluginMetadata; + QStringList m_loadedPlugins; +}; + +#endif // PLUGIN_REGISTRY_H diff --git a/tests/test_app_lifecycle.cpp b/tests/test_app_lifecycle.cpp index 6a5f3d2..f68709f 100644 --- a/tests/test_app_lifecycle.cpp +++ b/tests/test_app_lifecycle.cpp @@ -1,21 +1,13 @@ #include #include "app_lifecycle.h" #include "plugin_manager.h" +#include "plugin_registry.h" #include #include -namespace PluginManager { - extern QStringList s_plugins_dirs; - extern QStringList s_loaded_plugins; - extern QHash s_known_plugins; - extern QHash 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"); } // ============================================================================= diff --git a/tests/test_plugin_manager.cpp b/tests/test_plugin_manager.cpp index ea595ef..11bc5d7 100644 --- a/tests/test_plugin_manager.cpp +++ b/tests/test_plugin_manager.cpp @@ -1,5 +1,6 @@ #include #include "plugin_manager.h" +#include "plugin_registry.h" #include "logos_core.h" #include #include @@ -10,18 +11,9 @@ #include #include -namespace PluginManager { - extern QStringList s_plugins_dirs; - extern QStringList s_loaded_plugins; - extern QHash s_known_plugins; - extern QHash 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: -// //manifest.json -// // (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()); }