mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: Remote Process Memory plugin, source menu icons, base address fix
- Remote Process Memory plugin: shared-memory IPC payload injected into target process (CreateRemoteThread on Win, ptrace+dlopen on Linux), VirtualQuery-based memory safety, PEB-based image base, batch reads - Source dropdown: SVG icons per provider type, DLL filename shown - Fix base address not updating when switching to a new source provider - ProviderRegistry carries DLL filename from PluginManager Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2302,8 +2302,7 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
|
||||
m_doc->undoStack.clear();
|
||||
m_doc->provider = std::move(provider);
|
||||
m_doc->dataPath.clear();
|
||||
if (m_doc->tree.baseAddress == 0)
|
||||
m_doc->tree.baseAddress = newBase;
|
||||
m_doc->tree.baseAddress = (newBase != 0) ? newBase : m_doc->tree.baseAddress;
|
||||
|
||||
// Re-evaluate stored formula against the new provider
|
||||
if (!m_doc->tree.baseAddressFormula.isEmpty()) {
|
||||
@@ -2352,6 +2351,9 @@ void RcxController::switchToSavedSource(int idx) {
|
||||
// Restore formula before attach so it can be re-evaluated against the new provider
|
||||
m_doc->tree.baseAddressFormula = entry.baseAddressFormula;
|
||||
attachViaPlugin(entry.kind, entry.providerTarget);
|
||||
// Restore saved base address (user may have navigated away from provider default)
|
||||
if (entry.baseAddress != 0 && entry.baseAddressFormula.isEmpty())
|
||||
m_doc->tree.baseAddress = entry.baseAddress;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2421,8 +2423,7 @@ void RcxController::selectSource(const QString& text) {
|
||||
m_doc->undoStack.clear();
|
||||
m_doc->provider = std::move(provider);
|
||||
m_doc->dataPath.clear();
|
||||
if (m_doc->tree.baseAddress == 0)
|
||||
m_doc->tree.baseAddress = newBase;
|
||||
m_doc->tree.baseAddress = (newBase != 0) ? newBase : m_doc->tree.baseAddress;
|
||||
resetSnapshot();
|
||||
emit m_doc->documentChanged();
|
||||
|
||||
|
||||
@@ -909,7 +909,7 @@ void RcxEditor::reformatMargins() {
|
||||
// Place offset in the parent's indent slot (one level above the field's own indent)
|
||||
// so the field's own 3-char indent acts as visual separator from the type column
|
||||
int col = kFoldCol + (lm.depth - 2) * 3;
|
||||
int slotWidth = 3;
|
||||
int slotWidth = 5;
|
||||
|
||||
auto pos = [&](int c) -> long {
|
||||
return m_sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN,
|
||||
@@ -1756,8 +1756,8 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
}
|
||||
|
||||
commitInlineEdit();
|
||||
m_currentSelIds.clear(); // stale — normal handler will re-establish
|
||||
// Fall through to normal click handler below
|
||||
m_currentSelIds.clear();
|
||||
return true; // consume — metadata was recomposed; stale coords unsafe
|
||||
}
|
||||
// Single-click on fold column (" - " / " + ") toggles fold
|
||||
// Other left-clicks emit nodeClicked for selection
|
||||
|
||||
78
src/main.cpp
78
src/main.cpp
@@ -97,6 +97,53 @@ static LONG WINAPI crashHandler(EXCEPTION_POINTERS* ep) {
|
||||
#endif
|
||||
fflush(stderr);
|
||||
|
||||
// Phase 1.5: write a full minidump next to the executable
|
||||
{
|
||||
// Build dump path: <exe_dir>/reclass_crash_<YYYYMMDD_HHMMSS>.dmp
|
||||
wchar_t exePath[MAX_PATH] = {};
|
||||
GetModuleFileNameW(NULL, exePath, MAX_PATH);
|
||||
// Strip exe filename to get directory
|
||||
wchar_t* lastSlash = wcsrchr(exePath, L'\\');
|
||||
if (lastSlash) *(lastSlash + 1) = L'\0';
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
wchar_t dumpPath[MAX_PATH];
|
||||
_snwprintf(dumpPath, MAX_PATH,
|
||||
L"%sreclass_crash_%04d%02d%02d_%02d%02d%02d.dmp",
|
||||
exePath, st.wYear, st.wMonth, st.wDay,
|
||||
st.wHour, st.wMinute, st.wSecond);
|
||||
|
||||
HANDLE hFile = CreateFileW(dumpPath, GENERIC_WRITE, 0, NULL,
|
||||
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
if (hFile != INVALID_HANDLE_VALUE) {
|
||||
MINIDUMP_EXCEPTION_INFORMATION mei;
|
||||
mei.ThreadId = GetCurrentThreadId();
|
||||
mei.ExceptionPointers = ep;
|
||||
mei.ClientPointers = FALSE;
|
||||
|
||||
// MiniDumpWithFullMemory: captures entire process address space
|
||||
// so we can inspect all heap objects, Qt state, node trees, etc.
|
||||
BOOL ok = MiniDumpWriteDump(
|
||||
GetCurrentProcess(), GetCurrentProcessId(), hFile,
|
||||
static_cast<MINIDUMP_TYPE>(MiniDumpWithFullMemory
|
||||
| MiniDumpWithHandleData
|
||||
| MiniDumpWithThreadInfo
|
||||
| MiniDumpWithUnloadedModules),
|
||||
&mei, NULL, NULL);
|
||||
CloseHandle(hFile);
|
||||
|
||||
if (ok) {
|
||||
fprintf(stderr, "Dump : %ls\n", dumpPath);
|
||||
} else {
|
||||
fprintf(stderr, "Dump : FAILED (error %lu)\n", GetLastError());
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "Dump : could not create file (error %lu)\n", GetLastError());
|
||||
}
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
// Phase 2: attempt symbol resolution + stack walk
|
||||
// Copy context so StackWalk64 can mutate it safely
|
||||
CONTEXT ctxCopy = *ep->ContextRecord;
|
||||
@@ -689,9 +736,11 @@ private:
|
||||
label->setGeometry(tw + 1 + gutter, 0,
|
||||
qMax(0, width() - (tw + 1 + gutter)), h);
|
||||
|
||||
// Shared baseline so tab text and status text align
|
||||
// Shared baseline so tab text and status text align.
|
||||
// Nudge up by half the accent-line height so text centres
|
||||
// in the visible area below the accent bar, not in the full bar.
|
||||
QFontMetrics fm(font());
|
||||
int by = (h + fm.ascent()) / 2;
|
||||
int by = (h + fm.ascent()) / 2 - (ViewTabButton::kAccentH + 1) / 2;
|
||||
|
||||
// Push baseline to buttons
|
||||
auto* lay = tabRow->layout();
|
||||
@@ -1136,6 +1185,7 @@ void MainWindow::selfTest() {
|
||||
// Attach process memory to self — provider base will be set to the editor address
|
||||
DWORD pid = GetCurrentProcessId();
|
||||
QString target = QString("%1:Reclass.exe").arg(pid);
|
||||
|
||||
ctrl->attachViaPlugin(QStringLiteral("processmemory"), target);
|
||||
#else
|
||||
project_new();
|
||||
@@ -2222,14 +2272,31 @@ void MainWindow::populateSourceMenu() {
|
||||
m_sourceMenu->clear();
|
||||
auto* ctrl = activeController();
|
||||
|
||||
m_sourceMenu->addAction("File", this, [this]() {
|
||||
// Icon map for known provider identifiers
|
||||
static const QHash<QString, QString> s_providerIcons = {
|
||||
{QStringLiteral("processmemory"), QStringLiteral(":/vsicons/server-process.svg")},
|
||||
{QStringLiteral("remoteprocessmemory"), QStringLiteral(":/vsicons/remote.svg")},
|
||||
{QStringLiteral("windbgmemory"), QStringLiteral(":/vsicons/debug.svg")},
|
||||
{QStringLiteral("reclass.netcompatlayer"), QStringLiteral(":/vsicons/plug.svg")},
|
||||
};
|
||||
|
||||
m_sourceMenu->addAction(QIcon(QStringLiteral(":/vsicons/file-binary.svg")),
|
||||
QStringLiteral("File"), this, [this]() {
|
||||
if (auto* c = activeController()) c->selectSource(QStringLiteral("File"));
|
||||
});
|
||||
|
||||
const auto& providers = ProviderRegistry::instance().providers();
|
||||
for (const auto& prov : providers) {
|
||||
QString name = prov.name;
|
||||
m_sourceMenu->addAction(name, this, [this, name]() {
|
||||
auto it = s_providerIcons.constFind(prov.identifier);
|
||||
QIcon icon(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]() {
|
||||
if (auto* c = activeController()) c->selectSource(name);
|
||||
});
|
||||
}
|
||||
@@ -2247,7 +2314,8 @@ void MainWindow::populateSourceMenu() {
|
||||
act->setChecked(i == ctrl->activeSourceIndex());
|
||||
}
|
||||
m_sourceMenu->addSeparator();
|
||||
m_sourceMenu->addAction("Clear All", this, [this]() {
|
||||
m_sourceMenu->addAction(QIcon(QStringLiteral(":/vsicons/clear-all.svg")),
|
||||
QStringLiteral("Clear All"), this, [this]() {
|
||||
if (auto* c = activeController()) c->clearSources();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -92,7 +92,8 @@ bool PluginManager::LoadPlugin(const QString& path)
|
||||
IProviderPlugin* provider = static_cast<IProviderPlugin*>(plugin);
|
||||
QString name = QString::fromStdString(plugin->Name());
|
||||
QString identifier = name.toLower().replace(" ", "");
|
||||
ProviderRegistry::instance().registerProvider(name, identifier, provider);
|
||||
QString dllFileName = QFileInfo(path).fileName();
|
||||
ProviderRegistry::instance().registerProvider(name, identifier, provider, dllFileName);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -6,7 +6,8 @@ ProviderRegistry& ProviderRegistry::instance() {
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
void ProviderRegistry::registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin) {
|
||||
void ProviderRegistry::registerProvider(const QString& name, const QString& identifier,
|
||||
IProviderPlugin* plugin, const QString& dllFileName) {
|
||||
// Check if already registered
|
||||
for (const auto& info : m_providers) {
|
||||
if (info.identifier == identifier) {
|
||||
@@ -14,8 +15,8 @@ void ProviderRegistry::registerProvider(const QString& name, const QString& iden
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_providers.append(ProviderInfo(name, identifier, plugin));
|
||||
|
||||
m_providers.append(ProviderInfo(name, identifier, plugin, dllFileName));
|
||||
qDebug() << "ProviderRegistry: Registered plugin provider:" << name << "(" << identifier << ")";
|
||||
}
|
||||
|
||||
|
||||
@@ -25,10 +25,13 @@ public:
|
||||
IProviderPlugin* plugin; // Plugin (if plugin-based)
|
||||
BuiltinFactory factory; // Factory (if built-in)
|
||||
bool isBuiltin;
|
||||
|
||||
ProviderInfo(const QString& n, const QString& id, IProviderPlugin* p)
|
||||
: name(n), identifier(id), plugin(p), factory(nullptr), isBuiltin(false) {}
|
||||
|
||||
QString dllFileName; // Original DLL/SO filename (plugin-based only)
|
||||
|
||||
ProviderInfo(const QString& n, const QString& id, IProviderPlugin* p,
|
||||
const QString& dll = {})
|
||||
: name(n), identifier(id), plugin(p), factory(nullptr),
|
||||
isBuiltin(false), dllFileName(dll) {}
|
||||
|
||||
ProviderInfo(const QString& n, const QString& id, BuiltinFactory f)
|
||||
: name(n), identifier(id), plugin(nullptr), factory(f), isBuiltin(true) {}
|
||||
};
|
||||
@@ -36,7 +39,8 @@ public:
|
||||
static ProviderRegistry& instance();
|
||||
|
||||
// Register a plugin-based provider
|
||||
void registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin);
|
||||
void registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin,
|
||||
const QString& dllFileName = {});
|
||||
|
||||
// Register a built-in provider with a factory function
|
||||
void registerBuiltinProvider(const QString& name, const QString& identifier, BuiltinFactory factory);
|
||||
|
||||
@@ -51,5 +51,9 @@
|
||||
<file alias="chevron-down.svg">vsicons/chevron-down.svg</file>
|
||||
<file alias="folder.svg">vsicons/folder.svg</file>
|
||||
<file alias="symbol-enum.svg">vsicons/symbol-enum.svg</file>
|
||||
<file alias="server-process.svg">vsicons/server-process.svg</file>
|
||||
<file alias="remote.svg">vsicons/remote.svg</file>
|
||||
<file alias="plug.svg">vsicons/plug.svg</file>
|
||||
<file alias="clear-all.svg">vsicons/clear-all.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
Reference in New Issue
Block a user