#include "pages.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static QLabel* section_label(const QString& t) { auto* l = new QLabel(t); l->setObjectName("sectionLabel"); return l; } static QLabel* h2(const QString& t) { auto* l = new QLabel(t); l->setObjectName("h2"); return l; } static QFrame* card() { auto* f = new QFrame; f->setObjectName("card"); f->setFrameShape(QFrame::NoFrame); return f; } static QLabel* pill(const QString& t, const QString& kind = "") { auto* l = new QLabel(t); l->setObjectName("pill"); if (!kind.isEmpty()) l->setProperty("kind", kind); return l; } // ModulesPage // WorkbenchPage static const char* CAP_NAMES[] = { "ALLOC","READ_KMEM","WRITE_KMEM","MSR_R","MSR_W","PHYSMEM","CPUID_TSC","CALLBACKS","HOSTCALL","INTROSPECT" }; WorkbenchPage::WorkbenchPage(DriverClient* d, QWidget* p) : QWidget(p), drv(d) { auto* root = new QVBoxLayout(this); root->setContentsMargins(10, 10, 10, 8); root->setSpacing(14); // load bar - full width, minimal auto* loadRow = new QHBoxLayout; loadRow->setSpacing(8); loadPath = new QLineEdit; loadPath->setPlaceholderText("path to .wasm (or drag file here / Ctrl+O)"); auto* browseBtn = new QPushButton("Browse"); auto* loadBtn = new QPushButton("Load"); loadBtn->setObjectName("btnPrimary"); loadBudget = new QSpinBox; loadBudget->setRange(0, 1 << 30); loadBudget->setToolTip("pool budget in bytes, 0 = 4MB default"); loadBudget->setFixedWidth(120); loadBudget->setSpecialValueText("default pool"); loadRow->addWidget(loadPath, 1); loadRow->addWidget(loadBudget); loadRow->addWidget(browseBtn); loadRow->addWidget(loadBtn); root->addLayout(loadRow); connect(browseBtn, &QPushButton::clicked, this, [this]{ QString p = QFileDialog::getOpenFileName(this, "Select .wasm", QString(), "WebAssembly (*.wasm);;All (*.*)"); if (!p.isEmpty()) loadPath->setText(p); }); connect(loadBtn, &QPushButton::clicked, this, [this]{ if (loadPath->text().isEmpty()) { emit toast("pick a .wasm first", 1); return; } quint32 id = 0; QString err; if (drv->load_module(loadPath->text(), loadBudget->value(), id, err)) { pending_select = id; emit toast(QString("loaded module %1").arg(id), 2); emit moduleLoaded(id); } else { emit toast(QString("load failed: %1").arg(err), 1); } }); connect(loadPath, &QLineEdit::returnPressed, loadBtn, &QPushButton::click); // horizontal split: module list left, inspector+invoke right auto* mainSplit = new QSplitter(Qt::Horizontal); mainSplit->setChildrenCollapsible(false); mainSplit->setObjectName("wbSplit"); // LEFT: module list auto* leftCol = new QWidget; auto* lcv = new QVBoxLayout(leftCol); lcv->setContentsMargins(0, 0, 0, 0); lcv->setSpacing(8); auto* leftHead = new QHBoxLayout; leftHead->setContentsMargins(2, 0, 2, 0); auto* leftLbl = new QLabel("Loaded"); leftLbl->setObjectName("h2"); moduleCount = new QLabel("0"); moduleCount->setObjectName("dim"); auto* unloadAll = new QPushButton("Unload all"); unloadAll->setToolTip("Unload every module"); leftHead->addWidget(leftLbl); leftHead->addWidget(moduleCount); leftHead->addStretch(); leftHead->addWidget(unloadAll); lcv->addLayout(leftHead); moduleList = new QListWidget; moduleList->setObjectName("nav"); // reuse muted nav style moduleList->setFocusPolicy(Qt::StrongFocus); moduleList->setFrameShape(QFrame::NoFrame); lcv->addWidget(moduleList, 1); mainSplit->addWidget(leftCol); connect(unloadAll, &QPushButton::clicked, this, [this]{ drv->unload_all(); clearSelection(); emit toast("unloaded all modules", 2); }); connect(moduleList, &QListWidget::currentItemChanged, this, [this](QListWidgetItem* cur, QListWidgetItem*){ if (!cur) return; quint32 id = cur->data(Qt::UserRole).toUInt(); emit moduleSelected(id); }); // RIGHT: inspector + invoke wrapper (empty state OR infoPanel) auto* rightCol = new QWidget; auto* rcv = new QVBoxLayout(rightCol); rcv->setContentsMargins(0, 0, 0, 0); rcv->setSpacing(0); emptyPanel = new QWidget; auto* el = new QVBoxLayout(emptyPanel); el->setContentsMargins(0, 60, 0, 0); auto* emptyLbl = new QLabel("Select a module from the list on the left,\nor load a .wasm above."); emptyLbl->setAlignment(Qt::AlignCenter); emptyLbl->setObjectName("emptyState"); el->addWidget(emptyLbl); rcv->addWidget(emptyPanel); infoPanel = new QWidget; auto* ipl = new QVBoxLayout(infoPanel); ipl->setContentsMargins(0, 0, 0, 0); ipl->setSpacing(14); // module header card with unload button auto* headCard = card(); auto* hl = new QHBoxLayout(headCard); hl->setContentsMargins(20, 14, 16, 14); hl->setSpacing(10); auto* headText = new QVBoxLayout; headText->setSpacing(2); modTitle = new QLabel; modTitle->setObjectName("moduleTitle"); modMeta = new QLabel; modMeta->setObjectName("mono"); modMeta->setStyleSheet("color:#7b818c;"); headText->addWidget(modTitle); headText->addWidget(modMeta); hl->addLayout(headText, 1); auto* unloadOne = new QPushButton("Unload"); unloadOne->setObjectName("btnDanger"); hl->addWidget(unloadOne, 0, Qt::AlignTop); ipl->addWidget(headCard); connect(unloadOne, &QPushButton::clicked, this, [this]{ if (!current.valid) return; quint32 id = current.base.id; drv->unload(id); clearSelection(); emit toast(QString("unloaded module %1").arg(id), 2); }); // caps card auto* capCard = card(); auto* cl = new QVBoxLayout(capCard); cl->setContentsMargins(20, 14, 20, 14); cl->setSpacing(8); cl->addWidget(section_label("Capabilities")); capsBar = new QWidget; auto* capsLay = new QHBoxLayout(capsBar); capsLay->setContentsMargins(0, 0, 0, 0); capsLay->setSpacing(6); capsLay->addStretch(); cl->addWidget(capsBar); ipl->addWidget(capCard); // inner split: exports/imports left, invoke right auto* split = new QSplitter(Qt::Horizontal); split->setChildrenCollapsible(false); split->setObjectName("wbSplit"); split->setHandleWidth(3); // exports/imports tabs card auto* eiCard = card(); auto* eil = new QVBoxLayout(eiCard); eil->setContentsMargins(0, 0, 0, 0); auto* tabs = new QTabWidget; tabs->setObjectName("wbTabs"); expTable = new QTableWidget(0, 2); expTable->setObjectName("dataTable"); expTable->setHorizontalHeaderLabels({"#", "Name"}); expTable->horizontalHeader()->setStretchLastSection(true); expTable->setColumnWidth(0, 40); expTable->verticalHeader()->setVisible(false); expTable->setEditTriggers(QAbstractItemView::NoEditTriggers); expTable->setSelectionBehavior(QAbstractItemView::SelectRows); expTable->setShowGrid(false); expTable->setAlternatingRowColors(true); tabs->addTab(expTable, "Exports"); impTable = new QTableWidget(0, 2); impTable->setObjectName("dataTable"); impTable->setHorizontalHeaderLabels({"#", "Name"}); impTable->horizontalHeader()->setStretchLastSection(true); impTable->setColumnWidth(0, 40); impTable->verticalHeader()->setVisible(false); impTable->setEditTriggers(QAbstractItemView::NoEditTriggers); impTable->setShowGrid(false); impTable->setAlternatingRowColors(true); tabs->addTab(impTable, "Imports"); eil->addWidget(tabs); split->addWidget(eiCard); connect(expTable, &QTableWidget::itemDoubleClicked, this, [this](QTableWidgetItem* it){ if (it->column() != 1) return; callExport->setText(it->text()); }); // invoke card auto* invCard = card(); auto* il = new QVBoxLayout(invCard); il->setContentsMargins(20, 16, 20, 16); il->setSpacing(10); il->addWidget(h2("Invoke export")); auto* form = new QFormLayout; form->setLabelAlignment(Qt::AlignRight); form->setSpacing(8); callExport = new QLineEdit; callExport->setPlaceholderText("pick from Exports list, or type name"); callArgs = new QLineEdit; callArgs->setPlaceholderText("hex/dec, comma-separated"); callArgs->setFont(QFont("JetBrainsMono NF", 10)); callTimeout = new QSpinBox; callTimeout->setRange(0, 60000); callTimeout->setSuffix(" ms"); form->addRow("Export", callExport); form->addRow("Args", callArgs); form->addRow("Timeout", callTimeout); il->addLayout(form); auto* callBtn = new QPushButton("Call"); callBtn->setObjectName("btnPrimary"); il->addWidget(callBtn); il->addWidget(section_label("Last result")); callResult = new QLabel("no calls yet"); callResult->setObjectName("resultLabel"); callResult->setTextInteractionFlags(Qt::TextSelectableByMouse); il->addWidget(callResult); il->addWidget(section_label("Call history")); callHist = new QTableWidget(0, 3); callHist->setObjectName("dataTable"); callHist->setHorizontalHeaderLabels({"Time", "Export", "Result"}); callHist->horizontalHeader()->setStretchLastSection(false); callHist->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); callHist->setColumnWidth(0, 80); callHist->setColumnWidth(2, 160); callHist->verticalHeader()->setVisible(false); callHist->setEditTriggers(QAbstractItemView::NoEditTriggers); callHist->setShowGrid(false); callHist->setAlternatingRowColors(true); il->addWidget(callHist, 1); split->addWidget(invCard); split->setStretchFactor(0, 55); split->setStretchFactor(1, 45); split->setSizes({560, 440}); ipl->addWidget(split, 1); rcv->addWidget(infoPanel); infoPanel->hide(); mainSplit->addWidget(rightCol); mainSplit->setStretchFactor(0, 0); mainSplit->setStretchFactor(1, 1); mainSplit->setSizes({260, 900}); root->addWidget(mainSplit, 1); connect(callBtn, &QPushButton::clicked, this, [this]{ if (!current.valid) return; QVector args; for (const QString& tok : callArgs->text().split(',', Qt::SkipEmptyParts)) { QString s = tok.trimmed(); if (s.isEmpty()) continue; bool ok = false; quint64 v = s.startsWith("0x", Qt::CaseInsensitive) ? s.mid(2).toULongLong(&ok, 16) : s.toULongLong(&ok, 0); if (ok) args.push_back(v); } quint64 rv = 0; QString err; bool ok = drv->call_export(current.base.id, callExport->text(), callTimeout->value(), args, rv, err); int row = callHist->rowCount(); callHist->insertRow(0); callHist->setItem(0, 0, new QTableWidgetItem(QDateTime::currentDateTime().toString("HH:mm:ss"))); callHist->setItem(0, 1, new QTableWidgetItem(callExport->text())); QString rs = ok ? QString::asprintf("0x%llx", (unsigned long long)rv) : ("err: " + err); auto* ri = new QTableWidgetItem(rs); ri->setForeground(ok ? QColor("#ffffff") : QColor("#e06c6c")); callHist->setItem(0, 2, ri); if (callHist->rowCount() > 100) callHist->removeRow(100); if (ok) { callResult->setText(QString::asprintf("0x%016llx", (unsigned long long)rv)); callResult->setStyleSheet("color:#f3f4f6; font-family:'JetBrainsMono NF'; font-size:15px; font-weight:500;"); emit toast(QString("call ok: 0x%1").arg(rv, 0, 16), 2); } else { callResult->setText(err); callResult->setStyleSheet("color:#e06c6c; font-family:'JetBrainsMono NF'; font-size:13px;"); emit toast(QString("call failed: %1").arg(err), 1); } (void)row; }); } void WorkbenchPage::refreshModules(const QVector& mods) { // remember which module was selected, restore selection after rebuild quint32 prev = current.valid ? current.base.id : (quint32)-1; if (pending_select) { prev = pending_select; pending_select = 0; } // hide the toolkit guest from this list. it's a GUI-internal module the // Explorer tab drives; if the user sees it here alongside their own wasm // it just looks like something they didn't load. QVector visible; for (const auto& m : mods) { QString base = QFileInfo(m.name).fileName(); if (base.compare("toolkit.wasm", Qt::CaseInsensitive) == 0) continue; visible.push_back(m); } moduleList->blockSignals(true); moduleList->clear(); for (const auto& m : visible) { QString base = QFileInfo(m.name).fileName(); if (base.isEmpty()) base = m.name; auto* it = new QListWidgetItem(QString("%1 %2").arg(m.id, 2, 10, QChar('0')).arg(base)); it->setData(Qt::UserRole, m.id); it->setToolTip(QString("%1\nhash %2\nwasm %3 B, mem %4 KB, pool %5 B") .arg(m.name) .arg(m.hash, 16, 16, QChar('0')) .arg(m.wasm_size).arg(m.mem_pages * 64).arg(m.pool_bytes)); moduleList->addItem(it); if (m.id == prev) moduleList->setCurrentItem(it); } moduleList->blockSignals(false); moduleCount->setText(QString("(%1)").arg(visible.size())); if (visible.isEmpty()) clearSelection(); else if (prev != (quint32)-1) { // trigger the same signal path so info reloads even if selection didn't change for (int i = 0; i < moduleList->count(); i++) { if (moduleList->item(i)->data(Qt::UserRole).toUInt() == prev) { emit moduleSelected(prev); break; } } } } void WorkbenchPage::setSelected(quint32 id, const ModuleInfo& info) { (void)id; current = info; if (!info.valid) { clearSelection(); return; } emptyPanel->hide(); infoPanel->show(); modTitle->setText(QFileInfo(info.base.name).fileName().isEmpty() ? info.base.name : QFileInfo(info.base.name).fileName()); modMeta->setText(QString::asprintf("id %u %016llx %.1f KB wasm %u KB mem %llu B pool", info.base.id, (unsigned long long)info.base.hash, info.base.wasm_size/1024.0, info.base.mem_pages*64, (unsigned long long)info.base.pool_bytes)); refreshCaps(info.caps); // filter out internal exports (__gvm_caps) so the user sees only what they wrote QStringList user_exports; for (const auto& e : info.exports) if (!e.startsWith("__gvm_")) user_exports << e; expTable->setRowCount(user_exports.size()); QFont mono("JetBrainsMono NF", 10); for (int i = 0; i < user_exports.size(); i++) { auto* n = new QTableWidgetItem(QString::number(i)); n->setFont(mono); n->setForeground(QColor("#7b818c")); expTable->setItem(i, 0, n); auto* e = new QTableWidgetItem(user_exports[i]); e->setFont(mono); expTable->setItem(i, 1, e); } impTable->setRowCount(info.imports.size()); for (int i = 0; i < info.imports.size(); i++) { auto* n = new QTableWidgetItem(QString::number(i)); n->setFont(mono); n->setForeground(QColor("#7b818c")); impTable->setItem(i, 0, n); auto* e = new QTableWidgetItem(info.imports[i]); e->setFont(mono); e->setForeground(QColor("#a8adb8")); impTable->setItem(i, 1, e); } // auto-populate export field with first user export, but don't clobber // whatever the user typed unless it doesn't match anything from this module if (!user_exports.isEmpty() && !user_exports.contains(callExport->text())) callExport->setText(user_exports.first()); // highlight the corresponding row in the list without re-emitting signals moduleList->blockSignals(true); for (int i = 0; i < moduleList->count(); i++) { if (moduleList->item(i)->data(Qt::UserRole).toUInt() == info.base.id) { moduleList->setCurrentRow(i); break; } } moduleList->blockSignals(false); } void WorkbenchPage::clearSelection() { current = {}; infoPanel->hide(); emptyPanel->show(); } void WorkbenchPage::refreshCaps(quint32 caps) { auto* lay = static_cast(capsBar->layout()); while (auto* it = lay->takeAt(0)) { if (it->widget()) it->widget()->deleteLater(); delete it; } for (int i = 0; i < 10; i++) { bool on = (caps >> i) & 1; lay->addWidget(pill(CAP_NAMES[i], on ? "on" : "off")); } lay->addStretch(); } // MemoryPage MemoryPage::MemoryPage(DriverClient* d, QWidget* p) : QWidget(p), drv(d) { auto* root = new QVBoxLayout(this); root->setContentsMargins(10, 10, 10, 8); root->setSpacing(14); auto* c = card(); auto* cl = new QVBoxLayout(c); cl->setContentsMargins(20, 16, 20, 16); cl->setSpacing(10); cl->addWidget(h2("Read")); auto* form = new QFormLayout; form->setSpacing(8); modId = new QSpinBox; modId->setRange(-1, 999); modId->setValue(-1); modId->setSpecialValueText("(not set)"); exp = new QLineEdit; exp->setText("toolkit_read_kmem"); addr = new QLineEdit; addr->setText("0xfffff800`00000000"); addr->setFont(QFont("JetBrainsMono NF", 10)); length = new QSpinBox; length->setRange(16, 4096); length->setValue(256); length->setSuffix(" bytes"); form->addRow("Toolkit module id", modId); form->addRow("Export", exp); form->addRow("Address", addr); form->addRow("Length", length); cl->addLayout(form); auto* readBtn = new QPushButton("Read"); readBtn->setObjectName("btnPrimary"); cl->addWidget(readBtn, 0, Qt::AlignLeft); status = new QLabel(""); status->setObjectName("dim"); cl->addWidget(status); root->addWidget(c); hex = new QPlainTextEdit; hex->setReadOnly(true); hex->setFont(QFont("JetBrainsMono NF", 10)); hex->setObjectName("hexView"); hex->setPlaceholderText("Hex output will appear here after a successful read."); root->addWidget(hex, 1); connect(readBtn, &QPushButton::clicked, this, [this]{ status->clear(); if (modId->value() < 0) { status->setText("set toolkit mod id first"); return; } QString a = addr->text(); a.remove('`').remove(' '); bool ok = false; quint64 va = a.startsWith("0x", Qt::CaseInsensitive) ? a.mid(2).toULongLong(&ok, 16) : a.toULongLong(&ok, 0); if (!ok) { status->setText("bad address"); return; } QVector argv = { va, 0, (quint64)length->value() }; quint64 rv = 0; QString err; if (drv->call_export(modId->value(), exp->text(), 2000, argv, rv, err)) status->setText(QString::asprintf("toolkit rv=0x%llx bytes read. hex readback needs the new IOCTL.", (unsigned long long)rv)); else status->setText("call failed: " + err); }); } // EventsPage static const char* evt_tag(int k) { switch (k) { case EVT_PROC: return "PROC"; case EVT_EXIT: return "EXIT"; case EVT_IMAGE: return "IMAGE"; case EVT_CALL: return "CALL"; case EVT_LOAD: return "LOAD"; case EVT_UNLOAD: return "UNLD"; case EVT_ERROR: return "ERR"; default: return "MSG"; } } static QColor evt_col(int k) { switch (k) { case EVT_PROC: case EVT_LOAD: return QColor("#8fbf5c"); case EVT_EXIT: case EVT_ERROR: return QColor("#cd5c5c"); case EVT_IMAGE: return QColor("#7cb1f0"); case EVT_CALL: return QColor("#d4a259"); case EVT_UNLOAD: return QColor("#b96060"); default: return QColor("#d4d4d4"); } } EventsPage::EventsPage(DriverClient* d, QWidget* p) : QWidget(p), drv(d) { auto* root = new QVBoxLayout(this); root->setContentsMargins(10, 10, 10, 8); root->setSpacing(10); // control card: load + start + stop + status auto* ctlCard = card(); auto* cl = new QHBoxLayout(ctlCard); cl->setContentsMargins(20, 12, 20, 12); cl->setSpacing(8); loadBtn = new QPushButton("Load tracer"); loadBtn->setToolTip("Load samples/process_tracer.wasm"); startBtn = new QPushButton("Start (poll)"); startBtn->setToolTip("Enable callbacks. Events log via driver notify handlers."); reactiveBtn = new QPushButton("Start reactive"); reactiveBtn->setObjectName("btnPrimary"); reactiveBtn->setToolTip("Enable callbacks and spawn kernel dispatch worker that invokes guest exports."); stopBtn = new QPushButton("Stop"); stopBtn->setObjectName("btnDanger"); stopBtn->setToolTip("Unload the tracer module (removes registered callbacks)"); statusLbl = new QLabel; cl->addWidget(loadBtn); cl->addWidget(startBtn); cl->addWidget(reactiveBtn); cl->addWidget(stopBtn); cl->addSpacing(8); cl->addWidget(statusLbl, 1); root->addWidget(ctlCard); set_status("no tracer loaded", "#8b8b8b"); startBtn->setEnabled(false); reactiveBtn->setEnabled(false); stopBtn->setEnabled(false); connect(loadBtn, &QPushButton::clicked, this, &EventsPage::on_load_tracer); connect(startBtn, &QPushButton::clicked, this, [this]{ on_start(false); }); connect(reactiveBtn, &QPushButton::clicked, this, [this]{ on_start(true); }); connect(stopBtn, &QPushButton::clicked, this, &EventsPage::on_stop); // filter bar auto* barCard = card(); auto* bl = new QHBoxLayout(barCard); bl->setContentsMargins(20, 12, 20, 12); bl->setSpacing(12); cbProc = new QCheckBox("Proc"); cbProc->setChecked(true); cbExit = new QCheckBox("Exit"); cbExit->setChecked(true); cbImage = new QCheckBox("Image"); cbImage->setChecked(true); cbCall = new QCheckBox("Call"); cbCall->setChecked(true); cbError = new QCheckBox("Error"); cbError->setChecked(true); filter = new QLineEdit; filter->setPlaceholderText("Substring filter"); filter->setFixedWidth(260); bl->addWidget(cbProc); bl->addWidget(cbExit); bl->addWidget(cbImage); bl->addWidget(cbCall); bl->addWidget(cbError); bl->addStretch(); bl->addWidget(filter); root->addWidget(barCard); table = new QTableWidget(0, 3); table->setObjectName("dataTable"); table->setHorizontalHeaderLabels({"Time", "Kind", "Line"}); table->horizontalHeader()->setStretchLastSection(true); table->setColumnWidth(0, 110); table->setColumnWidth(1, 80); table->verticalHeader()->setVisible(false); table->setEditTriggers(QAbstractItemView::NoEditTriggers); table->setShowGrid(false); table->setAlternatingRowColors(true); root->addWidget(table, 1); } void EventsPage::append(const QVector& entries) { for (const auto& e : entries) { bool show = (e.kind == EVT_PROC && cbProc->isChecked()) || (e.kind == EVT_EXIT && cbExit->isChecked()) || (e.kind == EVT_IMAGE && cbImage->isChecked()) || ((e.kind == EVT_CALL || e.kind == EVT_LOAD || e.kind == EVT_UNLOAD) && cbCall->isChecked()) || (e.kind == EVT_ERROR && cbError->isChecked()); if (!show) continue; QString f = filter->text(); if (!f.isEmpty() && !e.line.contains(f)) continue; table->insertRow(0); auto* ts = new QTableWidgetItem(e.ts); ts->setFont(QFont("JetBrainsMono NF", 10)); ts->setForeground(QColor("#8a8a8a")); table->setItem(0, 0, ts); auto* kd = new QTableWidgetItem(evt_tag(e.kind)); kd->setFont(QFont("JetBrainsMono NF", 10)); kd->setForeground(evt_col(e.kind)); table->setItem(0, 1, kd); auto* ln = new QTableWidgetItem(e.line); ln->setFont(QFont("JetBrainsMono NF", 10)); table->setItem(0, 2, ln); if (table->rowCount() > 2000) table->removeRow(2000); } } void EventsPage::clear() { table->setRowCount(0); } void EventsPage::set_status(const QString& s, const QString& color) { statusLbl->setText(s); statusLbl->setStyleSheet(QString("color:%1; font-size:12px;").arg(color)); } void EventsPage::on_load_tracer() { QString here = QCoreApplication::applicationDirPath(); QStringList candidates = { here + "/../features/process_tracer.wasm", here + "/features/process_tracer.wasm", here + "/process_tracer.wasm", here + "/../samples/process_tracer.wasm", }; QString path; for (const auto& p : candidates) { if (QFile::exists(p)) { path = p; break; } } if (path.isEmpty()) { path = QFileDialog::getOpenFileName(this, "Select process_tracer.wasm", QString(), "WebAssembly (*.wasm)"); if (path.isEmpty()) return; } quint32 id = 0; QString err; if (!drv->load_module(path, 0, id, err)) { set_status(QString("load failed: %1").arg(err), "#e06c6c"); emit toast("load failed: " + err, 1); return; } tracer_id = (int)id; set_status(QString("tracer loaded as module %1").arg(id), "#7fb069"); startBtn->setEnabled(true); reactiveBtn->setEnabled(true); stopBtn->setEnabled(true); loadBtn->setEnabled(false); emit toast(QString("tracer loaded (module %1)").arg(id), 2); } void EventsPage::on_start(bool reactive) { if (tracer_id < 0) { emit toast("load tracer first", 1); return; } const char* fn = reactive ? "start_reactive" : "start"; quint64 rv = 0; QString err; if (drv->call_export((quint32)tracer_id, fn, 2000, {}, rv, err)) { running = true; set_status(QString("%1 running (module %2)").arg(reactive ? "reactive dispatch" : "callbacks").arg(tracer_id), "#7fb069"); emit toast(QString("%1 on").arg(reactive ? "reactive" : "polling"), 2); } else { set_status("start failed: " + err, "#e06c6c"); emit toast("start failed: " + err, 1); } } void EventsPage::on_stop() { // stop dispatch worker + remove kernel callbacks first, THEN unload the // module. otherwise callbacks keep firing after the module is gone. drv->notify_stop(); if (tracer_id >= 0) drv->unload((quint32)tracer_id); tracer_id = -1; running = false; set_status("stopped", "#8b8b8b"); loadBtn->setEnabled(true); startBtn->setEnabled(false); reactiveBtn->setEnabled(false); stopBtn->setEnabled(false); emit toast("callbacks removed, tracer unloaded", 0); } // LogPage LogPage::LogPage(QWidget* p) : QWidget(p) { auto* root = new QVBoxLayout(this); root->setContentsMargins(10, 10, 10, 8); root->setSpacing(14); auto* bar = new QHBoxLayout; auto* clearBtn = new QPushButton("Clear"); autoscroll = new QCheckBox("Auto-scroll"); autoscroll->setChecked(true); statusLbl = new QLabel(""); statusLbl->setObjectName("dim"); bar->addWidget(clearBtn); bar->addWidget(autoscroll); bar->addStretch(); bar->addWidget(statusLbl); root->addLayout(bar); view = new QPlainTextEdit; view->setReadOnly(true); view->setFont(QFont("JetBrainsMono NF", 10)); view->setObjectName("logView"); view->setMaximumBlockCount(4000); root->addWidget(view, 1); connect(clearBtn, &QPushButton::clicked, this, [this]{ view->clear(); }); } void LogPage::append(const QVector& entries, quint64 seq, quint32 dropped) { for (const auto& e : entries) { QString col = evt_col(e.kind).name(); QString html = QString("%1 %3") .arg(e.ts, col, e.line.toHtmlEscaped()); view->appendHtml(html); } statusLbl->setText(QString("seq %1 | %2 dropped").arg(seq).arg(dropped)); if (autoscroll->isChecked()) { auto* sb = view->verticalScrollBar(); sb->setValue(sb->maximum()); } } void LogPage::clear() { view->clear(); } // ProfilerPage ProfilerPage::ProfilerPage(QWidget* p) : QWidget(p) { auto* root = new QVBoxLayout(this); root->setContentsMargins(10, 10, 10, 8); root->setSpacing(12); auto* row = new QHBoxLayout; auto* clr = new QPushButton("Reset counts"); total = new QLabel("no data yet"); total->setStyleSheet("color:#7b828e; font-size:12px;"); row->addWidget(clr); row->addWidget(total, 1); root->addLayout(row); table = new QTableWidget(0, 5); table->setObjectName("dataTable"); table->setHorizontalHeaderLabels({"Function", "Count", "Total", "Max", "Avg"}); table->horizontalHeader()->setStretchLastSection(false); table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); table->setColumnWidth(1, 100); table->setColumnWidth(2, 140); table->setColumnWidth(3, 140); table->setColumnWidth(4, 140); table->verticalHeader()->setVisible(false); table->setEditTriggers(QAbstractItemView::NoEditTriggers); table->setShowGrid(false); table->setAlternatingRowColors(true); table->setSortingEnabled(true); root->addWidget(table, 1); connect(clr, &QPushButton::clicked, this, &ProfilerPage::clear); } static QString fmt_time_ns(quint64 ns) { if (ns < 1000) return QString::asprintf("%llu ns", (unsigned long long)ns); if (ns < 1000 * 1000) return QString::asprintf("%.2f us", ns / 1000.0); if (ns < 1000ULL*1000*1000) return QString::asprintf("%.2f ms", ns / 1e6); return QString::asprintf("%.2f s", ns / 1e9); } void ProfilerPage::observe(const QVector& events) { for (const auto& e : events) { auto& s = stats[e.name]; s.count++; if (e.kind == TRK_CALL) { s.last_call_ts = e.timestamp_100ns; } else if (e.kind == TRK_RETURN && s.last_call_ts && e.timestamp_100ns >= s.last_call_ts) { quint64 delta_ns = (e.timestamp_100ns - s.last_call_ts) * 100; // 100ns ticks -> ns s.total_ns += delta_ns; if (delta_ns > s.max_ns) s.max_ns = delta_ns; s.last_call_ts = 0; } } refresh(); } void ProfilerPage::refresh() { table->setSortingEnabled(false); table->setRowCount(stats.size()); QFont mono("JetBrainsMono NF", 10); int r = 0; quint64 grand = 0; for (auto it = stats.constBegin(); it != stats.constEnd(); ++it, r++) { const auto& s = it.value(); grand += s.count; auto* n = new QTableWidgetItem(it.key()); n->setFont(mono); table->setItem(r, 0, n); auto* c = new QTableWidgetItem; c->setData(Qt::DisplayRole, (qulonglong)s.count); c->setFont(mono); table->setItem(r, 1, c); auto* t = new QTableWidgetItem(s.total_ns ? fmt_time_ns(s.total_ns) : ""); t->setData(Qt::UserRole, (qulonglong)s.total_ns); t->setFont(mono); t->setForeground(QColor("#ffffff")); table->setItem(r, 2, t); auto* mx = new QTableWidgetItem(s.max_ns ? fmt_time_ns(s.max_ns) : ""); mx->setFont(mono); table->setItem(r, 3, mx); quint64 avg = s.count ? s.total_ns / s.count : 0; auto* av = new QTableWidgetItem(avg ? fmt_time_ns(avg) : ""); av->setFont(mono); av->setForeground(QColor("#8a919e")); table->setItem(r, 4, av); } table->setSortingEnabled(true); table->sortItems(1, Qt::DescendingOrder); total->setText(QString("%1 unique functions | %2 events total").arg(stats.size()).arg(grand)); } void ProfilerPage::clear() { stats.clear(); table->setRowCount(0); total->setText("no data yet"); } // DeployPage DeployPage::DeployPage(DriverClient* d, QWidget* p) : QWidget(p), drv(d) { auto* root = new QVBoxLayout(this); root->setContentsMargins(10, 10, 10, 8); root->setSpacing(14); auto* stateCard = card(); auto* sl = new QVBoxLayout(stateCard); sl->setContentsMargins(20, 16, 20, 16); sl->setSpacing(10); sl->addWidget(section_label("Status")); auto* g = new QGridLayout; g->setHorizontalSpacing(30); g->setVerticalSpacing(10); auto mklbl = [](const QString& k){ auto* l = new QLabel(k); l->setStyleSheet("color:#8a8a8a;"); return l; }; g->addWidget(mklbl("service"), 0, 0); svcState = new QLabel("-"); g->addWidget(svcState, 0, 1); g->addWidget(mklbl("state"), 1, 0); runState = new QLabel("-"); g->addWidget(runState, 1, 1); g->addWidget(mklbl("driver"), 2, 0); devState = new QLabel("-"); g->addWidget(devState, 2, 1); sl->addLayout(g); root->addWidget(stateCard); auto* actCard = card(); auto* al = new QVBoxLayout(actCard); al->setContentsMargins(20, 16, 20, 16); al->setSpacing(10); al->addWidget(section_label("Actions")); auto* btns = new QHBoxLayout; auto* install = new QPushButton("Install"); auto* start = new QPushButton("Start"); start->setObjectName("btnPrimary"); auto* stop = new QPushButton("Stop"); auto* remove = new QPushButton("Remove"); remove->setObjectName("btnDanger"); auto* refresh = new QPushButton("Refresh"); btns->addWidget(install); btns->addWidget(start); btns->addWidget(stop); btns->addWidget(remove); btns->addStretch(); btns->addWidget(refresh); al->addLayout(btns); root->addWidget(actCard); root->addStretch(); connect(install, &QPushButton::clicked, this, [this]{ wchar_t path[MAX_PATH]; GetModuleFileNameW(nullptr, path, MAX_PATH); QString exe = QString::fromWCharArray(path); QString dir = QFileInfo(exe).absolutePath(); drv->run_sc(QString("create Goodmans type= kernel binPath= \"%1\\Goodmans.sys\"").arg(dir)); emit stateChanged(); }); connect(start, &QPushButton::clicked, this, [this]{ drv->run_sc("start Goodmans"); emit stateChanged(); }); connect(stop, &QPushButton::clicked, this, [this]{ drv->run_sc("stop Goodmans"); emit stateChanged(); }); connect(remove, &QPushButton::clicked, this, [this]{ drv->run_sc("stop Goodmans"); drv->run_sc("delete Goodmans"); emit stateChanged(); }); connect(refresh, &QPushButton::clicked, this, [this]{ emit stateChanged(); }); } void DeployPage::refresh(bool devUp, bool present, bool running) { auto set = [](QLabel* l, const QString& t, const QString& color){ l->setText(t); l->setStyleSheet(QString("color:%1; font-weight:600;").arg(color)); }; set(svcState, present ? "installed" : "not installed", present ? "#8fbf5c" : "#cd5c5c"); set(runState, running ? "running" : "stopped", running ? "#8fbf5c" : "#e5b055"); set(devState, devUp ? "loaded" : "not loaded", devUp ? "#8fbf5c" : "#cd5c5c"); } // TracePage static const char* trace_kind_str(quint32 k) { switch (k) { case TRK_IMPORT: return "IMPORT"; case TRK_CALL: return "CALL"; case TRK_RETURN: return "RETURN"; case TRK_TRAP: return "TRAP"; default: return "?"; } } static QColor trace_kind_col(quint32 k) { switch (k) { case TRK_IMPORT: return QColor("#7cb1f0"); case TRK_CALL: return QColor("#d4a259"); case TRK_RETURN: return QColor("#8fbf5c"); case TRK_TRAP: return QColor("#cd5c5c"); default: return QColor("#8a8a8a"); } } // argument name hints per host import so details pane can label them properly. // values are decoded per column type: 'A' = kernel address hex, 'X' = hex u64, // 'x' = hex u32, 'd' = decimal u32. struct ArgSpec { const char* fn; const char* names[4]; const char types[4]; }; static const ArgSpec ARG_SPECS[] = { { "host_read_u8", {"kaddr", "", "", ""}, {'A','\0','\0','\0'} }, { "host_read_u32", {"kaddr", "", "", ""}, {'A','\0','\0','\0'} }, { "host_read_u64", {"kaddr", "", "", ""}, {'A','\0','\0','\0'} }, { "host_write_u64", {"kaddr", "value", "", ""}, {'A','X','\0','\0'} }, { "host_read_bytes", {"kaddr", "guest_off", "len", ""}, {'A','x','d','\0'} }, { "host_write_bytes", {"kaddr", "guest_off", "len", ""}, {'A','x','d','\0'} }, { "host_alloc", {"size", "", "", ""}, {'d','\0','\0','\0'} }, { "host_free", {"kernel_va", "", "", ""}, {'A','\0','\0','\0'} }, { "host_dbg_print", {"guest_off", "len", "", ""}, {'x','d','\0','\0'} }, { "host_readmsr", {"msr", "", "", ""}, {'x','\0','\0','\0'} }, { "host_writemsr", {"msr", "value", "", ""}, {'x','X','\0','\0'} }, { "host_cpuid", {"leaf", "sub", "guest_off",""}, {'x','x','x','\0'} }, { "host_phys_read", {"pa", "guest_off","len", ""}, {'X','x','d','\0'} }, { "host_phys_write", {"pa", "guest_off","len", ""}, {'X','x','d','\0'} }, { "host_rdtsc", {"","","",""}, {'\0','\0','\0','\0'}}, { "host_current_irql", {"","","",""}, {'\0','\0','\0','\0'}}, { "host_process_id", {"","","",""}, {'\0','\0','\0','\0'}}, { "host_thread_id", {"","","",""}, {'\0','\0','\0','\0'}}, { "host_current_process", {"","","",""}, {'\0','\0','\0','\0'}}, { nullptr, {}, {} } }; static const ArgSpec* find_argspec(const QString& fn) { for (int i = 0; ARG_SPECS[i].fn; i++) if (fn == ARG_SPECS[i].fn) return &ARG_SPECS[i]; return nullptr; } static QString fmt_arg_val(char type, quint64 v) { switch (type) { case 'A': // kernel address split for readability return QString::asprintf("0x%08x`%08x", (quint32)(v >> 32), (quint32)v); case 'X': return QString::asprintf("0x%016llx", (unsigned long long)v); case 'x': return QString::asprintf("0x%x", (quint32)v); case 'd': return QString::number((quint32)v); default: return QString::asprintf("0x%llx", (unsigned long long)v); } } static QString short_args_display(const TraceEvent& e) { const ArgSpec* spec = find_argspec(e.name); if (!spec) { QString s; for (int i = 0; i < e.argv.size(); i++) { if (i) s += ", "; s += QString::asprintf("0x%llx", (unsigned long long)e.argv[i]); } return s; } QString s; for (int i = 0; i < e.argv.size() && spec->types[i]; i++) { if (i) s += ", "; s += fmt_arg_val(spec->types[i], e.argv[i]); } return s; } TracePage::TracePage(DriverClient* d, QWidget* p) : QWidget(p), drv(d) { auto* root = new QVBoxLayout(this); root->setContentsMargins(10, 10, 10, 8); root->setSpacing(12); // toolbar auto* toolbar = new QFrame; toolbar->setObjectName("toolbar"); auto* tb = new QHBoxLayout(toolbar); tb->setContentsMargins(14, 8, 14, 8); tb->setSpacing(8); toggleBtn = new QPushButton("Start"); toggleBtn->setObjectName("btnPrimary"); toggleBtn->setMinimumWidth(80); toggleBtn->setShortcut(QKeySequence("F5")); toggleBtn->setToolTip("Toggle tracing (F5)"); tb->addWidget(toggleBtn); pauseBtn = new QPushButton("Pause"); pauseBtn->setEnabled(false); pauseBtn->setShortcut(QKeySequence("F6")); pauseBtn->setToolTip("Pause UI capture (F6)"); tb->addWidget(pauseBtn); clearBtn = new QPushButton("Clear"); clearBtn->setShortcut(QKeySequence("Ctrl+L")); clearBtn->setToolTip("Clear view (Ctrl+L)"); tb->addWidget(clearBtn); openBtn = new QPushButton("Open…"); openBtn->setToolTip("Load .gtrace file"); tb->addWidget(openBtn); exportBtn = new QPushButton("Save…"); exportBtn->setShortcut(QKeySequence("Ctrl+S")); exportBtn->setToolTip("Save trace to .gtrace or .csv (Ctrl+S)"); tb->addWidget(exportBtn); auto* sep1 = new QFrame; sep1->setFrameShape(QFrame::VLine); sep1->setObjectName("tbsep"); sep1->setFixedHeight(24); tb->addWidget(sep1); onlyModule = new QCheckBox("only"); onlyModule->setToolTip("Limit tracing to a specific module id"); moduleFilter = new QSpinBox; moduleFilter->setRange(0, 999); moduleFilter->setValue(0); moduleFilter->setFixedWidth(60); tb->addWidget(onlyModule); tb->addWidget(moduleFilter); regexMode = new QCheckBox(".*"); regexMode->setToolTip("Treat filter as regex"); tb->addWidget(regexMode); auto* sep2 = new QFrame; sep2->setFrameShape(QFrame::VLine); sep2->setObjectName("tbsep"); sep2->setFixedHeight(24); tb->addWidget(sep2); filter = new QLineEdit; filter->setPlaceholderText("filter (Ctrl+F): substring, module id, kind, or regex"); filter->setClearButtonEnabled(true); tb->addWidget(filter, 1); root->addWidget(toolbar); // splitter: table above, details below auto* split = new QSplitter(Qt::Vertical); split->setObjectName("wbSplit"); split->setChildrenCollapsible(false); split->setHandleWidth(4); table = new QTableWidget(0, 9); table->setObjectName("dataTable"); table->setHorizontalHeaderLabels({"", "Time", "Δt", "TID", "IRQL", "Mod", "Kind", "Function", "Arguments"}); table->horizontalHeader()->setStretchLastSection(true); table->setColumnWidth(0, 14); table->setColumnWidth(1, 110); table->setColumnWidth(2, 80); table->setColumnWidth(3, 60); table->setColumnWidth(4, 50); table->setColumnWidth(5, 46); table->setColumnWidth(6, 78); table->setColumnWidth(7, 200); table->verticalHeader()->setVisible(false); table->verticalHeader()->setDefaultSectionSize(24); table->setEditTriggers(QAbstractItemView::NoEditTriggers); table->setShowGrid(false); table->setAlternatingRowColors(true); table->setSelectionBehavior(QAbstractItemView::SelectRows); table->setSelectionMode(QAbstractItemView::SingleSelection); split->addWidget(table); details = new QPlainTextEdit; details->setReadOnly(true); details->setObjectName("detailsPane"); details->setPlaceholderText("Select an event to see its full details."); details->setMinimumHeight(120); split->addWidget(details); split->setStretchFactor(0, 70); split->setStretchFactor(1, 30); split->setSizes({520, 200}); root->addWidget(split, 1); // status bar at bottom auto* statusRow = new QHBoxLayout; statusRow->setContentsMargins(4, 0, 4, 0); modeLbl = new QLabel; modeLbl->setObjectName("dim"); statsLbl = new QLabel; statsLbl->setObjectName("dim"); statusRow->addWidget(modeLbl); statusRow->addStretch(); statusRow->addWidget(statsLbl); root->addLayout(statusRow); refresh_stats(); connect(toggleBtn, &QPushButton::clicked, this, &TracePage::toggleTracing); connect(pauseBtn, &QPushButton::clicked, this, [this]{ paused = !paused; pauseBtn->setText(paused ? "Resume" : "Pause"); }); connect(clearBtn, &QPushButton::clicked, this, &TracePage::clear); connect(openBtn, &QPushButton::clicked, this, [this]{ QString path = QFileDialog::getOpenFileName(this, "Load trace", QString(), "Goodmans trace (*.gtrace);;All (*.*)"); if (!path.isEmpty()) loadTraceFile(path); }); connect(exportBtn, &QPushButton::clicked, this, [this]{ QString path = QFileDialog::getSaveFileName(this, "Save trace", "trace.gtrace", "Goodmans trace (*.gtrace);;CSV (*.csv)"); if (!path.isEmpty()) saveTraceFile(path); }); connect(filter, &QLineEdit::textChanged, this, [this]{ rebuild_from_backing(); }); connect(regexMode, &QCheckBox::toggled, this, [this]{ rebuild_from_backing(); }); connect(table, &QTableWidget::itemSelectionChanged, this, [this]{ auto sel = table->selectedItems(); if (sel.isEmpty()) { details->clear(); return; } int row = sel.first()->row(); int back_idx = table->item(row, 0)->data(Qt::UserRole).toInt(); if (back_idx >= 0 && back_idx < backing.size()) render_details(backing[back_idx]); }); auto* bmSc = new QShortcut(QKeySequence("Ctrl+B"), this); connect(bmSc, &QShortcut::activated, this, &TracePage::toggleBookmark); } void TracePage::toggleTracing() { if (!tracing_on) { int mode = onlyModule->isChecked() ? TRACE_ON_MODULE : TRACE_ON_ALL; quint32 mid = onlyModule->isChecked() ? (quint32)moduleFilter->value() : 0; if (drv->trace_ctl(mode, mid)) { tracing_on = true; toggleBtn->setText("Stop"); pauseBtn->setEnabled(true); modeLbl->setText(onlyModule->isChecked() ? QString("tracing module %1").arg(mid) : "tracing all modules"); modeLbl->setStyleSheet("color: #8fbf5c;"); emit toast("tracing on", 2); } else emit toast("failed to enable trace", 1); } else { if (drv->trace_ctl(TRACE_OFF)) { tracing_on = false; toggleBtn->setText("Start"); pauseBtn->setEnabled(false); paused = false; pauseBtn->setText("Pause"); modeLbl->setText("tracing off"); modeLbl->setStyleSheet("color: #6a7280;"); emit toast("tracing off", 0); } } } void TracePage::toggleBookmark() { auto sel = table->selectedItems(); if (sel.isEmpty()) return; int row = sel.first()->row(); int back_idx = table->item(row, 0)->data(Qt::UserRole).toInt(); if (bookmarks.contains(back_idx)) { bookmarks.remove(back_idx); table->item(row, 0)->setText(""); } else { bookmarks.insert(back_idx); table->item(row, 0)->setText("●"); } } bool TracePage::filter_match(const TraceEvent& e) const { QString f = filter->text(); if (f.isEmpty()) return true; if (regexMode->isChecked()) { QRegularExpression re(f, QRegularExpression::CaseInsensitiveOption); if (!re.isValid()) return true; // don't hide during typing return re.match(e.name).hasMatch() || re.match(QString::number(e.module_id)).hasMatch() || re.match(trace_kind_str(e.kind)).hasMatch(); } return e.name.contains(f, Qt::CaseInsensitive) || QString::number(e.module_id) == f || QString(trace_kind_str(e.kind)).contains(f, Qt::CaseInsensitive); } void TracePage::insert_row(int row, const TraceEvent& e, int back_idx) { QFont mono("JetBrainsMono NF", 10); table->insertRow(row); auto* bm = new QTableWidgetItem(bookmarks.contains(back_idx) ? "●" : ""); bm->setForeground(QColor("#ffffff")); bm->setTextAlignment(Qt::AlignCenter); bm->setData(Qt::UserRole, back_idx); table->setItem(row, 0, bm); auto* ts = new QTableWidgetItem(e.ts); ts->setFont(mono); ts->setForeground(QColor("#8a919e")); table->setItem(row, 1, ts); // delta-time: microseconds since first event quint64 delta_100ns = (first_ts && e.timestamp_100ns >= first_ts) ? e.timestamp_100ns - first_ts : 0; double delta_ms = delta_100ns / 10000.0; QString dt = delta_ms < 1.0 ? QString::asprintf("+%.1f us", delta_100ns / 10.0) : (delta_ms < 1000.0 ? QString::asprintf("+%.2f ms", delta_ms) : QString::asprintf("+%.3f s", delta_ms / 1000.0)); auto* dtc = new QTableWidgetItem(dt); dtc->setFont(mono); dtc->setForeground(QColor("#7b828e")); table->setItem(row, 2, dtc); auto* tid = new QTableWidgetItem(QString::number(e.thread_id)); tid->setFont(mono); tid->setForeground(QColor("#8a919e")); table->setItem(row, 3, tid); QString irqlStr; switch (e.irql) { case 0: irqlStr = "PASSIVE"; break; case 1: irqlStr = "APC"; break; case 2: irqlStr = "DISPATCH"; break; default: irqlStr = QString::number(e.irql); } auto* iq = new QTableWidgetItem(irqlStr); iq->setFont(mono); iq->setForeground(e.irql >= 2 ? QColor("#e5b055") : QColor("#8a919e")); table->setItem(row, 4, iq); auto* mid = new QTableWidgetItem(QString::number(e.module_id)); mid->setFont(mono); table->setItem(row, 5, mid); auto* kd = new QTableWidgetItem(trace_kind_str(e.kind)); kd->setFont(mono); kd->setForeground(trace_kind_col(e.kind)); table->setItem(row, 6, kd); auto* nm = new QTableWidgetItem(e.name); nm->setFont(mono); table->setItem(row, 7, nm); QString argstr = short_args_display(e); if (e.kind != TRK_TRAP && e.kind != TRK_CALL) argstr += QString::asprintf(" 0x%llx", (unsigned long long)e.rv); auto* ar = new QTableWidgetItem(argstr); ar->setFont(mono); ar->setForeground(e.kind == TRK_TRAP ? QColor("#cd5c5c") : QColor("#b0b6c0")); table->setItem(row, 8, ar); } void TracePage::rebuild_from_backing() { table->setRowCount(0); shown_events = 0; // insert newest first (like live append) for (int i = backing.size() - 1; i >= 0; i--) { if (!filter_match(backing[i])) continue; insert_row(table->rowCount(), backing[i], i); shown_events++; } refresh_stats(); } void TracePage::render_details(const TraceEvent& e) { QString out; QString irqlName = e.irql == 0 ? "PASSIVE_LEVEL" : (e.irql == 1 ? "APC_LEVEL" : (e.irql == 2 ? "DISPATCH_LEVEL" : QString("IRQL %1").arg(e.irql))); out += QString("
");
    out += QString("%2  at %3 | module %4 | TID %5 | %6\n")
        .arg(trace_kind_col(e.kind).name(), trace_kind_str(e.kind), e.ts).arg(e.module_id).arg(e.thread_id).arg(irqlName);
    out += QString("function   %1\n").arg(e.name.toHtmlEscaped());

    const ArgSpec* spec = find_argspec(e.name);
    if (!e.argv.isEmpty()) {
        out += "arguments\n";
        for (int i = 0; i < e.argv.size(); i++) {
            QString name = (spec && spec->names[i][0]) ? spec->names[i] : QString("arg%1").arg(i);
            char type = (spec && spec->types[i]) ? spec->types[i] : 'X';
            out += QString("  %1  %2\n")
                .arg(name.leftJustified(12, ' ')).arg(fmt_arg_val(type, e.argv[i]));
        }
    }
    if (e.kind == TRK_TRAP) {
        out += QString("trap reason  %1\n").arg(e.name.toHtmlEscaped());
    } else if (e.kind != TRK_CALL) {
        out += QString("return     0x%1  (%2)\n")
            .arg((unsigned long long)e.rv, 0, 16).arg((unsigned long long)e.rv);
    }
    out += "
