]> Git Repo - VerusCoin.git/blame - src/qt/rpcconsole.cpp
qt: add license header to source files
[VerusCoin.git] / src / qt / rpcconsole.cpp
CommitLineData
e592d43f
WL
1// Copyright (c) 2011-2013 The Bitcoin developers
2// Distributed under the MIT/X11 software license, see the accompanying
3// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
460c51fd
WL
5#include "rpcconsole.h"
6#include "ui_rpcconsole.h"
7
8#include "clientmodel.h"
9#include "bitcoinrpc.h"
10#include "guiutil.h"
11
12#include <QTime>
460c51fd 13#include <QThread>
460c51fd 14#include <QKeyEvent>
25c0cce7 15#if QT_VERSION < 0x050000
c6aa86af 16#include <QUrl>
25c0cce7 17#endif
5a060b8d 18#include <QScrollBar>
460c51fd 19
c7441658 20#include <openssl/crypto.h>
460c51fd 21
af7b88f2 22// TODO: add a scrollback limit, as there is currently none
460c51fd
WL
23// TODO: make it possible to filter out categories (esp debug messages when implemented)
24// TODO: receive errors and debug messages through ClientModel
25
460c51fd 26const int CONSOLE_HISTORY = 50;
c6aa86af
WL
27const QSize ICON_SIZE(24, 24);
28
ce14345a
SE
29const int INITIAL_TRAFFIC_GRAPH_MINS = 30;
30
c6aa86af
WL
31const struct {
32 const char *url;
33 const char *source;
34} ICON_MAPPING[] = {
35 {"cmd-request", ":/icons/tx_input"},
36 {"cmd-reply", ":/icons/tx_output"},
37 {"cmd-error", ":/icons/tx_output"},
38 {"misc", ":/icons/tx_inout"},
39 {NULL, NULL}
40};
41
460c51fd
WL
42/* Object for executing console RPC commands in a separate thread.
43*/
32af5266 44class RPCExecutor : public QObject
460c51fd
WL
45{
46 Q_OBJECT
32af5266 47
460c51fd 48public slots:
460c51fd 49 void request(const QString &command);
32af5266 50
460c51fd
WL
51signals:
52 void reply(int category, const QString &command);
53};
54
55#include "rpcconsole.moc"
56
576b5efe
WL
57/**
58 * Split shell command line into a list of arguments. Aims to emulate \c bash and friends.
9c94bdac 59 *
576b5efe
WL
60 * - Arguments are delimited with whitespace
61 * - Extra whitespace at the beginning and end and between arguments will be ignored
9c94bdac
WL
62 * - Text can be "double" or 'single' quoted
63 * - The backslash \c \ is used as escape character
576b5efe 64 * - Outside quotes, any character can be escaped
9c94bdac
WL
65 * - Within double quotes, only escape \c " and backslashes before a \c " or another backslash
66 * - Within single quotes, no escaping is possible and no special interpretation takes place
576b5efe
WL
67 *
68 * @param[out] args Parsed arguments will be appended to this list
69 * @param[in] strCommand Command line to split
70 */
71bool parseCommandLine(std::vector<std::string> &args, const std::string &strCommand)
460c51fd 72{
576b5efe
WL
73 enum CmdParseState
74 {
75 STATE_EATING_SPACES,
76 STATE_ARGUMENT,
77 STATE_SINGLEQUOTED,
78 STATE_DOUBLEQUOTED,
79 STATE_ESCAPE_OUTER,
576b5efe
WL
80 STATE_ESCAPE_DOUBLEQUOTED
81 } state = STATE_EATING_SPACES;
82 std::string curarg;
83 foreach(char ch, strCommand)
84 {
85 switch(state)
ae744c8b 86 {
9c94bdac
WL
87 case STATE_ARGUMENT: // In or after argument
88 case STATE_EATING_SPACES: // Handle runs of whitespace
576b5efe
WL
89 switch(ch)
90 {
91 case '"': state = STATE_DOUBLEQUOTED; break;
92 case '\'': state = STATE_SINGLEQUOTED; break;
93 case '\\': state = STATE_ESCAPE_OUTER; break;
94 case ' ': case '\n': case '\t':
95 if(state == STATE_ARGUMENT) // Space ends argument
96 {
97 args.push_back(curarg);
98 curarg.clear();
99 }
100 state = STATE_EATING_SPACES;
101 break;
102 default: curarg += ch; state = STATE_ARGUMENT;
103 }
104 break;
105 case STATE_SINGLEQUOTED: // Single-quoted string
106 switch(ch)
107 {
108 case '\'': state = STATE_ARGUMENT; break;
576b5efe
WL
109 default: curarg += ch;
110 }
111 break;
112 case STATE_DOUBLEQUOTED: // Double-quoted string
113 switch(ch)
114 {
115 case '"': state = STATE_ARGUMENT; break;
116 case '\\': state = STATE_ESCAPE_DOUBLEQUOTED; break;
117 default: curarg += ch;
118 }
119 break;
120 case STATE_ESCAPE_OUTER: // '\' outside quotes
121 curarg += ch; state = STATE_ARGUMENT;
122 break;
576b5efe 123 case STATE_ESCAPE_DOUBLEQUOTED: // '\' in double-quoted text
9c94bdac 124 if(ch != '"' && ch != '\\') curarg += '\\'; // keep '\' for everything but the quote and '\' itself
576b5efe
WL
125 curarg += ch; state = STATE_DOUBLEQUOTED;
126 break;
ae744c8b
WL
127 }
128 }
576b5efe 129 switch(state) // final state
460c51fd 130 {
576b5efe
WL
131 case STATE_EATING_SPACES:
132 return true;
133 case STATE_ARGUMENT:
134 args.push_back(curarg);
135 return true;
136 default: // ERROR to end in one of the other states
137 return false;
460c51fd 138 }
576b5efe 139}
460c51fd 140
576b5efe
WL
141void RPCExecutor::request(const QString &command)
142{
143 std::vector<std::string> args;
144 if(!parseCommandLine(args, command.toStdString()))
145 {
146 emit reply(RPCConsole::CMD_ERROR, QString("Parse error: unbalanced ' or \""));
147 return;
148 }
149 if(args.empty())
150 return; // Nothing to do
b5c1467a
WL
151 try
152 {
460c51fd 153 std::string strPrint;
576b5efe
WL
154 // Convert argument list to JSON objects in method-dependent way,
155 // and pass it along with the method name to the dispatcher.
156 json_spirit::Value result = tableRPC.execute(
157 args[0],
158 RPCConvertValues(args[0], std::vector<std::string>(args.begin() + 1, args.end())));
460c51fd
WL
159
160 // Format result reply
161 if (result.type() == json_spirit::null_type)
162 strPrint = "";
163 else if (result.type() == json_spirit::str_type)
164 strPrint = result.get_str();
165 else
0db9a805 166 strPrint = write_string(result, true);
460c51fd
WL
167
168 emit reply(RPCConsole::CMD_REPLY, QString::fromStdString(strPrint));
169 }
170 catch (json_spirit::Object& objError)
171 {
b5c1467a
WL
172 try // Nice formatting for standard-format error
173 {
174 int code = find_value(objError, "code").get_int();
175 std::string message = find_value(objError, "message").get_str();
176 emit reply(RPCConsole::CMD_ERROR, QString::fromStdString(message) + " (code " + QString::number(code) + ")");
177 }
178 catch(std::runtime_error &) // raised when converting to invalid type, i.e. missing code or message
9c94bdac 179 { // Show raw JSON object
0db9a805 180 emit reply(RPCConsole::CMD_ERROR, QString::fromStdString(write_string(json_spirit::Value(objError), false)));
b5c1467a 181 }
460c51fd
WL
182 }
183 catch (std::exception& e)
184 {
185 emit reply(RPCConsole::CMD_ERROR, QString("Error: ") + QString::fromStdString(e.what()));
186 }
187}
188
189RPCConsole::RPCConsole(QWidget *parent) :
190 QDialog(parent),
191 ui(new Ui::RPCConsole),
bfad9982 192 clientModel(0),
460c51fd
WL
193 historyPtr(0)
194{
195 ui->setupUi(this);
c431e9f1 196 GUIUtil::restoreWindowGeometry("nRPCConsoleWindow", this->size(), this);
460c51fd 197
81605d90 198#ifndef Q_OS_MAC
a3b4caac 199 ui->openDebugLogfileButton->setIcon(QIcon(":/icons/export"));
5d6b3027
PK
200 ui->showCLOptionsButton->setIcon(QIcon(":/icons/options"));
201#endif
202
460c51fd
WL
203 // Install event filter for up and down arrow
204 ui->lineEdit->installEventFilter(this);
62904b33 205 ui->messagesWidget->installEventFilter(this);
460c51fd 206
460c51fd
WL
207 connect(ui->clearButton, SIGNAL(clicked()), this, SLOT(clear()));
208
c7441658
PK
209 // set OpenSSL version label
210 ui->openSSLVersion->setText(SSLeay_version(SSLEAY_VERSION));
211
460c51fd 212 startExecutor();
ce14345a 213 setTrafficGraphRange(INITIAL_TRAFFIC_GRAPH_MINS);
460c51fd
WL
214
215 clear();
216}
217
218RPCConsole::~RPCConsole()
219{
c431e9f1 220 GUIUtil::saveWindowGeometry("nRPCConsoleWindow", this);
460c51fd
WL
221 emit stopExecutor();
222 delete ui;
223}
224
460c51fd
WL
225bool RPCConsole::eventFilter(QObject* obj, QEvent *event)
226{
62904b33 227 if(event->type() == QEvent::KeyPress) // Special key handling
460c51fd 228 {
62904b33
WL
229 QKeyEvent *keyevt = static_cast<QKeyEvent*>(event);
230 int key = keyevt->key();
231 Qt::KeyboardModifiers mod = keyevt->modifiers();
232 switch(key)
460c51fd 233 {
62904b33
WL
234 case Qt::Key_Up: if(obj == ui->lineEdit) { browseHistory(-1); return true; } break;
235 case Qt::Key_Down: if(obj == ui->lineEdit) { browseHistory(1); return true; } break;
236 case Qt::Key_PageUp: /* pass paging keys to messages widget */
237 case Qt::Key_PageDown:
238 if(obj == ui->lineEdit)
460c51fd 239 {
62904b33
WL
240 QApplication::postEvent(ui->messagesWidget, new QKeyEvent(*keyevt));
241 return true;
242 }
243 break;
244 default:
245 // Typing in messages widget brings focus to line edit, and redirects key there
246 // Exclude most combinations and keys that emit no text, except paste shortcuts
247 if(obj == ui->messagesWidget && (
248 (!mod && !keyevt->text().isEmpty() && key != Qt::Key_Tab) ||
249 ((mod & Qt::ControlModifier) && key == Qt::Key_V) ||
250 ((mod & Qt::ShiftModifier) && key == Qt::Key_Insert)))
251 {
252 ui->lineEdit->setFocus();
253 QApplication::postEvent(ui->lineEdit, new QKeyEvent(*keyevt));
254 return true;
460c51fd
WL
255 }
256 }
257 }
258 return QDialog::eventFilter(obj, event);
259}
260
261void RPCConsole::setClientModel(ClientModel *model)
262{
ce14345a
SE
263 clientModel = model;
264 ui->trafficGraph->setClientModel(model);
460c51fd
WL
265 if(model)
266 {
1fc57d56
PK
267 // Keep up to date with client
268 setNumConnections(model->getNumConnections());
460c51fd 269 connect(model, SIGNAL(numConnectionsChanged(int)), this, SLOT(setNumConnections(int)));
1fc57d56
PK
270
271 setNumBlocks(model->getNumBlocks(), model->getNumBlocksOfPeers());
fe4a6550 272 connect(model, SIGNAL(numBlocksChanged(int,int)), this, SLOT(setNumBlocks(int,int)));
460c51fd 273
ce14345a
SE
274 updateTrafficStats(model->getTotalBytesRecv(), model->getTotalBytesSent());
275 connect(model, SIGNAL(bytesChanged(quint64,quint64)), this, SLOT(updateTrafficStats(quint64, quint64)));
276
460c51fd
WL
277 // Provide initial values
278 ui->clientVersion->setText(model->formatFullVersion());
279 ui->clientName->setText(model->clientName());
280 ui->buildDate->setText(model->formatBuildDate());
41c6b8ab 281 ui->startupTime->setText(model->formatClientStartupTime());
460c51fd 282
460c51fd 283 ui->isTestNet->setChecked(model->isTestNet());
460c51fd
WL
284 }
285}
286
c6aa86af 287static QString categoryClass(int category)
460c51fd
WL
288{
289 switch(category)
290 {
c6aa86af
WL
291 case RPCConsole::CMD_REQUEST: return "cmd-request"; break;
292 case RPCConsole::CMD_REPLY: return "cmd-reply"; break;
293 case RPCConsole::CMD_ERROR: return "cmd-error"; break;
294 default: return "misc";
460c51fd
WL
295 }
296}
297
298void RPCConsole::clear()
299{
300 ui->messagesWidget->clear();
af7b88f2
PK
301 history.clear();
302 historyPtr = 0;
460c51fd
WL
303 ui->lineEdit->clear();
304 ui->lineEdit->setFocus();
305
c6aa86af
WL
306 // Add smoothly scaled icon images.
307 // (when using width/height on an img, Qt uses nearest instead of linear interpolation)
308 for(int i=0; ICON_MAPPING[i].url; ++i)
309 {
310 ui->messagesWidget->document()->addResource(
311 QTextDocument::ImageResource,
312 QUrl(ICON_MAPPING[i].url),
313 QImage(ICON_MAPPING[i].source).scaled(ICON_SIZE, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
314 }
315
316 // Set default style sheet
317 ui->messagesWidget->document()->setDefaultStyleSheet(
318 "table { }"
319 "td.time { color: #808080; padding-top: 3px; } "
320 "td.message { font-family: Monospace; font-size: 12px; } "
321 "td.cmd-request { color: #006060; } "
322 "td.cmd-error { color: red; } "
323 "b { color: #006060; } "
324 );
325
8b4d6536
PK
326 message(CMD_REPLY, (tr("Welcome to the Bitcoin RPC console.") + "<br>" +
327 tr("Use up and down arrows to navigate history, and <b>Ctrl-L</b> to clear screen.") + "<br>" +
328 tr("Type <b>help</b> for an overview of available commands.")), true);
460c51fd
WL
329}
330
c6aa86af 331void RPCConsole::message(int category, const QString &message, bool html)
460c51fd 332{
460c51fd 333 QTime time = QTime::currentTime();
c6aa86af
WL
334 QString timeString = time.toString();
335 QString out;
336 out += "<table><tr><td class=\"time\" width=\"65\">" + timeString + "</td>";
337 out += "<td class=\"icon\" width=\"32\"><img src=\"" + categoryClass(category) + "\"></td>";
338 out += "<td class=\"message " + categoryClass(category) + "\" valign=\"middle\">";
339 if(html)
340 out += message;
341 else
342 out += GUIUtil::HtmlEscape(message, true);
343 out += "</td></tr></table>";
344 ui->messagesWidget->append(out);
460c51fd
WL
345}
346
347void RPCConsole::setNumConnections(int count)
348{
349 ui->numberOfConnections->setText(QString::number(count));
350}
351
fe4a6550 352void RPCConsole::setNumBlocks(int count, int countOfPeers)
460c51fd
WL
353{
354 ui->numberOfBlocks->setText(QString::number(count));
54413aab
PK
355 // If there is no current countOfPeers available display N/A instead of 0, which can't ever be true
356 ui->totalBlocks->setText(countOfPeers == 0 ? tr("N/A") : QString::number(countOfPeers));
460c51fd 357 if(clientModel)
460c51fd 358 ui->lastBlockTime->setText(clientModel->getLastBlockDate().toString());
460c51fd
WL
359}
360
361void RPCConsole::on_lineEdit_returnPressed()
362{
363 QString cmd = ui->lineEdit->text();
364 ui->lineEdit->clear();
365
366 if(!cmd.isEmpty())
367 {
368 message(CMD_REQUEST, cmd);
369 emit cmdRequest(cmd);
370 // Truncate history from current position
371 history.erase(history.begin() + historyPtr, history.end());
372 // Append command to history
373 history.append(cmd);
374 // Enforce maximum history size
375 while(history.size() > CONSOLE_HISTORY)
376 history.removeFirst();
377 // Set pointer to end of history
378 historyPtr = history.size();
5a060b8d
WL
379 // Scroll console view to end
380 scrollToEnd();
460c51fd
WL
381 }
382}
383
384void RPCConsole::browseHistory(int offset)
385{
386 historyPtr += offset;
387 if(historyPtr < 0)
388 historyPtr = 0;
389 if(historyPtr > history.size())
390 historyPtr = history.size();
391 QString cmd;
392 if(historyPtr < history.size())
393 cmd = history.at(historyPtr);
394 ui->lineEdit->setText(cmd);
395}
396
397void RPCConsole::startExecutor()
398{
bfad9982 399 QThread *thread = new QThread;
460c51fd
WL
400 RPCExecutor *executor = new RPCExecutor();
401 executor->moveToThread(thread);
402
460c51fd
WL
403 // Replies from executor object must go to this object
404 connect(executor, SIGNAL(reply(int,QString)), this, SLOT(message(int,QString)));
405 // Requests from this object must go to executor
406 connect(this, SIGNAL(cmdRequest(QString)), executor, SLOT(request(QString)));
bfad9982 407
460c51fd
WL
408 // On stopExecutor signal
409 // - queue executor for deletion (in execution thread)
410 // - quit the Qt event loop in the execution thread
411 connect(this, SIGNAL(stopExecutor()), executor, SLOT(deleteLater()));
412 connect(this, SIGNAL(stopExecutor()), thread, SLOT(quit()));
413 // Queue the thread for deletion (in this thread) when it is finished
414 connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
415
416 // Default implementation of QThread::run() simply spins up an event loop in the thread,
417 // which is what we want.
418 thread->start();
419}
420
b8417243
WL
421void RPCConsole::on_tabWidget_currentChanged(int index)
422{
423 if(ui->tabWidget->widget(index) == ui->tab_console)
424 {
b8417243
WL
425 ui->lineEdit->setFocus();
426 }
427}
4d3dda5d
PK
428
429void RPCConsole::on_openDebugLogfileButton_clicked()
430{
431 GUIUtil::openDebugLogfile();
432}
5a060b8d
WL
433
434void RPCConsole::scrollToEnd()
435{
436 QScrollBar *scrollbar = ui->messagesWidget->verticalScrollBar();
437 scrollbar->setValue(scrollbar->maximum());
438}
5d6b3027
PK
439
440void RPCConsole::on_showCLOptionsButton_clicked()
441{
442 GUIUtil::HelpMessageBox help;
443 help.exec();
444}
ce14345a
SE
445
446void RPCConsole::on_sldGraphRange_valueChanged(int value)
447{
448 const int multiplier = 5; // each position on the slider represents 5 min
449 int mins = value * multiplier;
450 setTrafficGraphRange(mins);
451}
452
453QString RPCConsole::FormatBytes(quint64 bytes)
454{
455 if(bytes < 1024)
456 return QString(tr("%1 B")).arg(bytes);
457 if(bytes < 1024 * 1024)
458 return QString(tr("%1 KB")).arg(bytes / 1024);
459 if(bytes < 1024 * 1024 * 1024)
460 return QString(tr("%1 MB")).arg(bytes / 1024 / 1024);
461
462 return QString(tr("%1 GB")).arg(bytes / 1024 / 1024 / 1024);
463}
464
465void RPCConsole::setTrafficGraphRange(int mins)
466{
467 ui->trafficGraph->setGraphRangeMins(mins);
468 if(mins < 60) {
469 ui->lblGraphRange->setText(QString(tr("%1 m")).arg(mins));
470 } else {
471 int hours = mins / 60;
472 int minsLeft = mins % 60;
473 if(minsLeft == 0) {
474 ui->lblGraphRange->setText(QString(tr("%1 h")).arg(hours));
475 } else {
476 ui->lblGraphRange->setText(QString(tr("%1 h %2 m")).arg(hours).arg(minsLeft));
477 }
478 }
479}
480
481void RPCConsole::updateTrafficStats(quint64 totalBytesIn, quint64 totalBytesOut)
482{
483 ui->lblBytesIn->setText(FormatBytes(totalBytesIn));
484 ui->lblBytesOut->setText(FormatBytes(totalBytesOut));
485}
486
487void RPCConsole::on_btnClearTrafficGraph_clicked()
488{
489 ui->trafficGraph->clear();
490}
This page took 0.191439 seconds and 4 git commands to generate.