mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
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
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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<ssize_t>(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<ssize_t>(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/<pid>/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<rcx::Provider> ProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
|
||||
std::unique_ptr<rcx::Provider> 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<rcx::Provider> ProcessMemoryPlugin::createProvider(const QString
|
||||
|
||||
QString name = parts.size() > 1 ? parts[1] : QString("PID %1").arg(pid);
|
||||
|
||||
auto provider = std::make_unique<ProcessMemoryProvider>(pid, name);
|
||||
auto provider = std::make_unique<ProcessMemoryWindowsProvider>(pid, name);
|
||||
if (!provider->isValid())
|
||||
{
|
||||
if (errorMsg)
|
||||
@@ -352,7 +352,7 @@ std::unique_ptr<rcx::Provider> 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<PluginProcessInfo> pluginProcesses = enumerateProcesses();
|
||||
@@ -440,7 +440,7 @@ bool ProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
return false;
|
||||
}
|
||||
|
||||
QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
|
||||
QVector<PluginProcessInfo> ProcessMemoryWindowsPlugin::enumerateProcesses()
|
||||
{
|
||||
QVector<PluginProcessInfo> processes;
|
||||
|
||||
@@ -543,5 +543,5 @@ QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
|
||||
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
|
||||
{
|
||||
return new ProcessMemoryPlugin();
|
||||
return new ProcessMemoryWindowsPlugin();
|
||||
}
|
||||
@@ -5,14 +5,14 @@
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
34
plugins/WinDbgMemory/CMakeLists.txt
Normal file
34
plugins/WinDbgMemory/CMakeLists.txt
Normal file
@@ -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"
|
||||
)
|
||||
510
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp
Normal file
510
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp
Normal file
@@ -0,0 +1,510 @@
|
||||
#include "WinDbgMemoryPlugin.h"
|
||||
|
||||
#include <QStyle>
|
||||
#include <QApplication>
|
||||
#include <QMessageBox>
|
||||
#include <QDialog>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QDebug>
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <initguid.h>
|
||||
#include <dbgeng.h>
|
||||
#pragma comment(lib, "dbgeng.lib")
|
||||
#endif
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Thread dispatch helper
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
template<typename Fn>
|
||||
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>(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<void*>(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<rcx::Provider> WinDbgMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
|
||||
{
|
||||
auto provider = std::make_unique<WinDbgMemoryProvider>(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();
|
||||
}
|
||||
122
plugins/WinDbgMemory/WinDbgMemoryPlugin.h
Normal file
122
plugins/WinDbgMemory/WinDbgMemoryPlugin.h
Normal file
@@ -0,0 +1,122 @@
|
||||
#pragma once
|
||||
#include "../../src/iplugin.h"
|
||||
#include "../../src/core.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
|
||||
// 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<typename Fn>
|
||||
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<rcx::Provider> 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();
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<QWidget*>(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<T, cmd::ChangeArrayMeta>) {
|
||||
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<QByteArray>(this);
|
||||
connect(m_refreshWatcher, &QFutureWatcher<QByteArray>::finished,
|
||||
m_refreshWatcher = new QFutureWatcher<PageMap>(this);
|
||||
connect(m_refreshWatcher, &QFutureWatcher<PageMap>::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<uint64_t>& visited,
|
||||
QVector<QPair<uint64_t,int>>& 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<int> 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<QPair<uint64_t,int>> ranges;
|
||||
ranges.append({0, extent});
|
||||
|
||||
if (m_snapshotProv) {
|
||||
QSet<uint64_t> 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<int>(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<int64_t>(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<SnapshotProvider>(m_doc->provider, std::move(newData));
|
||||
m_snapshotProv = std::make_unique<SnapshotProvider>(
|
||||
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<int>::max());
|
||||
if (treeExtent > 0) return static_cast<int>(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();
|
||||
}
|
||||
|
||||
|
||||
@@ -141,10 +141,11 @@ private:
|
||||
TypeSelectorPopup* m_cachedPopup = nullptr;
|
||||
|
||||
// ── Auto-refresh state ──
|
||||
using PageMap = QHash<uint64_t, QByteArray>;
|
||||
QTimer* m_refreshTimer = nullptr;
|
||||
QFutureWatcher<QByteArray>* m_refreshWatcher = nullptr;
|
||||
QFutureWatcher<PageMap>* m_refreshWatcher = nullptr;
|
||||
std::unique_ptr<SnapshotProvider> m_snapshotProv;
|
||||
QByteArray m_prevSnapshot;
|
||||
PageMap m_prevPages;
|
||||
QSet<int64_t> 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<uint64_t>& visited,
|
||||
QVector<QPair<uint64_t,int>>& ranges) const;
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <QCursor>
|
||||
#include <QMenu>
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -1,28 +1,65 @@
|
||||
#pragma once
|
||||
#include "provider.h"
|
||||
#include <QHash>
|
||||
#include <memory>
|
||||
|
||||
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<Provider> m_real;
|
||||
QByteArray m_data;
|
||||
QHash<uint64_t, QByteArray> 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<Provider> real, QByteArray snapshot)
|
||||
: m_real(std::move(real)), m_data(std::move(snapshot)) {}
|
||||
using PageMap = QHash<uint64_t, QByteArray>;
|
||||
|
||||
SnapshotProvider(std::shared_ptr<Provider> 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<char*>(buf);
|
||||
uint64_t cur = addr;
|
||||
int remaining = len;
|
||||
while (remaining > 0) {
|
||||
uint64_t pageAddr = cur & kPageMask;
|
||||
int pageOff = static_cast<int>(cur - pageAddr);
|
||||
int chunk = qMin(remaining, static_cast<int>(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<uint64_t>(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<const char*>(buf);
|
||||
uint64_t cur = addr;
|
||||
int remaining = len;
|
||||
while (remaining > 0) {
|
||||
uint64_t pageAddr = cur & kPageMask;
|
||||
int pageOff = static_cast<int>(cur - pageAddr);
|
||||
int chunk = qMin(remaining, static_cast<int>(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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
185
tests/test_com_security.cpp
Normal file
185
tests/test_com_security.cpp
Normal file
@@ -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 <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <objbase.h>
|
||||
#include <initguid.h>
|
||||
#include <dbgeng.h>
|
||||
#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
|
||||
}
|
||||
65
tests/test_dbgconnect.cpp
Normal file
65
tests/test_dbgconnect.cpp
Normal file
@@ -0,0 +1,65 @@
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <windows.h>
|
||||
#include <initguid.h>
|
||||
#include <dbgeng.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
463
tests/test_windbg_provider.cpp
Normal file
463
tests/test_windbg_provider.cpp
Normal file
@@ -0,0 +1,463 @@
|
||||
#include <QTest>
|
||||
#include <QByteArray>
|
||||
#include <QProcess>
|
||||
#include <QThread>
|
||||
#include <QtConcurrent>
|
||||
#include <QFuture>
|
||||
#include <cstring>
|
||||
|
||||
#include "providers/provider.h"
|
||||
#include "../plugins/WinDbgMemory/WinDbgMemoryPlugin.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <tlhelp32.h>
|
||||
#include <initguid.h>
|
||||
#include <dbgeng.h>
|
||||
#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<QByteArray> 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<QByteArray> 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<QByteArray> 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<QString> 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"
|
||||
Reference in New Issue
Block a user