From c856ba26971802c2a28525caa45d04fb804455aa Mon Sep 17 00:00:00 2001 From: IChooseYou Date: Sat, 14 Feb 2026 13:40:58 -0700 Subject: [PATCH] WinDbg plugin, ProcessMemoryWindows, dialog cleanup, and misc fixes - Add WinDbgMemory plugin with debug server connection support - Replace ProcessMemory plugin with Windows-specific ProcessMemoryWindows - Simplify WinDbg dialog: single panel, no tabs, palette-based theming - Fix example text visibility on dark themes (QPalette::Dark -> Disabled WindowText) - Fix "file" -> "File" capitalization in source menu - Add windbg_provider and com_security tests --- CMakeLists.txt | 21 +- .../CMakeLists.txt | 18 +- .../ProcessMemoryWindowsPlugin.cpp} | 46 +- .../ProcessMemoryWindowsPlugin.h} | 18 +- plugins/WinDbgMemory/CMakeLists.txt | 34 ++ plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp | 510 ++++++++++++++++++ plugins/WinDbgMemory/WinDbgMemoryPlugin.h | 122 +++++ src/compose.cpp | 9 +- src/controller.cpp | 175 ++++-- src/controller.h | 9 +- src/editor.cpp | 19 +- src/main.cpp | 2 +- src/providers/snapshot_provider.h | 92 +++- src/titlebar.cpp | 1 + tests/test_com_security.cpp | 185 +++++++ tests/test_dbgconnect.cpp | 65 +++ tests/test_theme.cpp | 11 +- tests/test_windbg_provider.cpp | 463 ++++++++++++++++ 18 files changed, 1692 insertions(+), 108 deletions(-) rename plugins/{ProcessMemory => ProcessMemoryWindows}/CMakeLists.txt (61%) rename plugins/{ProcessMemory/ProcessMemoryPlugin.cpp => ProcessMemoryWindows/ProcessMemoryWindowsPlugin.cpp} (90%) rename plugins/{ProcessMemory/ProcessMemoryPlugin.h => ProcessMemoryWindows/ProcessMemoryWindowsPlugin.h} (80%) create mode 100644 plugins/WinDbgMemory/CMakeLists.txt create mode 100644 plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp create mode 100644 plugins/WinDbgMemory/WinDbgMemoryPlugin.h create mode 100644 tests/test_com_security.cpp create mode 100644 tests/test_dbgconnect.cpp create mode 100644 tests/test_windbg_provider.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f22188..75dc4fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -257,6 +257,24 @@ if(BUILD_TESTING) target_link_libraries(test_theme PRIVATE ${QT}::Widgets ${QT}::Test) add_test(NAME test_theme COMMAND test_theme) + add_executable(test_windbg_provider tests/test_windbg_provider.cpp + plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp) + target_include_directories(test_windbg_provider PRIVATE src plugins/WinDbgMemory) + target_link_libraries(test_windbg_provider PRIVATE + ${QT}::Widgets ${QT}::Concurrent ${QT}::Test) + if(WIN32) + target_link_libraries(test_windbg_provider PRIVATE dbgeng ole32) + endif() + add_test(NAME test_windbg_provider COMMAND test_windbg_provider) + + # Standalone test: proves whether CoInitializeSecurity is needed for DebugConnect + # Requires a running WinDbg debug server on port 5055 + if(WIN32) + add_executable(test_com_security tests/test_com_security.cpp) + target_link_libraries(test_com_security PRIVATE dbgeng ole32 version) + add_test(NAME test_com_security COMMAND test_com_security) + endif() + # Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe # that links the broadest set of Qt modules; all test exes share the same output dir) if(TARGET ${QT}::windeployqt) @@ -270,4 +288,5 @@ if(BUILD_TESTING) ) endif() endif() -add_subdirectory(plugins/ProcessMemory) +add_subdirectory(plugins/ProcessMemoryWindows) +add_subdirectory(plugins/WinDbgMemory) diff --git a/plugins/ProcessMemory/CMakeLists.txt b/plugins/ProcessMemoryWindows/CMakeLists.txt similarity index 61% rename from plugins/ProcessMemory/CMakeLists.txt rename to plugins/ProcessMemoryWindows/CMakeLists.txt index 4bd2e9b..c28ea3b 100644 --- a/plugins/ProcessMemory/CMakeLists.txt +++ b/plugins/ProcessMemoryWindows/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.20) -project(ProcessMemoryPlugin LANGUAGES CXX) +project(ProcessMemoryWindowsPlugin LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -12,36 +12,36 @@ set(CMAKE_AUTOUIC ON) # Plugin sources set(PLUGIN_SOURCES - ProcessMemoryPlugin.h - ProcessMemoryPlugin.cpp + ProcessMemoryWindowsPlugin.h + ProcessMemoryWindowsPlugin.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}) +add_library(ProcessMemoryWindowsPlugin SHARED ${PLUGIN_SOURCES}) # Link Qt -target_link_libraries(ProcessMemoryPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS}) +target_link_libraries(ProcessMemoryWindowsPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS}) # Platform-specific linking if(WIN32) - target_link_libraries(ProcessMemoryPlugin PRIVATE psapi shell32) + target_link_libraries(ProcessMemoryWindowsPlugin PRIVATE psapi shell32) endif() # On Linux, hide all symbols by default so only RCX_PLUGIN_EXPORT-marked ones are exported if(UNIX AND NOT APPLE) - target_compile_options(ProcessMemoryPlugin PRIVATE -fvisibility=hidden) + target_compile_options(ProcessMemoryWindowsPlugin PRIVATE -fvisibility=hidden) endif() # Include directories -target_include_directories(ProcessMemoryPlugin PRIVATE +target_include_directories(ProcessMemoryWindowsPlugin PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../src ) # Output to Plugins folder -set_target_properties(ProcessMemoryPlugin PROPERTIES +set_target_properties(ProcessMemoryWindowsPlugin PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" ) diff --git a/plugins/ProcessMemory/ProcessMemoryPlugin.cpp b/plugins/ProcessMemoryWindows/ProcessMemoryWindowsPlugin.cpp similarity index 90% rename from plugins/ProcessMemory/ProcessMemoryPlugin.cpp rename to plugins/ProcessMemoryWindows/ProcessMemoryWindowsPlugin.cpp index 81dd366..4ff2c4f 100644 --- a/plugins/ProcessMemory/ProcessMemoryPlugin.cpp +++ b/plugins/ProcessMemoryWindows/ProcessMemoryWindowsPlugin.cpp @@ -1,4 +1,4 @@ -#include "ProcessMemoryPlugin.h" +#include "ProcessMemoryWindowsPlugin.h" #include "../../src/processpicker.h" @@ -32,12 +32,12 @@ #endif // ────────────────────────────────────────────────────────────────────────── -// ProcessMemoryProvider implementation +// ProcessMemoryWindowsProvider implementation // ────────────────────────────────────────────────────────────────────────── #ifdef _WIN32 -ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName) +ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName) : m_handle(nullptr) , m_pid(pid) , m_processName(processName) @@ -60,7 +60,7 @@ ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& proces cacheModules(); } -bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const +bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const { if (!m_handle || len <= 0) return false; @@ -71,7 +71,7 @@ bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const return bytesRead > 0; } -bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len) +bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len) { if (!m_handle || !m_writable || len <= 0) return false; @@ -81,7 +81,7 @@ bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len) return false; } -QString ProcessMemoryProvider::getSymbol(uint64_t addr) const +QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const { for (const auto& mod : m_modules) { @@ -96,7 +96,7 @@ QString ProcessMemoryProvider::getSymbol(uint64_t addr) const return {}; } -void ProcessMemoryProvider::cacheModules() +void ProcessMemoryWindowsProvider::cacheModules() { HMODULE mods[1024]; DWORD needed = 0; @@ -126,7 +126,7 @@ void ProcessMemoryProvider::cacheModules() #elif defined(__linux__) -ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName) +ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName) : m_fd(-1) , m_pid(pid) , m_processName(processName) @@ -152,7 +152,7 @@ ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& proces } -bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const +bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const { if (m_fd < 0 || len <= 0) return false; @@ -176,7 +176,7 @@ bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const return nread == static_cast(len); } -bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len) +bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len) { if (m_fd < 0 || !m_writable || len <= 0) return false; @@ -200,7 +200,7 @@ bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len) return nwritten == static_cast(len); } -QString ProcessMemoryProvider::getSymbol(uint64_t addr) const +QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const { for (const auto& mod : m_modules) { @@ -215,7 +215,7 @@ QString ProcessMemoryProvider::getSymbol(uint64_t addr) const return {}; } -void ProcessMemoryProvider::cacheModules() +void ProcessMemoryWindowsProvider::cacheModules() { // Parse /proc//maps to discover loaded modules QString mapsPath = QStringLiteral("/proc/%1/maps").arg(m_pid); @@ -288,7 +288,7 @@ void ProcessMemoryProvider::cacheModules() #endif // platform -ProcessMemoryProvider::~ProcessMemoryProvider() +ProcessMemoryWindowsProvider::~ProcessMemoryWindowsProvider() { #ifdef _WIN32 if (m_handle) @@ -299,7 +299,7 @@ ProcessMemoryProvider::~ProcessMemoryProvider() #endif } -int ProcessMemoryProvider::size() const +int ProcessMemoryWindowsProvider::size() const { #ifdef _WIN32 return m_handle ? 0x10000 : 0; @@ -309,22 +309,22 @@ int ProcessMemoryProvider::size() const } // ────────────────────────────────────────────────────────────────────────── -// ProcessMemoryPlugin implementation +// ProcessMemoryWindowsPlugin implementation // ────────────────────────────────────────────────────────────────────────── -QIcon ProcessMemoryPlugin::Icon() const +QIcon ProcessMemoryWindowsPlugin::Icon() const { return qApp->style()->standardIcon(QStyle::SP_ComputerIcon); } -bool ProcessMemoryPlugin::canHandle(const QString& target) const +bool ProcessMemoryWindowsPlugin::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) +std::unique_ptr ProcessMemoryWindowsPlugin::createProvider(const QString& target, QString* errorMsg) { // Parse target: "pid:name" or just "pid" QStringList parts = target.split(':'); @@ -339,7 +339,7 @@ std::unique_ptr ProcessMemoryPlugin::createProvider(const QString QString name = parts.size() > 1 ? parts[1] : QString("PID %1").arg(pid); - auto provider = std::make_unique(pid, name); + auto provider = std::make_unique(pid, name); if (!provider->isValid()) { if (errorMsg) @@ -352,7 +352,7 @@ std::unique_ptr ProcessMemoryPlugin::createProvider(const QString return provider; } -uint64_t ProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const +uint64_t ProcessMemoryWindowsPlugin::getInitialBaseAddress(const QString& target) const { #ifdef _WIN32 // Parse PID from target @@ -409,7 +409,7 @@ uint64_t ProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const #endif } -bool ProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target) +bool ProcessMemoryWindowsPlugin::selectTarget(QWidget* parent, QString* target) { // Use custom process enumeration from plugin QVector pluginProcesses = enumerateProcesses(); @@ -440,7 +440,7 @@ bool ProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target) return false; } -QVector ProcessMemoryPlugin::enumerateProcesses() +QVector ProcessMemoryWindowsPlugin::enumerateProcesses() { QVector processes; @@ -543,5 +543,5 @@ QVector ProcessMemoryPlugin::enumerateProcesses() extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin() { - return new ProcessMemoryPlugin(); + return new ProcessMemoryWindowsPlugin(); } diff --git a/plugins/ProcessMemory/ProcessMemoryPlugin.h b/plugins/ProcessMemoryWindows/ProcessMemoryWindowsPlugin.h similarity index 80% rename from plugins/ProcessMemory/ProcessMemoryPlugin.h rename to plugins/ProcessMemoryWindows/ProcessMemoryWindowsPlugin.h index 5a5fa74..0394afd 100644 --- a/plugins/ProcessMemory/ProcessMemoryPlugin.h +++ b/plugins/ProcessMemoryWindows/ProcessMemoryWindowsPlugin.h @@ -5,14 +5,14 @@ #include /** - * Process memory provider - * Reads/writes memory from a live process using platform APIs + * Process memory provider (Windows) + * Reads/writes memory from a live process using Windows platform APIs */ -class ProcessMemoryProvider : public rcx::Provider +class ProcessMemoryWindowsProvider : public rcx::Provider { public: - ProcessMemoryProvider(uint32_t pid, const QString& processName); - ~ProcessMemoryProvider() override; + ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName); + ~ProcessMemoryWindowsProvider() override; // Required overrides bool read(uint64_t addr, void* buf, int len) const override; @@ -57,15 +57,15 @@ private: }; /** - * Plugin that provides ProcessMemoryProvider + * Plugin that provides ProcessMemoryWindowsProvider */ -class ProcessMemoryPlugin : public IProviderPlugin +class ProcessMemoryWindowsPlugin : public IProviderPlugin { public: - std::string Name() const override { return "Process Memory"; } + std::string Name() const override { return "Process Memory Windows"; } std::string Version() const override { return "1.0.0"; } std::string Author() const override { return "Reclass"; } - std::string Description() const override { return "Read and write memory from local running processes"; } + std::string Description() const override { return "Read and write memory from local running processes (Windows)"; } k_ELoadType LoadType() const override { return k_ELoadTypeAuto; } QIcon Icon() const override; diff --git a/plugins/WinDbgMemory/CMakeLists.txt b/plugins/WinDbgMemory/CMakeLists.txt new file mode 100644 index 0000000..eb3ffc0 --- /dev/null +++ b/plugins/WinDbgMemory/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.20) +project(WinDbgMemoryPlugin LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Qt is found by the parent project; QT variable (Qt5 or Qt6) is inherited + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +# Plugin sources +set(PLUGIN_SOURCES + WinDbgMemoryPlugin.h + WinDbgMemoryPlugin.cpp +) + +# Create shared library (DLL) +add_library(WinDbgMemoryPlugin SHARED ${PLUGIN_SOURCES}) + +# Link Qt + DbgEng +target_link_libraries(WinDbgMemoryPlugin PRIVATE ${QT}::Widgets dbgeng ole32) + +# Include directories +target_include_directories(WinDbgMemoryPlugin PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../../src +) + +# Output to Plugins folder +set_target_properties(WinDbgMemoryPlugin PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" +) diff --git a/plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp b/plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp new file mode 100644 index 0000000..bd586bb --- /dev/null +++ b/plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp @@ -0,0 +1,510 @@ +#include "WinDbgMemoryPlugin.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#pragma comment(lib, "dbgeng.lib") +#endif + +// ────────────────────────────────────────────────────────────────────────── +// Thread dispatch helper +// ────────────────────────────────────────────────────────────────────────── + +template +void WinDbgMemoryProvider::dispatchToOwner(Fn&& fn) const +{ + if (!m_dispatcher) { fn(); return; } + + if (QThread::currentThread() == m_dispatcher->thread()) { + // Already on the owning thread — call directly + fn(); + } else { + // Marshal to the owning thread and block until done + QMetaObject::invokeMethod(m_dispatcher, std::forward(fn), + Qt::BlockingQueuedConnection); + } +} + +// ────────────────────────────────────────────────────────────────────────── +// WinDbgMemoryProvider implementation +// ────────────────────────────────────────────────────────────────────────── + +WinDbgMemoryProvider::WinDbgMemoryProvider(const QString& target) +{ + // Create a dedicated thread for all DbgEng COM operations. + // DbgEng's remote transport (TCP/named-pipe) is thread-affine — all + // calls must happen on the thread that called DebugConnect/DebugCreate. + // A private thread with its own event loop guarantees: + // 1. dispatchToOwner() works from any calling thread (main, thread-pool, etc.) + // 2. No deadlock — the DbgEng thread is never blocked by the caller + m_dbgThread = new QThread(); + m_dbgThread->setObjectName(QStringLiteral("DbgEngThread")); + m_dbgThread->start(); + + m_dispatcher = new DbgEngDispatcher(); + m_dispatcher->moveToThread(m_dbgThread); + +#ifdef _WIN32 + // Run all DbgEng initialization on the dedicated thread. + // BlockingQueuedConnection blocks us until the lambda finishes, + // so member variables written inside are visible after the call. + dispatchToOwner([this, &target]() { + HRESULT hr; + + qDebug() << "[WinDbg] Opening target:" << target + << "on DbgEng thread" << QThread::currentThread(); + + if (target.startsWith("tcp:", Qt::CaseInsensitive) + || target.startsWith("npipe:", Qt::CaseInsensitive)) + { + // ── Remote: connect to existing WinDbg debug server ── + QByteArray connUtf8 = target.toUtf8(); + qDebug() << "[WinDbg] DebugConnect:" << target; + hr = DebugConnect(connUtf8.constData(), IID_IDebugClient, (void**)&m_client); + qDebug() << "[WinDbg] DebugConnect hr=" << Qt::hex << (unsigned long)hr + << "client=" << (void*)m_client; + if (FAILED(hr) || !m_client) { + qWarning() << "[WinDbg] DebugConnect FAILED hr=0x" << Qt::hex << (unsigned long)hr; + return; + } + m_isRemote = true; + } + else + { + // ── Local: create debug client for pid/dump ── + hr = DebugCreate(IID_IDebugClient, (void**)&m_client); + qDebug() << "[WinDbg] DebugCreate hr=" << Qt::hex << (unsigned long)hr + << "client=" << (void*)m_client; + if (FAILED(hr) || !m_client) { + qWarning() << "[WinDbg] DebugCreate FAILED hr=0x" << Qt::hex << (unsigned long)hr; + return; + } + + if (target.startsWith("pid:", Qt::CaseInsensitive)) + { + bool ok = false; + ULONG pid = target.mid(4).trimmed().toULong(&ok); + if (!ok || pid == 0) { + qWarning() << "[WinDbg] Invalid PID in target:" << target; + cleanup(); + return; + } + + qDebug() << "[WinDbg] Attaching to PID" << pid << "(non-invasive)"; + hr = m_client->AttachProcess( + 0, pid, + DEBUG_ATTACH_NONINVASIVE | DEBUG_ATTACH_NONINVASIVE_NO_SUSPEND); + qDebug() << "[WinDbg] AttachProcess hr=" << Qt::hex << (unsigned long)hr; + if (FAILED(hr)) { + qWarning() << "[WinDbg] AttachProcess FAILED"; + cleanup(); + return; + } + } + else if (target.startsWith("dump:", Qt::CaseInsensitive)) + { + QString path = target.mid(5).trimmed(); + QByteArray pathUtf8 = path.toUtf8(); + + qDebug() << "[WinDbg] Opening dump file:" << path; + hr = m_client->OpenDumpFile(pathUtf8.constData()); + qDebug() << "[WinDbg] OpenDumpFile hr=" << Qt::hex << (unsigned long)hr; + if (FAILED(hr)) { + qWarning() << "[WinDbg] OpenDumpFile FAILED"; + cleanup(); + return; + } + } + else + { + qWarning() << "[WinDbg] Unknown target format:" << target; + cleanup(); + return; + } + } + + initInterfaces(); + + // WaitForEvent to finalize the attach/dump load. + // For remote connections the server session is already active — skip. + if (m_control && !m_isRemote) { + qDebug() << "[WinDbg] WaitForEvent..."; + hr = m_control->WaitForEvent(0, 10000); + qDebug() << "[WinDbg] WaitForEvent hr=" << Qt::hex << (unsigned long)hr; + } + + querySessionInfo(); + }); + +#else + Q_UNUSED(target); +#endif +} + +void WinDbgMemoryProvider::initInterfaces() +{ +#ifdef _WIN32 + if (!m_client) return; + + HRESULT hr; + hr = m_client->QueryInterface(IID_IDebugDataSpaces, (void**)&m_dataSpaces); + qDebug() << "[WinDbg] IDebugDataSpaces hr=" << Qt::hex << (unsigned long)hr + << "ptr=" << (void*)m_dataSpaces; + + hr = m_client->QueryInterface(IID_IDebugControl, (void**)&m_control); + qDebug() << "[WinDbg] IDebugControl hr=" << Qt::hex << (unsigned long)hr + << "ptr=" << (void*)m_control; + + hr = m_client->QueryInterface(IID_IDebugSymbols, (void**)&m_symbols); + qDebug() << "[WinDbg] IDebugSymbols hr=" << Qt::hex << (unsigned long)hr + << "ptr=" << (void*)m_symbols; + + if (!m_dataSpaces) { + qWarning() << "[WinDbg] No IDebugDataSpaces — cleaning up"; + cleanup(); + } +#endif +} + +void WinDbgMemoryProvider::querySessionInfo() +{ +#ifdef _WIN32 + if (!m_client) return; + HRESULT hr; + + if (m_control) { + ULONG debugClass = 0, debugQualifier = 0; + hr = m_control->GetDebuggeeType(&debugClass, &debugQualifier); + qDebug() << "[WinDbg] GetDebuggeeType hr=" << Qt::hex << (unsigned long)hr + << "class=" << debugClass << "qualifier=" << debugQualifier; + if (SUCCEEDED(hr)) { + m_isLive = (debugQualifier < DEBUG_DUMP_SMALL); + m_writable = m_isLive; + } + } + + if (m_symbols) { + ULONG numModules = 0, numUnloaded = 0; + hr = m_symbols->GetNumberModules(&numModules, &numUnloaded); + qDebug() << "[WinDbg] GetNumberModules hr=" << Qt::hex << (unsigned long)hr + << "loaded=" << numModules << "unloaded=" << numUnloaded; + if (SUCCEEDED(hr) && numModules > 0) { + char modName[256] = {}; + ULONG modSize = 0; + hr = m_symbols->GetModuleNames(0, 0, nullptr, 0, nullptr, + modName, sizeof(modName), &modSize, + nullptr, 0, nullptr); + if (SUCCEEDED(hr) && modSize > 0) + m_name = QString::fromUtf8(modName); + } + } + + if (m_name.isEmpty()) + m_name = m_isLive ? QStringLiteral("DbgEng (Live)") : QStringLiteral("DbgEng (Dump)"); + + if (m_symbols) { + ULONG numModules = 0, numUnloaded = 0; + hr = m_symbols->GetNumberModules(&numModules, &numUnloaded); + if (SUCCEEDED(hr) && numModules > 0) { + ULONG64 moduleBase = 0; + hr = m_symbols->GetModuleByIndex(0, &moduleBase); + qDebug() << "[WinDbg] Module 0 base=" << Qt::hex << moduleBase; + if (SUCCEEDED(hr)) + m_base = moduleBase; + } + } + + if (m_base && m_dataSpaces) { + uint8_t probe[2] = {}; + ULONG got = 0; + hr = m_dataSpaces->ReadVirtual(m_base, probe, 2, &got); + qDebug() << "[WinDbg] Probe read at" << Qt::hex << m_base + << "hr=" << (unsigned long)hr << "got=" << got + << "bytes:" << (int)probe[0] << (int)probe[1]; + if (FAILED(hr) || got == 0) { + qWarning() << "[WinDbg] Probe read FAILED — cleaning up"; + cleanup(); + return; + } + } + + qDebug() << "[WinDbg] Ready. name=" << m_name + << "base=" << Qt::hex << m_base << "isLive=" << m_isLive; +#endif +} + +WinDbgMemoryProvider::~WinDbgMemoryProvider() +{ +#ifdef _WIN32 + // Dispatch COM cleanup to the DbgEng thread (thread-affine release) + if (m_dbgThread && m_dbgThread->isRunning() && m_dispatcher) { + dispatchToOwner([this]() { + if (m_client) { + if (m_isRemote) + m_client->EndSession(DEBUG_END_DISCONNECT); + else + m_client->DetachProcesses(); + } + cleanup(); + }); + } else { + // Thread not running — clean up directly (best-effort) + if (m_client) { + if (m_isRemote) + m_client->EndSession(DEBUG_END_DISCONNECT); + else + m_client->DetachProcesses(); + } + cleanup(); + } +#else + cleanup(); +#endif + + // Stop the dedicated thread + if (m_dbgThread) { + m_dbgThread->quit(); + m_dbgThread->wait(3000); + delete m_dbgThread; + m_dbgThread = nullptr; + } + delete m_dispatcher; + m_dispatcher = nullptr; +} + +void WinDbgMemoryProvider::cleanup() +{ +#ifdef _WIN32 + if (m_symbols) { m_symbols->Release(); m_symbols = nullptr; } + if (m_control) { m_control->Release(); m_control = nullptr; } + if (m_dataSpaces) { m_dataSpaces->Release(); m_dataSpaces = nullptr; } + if (m_client) { m_client->Release(); m_client = nullptr; } +#endif +} + +bool WinDbgMemoryProvider::read(uint64_t addr, void* buf, int len) const +{ +#ifdef _WIN32 + if (!m_dataSpaces || len <= 0) return false; + + bool result = false; + dispatchToOwner([&]() { + ULONG bytesRead = 0; + HRESULT hr = m_dataSpaces->ReadVirtual(m_base + addr, buf, (ULONG)len, &bytesRead); + if (FAILED(hr) || (int)bytesRead < len) + memset((char*)buf + bytesRead, 0, len - bytesRead); + result = bytesRead > 0; + }); + return result; +#else + Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len); + return false; +#endif +} + +bool WinDbgMemoryProvider::write(uint64_t addr, const void* buf, int len) +{ +#ifdef _WIN32 + if (!m_dataSpaces || !m_writable || len <= 0) return false; + + bool result = false; + dispatchToOwner([&]() { + ULONG bytesWritten = 0; + HRESULT hr = m_dataSpaces->WriteVirtual(m_base + addr, const_cast(buf), + (ULONG)len, &bytesWritten); + result = SUCCEEDED(hr) && bytesWritten == (ULONG)len; + }); + return result; +#else + Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len); + return false; +#endif +} + +int WinDbgMemoryProvider::size() const +{ +#ifdef _WIN32 + return m_dataSpaces ? 0x10000 : 0; +#else + return 0; +#endif +} + +bool WinDbgMemoryProvider::isReadable(uint64_t /*addr*/, int len) const +{ +#ifdef _WIN32 + // DbgEng's ReadVirtual can read any mapped virtual address. + return m_dataSpaces != nullptr && len >= 0; +#else + return false; +#endif +} + +QString WinDbgMemoryProvider::getSymbol(uint64_t addr) const +{ +#ifdef _WIN32 + if (!m_symbols) return {}; + + QString result; + dispatchToOwner([&]() { + char nameBuf[512] = {}; + ULONG nameSize = 0; + ULONG64 displacement = 0; + HRESULT hr = m_symbols->GetNameByOffset(m_base + addr, nameBuf, sizeof(nameBuf), + &nameSize, &displacement); + if (SUCCEEDED(hr) && nameSize > 0) { + result = QString::fromUtf8(nameBuf); + if (displacement > 0) + result += QStringLiteral("+0x%1").arg(displacement, 0, 16); + } + }); + return result; +#else + Q_UNUSED(addr); + return {}; +#endif +} + +// ────────────────────────────────────────────────────────────────────────── +// WinDbgMemoryPlugin implementation +// ────────────────────────────────────────────────────────────────────────── + +QIcon WinDbgMemoryPlugin::Icon() const +{ + return qApp->style()->standardIcon(QStyle::SP_DriveNetIcon); +} + +bool WinDbgMemoryPlugin::canHandle(const QString& target) const +{ + return target.startsWith("tcp:", Qt::CaseInsensitive) + || target.startsWith("npipe:", Qt::CaseInsensitive) + || target.startsWith("pid:", Qt::CaseInsensitive) + || target.startsWith("dump:", Qt::CaseInsensitive); +} + +std::unique_ptr WinDbgMemoryPlugin::createProvider(const QString& target, QString* errorMsg) +{ + auto provider = std::make_unique(target); + if (!provider->isValid()) + { + if (errorMsg) { + if (target.startsWith("tcp:", Qt::CaseInsensitive) + || target.startsWith("npipe:", Qt::CaseInsensitive)) + *errorMsg = QString("Failed to connect to debug server.\n\n" + "Target: %1\n\n" + "Make sure WinDbg is running with a matching .server command\n" + "(e.g. .server tcp:port=5055) and the port/pipe is reachable.") + .arg(target); + else if (target.startsWith("pid:", Qt::CaseInsensitive)) + *errorMsg = QString("Failed to attach to process.\n\n" + "Target: %1\n\n" + "Make sure the process is running and you have " + "sufficient privileges (try Run as Administrator).") + .arg(target); + else + *errorMsg = QString("Failed to open dump file.\n\n" + "Target: %1\n\n" + "Make sure the file exists and is a valid dump.") + .arg(target); + } + return nullptr; + } + return provider; +} + +uint64_t WinDbgMemoryPlugin::getInitialBaseAddress(const QString& target) const +{ + Q_UNUSED(target); + return 0; +} + +bool WinDbgMemoryPlugin::selectTarget(QWidget* parent, QString* target) +{ + QDialog dlg(parent); + dlg.setWindowTitle("WinDbg Settings"); + dlg.resize(460, 260); + + QPalette dlgPal = qApp->palette(); + dlg.setPalette(dlgPal); + dlg.setAutoFillBackground(true); + + auto* layout = new QVBoxLayout(&dlg); + + layout->addWidget(new QLabel( + "Connect to a running WinDbg debug server.\n" + "In WinDbg, run: .server tcp:port=5055")); + + layout->addSpacing(8); + layout->addWidget(new QLabel("Connection string:")); + auto* connEdit = new QLineEdit; + connEdit->setPlaceholderText("tcp:Port=5055,Server=localhost"); + connEdit->setText("tcp:Port=5055,Server=localhost"); + layout->addWidget(connEdit); + + layout->addSpacing(4); + layout->addWidget(new QLabel("Run one of these in WinDbg first:")); + + auto addExample = [&](const QString& text) { + auto* row = new QHBoxLayout; + auto* label = new QLabel(text); + QPalette lp = dlgPal; + lp.setColor(QPalette::WindowText, dlgPal.color(QPalette::Disabled, QPalette::WindowText)); + label->setPalette(lp); + label->setTextInteractionFlags(Qt::TextSelectableByMouse); + row->addWidget(label, 1); + auto* copyBtn = new QPushButton("Copy"); + copyBtn->setFixedWidth(50); + copyBtn->setToolTip("Copy to clipboard"); + QObject::connect(copyBtn, &QPushButton::clicked, [text]() { + QGuiApplication::clipboard()->setText(text); + }); + row->addWidget(copyBtn); + layout->addLayout(row); + }; + + addExample(".server tcp:port=5055"); + addExample(".server npipe:pipe=reclass"); + layout->addStretch(); + + auto* btnLayout = new QHBoxLayout; + btnLayout->addStretch(); + auto* okBtn = new QPushButton("OK"); + auto* cancelBtn = new QPushButton("Cancel"); + btnLayout->addWidget(okBtn); + btnLayout->addWidget(cancelBtn); + layout->addLayout(btnLayout); + + QObject::connect(okBtn, &QPushButton::clicked, &dlg, &QDialog::accept); + QObject::connect(cancelBtn, &QPushButton::clicked, &dlg, &QDialog::reject); + + if (dlg.exec() != QDialog::Accepted) + return false; + + QString conn = connEdit->text().trimmed(); + if (conn.isEmpty()) return false; + *target = conn; + return true; +} + +// ────────────────────────────────────────────────────────────────────────── +// Plugin factory +// ────────────────────────────────────────────────────────────────────────── + +extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin() +{ + return new WinDbgMemoryPlugin(); +} diff --git a/plugins/WinDbgMemory/WinDbgMemoryPlugin.h b/plugins/WinDbgMemory/WinDbgMemoryPlugin.h new file mode 100644 index 0000000..e409b0a --- /dev/null +++ b/plugins/WinDbgMemory/WinDbgMemoryPlugin.h @@ -0,0 +1,122 @@ +#pragma once +#include "../../src/iplugin.h" +#include "../../src/core.h" + +#include +#include +#include + +// Forward declarations for DbgEng COM interfaces +struct IDebugClient; +struct IDebugDataSpaces; +struct IDebugControl; +struct IDebugSymbols; + +/** + * WinDbg memory provider + * + * Uses DbgEng to read memory from: + * - An existing WinDbg debug server via DebugConnect (tcp/npipe) + * - A live process by PID via DebugCreate (non-invasive attach) + * - A crash dump (.dmp) file via DebugCreate + * + * Target string format: + * "tcp:Port=5055,Server=localhost" - connect to WinDbg debug server (TCP) + * "npipe:Pipe=name,Server=localhost" - connect to WinDbg debug server (named pipe) + * "pid:1234" - attach to process 1234 + * "dump:C:/path/to/file.dmp" - open dump file + * + * Threading: All DbgEng COM calls are dispatched to the thread that created + * the connection (DebugConnect/DebugCreate). This is required because the + * remote transport (TCP/named-pipe) binds to the creating thread. The + * controller's background refresh threads call read() which transparently + * marshals to the owning thread via BlockingQueuedConnection. + */ + +// Helper QObject that lives on the DbgEng-owning thread. +// Used as a target for QMetaObject::invokeMethod to marshal calls. +class DbgEngDispatcher : public QObject { + Q_OBJECT +public: + using QObject::QObject; +}; + +class WinDbgMemoryProvider : public rcx::Provider +{ +public: + /// Create a provider from a target string + WinDbgMemoryProvider(const QString& target); + ~WinDbgMemoryProvider() override; + + // Required overrides + bool read(uint64_t addr, void* buf, int len) const override; + int size() const override; + + // Optional overrides + bool isReadable(uint64_t addr, int len) const override; + bool write(uint64_t addr, const void* buf, int len) override; + bool isWritable() const override { return m_writable; } + QString name() const override { return m_name; } + QString kind() const override { return QStringLiteral("WinDbg"); } + QString getSymbol(uint64_t addr) const override; + + bool isLive() const override { return m_isLive; } + uint64_t base() const override { return m_base; } + void setBase(uint64_t b) override { m_base = b; } + +private: + void initInterfaces(); // get IDebugDataSpaces/Control/Symbols from client + void querySessionInfo(); // determine live/dump, writable, name, base + void cleanup(); + + // Marshal a lambda to the DbgEng-owning thread. If already on that + // thread, calls directly. Otherwise blocks via QueuedConnection. + template + void dispatchToOwner(Fn&& fn) const; + + IDebugClient* m_client = nullptr; + IDebugDataSpaces* m_dataSpaces = nullptr; + IDebugControl* m_control = nullptr; + IDebugSymbols* m_symbols = nullptr; + + QString m_name; + uint64_t m_base = 0; + bool m_isLive = false; + bool m_writable = false; + bool m_isRemote = false; // true when connected via DebugConnect (tcp/npipe) + + // Dedicated thread for DbgEng COM operations. The remote TCP/pipe + // transport is thread-affine — all calls must happen on the thread + // that called DebugConnect. A private thread with its own event loop + // ensures dispatchToOwner() works from any calling thread (including + // QtConcurrent workers and the main/GUI thread) without deadlock. + QThread* m_dbgThread = nullptr; + DbgEngDispatcher* m_dispatcher = nullptr; +}; + +/** + * Plugin that provides WinDbgMemoryProvider + * + * Uses DbgEng to read memory via: + * - Remote connection to an existing WinDbg debug server (tcp/npipe) + * - Local non-invasive attach to a live process (pid) + * - Local crash dump file (dump) + */ +class WinDbgMemoryPlugin : public IProviderPlugin +{ +public: + std::string Name() const override { return "WinDbg Memory"; } + std::string Version() const override { return "2.0.0"; } + std::string Author() const override { return "Reclass"; } + std::string Description() const override { return "Read memory via DbgEng (live process attach or crash dump)"; } + 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; +}; + +// Plugin export +extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin(); diff --git a/src/compose.cpp b/src/compose.cpp index d52d02f..3b1352d 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -459,8 +459,13 @@ void composeNode(ComposeState& state, const NodeTree& tree, ptrVal = (node.kind == NodeKind::Pointer32) ? (uint64_t)prov.readU32(absAddr) : prov.readU64(absAddr); if (ptrVal != 0) { - uint64_t pBase = ptrToProviderAddr(tree, ptrVal); - if (pBase == UINT64_MAX) ptrVal = 0; // ptr below base: invalid + // Treat sentinel values as invalid pointers + if (ptrVal == UINT64_MAX || (node.kind == NodeKind::Pointer32 && ptrVal == 0xFFFFFFFF)) + ptrVal = 0; + else { + uint64_t pBase = ptrToProviderAddr(tree, ptrVal); + if (pBase == UINT64_MAX) ptrVal = 0; // ptr below base: invalid + } } } diff --git a/src/controller.cpp b/src/controller.cpp index 78a9565..a95da92 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -293,6 +293,9 @@ void RcxController::connectEditor(RcxEditor* editor) { break; case EditTarget::BaseAddress: { QString s = text.trimmed(); + s.remove('`'); // WinDbg backtick separators (e.g. 7ff6`6cce0000) + s.remove('\n'); + s.remove('\r'); // Support simple equations: 0x10+0x4, 0x100-0x10, etc. uint64_t newBase = 0; bool ok = true; @@ -347,7 +350,7 @@ void RcxController::connectEditor(RcxEditor* editor) { if (text.startsWith(QStringLiteral("#saved:"))) { int idx = text.mid(7).toInt(); switchToSavedSource(idx); - } else if (text == QStringLiteral("file")) { + } else if (text == QStringLiteral("File")) { auto* w = qobject_cast(parent()); QString path = QFileDialog::getOpenFileName(w, "Load Binary Data", {}, "All Files (*)"); if (!path.isEmpty()) { @@ -910,7 +913,7 @@ void RcxController::applyCommand(const Command& command, bool isUndo) { qWarning() << "WriteBytes failed at address" << QString::number(c.addr, 16); // Patch snapshot so compose sees the new value immediately if (m_snapshotProv) - m_snapshotProv->patchSnapshot(c.addr, bytes.constData(), bytes.size()); + m_snapshotProv->patchPages(c.addr, bytes.constData(), bytes.size()); } else if constexpr (std::is_same_v) { int idx = tree.indexOfId(c.nodeId); if (idx >= 0) { @@ -1896,15 +1899,66 @@ void RcxController::pushSavedSourcesToEditors() { void RcxController::setupAutoRefresh() { m_refreshTimer = new QTimer(this); - m_refreshTimer->setInterval(2000); + m_refreshTimer->setInterval(660); connect(m_refreshTimer, &QTimer::timeout, this, &RcxController::onRefreshTick); m_refreshTimer->start(); - m_refreshWatcher = new QFutureWatcher(this); - connect(m_refreshWatcher, &QFutureWatcher::finished, + m_refreshWatcher = new QFutureWatcher(this); + connect(m_refreshWatcher, &QFutureWatcher::finished, this, &RcxController::onReadComplete); } +// Recursively collect memory ranges for a struct and its pointer targets. +// memBase is the provider-relative address where this struct's data lives. +void RcxController::collectPointerRanges( + uint64_t structId, uint64_t memBase, + int depth, int maxDepth, + QSet& visited, + QVector>& ranges) const +{ + if (depth >= maxDepth) return; + uint64_t key = memBase ^ (structId * 0x9E3779B97F4A7C15ULL); + if (visited.contains(key)) return; + visited.insert(key); + + int span = m_doc->tree.structSpan(structId); + if (span <= 0) return; + ranges.append({memBase, span}); + + if (!m_snapshotProv) return; + + // Walk children looking for non-collapsed pointers + QVector children = m_doc->tree.childrenOf(structId); + for (int ci : children) { + const Node& child = m_doc->tree.nodes[ci]; + if (child.kind != NodeKind::Pointer32 && child.kind != NodeKind::Pointer64) + continue; + if (child.collapsed || child.refId == 0) continue; + + uint64_t ptrAddr = memBase + child.offset; + int ptrSize = child.byteSize(); + if (!m_snapshotProv->isReadable(ptrAddr, ptrSize)) continue; + + uint64_t ptrVal = (child.kind == NodeKind::Pointer32) + ? (uint64_t)m_snapshotProv->readU32(ptrAddr) + : m_snapshotProv->readU64(ptrAddr); + if (ptrVal == 0 || ptrVal == UINT64_MAX || ptrVal < m_doc->tree.baseAddress) continue; + + uint64_t pBase = ptrVal - m_doc->tree.baseAddress; + collectPointerRanges(child.refId, pBase, depth + 1, maxDepth, + visited, ranges); + } + + // Embedded struct references (struct node with refId but no own children) + int idx = m_doc->tree.indexOfId(structId); + if (idx >= 0) { + const Node& sn = m_doc->tree.nodes[idx]; + if (sn.kind == NodeKind::Struct && sn.refId != 0 && children.isEmpty()) + collectPointerRanges(sn.refId, memBase, depth, maxDepth, + visited, ranges); + } +} + void RcxController::onRefreshTick() { if (m_readInFlight) return; if (!m_doc->provider || !m_doc->provider->isLive()) return; @@ -1915,75 +1969,120 @@ void RcxController::onRefreshTick() { int extent = computeDataExtent(); if (extent <= 0) return; + // Collect all needed ranges: main struct + pointer targets + QVector> ranges; + ranges.append({0, extent}); + + if (m_snapshotProv) { + QSet visited; + uint64_t rootId = m_viewRootId; + if (rootId == 0 && !m_doc->tree.nodes.isEmpty()) + rootId = m_doc->tree.nodes[0].id; + collectPointerRanges(rootId, 0, 0, 4, visited, ranges); + } + m_readInFlight = true; m_readGen = m_refreshGen; - // Capture shared_ptr copy — keeps provider alive during async read auto prov = m_doc->provider; - uint64_t base = prov->base(); - qDebug() << "[Refresh] reading" << extent << "bytes from base" << Qt::hex << base; - m_refreshWatcher->setFuture(QtConcurrent::run([prov, extent]() -> QByteArray { - return prov->readBytes(0, extent); + qDebug() << "[Refresh] reading" << ranges.size() << "ranges from base" + << Qt::hex << prov->base(); + m_refreshWatcher->setFuture(QtConcurrent::run([prov, ranges]() -> PageMap { + constexpr uint64_t kPageSize = 4096; + constexpr uint64_t kPageMask = ~(kPageSize - 1); + PageMap pages; + for (const auto& r : ranges) { + uint64_t pageStart = r.first & kPageMask; + uint64_t end = r.first + r.second; + uint64_t pageEnd = (end + kPageSize - 1) & kPageMask; + for (uint64_t p = pageStart; p < pageEnd; p += kPageSize) { + if (!pages.contains(p)) + pages[p] = prov->readBytes(p, static_cast(kPageSize)); + } + } + return pages; })); } void RcxController::onReadComplete() { m_readInFlight = false; - // Stale read (provider changed while we were reading) — discard if (m_readGen != m_refreshGen) return; - QByteArray newData = m_refreshWatcher->result(); + PageMap newPages; + try { + newPages = m_refreshWatcher->result(); + } catch (const std::exception& e) { + qWarning() << "[Refresh] async read threw:" << e.what(); + return; + } catch (...) { + qWarning() << "[Refresh] async read threw unknown exception"; + return; + } - // Fast path: no changes at all — skip full recompose - if (!m_prevSnapshot.isEmpty() && m_prevSnapshot.size() == newData.size() - && memcmp(m_prevSnapshot.constData(), newData.constData(), newData.size()) == 0) + // All-zero guard: if page 0 is all zeros and we already have data, discard + if (!m_prevPages.isEmpty() && newPages.contains(0)) { + const QByteArray& p0 = newPages.value(0); + bool allZero = true; + for (int i = 0; i < p0.size(); ++i) { + if (p0[i] != 0) { allZero = false; break; } + } + if (allZero) { + qDebug() << "[Refresh] discarding all-zero page-0, keeping stale snapshot"; + return; + } + } + + // Fast path: no changes at all + if (newPages == m_prevPages) return; - // Compute which byte offsets changed + // Compute which byte offsets changed (for change highlighting). + // Skip on first snapshot — nothing to compare against. m_changedOffsets.clear(); - if (!m_prevSnapshot.isEmpty()) { - int compareLen = qMin(m_prevSnapshot.size(), newData.size()); - const char* oldP = m_prevSnapshot.constData(); - const char* newP = newData.constData(); - for (int i = 0; i < compareLen; i++) { - if (oldP[i] != newP[i]) - m_changedOffsets.insert(i); + if (!m_prevPages.isEmpty()) { + for (auto it = newPages.constBegin(); it != newPages.constEnd(); ++it) { + uint64_t pageAddr = it.key(); + const QByteArray& newPage = it.value(); + auto oldIt = m_prevPages.constFind(pageAddr); + if (oldIt == m_prevPages.constEnd()) + continue; // new page, no previous data to diff against + const QByteArray& oldPage = oldIt.value(); + int cmpLen = qMin(oldPage.size(), newPage.size()); + for (int i = 0; i < cmpLen; ++i) { + if (oldPage[i] != newPage[i]) + m_changedOffsets.insert(static_cast(pageAddr) + i); + } } - // Bytes beyond old snapshot are all "new" - for (int i = compareLen; i < newData.size(); i++) - m_changedOffsets.insert(i); } - m_prevSnapshot = newData; - // Update or create snapshot provider + int mainExtent = computeDataExtent(); + m_prevPages = newPages; + if (m_snapshotProv) - m_snapshotProv->updateSnapshot(std::move(newData)); + m_snapshotProv->updatePages(std::move(newPages), mainExtent); else - m_snapshotProv = std::make_unique(m_doc->provider, std::move(newData)); + m_snapshotProv = std::make_unique( + m_doc->provider, std::move(newPages), mainExtent); refresh(); - - // Clear changed offsets after refresh consumed them m_changedOffsets.clear(); } int RcxController::computeDataExtent() const { - // Prefer tree-based extent: exact bytes needed for rendering + static constexpr int64_t kMaxMainExtent = 16 * 1024 * 1024; // 16 MB cap + int64_t treeExtent = 0; for (int i = 0; i < m_doc->tree.nodes.size(); i++) { const Node& node = m_doc->tree.nodes[i]; int64_t off = m_doc->tree.computeOffset(i); - // byteSize() returns 0 for Array-of-Struct/Array; use structSpan() for containers int sz = (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) ? m_doc->tree.structSpan(node.id) : node.byteSize(); int64_t end = off + sz; if (end > treeExtent) treeExtent = end; } - // Clamp to max int (readBytes takes int length) - if (treeExtent > 0) return (int)qMin(treeExtent, (int64_t)std::numeric_limits::max()); + if (treeExtent > 0) return static_cast(qMin(treeExtent, kMaxMainExtent)); - // Fallback: provider size (empty tree) int provSize = m_doc->provider->size(); if (provSize > 0) return provSize; return 0; @@ -1993,7 +2092,7 @@ void RcxController::resetSnapshot() { m_refreshGen++; m_readInFlight = false; m_snapshotProv.reset(); - m_prevSnapshot.clear(); + m_prevPages.clear(); m_changedOffsets.clear(); } diff --git a/src/controller.h b/src/controller.h index 2c699e3..08dcf39 100644 --- a/src/controller.h +++ b/src/controller.h @@ -141,10 +141,11 @@ private: TypeSelectorPopup* m_cachedPopup = nullptr; // ── Auto-refresh state ── + using PageMap = QHash; QTimer* m_refreshTimer = nullptr; - QFutureWatcher* m_refreshWatcher = nullptr; + QFutureWatcher* m_refreshWatcher = nullptr; std::unique_ptr m_snapshotProv; - QByteArray m_prevSnapshot; + PageMap m_prevPages; QSet m_changedOffsets; uint64_t m_refreshGen = 0; uint64_t m_readGen = 0; @@ -166,6 +167,10 @@ private: void onReadComplete(); int computeDataExtent() const; void resetSnapshot(); + void collectPointerRanges(uint64_t structId, uint64_t memBase, + int depth, int maxDepth, + QSet& visited, + QVector>& ranges) const; }; } // namespace rcx diff --git a/src/editor.cpp b/src/editor.cpp index 98a6b3e..3c3098d 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include "themes/thememanager.h" namespace rcx { @@ -1603,6 +1604,22 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) { case Qt::Key_End: m_sci->setCursorPosition(m_editState.line, editEndCol()); return true; + case Qt::Key_V: + if (ke->modifiers() & Qt::ControlModifier) { + // Sanitized paste: strip newlines (and backticks for base addresses) + QString clip = QApplication::clipboard()->text(); + clip.remove('\n'); + clip.remove('\r'); + if (m_editState.target == EditTarget::BaseAddress) + clip.remove('`'); + if (!clip.isEmpty()) { + QByteArray utf8 = clip.toUtf8(); + m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACESEL, + (uintptr_t)0, utf8.constData()); + } + return true; + } + return false; default: return false; } @@ -1961,7 +1978,7 @@ void RcxEditor::showSourcePicker() { int zoom = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETZOOM); menuFont.setPointSize(menuFont.pointSize() + zoom); menu.setFont(menuFont); - menu.addAction("file"); + menu.addAction("File"); // Add all registered providers from global registry const auto& providers = ProviderRegistry::instance().providers(); diff --git a/src/main.cpp b/src/main.cpp index 84b810f..d3f3bcc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -465,7 +465,7 @@ void MainWindow::styleTabCloseButtons() { const auto& t = ThemeManager::instance().current(); QString style = QStringLiteral( - "QToolButton { color: %1; border: none; padding: 0px 4px; font-size: 12px; }" + "QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }" "QToolButton:hover { color: %2; }") .arg(t.textDim.name(), t.indHoverSpan.name()); diff --git a/src/providers/snapshot_provider.h b/src/providers/snapshot_provider.h index 408f2e5..74c23d9 100644 --- a/src/providers/snapshot_provider.h +++ b/src/providers/snapshot_provider.h @@ -1,28 +1,65 @@ #pragma once #include "provider.h" +#include #include namespace rcx { -// Provider that reads from a cached QByteArray snapshot but delegates -// metadata (name, kind, getSymbol) to the underlying real provider. -// Used for async refresh: worker thread reads bulk data into a snapshot, -// UI thread composes against it without blocking. +// Page-based snapshot provider. +// +// During async refresh the controller reads pages for the main struct and +// every reachable pointer target. Compose reads entirely from this page +// table — no fallback to the real provider, no blocking I/O on the UI +// thread. Pages that were never fetched (truly invalid pointers) simply +// read as zeros. class SnapshotProvider : public Provider { std::shared_ptr m_real; - QByteArray m_data; + QHash m_pages; // page-aligned addr → 4096-byte page + int m_mainExtent = 0; // logical size of the main struct range + + static constexpr uint64_t kPageSize = 4096; + static constexpr uint64_t kPageMask = ~(kPageSize - 1); public: - SnapshotProvider(std::shared_ptr real, QByteArray snapshot) - : m_real(std::move(real)), m_data(std::move(snapshot)) {} + using PageMap = QHash; + + SnapshotProvider(std::shared_ptr real, PageMap pages, int mainExtent) + : m_real(std::move(real)) + , m_pages(std::move(pages)) + , m_mainExtent(mainExtent) {} bool read(uint64_t addr, void* buf, int len) const override { - if (!isReadable(addr, len)) return false; - std::memcpy(buf, m_data.constData() + addr, len); + if (len <= 0) return false; + char* out = static_cast(buf); + uint64_t cur = addr; + int remaining = len; + while (remaining > 0) { + uint64_t pageAddr = cur & kPageMask; + int pageOff = static_cast(cur - pageAddr); + int chunk = qMin(remaining, static_cast(kPageSize - pageOff)); + auto it = m_pages.constFind(pageAddr); + if (it != m_pages.constEnd()) { + std::memcpy(out, it->constData() + pageOff, chunk); + } else { + std::memset(out, 0, chunk); + } + out += chunk; + cur += chunk; + remaining -= chunk; + } return true; } - int size() const override { return m_data.size(); } + bool isReadable(uint64_t addr, int len) const override { + if (len <= 0) return (len == 0); + uint64_t end = addr + static_cast(len); + for (uint64_t p = addr & kPageMask; p < end; p += kPageSize) { + if (!m_pages.contains(p)) return false; + } + return true; + } + + int size() const override { return m_mainExtent; } bool isWritable() const override { return m_real ? m_real->isWritable() : false; } bool isLive() const override { return m_real ? m_real->isLive() : false; } QString name() const override { return m_real ? m_real->name() : QString(); } @@ -34,21 +71,36 @@ public: bool write(uint64_t addr, const void* buf, int len) override { if (!m_real) return false; bool ok = m_real->write(addr, buf, len); - if (ok && isReadable(addr, len)) - std::memcpy(m_data.data() + addr, buf, len); + if (ok) patchPages(addr, buf, len); return ok; } - // Update the entire snapshot (called after async read completes) - void updateSnapshot(QByteArray data) { m_data = std::move(data); } - - // Patch specific bytes in the snapshot (called after user writes a value) - void patchSnapshot(uint64_t addr, const void* buf, int len) { - if (isReadable(addr, len)) - std::memcpy(m_data.data() + addr, buf, len); + // Replace the entire page table (called after async read completes) + void updatePages(PageMap pages, int mainExtent) { + m_pages = std::move(pages); + m_mainExtent = mainExtent; } - const QByteArray& snapshot() const { return m_data; } + // Patch specific bytes in existing pages (called after user writes a value) + void patchPages(uint64_t addr, const void* buf, int len) { + const char* src = static_cast(buf); + uint64_t cur = addr; + int remaining = len; + while (remaining > 0) { + uint64_t pageAddr = cur & kPageMask; + int pageOff = static_cast(cur - pageAddr); + int chunk = qMin(remaining, static_cast(kPageSize - pageOff)); + auto it = m_pages.find(pageAddr); + if (it != m_pages.end()) { + std::memcpy(it->data() + pageOff, src, chunk); + } + src += chunk; + cur += chunk; + remaining -= chunk; + } + } + + const PageMap& pages() const { return m_pages; } }; } // namespace rcx diff --git a/src/titlebar.cpp b/src/titlebar.cpp index 0c98789..4c532c0 100644 --- a/src/titlebar.cpp +++ b/src/titlebar.cpp @@ -20,6 +20,7 @@ TitleBarWidget::TitleBarWidget(QWidget* parent) // App name m_appLabel = new QLabel(QStringLiteral("Reclass"), this); m_appLabel->setContentsMargins(10, 0, 4, 0); + m_appLabel->setAlignment(Qt::AlignVCenter); m_appLabel->setAttribute(Qt::WA_TransparentForMouseEvents); layout->addWidget(m_appLabel); diff --git a/tests/test_com_security.cpp b/tests/test_com_security.cpp new file mode 100644 index 0000000..a76b08d --- /dev/null +++ b/tests/test_com_security.cpp @@ -0,0 +1,185 @@ +/** + * test_com_security.cpp — DebugConnect transport diagnostic + * + * Tests EVERY transport to find what works from MinGW: + * 1. TCP to WinDbg .server (port 5055) + * 2. Named pipe to WinDbg .server + * 3. TCP with various COM security configs + * 4. DebugCreate local (baseline) + * + * SETUP: In WinDbg, run BOTH of these: + * .server tcp:port=5055 + * .server npipe:pipe=reclass + * + * Then run this test. + */ + +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#include +#endif + +#ifdef _WIN32 +static void try_connect(const char* label, const char* connStr) +{ + printf(" %-40s → ", label); + fflush(stdout); + + IDebugClient* client = nullptr; + HRESULT hr = DebugConnect(connStr, IID_IDebugClient, (void**)&client); + + if (SUCCEEDED(hr) && client) { + printf("SUCCESS (hr=0x%08lX)\n", (unsigned long)hr); + + // Try to get data spaces and read something + IDebugDataSpaces* ds = nullptr; + IDebugSymbols* sym = nullptr; + IDebugControl* ctrl = nullptr; + client->QueryInterface(IID_IDebugDataSpaces, (void**)&ds); + client->QueryInterface(IID_IDebugSymbols, (void**)&sym); + client->QueryInterface(IID_IDebugControl, (void**)&ctrl); + + if (ctrl) { + HRESULT hrWait = ctrl->WaitForEvent(0, 5000); + printf(" WaitForEvent: hr=0x%08lX\n", (unsigned long)hrWait); + } + + if (sym) { + ULONG numMods = 0, numUnloaded = 0; + sym->GetNumberModules(&numMods, &numUnloaded); + printf(" Modules: %lu loaded\n", numMods); + + if (numMods > 0 && ds) { + ULONG64 base = 0; + sym->GetModuleByIndex(0, &base); + unsigned char buf[2] = {}; + ULONG got = 0; + ds->ReadVirtual(base, buf, 2, &got); + printf(" Read at 0x%llX: got=%lu bytes=[%02X %02X]\n", + (unsigned long long)base, got, buf[0], buf[1]); + } + } + + if (sym) sym->Release(); + if (ds) ds->Release(); + if (ctrl) ctrl->Release(); + client->Release(); + } else { + char buf[256] = {}; + FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, (DWORD)hr, 0, buf, sizeof(buf), nullptr); + for (char* p = buf + strlen(buf) - 1; p >= buf && (*p == '\r' || *p == '\n'); --p) + *p = '\0'; + printf("FAIL hr=0x%08lX (%s)\n", (unsigned long)hr, buf); + } +} +#endif + +int main() +{ +#ifdef _WIN32 + char hostname[256] = {}; + DWORD hsize = sizeof(hostname); + GetComputerNameA(hostname, &hsize); + + printf("=== DebugConnect Transport Diagnostic ===\n"); + printf("Machine: %s\n\n", hostname); + + // ── Baseline: DebugCreate (local) ── + printf("[1] DebugCreate (local, no network)\n"); + { + IDebugClient* client = nullptr; + HRESULT hr = DebugCreate(IID_IDebugClient, (void**)&client); + printf(" DebugCreate: %s (hr=0x%08lX)\n\n", + SUCCEEDED(hr) ? "OK" : "FAIL", (unsigned long)hr); + if (client) client->Release(); + } + + // ── TCP variants ── + printf("[2] TCP connections (need: .server tcp:port=5055)\n"); + try_connect("tcp:Port=5055,Server=localhost", + "tcp:Port=5055,Server=localhost"); + try_connect("tcp:Port=5055,Server=127.0.0.1", + "tcp:Port=5055,Server=127.0.0.1"); + { + char conn[512]; + snprintf(conn, sizeof(conn), "tcp:Port=5055,Server=%s", hostname); + try_connect(conn, conn); + } + printf("\n"); + + // ── Named pipe variants ── + printf("[3] Named pipe connections (need: .server npipe:pipe=reclass)\n"); + try_connect("npipe:Pipe=reclass,Server=localhost", + "npipe:Pipe=reclass,Server=localhost"); + { + char conn[512]; + snprintf(conn, sizeof(conn), "npipe:Pipe=reclass,Server=%s", hostname); + try_connect(conn, conn); + } + try_connect("npipe:Pipe=reclass", + "npipe:Pipe=reclass"); + printf("\n"); + + // ── TCP with COM security ── + printf("[4] TCP with explicit COM init (MTA + IMPERSONATE)\n"); + { + // This runs in-process so CoInitialize affects subsequent calls + CoInitializeEx(nullptr, COINIT_MULTITHREADED); + CoInitializeSecurity( + nullptr, -1, nullptr, nullptr, + RPC_C_AUTHN_LEVEL_DEFAULT, + RPC_C_IMP_LEVEL_IMPERSONATE, + nullptr, EOAC_NONE, nullptr); + try_connect("tcp:Port=5055,Server=localhost (MTA+SEC)", + "tcp:Port=5055,Server=localhost"); + try_connect("npipe:Pipe=reclass (MTA+SEC)", + "npipe:Pipe=reclass,Server=localhost"); + CoUninitialize(); + } + printf("\n"); + + // ── Check if dbgeng.dll is the system one ── + printf("[5] DbgEng DLL info\n"); + { + HMODULE hmod = GetModuleHandleA("dbgeng.dll"); + if (hmod) { + char path[MAX_PATH] = {}; + GetModuleFileNameA(hmod, path, MAX_PATH); + printf(" dbgeng.dll loaded from: %s\n", path); + + // Get version + DWORD verSize = GetFileVersionInfoSizeA(path, nullptr); + if (verSize > 0) { + auto* verData = (char*)malloc(verSize); + if (GetFileVersionInfoA(path, 0, verSize, verData)) { + VS_FIXEDFILEINFO* fileInfo = nullptr; + UINT len = 0; + if (VerQueryValueA(verData, "\\", (void**)&fileInfo, &len)) { + printf(" Version: %d.%d.%d.%d\n", + HIWORD(fileInfo->dwFileVersionMS), + LOWORD(fileInfo->dwFileVersionMS), + HIWORD(fileInfo->dwFileVersionLS), + LOWORD(fileInfo->dwFileVersionLS)); + } + } + free(verData); + } + } else { + printf(" dbgeng.dll not loaded yet\n"); + } + } + + printf("\n=== Done ===\n"); + return 0; +#else + printf("Windows only.\n"); + return 0; +#endif +} diff --git a/tests/test_dbgconnect.cpp b/tests/test_dbgconnect.cpp new file mode 100644 index 0000000..3255201 --- /dev/null +++ b/tests/test_dbgconnect.cpp @@ -0,0 +1,65 @@ +#include +#include +#include +#include +#include + +int main() +{ + const char* connStr = "tcp:Port=5057,Server=localhost"; + printf("Attempting DebugConnect(\"%s\")...\n", connStr); + + IDebugClient* client = nullptr; + HRESULT hr = DebugConnect(connStr, IID_IDebugClient, (void**)&client); + printf("DebugConnect returned: 0x%08lX\n", hr); + + if (SUCCEEDED(hr) && client) { + printf("Connected! Getting IDebugDataSpaces...\n"); + + IDebugDataSpaces* ds = nullptr; + hr = client->QueryInterface(IID_IDebugDataSpaces, (void**)&ds); + printf("QueryInterface(IDebugDataSpaces) = 0x%08lX\n", hr); + + if (ds) { + IDebugControl* ctrl = nullptr; + client->QueryInterface(IID_IDebugControl, (void**)&ctrl); + + if (ctrl) { + printf("Waiting for event...\n"); + hr = ctrl->WaitForEvent(0, 5000); + printf("WaitForEvent = 0x%08lX\n", hr); + ctrl->Release(); + } + + // Try to read 2 bytes + IDebugSymbols* sym = nullptr; + client->QueryInterface(IID_IDebugSymbols, (void**)&sym); + if (sym) { + ULONG numMods = 0, numUnloaded = 0; + hr = sym->GetNumberModules(&numMods, &numUnloaded); + printf("GetNumberModules = 0x%08lX, numMods=%lu\n", hr, numMods); + + if (numMods > 0) { + ULONG64 base = 0; + hr = sym->GetModuleByIndex(0, &base); + printf("Module[0] base = 0x%llX (hr=0x%08lX)\n", base, hr); + + if (SUCCEEDED(hr) && base) { + uint8_t buf[4] = {}; + ULONG got = 0; + hr = ds->ReadVirtual(base, buf, 4, &got); + printf("ReadVirtual(%llX, 4) = 0x%08lX, got=%lu, data=[%02X %02X %02X %02X]\n", + base, hr, got, buf[0], buf[1], buf[2], buf[3]); + } + } + sym->Release(); + } + ds->Release(); + } + client->Release(); + } else { + printf("DebugConnect FAILED. hr=0x%08lX\n", hr); + } + + return 0; +} diff --git a/tests/test_theme.cpp b/tests/test_theme.cpp index 85b35d0..19c6e38 100644 --- a/tests/test_theme.cpp +++ b/tests/test_theme.cpp @@ -92,9 +92,16 @@ private slots: void themeManagerHasBuiltIns() { auto& tm = ThemeManager::instance(); auto all = tm.themes(); - QVERIFY(all.size() >= 2); + QVERIFY(all.size() >= 3); QCOMPARE(all[0].name, QString("Reclass Dark")); - QCOMPARE(all[1].name, QString("Warm")); + // VS2022 Dark and Warm are also loaded (order depends on filename sort) + bool hasVs = false, hasWarm = false; + for (const auto& t : all) { + if (t.name == "VS2022 Dark") hasVs = true; + if (t.name == "Warm") hasWarm = true; + } + QVERIFY(hasVs); + QVERIFY(hasWarm); } void themeManagerSwitch() { diff --git a/tests/test_windbg_provider.cpp b/tests/test_windbg_provider.cpp new file mode 100644 index 0000000..337f250 --- /dev/null +++ b/tests/test_windbg_provider.cpp @@ -0,0 +1,463 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "providers/provider.h" +#include "../plugins/WinDbgMemory/WinDbgMemoryPlugin.h" + +#ifdef _WIN32 +#include +#include +#include +#include +#endif + +using namespace rcx; + +static const char* CDB_PATH = "C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x64\\cdb.exe"; +static const int DBG_PORT = 5055; + +class TestWinDbgProvider : public QObject { + Q_OBJECT + +private: + QProcess* m_cdbProcess = nullptr; + uint32_t m_notepadPid = 0; + bool m_weSpawnedNotepad = false; + QString m_connString; + + static uint32_t findProcess(const wchar_t* name) + { +#ifdef _WIN32 + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snap == INVALID_HANDLE_VALUE) return 0; + PROCESSENTRY32W entry; + entry.dwSize = sizeof(entry); + uint32_t pid = 0; + if (Process32FirstW(snap, &entry)) { + do { + if (_wcsicmp(entry.szExeFile, name) == 0) { + pid = entry.th32ProcessID; + break; + } + } while (Process32NextW(snap, &entry)); + } + CloseHandle(snap); + return pid; +#else + Q_UNUSED(name); return 0; +#endif + } + + static uint32_t launchNotepad() + { +#ifdef _WIN32 + STARTUPINFOW si{}; + si.cb = sizeof(si); + PROCESS_INFORMATION pi{}; + if (CreateProcessW(L"C:\\Windows\\notepad.exe", nullptr, nullptr, nullptr, + FALSE, 0, nullptr, nullptr, &si, &pi)) { + WaitForInputIdle(pi.hProcess, 3000); + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + return pi.dwProcessId; + } + return 0; +#else + return 0; +#endif + } + + static void terminateProcess(uint32_t pid) + { +#ifdef _WIN32 + HANDLE h = OpenProcess(PROCESS_TERMINATE, FALSE, pid); + if (h) { TerminateProcess(h, 0); CloseHandle(h); } +#else + Q_UNUSED(pid); +#endif + } + +private slots: + + // ── Fixture ── + + /// Try a quick DebugConnect to see if the port is already serving. + static bool canConnect(const QString& connStr) + { +#ifdef _WIN32 + IDebugClient* probe = nullptr; + QByteArray utf8 = connStr.toUtf8(); + HRESULT hr = DebugConnect(utf8.constData(), IID_IDebugClient, (void**)&probe); + if (SUCCEEDED(hr) && probe) { + probe->EndSession(DEBUG_END_DISCONNECT); + probe->Release(); + return true; + } + return false; +#else + Q_UNUSED(connStr); + return false; +#endif + } + + void initTestCase() + { + m_connString = QString("tcp:Port=%1,Server=localhost").arg(DBG_PORT); + + // If a debug server is already listening (e.g. WinDbg with .server), + // skip launching our own cdb.exe. + if (canConnect(m_connString)) { + qDebug() << "Debug server already running on port" << DBG_PORT << "— using it"; + return; + } + + // No server running — launch cdb ourselves + m_notepadPid = findProcess(L"notepad.exe"); + if (m_notepadPid == 0) { + m_notepadPid = launchNotepad(); + m_weSpawnedNotepad = true; + } + QVERIFY2(m_notepadPid != 0, "Need notepad.exe running"); + qDebug() << "Using notepad.exe PID:" << m_notepadPid; + + m_cdbProcess = new QProcess(this); + QStringList args; + args << "-server" << QString("tcp:port=%1").arg(DBG_PORT) + << "-pv" + << "-p" << QString::number(m_notepadPid); + + m_cdbProcess->setProgram(CDB_PATH); + m_cdbProcess->setArguments(args); + m_cdbProcess->start(); + + QVERIFY2(m_cdbProcess->waitForStarted(5000), "Failed to start cdb.exe"); + QThread::sleep(3); + + qDebug() << "cdb.exe debug server started on port" << DBG_PORT; + } + + void cleanupTestCase() + { + if (m_cdbProcess) { + m_cdbProcess->write("q\n"); + if (!m_cdbProcess->waitForFinished(5000)) + m_cdbProcess->kill(); + delete m_cdbProcess; + m_cdbProcess = nullptr; + } + + if (m_weSpawnedNotepad && m_notepadPid) + terminateProcess(m_notepadPid); + } + + // ── Plugin metadata ── + + void plugin_name() + { + WinDbgMemoryPlugin plugin; + QCOMPARE(plugin.Name(), std::string("WinDbg Memory")); + } + + void plugin_version() + { + WinDbgMemoryPlugin plugin; + QCOMPARE(plugin.Version(), std::string("2.0.0")); + } + + void plugin_canHandle_tcp() + { + WinDbgMemoryPlugin plugin; + QVERIFY(plugin.canHandle("tcp:Port=5055,Server=localhost")); + QVERIFY(plugin.canHandle("TCP:Port=1234,Server=10.0.0.1")); + } + + void plugin_canHandle_npipe() + { + WinDbgMemoryPlugin plugin; + QVERIFY(plugin.canHandle("npipe:Pipe=test,Server=localhost")); + } + + void plugin_canHandle_pid() + { + WinDbgMemoryPlugin plugin; + QVERIFY(plugin.canHandle("pid:1234")); + } + + void plugin_canHandle_dump() + { + WinDbgMemoryPlugin plugin; + QVERIFY(plugin.canHandle("dump:C:/test.dmp")); + } + + void plugin_canHandle_invalid() + { + WinDbgMemoryPlugin plugin; + QVERIFY(!plugin.canHandle("")); + QVERIFY(!plugin.canHandle("1234")); + QVERIFY(!plugin.canHandle("file:///test.bin")); + } + + // ── Connection failure ── + + void provider_connect_badPort() + { + WinDbgMemoryProvider prov("tcp:Port=59999,Server=localhost"); + QVERIFY(!prov.isValid()); + QCOMPARE(prov.size(), 0); + } + + void provider_connect_badPipe() + { + WinDbgMemoryProvider prov("npipe:Pipe=nonexistent_reclass_test_pipe,Server=localhost"); + QVERIFY(!prov.isValid()); + QCOMPARE(prov.size(), 0); + } + + void plugin_createProvider_badConnection() + { + WinDbgMemoryPlugin plugin; + QString error; + auto prov = plugin.createProvider("tcp:Port=59999,Server=localhost", &error); + QVERIFY(prov == nullptr); + QVERIFY(!error.isEmpty()); + } + + // ── Connect and read (main thread) ── + + void provider_connect_valid() + { + WinDbgMemoryProvider prov(m_connString); + QVERIFY2(prov.isValid(), "Should connect to cdb debug server"); + QCOMPARE(prov.kind(), QStringLiteral("WinDbg")); + QVERIFY(prov.size() > 0); + } + + void provider_name() + { + WinDbgMemoryProvider prov(m_connString); + QVERIFY(prov.isValid()); + QVERIFY(!prov.name().isEmpty()); + qDebug() << "Provider name:" << prov.name(); + } + + void provider_isLive() + { + WinDbgMemoryProvider prov(m_connString); + QVERIFY(prov.isValid()); + QVERIFY(prov.isLive()); + } + + void provider_baseAddress() + { + WinDbgMemoryProvider prov(m_connString); + QVERIFY(prov.isValid()); + QVERIFY2(prov.base() != 0, "Should have a non-zero base from first module"); + qDebug() << "Base address:" << QString("0x%1").arg(prov.base(), 0, 16); + } + + void provider_setBase() + { + WinDbgMemoryProvider prov(m_connString); + QVERIFY(prov.isValid()); + uint64_t orig = prov.base(); + prov.setBase(0x1000); + QCOMPARE(prov.base(), (uint64_t)0x1000); + prov.setBase(orig); + QCOMPARE(prov.base(), orig); + } + + // ── Read: MZ header on main thread ── + + void provider_read_mz_mainThread() + { + WinDbgMemoryProvider prov(m_connString); + QVERIFY(prov.isValid()); + + uint8_t buf[2] = {}; + bool ok = prov.read(0, buf, 2); + QVERIFY2(ok, "Failed to read from debug session (main thread)"); + QCOMPARE(buf[0], (uint8_t)'M'); + QCOMPARE(buf[1], (uint8_t)'Z'); + } + + // ── Read: MZ header from a background thread (the actual failure case) ── + + void provider_read_mz_backgroundThread() + { + WinDbgMemoryProvider prov(m_connString); + QVERIFY(prov.isValid()); + + // Simulate what the controller's refresh does: + // read from a QtConcurrent worker thread. + QFuture future = QtConcurrent::run([&prov]() -> QByteArray { + return prov.readBytes(0, 128); + }); + future.waitForFinished(); + QByteArray data = future.result(); + + QCOMPARE(data.size(), 128); + QCOMPARE((uint8_t)data[0], (uint8_t)'M'); + QCOMPARE((uint8_t)data[1], (uint8_t)'Z'); + } + + // ── Read: bulk data from background thread ── + + void provider_read_4k_backgroundThread() + { + WinDbgMemoryProvider prov(m_connString); + QVERIFY(prov.isValid()); + + QFuture future = QtConcurrent::run([&prov]() -> QByteArray { + return prov.readBytes(0, 4096); + }); + future.waitForFinished(); + QByteArray data = future.result(); + + QCOMPARE(data.size(), 4096); + QCOMPARE((uint8_t)data[0], (uint8_t)'M'); + QCOMPARE((uint8_t)data[1], (uint8_t)'Z'); + + // Verify it's not all zeros (the old failure mode) + bool allZero = true; + for (int i = 0; i < data.size(); ++i) { + if (data[i] != 0) { allZero = false; break; } + } + QVERIFY2(!allZero, "Data is all zeros — background thread read failed"); + } + + // ── Multiple sequential background reads (simulates refresh timer) ── + + void provider_read_multipleRefreshes() + { + WinDbgMemoryProvider prov(m_connString); + QVERIFY(prov.isValid()); + + for (int i = 0; i < 5; ++i) { + QFuture future = QtConcurrent::run([&prov]() -> QByteArray { + return prov.readBytes(0, 128); + }); + future.waitForFinished(); + QByteArray data = future.result(); + QCOMPARE(data.size(), 128); + QCOMPARE((uint8_t)data[0], (uint8_t)'M'); + QCOMPARE((uint8_t)data[1], (uint8_t)'Z'); + } + } + + // ── Read helpers ── + + void provider_readU16() + { + WinDbgMemoryProvider prov(m_connString); + QVERIFY(prov.isValid()); + QCOMPARE(prov.readU16(0), (uint16_t)0x5A4D); // "MZ" little-endian + } + + void provider_read_peSignature() + { + WinDbgMemoryProvider prov(m_connString); + QVERIFY(prov.isValid()); + + uint32_t peOffset = prov.readU32(0x3C); + QVERIFY2(peOffset > 0 && peOffset < 0x1000, "PE offset should be reasonable"); + + uint8_t sig[4] = {}; + bool ok = prov.read(peOffset, sig, 4); + QVERIFY(ok); + QCOMPARE(sig[0], (uint8_t)'P'); + QCOMPARE(sig[1], (uint8_t)'E'); + QCOMPARE(sig[2], (uint8_t)0); + QCOMPARE(sig[3], (uint8_t)0); + } + + // ── Edge cases ── + + void provider_read_zeroLength() + { + WinDbgMemoryProvider prov(m_connString); + QVERIFY(prov.isValid()); + uint8_t buf = 0xFF; + QVERIFY(!prov.read(0, &buf, 0)); + } + + void provider_read_negativeLength() + { + WinDbgMemoryProvider prov(m_connString); + QVERIFY(prov.isValid()); + uint8_t buf = 0xFF; + QVERIFY(!prov.read(0, &buf, -1)); + } + + // ── getSymbol ── + + void provider_getSymbol() + { + WinDbgMemoryProvider prov(m_connString); + QVERIFY(prov.isValid()); + QString sym = prov.getSymbol(0); + qDebug() << "Symbol at base+0:" << sym; + // Should not crash; may or may not resolve + } + + void provider_getSymbol_backgroundThread() + { + WinDbgMemoryProvider prov(m_connString); + QVERIFY(prov.isValid()); + + QFuture future = QtConcurrent::run([&prov]() -> QString { + return prov.getSymbol(0); + }); + future.waitForFinished(); + // Should not crash from background thread + qDebug() << "Symbol (bg thread):" << future.result(); + } + + // ── createProvider full flow ── + + void plugin_createProvider_valid() + { + WinDbgMemoryPlugin plugin; + QString error; + auto prov = plugin.createProvider(m_connString, &error); + QVERIFY2(prov != nullptr, qPrintable("createProvider failed: " + error)); + QVERIFY(prov->isValid()); + + uint8_t mz[2] = {}; + QVERIFY(prov->read(0, mz, 2)); + QCOMPARE(mz[0], (uint8_t)'M'); + QCOMPARE(mz[1], (uint8_t)'Z'); + } + + // ── Multiple concurrent connections ── + + void provider_multipleConcurrent() + { + WinDbgMemoryProvider prov1(m_connString); + WinDbgMemoryProvider prov2(m_connString); + + QVERIFY(prov1.isValid()); + QVERIFY(prov2.isValid()); + + QCOMPARE(prov1.readU16(0), (uint16_t)0x5A4D); + QCOMPARE(prov2.readU16(0), (uint16_t)0x5A4D); + } + + // ── Factory ── + + void factory_createPlugin() + { + IPlugin* raw = CreatePlugin(); + QVERIFY(raw != nullptr); + QCOMPARE(raw->Type(), IPlugin::ProviderPlugin); + QCOMPARE(raw->Name(), std::string("WinDbg Memory")); + delete raw; + } +}; + +QTEST_MAIN(TestWinDbgProvider) +#include "test_windbg_provider.moc"