Compare commits

...

9 Commits

Author SHA1 Message Date
IChooseYou
078a6028f0 fix: WinDbg provider stops auto-selecting module, new tabs inherit source
- WinDbg provider no longer picks arbitrary module[0] as name/base
  (was showing "WS2_32" for kernel dumps). Name is now generic
  "WinDbg (Live)" / "WinDbg (Dump)", base stays 0 so controller
  doesn't override user's address.
- Added throttled read failure logging to WinDbg provider.
- New tabs (File→New Class, workspace right-click) inherit the
  current tab's source/provider so users don't have to re-attach.
- Updated WinDbg provider tests for new behavior.
2026-02-23 08:08:46 -07:00
IChooseYou
67218d3e48 fix: move payload init out of DllMain to avoid loader lock deadlock
RcxPayloadInit() is now an exported function called after LoadLibrary
returns. DllMain only handles cleanup on detach. Timer queue creation
under the loader lock was crashing target processes.
2026-02-22 13:14:01 -07:00
IChooseYou
f651edd740 feat: remove nonce/bootstrap from remote process IPC, use PID-only naming
Shared memory names simplified to Local\RCX_SHM_<pid>, no bootstrap
handshake needed. Payload uses CreateTimerQueueTimer (10ms poll) instead
of a dedicated server thread.
2026-02-22 11:36:24 -07:00
IChooseYou
25aaace382 Merge remote-tracking branch 'origin/fix-issue-2' 2026-02-22 11:09:05 -07:00
Sen66
b5ddb042b8 Try to fix missing DLLs at CI windows builds
Fix https://github.com/IChooseYou/Reclass/issues/2
2026-02-22 19:06:50 +01:00
IChooseYou
e900dea836 fix: menu bar item paint no longer covers title bar bottom border
Take full ownership of CE_MenuBarItem in MenuBarStyle — never
delegate to Fusion which unconditionally fills the full item rect.
Non-hovered items draw text only (transparent bg lets parent border
show through). Hover/pressed states fill adjusted rect leaving 1px
for the border. Pressed state uses darker(130) for visual feedback.
2026-02-22 11:05:54 -07:00
IChooseYou
b647a334bc docs: fix Remote Process description 2026-02-22 09:14:04 -07:00
IChooseYou
fc390bc1f7 docs: add Remote Process data source to README 2026-02-22 09:06:32 -07:00
IChooseYou
7efe740ec1 fix: hover invisible when theme.hover == background, remove CSS on QMenuBar
Move hover color fixup into Theme::fromJson so all consumers get a
visible hover automatically. Remove duplicate lighter(130) fallback
from applyGlobalTheme. Replace QMenuBar CSS with QPalette so
MenuBarStyle QProxyStyle is not bypassed. Add PE_PanelMenuBar and
CE_MenuBarEmptyArea suppression so Fusion never paints over the
title bar background.
2026-02-22 08:58:57 -07:00
22 changed files with 843 additions and 443 deletions

View File