"; details->setPlainText(""); details->appendHtml(out); } void TracePage::refresh_stats() { QString s; s = QString("events: %1 total | %2 shown").arg(total_events).arg(shown_events); if (!kind_counts.isEmpty()) { s += " | "; QStringList parts; for (auto it = kind_counts.constBegin(); it != kind_counts.constEnd(); ++it) parts << QString("%1=%2").arg(it.key()).arg(it.value()); s += parts.join(" "); } statsLbl->setText(s); } void TracePage::append(const QVector& events) { if (paused) return; for (const auto& e : events) { if (first_ts == 0) first_ts = e.timestamp_100ns; int back_idx = backing.size(); backing.push_back(e); total_events++; kind_counts[trace_kind_str(e.kind)]++; if (!filter_match(e)) continue; insert_row(0, e, back_idx); shown_events++; if (table->rowCount() > 5000) table->removeRow(5000); } while (backing.size() > 20000) { backing.pop_front(); bookmarks.clear(); // indices invalidated } refresh_stats(); } void TracePage::saveTraceFile(const QString& path) { QFile f(path); if (!f.open(QIODevice::WriteOnly)) { emit toast("cannot open file", 1); return; } if (path.endsWith(".csv", Qt::CaseInsensitive)) { QTextStream out(&f); out << "time,delta_us,tid,irql,module,kind,function,args,result\n"; for (const auto& e : backing) { quint64 dt = (first_ts && e.timestamp_100ns >= first_ts) ? (e.timestamp_100ns - first_ts) / 10 : 0; out << e.ts << "," << dt << "," << e.thread_id << "," << e.irql << "," << e.module_id << "," << trace_kind_str(e.kind) << "," << e.name << ",\"" << short_args_display(e) << "\"," << QString::asprintf("0x%llx", (unsigned long long)e.rv) << "\n"; } } else { // .gtrace binary: [magic 'GTR1'][count u32][first_ts u64][entries...] QDataStream ds(&f); ds.setByteOrder(QDataStream::LittleEndian); ds << (quint32)0x31525447u << (quint32)backing.size() << (quint64)first_ts; for (const auto& e : backing) { ds << e.timestamp_100ns << e.module_id << e.kind << e.thread_id << e.irql << (quint32)e.argv.size() << e.rv; ds << e.name; for (int i = 0; i < 4; i++) ds << (quint64)(i < e.argv.size() ? e.argv[i] : 0); } } f.close(); emit toast(QString("saved %1 events -> %2").arg(backing.size()).arg(QFileInfo(path).fileName()), 2); } void TracePage::loadTraceFile(const QString& path) { QFile f(path); if (!f.open(QIODevice::ReadOnly)) { emit toast("cannot open file", 1); return; } QDataStream ds(&f); ds.setByteOrder(QDataStream::LittleEndian); quint32 magic = 0, count = 0; quint64 fts = 0; ds >> magic >> count >> fts; if (magic != 0x31525447u) { emit toast("not a .gtrace file", 1); return; } clear(); first_ts = fts; QVector loaded; for (quint32 i = 0; i < count; i++) { TraceEvent e; quint32 argc; ds >> e.timestamp_100ns >> e.module_id >> e.kind >> e.thread_id >> e.irql >> argc >> e.rv; ds >> e.name; for (int a = 0; a < 4; a++) { quint64 v; ds >> v; if ((quint32)a < argc) e.argv.push_back(v); } SYSTEMTIME sys; FILETIME ft, lft; ft.dwLowDateTime = (DWORD) e.timestamp_100ns; ft.dwHighDateTime = (DWORD)(e.timestamp_100ns >> 32); FileTimeToLocalFileTime(&ft, &lft); FileTimeToSystemTime(&lft, &sys); e.ts = QString::asprintf("%02u:%02u:%02u.%03u", sys.wHour, sys.wMinute, sys.wSecond, sys.wMilliseconds); loaded.push_back(e); } f.close(); append(loaded); emit toast(QString("loaded %1 events from %2").arg(count).arg(QFileInfo(path).fileName()), 2); } void TracePage::clear() { table->setRowCount(0); backing.clear(); kind_counts.clear(); total_events = 0; shown_events = 0; details->clear(); refresh_stats(); } // ExplorerPage ExplorerPage::ExplorerPage(DriverClient* d, QWidget* p) : QWidget(p), drv(d) { auto* root = new QVBoxLayout(this); root->setContentsMargins(10, 10, 10, 8); root->setSpacing(12); auto* topbar = new QFrame; topbar->setObjectName("toolbar"); auto* tb = new QHBoxLayout(topbar); tb->setContentsMargins(14, 10, 14, 10); tb->setSpacing(10); auto* explain = new QLabel("Explorer uses a helper guest (toolkit.wasm) to read kernel state."); explain->setStyleSheet("color:#999999; font-size:12px;"); tb->addWidget(explain); tb->addStretch(); toolkitLbl = new QLabel("toolkit: not loaded"); toolkitLbl->setStyleSheet("color:#cd5c5c; font-weight:600;"); tb->addWidget(toolkitLbl); loadToolkitBtn = new QPushButton("Load toolkit"); loadToolkitBtn->setObjectName("btnPrimary"); loadToolkitBtn->setToolTip("Loads toolkit.wasm from the app directory (or pick another)."); tb->addWidget(loadToolkitBtn); root->addWidget(topbar); auto* tabs = new QTabWidget; tabs->setObjectName("wbTabs"); // processes tab { auto* w = new QWidget; auto* l = new QVBoxLayout(w); l->setContentsMargins(0, 8, 0, 0); auto* row = new QHBoxLayout; auto* refreshBtn = new QPushButton("Refresh"); refreshBtn->setObjectName("btnPrimary"); row->addWidget(refreshBtn); row->addStretch(); l->addLayout(row); procTable = new QTableWidget(0, 4); procTable->setObjectName("dataTable"); procTable->setHorizontalHeaderLabels({"PID", "Name", "EPROCESS", "Threads"}); procTable->horizontalHeader()->setStretchLastSection(false); procTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); procTable->setColumnWidth(0, 80); procTable->setColumnWidth(2, 200); procTable->setColumnWidth(3, 80); procTable->verticalHeader()->setVisible(false); procTable->setEditTriggers(QAbstractItemView::NoEditTriggers); procTable->setShowGrid(false); procTable->setAlternatingRowColors(true); l->addWidget(procTable, 1); connect(refreshBtn, &QPushButton::clicked, this, &ExplorerPage::refresh_procs); tabs->addTab(w, "Processes"); } // memory tab { auto* w = new QWidget; auto* l = new QVBoxLayout(w); l->setContentsMargins(0, 8, 0, 0); auto* row = new QHBoxLayout; row->addWidget(new QLabel("Address:")); memAddr = new QLineEdit("0xfffff800`00000000"); memAddr->setFont(QFont("JetBrainsMono NF", 10)); memAddr->setFixedWidth(280); row->addWidget(memAddr); row->addWidget(new QLabel("Bytes:")); memLen = new QSpinBox; memLen->setRange(16, 4096); memLen->setValue(256); memLen->setFixedWidth(80); row->addWidget(memLen); auto* readBtn = new QPushButton("Read"); readBtn->setObjectName("btnPrimary"); row->addWidget(readBtn); row->addStretch(); l->addLayout(row); memHex = new QPlainTextEdit; memHex->setReadOnly(true); memHex->setFont(QFont("JetBrainsMono NF", 10)); memHex->setObjectName("hexView"); l->addWidget(memHex, 1); connect(readBtn, &QPushButton::clicked, this, &ExplorerPage::read_memory); tabs->addTab(w, "Memory"); } // MSR tab { auto* w = new QWidget; auto* l = new QVBoxLayout(w); l->setContentsMargins(0, 8, 0, 0); auto* row = new QHBoxLayout; auto* readBtn = new QPushButton("Read presets"); readBtn->setObjectName("btnPrimary"); row->addWidget(readBtn); msrCustom = new QLineEdit; msrCustom->setPlaceholderText("custom MSR index (hex or dec)"); msrCustom->setFont(QFont("JetBrainsMono NF", 10)); row->addWidget(msrCustom); auto* customBtn = new QPushButton("Read"); row->addWidget(customBtn); row->addStretch(); l->addLayout(row); msrTable = new QTableWidget(0, 3); msrTable->setObjectName("dataTable"); msrTable->setHorizontalHeaderLabels({"MSR", "Name", "Value"}); msrTable->horizontalHeader()->setStretchLastSection(false); msrTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); msrTable->setColumnWidth(0, 100); msrTable->setColumnWidth(2, 220); msrTable->verticalHeader()->setVisible(false); msrTable->setEditTriggers(QAbstractItemView::NoEditTriggers); msrTable->setShowGrid(false); msrTable->setAlternatingRowColors(true); l->addWidget(msrTable, 1); connect(readBtn, &QPushButton::clicked, this, &ExplorerPage::read_msrs); connect(customBtn, &QPushButton::clicked, this, [this]{ if (toolkit_id < 0) { emit toast("load toolkit first", 1); return; } QString s = msrCustom->text().trimmed(); if (s.isEmpty()) return; bool ok = false; quint32 idx = s.startsWith("0x", Qt::CaseInsensitive) ? s.mid(2).toUInt(&ok, 16) : s.toUInt(&ok, 0); if (!ok) { emit toast("bad MSR", 1); return; } QVector argv = { idx }; quint64 rv = 0; QString err; if (drv->call_export(toolkit_id, "toolkit_readmsr", 2000, argv, rv, err)) { int row = msrTable->rowCount(); msrTable->insertRow(row); msrTable->setItem(row, 0, new QTableWidgetItem(QString::asprintf("0x%x", idx))); msrTable->setItem(row, 1, new QTableWidgetItem("(custom)")); msrTable->setItem(row, 2, new QTableWidgetItem(QString::asprintf("0x%016llx", (unsigned long long)rv))); for (int c = 0; c < 3; c++) msrTable->item(row, c)->setFont(QFont("JetBrainsMono NF", 10)); } else emit toast(err, 1); }); tabs->addTab(w, "MSRs"); } // CPUID tab { auto* w = new QWidget; auto* l = new QVBoxLayout(w); l->setContentsMargins(0, 8, 0, 0); auto* row = new QHBoxLayout; row->addWidget(new QLabel("Max leaf:")); cpuidMax = new QSpinBox; cpuidMax->setRange(0, 0x40); cpuidMax->setValue(0x18); cpuidMax->setFixedWidth(80); row->addWidget(cpuidMax); auto* dumpBtn = new QPushButton("Dump"); dumpBtn->setObjectName("btnPrimary"); row->addWidget(dumpBtn); row->addStretch(); l->addLayout(row); cpuidTable = new QTableWidget(0, 6); cpuidTable->setObjectName("dataTable"); cpuidTable->setHorizontalHeaderLabels({"Leaf", "Sub", "EAX", "EBX", "ECX", "EDX"}); cpuidTable->horizontalHeader()->setStretchLastSection(true); cpuidTable->setColumnWidth(0, 70); cpuidTable->setColumnWidth(1, 50); for (int i = 2; i <= 5; i++) cpuidTable->setColumnWidth(i, 110); cpuidTable->verticalHeader()->setVisible(false); cpuidTable->setEditTriggers(QAbstractItemView::NoEditTriggers); cpuidTable->setShowGrid(false); cpuidTable->setAlternatingRowColors(true); l->addWidget(cpuidTable, 1); connect(dumpBtn, &QPushButton::clicked, this, &ExplorerPage::read_cpuid); tabs->addTab(w, "CPUID"); } root->addWidget(tabs, 1); connect(loadToolkitBtn, &QPushButton::clicked, this, [this]{ QString here = QCoreApplication::applicationDirPath(); QString def = here + "/toolkit.wasm"; QString path; if (QFile::exists(def)) { // one-click if the shipped toolkit is next to the exe path = def; } else { path = QFileDialog::getOpenFileName(this, "Select toolkit.wasm", QString(), "WebAssembly (*.wasm)"); if (path.isEmpty()) return; } quint32 id = 0; QString err; if (drv->load_module(path, 0, id, err)) { setToolkitId((int)id); emit toast(QString("toolkit loaded as module %1").arg(id), 2); } else emit toast("load failed: " + err, 1); }); } void ExplorerPage::setToolkitId(int id) { toolkit_id = id; if (id < 0) { toolkitLbl->setText("toolkit: not loaded"); toolkitLbl->setStyleSheet("color:#cd5c5c; font-weight:600;"); } else { toolkitLbl->setText(QString("toolkit: module %1").arg(id)); toolkitLbl->setStyleSheet("color:#8fbf5c; font-weight:600;"); } } void ExplorerPage::refresh_procs() { if (toolkit_id < 0) { emit toast("load toolkit first", 1); return; } // 1. get scratch offset QVector a0; quint64 scratch = 0; QString err; if (!drv->call_export(toolkit_id, "toolkit_scratch_off", 2000, a0, scratch, err)) { emit toast("toolkit_scratch_off: " + err, 1); return; } // 2. enumerate procs into scratch QVector a1 = { scratch, 64 }; quint64 count = 0; if (!drv->call_export(toolkit_id, "toolkit_enum_procs", 5000, a1, count, err)) { emit toast("toolkit_enum_procs: " + err, 1); return; } // 3. read scratch back QByteArray data; if (!drv->read_guest((quint32)toolkit_id, (quint32)scratch, (quint32)(count * 32), data, err)) { emit toast("read_guest: " + err, 1); return; } procTable->setRowCount((int)count); QFont mono("JetBrainsMono NF", 10); for (int i = 0; i < (int)count; i++) { const char* p = data.constData() + i * 32; quint64 pid = *(const quint64*)(p + 0); quint64 eproc = *(const quint64*)(p + 8); char name[16] = {0}; memcpy(name, p + 16, 15); auto* c0 = new QTableWidgetItem(QString::number(pid)); c0->setFont(mono); auto* c1 = new QTableWidgetItem(QString::fromLatin1(name)); auto* c2 = new QTableWidgetItem(QString::asprintf("0x%016llx", (unsigned long long)eproc)); c2->setFont(mono); c2->setForeground(QColor("#8a8a8a")); auto* c3 = new QTableWidgetItem("-"); c3->setFont(mono); procTable->setItem(i, 0, c0); procTable->setItem(i, 1, c1); procTable->setItem(i, 2, c2); procTable->setItem(i, 3, c3); } } void ExplorerPage::read_memory() { if (toolkit_id < 0) { emit toast("load toolkit first", 1); return; } QString a = memAddr->text(); a.remove('`').remove(' '); bool ok = false; quint64 va = a.startsWith("0x", Qt::CaseInsensitive) ? a.mid(2).toULongLong(&ok, 16) : a.toULongLong(&ok, 0); if (!ok) { emit toast("bad address", 1); return; } QString err; quint64 scratch = 0; if (!drv->call_export(toolkit_id, "toolkit_scratch_off", 2000, {}, scratch, err)) { emit toast(err, 1); return; } quint32 len = (quint32)memLen->value(); QVector argv = { va, scratch, len }; quint64 bytes_read = 0; if (!drv->call_export(toolkit_id, "toolkit_read_kmem", 2000, argv, bytes_read, err)) { emit toast(err, 1); return; } if (bytes_read == 0) { memHex->setPlainText("(zero bytes read - probably invalid address)"); return; } QByteArray data; if (!drv->read_guest((quint32)toolkit_id, (quint32)scratch, (quint32)bytes_read, data, err)) { emit toast("read_guest: " + err, 1); return; } QString out; for (int off = 0; off < data.size(); off += 16) { out += QString::asprintf("%016llx ", (unsigned long long)(va + off)); QString hex, ascii; for (int i = 0; i < 16; i++) { if (off + i < data.size()) { unsigned char b = (unsigned char)data[off + i]; hex += QString::asprintf("%02x ", b); ascii += (b >= 0x20 && b < 0x7f) ? QChar(b) : '.'; } else { hex += " "; ascii += ' '; } if (i == 7) hex += " "; } out += hex + " " + ascii + "\n"; } memHex->setPlainText(out); } static const struct { quint32 idx; const char* name; } MSR_PRESETS[] = { { 0x001b, "IA32_APIC_BASE" }, { 0x003a, "IA32_FEATURE_CONTROL" }, { 0x00c0, "IA32_PMC0" }, { 0x00c1, "IA32_PMC1" }, { 0x0174, "IA32_SYSENTER_CS" }, { 0x0175, "IA32_SYSENTER_ESP" }, { 0x0176, "IA32_SYSENTER_EIP" }, { 0x01d9, "IA32_DEBUGCTL" }, { 0x0277, "IA32_PAT" }, { 0x02ff, "IA32_MTRR_DEF_TYPE" }, { 0xc0000080, "IA32_EFER" }, { 0xc0000081, "IA32_STAR" }, { 0xc0000082, "IA32_LSTAR" }, { 0xc0000083, "IA32_CSTAR" }, { 0xc0000084, "IA32_FMASK" }, { 0xc0000100, "IA32_FS_BASE" }, { 0xc0000101, "IA32_GS_BASE" }, { 0xc0000102, "IA32_KERNEL_GS_BASE" }, { 0xc0000103, "IA32_TSC_AUX" }, { 0, nullptr } }; void ExplorerPage::read_msrs() { if (toolkit_id < 0) { emit toast("load toolkit first", 1); return; } msrTable->setRowCount(0); QFont mono("JetBrainsMono NF", 10); for (int i = 0; MSR_PRESETS[i].name; i++) { QVector argv = { MSR_PRESETS[i].idx }; quint64 rv = 0; QString err; bool ok = drv->call_export(toolkit_id, "toolkit_readmsr", 2000, argv, rv, err); int row = msrTable->rowCount(); msrTable->insertRow(row); msrTable->setItem(row, 0, new QTableWidgetItem(QString::asprintf("0x%x", MSR_PRESETS[i].idx))); msrTable->setItem(row, 1, new QTableWidgetItem(MSR_PRESETS[i].name)); msrTable->setItem(row, 2, new QTableWidgetItem(ok ? QString::asprintf("0x%016llx", (unsigned long long)rv) : QString("err: " + err))); for (int c = 0; c < 3; c++) msrTable->item(row, c)->setFont(mono); if (!ok) msrTable->item(row, 2)->setForeground(QColor("#cd5c5c")); } } void ExplorerPage::read_cpuid() { if (toolkit_id < 0) { emit toast("load toolkit first", 1); return; } cpuidTable->setRowCount(0); QFont mono("JetBrainsMono NF", 10); QString err; quint64 scratch = 0; if (!drv->call_export(toolkit_id, "toolkit_scratch_off", 2000, {}, scratch, err)) { emit toast(err, 1); return; } int maxL = cpuidMax->value(); for (int leaf = 0; leaf <= maxL; leaf++) { QVector argv = { (quint32)leaf, 0, (quint32)scratch }; quint64 rv = 0; if (!drv->call_export(toolkit_id, "toolkit_cpuid", 2000, argv, rv, err)) continue; QByteArray data; if (!drv->read_guest((quint32)toolkit_id, (quint32)scratch, 16, data, err)) continue; quint32 eax = *(const quint32*)(data.constData() + 0); quint32 ebx = *(const quint32*)(data.constData() + 4); quint32 ecx = *(const quint32*)(data.constData() + 8); quint32 edx = *(const quint32*)(data.constData() + 12); int row = cpuidTable->rowCount(); cpuidTable->insertRow(row); cpuidTable->setItem(row, 0, new QTableWidgetItem(QString::asprintf("0x%02x", leaf))); cpuidTable->setItem(row, 1, new QTableWidgetItem("0")); cpuidTable->setItem(row, 2, new QTableWidgetItem(QString::asprintf("0x%08x", eax))); cpuidTable->setItem(row, 3, new QTableWidgetItem(QString::asprintf("0x%08x", ebx))); cpuidTable->setItem(row, 4, new QTableWidgetItem(QString::asprintf("0x%08x", ecx))); cpuidTable->setItem(row, 5, new QTableWidgetItem(QString::asprintf("0x%08x", edx))); for (int c = 0; c < 6; c++) cpuidTable->item(row, c)->setFont(mono); } }