]>
Commit | Line | Data |
---|---|---|
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 | 26 | const int CONSOLE_HISTORY = 50; |
c6aa86af WL |
27 | const QSize ICON_SIZE(24, 24); |
28 | ||
ce14345a SE |
29 | const int INITIAL_TRAFFIC_GRAPH_MINS = 30; |
30 | ||
c6aa86af WL |
31 | const 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 | 44 | class RPCExecutor : public QObject |
460c51fd WL |
45 | { |
46 | Q_OBJECT | |
32af5266 | 47 | |
460c51fd | 48 | public slots: |
460c51fd | 49 | void request(const QString &command); |
32af5266 | 50 | |
460c51fd WL |
51 | signals: |
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 | */ | |
71 | bool 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 |
141 | void 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 | ||
189 | RPCConsole::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 | ||
218 | RPCConsole::~RPCConsole() | |
219 | { | |
c431e9f1 | 220 | GUIUtil::saveWindowGeometry("nRPCConsoleWindow", this); |
460c51fd WL |
221 | emit stopExecutor(); |
222 | delete ui; | |
223 | } | |
224 | ||
460c51fd WL |
225 | bool 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 | ||
261 | void 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 | 287 | static 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 | ||
298 | void 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 | 331 | void 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 | ||
347 | void RPCConsole::setNumConnections(int count) | |
348 | { | |
349 | ui->numberOfConnections->setText(QString::number(count)); | |
350 | } | |
351 | ||
fe4a6550 | 352 | void 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 | ||
361 | void 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 | ||
384 | void 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 | ||
397 | void 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 |
421 | void 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 | |
429 | void RPCConsole::on_openDebugLogfileButton_clicked() | |
430 | { | |
431 | GUIUtil::openDebugLogfile(); | |
432 | } | |
5a060b8d WL |
433 | |
434 | void RPCConsole::scrollToEnd() | |
435 | { | |
436 | QScrollBar *scrollbar = ui->messagesWidget->verticalScrollBar(); | |
437 | scrollbar->setValue(scrollbar->maximum()); | |
438 | } | |
5d6b3027 PK |
439 | |
440 | void RPCConsole::on_showCLOptionsButton_clicked() | |
441 | { | |
442 | GUIUtil::HelpMessageBox help; | |
443 | help.exec(); | |
444 | } | |
ce14345a SE |
445 | |
446 | void 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 | ||
453 | QString 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 | ||
465 | void 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 | ||
481 | void RPCConsole::updateTrafficStats(quint64 totalBytesIn, quint64 totalBytesOut) | |
482 | { | |
483 | ui->lblBytesIn->setText(FormatBytes(totalBytesIn)); | |
484 | ui->lblBytesOut->setText(FormatBytes(totalBytesOut)); | |
485 | } | |
486 | ||
487 | void RPCConsole::on_btnClearTrafficGraph_clicked() | |
488 | { | |
489 | ui->trafficGraph->clear(); | |
490 | } |