@@ -49,6 +49,7 @@ jobs:
- name: Package release zip
shell: bash
run: |
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
mkdir -p release
cp build/Reclass.exe release/
cp build/ReclassMcpBridge.exe release/
@@ -57,6 +58,7 @@ jobs:
cp -r build/styles release/ 2>/dev/null || true
cp -r build/imageformats release/ 2>/dev/null || true
cp -r build/iconengines release/ 2>/dev/null || true
windeployqt --no-translations --no-system-d3d-compiler --no-opengl-sw release/Reclass.exe
mkdir -p release/Plugins
cp build/Plugins/*.dll release/Plugins/ 2>/dev/null || true
cp -r build/themes release/ 2>/dev/null || true

View File

@@ -372,6 +372,21 @@ if(BUILD_TESTING)
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
add_test(NAME test_options_dialog COMMAND test_options_dialog)
add_executable(test_source_provider tests/test_source_provider.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}
src/resources.qrc)
target_include_directories(test_source_provider PRIVATE src third_party/fadec)
target_link_libraries(test_source_provider PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test ${QT}::Svg
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_source_provider PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_source_provider COMMAND test_source_provider)
if(WIN32)
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
@@ -381,6 +396,19 @@ if(BUILD_TESTING)
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
endif()
add_executable(bench_large_class tests/bench_large_class.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
src/providerregistry.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(bench_large_class PRIVATE src third_party/fadec)
target_link_libraries(bench_large_class PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(bench_large_class PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME bench_large_class COMMAND bench_large_class)
# 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)

View File

@@ -42,6 +42,7 @@ Built with C++17, Qt 6, and QScintilla. The entire editor surface is rendered as
- **File** — open any binary file and inspect its contents as structured data
- **Process** — attach to a live process and read its memory in real time
- **Remote Process** — read another process's memory via shared memory
- **WinDbg** — load `.dmp` crash dump files or connect to live debugging sessions
---

View File

@@ -5,9 +5,7 @@
#include <QStyle>
#include <QApplication>
#include <QMessageBox>
#include <QInputDialog>
#include <QPushButton>
#include <QUuid>
#include <QDir>
#include <QFileInfo>
#include <QPixmap>
@@ -65,12 +63,12 @@ struct IpcClient {
/* ── connect / disconnect ──────────────────────────────────────── */
bool connect(uint32_t pid, const QByteArray& nonce, int timeoutMs = 5000)
bool connect(uint32_t pid, int timeoutMs = 5000)
{
char shmName[128], reqName[128], rspName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce.constData());
rcx_rpc_req_name(reqName, sizeof(reqName), pid, nonce.constData());
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid, nonce.constData());
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
#ifdef _WIN32
/* poll for shared memory to appear (payload creating it) */
@@ -373,51 +371,6 @@ static QString payloadPath()
#endif
}
/* Create bootstrap shared memory with the nonce */
static bool createBootstrapShm(uint32_t pid, const QByteArray& nonce)
{
char bootName[128];
rcx_rpc_boot_name(bootName, sizeof(bootName), pid);
#ifdef _WIN32
HANDLE hBoot = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, 0, RCX_RPC_BOOT_SIZE,
bootName);
if (!hBoot) return false;
auto* view = static_cast<RcxRpcBootHeader*>(
MapViewOfFile(hBoot, FILE_MAP_WRITE, 0, 0, RCX_RPC_BOOT_SIZE));
if (!view) { CloseHandle(hBoot); return false; }
memset(view, 0, RCX_RPC_BOOT_SIZE);
view->nonceLength = (uint32_t)nonce.size();
memcpy(view->nonce, nonce.constData(), qMin(nonce.size(), 59));
UnmapViewOfFile(view);
/* keep hBoot open until payload reads it (payload unlinks after reading) */
/* leak intentional: closed when process exits or payload consumes it */
return true;
#else
int fd = shm_open(bootName, O_CREAT | O_RDWR, 0600);
if (fd < 0) return false;
if (ftruncate(fd, RCX_RPC_BOOT_SIZE) != 0) { close(fd); return false; }
void* view = mmap(nullptr, RCX_RPC_BOOT_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
close(fd);
if (view == MAP_FAILED) return false;
auto* boot = static_cast<RcxRpcBootHeader*>(view);
memset(boot, 0, RCX_RPC_BOOT_SIZE);
boot->nonceLength = (uint32_t)nonce.size();
memcpy(boot->nonce, nonce.constData(), qMin(nonce.size(), 59));
munmap(view, RCX_RPC_BOOT_SIZE);
/* payload unlinks after consuming */
return true;
#endif
}
#ifdef _WIN32
/* ── Windows injection: CreateRemoteThread + LoadLibraryA ─────────── */
@@ -447,7 +400,7 @@ static bool injectPayload(uint32_t pid, QString* errorMsg)
WriteProcessMemory(hProc, remotePath, pathUtf8.constData(), pathLen, nullptr);
/* create remote thread calling LoadLibraryA(path) */
/* Step 1: LoadLibraryA — loads the DLL (DllMain is minimal) */
HMODULE hK32 = GetModuleHandleA("kernel32.dll");
auto pLoadLib = reinterpret_cast<LPTHREAD_START_ROUTINE>(
GetProcAddress(hK32, "LoadLibraryA"));
@@ -464,19 +417,81 @@ static bool injectPayload(uint32_t pid, QString* errorMsg)
WaitForSingleObject(hThread, 10000);
/* check if LoadLibrary returned non-null */
DWORD exitCode = 0;
GetExitCodeThread(hThread, &exitCode);
CloseHandle(hThread);
VirtualFreeEx(hProc, remotePath, 0, MEM_RELEASE);
CloseHandle(hProc);
if (exitCode == 0) {
CloseHandle(hProc);
if (errorMsg) *errorMsg = QStringLiteral("LoadLibrary returned NULL in target.\n"
"Ensure rcx_payload.dll is in: %1").arg(path);
return false;
}
/* Step 2: Call RcxPayloadInit() — safe to create timer queues now
(loader lock is no longer held after LoadLibrary returned) */
HMODULE hPayloadRemote = (HMODULE)(uintptr_t)exitCode;
auto pGetProcAddr = reinterpret_cast<FARPROC(WINAPI*)(HMODULE, LPCSTR)>(
GetProcAddress(hK32, "GetProcAddress"));
/* Write "RcxPayloadInit\0" into target, call GetProcAddress remotely */
const char initName[] = "RcxPayloadInit";
void* remoteInitName = VirtualAllocEx(hProc, nullptr, sizeof(initName),
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (remoteInitName) {
WriteProcessMemory(hProc, remoteInitName, initName, sizeof(initName), nullptr);
/* We need to call GetProcAddress(hPayload, "RcxPayloadInit") then call the result.
Simpler approach: write small shellcode that does both calls. */
uint8_t shellcode[128];
int off = 0;
/* sub rsp, 40 ; shadow space + alignment */
shellcode[off++] = 0x48; shellcode[off++] = 0x83; shellcode[off++] = 0xEC; shellcode[off++] = 0x28;
/* mov rcx, hPayloadRemote ; first arg = module handle */
shellcode[off++] = 0x48; shellcode[off++] = 0xB9;
uint64_t hMod = (uint64_t)(uintptr_t)hPayloadRemote;
memcpy(shellcode + off, &hMod, 8); off += 8;
/* mov rdx, remoteInitName ; second arg = "RcxPayloadInit" */
shellcode[off++] = 0x48; shellcode[off++] = 0xBA;
uint64_t pName = (uint64_t)(uintptr_t)remoteInitName;
memcpy(shellcode + off, &pName, 8); off += 8;
/* mov rax, GetProcAddress */
shellcode[off++] = 0x48; shellcode[off++] = 0xB8;
uint64_t pGPA = (uint64_t)(uintptr_t)pGetProcAddr;
memcpy(shellcode + off, &pGPA, 8); off += 8;
/* call rax ; rax = RcxPayloadInit */
shellcode[off++] = 0xFF; shellcode[off++] = 0xD0;
/* test rax, rax */
shellcode[off++] = 0x48; shellcode[off++] = 0x85; shellcode[off++] = 0xC0;
/* jz skip (jump over the call if null) */
shellcode[off++] = 0x74; shellcode[off++] = 0x02;
/* call rax ; RcxPayloadInit() */
shellcode[off++] = 0xFF; shellcode[off++] = 0xD0;
/* skip: add rsp, 40 */
shellcode[off++] = 0x48; shellcode[off++] = 0x83; shellcode[off++] = 0xC4; shellcode[off++] = 0x28;
/* ret */
shellcode[off++] = 0xC3;
void* remoteCode = VirtualAllocEx(hProc, nullptr, (SIZE_T)off,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (remoteCode) {
WriteProcessMemory(hProc, remoteCode, shellcode, (SIZE_T)off, nullptr);
HANDLE hThread2 = CreateRemoteThread(hProc, nullptr, 0,
(LPTHREAD_START_ROUTINE)remoteCode, nullptr, 0, nullptr);
if (hThread2) {
WaitForSingleObject(hThread2, 10000);
CloseHandle(hThread2);
}
VirtualFreeEx(hProc, remoteCode, 0, MEM_RELEASE);
}
VirtualFreeEx(hProc, remoteInitName, 0, MEM_RELEASE);
}
CloseHandle(hProc);
return true;
}
@@ -717,24 +732,23 @@ bool RemoteProcessMemoryPlugin::canHandle(const QString& target) const
std::unique_ptr<rcx::Provider>
RemoteProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
{
/* target = "rpm:{pid}:{nonce}:{name}" */
/* target = "rpm:{pid}:{name}" */
QStringList parts = target.split(':');
if (parts.size() < 4 || parts[0] != QStringLiteral("rpm")) {
if (parts.size() < 3 || parts[0] != QStringLiteral("rpm")) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid target: ") + target;
return nullptr;
}
bool ok;
uint32_t pid = parts[1].toUInt(&ok);
QString nonce = parts[2];
QString name = parts.mid(3).join(':'); /* name may contain colons */
uint32_t pid = parts[1].toUInt(&ok);
QString name = parts.mid(2).join(':'); /* name may contain colons */
if (!ok || pid == 0) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid PID in target.");
return nullptr;
}
auto ipc = getOrCreateConnection(pid, nonce, errorMsg);
auto ipc = getOrCreateConnection(pid, errorMsg);
if (!ipc) return nullptr;
return std::make_unique<RemoteProcessProvider>(pid, name, ipc);
@@ -745,7 +759,7 @@ uint64_t RemoteProcessMemoryPlugin::getInitialBaseAddress(const QString& target)
/* Read imageBase directly from the shared-memory header -- zero IPC cost.
The payload filled it at init from PEB->Ldr (Win) / /proc/self/maps (Linux). */
QStringList parts = target.split(':');
if (parts.size() < 3 || parts[0] != QStringLiteral("rpm"))
if (parts.size() < 2 || parts[0] != QStringLiteral("rpm"))
return 0;
bool ok;
@@ -793,35 +807,17 @@ bool RemoteProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
QAbstractButton* clicked = box.clickedButton();
if (clicked == injectBtn) {
/* generate nonce */
QString nonce = QUuid::createUuid().toString(QUuid::Id128).left(16);
QByteArray nonceUtf8 = nonce.toUtf8();
/* create bootstrap, inject */
if (!createBootstrapShm(pid, nonceUtf8)) {
QMessageBox::critical(parent, QStringLiteral("Error"),
QStringLiteral("Failed to create bootstrap shared memory."));
return false;
}
QString injectErr;
if (!injectPayload(pid, &injectErr)) {
QMessageBox::critical(parent, QStringLiteral("Injection Failed"), injectErr);
return false;
}
*target = QStringLiteral("rpm:%1:%2:%3").arg(pid).arg(nonce, name);
*target = QStringLiteral("rpm:%1:%2").arg(pid).arg(name);
return true;
}
else if (clicked == connectBtn) {
bool ok;
QString nonce = QInputDialog::getText(parent,
QStringLiteral("Connect to Payload"),
QStringLiteral("Enter the payload nonce:"),
QLineEdit::Normal, QString(), &ok);
if (!ok || nonce.isEmpty()) return false;
*target = QStringLiteral("rpm:%1:%2:%3").arg(pid).arg(nonce, name);
*target = QStringLiteral("rpm:%1:%2").arg(pid).arg(name);
return true;
}
@@ -903,7 +899,7 @@ QVector<PluginProcessInfo> RemoteProcessMemoryPlugin::enumerateProcesses()
std::shared_ptr<IpcClient>
RemoteProcessMemoryPlugin::getOrCreateConnection(
uint32_t pid, const QString& nonce, QString* errorMsg)
uint32_t pid, QString* errorMsg)
{
QMutexLocker lock(&m_connectionsMutex);
@@ -912,7 +908,7 @@ RemoteProcessMemoryPlugin::getOrCreateConnection(
return *it;
auto ipc = std::make_shared<IpcClient>();
if (!ipc->connect(pid, nonce.toUtf8())) {
if (!ipc->connect(pid)) {
if (errorMsg)
*errorMsg = QStringLiteral("Failed to connect IPC to PID %1.\n"
"Is the payload running?").arg(pid);

View File

@@ -77,7 +77,7 @@ public:
private:
std::shared_ptr<IpcClient> getOrCreateConnection(
uint32_t pid, const QString& nonce, QString* errorMsg);
uint32_t pid, QString* errorMsg);
mutable QMutex m_connectionsMutex;
QHash<uint32_t, std::shared_ptr<IpcClient>> m_connections;

View File

@@ -2,9 +2,8 @@
* rcx_payload -- injected into target process.
*
* Pure Win32 / POSIX, NO Qt, minimal footprint.
* Reads a nonce from bootstrap shared memory, creates the main IPC
* channel (shared memory + events/semaphores), and runs a server
* thread that handles RPC commands from the editor plugin.
* Creates the main IPC channel (shared memory + events/semaphores)
* using PID-only naming and uses a timer queue for polling.
*/
#include "../rcx_rpc_protocol.h"
@@ -18,12 +17,13 @@
#include <psapi.h>
/* ── globals ──────────────────────────────────────────────────────── */
static HANDLE g_hShm = nullptr;
static void* g_mappedView = nullptr;
static HANDLE g_hReqEvent = nullptr;
static HANDLE g_hRspEvent = nullptr;
static HANDLE g_hThread = nullptr;
static volatile LONG g_shutdown = 0;
static HANDLE g_hShm = nullptr;
static void* g_mappedView = nullptr;
static HANDLE g_hReqEvent = nullptr;
static HANDLE g_hRspEvent = nullptr;
static HANDLE g_hTimerQueue = nullptr;
static HANDLE g_hPollTimer = nullptr;
static volatile LONG g_initialized = 0;
/* ── memory safety via VirtualQuery ────────────────────────────────── */
@@ -167,135 +167,147 @@ static void handle_enum_modules(RcxRpcHeader* hdr, uint8_t* data)
hdr->status = RCX_RPC_STATUS_OK;
}
/* ── server thread ────────────────────────────────────────────────── */
/* forward declaration */
void RcxPayloadCleanup();
static DWORD WINAPI ServerThread(LPVOID)
/* ── timer callback (non-blocking poll) ───────────────────────────── */
static VOID CALLBACK RcxPollTimerCallback(PVOID, BOOLEAN)
{
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
if (!g_mappedView || !g_hReqEvent || !g_hRspEvent)
return;
/* non-blocking check: is there a pending request? */
DWORD rc = WaitForSingleObject(g_hReqEvent, 0);
if (rc != WAIT_OBJECT_0)
return;
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
auto* data = reinterpret_cast<uint8_t*>(g_mappedView) + RCX_RPC_DATA_OFFSET;
/* signal readiness */
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 1);
hdr->status = RCX_RPC_STATUS_OK;
while (!InterlockedCompareExchange(&g_shutdown, 0, 0)) {
DWORD rc = WaitForSingleObject(g_hReqEvent, 250);
if (rc == WAIT_TIMEOUT)
continue;
if (rc != WAIT_OBJECT_0)
break;
hdr->status = RCX_RPC_STATUS_OK;
switch (static_cast<RcxRpcCommand>(hdr->command)) {
case RPC_CMD_READ_BATCH: handle_read_batch(hdr, data); break;
case RPC_CMD_WRITE: handle_write(hdr, data); break;
case RPC_CMD_ENUM_MODULES: handle_enum_modules(hdr, data); break;
case RPC_CMD_PING: break;
case RPC_CMD_SHUTDOWN:
InterlockedExchange(&g_shutdown, 1);
break;
default:
hdr->status = RCX_RPC_STATUS_ERROR;
break;
}
SetEvent(g_hRspEvent);
if (static_cast<RcxRpcCommand>(hdr->command) == RPC_CMD_SHUTDOWN)
break;
switch (static_cast<RcxRpcCommand>(hdr->command)) {
case RPC_CMD_READ_BATCH: handle_read_batch(hdr, data); break;
case RPC_CMD_WRITE: handle_write(hdr, data); break;
case RPC_CMD_ENUM_MODULES: handle_enum_modules(hdr, data); break;
case RPC_CMD_PING: break;
case RPC_CMD_SHUTDOWN:
RcxPayloadCleanup();
return;
default:
hdr->status = RCX_RPC_STATUS_ERROR;
break;
}
/* mark not-ready so the host process can detect shutdown */
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 0);
return 0;
SetEvent(g_hRspEvent);
}
/* ── cleanup ──────────────────────────────────────────────────────── */
static void Cleanup(bool waitThread)
void RcxPayloadCleanup()
{
InterlockedExchange(&g_shutdown, 1);
if (!InterlockedCompareExchange(&g_initialized, 0, 0))
return;
/* wake the thread if it's blocked on REQ */
if (g_hReqEvent) SetEvent(g_hReqEvent);
if (waitThread && g_hThread) {
WaitForSingleObject(g_hThread, 2000);
/* stop the poll timer first */
if (g_hTimerQueue) {
DeleteTimerQueueEx(g_hTimerQueue, INVALID_HANDLE_VALUE); /* waits for callbacks */
g_hTimerQueue = nullptr;
g_hPollTimer = nullptr;
}
if (g_hThread) { CloseHandle(g_hThread); g_hThread = nullptr; }
if (g_mappedView){ UnmapViewOfFile(g_mappedView); g_mappedView = nullptr; }
if (g_hShm) { CloseHandle(g_hShm); g_hShm = nullptr; }
if (g_hReqEvent) { CloseHandle(g_hReqEvent); g_hReqEvent = nullptr; }
if (g_hRspEvent) { CloseHandle(g_hRspEvent); g_hRspEvent = nullptr; }
/* mark not-ready */
if (g_mappedView) {
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 0);
}
if (g_mappedView) { UnmapViewOfFile(g_mappedView); g_mappedView = nullptr; }
if (g_hShm) { CloseHandle(g_hShm); g_hShm = nullptr; }
if (g_hReqEvent) { CloseHandle(g_hReqEvent); g_hReqEvent = nullptr; }
if (g_hRspEvent) { CloseHandle(g_hRspEvent); g_hRspEvent = nullptr; }
InterlockedExchange(&g_initialized, 0);
}
/* ── DllMain ──────────────────────────────────────────────────────── */
/* ── init (called AFTER DllMain returns — safe for timer queues) ── */
BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID reserved)
extern "C" __declspec(dllexport)
bool RcxPayloadInit()
{
if (reason == DLL_PROCESS_ATTACH) {
uint32_t pid = GetCurrentProcessId();
if (InterlockedCompareExchange(&g_initialized, 1, 0) != 0)
return true; /* already initialized */
/* ── read nonce from bootstrap shm ── */
char bootName[128];
rcx_rpc_boot_name(bootName, sizeof(bootName), pid);
uint32_t pid = GetCurrentProcessId();
HANDLE hBoot = OpenFileMappingA(FILE_MAP_READ, FALSE, bootName);
if (!hBoot) return TRUE; /* no bootstrap = nothing to do */
char shmName[128], reqName[128], rspName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
auto* bootView = static_cast<const RcxRpcBootHeader*>(
MapViewOfFile(hBoot, FILE_MAP_READ, 0, 0, RCX_RPC_BOOT_SIZE));
if (!bootView) { CloseHandle(hBoot); return TRUE; }
char nonce[64] = {};
uint32_t nLen = bootView->nonceLength;
if (nLen > 59) nLen = 59;
memcpy(nonce, bootView->nonce, nLen);
nonce[nLen] = '\0';
UnmapViewOfFile(bootView);
CloseHandle(hBoot);
/* ── create main shared memory ── */
char shmName[128], reqName[128], rspName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce);
rcx_rpc_req_name(reqName, sizeof(reqName), pid, nonce);
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid, nonce);
g_hShm = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, 0, RCX_RPC_SHM_SIZE, shmName);
if (!g_hShm) return TRUE;
g_mappedView = MapViewOfFile(g_hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
if (!g_mappedView) { CloseHandle(g_hShm); g_hShm = nullptr; return TRUE; }
memset(g_mappedView, 0, RCX_RPC_HEADER_SIZE);
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
hdr->version = RCX_RPC_VERSION;
/* image base from PEB: gs:[0x60] → PEB, +0x18 → Ldr, Flink → first entry, +0x30 → DllBase */
{
uint64_t peb;
asm volatile("mov %%gs:0x60, %0" : "=r"(peb));
uint64_t ldr = *reinterpret_cast<uint64_t*>(peb + 0x18);
uint64_t firstLink = *reinterpret_cast<uint64_t*>(ldr + 0x10); /* InLoadOrderModuleList.Flink */
hdr->imageBase = *reinterpret_cast<uint64_t*>(firstLink + 0x30); /* DllBase */
}
/* ── create events ── */
g_hReqEvent = CreateEventA(nullptr, FALSE, FALSE, reqName);
g_hRspEvent = CreateEventA(nullptr, FALSE, FALSE, rspName);
if (!g_hReqEvent || !g_hRspEvent) { Cleanup(false); return TRUE; }
/* ── start server thread (payloadReady set by the thread) ── */
g_hThread = CreateThread(nullptr, 0, ServerThread, nullptr, 0, nullptr);
if (!g_hThread) { Cleanup(false); return TRUE; }
}
else if (reason == DLL_PROCESS_DETACH) {
/* reserved != NULL → process is terminating (threads already dead) */
Cleanup(reserved == nullptr);
g_hShm = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, 0, RCX_RPC_SHM_SIZE, shmName);
if (!g_hShm) {
InterlockedExchange(&g_initialized, 0);
return false;
}
g_mappedView = MapViewOfFile(g_hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
if (!g_mappedView) {
CloseHandle(g_hShm); g_hShm = nullptr;
InterlockedExchange(&g_initialized, 0);
return false;
}
memset(g_mappedView, 0, RCX_RPC_HEADER_SIZE);
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
hdr->version = RCX_RPC_VERSION;
/* image base from PEB */
{
uint64_t peb;
asm volatile("mov %%gs:0x60, %0" : "=r"(peb));
uint64_t ldr = *reinterpret_cast<uint64_t*>(peb + 0x18);
uint64_t firstLink = *reinterpret_cast<uint64_t*>(ldr + 0x10);
hdr->imageBase = *reinterpret_cast<uint64_t*>(firstLink + 0x30);
}
g_hReqEvent = CreateEventA(nullptr, FALSE, FALSE, reqName);
g_hRspEvent = CreateEventA(nullptr, FALSE, FALSE, rspName);
if (!g_hReqEvent || !g_hRspEvent) {
RcxPayloadCleanup();
return false;
}
/* create dedicated timer queue + fast poll timer (10ms interval) */
g_hTimerQueue = CreateTimerQueue();
if (!g_hTimerQueue) {
RcxPayloadCleanup();
return false;
}
if (!CreateTimerQueueTimer(&g_hPollTimer, g_hTimerQueue,
RcxPollTimerCallback, nullptr,
0, /* start immediately */
10, /* 10ms repeat */
WT_EXECUTEDEFAULT)) {
RcxPayloadCleanup();
return false;
}
/* mark ready */
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 1);
return true;
}
/* ── DllMain — minimal, no heavy work under loader lock ───────────── */
BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID)
{
if (reason == DLL_PROCESS_DETACH) {
RcxPayloadCleanup();
}
return TRUE;
}
@@ -529,37 +541,14 @@ static void payload_init()
{
uint32_t pid = (uint32_t)getpid();
/* ── read nonce from bootstrap shm ── */
char bootName[128];
rcx_rpc_boot_name(bootName, sizeof(bootName), pid);
int bootFd = shm_open(bootName, O_RDONLY, 0);
if (bootFd < 0) return;
void* bootView = mmap(nullptr, RCX_RPC_BOOT_SIZE, PROT_READ,
MAP_SHARED, bootFd, 0);
close(bootFd);
if (bootView == MAP_FAILED) return;
auto* boot = static_cast<const RcxRpcBootHeader*>(bootView);
char nonce[64] = {};
uint32_t nLen = boot->nonceLength;
if (nLen > 59) nLen = 59;
memcpy(nonce, boot->nonce, nLen);
nonce[nLen] = '\0';
munmap(bootView, RCX_RPC_BOOT_SIZE);
/* one-shot, unlink bootstrap */
shm_unlink(bootName);
/* ── open /proc/self/mem for safe access ── */
g_memFd = open("/proc/self/mem", O_RDWR);
if (g_memFd < 0) return;
/* ── create main shared memory ── */
rcx_rpc_shm_name(g_shmName, sizeof(g_shmName), pid, nonce);
rcx_rpc_req_name(g_reqName, sizeof(g_reqName), pid, nonce);
rcx_rpc_rsp_name(g_rspName, sizeof(g_rspName), pid, nonce);
/* ── create main shared memory (PID-only naming) ── */
rcx_rpc_shm_name(g_shmName, sizeof(g_shmName), pid);
rcx_rpc_req_name(g_reqName, sizeof(g_reqName), pid);
rcx_rpc_rsp_name(g_rspName, sizeof(g_rspName), pid);
g_shmFd = shm_open(g_shmName, O_CREAT | O_RDWR, 0600);
if (g_shmFd < 0) return;

View File

@@ -15,7 +15,6 @@
#define RCX_RPC_HEADER_SIZE 4096
#define RCX_RPC_DATA_OFFSET RCX_RPC_HEADER_SIZE
#define RCX_RPC_DATA_SIZE (RCX_RPC_SHM_SIZE - RCX_RPC_DATA_OFFSET)
#define RCX_RPC_BOOT_SIZE 64
/* status codes */
#define RCX_RPC_STATUS_OK 0
@@ -83,47 +82,32 @@ struct RcxRpcHeader {
uint8_t _pad[RCX_RPC_HEADER_SIZE - 48];
};
/* Bootstrap shm -- 64 bytes, carries the nonce from plugin to payload */
struct RcxRpcBootHeader {
uint32_t nonceLength;
char nonce[60];
};
/* ── name formatting helpers (PID-only, no nonce) ─────────────────── */
/* ── name formatting helpers ───────────────────────────────────────── */
static inline void rcx_rpc_boot_name(char* buf, int n, uint32_t pid) {
static inline void rcx_rpc_shm_name(char* buf, int n, uint32_t pid) {
#ifdef _WIN32
snprintf(buf, n, "Local\\RCX_BOOT_%u", pid);
snprintf(buf, n, "Local\\RCX_SHM_%u", pid);
#else
snprintf(buf, n, "/rcx_boot_%u", pid);
snprintf(buf, n, "/rcx_shm_%u", pid);
#endif
}
static inline void rcx_rpc_shm_name(char* buf, int n, uint32_t pid, const char* nonce) {
static inline void rcx_rpc_req_name(char* buf, int n, uint32_t pid) {
#ifdef _WIN32
snprintf(buf, n, "Local\\RCX_SHM_%u_%s", pid, nonce);
snprintf(buf, n, "Local\\RCX_REQ_%u", pid);
#else
snprintf(buf, n, "/rcx_shm_%u_%s", pid, nonce);
snprintf(buf, n, "/rcx_req_%u", pid);
#endif
}
static inline void rcx_rpc_req_name(char* buf, int n, uint32_t pid, const char* nonce) {
static inline void rcx_rpc_rsp_name(char* buf, int n, uint32_t pid) {
#ifdef _WIN32
snprintf(buf, n, "Local\\RCX_REQ_%u_%s", pid, nonce);
snprintf(buf, n, "Local\\RCX_RSP_%u", pid);
#else
snprintf(buf, n, "/rcx_req_%u_%s", pid, nonce);
#endif
}
static inline void rcx_rpc_rsp_name(char* buf, int n, uint32_t pid, const char* nonce) {
#ifdef _WIN32
snprintf(buf, n, "Local\\RCX_RSP_%u_%s", pid, nonce);
#else
snprintf(buf, n, "/rcx_rsp_%u_%s", pid, nonce);
snprintf(buf, n, "/rcx_rsp_%u", pid);
#endif
}
#ifdef __cplusplus
static_assert(sizeof(RcxRpcHeader) == RCX_RPC_HEADER_SIZE, "Header must be 4096 bytes");
static_assert(sizeof(RcxRpcBootHeader) <= RCX_RPC_BOOT_SIZE, "Boot header must fit 64 bytes");
static_assert(sizeof(RcxRpcHeader) == RCX_RPC_HEADER_SIZE, "Header must be 4096 bytes");
#endif

View File

@@ -4,7 +4,7 @@
*
* Usage:
* test_rpc_client (auto-spawn host)
* test_rpc_client <pid> <nonce> [testbuf_hex testlen]
* test_rpc_client <pid> [testbuf_hex testlen]
*/
#include "../rcx_rpc_protocol.h"
@@ -45,12 +45,12 @@ struct TestIpcClient {
void* view = nullptr;
bool ok = false;
bool connect(uint32_t pid, const char* nonce, int timeoutMs = 5000)
bool connect(uint32_t pid, int timeoutMs = 5000)
{
char shmName[128], reqName[128], rspName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce);
rcx_rpc_req_name(reqName, sizeof(reqName), pid, nonce);
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid, nonce);
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
#ifdef _WIN32
ULONGLONG deadline = GetTickCount64() + (ULONGLONG)timeoutMs;
@@ -268,7 +268,7 @@ static pid_t g_hostPid = 0;
#endif
static FILE* g_hostPipe = nullptr;
static bool spawn_host(uint32_t* outPid, char* outNonce,
static bool spawn_host(uint32_t* outPid,
uint64_t* outTestBuf, uint32_t* outTestLen)
{
/* resolve path to test_rpc_host next to ourselves */
@@ -302,11 +302,11 @@ static bool spawn_host(uint32_t* outPid, char* outNonce,
return false;
}
/* parse: READY pid=X nonce=Y testbuf=0xZ testlen=N */
/* parse: READY pid=X testbuf=0xZ testlen=N */
unsigned long long tbuf = 0;
unsigned tlen = 0;
if (sscanf(line, "READY pid=%u nonce=%63s testbuf=0x%llx testlen=%u",
outPid, outNonce, &tbuf, &tlen) < 2) {
if (sscanf(line, "READY pid=%u testbuf=0x%llx testlen=%u",
outPid, &tbuf, &tlen) < 1) {
fprintf(stderr, "ERROR: cannot parse host output: %s\n", line);
return false;
}
@@ -341,30 +341,28 @@ static void print_fail(const char* name) { printf(" [FAIL] %s\n", name); exit(1
int main(int argc, char** argv)
{
uint32_t pid = 0;
char nonce[64] = {};
uint64_t testBuf = 0;
uint32_t testLen = 0;
bool autoMode = false;
if (argc >= 3) {
if (argc >= 2) {
pid = (uint32_t)atoi(argv[1]);
strncpy(nonce, argv[2], 63);
if (argc >= 5) {
testBuf = (uint64_t)strtoull(argv[3], nullptr, 0);
testLen = (uint32_t)atoi(argv[4]);
if (argc >= 4) {
testBuf = (uint64_t)strtoull(argv[2], nullptr, 0);
testLen = (uint32_t)atoi(argv[3]);
}
} else {
autoMode = true;
printf("Auto-spawning test_rpc_host...\n");
if (!spawn_host(&pid, nonce, &testBuf, &testLen)) return 1;
if (!spawn_host(&pid, &testBuf, &testLen)) return 1;
}
printf("Connecting to PID=%u nonce=%s testbuf=0x%llx testlen=%u\n\n",
pid, nonce, (unsigned long long)testBuf, testLen);
printf("Connecting to PID=%u testbuf=0x%llx testlen=%u\n\n",
pid, (unsigned long long)testBuf, testLen);
/* ── connect ── */
TestIpcClient ipc;
if (!ipc.connect(pid, nonce)) {
if (!ipc.connect(pid)) {
fprintf(stderr, "ERROR: IPC connect failed\n");
if (autoMode) cleanup_host();
return 1;

View File

@@ -1,7 +1,7 @@
/*
* test_rpc_host -- loads rcx_payload in-process, acts as the "target".
*
* Usage: test_rpc_host [nonce]
* Usage: test_rpc_host
*
* Prints a READY line (machine-parseable), then waits for the payload
* to shut down (RPC_CMD_SHUTDOWN from the client).
@@ -68,50 +68,11 @@ static int payload_path(char* out, int outLen)
return 0;
}
/* Create bootstrap shared memory with the nonce */
static int create_bootstrap(uint32_t pid, const char* nonce)
{
char bootName[128];
rcx_rpc_boot_name(bootName, sizeof(bootName), pid);
#ifdef _WIN32
HANDLE h = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, 0, RCX_RPC_BOOT_SIZE, bootName);
if (!h) return -1;
void* v = MapViewOfFile(h, FILE_MAP_WRITE, 0, 0, RCX_RPC_BOOT_SIZE);
if (!v) { CloseHandle(h); return -1; }
RcxRpcBootHeader* boot = (RcxRpcBootHeader*)v;
memset(boot, 0, RCX_RPC_BOOT_SIZE);
boot->nonceLength = (uint32_t)strlen(nonce);
strncpy(boot->nonce, nonce, 59);
UnmapViewOfFile(v);
/* keep h open for payload to read */
return 0;
#else
int fd = shm_open(bootName, O_CREAT | O_RDWR, 0600);
if (fd < 0) return -1;
if (ftruncate(fd, RCX_RPC_BOOT_SIZE) != 0) { close(fd); return -1; }
void* v = mmap(nullptr, RCX_RPC_BOOT_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
close(fd);
if (v == MAP_FAILED) return -1;
RcxRpcBootHeader* boot = (RcxRpcBootHeader*)v;
memset(boot, 0, RCX_RPC_BOOT_SIZE);
boot->nonceLength = (uint32_t)strlen(nonce);
strncpy(boot->nonce, nonce, 59);
munmap(v, RCX_RPC_BOOT_SIZE);
return 0;
#endif
}
/* Open the main shared memory (read-only, just to monitor payloadReady) */
static void* open_main_shm(uint32_t pid, const char* nonce)
static void* open_main_shm(uint32_t pid)
{
char shmName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce);
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
#ifdef _WIN32
HANDLE h = nullptr;
@@ -142,21 +103,14 @@ static uint8_t g_testBuf[65536];
/* ── main ─────────────────────────────────────────────────────────── */
int main(int argc, char** argv)
int main(int, char**)
{
const char* nonce = (argc > 1) ? argv[1] : "test0001";
uint32_t pid = current_pid();
/* fill test buffer with known pattern */
for (int i = 0; i < (int)sizeof(g_testBuf); ++i)
g_testBuf[i] = (uint8_t)(i & 0xFF);
/* create bootstrap shm */
if (create_bootstrap(pid, nonce) != 0) {
fprintf(stderr, "ERROR: failed to create bootstrap shm\n");
return 1;
}
/* load payload */
char plPath[1024];
if (payload_path(plPath, sizeof(plPath)) != 0) {
@@ -171,6 +125,15 @@ int main(int argc, char** argv)
plPath, GetLastError());
return 1;
}
/* Call RcxPayloadInit() — DllMain is minimal, init must be explicit */
typedef bool (*RcxPayloadInitFn)();
auto pfnInit = (RcxPayloadInitFn)GetProcAddress(hPayload, "RcxPayloadInit");
if (!pfnInit || !pfnInit()) {
fprintf(stderr, "ERROR: RcxPayloadInit() failed or not found\n");
FreeLibrary(hPayload);
return 1;
}
#else
void* hPayload = dlopen(plPath, RTLD_NOW);
if (!hPayload) {
@@ -180,7 +143,7 @@ int main(int argc, char** argv)
#endif
/* open main shm and wait for payloadReady */
void* shmView = open_main_shm(pid, nonce);
void* shmView = open_main_shm(pid);
if (!shmView) {
fprintf(stderr, "ERROR: failed to open main shared memory\n");
return 1;
@@ -197,8 +160,8 @@ int main(int argc, char** argv)
}
/* print READY line for the client to parse */
printf("READY pid=%u nonce=%s testbuf=0x%llx testlen=%u\n",
pid, nonce,
printf("READY pid=%u testbuf=0x%llx testlen=%u\n",
pid,
(unsigned long long)(uintptr_t)g_testBuf,
(unsigned)sizeof(g_testBuf));
fflush(stdout);
@@ -210,7 +173,7 @@ int main(int argc, char** argv)
printf("Payload shut down, exiting.\n");
#ifdef _WIN32
/* give the server thread a moment to exit */
/* give the timer queue a moment to drain */
Sleep(200);
FreeLibrary(hPayload);
if (shmView) UnmapViewOfFile(shmView);

View File

@@ -197,53 +197,15 @@ void WinDbgMemoryProvider::querySessionInfo()
}
}
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;
}
}
// WinDbg provides access to the entire virtual address space.
// Do NOT auto-select a module as base — let the user set their
// own base address. m_base stays 0 so the controller won't
// override tree.baseAddress.
m_name = m_isLive ? QStringLiteral("WinDbg (Live)")
: QStringLiteral("WinDbg (Dump)");
qDebug() << "[WinDbg] Ready. name=" << m_name
<< "base=" << Qt::hex << m_base << "isLive=" << m_isLive;
<< "isLive=" << m_isLive;
#endif
}
@@ -305,8 +267,18 @@ bool WinDbgMemoryProvider::read(uint64_t addr, void* buf, int len) const
dispatchToOwner([&]() {
ULONG bytesRead = 0;
HRESULT hr = m_dataSpaces->ReadVirtual(addr, buf, (ULONG)len, &bytesRead);
if (FAILED(hr) || (int)bytesRead < len)
memset((char*)buf + bytesRead, 0, len - bytesRead);
if (SUCCEEDED(hr) && (int)bytesRead >= len) {
result = true;
return;
}
// Partial or failed read — zero-fill remainder and log
memset((char*)buf + bytesRead, 0, len - bytesRead);
++m_readFailCount;
if (m_readFailCount <= 5 || (m_readFailCount % 100) == 0)
qDebug() << "[WinDbg] ReadVirtual FAILED addr=0x" << Qt::hex << addr
<< "len=" << Qt::dec << len
<< "hr=0x" << Qt::hex << (unsigned long)hr
<< "got=" << Qt::dec << bytesRead;
result = bytesRead > 0;
});
return result;

View File

@@ -83,6 +83,7 @@ private:
bool m_isLive = false;
bool m_writable = false;
bool m_isRemote = false; // true when connected via DebugConnect (tcp/npipe)
mutable int m_readFailCount = 0;
// Dedicated thread for DbgEng COM operations. The remote TCP/pipe
// transport is thread-affine — all calls must happen on the thread

View File

@@ -303,6 +303,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.lineKind = LineKind::Field;
lm.nodeKind = node.elementKind;
lm.isArrayElement = true;
lm.arrayElementIdx = i;
lm.offsetText = fmt::fmtOffsetMargin(elemAddr, false, state.offsetHexDigits);
lm.offsetAddr = elemAddr;
lm.ptrBase = state.currentPtrBase;

View File

@@ -569,9 +569,9 @@ void RcxController::refresh() {
// Prune stale selections (nodes removed by undo/redo/delete)
QSet<uint64_t> valid;
for (uint64_t id : m_selIds) {
uint64_t nodeId = id & ~kFooterIdBit; // Strip footer bit for lookup
uint64_t nodeId = id & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask);
if (m_doc->tree.indexOfId(nodeId) >= 0)
valid.insert(id); // Keep original ID (with footer bit if present)
valid.insert(id); // Keep original ID (with footer/array bits if present)
}
m_selIds = valid;
@@ -1583,13 +1583,35 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
// ── Always-available actions ──
menu.addAction(icon("diff-added.svg"), "Append 128 bytes", [this]() {
menu.addAction(icon("diff-added.svg"), "Append bytes...", [this, &menu]() {
bool ok;
QString input = QInputDialog::getText(menu.parentWidget(),
QStringLiteral("Append bytes"),
QStringLiteral("Byte count (decimal or 0x hex):"),
QLineEdit::Normal, QStringLiteral("128"), &ok);
if (!ok || input.trimmed().isEmpty()) return;
QString trimmed = input.trimmed();
int byteCount = 0;
if (trimmed.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
byteCount = trimmed.mid(2).toInt(&ok, 16);
else
byteCount = trimmed.toInt(&ok, 10);
if (!ok || byteCount <= 0) return;
uint64_t target = m_viewRootId ? m_viewRootId : 0;
int hex64Count = byteCount / 8;
int remainBytes = byteCount % 8;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Append 128 bytes"));
for (int i = 0; i < 16; i++)
m_doc->undoStack.beginMacro(QStringLiteral("Append %1 bytes").arg(byteCount));
int idx = 0;
for (int i = 0; i < hex64Count; i++, idx++)
insertNode(target, -1, NodeKind::Hex64,
QStringLiteral("field_%1").arg(i));
QStringLiteral("field_%1").arg(idx));
for (int i = 0; i < remainBytes; i++, idx++)
insertNode(target, -1, NodeKind::Hex8,
QStringLiteral("field_%1").arg(idx));
m_doc->undoStack.endMacro();
m_suppressRefresh = false;
refresh();
@@ -1674,11 +1696,17 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
bool ctrl = mods & Qt::ControlModifier;
bool shift = mods & Qt::ShiftModifier;
// Compute effective selection ID: footers use nodeId | kFooterIdBit
// Compute effective selection ID:
// footers → nodeId | kFooterIdBit
// array elements → nodeId | kArrayElemBit | (elemIdx << 48)
// everything else → nodeId
auto effectiveId = [this](int ln, uint64_t nid) -> uint64_t {
if (ln >= 0 && ln < m_lastResult.meta.size() &&
m_lastResult.meta[ln].lineKind == LineKind::Footer)
if (ln < 0 || ln >= m_lastResult.meta.size()) return nid;
const auto& lm = m_lastResult.meta[ln];
if (lm.lineKind == LineKind::Footer)
return nid | kFooterIdBit;
if (lm.isArrayElement && lm.arrayElementIdx >= 0)
return makeArrayElemSelId(nid, lm.arrayElementIdx);
return nid;
};
@@ -1727,8 +1755,8 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
if (m_selIds.size() == 1) {
uint64_t sid = *m_selIds.begin();
// Strip footer bit for node lookup
int idx = m_doc->tree.indexOfId(sid & ~kFooterIdBit);
// Strip footer/array bits for node lookup
int idx = m_doc->tree.indexOfId(sid & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask));
if (idx >= 0) emit nodeSelected(idx);
}
}
@@ -2298,11 +2326,11 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
return;
}
uint64_t newBase = provider->base();
m_doc->undoStack.clear();
m_doc->provider = std::move(provider);
m_doc->dataPath.clear();
m_doc->tree.baseAddress = (newBase != 0) ? newBase : m_doc->tree.baseAddress;
// Don't overwrite baseAddress — caller (e.g. selfTest) already set it.
// User-initiated source switches go through selectSource() which does update it.
// Re-evaluate stored formula against the new provider
if (!m_doc->tree.baseAddressFormula.isEmpty()) {
@@ -2467,6 +2495,12 @@ void RcxController::clearSources() {
refresh();
}
void RcxController::copySavedSources(const QVector<SavedSourceEntry>& sources, int activeIdx) {
m_savedSources = sources;
m_activeSourceIdx = activeIdx;
pushSavedSourcesToEditors();
}
void RcxController::pushSavedSourcesToEditors() {
QVector<SavedSourceDisplay> display;
display.reserve(m_savedSources.size());

View File

@@ -131,6 +131,7 @@ public:
void switchSource(int idx) { switchToSavedSource(idx); }
void clearSources();
void selectSource(const QString& text);
void copySavedSources(const QVector<SavedSourceEntry>& sources, int activeIdx);
// Value tracking toggle (per-tab, off by default)
bool trackValues() const { return m_trackValues; }

View File

@@ -481,6 +481,17 @@ static constexpr uint64_t kCommandRowId = UINT64_MAX;
static constexpr int kCommandRowLine = 0;
static constexpr int kFirstDataLine = 1;
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
static constexpr uint64_t kArrayElemBit = 0x4000000000000000ULL; // marks array element selection
static constexpr uint64_t kArrayElemShift = 48; // bits 48-61 hold element index
static constexpr uint64_t kArrayElemMask = 0x3FFF000000000000ULL; // 14 bits → max 16383 elements
// Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 48)
inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) {
return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0x3FFF) << kArrayElemShift);
}
inline int arrayElemIdxFromSelId(uint64_t selId) {
return (int)((selId & kArrayElemMask) >> kArrayElemShift);
}
struct LineMeta {
int nodeIdx = -1;

View File

@@ -787,6 +787,14 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
m_meta = result.meta;
m_layout = result.layout;
// Build nodeId → display-line index for O(1) hover/selection lookup
m_nodeLineIndex.clear();
m_nodeLineIndex.reserve(m_meta.size());
for (int i = 0; i < m_meta.size(); i++) {
if (m_meta[i].nodeId != 0)
m_nodeLineIndex[m_meta[i].nodeId].append(i);
}
// Dynamically resize margin to fit the current hex digit tier
QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0'));
m_sci->setMarginWidth(0, marginSizer);
@@ -835,9 +843,12 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
m_applyingDocument = false;
// Re-apply hover markers (setText() clears all Scintilla markers).
// Reset m_prevHoveredNodeId so the incremental logic re-adds markers.
// applyHoverCursor() is NOT called here — it evaluates hitTest() against
// composed text that updateCommandRow() will overwrite. The correct call
// happens via applySelectionOverlays() after all text is finalized.
m_prevHoveredNodeId = 0;
m_prevHoveredLine = -1;
applyHoverHighlight();
}
@@ -1064,18 +1075,33 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_EDITABLE);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (unsigned long)0, docLen);
for (int i = 0; i < m_meta.size(); i++) {
if (isSyntheticLine(m_meta[i])) continue;
uint64_t nodeId = m_meta[i].nodeId;
bool isFooter = (m_meta[i].lineKind == LineKind::Footer);
// Footers check for footerId, non-footers check for plain nodeId
uint64_t checkId = isFooter ? (nodeId | kFooterIdBit) : nodeId;
if (selIds.contains(checkId)) {
m_sci->markerAdd(i, M_SELECTED);
m_sci->markerAdd(i, M_ACCENT);
// Use index: iterate selected IDs, look up their lines
for (uint64_t selId : selIds) {
bool isFooterSel = (selId & kFooterIdBit) != 0;
bool isArrayElemSel = (selId & kArrayElemBit) != 0;
int arrayElemIdx = isArrayElemSel ? arrayElemIdxFromSelId(selId) : -1;
uint64_t nodeId = selId & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask);
auto it = m_nodeLineIndex.constFind(nodeId);
if (it == m_nodeLineIndex.constEnd()) continue;
for (int ln : *it) {
if (isSyntheticLine(m_meta[ln])) continue;
bool isFooter = (m_meta[ln].lineKind == LineKind::Footer);
// Match selection type to line type
if (isFooterSel && !isFooter) continue;
if (!isFooterSel && isFooter) continue;
// Array element: match by element index
if (isArrayElemSel) {
if (!m_meta[ln].isArrayElement || m_meta[ln].arrayElementIdx != arrayElemIdx)
continue;
} else if (m_meta[ln].isArrayElement) {
// Plain nodeId selection shouldn't highlight individual array elements
// (the header line is enough)
continue;
}
m_sci->markerAdd(ln, M_SELECTED);
m_sci->markerAdd(ln, M_ACCENT);
if (!isFooter)
paintEditableSpans(i);
paintEditableSpans(ln);
}
}
@@ -1088,28 +1114,63 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
}
void RcxEditor::applyHoverHighlight() {
m_sci->markerDeleteAll(M_HOVER);
uint64_t prevId = m_prevHoveredNodeId;
int prevLine = m_prevHoveredLine;
m_prevHoveredNodeId = m_hoveredNodeId;
m_prevHoveredLine = m_hoveredLine;
// Fast path: nothing changed (same node AND same line)
if (prevId == m_hoveredNodeId && prevLine == m_hoveredLine
&& m_hoveredNodeId != 0) return;
// Remove old hover markers
if (prevId != 0) {
// Check if old hovered line was a single-line highlight (footer or array element)
bool prevSingleLine = (prevLine >= 0 && prevLine < m_meta.size() &&
(m_meta[prevLine].lineKind == LineKind::Footer || m_meta[prevLine].isArrayElement));
if (prevSingleLine) {
m_sci->markerDelete(prevLine, M_HOVER);
} else {
auto it = m_nodeLineIndex.constFind(prevId);
if (it != m_nodeLineIndex.constEnd()) {
for (int ln : *it)
m_sci->markerDelete(ln, M_HOVER);
}
}
}
if (m_editState.active) return;
if (!m_hoverInside) return;
if (m_hoveredNodeId == 0) return;
// Check if hovered line is a footer - footers highlight independently
// Footer and array elements highlight only the specific line
bool hoveringFooter = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
m_meta[m_hoveredLine].lineKind == LineKind::Footer);
bool hoveringArrayElem = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
m_meta[m_hoveredLine].isArrayElement);
// Check if the hovered item is already selected (using appropriate ID)
uint64_t checkId = hoveringFooter ? (m_hoveredNodeId | kFooterIdBit) : m_hoveredNodeId;
uint64_t checkId;
if (hoveringFooter)
checkId = m_hoveredNodeId | kFooterIdBit;
else if (hoveringArrayElem)
checkId = makeArrayElemSelId(m_hoveredNodeId, m_meta[m_hoveredLine].arrayElementIdx);
else
checkId = m_hoveredNodeId;
if (m_currentSelIds.contains(checkId)) return;
if (hoveringFooter) {
// Footer: only highlight this specific line
if (hoveringFooter || hoveringArrayElem) {
// Single-line highlight for footers and array elements
m_sci->markerAdd(m_hoveredLine, M_HOVER);
} else {
// Non-footer: highlight all matching lines except footers
for (int i = 0; i < m_meta.size(); i++) {
if (m_meta[i].nodeId == m_hoveredNodeId &&
m_meta[i].lineKind != LineKind::Footer)
m_sci->markerAdd(i, M_HOVER);
// Non-footer, non-array-element: highlight all lines for this node
auto it = m_nodeLineIndex.constFind(m_hoveredNodeId);
if (it != m_nodeLineIndex.constEnd()) {
for (int ln : *it) {
if (m_meta[ln].lineKind != LineKind::Footer &&
!m_meta[ln].isArrayElement)
m_sci->markerAdd(ln, M_HOVER);
}
}
}
}
@@ -2617,11 +2678,16 @@ void RcxEditor::updateEditableIndicators(int line) {
return;
}
// Helper to check if a line's node is selected (handles footer IDs)
// Helper to check if a line's node is selected (handles footer/array element IDs)
auto isLineSelected = [this](const LineMeta* lm) -> bool {
if (!lm) return false;
bool isFooter = (lm->lineKind == LineKind::Footer);
uint64_t checkId = isFooter ? (lm->nodeId | kFooterIdBit) : lm->nodeId;
uint64_t checkId;
if (lm->lineKind == LineKind::Footer)
checkId = lm->nodeId | kFooterIdBit;
else if (lm->isArrayElement && lm->arrayElementIdx >= 0)
checkId = makeArrayElemSelId(lm->nodeId, lm->arrayElementIdx);
else
checkId = lm->nodeId;
return m_currentSelIds.contains(checkId);
};

View File

@@ -4,6 +4,7 @@
#include <QWidget>
#include <QSet>
#include <QPoint>
#include <QHash>
class QsciScintilla;
class QsciLexerCPP;
@@ -95,8 +96,12 @@ private:
bool m_hoverInside = false;
uint64_t m_hoveredNodeId = 0;
int m_hoveredLine = -1;
uint64_t m_prevHoveredNodeId = 0; // for incremental marker update
int m_prevHoveredLine = -1; // for incremental marker update
QSet<uint64_t> m_currentSelIds;
QVector<int> m_hoverSpanLines; // Lines with hover span indicators
// ── nodeId → display-line index (built in applyDocument) ──
QHash<uint64_t, QVector<int>> m_nodeLineIndex;
// ── Drag selection ──
bool m_dragging = false;
bool m_dragStarted = false; // true once drag threshold exceeded

View File

@@ -251,6 +251,9 @@ public:
// Kill the 1px frame margin Fusion reserves around QMenu contents
if (metric == PM_MenuPanelWidth)
return 0;
// Kill the separator between dock widgets / central widget
if (metric == PM_DockWidgetSeparatorExtent)
return 0;
return QProxyStyle::pixelMetric(metric, opt, w);
}
void drawPrimitive(PrimitiveElement elem, const QStyleOption* opt,
@@ -261,21 +264,37 @@ public:
// Kill the status bar item frame and panel border
if (elem == PE_FrameStatusBarItem || elem == PE_PanelStatusBar)
return;
// Transparent menu bar background (no CSS needed)
if (elem == PE_PanelMenuBar)
return;
QProxyStyle::drawPrimitive(elem, opt, p, w);
}
void drawControl(ControlElement element, const QStyleOption* opt,
QPainter* p, const QWidget* w) const override {
// Menu bar items (File, Edit, View…) — direct paint, Fusion ignores palette
// Suppress Fusion's CE_MenuBarEmptyArea — it fills with palette.window()
// bypassing PE_PanelMenuBar. TitleBarWidget paints the background.
if (element == CE_MenuBarEmptyArea)
return;
// Menu bar items — fully owned painting (Fusion fills full rect, hiding border)
if (element == CE_MenuBarItem) {
if (auto* mi = qstyleoption_cast<const QStyleOptionMenuItem*>(opt)) {
if (mi->state & (State_Selected | State_Sunken)) {
QStyleOptionMenuItem patched = *mi;
patched.state &= ~(State_Selected | State_Sunken);
patched.palette.setColor(QPalette::ButtonText,
mi->palette.color(QPalette::Link)); // amber text only
QProxyStyle::drawControl(element, &patched, p, w);
return;
}
QRect area = mi->rect.adjusted(0, 0, 0, -1); // leave 1px for border
bool selected = mi->state & State_Selected;
bool sunken = mi->state & State_Sunken;
// Only fill background for hover/pressed — non-hovered stays
// transparent so the parent's border line shows through.
if (sunken)
p->fillRect(area, mi->palette.color(QPalette::Mid).darker(130));
else if (selected)
p->fillRect(area, mi->palette.color(QPalette::Mid));
QColor fg = (selected || sunken)
? mi->palette.color(QPalette::Link)
: mi->palette.color(QPalette::ButtonText);
p->setPen(fg);
p->drawText(area, Qt::AlignCenter | Qt::TextShowMnemonic, mi->text);
return; // never delegate to Fusion
}
}
// Popup menu items — palette patch then delegate to Fusion
@@ -285,7 +304,7 @@ public:
&& mi->menuItemType != QStyleOptionMenuItem::Separator) {
QStyleOptionMenuItem patched = *mi;
patched.palette.setColor(QPalette::Highlight,
mi->palette.color(QPalette::Mid)); // theme.border
mi->palette.color(QPalette::Mid)); // theme.hover
patched.palette.setColor(QPalette::HighlightedText,
mi->palette.color(QPalette::Link)); // theme.indHoverSpan
QProxyStyle::drawControl(element, &patched, p, w);
@@ -1352,8 +1371,7 @@ void MainWindow::toggleMcp() {
void MainWindow::applyTheme(const Theme& theme) {
applyGlobalTheme(theme);
// Kill the 1px separator line between central widget and status bar
setStyleSheet("QMainWindow::separator { height: 0px; width: 0px; }");
// Separator killed via PM_DockWidgetSeparatorExtent in MenuBarStyle
// Custom title bar
m_titleBar->applyTheme(theme);
@@ -1976,7 +1994,22 @@ QMdiSubWindow* MainWindow::project_new(const QString& classKeyword) {
buildEmptyStruct(doc->tree, classKeyword);
// Inherit source from current tab (if any)
auto* currentCtrl = activeController();
if (currentCtrl && currentCtrl->document()->provider
&& currentCtrl->document()->provider->isValid()) {
doc->provider = currentCtrl->document()->provider;
}
auto* sub = createTab(doc);
// Copy saved sources to new tab's controller
if (currentCtrl && !currentCtrl->savedSources().isEmpty()) {
auto& newTab = m_tabs[sub];
newTab.ctrl->copySavedSources(currentCtrl->savedSources(),
currentCtrl->activeSourceIndex());
}
rebuildWorkspaceModel();
return sub;
}
@@ -2280,8 +2313,16 @@ void MainWindow::populateSourceMenu() {
{QStringLiteral("reclass.netcompatlayer"), QStringLiteral(":/vsicons/plug.svg")},
};
m_sourceMenu->addAction(QIcon(QStringLiteral(":/vsicons/file-binary.svg")),
QStringLiteral("File"), this, [this]() {
auto addSourceAction = [this](const QString& text, const QIcon& icon, auto&& slot) {
auto* act = m_sourceMenu->addAction(icon, text);
act->setIconVisibleInMenu(true);
connect(act, &QAction::triggered, this, std::forward<decltype(slot)>(slot));
return act;
};
addSourceAction(QStringLiteral("File"),
makeIcon(QStringLiteral(":/vsicons/file-binary.svg")),
[this]() {
if (auto* c = activeController()) c->selectSource(QStringLiteral("File"));
});
@@ -2289,14 +2330,14 @@ void MainWindow::populateSourceMenu() {
for (const auto& prov : providers) {
QString name = prov.name;
auto it = s_providerIcons.constFind(prov.identifier);
QIcon icon(it != s_providerIcons.constEnd() ? *it
: QStringLiteral(":/vsicons/extensions.svg"));
QIcon icon = makeIcon(it != s_providerIcons.constEnd() ? *it
: QStringLiteral(":/vsicons/extensions.svg"));
QString label = prov.dllFileName.isEmpty()
? name
: QStringLiteral("%1 (%2)").arg(name, prov.dllFileName);
m_sourceMenu->addAction(icon, label, this, [this, name]() {
addSourceAction(label, icon, [this, name]() {
if (auto* c = activeController()) c->selectSource(name);
});
}
@@ -2306,18 +2347,20 @@ void MainWindow::populateSourceMenu() {
for (int i = 0; i < ctrl->savedSources().size(); i++) {
const auto& e = ctrl->savedSources()[i];
auto* act = m_sourceMenu->addAction(
QStringLiteral("%1 '%2'").arg(e.kind, e.displayName),
this, [this, i]() {
if (auto* c = activeController()) c->switchSource(i);
});
QStringLiteral("%1 '%2'").arg(e.kind, e.displayName));
act->setCheckable(true);
act->setChecked(i == ctrl->activeSourceIndex());
connect(act, &QAction::triggered, this, [this, i]() {
if (auto* c = activeController()) c->switchSource(i);
});
}
m_sourceMenu->addSeparator();
m_sourceMenu->addAction(QIcon(QStringLiteral(":/vsicons/clear-all.svg")),
QStringLiteral("Clear All"), this, [this]() {
auto* clearAct = addSourceAction(QStringLiteral("Clear All"),
makeIcon(QStringLiteral(":/vsicons/clear-all.svg")),
[this]() {
if (auto* c = activeController()) c->clearSources();
});
Q_UNUSED(clearAct);
}
}

View File

@@ -1,4 +1,5 @@
#include "theme.h"
#include <QtGlobal>
#include <type_traits>
namespace rcx {
@@ -61,6 +62,15 @@ Theme Theme::fromJson(const QJsonObject& o) {
t.indHeatWarm = t.indHoverSpan.isValid() ? t.indHoverSpan : t.syntaxString;
if (!t.indHeatHot.isValid())
t.indHeatHot = t.markerPtr;
// Ensure hover is visually distinct from background
if (t.hover.isValid() && t.background.isValid()) {
int dist = qAbs(t.hover.red() - t.background.red())
+ qAbs(t.hover.green() - t.background.green())
+ qAbs(t.hover.blue() - t.background.blue());
if (dist < 20)
t.hover = t.background.lighter(130);
}
return t;
}

View File

@@ -76,14 +76,16 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
.arg(theme.textDim.name()));
// Menu bar styling — transparent background, themed text
m_menuBar->setStyleSheet(
QStringLiteral(
"QMenuBar { background: transparent; border: none; }"
"QMenuBar::item { background: transparent; color: %1; padding: 8px 8px 4px 8px; }"
"QMenuBar::item:selected { background: %2; }"
"QMenuBar::item:pressed { background: %2; }")
.arg(theme.textDim.name(), theme.hover.name()));
// Menu bar palette — hover/bg handled by MenuBarStyle QProxyStyle.
// Set Window + Button to background so Fusion never paints a foreign color.
{
QPalette mbPal = m_menuBar->palette();
mbPal.setColor(QPalette::Window, theme.background);
mbPal.setColor(QPalette::Button, theme.background);
mbPal.setColor(QPalette::ButtonText, theme.textDim);
m_menuBar->setPalette(mbPal);
m_menuBar->setAutoFillBackground(false);
}
// Chrome buttons
QString btnStyle = QStringLiteral(

View File

@@ -256,8 +256,9 @@ private slots:
{
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);
// WinDbg provider no longer auto-selects a module base — it returns 0
// so the controller doesn't override the user's chosen base address.
QCOMPARE(prov.base(), (uint64_t)0);
}
// ── Read: MZ header on main thread ──
@@ -446,6 +447,139 @@ private slots:
QCOMPARE(raw->Name(), std::string("WinDbg Memory"));
delete raw;
}
// ── Kernel session tests ──
// Requires a WinDbg instance with a kernel dump loaded and
// .server tcp:port=5055 running. Skipped automatically if
// no server is available. Override with WINDBG_KERNEL_CONN env var.
void provider_kernel_connect()
{
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
"tcp:Port=5055,Server=localhost");
if (!canConnect(kernelConn))
QSKIP("No kernel debug server available (set WINDBG_KERNEL_CONN)");
WinDbgMemoryProvider prov(kernelConn);
QVERIFY2(prov.isValid(), "Should connect to kernel debug server");
QCOMPARE(prov.kind(), QStringLiteral("WinDbg"));
qDebug() << "Kernel provider name:" << prov.name();
qDebug() << "Kernel provider base:" << QString("0x%1").arg(prov.base(), 0, 16);
qDebug() << "Kernel provider isLive:" << prov.isLive();
// Name should not be an arbitrary user-mode DLL
QVERIFY2(!prov.name().contains("WS2_32", Qt::CaseInsensitive),
qPrintable("Name should not be 'WS2_32', got: " + prov.name()));
}
void provider_kernel_read_base()
{
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
"tcp:Port=5055,Server=localhost");
if (!canConnect(kernelConn))
QSKIP("No kernel debug server available");
WinDbgMemoryProvider prov(kernelConn);
QVERIFY(prov.isValid());
// Provider no longer auto-selects a base. Use a known kernel address
// from env, or skip.
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
if (addrStr.isEmpty())
QSKIP("Set WINDBG_KERNEL_ADDR to a readable kernel address");
bool ok = false;
uint64_t addr = addrStr.toULongLong(&ok, 16);
QVERIFY2(ok && addr != 0, "WINDBG_KERNEL_ADDR must be a valid hex address");
uint8_t buf[16] = {};
ok = prov.read(addr, buf, 16);
QVERIFY2(ok, "Should read from kernel address");
bool allZero = true;
for (int i = 0; i < 16; ++i) {
if (buf[i] != 0) { allZero = false; break; }
}
QVERIFY2(!allZero, "Kernel read returned all zeros");
}
void provider_kernel_read_high_address()
{
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
"tcp:Port=5055,Server=localhost");
if (!canConnect(kernelConn))
QSKIP("No kernel debug server available");
WinDbgMemoryProvider prov(kernelConn);
QVERIFY(prov.isValid());
// Use env var for a specific kernel address (e.g. _EPROCESS),
// otherwise fall back to the provider's base.
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
uint64_t addr = 0;
if (!addrStr.isEmpty()) {
bool ok = false;
addr = addrStr.toULongLong(&ok, 16);
if (!ok) addr = 0;
}
if (addr == 0) addr = prov.base();
uint8_t buf[64] = {};
bool ok = prov.read(addr, buf, 64);
QVERIFY2(ok, qPrintable(QString("Should read kernel addr 0x%1")
.arg(addr, 0, 16)));
bool allZero = true;
for (int i = 0; i < 64; ++i) {
if (buf[i] != 0) { allZero = false; break; }
}
QVERIFY2(!allZero, "Kernel high-address read returned all zeros");
qDebug() << "Read 64 bytes at" << QString("0x%1").arg(addr, 0, 16)
<< "first 8:" << QString("%1 %2 %3 %4 %5 %6 %7 %8")
.arg(buf[0], 2, 16, QChar('0'))
.arg(buf[1], 2, 16, QChar('0'))
.arg(buf[2], 2, 16, QChar('0'))
.arg(buf[3], 2, 16, QChar('0'))
.arg(buf[4], 2, 16, QChar('0'))
.arg(buf[5], 2, 16, QChar('0'))
.arg(buf[6], 2, 16, QChar('0'))
.arg(buf[7], 2, 16, QChar('0'));
}
void provider_kernel_read_backgroundThread()
{
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
"tcp:Port=5055,Server=localhost");
if (!canConnect(kernelConn))
QSKIP("No kernel debug server available");
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
if (addrStr.isEmpty())
QSKIP("Set WINDBG_KERNEL_ADDR to a readable kernel address");
bool ok = false;
uint64_t addr = addrStr.toULongLong(&ok, 16);
QVERIFY2(ok && addr != 0, "WINDBG_KERNEL_ADDR must be a valid hex address");
WinDbgMemoryProvider prov(kernelConn);
QVERIFY(prov.isValid());
// Simulate the controller's async refresh pattern
QFuture<QByteArray> future = QtConcurrent::run([&prov, addr]() -> QByteArray {
return prov.readBytes(addr, 4096);
});
future.waitForFinished();
QByteArray data = future.result();
QCOMPARE(data.size(), 4096);
bool allZero = true;
for (int i = 0; i < data.size(); ++i) {
if (data[i] != '\0') { allZero = false; break; }
}
QVERIFY2(!allZero, "Kernel background read returned all zeros");
}
};
QTEST_MAIN(TestWinDbgProvider)

159
tools/test_hover.py Normal file
View File

@@ -0,0 +1,159 @@
"""
Structural hover test: validate that all themes produce visible hover colors
and that the QProxyStyle code handles the required control elements.
No pixel sampling — checks theme JSON values and source code patterns.
"""
import json
import os
import re
import sys
def hex_to_rgb(h):
h = h.lstrip('#')
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
def color_dist(c1, c2):
return sum(abs(a - b) for a, b in zip(c1, c2))
def lighter_130(rgb):
"""Approximate Qt's QColor::lighter(130) for dark grays."""
r, g, b = rgb
return (min(255, int(r * 1.3) + 1),
min(255, int(g * 1.3) + 1),
min(255, int(b * 1.3) + 1))
def load_themes():
themes = {}
theme_dir = os.path.join(os.path.dirname(__file__),
'..', 'src', 'themes', 'defaults')
if not os.path.isdir(theme_dir):
return themes
for name in os.listdir(theme_dir):
if name.endswith('.json'):
with open(os.path.join(theme_dir, name)) as f:
themes[name] = json.load(f)
return themes
def test_hover_visibility(themes):
"""Every theme must have hover visually distinct from background.
If raw values are identical, Theme::fromJson applies lighter(130)."""
ok = True
for name, data in sorted(themes.items()):
bg = hex_to_rgb(data['background'])
hover = hex_to_rgb(data['hover'])
dist = color_dist(bg, hover)
if dist < 20:
# fromJson will fix this — verify the fix produces sufficient contrast
fixed = lighter_130(bg)
fixed_dist = color_dist(bg, fixed)
if fixed_dist < 15:
print(f" FAIL: {name}: hover==bg and lighter(130) still too close "
f"(dist={fixed_dist})")
ok = False
else:
print(f" OK: {name}: hover==bg, fromJson fixup -> "
f"dist {dist}->{fixed_dist}")
else:
print(f" OK: {name}: hover distinct (dist={dist})")
return ok
def test_proxystyle_handlers():
"""Verify MenuBarStyle handles CE_MenuBarItem, CE_MenuItem, CE_MenuBarEmptyArea."""
src = os.path.join(os.path.dirname(__file__), '..', 'src', 'main.cpp')
with open(src) as f:
code = f.read()
required = {
'CE_MenuBarItem': r'element\s*==\s*CE_MenuBarItem',
'CE_MenuItem': r'element\s*==\s*CE_MenuItem',
'CE_MenuBarEmptyArea': r'element\s*==\s*CE_MenuBarEmptyArea',
'State_Selected': r'State_Selected',
'QPalette::Mid': r'QPalette::Mid',
}
ok = True
for label, pattern in required.items():
if re.search(pattern, code):
print(f" OK: MenuBarStyle handles {label}")
else:
print(f" FAIL: MenuBarStyle missing {label}")
ok = False
return ok
def test_no_menubar_css():
"""Verify no CSS stylesheet is set on QMenuBar (would bypass QProxyStyle)."""
src_dir = os.path.join(os.path.dirname(__file__), '..', 'src')
ok = True
for root, _, files in os.walk(src_dir):
for fname in files:
if not fname.endswith('.cpp'):
continue
path = os.path.join(root, fname)
with open(path, encoding='utf-8', errors='replace') as f:
for i, line in enumerate(f, 1):
# Check for menuBar/m_menuBar stylesheet calls
if ('menuBar' in line or 'm_menuBar' in line) and \
'setStyleSheet' in line:
print(f" FAIL: CSS on QMenuBar at {fname}:{i}: "
f"{line.strip()}")
ok = False
if ok:
print(" OK: No CSS on QMenuBar")
return ok
def test_hover_fixup_in_fromjson():
"""Verify Theme::fromJson applies the hover fixup."""
src = os.path.join(os.path.dirname(__file__),
'..', 'src', 'themes', 'theme.cpp')
with open(src) as f:
code = f.read()
if 'lighter(130)' in code and 't.hover' in code:
print(" OK: Theme::fromJson has hover fixup")
return True
else:
print(" FAIL: Theme::fromJson missing hover fixup")
return False
def main():
themes = load_themes()
if not themes:
print("FAIL: No theme files found")
return 1
all_ok = True
print("--- Test 1: Hover visibility across themes ---")
all_ok &= test_hover_visibility(themes)
print("\n--- Test 2: QProxyStyle handles required elements ---")
all_ok &= test_proxystyle_handlers()
print("\n--- Test 3: No CSS on QMenuBar ---")
all_ok &= test_no_menubar_css()
print("\n--- Test 4: Theme::fromJson hover fixup ---")
all_ok &= test_hover_fixup_in_fromjson()
print(f"\n{'='*50}")
if all_ok:
print("ALL HOVER TESTS PASSED")
return 0
else:
print("SOME HOVER TESTS FAILED")
return 1
if __name__ == '__main__':
sys.exit(main())