mirror of
https://github.com/logos-co/logos-liblogos.git
synced 2026-08-27 12:51:10 +00:00
separate plugin manaager into separatge files with better defined responsibility
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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); }
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user