752ecec5cc
Generally useless information. Only updates on connect time, not after that. Peers can easily lie and the median filter is not effective in preventing that. In the past it was used for progress display in the GUI but `CheckPoints::guessVerificationProgress` provides a better way that is now used. It was too easy to mislead it. Peers do lie about it in practice, see issue #4065. From the RPC, `getpeerinfo` gives the peer raw values, which are more useful.
495 lines
16 KiB
C++
495 lines
16 KiB
C++
// Copyright (c) 2011-2014 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 "rpcconsole.h"
|
|
#include "ui_rpcconsole.h"
|
|
|
|
#include "clientmodel.h"
|
|
#include "guiutil.h"
|
|
|
|
#include "rpcserver.h"
|
|
#include "rpcclient.h"
|
|
|
|
#include "json/json_spirit_value.h"
|
|
#include <openssl/crypto.h>
|
|
#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 (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(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 (std::exception& e)
|
|
{
|
|
emit reply(RPCConsole::CMD_ERROR, QString("Error: ") + QString::fromStdString(e.what()));
|
|
}
|
|
}
|
|
|
|
RPCConsole::RPCConsole(QWidget *parent) :
|
|
QDialog(parent),
|
|
ui(new Ui::RPCConsole),
|
|
clientModel(0),
|
|
historyPtr(0)
|
|
{
|
|
ui->setupUi(this);
|
|
GUIUtil::restoreWindowGeometry("nRPCConsoleWindow", this->size(), this);
|
|
|
|
#ifndef Q_OS_MAC
|
|
ui->openDebugLogfileButton->setIcon(QIcon(":/icons/export"));
|
|
#endif
|
|
|
|
// 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 OpenSSL version label
|
|
ui->openSSLVersion->setText(SSLeay_version(SSLEAY_VERSION));
|
|
|
|
startExecutor();
|
|
setTrafficGraphRange(INITIAL_TRAFFIC_GRAPH_MINS);
|
|
|
|
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 QDialog::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());
|
|
connect(model, SIGNAL(numBlocksChanged(int)), this, SLOT(setNumBlocks(int)));
|
|
|
|
updateTrafficStats(model->getTotalBytesRecv(), model->getTotalBytesSent());
|
|
connect(model, SIGNAL(bytesChanged(quint64,quint64)), this, SLOT(updateTrafficStats(quint64, quint64)));
|
|
|
|
// 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(model->getNetworkName());
|
|
}
|
|
}
|
|
|
|
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),
|
|
QImage(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.message { font-family: monospace; font-size: 12px; } " // Todo: Remove fixed font-size
|
|
"td.cmd-request { color: #006060; } "
|
|
"td.cmd-error { color: red; } "
|
|
"b { color: #006060; } "
|
|
);
|
|
|
|
message(CMD_REPLY, (tr("Welcome to the Bitcoin 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::reject()
|
|
{
|
|
// Ignore escape keypress if this is not a seperate window
|
|
if(windowType() != Qt::Widget)
|
|
QDialog::reject();
|
|
}
|
|
|
|
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)
|
|
{
|
|
ui->numberOfBlocks->setText(QString::number(count));
|
|
if(clientModel)
|
|
ui->lastBlockTime->setText(clientModel->getLastBlockDate().toString());
|
|
}
|
|
|
|
void RPCConsole::on_lineEdit_returnPressed()
|
|
{
|
|
QString cmd = ui->lineEdit->text();
|
|
ui->lineEdit->clear();
|
|
|
|
if(!cmd.isEmpty())
|
|
{
|
|
message(CMD_REQUEST, cmd);
|
|
emit cmdRequest(cmd);
|
|
// Truncate history from current position
|
|
history.erase(history.begin() + historyPtr, history.end());
|
|
// 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);
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
|
|
void RPCConsole::updateTrafficStats(quint64 totalBytesIn, quint64 totalBytesOut)
|
|
{
|
|
ui->lblBytesIn->setText(FormatBytes(totalBytesIn));
|
|
ui->lblBytesOut->setText(FormatBytes(totalBytesOut));
|
|
}
|