mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
9 Commits
snapshot-2
...
snapshot-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
078a6028f0 | ||
|
|
67218d3e48 | ||
|
|
f651edd740 | ||
|
|
25aaace382 | ||
|
|
b5ddb042b8 | ||
|
|
e900dea836 | ||
|
|
b647a334bc | ||
|
|
fc390bc1f7 | ||
|
|
7efe740ec1 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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; }
|
||||
|
||||
11
src/core.h
11
src/core.h
@@ -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;
|
||||
|
||||
114
src/editor.cpp
114
src/editor.cpp
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
89
src/main.cpp
89
src/main.cpp
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
159
tools/test_hover.py
Normal 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())
|
||||
Reference in New Issue
Block a user