mirror of
https://github.com/Retropex/bitcoin.git
synced 2025-06-04 00:12:33 +02:00

Try to display a nicer message instead of dumping raw JSON object when possible. If the error somehow doesn't have the required 'code' and 'message' fields, fall back to printing raw JSON object.
425 lines
13 KiB
C++
425 lines
13 KiB
C++
#include "rpcconsole.h"
|
|
#include "ui_rpcconsole.h"
|
|
|
|
#include "clientmodel.h"
|
|
#include "bitcoinrpc.h"
|
|
#include "guiutil.h"
|
|
|
|
#include <QTime>
|
|
#include <QTimer>
|
|
#include <QThread>
|
|
#include <QTextEdit>
|
|
#include <QKeyEvent>
|
|
#include <QUrl>
|
|
#include <QScrollBar>
|
|
|
|
#include <openssl/crypto.h>
|
|
|
|
// TODO: make it possible to filter out categories (esp debug messages when implemented)
|
|
// TODO: receive errors and debug messages through ClientModel
|
|
|
|
const int CONSOLE_SCROLLBACK = 50;
|
|
const int CONSOLE_HISTORY = 50;
|
|
|
|
const QSize ICON_SIZE(24, 24);
|
|
|
|
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 start();
|
|
void request(const QString &command);
|
|
signals:
|
|
void reply(int category, const QString &command);
|
|
};
|
|
|
|
#include "rpcconsole.moc"
|
|
|
|
void RPCExecutor::start()
|
|
{
|
|
// Nothing to do
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* - Arguments can be "double" or 'single' quoted. Those are treated the same.
|
|
* - The backslash '\' is used as escape character
|
|
* - Outside quotes, any character can be escaped
|
|
* - Within double quotes, only escape double quotes with \" and backslashes with \\
|
|
* - Within single quotes, only escape single quotes with \' and backslashes with \\
|
|
*
|
|
* @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_SINGLEQUOTED,
|
|
STATE_ESCAPE_DOUBLEQUOTED
|
|
} state = STATE_EATING_SPACES;
|
|
std::string curarg;
|
|
foreach(char ch, strCommand)
|
|
{
|
|
switch(state)
|
|
{
|
|
case STATE_ARGUMENT: // After argument
|
|
case STATE_EATING_SPACES: // Handle runs of spaces
|
|
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;
|
|
case '\\': state = STATE_ESCAPE_SINGLEQUOTED; 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_SINGLEQUOTED: // '\' in single-quoted text
|
|
if(ch != '\'') curarg += '\\'; // keep '\' for everything but the quote
|
|
curarg += ch; state = STATE_SINGLEQUOTED;
|
|
break;
|
|
case STATE_ESCAPE_DOUBLEQUOTED: // '\' in double-quoted text
|
|
if(ch != '"') curarg += '\\'; // keep '\' for everything but the quote
|
|
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),
|
|
historyPtr(0)
|
|
{
|
|
ui->setupUi(this);
|
|
|
|
#ifndef Q_WS_MAC
|
|
ui->openDebugLogfileButton->setIcon(QIcon(":/icons/export"));
|
|
ui->showCLOptionsButton->setIcon(QIcon(":/icons/options"));
|
|
#endif
|
|
|
|
// Install event filter for up and down arrow
|
|
ui->lineEdit->installEventFilter(this);
|
|
|
|
connect(ui->clearButton, SIGNAL(clicked()), this, SLOT(clear()));
|
|
|
|
// set OpenSSL version label
|
|
ui->openSSLVersion->setText(SSLeay_version(SSLEAY_VERSION));
|
|
|
|
startExecutor();
|
|
|
|
clear();
|
|
}
|
|
|
|
RPCConsole::~RPCConsole()
|
|
{
|
|
emit stopExecutor();
|
|
delete ui;
|
|
}
|
|
|
|
bool RPCConsole::eventFilter(QObject* obj, QEvent *event)
|
|
{
|
|
if(obj == ui->lineEdit)
|
|
{
|
|
if(event->type() == QEvent::KeyPress)
|
|
{
|
|
QKeyEvent *key = static_cast<QKeyEvent*>(event);
|
|
switch(key->key())
|
|
{
|
|
case Qt::Key_Up: browseHistory(-1); return true;
|
|
case Qt::Key_Down: browseHistory(1); return true;
|
|
}
|
|
}
|
|
}
|
|
return QDialog::eventFilter(obj, event);
|
|
}
|
|
|
|
void RPCConsole::setClientModel(ClientModel *model)
|
|
{
|
|
this->clientModel = model;
|
|
if(model)
|
|
{
|
|
// Subscribe to information, replies, messages, errors
|
|
connect(model, SIGNAL(numConnectionsChanged(int)), this, SLOT(setNumConnections(int)));
|
|
connect(model, SIGNAL(numBlocksChanged(int,int)), this, SLOT(setNumBlocks(int,int)));
|
|
|
|
// Provide initial values
|
|
ui->clientVersion->setText(model->formatFullVersion());
|
|
ui->clientName->setText(model->clientName());
|
|
ui->buildDate->setText(model->formatBuildDate());
|
|
ui->startupTime->setText(model->formatClientStartupTime());
|
|
|
|
setNumConnections(model->getNumConnections());
|
|
ui->isTestNet->setChecked(model->isTestNet());
|
|
|
|
setNumBlocks(model->getNumBlocks(), model->getNumBlocksOfPeers());
|
|
}
|
|
}
|
|
|
|
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();
|
|
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; } "
|
|
"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::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)
|
|
{
|
|
ui->numberOfConnections->setText(QString::number(count));
|
|
}
|
|
|
|
void RPCConsole::setNumBlocks(int count, int countOfPeers)
|
|
{
|
|
ui->numberOfBlocks->setText(QString::number(count));
|
|
ui->totalBlocks->setText(QString::number(countOfPeers));
|
|
if(clientModel)
|
|
{
|
|
// If there is no current number available display N/A instead of 0, which can't ever be true
|
|
ui->totalBlocks->setText(clientModel->getNumBlocksOfPeers() == 0 ? tr("N/A") : QString::number(clientModel->getNumBlocksOfPeers()));
|
|
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);
|
|
|
|
// Notify executor when thread started (in executor thread)
|
|
connect(thread, SIGNAL(started()), executor, SLOT(start()));
|
|
// 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_showCLOptionsButton_clicked()
|
|
{
|
|
GUIUtil::HelpMessageBox help;
|
|
help.exec();
|
|
}
|