diff --git a/src/Cutter.pro b/src/Cutter.pro
index 3dfb6425..1940f1f5 100644
--- a/src/Cutter.pro
+++ b/src/Cutter.pro
@@ -329,6 +329,7 @@ SOURCES += \
dialogs/AsyncTaskDialog.cpp \
widgets/StackWidget.cpp \
widgets/RegistersWidget.cpp \
+ widgets/ThreadsWidget.cpp \
widgets/BacktraceWidget.cpp \
dialogs/OpenFileDialog.cpp \
common/CommandTask.cpp \
@@ -459,6 +460,7 @@ HEADERS += \
dialogs/AsyncTaskDialog.h \
widgets/StackWidget.h \
widgets/RegistersWidget.h \
+ widgets/ThreadsWidget.h \
widgets/BacktraceWidget.h \
dialogs/OpenFileDialog.h \
common/StringsTask.h \
@@ -552,6 +554,7 @@ FORMS += \
dialogs/AsyncTaskDialog.ui \
widgets/StackWidget.ui \
widgets/RegistersWidget.ui \
+ widgets/ThreadsWidget.ui \
widgets/BacktraceWidget.ui \
dialogs/OpenFileDialog.ui \
dialogs/preferences/DebugOptionsWidget.ui \
diff --git a/src/core/Cutter.cpp b/src/core/Cutter.cpp
index 3a68c01c..3beb001d 100644
--- a/src/core/Cutter.cpp
+++ b/src/core/Cutter.cpp
@@ -1028,6 +1028,16 @@ QJsonDocument CutterCore::getStack(int size)
return cmdj("pxrj " + QString::number(size) + " @ r:SP");
}
+QJsonDocument CutterCore::getProcessThreads(int pid)
+{
+ if (-1 == pid) {
+ // Return threads list of the currently debugged PID
+ return cmdj("dptj");
+ } else {
+ return cmdj("dptj " + QString::number(pid));
+ }
+}
+
QJsonDocument CutterCore::getRegisterValues()
{
return cmdj("drj");
@@ -1118,6 +1128,15 @@ void CutterCore::setRegister(QString regName, QString regValue)
emit refreshCodeViews();
}
+void CutterCore::setCurrentDebugThread(int tid)
+{
+ cmd("dpt=" + QString::number(tid));
+ emit registersChanged();
+ emit refreshCodeViews();
+ emit stackChanged();
+ syncAndSeekProgramCounter();
+}
+
void CutterCore::startDebug()
{
if (!currentlyDebugging) {
diff --git a/src/core/Cutter.h b/src/core/Cutter.h
index 72c2766a..846631b2 100644
--- a/src/core/Cutter.h
+++ b/src/core/Cutter.h
@@ -221,7 +221,14 @@ public:
QString getRegisterName(QString registerRole);
RVA getProgramCounterValue();
void setRegister(QString regName, QString regValue);
+ void setCurrentDebugThread(int tid);
QJsonDocument getStack(int size = 0x100);
+ /**
+ * @brief Get a list of a given process's threads
+ * @param pid The pid of the process, -1 for the currently debugged process
+ * @return JSON object result of dptj
+ */
+ QJsonDocument getProcessThreads(int pid);
QJsonDocument getBacktrace();
void startDebug();
void startEmulation();
diff --git a/src/core/MainWindow.cpp b/src/core/MainWindow.cpp
index e7dfb4f1..3b7e6755 100644
--- a/src/core/MainWindow.cpp
+++ b/src/core/MainWindow.cpp
@@ -61,6 +61,7 @@
#include "widgets/RegisterRefsWidget.h"
#include "widgets/DisassemblyWidget.h"
#include "widgets/StackWidget.h"
+#include "widgets/ThreadsWidget.h"
#include "widgets/RegistersWidget.h"
#include "widgets/BacktraceWidget.h"
#include "widgets/HexdumpWidget.h"
@@ -315,6 +316,7 @@ void MainWindow::initDocks()
stringsDock = new StringsWidget(this, ui->actionStrings);
flagsDock = new FlagsWidget(this, ui->actionFlags);
stackDock = new StackWidget(this, ui->actionStack);
+ threadsDock = new ThreadsWidget(this, ui->actionThreads);
backtraceDock = new BacktraceWidget(this, ui->actionBacktrace);
registersDock = new RegistersWidget(this, ui->actionRegisters);
memoryMapDock = new MemoryMapWidget(this, ui->actionMemoryMap);
@@ -833,10 +835,11 @@ void MainWindow::restoreDocks()
tabifyDockWidget(sectionsDock, commentsDock);
- // Add Stack, Registers and Backtrace vertically stacked
+ // Add Stack, Registers, Threads and Backtrace vertically stacked
addDockWidget(Qt::TopDockWidgetArea, stackDock);
splitDockWidget(stackDock, registersDock, Qt::Vertical);
tabifyDockWidget(stackDock, backtraceDock);
+ tabifyDockWidget(backtraceDock, threadsDock);
updateDockActionsChecked();
}
@@ -1166,6 +1169,7 @@ void MainWindow::showDebugDocks()
stackDock,
registersDock,
backtraceDock,
+ threadsDock,
memoryMapDock,
breakpointDock
};
diff --git a/src/core/MainWindow.h b/src/core/MainWindow.h
index ab5a11bb..29111b3a 100644
--- a/src/core/MainWindow.h
+++ b/src/core/MainWindow.h
@@ -249,6 +249,7 @@ private:
QDockWidget *asmDock = nullptr;
QDockWidget *calcDock = nullptr;
QDockWidget *stackDock = nullptr;
+ QDockWidget *threadsDock = nullptr;
QDockWidget *registersDock = nullptr;
QDockWidget *backtraceDock = nullptr;
QDockWidget *memoryMapDock = nullptr;
diff --git a/src/core/MainWindow.ui b/src/core/MainWindow.ui
index f55ad433..bf48b6e1 100644
--- a/src/core/MainWindow.ui
+++ b/src/core/MainWindow.ui
@@ -152,6 +152,7 @@
+
@@ -932,6 +933,14 @@
Backtrace
+
+
+ true
+
+
+ Threads
+
+
true
diff --git a/src/widgets/BacktraceWidget.cpp b/src/widgets/BacktraceWidget.cpp
index c81069af..685e7649 100644
--- a/src/widgets/BacktraceWidget.cpp
+++ b/src/widgets/BacktraceWidget.cpp
@@ -67,6 +67,12 @@ void BacktraceWidget::setBacktraceGrid()
modelBacktrace->setItem(i, 4, rowFrameSize);
i++;
}
+
+ // Remove irrelevant old rows
+ if (modelBacktrace->rowCount() > i) {
+ modelBacktrace->removeRows(i, modelBacktrace->rowCount() - i);
+ }
+
viewBacktrace->setModel(modelBacktrace);
viewBacktrace->resizeColumnsToContents();;
}
diff --git a/src/widgets/ThreadsWidget.cpp b/src/widgets/ThreadsWidget.cpp
new file mode 100644
index 00000000..6823b67d
--- /dev/null
+++ b/src/widgets/ThreadsWidget.cpp
@@ -0,0 +1,168 @@
+#include
+#include "ThreadsWidget.h"
+#include "ui_ThreadsWidget.h"
+#include "common/JsonModel.h"
+#include "QuickFilterView.h"
+#include
+
+#include "core/MainWindow.h"
+
+#define DEBUGGED_PID (-1)
+
+enum ColumnIndex {
+ COLUMN_CURRENT = 0,
+ COLUMN_PID,
+ COLUMN_STATUS,
+ COLUMN_PATH,
+};
+
+ThreadsWidget::ThreadsWidget(MainWindow *main, QAction *action) :
+ CutterDockWidget(main, action),
+ ui(new Ui::ThreadsWidget)
+{
+ ui->setupUi(this);
+
+ // Setup threads model
+ modelThreads = new QStandardItemModel(1, 4, this);
+ modelThreads->setHorizontalHeaderItem(COLUMN_CURRENT, new QStandardItem(tr("Current")));
+ modelThreads->setHorizontalHeaderItem(COLUMN_PID, new QStandardItem(tr("PID")));
+ modelThreads->setHorizontalHeaderItem(COLUMN_STATUS, new QStandardItem(tr("Status")));
+ modelThreads->setHorizontalHeaderItem(COLUMN_PATH, new QStandardItem(tr("Path")));
+ ui->viewThreads->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
+ ui->viewThreads->setFont(Config()->getFont());
+
+ modelFilter = new ThreadsFilterModel(this);
+ modelFilter->setSourceModel(modelThreads);
+ ui->viewThreads->setModel(modelFilter);
+
+ // CTRL+F switches to the filter view and opens it in case it's hidden
+ QShortcut *searchShortcut = new QShortcut(QKeySequence::Find, this);
+ connect(searchShortcut, &QShortcut::activated, ui->quickFilterView, &QuickFilterView::showFilter);
+ searchShortcut->setContext(Qt::WidgetWithChildrenShortcut);
+
+ // ESC switches back to the thread table and clears the buffer
+ QShortcut *clearShortcut = new QShortcut(QKeySequence(Qt::Key_Escape), this);
+ connect(clearShortcut, &QShortcut::activated, this, [this]() {
+ ui->quickFilterView->clearFilter();
+ ui->viewThreads->setFocus();
+ });
+ clearShortcut->setContext(Qt::WidgetWithChildrenShortcut);
+
+ refreshDeferrer = createRefreshDeferrer([this]() {
+ updateContents();
+ });
+
+ connect(ui->quickFilterView, &QuickFilterView::filterTextChanged, modelFilter,
+ &ThreadsFilterModel::setFilterWildcard);
+ connect(Core(), &CutterCore::refreshAll, this, &ThreadsWidget::updateContents);
+ connect(Core(), &CutterCore::seekChanged, this, &ThreadsWidget::updateContents);
+ connect(Config(), &Configuration::fontsUpdated, this, &ThreadsWidget::fontsUpdatedSlot);
+ connect(ui->viewThreads, &QTableView::activated, this, &ThreadsWidget::onActivated);
+}
+
+ThreadsWidget::~ThreadsWidget() {}
+
+void ThreadsWidget::updateContents()
+{
+ if (!refreshDeferrer->attemptRefresh(nullptr)) {
+ return;
+ }
+ setThreadsGrid();
+}
+
+QString ThreadsWidget::translateStatus(QString status)
+{
+ switch (status.toStdString().c_str()[0]) {
+ case R_DBG_PROC_STOP:
+ return "Stopped";
+ case R_DBG_PROC_RUN:
+ return "Running";
+ case R_DBG_PROC_SLEEP:
+ return "Sleeping";
+ case R_DBG_PROC_ZOMBIE:
+ return "Zombie";
+ case R_DBG_PROC_DEAD:
+ return "Dead";
+ case R_DBG_PROC_RAISED:
+ return "Raised event";
+ default:
+ return "Unknown status";
+ }
+}
+
+void ThreadsWidget::setThreadsGrid()
+{
+ QJsonArray threadsValues = Core()->getProcessThreads(DEBUGGED_PID).array();
+ int i = 0;
+ for (const QJsonValue &value : threadsValues) {
+ QJsonObject threadsItem = value.toObject();
+ int pid = threadsItem["pid"].toVariant().toInt();
+ QString status = translateStatus(threadsItem["status"].toString());
+ QString path = threadsItem["path"].toString();
+ bool current = threadsItem["current"].toBool();
+
+ QStandardItem *rowCurrent = new QStandardItem(QString(current ? "True" : "False"));
+ QStandardItem *rowPid = new QStandardItem(QString::number(pid));
+ QStandardItem *rowStatus = new QStandardItem(status);
+ QStandardItem *rowPath = new QStandardItem(path);
+
+ modelThreads->setItem(i, COLUMN_CURRENT, rowCurrent);
+ modelThreads->setItem(i, COLUMN_PID, rowPid);
+ modelThreads->setItem(i, COLUMN_STATUS, rowStatus);
+ modelThreads->setItem(i, COLUMN_PATH, rowPath);
+ i++;
+ }
+
+ // Remove irrelevant old rows
+ if (modelThreads->rowCount() > i) {
+ modelThreads->removeRows(i, modelThreads->rowCount() - i);
+ }
+
+ modelFilter->setSourceModel(modelThreads);
+ ui->viewThreads->resizeColumnsToContents();;
+}
+
+void ThreadsWidget::fontsUpdatedSlot()
+{
+ ui->viewThreads->setFont(Config()->getFont());
+}
+
+void ThreadsWidget::onActivated(const QModelIndex &index)
+{
+ if (!index.isValid())
+ return;
+
+ int tid = modelFilter->data(index.sibling(index.row(), COLUMN_PID)).toInt();
+
+ // Verify that the selected tid is still in the threads list since dpt= will
+ // attach to any given id. If it isn't found simply update the UI.
+ QJsonArray threadsValues = Core()->getProcessThreads(DEBUGGED_PID).array();
+ for (QJsonValue value : threadsValues) {
+ if (tid == value.toObject()["pid"].toInt()) {
+ Core()->setCurrentDebugThread(tid);
+ break;
+ }
+ }
+
+ updateContents();
+}
+
+ThreadsFilterModel::ThreadsFilterModel(QObject *parent)
+ : QSortFilterProxyModel(parent)
+{
+ setFilterCaseSensitivity(Qt::CaseInsensitive);
+ setSortCaseSensitivity(Qt::CaseInsensitive);
+}
+
+bool ThreadsFilterModel::filterAcceptsRow(int row, const QModelIndex &parent) const
+{
+ // All columns are checked for a match
+ for (int i = COLUMN_CURRENT; i <= COLUMN_PATH; ++i) {
+ QModelIndex index = sourceModel()->index(row, i, parent);
+ if (sourceModel()->data(index).toString().contains(filterRegExp())) {
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/src/widgets/ThreadsWidget.h b/src/widgets/ThreadsWidget.h
new file mode 100644
index 00000000..7c264a3d
--- /dev/null
+++ b/src/widgets/ThreadsWidget.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include "core/Cutter.h"
+#include "CutterDockWidget.h"
+
+class MainWindow;
+
+namespace Ui {
+class ThreadsWidget;
+}
+
+class ThreadsFilterModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+public:
+ ThreadsFilterModel(QObject *parent = nullptr);
+
+protected:
+ bool filterAcceptsRow(int row, const QModelIndex &parent) const override;
+};
+
+class ThreadsWidget : public CutterDockWidget
+{
+ Q_OBJECT
+
+public:
+ explicit ThreadsWidget(MainWindow *main, QAction *action = nullptr);
+ ~ThreadsWidget();
+
+private slots:
+ void updateContents();
+ void setThreadsGrid();
+ void fontsUpdatedSlot();
+ void onActivated(const QModelIndex &index);
+
+private:
+ QString translateStatus(QString status);
+ std::unique_ptr ui;
+ QStandardItemModel *modelThreads;
+ ThreadsFilterModel *modelFilter;
+ RefreshDeferrer *refreshDeferrer;
+};
diff --git a/src/widgets/ThreadsWidget.ui b/src/widgets/ThreadsWidget.ui
new file mode 100644
index 00000000..39b989b6
--- /dev/null
+++ b/src/widgets/ThreadsWidget.ui
@@ -0,0 +1,72 @@
+
+
+ ThreadsWidget
+
+
+
+ 0
+ 0
+ 463
+ 300
+
+
+
+ Threads
+
+
+
+
+ 10
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ true
+
+
+ false
+
+
+ QAbstractItemView::SingleSelection
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ QAbstractItemView::ScrollPerPixel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+
+
+ QuickFilterView
+ QWidget
+ widgets/QuickFilterView.h
+ 1
+
+
+
+
+