diff options
author | Wladimir J. van der Laan <laanwj@gmail.com> | 2014-06-03 10:59:53 +0200 |
---|---|---|
committer | Wladimir J. van der Laan <laanwj@gmail.com> | 2014-06-03 11:13:51 +0200 |
commit | 4c097f9669a28420da74b159a4d61e509da80d33 (patch) | |
tree | bd6c5a1a5448190a86ab34f0f96f2756a2e13a99 /src | |
parent | 522a8fa3777486725f06d6bbf5694b9cd32cbcce (diff) | |
parent | 65f78a111ff52c2212cc0a423662e7a41d1206dd (diff) |
Merge pull request #4225
65f78a1 Qt: Add GUI view of peer information. #4133 (Ashley Holman)
Diffstat (limited to 'src')
-rw-r--r-- | src/qt/Makefile.am | 3 | ||||
-rw-r--r-- | src/qt/clientmodel.cpp | 9 | ||||
-rw-r--r-- | src/qt/clientmodel.h | 3 | ||||
-rw-r--r-- | src/qt/forms/rpcconsole.ui | 275 | ||||
-rw-r--r-- | src/qt/guiutil.cpp | 48 | ||||
-rw-r--r-- | src/qt/guiutil.h | 6 | ||||
-rw-r--r-- | src/qt/peertablemodel.cpp | 238 | ||||
-rw-r--r-- | src/qt/peertablemodel.h | 79 | ||||
-rw-r--r-- | src/qt/rpcconsole.cpp | 192 | ||||
-rw-r--r-- | src/qt/rpcconsole.h | 29 |
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>&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(); |