diff --git a/plugins/ProcessMemory/ProcessMemoryPlugin.cpp b/plugins/ProcessMemory/ProcessMemoryPlugin.cpp index 2f04b4e..8a9ff0f 100644 --- a/plugins/ProcessMemory/ProcessMemoryPlugin.cpp +++ b/plugins/ProcessMemory/ProcessMemoryPlugin.cpp @@ -62,9 +62,10 @@ bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const if (!m_handle || len <= 0) return false; SIZE_T bytesRead = 0; - if (ReadProcessMemory(m_handle, (LPCVOID)(m_base + addr), buf, (SIZE_T)len, &bytesRead)) - return bytesRead == (SIZE_T)len; - return false; + ReadProcessMemory(m_handle, (LPCVOID)(m_base + addr), buf, (SIZE_T)len, &bytesRead); + if ((int)bytesRead < len) + memset((char*)buf + bytesRead, 0, len - bytesRead); + return bytesRead > 0; } bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len) @@ -298,9 +299,9 @@ ProcessMemoryProvider::~ProcessMemoryProvider() int ProcessMemoryProvider::size() const { #ifdef _WIN32 - return m_handle ? INT_MAX : 0; + return m_handle ? 0x10000 : 0; #elif defined(__linux__) - return m_fd ? INT_MAX : 0; + return (m_fd >= 0) ? 0x10000 : 0; #endif } diff --git a/plugins/ProcessMemory/ProcessMemoryPlugin.h b/plugins/ProcessMemory/ProcessMemoryPlugin.h index 089d4fc..8c2ab1e 100644 --- a/plugins/ProcessMemory/ProcessMemoryPlugin.h +++ b/plugins/ProcessMemory/ProcessMemoryPlugin.h @@ -25,6 +25,10 @@ public: QString kind() const override { return QStringLiteral("LocalProcess"); } QString getSymbol(uint64_t addr) const override; + bool isLive() const override { return true; } + uint64_t base() const override { return m_base; } + void setBase(uint64_t b) override { m_base = b; } + // Process-specific helpers uint32_t pid() const { return m_pid; } uint64_t baseAddress() const { return m_base; } diff --git a/src/controller.cpp b/src/controller.cpp index bf6e02d..93f04ef 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -447,9 +447,12 @@ void RcxController::connectEditor(RcxEditor* editor) { // Apply provider or show error if (provider) { + uint64_t newBase = provider->base(); m_doc->undoStack.clear(); m_doc->provider = std::move(provider); m_doc->dataPath.clear(); + m_doc->tree.baseAddress = newBase; + resetSnapshot(); emit m_doc->documentChanged(); refresh(); } else if (!errorMsg.isEmpty()) { @@ -860,6 +863,13 @@ void RcxController::applyCommand(const Command& command, bool isUndo) { } } else if constexpr (std::is_same_v) { tree.baseAddress = isUndo ? c.oldBase : c.newBase; + qDebug() << "[ChangeBase] tree.baseAddress =" << Qt::hex << tree.baseAddress + << "provider =" << (m_doc->provider ? "yes" : "null"); + if (m_doc->provider) { + m_doc->provider->setBase(tree.baseAddress); + qDebug() << "[ChangeBase] provider->base() now =" << Qt::hex << m_doc->provider->base(); + } + resetSnapshot(); } else if constexpr (std::is_same_v) { const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes; if (!m_doc->provider->writeBytes(c.addr, bytes)) @@ -1585,6 +1595,9 @@ void RcxController::attachToProcess(uint32_t pid, const QString& processName) { } } + qDebug() << "[AttachProcess]" << processName << "PID" << pid + << "base" << Qt::hex << base << "regionSize" << regionSize; + m_doc->undoStack.clear(); m_doc->provider = std::make_shared( hProc, base, regionSize, processName); @@ -1662,6 +1675,8 @@ void RcxController::onRefreshTick() { // Capture shared_ptr copy — keeps provider alive during async read auto prov = m_doc->provider; + uint64_t base = prov->base(); + qDebug() << "[Refresh] reading" << extent << "bytes from base" << Qt::hex << base; m_refreshWatcher->setFuture(QtConcurrent::run([prov, extent]() -> QByteArray { return prov->readBytes(0, extent); })); @@ -1709,19 +1724,20 @@ void RcxController::onReadComplete() { } int RcxController::computeDataExtent() const { - // Use provider size as the extent (for ProcessProvider this is the module/region size) - int provSize = m_doc->provider->size(); - if (provSize > 0) return provSize; - - // Fallback: walk tree to find maximum byte offset - int maxEnd = 0; + // Prefer tree-based extent: exact bytes needed for rendering + int treeExtent = 0; for (int i = 0; i < m_doc->tree.nodes.size(); i++) { int64_t off = m_doc->tree.computeOffset(i); int sz = m_doc->tree.nodes[i].byteSize(); int end = (int)(off + sz); - if (end > maxEnd) maxEnd = end; + if (end > treeExtent) treeExtent = end; } - return maxEnd; + if (treeExtent > 0) return treeExtent; + + // Fallback: provider size (empty tree) + int provSize = m_doc->provider->size(); + if (provSize > 0) return provSize; + return 0; } void RcxController::resetSnapshot() { diff --git a/src/controller.h b/src/controller.h index 6c95b05..11f7f71 100644 --- a/src/controller.h +++ b/src/controller.h @@ -112,6 +112,7 @@ public: // MCP bridge accessors void setSuppressRefresh(bool v) { m_suppressRefresh = v; } + void attachToProcess(uint32_t pid, const QString& processName); const QVector& savedSources() const { return m_savedSources; } int activeSourceIndex() const { return m_activeSourceIdx; } void switchSource(int idx) { switchToSavedSource(idx); } @@ -147,7 +148,6 @@ private: void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods); void updateCommandRow(); void performRealignment(uint64_t structId, int targetAlign); - void attachToProcess(uint32_t pid, const QString& processName); void switchToSavedSource(int idx); void pushSavedSourcesToEditors(); void showTypeSelectorPopup(RcxEditor* editor); diff --git a/src/main.cpp b/src/main.cpp index 19c5955..4ac27f3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -209,8 +209,9 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { // Load plugins m_pluginManager.LoadPlugins(); - // MCP bridge (stopped by default — user starts via File → Start MCP) + // MCP bridge (on by default) m_mcp = new McpBridge(this, this); + m_mcp->start(); connect(m_mdiArea, &QMdiArea::subWindowActivated, this, [this](QMdiSubWindow*) { @@ -248,7 +249,7 @@ void MainWindow::createMenus() { file->addSeparator(); file->addAction(makeIcon(":/vsicons/export.svg"), "Export &C++ Header...", this, &MainWindow::exportCpp); file->addSeparator(); - m_mcpAction = file->addAction("Start &MCP Server", this, &MainWindow::toggleMcp); + m_mcpAction = file->addAction("Stop &MCP Server", this, &MainWindow::toggleMcp); file->addSeparator(); file->addAction(makeIcon(":/vsicons/close.svg"), "E&xit", QKeySequence(Qt::Key_Close), this, &QMainWindow::close); diff --git a/src/mcp/mcp_bridge.cpp b/src/mcp/mcp_bridge.cpp index 9b62f9b..1be8b67 100644 --- a/src/mcp/mcp_bridge.cpp +++ b/src/mcp/mcp_bridge.cpp @@ -123,6 +123,7 @@ QJsonObject McpBridge::errReply(const QJsonValue& id, int code, const QString& m void McpBridge::sendJson(const QJsonObject& obj) { if (!m_client) return; QByteArray data = QJsonDocument(obj).toJson(QJsonDocument::Compact); + qDebug() << "[MCP] >>" << data.left(200); data.append('\n'); m_client->write(data); m_client->flush(); @@ -151,6 +152,7 @@ QJsonObject McpBridge::makeTextResult(const QString& text, bool isError) { // ════════════════════════════════════════════════════════════════════ void McpBridge::processLine(const QByteArray& line) { + qDebug() << "[MCP] <<" << line.trimmed().left(200); auto doc = QJsonDocument::fromJson(line); if (!doc.isObject()) { sendJson(errReply(QJsonValue(), -32700, "Parse error")); @@ -263,7 +265,7 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) { tools.append(QJsonObject{ {"name", "source.switch"}, {"description", "Switch active data source (provider). Use sourceIndex for saved sources, " - "or filePath to load a new binary file."}, + "filePath to load a binary file, or pid to attach to a live process."}, {"inputSchema", QJsonObject{ {"type", "object"}, {"properties", QJsonObject{ @@ -271,6 +273,10 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) { {"description", "MDI tab index (0-based). Omit for active tab."}}}, {"sourceIndex", QJsonObject{{"type", "integer"}}}, {"filePath", QJsonObject{{"type", "string"}}}, + {"pid", QJsonObject{{"type", "integer"}, + {"description", "Process ID to attach to for live memory reading."}}}, + {"processName", QJsonObject{{"type", "string"}, + {"description", "Display name for the process (optional with pid)."}}}, {"allViews", QJsonObject{{"type", "boolean"}}} }} }} @@ -549,10 +555,12 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) { } // Phase 2: Execute in undo macro - ctrl->setSuppressRefresh(true); + if (!m_slowMode) + ctrl->setSuppressRefresh(true); doc->undoStack.beginMacro(macroName); int applied = 0; + uint64_t lastRootStructId = 0; // track root-level struct inserts QStringList skippedOps; for (int i = 0; i < ops.size(); i++) { // Safety valve: keep paint events flowing for large batches @@ -594,6 +602,8 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) { } doc->undoStack.push(new RcxCommand(ctrl, cmd::Insert{n, {}})); + if (n.parentId == 0 && n.kind == NodeKind::Struct) + lastRootStructId = n.id; applied++; } else if (opType == "remove") { @@ -722,10 +732,22 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) { else { skippedOps.append(QStringLiteral("op[%1]: unknown op '%2'").arg(i).arg(opType)); } + + // Slow mode: refresh after each operation for visual feedback + if (m_slowMode && applied > 0) { + ctrl->refresh(); + QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 16); + } } doc->undoStack.endMacro(); - ctrl->setSuppressRefresh(false); + if (!m_slowMode) + ctrl->setSuppressRefresh(false); + + // Auto-switch view to newly created root struct + if (lastRootStructId) + ctrl->setViewRootId(lastRootStructId); + ctrl->refresh(); // Build response with assigned placeholder IDs @@ -770,6 +792,14 @@ QJsonObject McpBridge::toolSourceSwitch(const QJsonObject& args) { " (" + sources[idx].displayName + ")"); } + if (args.contains("pid")) { + uint32_t pid = (uint32_t)args.value("pid").toInteger(); + QString name = args.value("processName").toString(); + if (name.isEmpty()) name = QString("PID %1").arg(pid); + ctrl->attachToProcess(pid, name); + return makeTextResult("Attached to process " + name + " (PID " + QString::number(pid) + ")"); + } + if (args.contains("filePath")) { QString path = args.value("filePath").toString(); doc->loadData(path); @@ -777,7 +807,7 @@ QJsonObject McpBridge::toolSourceSwitch(const QJsonObject& args) { return makeTextResult("Loaded file: " + path); } - return makeTextResult("Provide sourceIndex or filePath", true); + return makeTextResult("Provide sourceIndex, filePath, or pid", true); } // ════════════════════════════════════════════════════════════════════ diff --git a/src/mcp/mcp_bridge.h b/src/mcp/mcp_bridge.h index 72389ca..2f1d8ab 100644 --- a/src/mcp/mcp_bridge.h +++ b/src/mcp/mcp_bridge.h @@ -20,6 +20,9 @@ public: void stop(); bool isRunning() const { return m_server != nullptr; } + bool slowMode() const { return m_slowMode; } + void setSlowMode(bool v) { m_slowMode = v; } + // Call from controller refresh / data change to notify MCP clients void notifyTreeChanged(); void notifyDataChanged(); @@ -30,6 +33,7 @@ private: QLocalSocket* m_client = nullptr; // single client for v1 QByteArray m_readBuffer; bool m_initialized = false; + bool m_slowMode = false; // JSON-RPC plumbing void onNewConnection(); diff --git a/src/providers/process_provider.h b/src/providers/process_provider.h index cec764d..56aefd8 100644 --- a/src/providers/process_provider.h +++ b/src/providers/process_provider.h @@ -38,10 +38,13 @@ public: bool isReadable(uint64_t, int len) const override { return len >= 0; } bool read(uint64_t addr, void* buf, int len) const override { + if (!m_handle || len <= 0) return false; SIZE_T got = 0; - BOOL ok = ReadProcessMemory(m_handle, + ReadProcessMemory(m_handle, (LPCVOID)(m_base + addr), buf, len, &got); - return ok && (int)got == len; + if ((int)got < len) + memset((char*)buf + got, 0, len - got); + return got > 0; } bool isWritable() const override { return true; } @@ -73,6 +76,8 @@ public: HANDLE handle() const { return m_handle; } uint64_t baseAddress() const { return m_base; } + uint64_t base() const override { return m_base; } + void setBase(uint64_t b) override { m_base = b; } void refreshModules() { m_modules.clear(); cacheModules(); } private: diff --git a/src/providers/provider.h b/src/providers/provider.h index fc700ae..9c9d1c6 100644 --- a/src/providers/provider.h +++ b/src/providers/provider.h @@ -33,6 +33,11 @@ public: // Examples: "File", "Process", "Socket" virtual QString kind() const { return QStringLiteral("File"); } + // Base address for providers that offset reads (e.g. process memory). + // For file/buffer providers this is always 0. + virtual uint64_t base() const { return 0; } + virtual void setBase(uint64_t newBase) { Q_UNUSED(newBase); } + // Resolve an absolute address to a symbol name. // Returns empty string if no symbol is known. // ProcessProvider: "ntdll.dll+0x1A30"