From 209fa5e0b6d78b9a3a0d3dccbec2be10990e0add Mon Sep 17 00:00:00 2001 From: Sen66 Date: Sun, 8 Feb 2026 23:24:57 +0100 Subject: [PATCH] basic plugin support --- CMakeLists.txt | 15 +- plugins/ProcessMemory/CMakeLists.txt | 38 +++ plugins/ProcessMemory/ProcessMemoryPlugin.cpp | 272 ++++++++++++++++++ plugins/ProcessMemory/ProcessMemoryPlugin.h | 75 +++++ src/controller.cpp | 46 +++ src/editor.cpp | 6 + src/iplugin.h | 130 +++++++++ src/main.cpp | 112 ++++++++ src/pluginmanager.cpp | 194 +++++++++++++ src/pluginmanager.h | 49 ++++ src/processpicker.cpp | 26 ++ src/processpicker.h | 2 + src/providerregistry.cpp | 57 ++++ src/providerregistry.h | 59 ++++ 14 files changed, 1076 insertions(+), 5 deletions(-) create mode 100644 plugins/ProcessMemory/CMakeLists.txt create mode 100644 plugins/ProcessMemory/ProcessMemoryPlugin.cpp create mode 100644 plugins/ProcessMemory/ProcessMemoryPlugin.h create mode 100644 src/iplugin.h create mode 100644 src/pluginmanager.cpp create mode 100644 src/pluginmanager.h create mode 100644 src/providerregistry.cpp create mode 100644 src/providerregistry.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a4a96c7..a74ff00 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,10 @@ add_executable(ReclassX src/core.h src/workspace_model.h src/providers/buffer_provider.h src/providers/null_provider.h src/providers/process_provider.h src/providers/provider.h src/providers/snapshot_provider.h + src/providerregistry.cpp + src/providerregistry.h + src/pluginmanager.cpp + src/pluginmanager.h ) target_include_directories(ReclassX PRIVATE src) @@ -105,7 +109,7 @@ if(BUILD_TESTING) target_link_libraries(test_compose PRIVATE Qt6::Core Qt6::Test) add_test(NAME test_compose COMMAND test_compose) - add_executable(test_editor tests/test_editor.cpp src/editor.cpp src/compose.cpp src/format.cpp) + add_executable(test_editor tests/test_editor.cpp src/editor.cpp src/compose.cpp src/format.cpp src/providerregistry.cpp) target_include_directories(test_editor PRIVATE src) target_link_libraries(test_editor PRIVATE Qt6::Widgets Qt6::PrintSupport Qt6::Test @@ -132,7 +136,7 @@ if(BUILD_TESTING) add_executable(test_controller tests/test_controller.cpp src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp - src/processpicker.cpp src/processpicker.ui) + src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp) target_include_directories(test_controller PRIVATE src) target_link_libraries(test_controller PRIVATE Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test @@ -141,7 +145,7 @@ if(BUILD_TESTING) add_executable(test_validation tests/test_validation.cpp src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp - src/processpicker.cpp src/processpicker.ui) + src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp) target_include_directories(test_validation PRIVATE src) target_link_libraries(test_validation PRIVATE Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test @@ -156,7 +160,7 @@ if(BUILD_TESTING) add_executable(test_context_menu tests/test_context_menu.cpp src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp - src/processpicker.cpp src/processpicker.ui) + src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp) target_include_directories(test_context_menu PRIVATE src) target_link_libraries(test_context_menu PRIVATE Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test @@ -165,10 +169,11 @@ if(BUILD_TESTING) add_executable(test_new_features tests/test_new_features.cpp src/generator.cpp src/compose.cpp src/format.cpp src/controller.cpp - src/editor.cpp src/processpicker.cpp src/processpicker.ui) + src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp) target_include_directories(test_new_features PRIVATE src) target_link_libraries(test_new_features PRIVATE Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test QScintilla::QScintilla dbghelp psapi) add_test(NAME test_new_features COMMAND test_new_features) endif() +add_subdirectory(plugins/ProcessMemory) diff --git a/plugins/ProcessMemory/CMakeLists.txt b/plugins/ProcessMemory/CMakeLists.txt new file mode 100644 index 0000000..b114ee8 --- /dev/null +++ b/plugins/ProcessMemory/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 3.20) +project(ProcessMemoryPlugin LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Find Qt +find_package(Qt6 REQUIRED COMPONENTS Widgets) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +# Plugin sources +set(PLUGIN_SOURCES + ProcessMemoryPlugin.h + ProcessMemoryPlugin.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui +) + +# Create shared library (DLL) +add_library(ProcessMemoryPlugin SHARED ${PLUGIN_SOURCES}) + +# Link Qt +target_link_libraries(ProcessMemoryPlugin PRIVATE Qt6::Widgets) + +# Include directories +target_include_directories(ProcessMemoryPlugin PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../../src +) + +# Output to Plugins folder +set_target_properties(ProcessMemoryPlugin PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" +) diff --git a/plugins/ProcessMemory/ProcessMemoryPlugin.cpp b/plugins/ProcessMemory/ProcessMemoryPlugin.cpp new file mode 100644 index 0000000..1f58a0f --- /dev/null +++ b/plugins/ProcessMemory/ProcessMemoryPlugin.cpp @@ -0,0 +1,272 @@ +#include "ProcessMemoryPlugin.h" +#include "../../src/processpicker.h" +#include +#include +#include +#include +#include +#include + +// ────────────────────────────────────────────────────────────────────────── +// ProcessMemoryProvider implementation +// ────────────────────────────────────────────────────────────────────────── + +ProcessMemoryProvider::ProcessMemoryProvider(DWORD pid, const QString& processName) + : m_handle(nullptr) + , m_pid(pid) + , m_processName(processName) + , m_writable(false) + , m_base(0) +{ + // Try to open with write access first + m_handle = OpenProcess(PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION, + FALSE, pid); + if (m_handle) + m_writable = true; + else + { + // Fall back to read-only + m_handle = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, pid); + m_writable = false; + } + + if (m_handle) + { + cacheModules(); + } +} + +ProcessMemoryProvider::~ProcessMemoryProvider() +{ + if (m_handle) + CloseHandle(m_handle); +} + +bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const +{ + if (!m_handle || len <= 0) return false; + + SIZE_T bytesRead = 0; + if (ReadProcessMemory(m_handle, (LPCVOID)(m_base + addr), buf, (SIZE_T)len, &bytesRead)) + return bytesRead == (SIZE_T)len; + return false; +} + +bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len) +{ + if (!m_handle || !m_writable || len <= 0) return false; + + SIZE_T bytesWritten = 0; + if (WriteProcessMemory(m_handle, (LPVOID)(m_base + addr), buf, (SIZE_T)len, &bytesWritten)) + return bytesWritten == (SIZE_T)len; + return false; +} + +QString ProcessMemoryProvider::getSymbol(uint64_t addr) const +{ + // TODO: Implement module enumeration with EnumProcessModules + // For now, just return empty (no symbol resolution) + Q_UNUSED(addr); + return {}; +} + +void ProcessMemoryProvider::cacheModules() +{ + HMODULE mods[1024]; + DWORD needed = 0; + if (!EnumProcessModulesEx(m_handle, mods, sizeof(mods), + &needed, LIST_MODULES_ALL)) + return; + int count = qMin((int)(needed / sizeof(HMODULE)), 1024); + m_modules.reserve(count); + for (int i = 0; i < count; ++i) + { + MODULEINFO mi{}; + WCHAR modName[MAX_PATH]; + if (GetModuleInformation(m_handle, mods[i], &mi, sizeof(mi)) + && GetModuleBaseNameW(m_handle, mods[i], modName, MAX_PATH)) + { + if ( i == 0 ) + m_base = (uint64_t)mi.lpBaseOfDll; + + m_modules.append({ + QString::fromWCharArray(modName), + (uint64_t)mi.lpBaseOfDll, + (uint64_t)mi.SizeOfImage + }); + } + } +} + +// ────────────────────────────────────────────────────────────────────────── +// ProcessMemoryPlugin implementation +// ────────────────────────────────────────────────────────────────────────── + +QIcon ProcessMemoryPlugin::Icon() const +{ + return qApp->style()->standardIcon(QStyle::SP_ComputerIcon); +} + +bool ProcessMemoryPlugin::canHandle(const QString& target) const +{ + // Target format: "pid:name" or just "pid" + QRegularExpression re("^\\d+"); + return re.match(target).hasMatch(); +} + +std::unique_ptr ProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg) +{ + // Parse target: "pid:name" or just "pid" + QStringList parts = target.split(':'); + bool ok = false; + DWORD pid = parts[0].toUInt(&ok); + + if (!ok || pid == 0) { + if (errorMsg) *errorMsg = "Invalid PID: " + target; + return nullptr; + } + + QString name = parts.size() > 1 ? parts[1] : QString("PID %1").arg(pid); + + auto provider = std::make_unique(pid, name); + if (!provider->isValid()) + { + if (errorMsg) + { + *errorMsg = QString("Failed to open process %1 (PID: %2)\n" + "Ensure the process is running and you have sufficient permissions.") + .arg(name).arg(pid); + } + return nullptr; + } + + return provider; +} + +uint64_t ProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const +{ +#ifdef _WIN32 + // Parse PID from target + QStringList parts = target.split(':'); + bool ok = false; + DWORD pid = parts[0].toUInt(&ok); + if (!ok || pid == 0) return 0; + + // Open process to get main module base + HANDLE hProc = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid); + if (!hProc) return 0; + + uint64_t base = 0; + HMODULE hMod = nullptr; + DWORD needed = 0; + + if (EnumProcessModulesEx(hProc, &hMod, sizeof(hMod), &needed, LIST_MODULES_ALL) && hMod) + { + MODULEINFO mi{}; + if (GetModuleInformation(hProc, hMod, &mi, sizeof(mi))) + { + base = (uint64_t)mi.lpBaseOfDll; + } + } + + CloseHandle(hProc); + return base; +#else + Q_UNUSED(target); + return 0; +#endif +} + +bool ProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target) +{ + // Use custom process enumeration from plugin + QVector pluginProcesses = enumerateProcesses(); + + // Convert to ProcessInfo for ProcessPicker + QList processes; + for (const auto& pinfo : pluginProcesses) + { + ProcessInfo info; + info.pid = pinfo.pid; + info.name = pinfo.name; + info.path = pinfo.path; + info.icon = pinfo.icon; + processes.append(info); + } + + // Show ProcessPicker with custom process list + ProcessPicker picker(processes, parent); + if (picker.exec() == QDialog::Accepted) { + uint32_t pid = picker.selectedProcessId(); + QString name = picker.selectedProcessName(); + + // Format target as "pid:name" + *target = QString("%1:%2").arg(pid).arg(name); + return true; + } + + return false; +} + +QVector ProcessMemoryPlugin::enumerateProcesses() +{ + QVector processes; + +#ifdef _WIN32 + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snapshot == INVALID_HANDLE_VALUE) { + return processes; + } + + PROCESSENTRY32W entry; + entry.dwSize = sizeof(entry); + + if (Process32FirstW(snapshot, &entry)) { + do { + PluginProcessInfo info; + info.pid = entry.th32ProcessID; + info.name = QString::fromWCharArray(entry.szExeFile); + + // Try to get full path and icon + HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, entry.th32ProcessID); + if (hProcess) { + wchar_t path[MAX_PATH * 2]; + DWORD pathLen = sizeof(path) / sizeof(wchar_t); + + // Try QueryFullProcessImageNameW first + if (QueryFullProcessImageNameW(hProcess, 0, path, &pathLen)) { + info.path = QString::fromWCharArray(path); + + // Extract icon + SHFILEINFOW sfi = {}; + if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi), SHGFI_ICON | SHGFI_SMALLICON)) { + if (sfi.hIcon) { + QPixmap pixmap = QPixmap::fromImage(QImage::fromHICON(sfi.hIcon)); + info.icon = QIcon(pixmap); + DestroyIcon(sfi.hIcon); + } + } + } + + CloseHandle(hProcess); + } + + processes.append(info); + + } while (Process32NextW(snapshot, &entry)); + } + + CloseHandle(snapshot); +#endif + + return processes; +} + +// ────────────────────────────────────────────────────────────────────────── +// Plugin factory +// ────────────────────────────────────────────────────────────────────────── + +extern "C" __declspec(dllexport) IPlugin* CreatePlugin() +{ + return new ProcessMemoryPlugin(); +} diff --git a/plugins/ProcessMemory/ProcessMemoryPlugin.h b/plugins/ProcessMemory/ProcessMemoryPlugin.h new file mode 100644 index 0000000..456ea7a --- /dev/null +++ b/plugins/ProcessMemory/ProcessMemoryPlugin.h @@ -0,0 +1,75 @@ +#pragma once +#include "../../src/iplugin.h" +#include "../../src/core.h" +#include +#include +#include +#include + +/** + * Windows process memory provider + * Reads/writes memory from a live process using Win32 API + */ +class ProcessMemoryProvider : public rcx::Provider { +public: + ProcessMemoryProvider(DWORD pid, const QString& processName); + ~ProcessMemoryProvider() override; + + // Required overrides + bool read(uint64_t addr, void* buf, int len) const override; + int size() const override { return m_handle ? INT_MAX : NULL; } // Process memory has no fixed size + + // Optional overrides + bool write(uint64_t addr, const void* buf, int len) override; + bool isWritable() const override { return m_writable; } + QString name() const override { return m_processName; } + QString kind() const override { return QStringLiteral("LocalProcess"); } + QString getSymbol(uint64_t addr) const override; + + // Process-specific helpers + DWORD pid() const { return m_pid; } + uint64_t baseAddress() const { return m_base; } + void refreshModules() { m_modules.clear(); cacheModules(); } + +private: + void cacheModules(); + +private: + HANDLE m_handle; + DWORD m_pid; + QString m_processName; + bool m_writable; + uint64_t m_base; + + struct ModuleInfo { + QString name; + uint64_t base; + uint64_t size; + }; + QVector m_modules; +}; + +/** + * Plugin that provides ProcessMemoryProvider + */ +class ProcessMemoryPlugin : public IProviderPlugin { +public: + std::string Name() const override { return "Process Memory"; } + std::string Version() const override { return "1.0.0"; } + std::string Author() const override { return "ReclassX"; } + std::string Description() const override { return "Read and write memory from local running Windows processes"; } + k_ELoadType LoadType() const override { return k_ELoadTypeAuto; } + QIcon Icon() const override; + + bool canHandle(const QString& target) const override; + std::unique_ptr createProvider(const QString& target, QString* errorMsg) override; + uint64_t getInitialBaseAddress(const QString& target) const override; + bool selectTarget(QWidget* parent, QString* target) override; + + // Optional: provide custom process list + bool providesProcessList() const override { return true; } + QVector enumerateProcesses() override; +}; + +// Plugin export +extern "C" __declspec(dllexport) IPlugin* CreatePlugin(); diff --git a/src/controller.cpp b/src/controller.cpp index 3361072..a6307f6 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -1,5 +1,6 @@ #include "controller.h" #include "providers/process_provider.h" +#include "providerregistry.h" #include "processpicker.h" #include #include @@ -405,6 +406,51 @@ void RcxController::connectEditor(RcxEditor* editor) { } #endif } + else + { + // Look up provider in registry + const auto* providerInfo = ProviderRegistry::instance().findProvider(text.toLower().replace(" ", "")); + + if (providerInfo) { + QString target; + bool selected = false; + + // Execute provider's target selection + if (providerInfo->isBuiltin) { + // Built-in provider with factory function + if (providerInfo->factory) { + selected = providerInfo->factory(qobject_cast(parent()), &target); + } + } else { + // Plugin-based provider + if (providerInfo->plugin) { + selected = providerInfo->plugin->selectTarget(qobject_cast(parent()), &target); + } + } + + if (selected && !target.isEmpty()) { + // Create provider from target + std::unique_ptr provider; + QString errorMsg; + + if (providerInfo->plugin) + { + provider = providerInfo->plugin->createProvider(target, &errorMsg); + } + + // Apply provider or show error + if (provider) { + m_doc->undoStack.clear(); + m_doc->provider = std::move(provider); + m_doc->dataPath.clear(); + emit m_doc->documentChanged(); + refresh(); + } else if (!errorMsg.isEmpty()) { + QMessageBox::warning(qobject_cast(parent()), "Provider Error", errorMsg); + } + } + } + } break; } case EditTarget::ArrayElementType: { diff --git a/src/editor.cpp b/src/editor.cpp index 9325b9e..1f4604d 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -1,4 +1,5 @@ #include "editor.h" +#include "providerregistry.h" #include #include #include @@ -1665,6 +1666,11 @@ void RcxEditor::showSourcePicker() { menu.addAction("file"); menu.addAction("process"); + // Add all registered providers from global registry + const auto& providers = ProviderRegistry::instance().providers(); + for (const auto& provider : providers) + menu.addAction(provider.name); + // Saved sources below separator (with checkmarks) if (!m_savedSourceDisplay.isEmpty()) { menu.addSeparator(); diff --git a/src/iplugin.h b/src/iplugin.h new file mode 100644 index 0000000..2e2ed9f --- /dev/null +++ b/src/iplugin.h @@ -0,0 +1,130 @@ +#pragma once +#include +#include +#include +#include + +// Forward declaration +namespace rcx { class Provider; } + +/** + * Plugin interface for ReclassX + * + * Plugins are loaded from the "Plugins" folder as DLLs. + * Each plugin must export a C function: extern "C" __declspec(dllexport) IPlugin* CreatePlugin(); + */ +class IPlugin { +public: + virtual ~IPlugin() = default; + + // Plugin metadata + virtual std::string Name() const = 0; + virtual std::string Version() const = 0; + virtual std::string Author() const = 0; + virtual std::string Description() const = 0; + virtual QIcon Icon() const { return QIcon(); } + + // Plugin type - determines what functionality it provides + enum k_EType + { + // Provides memory/data sources + ProviderPlugin, + + // In the future we could make plugins that change the main UI + // for loading different data sources + }; + virtual k_EType Type() const = 0; + + // Plugin load type - determines whether and when the plugin is loaded + // by the PluginManager + enum k_ELoadType + { + // Plugin is automatically loaded on startup + k_ELoadTypeAuto, + + // Plugin must be loaded manually via 'Manage Plugins' + k_ELoadTypeManual, + }; + virtual k_ELoadType LoadType() const = 0; +}; + +// Forward declarations +class QWidget; +class QTableWidget; + +/** + * Process information structure for custom process lists + */ +struct PluginProcessInfo { + uint32_t pid; + QString name; + QString path; + QIcon icon; + + PluginProcessInfo() : pid(0) {} + PluginProcessInfo(uint32_t p, const QString& n, const QString& pth = QString(), const QIcon& i = QIcon()) + : pid(p), name(n), path(pth), icon(i) {} +}; + +/** + * Provider plugin interface + * + * Plugins that implement this interface can create Provider instances + * for reading/writing memory from various sources (processes, files, network, etc.) + */ +class IProviderPlugin : public IPlugin { +public: + k_EType Type() const override { return ProviderPlugin; } + + /** + * Check if this plugin can create a provider for the given target + * @param target - Target identifier (e.g., PID for process, path for file) + * @return true if this plugin can handle the target + */ + virtual bool canHandle(const QString& target) const = 0; + + /** + * Create a provider instance + * @param target - Target identifier + * @param errorMsg - Output parameter for error message if creation fails + * @return Provider instance, or nullptr on failure + */ + virtual std::unique_ptr createProvider(const QString& target, QString* errorMsg = nullptr) = 0; + + /** + * Get initial base address for the provider (optional) + * Called after createProvider to set the document's base address + * @param target - Same target identifier passed to createProvider + * @return Initial base address, or 0 if not applicable + */ + virtual uint64_t getInitialBaseAddress(const QString& target) const { Q_UNUSED(target); return 0; } + + /** + * Show a dialog to select a target (e.g., process picker) + * @param parent - Parent widget for dialog + * @param target - Output parameter for selected target + * @return true if user selected a target, false if cancelled + */ + virtual bool selectTarget(QWidget* parent, QString* target) = 0; + + /** + * Get custom process list (optional) + * + * If implemented, this allows the plugin to override the default process enumeration. + * Return an empty list to use the default process picker. + * + * @return List of processes to display, or empty list to use default + */ + virtual QVector enumerateProcesses() { return QVector(); } + + /** + * Check if this plugin wants to override the process list + * @return true if enumerateProcesses() should be called + */ + virtual bool providesProcessList() const { return false; } +}; + +// Plugin factory function signature +typedef IPlugin* (*CreatePluginFunc)(); + +#define IPLUGIN_IID "com.reclassx.IPlugin/1.0" diff --git a/src/main.cpp b/src/main.cpp index adb75a9..76f8835 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,6 @@ #include "controller.h" #include "generator.h" +#include "pluginmanager.h" #include #include #include @@ -27,6 +28,8 @@ #include #include #include +#include +#include #include "workspace_model.h" #include #include @@ -152,6 +155,7 @@ private: QMdiArea* m_mdiArea; QLabel* m_statusLabel; + PluginManager m_pluginManager; struct TabState { RcxDocument* doc; @@ -170,6 +174,7 @@ private: void createMenus(); void createStatusBar(); + void showPluginsDialog(); QIcon makeIcon(const QString& svgPath); RcxController* activeController() const; @@ -205,6 +210,9 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { createMenus(); createStatusBar(); + // Load plugins + m_pluginManager.LoadPlugins(); + connect(m_mdiArea, &QMdiArea::subWindowActivated, this, [this](QMdiSubWindow*) { updateWindowTitle(); @@ -290,6 +298,10 @@ void MainWindow::createMenus() { node->addAction(makeIcon(":/vsicons/edit.svg"), "Re&name", QKeySequence(Qt::Key_F2), this, &MainWindow::renameNodeAction); node->addAction(makeIcon(":/vsicons/files.svg"), "D&uplicate", this, &MainWindow::duplicateNodeAction)->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_D)); + // Plugins + auto* plugins = menuBar()->addMenu("&Plugins"); + plugins->addAction("&Manage Plugins...", this, &MainWindow::showPluginsDialog); + // Help auto* help = menuBar()->addMenu("&Help"); help->addAction(makeIcon(":/vsicons/question.svg"), "&About ReclassX", this, &MainWindow::about); @@ -1014,6 +1026,106 @@ void MainWindow::rebuildWorkspaceModel() { m_workspaceTree->expandAll(); } +void MainWindow::showPluginsDialog() { + QDialog dialog(this); + dialog.setWindowTitle("Plugins"); + dialog.resize(600, 400); + + auto* layout = new QVBoxLayout(&dialog); + + auto* list = new QListWidget(); + layout->addWidget(list); + + auto refreshList = [&]() { + list->clear(); + + // Populate plugin list + for (IPlugin* plugin : m_pluginManager.plugins()) { + QString typeStr; + switch (plugin->Type()) + { + case IPlugin::ProviderPlugin: typeStr = "Provider"; break; + default: typeStr = "Unknown"; break; + } + + QString text = QString("%1 v%2\n %3\n Type: %4\n Author: %5") + .arg(QString::fromStdString(plugin->Name())) + .arg(QString::fromStdString(plugin->Version())) + .arg(QString::fromStdString(plugin->Description())) + .arg(typeStr) + .arg(QString::fromStdString(plugin->Author())); + + auto* item = new QListWidgetItem(plugin->Icon(), text); + item->setData(Qt::UserRole, QString::fromStdString(plugin->Name())); + list->addItem(item); + } + + if (m_pluginManager.plugins().isEmpty()) { + list->addItem("No plugins loaded"); + } + }; + + refreshList(); + + // Button row + auto* btnLayout = new QHBoxLayout(); + + auto* btnLoad = new QPushButton("Load Plugin..."); + connect(btnLoad, &QPushButton::clicked, [&, refreshList]() { + QString path = QFileDialog::getOpenFileName(&dialog, "Load Plugin", + QCoreApplication::applicationDirPath() + "/Plugins", + "Plugins (*.dll *.so *.dylib);;All Files (*)"); + + if (!path.isEmpty()) { + if (m_pluginManager.LoadPluginFromPath(path)) { + refreshList(); + m_statusLabel->setText("Plugin loaded successfully"); + } else { + QMessageBox::warning(&dialog, "Failed to Load Plugin", + "Could not load the selected plugin.\nCheck the console for details."); + } + } + }); + + auto* btnUnload = new QPushButton("Unload Selected"); + connect(btnUnload, &QPushButton::clicked, [&, list, refreshList]() { + auto* item = list->currentItem(); + if (!item) { + QMessageBox::information(&dialog, "No Selection", "Please select a plugin to unload."); + return; + } + + QString pluginName = item->data(Qt::UserRole).toString(); + if (pluginName.isEmpty()) return; + + auto reply = QMessageBox::question(&dialog, "Unload Plugin", + QString("Are you sure you want to unload '%1'?").arg(pluginName), + QMessageBox::Yes | QMessageBox::No); + + if (reply == QMessageBox::Yes) { + if (m_pluginManager.UnloadPlugin(pluginName)) { + refreshList(); + m_statusLabel->setText("Plugin unloaded"); + } else { + QMessageBox::warning(&dialog, "Failed to Unload", + "Could not unload the selected plugin."); + } + } + }); + + auto* btnClose = new QPushButton("Close"); + connect(btnClose, &QPushButton::clicked, &dialog, &QDialog::accept); + + btnLayout->addWidget(btnLoad); + btnLayout->addWidget(btnUnload); + btnLayout->addStretch(); + btnLayout->addWidget(btnClose); + + layout->addLayout(btnLayout); + + dialog.exec(); +} + } // namespace rcx // ── Entry point ── diff --git a/src/pluginmanager.cpp b/src/pluginmanager.cpp new file mode 100644 index 0000000..23d351f --- /dev/null +++ b/src/pluginmanager.cpp @@ -0,0 +1,194 @@ +#include "pluginmanager.h" +#include "providerregistry.h" +#include +#include +#include +#include + +PluginManager::~PluginManager() +{ + UnloadPlugins(); +} + +void PluginManager::LoadPlugins() +{ + // Get the Plugins directory relative to the executable + QString appDir = QCoreApplication::applicationDirPath(); + QString pluginsDir = appDir + "/Plugins"; + + QDir dir(pluginsDir); + if (!dir.exists()) + { + qWarning() << "PluginManager: Plugins directory not found:" << pluginsDir; + return; + } + + // Find all DLL files + QStringList filters; +#ifdef _WIN32 + filters << "*.dll"; +#elif defined(__APPLE__) + filters << "*.dylib"; +#else + filters << "*.so"; +#endif + + dir.setNameFilters(filters); + QFileInfoList files = dir.entryInfoList(QDir::Files); + + qDebug() << "PluginManager: Scanning for plugins in:" << pluginsDir; + qDebug() << "PluginManager: Found" << files.count() << "potential plugin(s)"; + + for (const QFileInfo& fileInfo : files) + { + LoadPlugin(fileInfo.absoluteFilePath()); + } + + qDebug() << "PluginManager: Loaded" << m_plugins.count() << "plugin(s)"; +} + +bool PluginManager::LoadPlugin(const QString& path) +{ + QLibrary* library = new QLibrary(path); + + // Load the library + if (!library->load()) + { + qWarning() << "PluginManager: Failed to load plugin:" << path; + qWarning() << "PluginManager: Error" << library->errorString(); + delete library; + return false; + } + + // Resolve the CreatePlugin function + CreatePluginFunc CreateFunc = (CreatePluginFunc)library->resolve("CreatePlugin"); + if (!CreateFunc) + { + qWarning() << "PluginManager: Plugin" << path << "does not export CreatePlugin()"; + library->unload(); + delete library; + return false; + } + + // Create plugin instance + IPlugin* plugin = CreateFunc(); + if (!plugin) + { + qWarning() << "PluginManager: CreatePlugin() returned nullptr for" << path; + library->unload(); + delete library; + return false; + } + + qDebug() << "PluginManager: Loaded plugin:" << plugin->Name() << plugin->Version() << "by" << plugin->Author(); + + // Store plugin entry + m_entries.append({library, plugin}); + m_plugins.append(plugin); + + // Auto-register providers in global registry + if (plugin->Type() == IPlugin::ProviderPlugin) + { + IProviderPlugin* provider = static_cast(plugin); + QString name = QString::fromStdString(plugin->Name()); + QString identifier = name.toLower().replace(" ", ""); + ProviderRegistry::instance().registerProvider(name, identifier, provider); + } + + return true; +} + +QVector PluginManager::providerPlugins() const +{ + QVector result; + for (IPlugin* plugin : m_plugins) + { + if (plugin->Type() == IPlugin::ProviderPlugin) + { + result.append(static_cast(plugin)); + } + } + return result; +} + +IPlugin* PluginManager::FindPlugin(const QString& name) const +{ + for (IPlugin* plugin : m_plugins) + { + if (QString::fromStdString(plugin->Name()) == name) + { + return plugin; + } + } + return nullptr; +} + +bool PluginManager::LoadPluginFromPath(const QString& path) +{ + // Check if already loaded + QFileInfo fileInfo(path); + QString fileName = fileInfo.fileName(); + + for (const auto& entry : m_entries) + { + if (entry.library->fileName().endsWith(fileName)) + { + qWarning() << "PluginManager: Plugin already loaded:" << fileName; + return false; + } + } + + return LoadPlugin(path); +} + +bool PluginManager::UnloadPlugin(const QString& name) +{ + for (int i = 0; i < m_entries.size(); ++i) + { + if (QString::fromStdString(m_entries[i].plugin->Name()) == name) + { + qDebug() << "PluginManager: Unloading plugin:" << name; + + IPlugin* plugin = m_entries[i].plugin; + + // Unregister provider from global registry + if (plugin->Type() == IPlugin::ProviderPlugin) + { + QString identifier = name.toLower().replace(" ", ""); + ProviderRegistry::instance().unregisterProvider(identifier); + } + + // Delete plugin instance + delete plugin; + + // Unload library + m_entries[i].library->unload(); + delete m_entries[i].library; + + // Remove from lists + m_entries.remove(i); + m_plugins.remove(i); + + return true; + } + } + + qWarning() << "PluginManager: Plugin not found:" << name; + return false; +} + +void PluginManager::UnloadPlugins() +{ + // Clear provider registry + ProviderRegistry::instance().clear(); + + // Delete plugin instances and unload libraries + for (int i = 0; i < m_entries.size(); ++i) { + delete m_entries[i].plugin; + m_entries[i].library->unload(); + delete m_entries[i].library; + } + + m_entries.clear(); + m_plugins.clear(); +} diff --git a/src/pluginmanager.h b/src/pluginmanager.h new file mode 100644 index 0000000..e7fb376 --- /dev/null +++ b/src/pluginmanager.h @@ -0,0 +1,49 @@ +#pragma once +#include "iplugin.h" +#include +#include +#include +#include + +/** + * Manages plugin loading and lifecycle + */ +class PluginManager +{ +public: + PluginManager() = default; + ~PluginManager(); + + // Load plugins from the "Plugins" folder + void LoadPlugins(); + + // Get all loaded plugins + const QVector& plugins() const { return m_plugins; } + + // Get plugins of a specific type + QVector providerPlugins() const; + + // Find plugin by name + IPlugin* FindPlugin(const QString& name) const; + + // Load a single plugin from path + bool LoadPluginFromPath(const QString& path); + + // Unload a specific plugin by name + bool UnloadPlugin(const QString& name); + + // Unload all plugins + void UnloadPlugins(); + +private: + struct PluginEntry + { + QLibrary* library; + IPlugin* plugin; + }; + + QVector m_entries; + QVector m_plugins; // Non-owning pointers for quick access + + bool LoadPlugin(const QString& path); +}; diff --git a/src/processpicker.cpp b/src/processpicker.cpp index d314c32..54704fd 100644 --- a/src/processpicker.cpp +++ b/src/processpicker.cpp @@ -16,6 +16,7 @@ ProcessPicker::ProcessPicker(QWidget *parent) : QDialog(parent) , ui(new Ui::ProcessPicker) + , m_useCustomList(false) { ui->setupUi(this); @@ -36,6 +37,31 @@ ProcessPicker::ProcessPicker(QWidget *parent) refreshProcessList(); } +ProcessPicker::ProcessPicker(const QList& customProcesses, QWidget *parent) + : QDialog(parent) + , ui(new Ui::ProcessPicker) + , m_useCustomList(true) +{ + ui->setupUi(this); + + // Configure table + ui->processTable->setColumnWidth(0, 80); + ui->processTable->setColumnWidth(1, 200); + ui->processTable->horizontalHeader()->setStretchLastSection(true); + ui->processTable->setWordWrap(false); + ui->processTable->setTextElideMode(Qt::ElideLeft); + + // Connect signals (no refresh button for custom lists) + ui->refreshButton->setVisible(false); + connect(ui->processTable, &QTableWidget::itemDoubleClicked, this, &ProcessPicker::onProcessSelected); + connect(ui->filterEdit, &QLineEdit::textChanged, this, &ProcessPicker::filterProcesses); + connect(ui->attachButton, &QPushButton::clicked, this, &ProcessPicker::onProcessSelected); + + // Use custom process list + m_allProcesses = customProcesses; + applyFilter(); +} + ProcessPicker::~ProcessPicker() { delete ui; diff --git a/src/processpicker.h b/src/processpicker.h index ea904d1..dc13232 100644 --- a/src/processpicker.h +++ b/src/processpicker.h @@ -22,6 +22,7 @@ class ProcessPicker : public QDialog public: explicit ProcessPicker(QWidget *parent = nullptr); + explicit ProcessPicker(const QList& customProcesses, QWidget *parent = nullptr); ~ProcessPicker(); uint32_t selectedProcessId() const; @@ -41,6 +42,7 @@ private: uint32_t m_selectedPid = 0; QString m_selectedName; QList m_allProcesses; + bool m_useCustomList = false; }; #endif // PROCESSPICKER_H diff --git a/src/providerregistry.cpp b/src/providerregistry.cpp new file mode 100644 index 0000000..e041f99 --- /dev/null +++ b/src/providerregistry.cpp @@ -0,0 +1,57 @@ +#include "providerregistry.h" +#include + +ProviderRegistry& ProviderRegistry::instance() { + static ProviderRegistry s_instance; + return s_instance; +} + +void ProviderRegistry::registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin) { + // Check if already registered + for (const auto& info : m_providers) { + if (info.identifier == identifier) { + qWarning() << "ProviderRegistry: Provider already registered:" << identifier; + return; + } + } + + m_providers.append(ProviderInfo(name, identifier, plugin)); + qDebug() << "ProviderRegistry: Registered plugin provider:" << name << "(" << identifier << ")"; +} + +void ProviderRegistry::registerBuiltinProvider(const QString& name, const QString& identifier, BuiltinFactory factory) { + // Check if already registered + for (const auto& info : m_providers) { + if (info.identifier == identifier) { + qWarning() << "ProviderRegistry: Provider already registered:" << identifier; + return; + } + } + + m_providers.append(ProviderInfo(name, identifier, factory)); + qDebug() << "ProviderRegistry: Registered builtin provider:" << name << "(" << identifier << ")"; +} + +void ProviderRegistry::unregisterProvider(const QString& identifier) { + for (int i = 0; i < m_providers.size(); ++i) { + if (m_providers[i].identifier == identifier) { + qDebug() << "ProviderRegistry: Unregistered provider:" << identifier; + m_providers.remove(i); + return; + } + } + qWarning() << "ProviderRegistry: Provider not found:" << identifier; +} + +const ProviderRegistry::ProviderInfo* ProviderRegistry::findProvider(const QString& identifier) const { + for (const auto& info : m_providers) { + if (info.identifier == identifier) { + return &info; + } + } + return nullptr; +} + +void ProviderRegistry::clear() { + m_providers.clear(); +} diff --git a/src/providerregistry.h b/src/providerregistry.h new file mode 100644 index 0000000..3d9a957 --- /dev/null +++ b/src/providerregistry.h @@ -0,0 +1,59 @@ +#pragma once +#include "iplugin.h" +#include +#include +#include + +// Forward declarations +namespace rcx { class Provider; } +class QWidget; + +/** + * Global registry for data source providers + * + * Providers register themselves here so they can be listed in the Source picker. + * Supports both plugin-based providers and built-in providers. + */ +class ProviderRegistry { +public: + // Factory function for creating built-in providers + using BuiltinFactory = std::function; + + struct ProviderInfo { + QString name; // Display name (e.g., "Process Memory") + QString identifier; // Unique ID (e.g., "process") + IProviderPlugin* plugin; // Plugin (if plugin-based) + BuiltinFactory factory; // Factory (if built-in) + bool isBuiltin; + + ProviderInfo(const QString& n, const QString& id, IProviderPlugin* p) + : name(n), identifier(id), plugin(p), factory(nullptr), isBuiltin(false) {} + + ProviderInfo(const QString& n, const QString& id, BuiltinFactory f) + : name(n), identifier(id), plugin(nullptr), factory(f), isBuiltin(true) {} + }; + + static ProviderRegistry& instance(); + + // Register a plugin-based provider + void registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin); + + // Register a built-in provider with a factory function + void registerBuiltinProvider(const QString& name, const QString& identifier, BuiltinFactory factory); + + // Unregister a provider (called when unloading plugins) + void unregisterProvider(const QString& identifier); + + // Get all registered providers + const QVector& providers() const { return m_providers; } + + // Find provider by identifier + const ProviderInfo* findProvider(const QString& identifier) const; + + // Clear all providers + void clear(); + +private: + ProviderRegistry() = default; + QVector m_providers; +};