aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWladimir J. van der Laan <laanwj@gmail.com>2014-06-03 10:59:53 +0200
committerWladimir J. van der Laan <laanwj@gmail.com>2014-06-03 11:13:51 +0200
commit4c097f9669a28420da74b159a4d61e509da80d33 (patch)
treebd6c5a1a5448190a86ab34f0f96f2756a2e13a99
parent522a8fa3777486725f06d6bbf5694b9cd32cbcce (diff)
parent65f78a111ff52c2212cc0a423662e7a41d1206dd (diff)
downloadbitcoin-4c097f9669a28420da74b159a4d61e509da80d33.tar.xz
Merge pull request #4225
65f78a1 Qt: Add GUI view of peer information. #4133 (Ashley Holman)
-rw-r--r--src/qt/Makefile.am3
-rw-r--r--src/qt/clientmodel.cpp9
-rw-r--r--src/qt/clientmodel.h3
-rw-r--r--src/qt/forms/rpcconsole.ui275
-rw-r--r--src/qt/guiutil.cpp48
-rw-r--r--src/qt/guiutil.h6
-rw-r--r--src/qt/peertablemodel.cpp238
-rw-r--r--src/qt/peertablemodel.h79
-rw-r--r--src/qt/rpcconsole.cpp192
-rw-r--r--src/qt/rpcconsole.h29
10 files changed, 870 insertions, 12 deletions
diff --git a/src/qt/Makefile.am b/src/qt/Makefile.am
index 648971bd8f..1d85113d78 100644
--- a/src/qt/Makefile.am
+++ b/src/qt/Makefile.am
@@ -126,6 +126,7 @@ QT_MOC_CPP = \
moc_optionsdialog.cpp \
moc_optionsmodel.cpp \
moc_overviewpage.cpp \
+ moc_peertablemodel.cpp \
moc_paymentserver.cpp \
moc_qvalidatedlineedit.cpp \
moc_qvaluecombobox.cpp \
@@ -191,6 +192,7 @@ BITCOIN_QT_H = \
overviewpage.h \
paymentrequestplus.h \
paymentserver.h \
+ peertablemodel.h \
qvalidatedlineedit.h \
qvaluecombobox.h \
receivecoinsdialog.h \
@@ -294,6 +296,7 @@ BITCOIN_QT_CPP += \
overviewpage.cpp \
paymentrequestplus.cpp \
paymentserver.cpp \
+ peertablemodel.cpp \
receivecoinsdialog.cpp \
receiverequestdialog.cpp \
recentrequeststablemodel.cpp \
diff --git a/src/qt/clientmodel.cpp b/src/qt/clientmodel.cpp
index d1f68ebd22..ce773e0f82 100644
--- a/src/qt/clientmodel.cpp
+++ b/src/qt/clientmodel.cpp
@@ -5,6 +5,7 @@
#include "clientmodel.h"
#include "guiconstants.h"
+#include "peertablemodel.h"
#include "alert.h"
#include "chainparams.h"
@@ -22,11 +23,12 @@
static const int64_t nClientStartupTime = GetTime();
ClientModel::ClientModel(OptionsModel *optionsModel, QObject *parent) :
- QObject(parent), optionsModel(optionsModel),
+ QObject(parent), optionsModel(optionsModel), peerTableModel(0),
cachedNumBlocks(0),
cachedReindexing(0), cachedImporting(0),
numBlocksAtStartup(-1), pollTimer(0)
{
+ peerTableModel = new PeerTableModel(this);
pollTimer = new QTimer(this);
connect(pollTimer, SIGNAL(timeout()), this, SLOT(updateTimer()));
pollTimer->start(MODEL_UPDATE_DELAY);
@@ -173,6 +175,11 @@ OptionsModel *ClientModel::getOptionsModel()
return optionsModel;
}
+PeerTableModel *ClientModel::getPeerTableModel()
+{
+ return peerTableModel;
+}
+
QString ClientModel::formatFullVersion() const
{
return QString::fromStdString(FormatFullVersion());
diff --git a/src/qt/clientmodel.h b/src/qt/clientmodel.h
index cab853d92d..c18d30178b 100644
--- a/src/qt/clientmodel.h
+++ b/src/qt/clientmodel.h
@@ -9,6 +9,7 @@
class AddressTableModel;
class OptionsModel;
+class PeerTableModel;
class TransactionTableModel;
class CWallet;
@@ -42,6 +43,7 @@ public:
~ClientModel();
OptionsModel *getOptionsModel();
+ PeerTableModel *getPeerTableModel();
//! Return number of connections, default is in- and outbound (total)
int getNumConnections(unsigned int flags = CONNECTIONS_ALL) const;
@@ -71,6 +73,7 @@ public:
private:
OptionsModel *optionsModel;
+ PeerTableModel *peerTableModel;
int cachedNumBlocks;
bool cachedReindexing;
diff --git a/src/qt/forms/rpcconsole.ui b/src/qt/forms/rpcconsole.ui
index fcb6bb60bb..bf737d9b99 100644
--- a/src/qt/forms/rpcconsole.ui
+++ b/src/qt/forms/rpcconsole.ui
@@ -652,6 +652,281 @@
</item>
</layout>
</widget>
+ <widget class="QWidget" name="tab_peers">
+ <attribute name="title">
+ <string>&amp;Peers</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="1">
+ <widget class="QLabel" name="peerHeading">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Select a peer to view detailed information.</string>
+ </property>
+ <property name="margin">
+ <number>3</number>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" rowspan="2">
+ <widget class="QTableView" name="peerWidget">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
+ <property name="editTriggers">
+ <set>QAbstractItemView::AnyKeyPressed|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed</set>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QWidget" name="detailWidget" native="true">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_3">
+ <property name="leftMargin">
+ <number>3</number>
+ </property>
+ <item row="12" column="0">
+ <widget class="QLabel" name="label_21">
+ <property name="text">
+ <string>Version:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="11" column="1">
+ <widget class="QLabel" name="peerPingTime">
+ <property name="text">
+ <string>N/A</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0">
+ <widget class="QLabel" name="label_19">
+ <property name="text">
+ <string>Last Receive:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="14" column="0">
+ <widget class="QLabel" name="label_28">
+ <property name="text">
+ <string>User Agent:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="12" column="1">
+ <widget class="QLabel" name="peerVersion">
+ <property name="text">
+ <string>N/A</string>
+ </property>
+ </widget>
+ </item>
+ <item row="8" column="1">
+ <widget class="QLabel" name="peerConnTime">
+ <property name="minimumSize">
+ <size>
+ <width>160</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>N/A</string>
+ </property>
+ </widget>
+ </item>
+ <item row="11" column="0">
+ <widget class="QLabel" name="label_26">
+ <property name="text">
+ <string>Ping Time:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1">
+ <widget class="QLabel" name="peerLastRecv">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>N/A</string>
+ </property>
+ </widget>
+ </item>
+ <item row="8" column="0">
+ <widget class="QLabel" name="label_22">
+ <property name="text">
+ <string>Connection Time:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="1">
+ <widget class="QLabel" name="peerBytesSent">
+ <property name="text">
+ <string>N/A</string>
+ </property>
+ </widget>
+ </item>
+ <item row="14" column="1">
+ <widget class="QLabel" name="peerSubversion">
+ <property name="text">
+ <string>N/A</string>
+ </property>
+ </widget>
+ </item>
+ <item row="15" column="0">
+ <widget class="QLabel" name="label_29">
+ <property name="text">
+ <string>Starting Height:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="7" column="1">
+ <widget class="QLabel" name="peerBytesRecv">
+ <property name="text">
+ <string>N/A</string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="0">
+ <widget class="QLabel" name="label_18">
+ <property name="text">
+ <string>Bytes Sent:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="7" column="0">
+ <widget class="QLabel" name="label_20">
+ <property name="text">
+ <string>Bytes Received:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="15" column="1">
+ <widget class="QLabel" name="peerHeight">
+ <property name="text">
+ <string>N/A</string>
+ </property>
+ </widget>
+ </item>
+ <item row="16" column="0">
+ <widget class="QLabel" name="label_24">
+ <property name="text">
+ <string>Ban Score:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="16" column="1">
+ <widget class="QLabel" name="peerBanScore">
+ <property name="text">
+ <string>N/A</string>
+ </property>
+ </widget>
+ </item>
+ <item row="17" column="0">
+ <widget class="QLabel" name="label_23">
+ <property name="text">
+ <string>Direction:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="17" column="1">
+ <widget class="QLabel" name="peerDirection">
+ <property name="text">
+ <string>N/A</string>
+ </property>
+ </widget>
+ </item>
+ <item row="19" column="0">
+ <widget class="QLabel" name="label_25">
+ <property name="text">
+ <string>Sync Node:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="19" column="1">
+ <widget class="QLabel" name="peerSyncNode">
+ <property name="text">
+ <string>N/A</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_15">
+ <property name="text">
+ <string>Last Send:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>Services:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_27">
+ <property name="text">
+ <string>IP Address/port:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLabel" name="peerLastSend">
+ <property name="text">
+ <string>N/A</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="peerServices">
+ <property name="text">
+ <string>N/A</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="peerAddr">
+ <property name="text">
+ <string>N/A</string>
+ </property>
+ </widget>
+ </item>
+ <item row="20" column="0">
+ <widget class="QWidget" name="widget" native="true">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
</widget>
</item>
</layout>
diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp
index 183fcac4a0..62db0487fc 100644
--- a/src/qt/guiutil.cpp
+++ b/src/qt/guiutil.cpp
@@ -11,6 +11,7 @@
#include "core.h"
#include "init.h"
+#include "protocol.h"
#include "util.h"
#ifdef WIN32
@@ -754,4 +755,51 @@ QString boostPathToQString(const boost::filesystem::path &path)
}
#endif
+QString formatDurationStr(int secs)
+{
+ QStringList strList;
+ int days = secs / 86400;
+ int hours = (secs % 86400) / 3600;
+ int mins = (secs % 3600) / 60;
+ int seconds = secs % 60;
+
+ if (days)
+ strList.append(QString(QObject::tr("%1 d")).arg(days));
+ if (hours)
+ strList.append(QString(QObject::tr("%1 h")).arg(hours));
+ if (mins)
+ strList.append(QString(QObject::tr("%1 m")).arg(mins));
+ if (seconds || (!days && !hours && !mins))
+ strList.append(QString(QObject::tr("%1 s")).arg(seconds));
+
+ return strList.join(" ");
+}
+
+QString formatServicesStr(uint64_t mask)
+{
+ QStringList strList;
+
+ // Just scan the last 8 bits for now.
+ for (int i=0; i < 8; i++) {
+ uint64_t check = 1 << i;
+ if (mask & check)
+ {
+ switch (check)
+ {
+ case NODE_NETWORK:
+ strList.append(QObject::tr("NETWORK"));
+ break;
+ default:
+ strList.append(QString("%1[%2]").arg(QObject::tr("UNKNOWN")).arg(check));
+ }
+ }
+ }
+
+ if (strList.size())
+ return strList.join(" & ");
+ else
+ return QObject::tr("None");
+
+}
+
} // namespace GUIUtil
diff --git a/src/qt/guiutil.h b/src/qt/guiutil.h
index 4f9416d1af..ea6b7de872 100644
--- a/src/qt/guiutil.h
+++ b/src/qt/guiutil.h
@@ -173,6 +173,12 @@ namespace GUIUtil
/* Convert OS specific boost path to QString through UTF-8 */
QString boostPathToQString(const boost::filesystem::path &path);
+ /* Convert seconds into a QString with days, hours, mins, secs */
+ QString formatDurationStr(int secs);
+
+ /* Format CNodeStats.nServices bitmask into a user-readable string */
+ QString formatServicesStr(uint64_t mask);
+
} // namespace GUIUtil
#endif // GUIUTIL_H
diff --git a/src/qt/peertablemodel.cpp b/src/qt/peertablemodel.cpp
new file mode 100644
index 0000000000..fba9d84e77
--- /dev/null
+++ b/src/qt/peertablemodel.cpp
@@ -0,0 +1,238 @@
+// Copyright (c) 2011-2013 The Bitcoin developers
+// Distributed under the MIT/X11 software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include "peertablemodel.h"
+
+#include "clientmodel.h"
+
+#include "net.h"
+#include "sync.h"
+
+#include <QDebug>
+#include <QList>
+#include <QTimer>
+
+bool NodeLessThan::operator()(const CNodeCombinedStats &left, const CNodeCombinedStats &right) const
+{
+ const CNodeStats *pLeft = &(left.nodestats);
+ const CNodeStats *pRight = &(right.nodestats);
+
+ if (order == Qt::DescendingOrder)
+ std::swap(pLeft, pRight);
+
+ switch(column)
+ {
+ case PeerTableModel::Address:
+ return pLeft->addrName.compare(pRight->addrName) < 0;
+ case PeerTableModel::Subversion:
+ return pLeft->cleanSubVer.compare(pRight->cleanSubVer) < 0;
+ case PeerTableModel::Height:
+ return pLeft->nStartingHeight < pRight->nStartingHeight;
+ }
+
+ return false;
+}
+
+// private implementation
+class PeerTablePriv
+{
+public:
+ /** Local cache of peer information */
+ QList<CNodeCombinedStats> cachedNodeStats;
+ /** Column to sort nodes by */
+ int sortColumn;
+ /** Order (ascending or descending) to sort nodes by */
+ Qt::SortOrder sortOrder;
+ /** Index of rows by node ID */
+ std::map<NodeId, int> mapNodeRows;
+
+ /** Pull a full list of peers from vNodes into our cache */
+ void refreshPeers() {
+ TRY_LOCK(cs_vNodes, lockNodes);
+ {
+ if (!lockNodes)
+ {
+ // skip the refresh if we can't immediately get the lock
+ return;
+ }
+ cachedNodeStats.clear();
+#if QT_VERSION >= 0x040700
+ cachedNodeStats.reserve(vNodes.size());
+#endif
+ BOOST_FOREACH(CNode* pnode, vNodes)
+ {
+ CNodeCombinedStats stats;
+ stats.statestats.nMisbehavior = -1;
+ pnode->copyStats(stats.nodestats);
+ cachedNodeStats.append(stats);
+ }
+ }
+
+ // if we can, retrieve the CNodeStateStats for each node.
+ TRY_LOCK(cs_main, lockMain);
+ {
+ if (lockMain)
+ {
+ BOOST_FOREACH(CNodeCombinedStats &stats, cachedNodeStats)
+ {
+ GetNodeStateStats(stats.nodestats.nodeid, stats.statestats);
+ }
+ }
+ }
+
+
+ if (sortColumn >= 0)
+ // sort cacheNodeStats (use stable sort to prevent rows jumping around unneceesarily)
+ qStableSort(cachedNodeStats.begin(), cachedNodeStats.end(), NodeLessThan(sortColumn, sortOrder));
+
+ // build index map
+ mapNodeRows.clear();
+ int row = 0;
+ BOOST_FOREACH(CNodeCombinedStats &stats, cachedNodeStats)
+ {
+ mapNodeRows.insert(std::pair<NodeId, int>(stats.nodestats.nodeid, row++));
+ }
+ }
+
+ int size()
+ {
+ return cachedNodeStats.size();
+ }
+
+ CNodeCombinedStats *index(int idx)
+ {
+ if(idx >= 0 && idx < cachedNodeStats.size()) {
+ return &cachedNodeStats[idx];
+ }
+ else
+ {
+ return 0;
+ }
+ }
+
+};
+
+PeerTableModel::PeerTableModel(ClientModel *parent) :
+ QAbstractTableModel(parent),clientModel(parent),timer(0)
+{
+ columns << tr("Address") << tr("User Agent") << tr("Start Height");
+ priv = new PeerTablePriv();
+ // default to unsorted
+ priv->sortColumn = -1;
+
+ // set up timer for auto refresh
+ timer = new QTimer();
+ connect(timer, SIGNAL(timeout()), SLOT(refresh()));
+
+ // load initial data
+ refresh();
+}
+
+void PeerTableModel::startAutoRefresh(int msecs)
+{
+ timer->setInterval(1000);
+ timer->start();
+}
+
+void PeerTableModel::stopAutoRefresh()
+{
+ timer->stop();
+}
+
+int PeerTableModel::rowCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent);
+ return priv->size();
+}
+
+int PeerTableModel::columnCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent);
+ return 3;
+}
+
+QVariant PeerTableModel::data(const QModelIndex &index, int role) const
+{
+ if(!index.isValid())
+ return QVariant();
+
+ CNodeCombinedStats *rec = static_cast<CNodeCombinedStats*>(index.internalPointer());
+
+ if(role == Qt::DisplayRole)
+ {
+ switch(index.column())
+ {
+ case Address:
+ return QVariant(rec->nodestats.addrName.c_str());
+ case Subversion:
+ return QVariant(rec->nodestats.cleanSubVer.c_str());
+ case Height:
+ return rec->nodestats.nStartingHeight;
+ }
+ }
+ return QVariant();
+}
+
+QVariant PeerTableModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if(orientation == Qt::Horizontal)
+ {
+ if(role == Qt::DisplayRole && section < columns.size())
+ {
+ return columns[section];
+ }
+ }
+ return QVariant();
+}
+
+Qt::ItemFlags PeerTableModel::flags(const QModelIndex &index) const
+{
+ if(!index.isValid())
+ return 0;
+
+ Qt::ItemFlags retval = Qt::ItemIsSelectable | Qt::ItemIsEnabled;
+ return retval;
+}
+
+QModelIndex PeerTableModel::index(int row, int column, const QModelIndex &parent) const
+{
+ Q_UNUSED(parent);
+ CNodeCombinedStats *data = priv->index(row);
+
+ if (data)
+ {
+ return createIndex(row, column, data);
+ }
+ else
+ {
+ return QModelIndex();
+ }
+}
+
+const CNodeCombinedStats *PeerTableModel::getNodeStats(int idx) {
+ return priv->index(idx);
+}
+
+void PeerTableModel::refresh()
+{
+ emit layoutAboutToBeChanged();
+ priv->refreshPeers();
+ emit layoutChanged();
+}
+
+int PeerTableModel::getRowByNodeId(NodeId nodeid)
+{
+ std::map<NodeId, int>::iterator it = priv->mapNodeRows.find(nodeid);
+ if (it == priv->mapNodeRows.end())
+ return -1;
+
+ return it->second;
+}
+
+void PeerTableModel::sort(int column, Qt::SortOrder order)
+{
+ priv->sortColumn = column;
+ priv->sortOrder = order;
+ refresh();
+}
diff --git a/src/qt/peertablemodel.h b/src/qt/peertablemodel.h
new file mode 100644
index 0000000000..d947e21240
--- /dev/null
+++ b/src/qt/peertablemodel.h
@@ -0,0 +1,79 @@
+// Copyright (c) 2011-2013 The Bitcoin developers
+// Distributed under the MIT/X11 software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#ifndef PEERTABLEMODEL_H
+#define PEERTABLEMODEL_H
+
+#include "main.h"
+#include "net.h"
+
+#include <QAbstractTableModel>
+#include <QStringList>
+
+class PeerTablePriv;
+class ClientModel;
+
+class QTimer;
+
+struct CNodeCombinedStats {
+ CNodeStats nodestats;
+ CNodeStateStats statestats;
+};
+
+class NodeLessThan
+{
+public:
+ NodeLessThan(int nColumn, Qt::SortOrder fOrder):
+ column(nColumn), order(fOrder) {}
+ bool operator()(const CNodeCombinedStats &left, const CNodeCombinedStats &right) const;
+
+private:
+ int column;
+ Qt::SortOrder order;
+};
+
+/**
+ Qt model providing information about connected peers, similar to the
+ "getpeerinfo" RPC call. Used by the rpc console UI.
+ */
+class PeerTableModel : public QAbstractTableModel
+{
+ Q_OBJECT
+
+public:
+ explicit PeerTableModel(ClientModel *parent = 0);
+ const CNodeCombinedStats *getNodeStats(int idx);
+ int getRowByNodeId(NodeId nodeid);
+ void startAutoRefresh(int msecs);
+ void stopAutoRefresh();
+
+ enum ColumnIndex {
+ Address = 0,
+ Subversion = 1,
+ Height = 2
+ };
+
+ /** @name Methods overridden from QAbstractTableModel
+ @{*/
+ int rowCount(const QModelIndex &parent) const;
+ int columnCount(const QModelIndex &parent) const;
+ QVariant data(const QModelIndex &index, int role) const;
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const;
+ QModelIndex index(int row, int column, const QModelIndex &parent) const;
+ Qt::ItemFlags flags(const QModelIndex &index) const;
+ void sort(int column, Qt::SortOrder order);
+ /*@}*/
+
+public slots:
+ void refresh();
+
+private:
+ ClientModel *clientModel;
+ QStringList columns;
+ PeerTablePriv *priv;
+ QTimer *timer;
+
+};
+
+#endif // PEERTABLEMODEL_H
diff --git a/src/qt/rpcconsole.cpp b/src/qt/rpcconsole.cpp
index 0a46a722e4..ed048cf3cc 100644
--- a/src/qt/rpcconsole.cpp
+++ b/src/qt/rpcconsole.cpp
@@ -7,6 +7,10 @@
#include "clientmodel.h"
#include "guiutil.h"
+#include "peertablemodel.h"
+
+#include "main.h"
+#include "util.h"
#include "rpcserver.h"
#include "rpcclient.h"
@@ -195,6 +199,10 @@ RPCConsole::RPCConsole(QWidget *parent) :
clientModel(0),
historyPtr(0)
{
+ detailNodeStats = CNodeCombinedStats();
+ detailNodeStats.nodestats.nodeid = -1;
+ detailNodeStats.statestats.nMisbehavior = -1;
+
ui->setupUi(this);
GUIUtil::restoreWindowGeometry("nRPCConsoleWindow", this->size(), this);
@@ -214,6 +222,7 @@ RPCConsole::RPCConsole(QWidget *parent) :
startExecutor();
setTrafficGraphRange(INITIAL_TRAFFIC_GRAPH_MINS);
+ ui->detailWidget->hide();
clear();
}
@@ -277,6 +286,23 @@ void RPCConsole::setClientModel(ClientModel *model)
updateTrafficStats(model->getTotalBytesRecv(), model->getTotalBytesSent());
connect(model, SIGNAL(bytesChanged(quint64,quint64)), this, SLOT(updateTrafficStats(quint64, quint64)));
+ // set up peer table
+ ui->peerWidget->setModel(model->getPeerTableModel());
+ ui->peerWidget->verticalHeader()->hide();
+ ui->peerWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ ui->peerWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
+ ui->peerWidget->setSelectionMode(QAbstractItemView::SingleSelection);
+ ui->peerWidget->setColumnWidth(PeerTableModel::Address, ADDRESS_COLUMN_WIDTH);
+ columnResizingFixer = new GUIUtil::TableViewLastColumnResizingFixer(ui->peerWidget, MINIMUM_COLUMN_WIDTH, MINIMUM_COLUMN_WIDTH);
+
+ // connect the peerWidget's selection model to our peerSelected() handler
+ QItemSelectionModel *peerSelectModel = ui->peerWidget->selectionModel();
+ connect(peerSelectModel,
+ SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
+ this,
+ SLOT(peerSelected(const QItemSelection &, const QItemSelection &)));
+ connect(model->getPeerTableModel(), SIGNAL(layoutChanged()), this, SLOT(peerLayoutChanged()));
+
// Provide initial values
ui->clientVersion->setText(model->formatFullVersion());
ui->clientName->setText(model->clientName());
@@ -474,17 +500,7 @@ QString RPCConsole::FormatBytes(quint64 bytes)
void RPCConsole::setTrafficGraphRange(int mins)
{
ui->trafficGraph->setGraphRangeMins(mins);
- if(mins < 60) {
- ui->lblGraphRange->setText(QString(tr("%1 m")).arg(mins));
- } else {
- int hours = mins / 60;
- int minsLeft = mins % 60;
- if(minsLeft == 0) {
- ui->lblGraphRange->setText(QString(tr("%1 h")).arg(hours));
- } else {
- ui->lblGraphRange->setText(QString(tr("%1 h %2 m")).arg(hours).arg(minsLeft));
- }
- }
+ ui->lblGraphRange->setText(GUIUtil::formatDurationStr(mins * 60));
}
void RPCConsole::updateTrafficStats(quint64 totalBytesIn, quint64 totalBytesOut)
@@ -492,3 +508,157 @@ void RPCConsole::updateTrafficStats(quint64 totalBytesIn, quint64 totalBytesOut)
ui->lblBytesIn->setText(FormatBytes(totalBytesIn));
ui->lblBytesOut->setText(FormatBytes(totalBytesOut));
}
+
+void RPCConsole::peerSelected(const QItemSelection &selected, const QItemSelection &deselected)
+{
+ if (selected.indexes().isEmpty())
+ return;
+
+ // mark the cached banscore as unknown
+ detailNodeStats.statestats.nMisbehavior = -1;
+
+ const CNodeCombinedStats *stats = clientModel->getPeerTableModel()->getNodeStats(selected.indexes().first().row());
+
+ if (stats)
+ {
+ detailNodeStats.nodestats.nodeid = stats->nodestats.nodeid;
+ updateNodeDetail(stats);
+ ui->detailWidget->show();
+ ui->detailWidget->setDisabled(false);
+ }
+}
+
+void RPCConsole::peerLayoutChanged()
+{
+ const CNodeCombinedStats *stats = NULL;
+ bool fUnselect = false, fReselect = false, fDisconnected = false;
+
+ if (detailNodeStats.nodestats.nodeid == -1)
+ // no node selected yet
+ return;
+
+ // find the currently selected row
+ int selectedRow;
+ QModelIndexList selectedModelIndex = ui->peerWidget->selectionModel()->selectedIndexes();
+ if (selectedModelIndex.isEmpty())
+ selectedRow = -1;
+ else
+ selectedRow = selectedModelIndex.first().row();
+
+ // check if our detail node has a row in the table (it may not necessarily
+ // be at selectedRow since its position can change after a layout change)
+ int detailNodeRow = clientModel->getPeerTableModel()->getRowByNodeId(detailNodeStats.nodestats.nodeid);
+
+ if (detailNodeRow < 0)
+ {
+ // detail node dissapeared from table (node disconnected)
+ fUnselect = true;
+ fDisconnected = true;
+ detailNodeStats.nodestats.nodeid = 0;
+ }
+ else
+ {
+ if (detailNodeRow != selectedRow)
+ {
+ // detail node moved position
+ fUnselect = true;
+ fReselect = true;
+ }
+
+ // get fresh stats on the detail node.
+ stats = clientModel->getPeerTableModel()->getNodeStats(detailNodeRow);
+ }
+
+ if (fUnselect && selectedRow >= 0)
+ {
+ ui->peerWidget->selectionModel()->select(QItemSelection(selectedModelIndex.first(), selectedModelIndex.last()),
+ QItemSelectionModel::Deselect);
+ }
+
+ if (fReselect)
+ {
+ ui->peerWidget->selectRow(detailNodeRow);
+ }
+
+ if (stats)
+ updateNodeDetail(stats);
+
+ if (fDisconnected)
+ {
+ ui->peerHeading->setText(QString(tr("Peer Disconnected")));
+ ui->detailWidget->setDisabled(true);
+ QDateTime dt = QDateTime::fromTime_t(detailNodeStats.nodestats.nLastSend);
+ if (detailNodeStats.nodestats.nLastSend)
+ ui->peerLastSend->setText(dt.toString("yyyy-MM-dd hh:mm:ss"));
+ dt.setTime_t(detailNodeStats.nodestats.nLastRecv);
+ if (detailNodeStats.nodestats.nLastRecv)
+ ui->peerLastRecv->setText(dt.toString("yyyy-MM-dd hh:mm:ss"));
+ dt.setTime_t(detailNodeStats.nodestats.nTimeConnected);
+ ui->peerConnTime->setText(dt.toString("yyyy-MM-dd hh:mm:ss"));
+ }
+}
+
+void RPCConsole::updateNodeDetail(const CNodeCombinedStats *combinedStats)
+{
+ CNodeStats stats = combinedStats->nodestats;
+
+ // keep a copy of timestamps, used to display dates upon disconnect
+ detailNodeStats.nodestats.nLastSend = stats.nLastSend;
+ detailNodeStats.nodestats.nLastRecv = stats.nLastRecv;
+ detailNodeStats.nodestats.nTimeConnected = stats.nTimeConnected;
+
+ // update the detail ui with latest node information
+ ui->peerHeading->setText(QString("<b>%1</b>").arg(tr("Node Detail")));
+ ui->peerAddr->setText(QString(stats.addrName.c_str()));
+ ui->peerServices->setText(GUIUtil::formatServicesStr(stats.nServices));
+ ui->peerLastSend->setText(stats.nLastSend ? GUIUtil::formatDurationStr(GetTime() - stats.nLastSend) : tr("never"));
+ ui->peerLastRecv->setText(stats.nLastRecv ? GUIUtil::formatDurationStr(GetTime() - stats.nLastRecv) : tr("never"));
+ ui->peerBytesSent->setText(FormatBytes(stats.nSendBytes));
+ ui->peerBytesRecv->setText(FormatBytes(stats.nRecvBytes));
+ ui->peerConnTime->setText(GUIUtil::formatDurationStr(GetTime() - stats.nTimeConnected));
+ ui->peerPingTime->setText(stats.dPingTime == 0 ? tr("N/A") : QString(tr("%1 secs")).arg(QString::number(stats.dPingTime, 'f', 3)));
+ ui->peerVersion->setText(QString("%1").arg(stats.nVersion));
+ ui->peerSubversion->setText(QString(stats.cleanSubVer.c_str()));
+ ui->peerDirection->setText(stats.fInbound ? tr("Inbound") : tr("Outbound"));
+ ui->peerHeight->setText(QString("%1").arg(stats.nStartingHeight));
+ ui->peerSyncNode->setText(stats.fSyncNode ? tr("Yes") : tr("No"));
+
+ // if we can, display the peer's ban score
+ CNodeStateStats statestats = combinedStats->statestats;
+ if (statestats.nMisbehavior >= 0)
+ {
+ // we have a new nMisbehavor value - update the cache
+ detailNodeStats.statestats.nMisbehavior = statestats.nMisbehavior;
+ }
+
+ // pull the ban score from cache. -1 means it hasn't been retrieved yet (lock busy).
+ if (detailNodeStats.statestats.nMisbehavior >= 0)
+ ui->peerBanScore->setText(QString("%1").arg(detailNodeStats.statestats.nMisbehavior));
+ else
+ ui->peerBanScore->setText(tr("Fetching..."));
+}
+
+void RPCConsole::resizeEvent(QResizeEvent *event)
+{
+ QWidget::resizeEvent(event);
+ columnResizingFixer->stretchColumnWidth(PeerTableModel::Address);
+}
+
+void RPCConsole::showEvent(QShowEvent *event)
+{
+ QWidget::showEvent(event);
+
+ // peerWidget needs a resize in case the dialog has non-default geometry
+ columnResizingFixer->stretchColumnWidth(PeerTableModel::Address);
+
+ // start the PeerTableModel refresh timer
+ clientModel->getPeerTableModel()->startAutoRefresh(1000);
+}
+
+void RPCConsole::hideEvent(QHideEvent *event)
+{
+ QWidget::hideEvent(event);
+
+ // stop PeerTableModel auto refresh
+ clientModel->getPeerTableModel()->stopAutoRefresh();
+}
diff --git a/src/qt/rpcconsole.h b/src/qt/rpcconsole.h
index 091a6d294f..c17d5397ec 100644
--- a/src/qt/rpcconsole.h
+++ b/src/qt/rpcconsole.h
@@ -5,10 +5,18 @@
#ifndef RPCCONSOLE_H
#define RPCCONSOLE_H
+#include "guiutil.h"
+#include "net.h"
+
+#include "peertablemodel.h"
+
#include <QDialog>
class ClientModel;
+class QItemSelection;
+class CNodeCombinedStats;
+
namespace Ui {
class RPCConsole;
}
@@ -35,6 +43,19 @@ public:
protected:
virtual bool eventFilter(QObject* obj, QEvent *event);
+private:
+ /** show detailed information on ui about selected node */
+ void updateNodeDetail(const CNodeCombinedStats *combinedStats);
+
+ enum ColumnWidths
+ {
+ ADDRESS_COLUMN_WIDTH = 250,
+ MINIMUM_COLUMN_WIDTH = 120
+ };
+
+ /** track the node that we are currently viewing detail on in the peers tab */
+ CNodeCombinedStats detailNodeStats;
+
private slots:
void on_lineEdit_returnPressed();
void on_tabWidget_currentChanged(int index);
@@ -44,6 +65,9 @@ private slots:
void on_sldGraphRange_valueChanged(int value);
/** update traffic statistics */
void updateTrafficStats(quint64 totalBytesIn, quint64 totalBytesOut);
+ void resizeEvent(QResizeEvent *event);
+ void showEvent(QShowEvent *event);
+ void hideEvent(QHideEvent *event);
public slots:
void clear();
@@ -57,6 +81,10 @@ public slots:
void browseHistory(int offset);
/** Scroll console view to end */
void scrollToEnd();
+ /** Handle selection of peer in peers list */
+ void peerSelected(const QItemSelection &selected, const QItemSelection &deselected);
+ /** Handle updated peer information */
+ void peerLayoutChanged();
signals:
// For RPC command executor
@@ -70,6 +98,7 @@ private:
Ui::RPCConsole *ui;
ClientModel *clientModel;
QStringList history;
+ GUIUtil::TableViewLastColumnResizingFixer *columnResizingFixer;
int historyPtr;
void startExecutor();