From 078a6028f0500fe73e5b945202c7c9590fa867a0 Mon Sep 17 00:00:00 2001 From: IChooseYou Date: Mon, 23 Feb 2026 08:08:46 -0700 Subject: [PATCH] fix: WinDbg provider stops auto-selecting module, new tabs inherit source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WinDbg provider no longer picks arbitrary module[0] as name/base (was showing "WS2_32" for kernel dumps). Name is now generic "WinDbg (Live)" / "WinDbg (Dump)", base stays 0 so controller doesn't override user's address. - Added throttled read failure logging to WinDbg provider. - New tabs (File→New Class, workspace right-click) inherit the current tab's source/provider so users don't have to re-attach. - Updated WinDbg provider tests for new behavior. --- CMakeLists.txt | 28 ++++ plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp | 66 +++------- plugins/WinDbgMemory/WinDbgMemoryPlugin.h | 1 + src/compose.cpp | 1 + src/controller.cpp | 60 +++++++-- src/controller.h | 1 + src/core.h | 11 ++ src/editor.cpp | 114 ++++++++++++---- src/editor.h | 5 + src/main.cpp | 15 +++ tests/test_windbg_provider.cpp | 138 +++++++++++++++++++- 11 files changed, 354 insertions(+), 86 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 006d6cb..6b335e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp b/plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp index 2b1074a..d13abc8 100644 --- a/plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp +++ b/plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp @@ -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; diff --git a/plugins/WinDbgMemory/WinDbgMemoryPlugin.h b/plugins/WinDbgMemory/WinDbgMemoryPlugin.h index 9c67ffd..b2b5a4e 100644 --- a/plugins/WinDbgMemory/WinDbgMemoryPlugin.h +++ b/plugins/WinDbgMemory/WinDbgMemoryPlugin.h @@ -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 diff --git a/src/compose.cpp b/src/compose.cpp index 7f5a93c..c7f1cba 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -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; diff --git a/src/controller.cpp b/src/controller.cpp index b2ec472..c23f6fb 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -569,9 +569,9 @@ void RcxController::refresh() { // Prune stale selections (nodes removed by undo/redo/delete) QSet 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& sources, int activeIdx) { + m_savedSources = sources; + m_activeSourceIdx = activeIdx; + pushSavedSourcesToEditors(); +} + void RcxController::pushSavedSourcesToEditors() { QVector display; display.reserve(m_savedSources.size()); diff --git a/src/controller.h b/src/controller.h index 4d864e9..525fc96 100644 --- a/src/controller.h +++ b/src/controller.h @@ -131,6 +131,7 @@ public: void switchSource(int idx) { switchToSavedSource(idx); } void clearSources(); void selectSource(const QString& text); + void copySavedSources(const QVector& sources, int activeIdx); // Value tracking toggle (per-tab, off by default) bool trackValues() const { return m_trackValues; } diff --git a/src/core.h b/src/core.h index 7996e00..14999b3 100644 --- a/src/core.h +++ b/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; diff --git a/src/editor.cpp b/src/editor.cpp index 152f440..8409ae3 100644 --- a/src/editor.cpp +++ b/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& 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& 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); }; diff --git a/src/editor.h b/src/editor.h index ceadd96..a5b17e2 100644 --- a/src/editor.h +++ b/src/editor.h @@ -4,6 +4,7 @@ #include #include #include +#include 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 m_currentSelIds; QVector m_hoverSpanLines; // Lines with hover span indicators + // ── nodeId → display-line index (built in applyDocument) ── + QHash> m_nodeLineIndex; // ── Drag selection ── bool m_dragging = false; bool m_dragStarted = false; // true once drag threshold exceeded diff --git a/src/main.cpp b/src/main.cpp index 540e67e..8e9040d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1994,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; } diff --git a/tests/test_windbg_provider.cpp b/tests/test_windbg_provider.cpp index 466c380..a01bf56 100644 --- a/tests/test_windbg_provider.cpp +++ b/tests/test_windbg_provider.cpp @@ -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 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)