dogecoin/src/qt/rpcconsole.cpp
Wladimir J. van der Laan 64cc3ecbd1 qt: Introduce PlatformStyle
Introduce a PlatformStyle to handle platform-specific customization of
the UI.

This replaces 'scicon', as well as #ifdefs to determine whether to place
icons on buttons.

The selected PlatformStyle defaults to the platform that the application
was compiled on, but can be overridden from the command line with
`-uiplatform=<x>`.

Also fixes the warning from #6328.
2015-10-31 14:49:40 +00:00

664 lines
22 KiB
C++

// Copyright (c) 2011-2014 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include "rpcconsole.h"
#include "ui_rpcconsole.h"
#include "clientmodel.h"
#include "guiutil.h"
#include "peertablemodel.h"
#include "platformstyle.h"
#include "main.h"
#include "chainparams.h"
#include "rpcserver.h"
#include "rpcclient.h"
#include "util.h"
#include "json/json_spirit_value.h"
#include <openssl/crypto.h>
#ifdef ENABLE_WALLET
#include <db_cxx.h>
#endif
#include <QKeyEvent>
#include <QScrollBar>
#include <QThread>
#include <QTime>
#if QT_VERSION < 0x050000
#include <QUrl>
#endif
// TODO: add a scrollback limit, as there is currently none
// TODO: make it possible to filter out categories (esp debug messages when implemented)
// TODO: receive errors and debug messages through ClientModel
const int CONSOLE_HISTORY = 50;
const QSize ICON_SIZE(24, 24);
const int INITIAL_TRAFFIC_GRAPH_MINS = 30;
const struct {
const char *url;
const char *source;
} ICON_MAPPING[] = {
{"cmd-request", ":/icons/tx_input"},
{"cmd-reply", ":/icons/tx_output"},
{"cmd-error", ":/icons/tx_output"},
{"misc", ":/icons/tx_inout"},
{NULL, NULL}
};
/* Object for executing console RPC commands in a separate thread.
*/
class RPCExecutor : public QObject
{
Q_OBJECT
public slots:
void request(const QString &command);
signals:
void reply(int category, const QString &command);
};
#include "rpcconsole.moc"
/**
* Split shell command line into a list of arguments. Aims to emulate \c bash and friends.
*
* - Arguments are delimited with whitespace
* - Extra whitespace at the beginning and end and between arguments will be ignored
* - Text can be "double" or 'single' quoted
* - The backslash \c \ is used as escape character
* - Outside quotes, any character can be escaped
* - Within double quotes, only escape \c " and backslashes before a \c " or another backslash
* - Within single quotes, no escaping is possible and no special interpretation takes place
*
* @param[out] args Parsed arguments will be appended to this list
* @param[in] strCommand Command line to split
*/
bool parseCommandLine(std::vector<std::string> &args, const std::string &strCommand)
{
enum CmdParseState
{
STATE_EATING_SPACES,
STATE_ARGUMENT,
STATE_SINGLEQUOTED,
STATE_DOUBLEQUOTED,
STATE_ESCAPE_OUTER,
STATE_ESCAPE_DOUBLEQUOTED
} state = STATE_EATING_SPACES;
std::string curarg;
foreach(char ch, strCommand)
{
switch(state)
{
case STATE_ARGUMENT: // In or after argument
case STATE_EATING_SPACES: // Handle runs of whitespace
switch(ch)
{
case '"': state = STATE_DOUBLEQUOTED; break;
case '\'': state = STATE_SINGLEQUOTED; break;
case '\\': state = STATE_ESCAPE_OUTER; break;
case ' ': case '\n': case '\t':
if(state == STATE_ARGUMENT) // Space ends argument
{
args.push_back(curarg);
curarg.clear();
}
state = STATE_EATING_SPACES;
break;
default: curarg += ch; state = STATE_ARGUMENT;
}
break;
case STATE_SINGLEQUOTED: // Single-quoted string
switch(ch)
{
case '\'': state = STATE_ARGUMENT; break;
default: curarg += ch;
}
break;
case STATE_DOUBLEQUOTED: // Double-quoted string
switch(ch)
{
case '"': state = STATE_ARGUMENT; break;
case '\\': state = STATE_ESCAPE_DOUBLEQUOTED; break;
default: curarg += ch;
}
break;
case STATE_ESCAPE_OUTER: // '\' outside quotes
curarg += ch; state = STATE_ARGUMENT;
break;
case STATE_ESCAPE_DOUBLEQUOTED: // '\' in double-quoted text
if(ch != '"' && ch != '\\') curarg += '\\'; // keep '\' for everything but the quote and '\' itself
curarg += ch; state = STATE_DOUBLEQUOTED;
break;
}
}
switch(state) // final state
{
case STATE_EATING_SPACES:
return true;
case STATE_ARGUMENT:
args.push_back(curarg);
return true;
default: // ERROR to end in one of the other states
return false;
}
}
void RPCExecutor::request(const QString &command)
{
std::vector<std::string> args;
if(!parseCommandLine(args, command.toStdString()))
{
emit reply(RPCConsole::CMD_ERROR, QString("Parse error: unbalanced ' or \""));
return;
}
if(args.empty())
return; // Nothing to do
try
{
std::string strPrint;
// Convert argument list to JSON objects in method-dependent way,
// and pass it along with the method name to the dispatcher.
json_spirit::Value result = tableRPC.execute(
args[0],
RPCConvertValues(args[0], std::vector<std::string>(args.begin() + 1, args.end())));
// Format result reply
if (result.type() == json_spirit::null_type)
strPrint = "";
else if (result.type() == json_spirit::str_type)
strPrint = result.get_str();
else
strPrint = write_string(result, true);
emit reply(RPCConsole::CMD_REPLY, QString::fromStdString(strPrint));
}
catch (const json_spirit::Object& objError)
{
try // Nice formatting for standard-format error
{
int code = find_value(objError, "code").get_int();
std::string message = find_value(objError, "message").get_str();
emit reply(RPCConsole::CMD_ERROR, QString::fromStdString(message) + " (code " + QString::number(code) + ")");
}
catch (const std::runtime_error&) // raised when converting to invalid type, i.e. missing code or message
{ // Show raw JSON object
emit reply(RPCConsole::CMD_ERROR, QString::fromStdString(write_string(json_spirit::Value(objError), false)));
}
}
catch (const std::exception& e)
{
emit reply(RPCConsole::CMD_ERROR, QString("Error: ") + QString::fromStdString(e.what()));
}
}
RPCConsole::RPCConsole(const PlatformStyle *platformStyle, QWidget *parent) :
QWidget(parent),
ui(new Ui::RPCConsole),
clientModel(0),
historyPtr(0),
cachedNodeid(-1),
contextMenu(0),
platformStyle(platformStyle)
{
ui->setupUi(this);
GUIUtil::restoreWindowGeometry("nRPCConsoleWindow", this->size(), this);
if (platformStyle->getImagesOnButtons()) {
ui->openDebugLogfileButton->setIcon(platformStyle->SingleColorIcon(":/icons/export"));
}
ui->clearButton->setIcon(platformStyle->SingleColorIcon(":/icons/remove"));
// Install event filter for up and down arrow
ui->lineEdit->installEventFilter(this);
ui->messagesWidget->installEventFilter(this);
connect(ui->clearButton, SIGNAL(clicked()), this, SLOT(clear()));
connect(ui->btnClearTrafficGraph, SIGNAL(clicked()), ui->trafficGraph, SLOT(clear()));
// set library version labels
ui->openSSLVersion->setText(SSLeay_version(SSLEAY_VERSION));
#ifdef ENABLE_WALLET
ui->berkeleyDBVersion->setText(DbEnv::version(0, 0, 0));
#else
ui->label_berkeleyDBVersion->hide();
ui->berkeleyDBVersion->hide();
#endif
startExecutor();
setTrafficGraphRange(INITIAL_TRAFFIC_GRAPH_MINS);
ui->detailWidget->hide();
ui->peerHeading->setText(tr("Select a peer to view detailed information."));
clear();
}
RPCConsole::~RPCConsole()
{
GUIUtil::saveWindowGeometry("nRPCConsoleWindow", this);
emit stopExecutor();
delete ui;
}
bool RPCConsole::eventFilter(QObject* obj, QEvent *event)
{
if(event->type() == QEvent::KeyPress) // Special key handling
{
QKeyEvent *keyevt = static_cast<QKeyEvent*>(event);
int key = keyevt->key();
Qt::KeyboardModifiers mod = keyevt->modifiers();
switch(key)
{
case Qt::Key_Up: if(obj == ui->lineEdit) { browseHistory(-1); return true; } break;
case Qt::Key_Down: if(obj == ui->lineEdit) { browseHistory(1); return true; } break;
case Qt::Key_PageUp: /* pass paging keys to messages widget */
case Qt::Key_PageDown:
if(obj == ui->lineEdit)
{
QApplication::postEvent(ui->messagesWidget, new QKeyEvent(*keyevt));
return true;
}
break;
default:
// Typing in messages widget brings focus to line edit, and redirects key there
// Exclude most combinations and keys that emit no text, except paste shortcuts
if(obj == ui->messagesWidget && (
(!mod && !keyevt->text().isEmpty() && key != Qt::Key_Tab) ||
((mod & Qt::ControlModifier) && key == Qt::Key_V) ||
((mod & Qt::ShiftModifier) && key == Qt::Key_Insert)))
{
ui->lineEdit->setFocus();
QApplication::postEvent(ui->lineEdit, new QKeyEvent(*keyevt));
return true;
}
}
}
return QWidget::eventFilter(obj, event);
}
void RPCConsole::setClientModel(ClientModel *model)
{
clientModel = model;
ui->trafficGraph->setClientModel(model);
if(model)
{
// Keep up to date with client
setNumConnections(model->getNumConnections());
connect(model, SIGNAL(numConnectionsChanged(int)), this, SLOT(setNumConnections(int)));
setNumBlocks(model->getNumBlocks(), model->getLastBlockDate());
connect(model, SIGNAL(numBlocksChanged(int,QDateTime)), this, SLOT(setNumBlocks(int,QDateTime)));
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);
ui->peerWidget->setColumnWidth(PeerTableModel::Subversion, SUBVERSION_COLUMN_WIDTH);
ui->peerWidget->setColumnWidth(PeerTableModel::Ping, PING_COLUMN_WIDTH);
// connect the peerWidget selection model to our peerSelected() handler
connect(ui->peerWidget->selectionModel(), 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());
ui->buildDate->setText(model->formatBuildDate());
ui->startupTime->setText(model->formatClientStartupTime());
ui->networkName->setText(QString::fromStdString(Params().NetworkIDString()));
}
}
static QString categoryClass(int category)
{
switch(category)
{
case RPCConsole::CMD_REQUEST: return "cmd-request"; break;
case RPCConsole::CMD_REPLY: return "cmd-reply"; break;
case RPCConsole::CMD_ERROR: return "cmd-error"; break;
default: return "misc";
}
}
void RPCConsole::clear()
{
ui->messagesWidget->clear();
history.clear();
historyPtr = 0;
ui->lineEdit->clear();
ui->lineEdit->setFocus();
// Add smoothly scaled icon images.
// (when using width/height on an img, Qt uses nearest instead of linear interpolation)
for(int i=0; ICON_MAPPING[i].url; ++i)
{
ui->messagesWidget->document()->addResource(
QTextDocument::ImageResource,
QUrl(ICON_MAPPING[i].url),
platformStyle->SingleColorImage(ICON_MAPPING[i].source).scaled(ICON_SIZE, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
}
// Set default style sheet
ui->messagesWidget->document()->setDefaultStyleSheet(
"table { }"
"td.time { color: #808080; padding-top: 3px; } "
"td.cmd-request { color: #006060; } "
"td.cmd-error { color: red; } "
"b { color: #006060; } "
);
message(CMD_REPLY, (tr("Welcome to the Dogecoin Core RPC console.") + "<br>" +
tr("Use up and down arrows to navigate history, and <b>Ctrl-L</b> to clear screen.") + "<br>" +
tr("Type <b>help</b> for an overview of available commands.")), true);
}
void RPCConsole::keyPressEvent(QKeyEvent *event)
{
if(windowType() != Qt::Widget && event->key() == Qt::Key_Escape)
{
close();
}
}
void RPCConsole::message(int category, const QString &message, bool html)
{
QTime time = QTime::currentTime();
QString timeString = time.toString();
QString out;
out += "<table><tr><td class=\"time\" width=\"65\">" + timeString + "</td>";
out += "<td class=\"icon\" width=\"32\"><img src=\"" + categoryClass(category) + "\"></td>";
out += "<td class=\"message " + categoryClass(category) + "\" valign=\"middle\">";
if(html)
out += message;
else
out += GUIUtil::HtmlEscape(message, true);
out += "</td></tr></table>";
ui->messagesWidget->append(out);
}
void RPCConsole::setNumConnections(int count)
{
if (!clientModel)
return;
QString connections = QString::number(count) + " (";
connections += tr("In:") + " " + QString::number(clientModel->getNumConnections(CONNECTIONS_IN)) + " / ";
connections += tr("Out:") + " " + QString::number(clientModel->getNumConnections(CONNECTIONS_OUT)) + ")";
ui->numberOfConnections->setText(connections);
}
void RPCConsole::setNumBlocks(int count, const QDateTime& blockDate)
{
ui->numberOfBlocks->setText(QString::number(count));
ui->lastBlockTime->setText(blockDate.toString());
}
void RPCConsole::on_lineEdit_returnPressed()
{
QString cmd = ui->lineEdit->text();
ui->lineEdit->clear();
if(!cmd.isEmpty())
{
message(CMD_REQUEST, cmd);
emit cmdRequest(cmd);
// Remove command, if already in history
history.removeOne(cmd);
// Append command to history
history.append(cmd);
// Enforce maximum history size
while(history.size() > CONSOLE_HISTORY)
history.removeFirst();
// Set pointer to end of history
historyPtr = history.size();
// Scroll console view to end
scrollToEnd();
}
}
void RPCConsole::browseHistory(int offset)
{
historyPtr += offset;
if(historyPtr < 0)
historyPtr = 0;
if(historyPtr > history.size())
historyPtr = history.size();
QString cmd;
if(historyPtr < history.size())
cmd = history.at(historyPtr);
ui->lineEdit->setText(cmd);
}
void RPCConsole::startExecutor()
{
QThread *thread = new QThread;
RPCExecutor *executor = new RPCExecutor();
executor->moveToThread(thread);
// Replies from executor object must go to this object
connect(executor, SIGNAL(reply(int,QString)), this, SLOT(message(int,QString)));
// Requests from this object must go to executor
connect(this, SIGNAL(cmdRequest(QString)), executor, SLOT(request(QString)));
// On stopExecutor signal
// - queue executor for deletion (in execution thread)
// - quit the Qt event loop in the execution thread
connect(this, SIGNAL(stopExecutor()), executor, SLOT(deleteLater()));
connect(this, SIGNAL(stopExecutor()), thread, SLOT(quit()));
// Queue the thread for deletion (in this thread) when it is finished
connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
// Default implementation of QThread::run() simply spins up an event loop in the thread,
// which is what we want.
thread->start();
}
void RPCConsole::on_tabWidget_currentChanged(int index)
{
if(ui->tabWidget->widget(index) == ui->tab_console)
{
ui->lineEdit->setFocus();
}
}
void RPCConsole::on_openDebugLogfileButton_clicked()
{
GUIUtil::openDebugLogfile();
}
void RPCConsole::scrollToEnd()
{
QScrollBar *scrollbar = ui->messagesWidget->verticalScrollBar();
scrollbar->setValue(scrollbar->maximum());
}
void RPCConsole::on_sldGraphRange_valueChanged(int value)
{
const int multiplier = 5; // each position on the slider represents 5 min
int mins = value * multiplier;
setTrafficGraphRange(mins);
}
QString RPCConsole::FormatBytes(quint64 bytes)
{
if(bytes < 1024)
return QString(tr("%1 B")).arg(bytes);
if(bytes < 1024 * 1024)
return QString(tr("%1 KB")).arg(bytes / 1024);
if(bytes < 1024 * 1024 * 1024)
return QString(tr("%1 MB")).arg(bytes / 1024 / 1024);
return QString(tr("%1 GB")).arg(bytes / 1024 / 1024 / 1024);
}
void RPCConsole::setTrafficGraphRange(int mins)
{
ui->trafficGraph->setGraphRangeMins(mins);
ui->lblGraphRange->setText(GUIUtil::formatDurationStr(mins * 60));
}
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)
{
Q_UNUSED(deselected);
if (!clientModel || selected.indexes().isEmpty())
return;
const CNodeCombinedStats *stats = clientModel->getPeerTableModel()->getNodeStats(selected.indexes().first().row());
if (stats)
updateNodeDetail(stats);
}
void RPCConsole::peerLayoutChanged()
{
if (!clientModel)
return;
const CNodeCombinedStats *stats = NULL;
bool fUnselect = false;
bool fReselect = false;
if (cachedNodeid == -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(cachedNodeid);
if (detailNodeRow < 0)
{
// detail node dissapeared from table (node disconnected)
fUnselect = true;
cachedNodeid = -1;
ui->detailWidget->hide();
ui->peerHeading->setText(tr("Select a peer to view detailed information."));
}
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);
}
void RPCConsole::updateNodeDetail(const CNodeCombinedStats *stats)
{
// Update cached nodeid
cachedNodeid = stats->nodeStats.nodeid;
// update the detail ui with latest node information
QString peerAddrDetails(QString::fromStdString(stats->nodeStats.addrName));
if (!stats->nodeStats.addrLocal.empty())
peerAddrDetails += "<br />" + tr("via %1").arg(QString::fromStdString(stats->nodeStats.addrLocal));
ui->peerHeading->setText(peerAddrDetails);
ui->peerServices->setText(GUIUtil::formatServicesStr(stats->nodeStats.nServices));
ui->peerLastSend->setText(stats->nodeStats.nLastSend ? GUIUtil::formatDurationStr(GetTime() - stats->nodeStats.nLastSend) : tr("never"));
ui->peerLastRecv->setText(stats->nodeStats.nLastRecv ? GUIUtil::formatDurationStr(GetTime() - stats->nodeStats.nLastRecv) : tr("never"));
ui->peerBytesSent->setText(FormatBytes(stats->nodeStats.nSendBytes));
ui->peerBytesRecv->setText(FormatBytes(stats->nodeStats.nRecvBytes));
ui->peerConnTime->setText(GUIUtil::formatDurationStr(GetTime() - stats->nodeStats.nTimeConnected));
ui->peerPingTime->setText(GUIUtil::formatPingTime(stats->nodeStats.dPingTime));
ui->timeoffset->setText(GUIUtil::formatTimeOffset(stats->nodeStats.nTimeOffset));
ui->peerVersion->setText(QString("%1").arg(stats->nodeStats.nVersion));
ui->peerSubversion->setText(QString::fromStdString(stats->nodeStats.cleanSubVer));
ui->peerDirection->setText(stats->nodeStats.fInbound ? tr("Inbound") : tr("Outbound"));
ui->peerHeight->setText(QString("%1").arg(stats->nodeStats.nStartingHeight));
// This check fails for example if the lock was busy and
// nodeStateStats couldn't be fetched.
if (stats->fNodeStateStatsAvailable) {
// Ban score is init to 0
ui->peerBanScore->setText(QString("%1").arg(stats->nodeStateStats.nMisbehavior));
// Sync height is init to -1
if (stats->nodeStateStats.nSyncHeight > -1)
ui->peerSyncHeight->setText(QString("%1").arg(stats->nodeStateStats.nSyncHeight));
else
ui->peerSyncHeight->setText(tr("Unknown"));
} else {
ui->peerBanScore->setText(tr("Fetching..."));
ui->peerSyncHeight->setText(tr("Fetching..."));
}
ui->detailWidget->show();
}
void RPCConsole::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
}
void RPCConsole::showEvent(QShowEvent *event)
{
QWidget::showEvent(event);
if (!clientModel)
return;
// start PeerTableModel auto refresh
clientModel->getPeerTableModel()->startAutoRefresh();
}
void RPCConsole::hideEvent(QHideEvent *event)
{
QWidget::hideEvent(event);
if (!clientModel)
return;
// stop PeerTableModel auto refresh
clientModel->getPeerTableModel()->stopAutoRefresh();
}