Line data Source code
1 : // Copyright (c) 2011-2020 The Bitcoin Core developers
2 : // Distributed under the MIT software license, see the accompanying
3 : // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4 :
5 : #if defined(HAVE_CONFIG_H)
6 : #include <config/bitcoin-config.h>
7 : #endif
8 :
9 : #include <qt/rpcconsole.h>
10 : #include <qt/forms/ui_debugwindow.h>
11 :
12 : #include <qt/bantablemodel.h>
13 : #include <qt/clientmodel.h>
14 : #include <qt/platformstyle.h>
15 : #include <qt/walletmodel.h>
16 : #include <chainparams.h>
17 : #include <interfaces/node.h>
18 : #include <netbase.h>
19 : #include <rpc/server.h>
20 : #include <rpc/client.h>
21 : #include <util/strencodings.h>
22 : #include <util/system.h>
23 :
24 : #include <univalue.h>
25 :
26 : #ifdef ENABLE_WALLET
27 : #include <wallet/db.h>
28 : #include <wallet/wallet.h>
29 : #endif
30 :
31 : #include <QFont>
32 : #include <QKeyEvent>
33 : #include <QMenu>
34 : #include <QMessageBox>
35 : #include <QScreen>
36 : #include <QScrollBar>
37 : #include <QSettings>
38 : #include <QString>
39 : #include <QStringList>
40 : #include <QTime>
41 : #include <QTimer>
42 :
43 :
44 : const int CONSOLE_HISTORY = 50;
45 : const int INITIAL_TRAFFIC_GRAPH_MINS = 30;
46 : const QSize FONT_RANGE(4, 40);
47 : const char fontSizeSettingsKey[] = "consoleFontSize";
48 :
49 : const struct {
50 : const char *url;
51 : const char *source;
52 : } ICON_MAPPING[] = {
53 : {"cmd-request", ":/icons/tx_input"},
54 : {"cmd-reply", ":/icons/tx_output"},
55 : {"cmd-error", ":/icons/tx_output"},
56 : {"misc", ":/icons/tx_inout"},
57 : {nullptr, nullptr}
58 : };
59 :
60 : namespace {
61 :
62 : // don't add private key handling cmd's to the history
63 2 : const QStringList historyFilter = QStringList()
64 1 : << "importprivkey"
65 1 : << "importmulti"
66 1 : << "sethdseed"
67 1 : << "signmessagewithprivkey"
68 1 : << "signrawtransactionwithkey"
69 1 : << "walletpassphrase"
70 1 : << "walletpassphrasechange"
71 1 : << "encryptwallet";
72 :
73 : }
74 :
75 : /* Object for executing console RPC commands in a separate thread.
76 : */
77 0 : class RPCExecutor : public QObject
78 : {
79 : Q_OBJECT
80 : public:
81 0 : explicit RPCExecutor(interfaces::Node& node) : m_node(node) {}
82 :
83 : public Q_SLOTS:
84 : void request(const QString &command, const WalletModel* wallet_model);
85 :
86 : Q_SIGNALS:
87 : void reply(int category, const QString &command);
88 :
89 : private:
90 : interfaces::Node& m_node;
91 : };
92 :
93 : /** Class for handling RPC timers
94 : * (used for e.g. re-locking the wallet after a timeout)
95 : */
96 : class QtRPCTimerBase: public QObject, public RPCTimerBase
97 : {
98 : Q_OBJECT
99 : public:
100 0 : QtRPCTimerBase(std::function<void()>& _func, int64_t millis):
101 0 : func(_func)
102 0 : {
103 0 : timer.setSingleShot(true);
104 0 : connect(&timer, &QTimer::timeout, [this]{ func(); });
105 0 : timer.start(millis);
106 0 : }
107 0 : ~QtRPCTimerBase() {}
108 : private:
109 : QTimer timer;
110 : std::function<void()> func;
111 : };
112 :
113 0 : class QtRPCTimerInterface: public RPCTimerInterface
114 : {
115 : public:
116 0 : ~QtRPCTimerInterface() {}
117 0 : const char *Name() override { return "Qt"; }
118 0 : RPCTimerBase* NewTimer(std::function<void()>& func, int64_t millis) override
119 : {
120 0 : return new QtRPCTimerBase(func, millis);
121 0 : }
122 : };
123 :
124 :
125 : #include <qt/rpcconsole.moc>
126 :
127 : /**
128 : * Split shell command line into a list of arguments and optionally execute the command(s).
129 : * Aims to emulate \c bash and friends.
130 : *
131 : * - Command nesting is possible with parenthesis; for example: validateaddress(getnewaddress())
132 : * - Arguments are delimited with whitespace or comma
133 : * - Extra whitespace at the beginning and end and between arguments will be ignored
134 : * - Text can be "double" or 'single' quoted
135 : * - The backslash \c \ is used as escape character
136 : * - Outside quotes, any character can be escaped
137 : * - Within double quotes, only escape \c " and backslashes before a \c " or another backslash
138 : * - Within single quotes, no escaping is possible and no special interpretation takes place
139 : *
140 : * @param[in] node optional node to execute command on
141 : * @param[out] strResult stringified result from the executed command(chain)
142 : * @param[in] strCommand Command line to split
143 : * @param[in] fExecute set true if you want the command to be executed
144 : * @param[out] pstrFilteredOut Command line, filtered to remove any sensitive data
145 : */
146 :
147 41 : bool RPCConsole::RPCParseCommandLine(interfaces::Node* node, std::string &strResult, const std::string &strCommand, const bool fExecute, std::string * const pstrFilteredOut, const WalletModel* wallet_model)
148 : {
149 41 : std::vector< std::vector<std::string> > stack;
150 41 : stack.push_back(std::vector<std::string>());
151 :
152 : enum CmdParseState
153 : {
154 : STATE_EATING_SPACES,
155 0 : STATE_EATING_SPACES_IN_ARG,
156 : STATE_EATING_SPACES_IN_BRACKETS,
157 0 : STATE_ARGUMENT,
158 : STATE_SINGLEQUOTED,
159 : STATE_DOUBLEQUOTED,
160 : STATE_ESCAPE_OUTER,
161 0 : STATE_ESCAPE_DOUBLEQUOTED,
162 0 : STATE_COMMAND_EXECUTED,
163 0 : STATE_COMMAND_EXECUTED_INNER
164 1166 : } state = STATE_EATING_SPACES;
165 41 : std::string curarg;
166 41 : UniValue lastResult;
167 41 : unsigned nDepthInsideSensitive = 0;
168 41 : size_t filter_begin_pos = 0, chpos;
169 41 : std::vector<std::pair<size_t, size_t>> filter_ranges;
170 :
171 155 : auto add_to_current_stack = [&](const std::string& strArg) {
172 114 : if (stack.back().empty() && (!nDepthInsideSensitive) && historyFilter.contains(QString::fromStdString(strArg), Qt::CaseInsensitive)) {
173 11 : nDepthInsideSensitive = 1;
174 11 : filter_begin_pos = chpos;
175 11 : }
176 : // Make sure stack is not empty before adding something
177 114 : if (stack.empty()) {
178 0 : stack.push_back(std::vector<std::string>());
179 0 : }
180 114 : stack.back().push_back(strArg);
181 114 : };
182 :
183 82 : auto close_out_params = [&]() {
184 41 : if (nDepthInsideSensitive) {
185 15 : if (!--nDepthInsideSensitive) {
186 11 : assert(filter_begin_pos);
187 11 : filter_ranges.push_back(std::make_pair(filter_begin_pos, chpos));
188 11 : filter_begin_pos = 0;
189 11 : }
190 : }
191 41 : stack.pop_back();
192 41 : };
193 :
194 41 : std::string strCommandTerminated = strCommand;
195 41 : if (strCommandTerminated.back() != '\n')
196 40 : strCommandTerminated += "\n";
197 1166 : for (chpos = 0; chpos < strCommandTerminated.size(); ++chpos)
198 : {
199 1132 : char ch = strCommandTerminated[chpos];
200 1132 : switch(state)
201 : {
202 : case STATE_COMMAND_EXECUTED_INNER:
203 : case STATE_COMMAND_EXECUTED:
204 : {
205 : bool breakParsing = true;
206 79 : switch(ch)
207 : {
208 7 : case '[': curarg.clear(); state = STATE_COMMAND_EXECUTED_INNER; break;
209 : default:
210 72 : if (state == STATE_COMMAND_EXECUTED_INNER)
211 : {
212 34 : if (ch != ']')
213 : {
214 : // append char to the current argument (which is also used for the query command)
215 27 : curarg += ch;
216 : break;
217 : }
218 7 : if (curarg.size() && fExecute)
219 : {
220 : // if we have a value query, query arrays with index and objects with a string key
221 7 : UniValue subelement;
222 7 : if (lastResult.isArray())
223 : {
224 2 : for(char argch: curarg)
225 1 : if (!IsDigit(argch))
226 0 : throw std::runtime_error("Invalid result query");
227 1 : subelement = lastResult[atoi(curarg.c_str())];
228 : }
229 6 : else if (lastResult.isObject())
230 6 : subelement = find_value(lastResult, curarg);
231 : else
232 0 : throw std::runtime_error("Invalid result query"); //no array or object: abort
233 7 : lastResult = subelement;
234 7 : }
235 :
236 : state = STATE_COMMAND_EXECUTED;
237 7 : break;
238 : }
239 : // don't break parsing when the char is required for the next argument
240 : breakParsing = false;
241 :
242 : // pop the stack and return the result to the current command arguments
243 38 : close_out_params();
244 :
245 : // don't stringify the json in case of a string to avoid doublequotes
246 38 : if (lastResult.isStr())
247 14 : curarg = lastResult.get_str();
248 : else
249 24 : curarg = lastResult.write(2);
250 :
251 : // if we have a non empty result, use it as stack argument otherwise as general result
252 38 : if (curarg.size())
253 : {
254 38 : if (stack.size())
255 16 : add_to_current_stack(curarg);
256 : else
257 22 : strResult = curarg;
258 : }
259 38 : curarg.clear();
260 : // assume eating space state
261 : state = STATE_EATING_SPACES;
262 38 : }
263 79 : if (breakParsing)
264 41 : break;
265 38 : }
266 : case STATE_ARGUMENT: // In or after argument
267 : case STATE_EATING_SPACES_IN_ARG:
268 : case STATE_EATING_SPACES_IN_BRACKETS:
269 : case STATE_EATING_SPACES: // Handle runs of whitespace
270 1087 : switch(ch)
271 : {
272 1 : case '"': state = STATE_DOUBLEQUOTED; break;
273 3 : case '\'': state = STATE_SINGLEQUOTED; break;
274 0 : case '\\': state = STATE_ESCAPE_OUTER; break;
275 : case '(': case ')': case '\n':
276 124 : if (state == STATE_EATING_SPACES_IN_ARG)
277 0 : throw std::runtime_error("Invalid Syntax");
278 124 : if (state == STATE_ARGUMENT)
279 : {
280 68 : if (ch == '(' && stack.size() && stack.back().size() > 0)
281 : {
282 17 : if (nDepthInsideSensitive) {
283 4 : ++nDepthInsideSensitive;
284 4 : }
285 17 : stack.push_back(std::vector<std::string>());
286 17 : }
287 :
288 : // don't allow commands after executed commands on baselevel
289 68 : if (!stack.size())
290 2 : throw std::runtime_error("Invalid Syntax");
291 :
292 66 : add_to_current_stack(curarg);
293 66 : curarg.clear();
294 : state = STATE_EATING_SPACES_IN_BRACKETS;
295 66 : }
296 122 : if ((ch == ')' || ch == '\n') && stack.size() > 0)
297 : {
298 54 : if (fExecute) {
299 : // Convert argument list to JSON objects in method-dependent way,
300 : // and pass it along with the method name to the dispatcher.
301 35 : UniValue params = RPCConvertValues(stack.back()[0], std::vector<std::string>(stack.back().begin() + 1, stack.back().end()));
302 35 : std::string method = stack.back()[0];
303 35 : std::string uri;
304 : #ifdef ENABLE_WALLET
305 35 : if (wallet_model) {
306 0 : QByteArray encodedName = QUrl::toPercentEncoding(wallet_model->getWalletName());
307 0 : uri = "/wallet/"+std::string(encodedName.constData(), encodedName.length());
308 0 : }
309 : #endif
310 35 : assert(node);
311 35 : lastResult = node->executeRpc(method, params, uri);
312 35 : }
313 :
314 : state = STATE_COMMAND_EXECUTED;
315 52 : curarg.clear();
316 52 : }
317 : break;
318 : case ' ': case ',': case '\t':
319 66 : if(state == STATE_EATING_SPACES_IN_ARG && curarg.empty() && ch == ',')
320 3 : throw std::runtime_error("Invalid Syntax");
321 :
322 63 : else if(state == STATE_ARGUMENT) // Space ends argument
323 : {
324 32 : add_to_current_stack(curarg);
325 32 : curarg.clear();
326 32 : }
327 63 : if ((state == STATE_EATING_SPACES_IN_BRACKETS || state == STATE_ARGUMENT) && ch == ',')
328 : {
329 : state = STATE_EATING_SPACES_IN_ARG;
330 8 : break;
331 : }
332 : state = STATE_EATING_SPACES;
333 55 : break;
334 893 : default: curarg += ch; state = STATE_ARGUMENT;
335 893 : }
336 : break;
337 : case STATE_SINGLEQUOTED: // Single-quoted string
338 3 : switch(ch)
339 : {
340 3 : case '\'': state = STATE_ARGUMENT; break;
341 0 : default: curarg += ch;
342 : }
343 : break;
344 : case STATE_DOUBLEQUOTED: // Double-quoted string
345 1 : switch(ch)
346 : {
347 1 : case '"': state = STATE_ARGUMENT; break;
348 0 : case '\\': state = STATE_ESCAPE_DOUBLEQUOTED; break;
349 0 : default: curarg += ch;
350 : }
351 : break;
352 : case STATE_ESCAPE_OUTER: // '\' outside quotes
353 0 : curarg += ch; state = STATE_ARGUMENT;
354 0 : break;
355 : case STATE_ESCAPE_DOUBLEQUOTED: // '\' in double-quoted text
356 0 : if(ch != '"' && ch != '\\') curarg += '\\'; // keep '\' for everything but the quote and '\' itself
357 0 : curarg += ch; state = STATE_DOUBLEQUOTED;
358 0 : break;
359 : }
360 7 : }
361 34 : if (pstrFilteredOut) {
362 12 : if (STATE_COMMAND_EXECUTED == state) {
363 3 : assert(!stack.empty());
364 3 : close_out_params();
365 : }
366 12 : *pstrFilteredOut = strCommand;
367 23 : for (auto i = filter_ranges.rbegin(); i != filter_ranges.rend(); ++i) {
368 11 : pstrFilteredOut->replace(i->first, i->second - i->first, "(…)");
369 : }
370 12 : }
371 34 : switch(state) // final state
372 : {
373 : case STATE_COMMAND_EXECUTED:
374 14 : if (lastResult.isStr())
375 8 : strResult = lastResult.get_str();
376 : else
377 6 : strResult = lastResult.write(2);
378 : case STATE_ARGUMENT:
379 : case STATE_EATING_SPACES:
380 34 : return true;
381 : default: // ERROR to end in one of the other states
382 0 : return false;
383 : }
384 50 : }
385 :
386 0 : void RPCExecutor::request(const QString &command, const WalletModel* wallet_model)
387 : {
388 : try
389 : {
390 0 : std::string result;
391 0 : std::string executableCommand = command.toStdString() + "\n";
392 :
393 : // Catch the console-only-help command before RPC call is executed and reply with help text as-if a RPC reply.
394 0 : if(executableCommand == "help-console\n") {
395 0 : Q_EMIT reply(RPCConsole::CMD_REPLY, QString(("\n"
396 : "This console accepts RPC commands using the standard syntax.\n"
397 : " example: getblockhash 0\n\n"
398 :
399 : "This console can also accept RPC commands using the parenthesized syntax.\n"
400 : " example: getblockhash(0)\n\n"
401 :
402 : "Commands may be nested when specified with the parenthesized syntax.\n"
403 : " example: getblock(getblockhash(0) 1)\n\n"
404 :
405 : "A space or a comma can be used to delimit arguments for either syntax.\n"
406 : " example: getblockhash 0\n"
407 : " getblockhash,0\n\n"
408 :
409 : "Named results can be queried with a non-quoted key string in brackets using the parenthesized syntax.\n"
410 : " example: getblock(getblockhash(0) 1)[tx]\n\n"
411 :
412 : "Results without keys can be queried with an integer in brackets using the parenthesized syntax.\n"
413 : " example: getblock(getblockhash(0),1)[tx][0]\n\n")));
414 0 : return;
415 : }
416 0 : if (!RPCConsole::RPCExecuteCommandLine(m_node, result, executableCommand, nullptr, wallet_model)) {
417 0 : Q_EMIT reply(RPCConsole::CMD_ERROR, QString("Parse error: unbalanced ' or \""));
418 0 : return;
419 : }
420 :
421 0 : Q_EMIT reply(RPCConsole::CMD_REPLY, QString::fromStdString(result));
422 0 : }
423 : catch (UniValue& objError)
424 : {
425 : try // Nice formatting for standard-format error
426 : {
427 0 : int code = find_value(objError, "code").get_int();
428 0 : std::string message = find_value(objError, "message").get_str();
429 0 : Q_EMIT reply(RPCConsole::CMD_ERROR, QString::fromStdString(message) + " (code " + QString::number(code) + ")");
430 0 : }
431 : catch (const std::runtime_error&) // raised when converting to invalid type, i.e. missing code or message
432 : { // Show raw JSON object
433 0 : Q_EMIT reply(RPCConsole::CMD_ERROR, QString::fromStdString(objError.write()));
434 0 : }
435 0 : }
436 : catch (const std::exception& e)
437 : {
438 0 : Q_EMIT reply(RPCConsole::CMD_ERROR, QString("Error: ") + QString::fromStdString(e.what()));
439 0 : }
440 0 : }
441 :
442 0 : RPCConsole::RPCConsole(interfaces::Node& node, const PlatformStyle *_platformStyle, QWidget *parent) :
443 0 : QWidget(parent),
444 0 : m_node(node),
445 0 : ui(new Ui::RPCConsole),
446 0 : platformStyle(_platformStyle)
447 0 : {
448 0 : ui->setupUi(this);
449 0 : QSettings settings;
450 0 : if (!restoreGeometry(settings.value("RPCConsoleWindowGeometry").toByteArray())) {
451 : // Restore failed (perhaps missing setting), center the window
452 0 : move(QGuiApplication::primaryScreen()->availableGeometry().center() - frameGeometry().center());
453 0 : }
454 :
455 0 : QChar nonbreaking_hyphen(8209);
456 0 : ui->dataDir->setToolTip(ui->dataDir->toolTip().arg(QString(nonbreaking_hyphen) + "datadir"));
457 0 : ui->blocksDir->setToolTip(ui->blocksDir->toolTip().arg(QString(nonbreaking_hyphen) + "blocksdir"));
458 0 : ui->openDebugLogfileButton->setToolTip(ui->openDebugLogfileButton->toolTip().arg(PACKAGE_NAME));
459 :
460 0 : if (platformStyle->getImagesOnButtons()) {
461 0 : ui->openDebugLogfileButton->setIcon(platformStyle->SingleColorIcon(":/icons/export"));
462 0 : }
463 0 : ui->clearButton->setIcon(platformStyle->SingleColorIcon(":/icons/remove"));
464 0 : ui->fontBiggerButton->setIcon(platformStyle->SingleColorIcon(":/icons/fontbigger"));
465 0 : ui->fontSmallerButton->setIcon(platformStyle->SingleColorIcon(":/icons/fontsmaller"));
466 :
467 : // Install event filter for up and down arrow
468 0 : ui->lineEdit->installEventFilter(this);
469 0 : ui->lineEdit->setMaxLength(16 * 1024 * 1024);
470 0 : ui->messagesWidget->installEventFilter(this);
471 :
472 0 : connect(ui->clearButton, &QPushButton::clicked, this, &RPCConsole::clear);
473 0 : connect(ui->fontBiggerButton, &QPushButton::clicked, this, &RPCConsole::fontBigger);
474 0 : connect(ui->fontSmallerButton, &QPushButton::clicked, this, &RPCConsole::fontSmaller);
475 0 : connect(ui->btnClearTrafficGraph, &QPushButton::clicked, ui->trafficGraph, &TrafficGraphWidget::clear);
476 :
477 : // disable the wallet selector by default
478 0 : ui->WalletSelector->setVisible(false);
479 0 : ui->WalletSelectorLabel->setVisible(false);
480 :
481 : // set library version labels
482 : #ifdef ENABLE_WALLET
483 0 : ui->berkeleyDBVersion->setText(QString::fromStdString(BerkeleyDatabaseVersion()));
484 : #else
485 : ui->label_berkeleyDBVersion->hide();
486 : ui->berkeleyDBVersion->hide();
487 : #endif
488 : // Register RPC timer interface
489 0 : rpcTimerInterface = new QtRPCTimerInterface();
490 : // avoid accidentally overwriting an existing, non QTThread
491 : // based timer interface
492 0 : m_node.rpcSetTimerInterfaceIfUnset(rpcTimerInterface);
493 :
494 0 : setTrafficGraphRange(INITIAL_TRAFFIC_GRAPH_MINS);
495 :
496 0 : ui->detailWidget->hide();
497 0 : ui->peerHeading->setText(tr("Select a peer to view detailed information."));
498 :
499 0 : consoleFontSize = settings.value(fontSizeSettingsKey, QFont().pointSize()).toInt();
500 0 : clear();
501 :
502 0 : GUIUtil::handleCloseWindowShortcut(this);
503 0 : }
504 :
505 0 : RPCConsole::~RPCConsole()
506 0 : {
507 0 : QSettings settings;
508 0 : settings.setValue("RPCConsoleWindowGeometry", saveGeometry());
509 0 : m_node.rpcUnsetTimerInterface(rpcTimerInterface);
510 0 : delete rpcTimerInterface;
511 0 : delete ui;
512 0 : }
513 :
514 0 : bool RPCConsole::eventFilter(QObject* obj, QEvent *event)
515 : {
516 0 : if(event->type() == QEvent::KeyPress) // Special key handling
517 : {
518 0 : QKeyEvent *keyevt = static_cast<QKeyEvent*>(event);
519 0 : int key = keyevt->key();
520 0 : Qt::KeyboardModifiers mod = keyevt->modifiers();
521 0 : switch(key)
522 : {
523 0 : case Qt::Key_Up: if(obj == ui->lineEdit) { browseHistory(-1); return true; } break;
524 0 : case Qt::Key_Down: if(obj == ui->lineEdit) { browseHistory(1); return true; } break;
525 : case Qt::Key_PageUp: /* pass paging keys to messages widget */
526 : case Qt::Key_PageDown:
527 0 : if(obj == ui->lineEdit)
528 : {
529 0 : QApplication::postEvent(ui->messagesWidget, new QKeyEvent(*keyevt));
530 0 : return true;
531 : }
532 : break;
533 : case Qt::Key_Return:
534 : case Qt::Key_Enter:
535 : // forward these events to lineEdit
536 0 : if(obj == autoCompleter->popup()) {
537 0 : QApplication::postEvent(ui->lineEdit, new QKeyEvent(*keyevt));
538 0 : autoCompleter->popup()->hide();
539 0 : return true;
540 : }
541 : break;
542 : default:
543 : // Typing in messages widget brings focus to line edit, and redirects key there
544 : // Exclude most combinations and keys that emit no text, except paste shortcuts
545 0 : if(obj == ui->messagesWidget && (
546 0 : (!mod && !keyevt->text().isEmpty() && key != Qt::Key_Tab) ||
547 0 : ((mod & Qt::ControlModifier) && key == Qt::Key_V) ||
548 0 : ((mod & Qt::ShiftModifier) && key == Qt::Key_Insert)))
549 : {
550 0 : ui->lineEdit->setFocus();
551 0 : QApplication::postEvent(ui->lineEdit, new QKeyEvent(*keyevt));
552 0 : return true;
553 : }
554 : }
555 0 : }
556 0 : return QWidget::eventFilter(obj, event);
557 0 : }
558 :
559 0 : void RPCConsole::setClientModel(ClientModel *model, int bestblock_height, int64_t bestblock_date, double verification_progress)
560 : {
561 0 : clientModel = model;
562 :
563 : bool wallet_enabled{false};
564 : #ifdef ENABLE_WALLET
565 0 : wallet_enabled = WalletModel::isWalletEnabled();
566 : #endif // ENABLE_WALLET
567 0 : if (model && !wallet_enabled) {
568 : // Show warning, for example if this is a prerelease version
569 0 : connect(model, &ClientModel::alertsChanged, this, &RPCConsole::updateAlerts);
570 0 : updateAlerts(model->getStatusBarWarnings());
571 0 : }
572 :
573 0 : ui->trafficGraph->setClientModel(model);
574 0 : if (model && clientModel->getPeerTableModel() && clientModel->getBanTableModel()) {
575 : // Keep up to date with client
576 0 : setNumConnections(model->getNumConnections());
577 0 : connect(model, &ClientModel::numConnectionsChanged, this, &RPCConsole::setNumConnections);
578 :
579 0 : setNumBlocks(bestblock_height, QDateTime::fromTime_t(bestblock_date), verification_progress, false);
580 0 : connect(model, &ClientModel::numBlocksChanged, this, &RPCConsole::setNumBlocks);
581 :
582 0 : updateNetworkState();
583 0 : connect(model, &ClientModel::networkActiveChanged, this, &RPCConsole::setNetworkActive);
584 :
585 0 : interfaces::Node& node = clientModel->node();
586 0 : updateTrafficStats(node.getTotalBytesRecv(), node.getTotalBytesSent());
587 0 : connect(model, &ClientModel::bytesChanged, this, &RPCConsole::updateTrafficStats);
588 :
589 0 : connect(model, &ClientModel::mempoolSizeChanged, this, &RPCConsole::setMempoolSize);
590 :
591 : // set up peer table
592 0 : ui->peerWidget->setModel(model->getPeerTableModel());
593 0 : ui->peerWidget->verticalHeader()->hide();
594 0 : ui->peerWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
595 0 : ui->peerWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
596 0 : ui->peerWidget->setSelectionMode(QAbstractItemView::ExtendedSelection);
597 0 : ui->peerWidget->setContextMenuPolicy(Qt::CustomContextMenu);
598 0 : ui->peerWidget->setColumnWidth(PeerTableModel::Address, ADDRESS_COLUMN_WIDTH);
599 0 : ui->peerWidget->setColumnWidth(PeerTableModel::Subversion, SUBVERSION_COLUMN_WIDTH);
600 0 : ui->peerWidget->setColumnWidth(PeerTableModel::Ping, PING_COLUMN_WIDTH);
601 0 : ui->peerWidget->horizontalHeader()->setStretchLastSection(true);
602 :
603 : // create peer table context menu actions
604 0 : QAction* disconnectAction = new QAction(tr("&Disconnect"), this);
605 0 : QAction* banAction1h = new QAction(tr("Ban for") + " " + tr("1 &hour"), this);
606 0 : QAction* banAction24h = new QAction(tr("Ban for") + " " + tr("1 &day"), this);
607 0 : QAction* banAction7d = new QAction(tr("Ban for") + " " + tr("1 &week"), this);
608 0 : QAction* banAction365d = new QAction(tr("Ban for") + " " + tr("1 &year"), this);
609 :
610 : // create peer table context menu
611 0 : peersTableContextMenu = new QMenu(this);
612 0 : peersTableContextMenu->addAction(disconnectAction);
613 0 : peersTableContextMenu->addAction(banAction1h);
614 0 : peersTableContextMenu->addAction(banAction24h);
615 0 : peersTableContextMenu->addAction(banAction7d);
616 0 : peersTableContextMenu->addAction(banAction365d);
617 :
618 0 : connect(banAction1h, &QAction::triggered, [this] { banSelectedNode(60 * 60); });
619 0 : connect(banAction24h, &QAction::triggered, [this] { banSelectedNode(60 * 60 * 24); });
620 0 : connect(banAction7d, &QAction::triggered, [this] { banSelectedNode(60 * 60 * 24 * 7); });
621 0 : connect(banAction365d, &QAction::triggered, [this] { banSelectedNode(60 * 60 * 24 * 365); });
622 :
623 : // peer table context menu signals
624 0 : connect(ui->peerWidget, &QTableView::customContextMenuRequested, this, &RPCConsole::showPeersTableContextMenu);
625 0 : connect(disconnectAction, &QAction::triggered, this, &RPCConsole::disconnectSelectedNode);
626 :
627 : // peer table signal handling - update peer details when selecting new node
628 0 : connect(ui->peerWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this, &RPCConsole::peerSelected);
629 : // peer table signal handling - update peer details when new nodes are added to the model
630 0 : connect(model->getPeerTableModel(), &PeerTableModel::layoutChanged, this, &RPCConsole::peerLayoutChanged);
631 : // peer table signal handling - cache selected node ids
632 0 : connect(model->getPeerTableModel(), &PeerTableModel::layoutAboutToBeChanged, this, &RPCConsole::peerLayoutAboutToChange);
633 :
634 : // set up ban table
635 0 : ui->banlistWidget->setModel(model->getBanTableModel());
636 0 : ui->banlistWidget->verticalHeader()->hide();
637 0 : ui->banlistWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
638 0 : ui->banlistWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
639 0 : ui->banlistWidget->setSelectionMode(QAbstractItemView::SingleSelection);
640 0 : ui->banlistWidget->setContextMenuPolicy(Qt::CustomContextMenu);
641 0 : ui->banlistWidget->setColumnWidth(BanTableModel::Address, BANSUBNET_COLUMN_WIDTH);
642 0 : ui->banlistWidget->setColumnWidth(BanTableModel::Bantime, BANTIME_COLUMN_WIDTH);
643 0 : ui->banlistWidget->horizontalHeader()->setStretchLastSection(true);
644 :
645 : // create ban table context menu action
646 0 : QAction* unbanAction = new QAction(tr("&Unban"), this);
647 :
648 : // create ban table context menu
649 0 : banTableContextMenu = new QMenu(this);
650 0 : banTableContextMenu->addAction(unbanAction);
651 :
652 : // ban table context menu signals
653 0 : connect(ui->banlistWidget, &QTableView::customContextMenuRequested, this, &RPCConsole::showBanTableContextMenu);
654 0 : connect(unbanAction, &QAction::triggered, this, &RPCConsole::unbanSelectedNode);
655 :
656 : // ban table signal handling - clear peer details when clicking a peer in the ban table
657 0 : connect(ui->banlistWidget, &QTableView::clicked, this, &RPCConsole::clearSelectedNode);
658 : // ban table signal handling - ensure ban table is shown or hidden (if empty)
659 0 : connect(model->getBanTableModel(), &BanTableModel::layoutChanged, this, &RPCConsole::showOrHideBanTableIfRequired);
660 0 : showOrHideBanTableIfRequired();
661 :
662 : // Provide initial values
663 0 : ui->clientVersion->setText(model->formatFullVersion());
664 0 : ui->clientUserAgent->setText(model->formatSubVersion());
665 0 : ui->dataDir->setText(model->dataDir());
666 0 : ui->blocksDir->setText(model->blocksDir());
667 0 : ui->startupTime->setText(model->formatClientStartupTime());
668 0 : ui->networkName->setText(QString::fromStdString(Params().NetworkIDString()));
669 :
670 : //Setup autocomplete and attach it
671 0 : QStringList wordList;
672 0 : std::vector<std::string> commandList = m_node.listRpcCommands();
673 0 : for (size_t i = 0; i < commandList.size(); ++i)
674 : {
675 0 : wordList << commandList[i].c_str();
676 0 : wordList << ("help " + commandList[i]).c_str();
677 : }
678 :
679 0 : wordList << "help-console";
680 0 : wordList.sort();
681 0 : autoCompleter = new QCompleter(wordList, this);
682 0 : autoCompleter->setModelSorting(QCompleter::CaseSensitivelySortedModel);
683 : // ui->lineEdit is initially disabled because running commands is only
684 : // possible from now on.
685 0 : ui->lineEdit->setEnabled(true);
686 0 : ui->lineEdit->setCompleter(autoCompleter);
687 0 : autoCompleter->popup()->installEventFilter(this);
688 : // Start thread to execute RPC commands.
689 0 : startExecutor();
690 0 : }
691 0 : if (!model) {
692 : // Client model is being set to 0, this means shutdown() is about to be called.
693 0 : thread.quit();
694 0 : thread.wait();
695 0 : }
696 0 : }
697 :
698 : #ifdef ENABLE_WALLET
699 0 : void RPCConsole::addWallet(WalletModel * const walletModel)
700 : {
701 : // use name for text and wallet model for internal data object (to allow to move to a wallet id later)
702 0 : ui->WalletSelector->addItem(walletModel->getDisplayName(), QVariant::fromValue(walletModel));
703 0 : if (ui->WalletSelector->count() == 2 && !isVisible()) {
704 : // First wallet added, set to default so long as the window isn't presently visible (and potentially in use)
705 0 : ui->WalletSelector->setCurrentIndex(1);
706 0 : }
707 0 : if (ui->WalletSelector->count() > 2) {
708 0 : ui->WalletSelector->setVisible(true);
709 0 : ui->WalletSelectorLabel->setVisible(true);
710 0 : }
711 0 : }
712 :
713 0 : void RPCConsole::removeWallet(WalletModel * const walletModel)
714 : {
715 0 : ui->WalletSelector->removeItem(ui->WalletSelector->findData(QVariant::fromValue(walletModel)));
716 0 : if (ui->WalletSelector->count() == 2) {
717 0 : ui->WalletSelector->setVisible(false);
718 0 : ui->WalletSelectorLabel->setVisible(false);
719 0 : }
720 0 : }
721 : #endif
722 :
723 0 : static QString categoryClass(int category)
724 : {
725 0 : switch(category)
726 : {
727 0 : case RPCConsole::CMD_REQUEST: return "cmd-request"; break;
728 0 : case RPCConsole::CMD_REPLY: return "cmd-reply"; break;
729 0 : case RPCConsole::CMD_ERROR: return "cmd-error"; break;
730 0 : default: return "misc";
731 : }
732 0 : }
733 :
734 0 : void RPCConsole::fontBigger()
735 : {
736 0 : setFontSize(consoleFontSize+1);
737 0 : }
738 :
739 0 : void RPCConsole::fontSmaller()
740 : {
741 0 : setFontSize(consoleFontSize-1);
742 0 : }
743 :
744 0 : void RPCConsole::setFontSize(int newSize)
745 : {
746 0 : QSettings settings;
747 :
748 : //don't allow an insane font size
749 0 : if (newSize < FONT_RANGE.width() || newSize > FONT_RANGE.height())
750 0 : return;
751 :
752 : // temp. store the console content
753 0 : QString str = ui->messagesWidget->toHtml();
754 :
755 : // replace font tags size in current content
756 0 : str.replace(QString("font-size:%1pt").arg(consoleFontSize), QString("font-size:%1pt").arg(newSize));
757 :
758 : // store the new font size
759 0 : consoleFontSize = newSize;
760 0 : settings.setValue(fontSizeSettingsKey, consoleFontSize);
761 :
762 : // clear console (reset icon sizes, default stylesheet) and re-add the content
763 0 : float oldPosFactor = 1.0 / ui->messagesWidget->verticalScrollBar()->maximum() * ui->messagesWidget->verticalScrollBar()->value();
764 0 : clear(false);
765 0 : ui->messagesWidget->setHtml(str);
766 0 : ui->messagesWidget->verticalScrollBar()->setValue(oldPosFactor * ui->messagesWidget->verticalScrollBar()->maximum());
767 0 : }
768 :
769 0 : void RPCConsole::clear(bool clearHistory)
770 : {
771 0 : ui->messagesWidget->clear();
772 0 : if(clearHistory)
773 : {
774 0 : history.clear();
775 0 : historyPtr = 0;
776 0 : }
777 0 : ui->lineEdit->clear();
778 0 : ui->lineEdit->setFocus();
779 :
780 : // Add smoothly scaled icon images.
781 : // (when using width/height on an img, Qt uses nearest instead of linear interpolation)
782 0 : for(int i=0; ICON_MAPPING[i].url; ++i)
783 : {
784 0 : ui->messagesWidget->document()->addResource(
785 : QTextDocument::ImageResource,
786 0 : QUrl(ICON_MAPPING[i].url),
787 0 : platformStyle->SingleColorImage(ICON_MAPPING[i].source).scaled(QSize(consoleFontSize*2, consoleFontSize*2), Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
788 : }
789 :
790 : // Set default style sheet
791 0 : QFontInfo fixedFontInfo(GUIUtil::fixedPitchFont());
792 0 : ui->messagesWidget->document()->setDefaultStyleSheet(
793 0 : QString(
794 : "table { }"
795 : "td.time { color: #808080; font-size: %2; padding-top: 3px; } "
796 : "td.message { font-family: %1; font-size: %2; white-space:pre-wrap; } "
797 : "td.cmd-request { color: #006060; } "
798 : "td.cmd-error { color: red; } "
799 : ".secwarning { color: red; }"
800 : "b { color: #006060; } "
801 0 : ).arg(fixedFontInfo.family(), QString("%1pt").arg(consoleFontSize))
802 : );
803 :
804 : #ifdef Q_OS_MAC
805 0 : QString clsKey = "(⌘)-L";
806 : #else
807 : QString clsKey = "Ctrl-L";
808 : #endif
809 :
810 0 : message(CMD_REPLY, (tr("Welcome to the %1 RPC console.").arg(PACKAGE_NAME) + "<br>" +
811 0 : tr("Use up and down arrows to navigate history, and %1 to clear screen.").arg("<b>"+clsKey+"</b>") + "<br>" +
812 0 : tr("Type %1 for an overview of available commands.").arg("<b>help</b>") + "<br>" +
813 0 : tr("For more information on using this console type %1.").arg("<b>help-console</b>") +
814 0 : "<br><span class=\"secwarning\"><br>" +
815 0 : tr("WARNING: Scammers have been active, telling users to type commands here, stealing their wallet contents. Do not use this console without fully understanding the ramifications of a command.") +
816 : "</span>"),
817 : true);
818 0 : }
819 :
820 0 : void RPCConsole::keyPressEvent(QKeyEvent *event)
821 : {
822 0 : if(windowType() != Qt::Widget && event->key() == Qt::Key_Escape)
823 : {
824 0 : close();
825 0 : }
826 0 : }
827 :
828 0 : void RPCConsole::message(int category, const QString &message, bool html)
829 : {
830 0 : QTime time = QTime::currentTime();
831 0 : QString timeString = time.toString();
832 0 : QString out;
833 0 : out += "<table><tr><td class=\"time\" width=\"65\">" + timeString + "</td>";
834 0 : out += "<td class=\"icon\" width=\"32\"><img src=\"" + categoryClass(category) + "\"></td>";
835 0 : out += "<td class=\"message " + categoryClass(category) + "\" valign=\"middle\">";
836 0 : if(html)
837 0 : out += message;
838 : else
839 0 : out += GUIUtil::HtmlEscape(message, false);
840 0 : out += "</td></tr></table>";
841 0 : ui->messagesWidget->append(out);
842 0 : }
843 :
844 0 : void RPCConsole::updateNetworkState()
845 : {
846 0 : QString connections = QString::number(clientModel->getNumConnections()) + " (";
847 0 : connections += tr("In:") + " " + QString::number(clientModel->getNumConnections(CONNECTIONS_IN)) + " / ";
848 0 : connections += tr("Out:") + " " + QString::number(clientModel->getNumConnections(CONNECTIONS_OUT)) + ")";
849 :
850 0 : if(!clientModel->node().getNetworkActive()) {
851 0 : connections += " (" + tr("Network activity disabled") + ")";
852 0 : }
853 :
854 0 : ui->numberOfConnections->setText(connections);
855 0 : }
856 :
857 0 : void RPCConsole::setNumConnections(int count)
858 : {
859 0 : if (!clientModel)
860 : return;
861 :
862 0 : updateNetworkState();
863 0 : }
864 :
865 0 : void RPCConsole::setNetworkActive(bool networkActive)
866 : {
867 0 : updateNetworkState();
868 0 : }
869 :
870 0 : void RPCConsole::setNumBlocks(int count, const QDateTime& blockDate, double nVerificationProgress, bool headers)
871 : {
872 0 : if (!headers) {
873 0 : ui->numberOfBlocks->setText(QString::number(count));
874 0 : ui->lastBlockTime->setText(blockDate.toString());
875 0 : }
876 0 : }
877 :
878 0 : void RPCConsole::setMempoolSize(long numberOfTxs, size_t dynUsage)
879 : {
880 0 : ui->mempoolNumberTxs->setText(QString::number(numberOfTxs));
881 :
882 0 : if (dynUsage < 1000000)
883 0 : ui->mempoolSize->setText(QString::number(dynUsage/1000.0, 'f', 2) + " KB");
884 : else
885 0 : ui->mempoolSize->setText(QString::number(dynUsage/1000000.0, 'f', 2) + " MB");
886 0 : }
887 :
888 0 : void RPCConsole::on_lineEdit_returnPressed()
889 : {
890 0 : QString cmd = ui->lineEdit->text();
891 :
892 0 : if(!cmd.isEmpty())
893 : {
894 0 : std::string strFilteredCmd;
895 : try {
896 0 : std::string dummy;
897 0 : if (!RPCParseCommandLine(nullptr, dummy, cmd.toStdString(), false, &strFilteredCmd)) {
898 : // Failed to parse command, so we cannot even filter it for the history
899 0 : throw std::runtime_error("Invalid command line");
900 : }
901 0 : } catch (const std::exception& e) {
902 0 : QMessageBox::critical(this, "Error", QString("Error: ") + QString::fromStdString(e.what()));
903 : return;
904 0 : }
905 :
906 0 : ui->lineEdit->clear();
907 :
908 0 : cmdBeforeBrowsing = QString();
909 :
910 : #ifdef ENABLE_WALLET
911 0 : WalletModel* wallet_model = ui->WalletSelector->currentData().value<WalletModel*>();
912 :
913 0 : if (m_last_wallet_model != wallet_model) {
914 0 : if (wallet_model) {
915 0 : message(CMD_REQUEST, tr("Executing command using \"%1\" wallet").arg(wallet_model->getWalletName()));
916 0 : } else {
917 0 : message(CMD_REQUEST, tr("Executing command without any wallet"));
918 : }
919 0 : m_last_wallet_model = wallet_model;
920 0 : }
921 : #endif
922 :
923 0 : message(CMD_REQUEST, QString::fromStdString(strFilteredCmd));
924 0 : Q_EMIT cmdRequest(cmd, m_last_wallet_model);
925 :
926 0 : cmd = QString::fromStdString(strFilteredCmd);
927 :
928 : // Remove command, if already in history
929 0 : history.removeOne(cmd);
930 : // Append command to history
931 0 : history.append(cmd);
932 : // Enforce maximum history size
933 0 : while(history.size() > CONSOLE_HISTORY)
934 0 : history.removeFirst();
935 : // Set pointer to end of history
936 0 : historyPtr = history.size();
937 :
938 : // Scroll console view to end
939 0 : scrollToEnd();
940 0 : }
941 0 : }
942 :
943 0 : void RPCConsole::browseHistory(int offset)
944 : {
945 : // store current text when start browsing through the history
946 0 : if (historyPtr == history.size()) {
947 0 : cmdBeforeBrowsing = ui->lineEdit->text();
948 0 : }
949 :
950 0 : historyPtr += offset;
951 0 : if(historyPtr < 0)
952 0 : historyPtr = 0;
953 0 : if(historyPtr > history.size())
954 0 : historyPtr = history.size();
955 0 : QString cmd;
956 0 : if(historyPtr < history.size())
957 0 : cmd = history.at(historyPtr);
958 0 : else if (!cmdBeforeBrowsing.isNull()) {
959 0 : cmd = cmdBeforeBrowsing;
960 0 : }
961 0 : ui->lineEdit->setText(cmd);
962 0 : }
963 :
964 0 : void RPCConsole::startExecutor()
965 : {
966 0 : RPCExecutor *executor = new RPCExecutor(m_node);
967 0 : executor->moveToThread(&thread);
968 :
969 : // Replies from executor object must go to this object
970 0 : connect(executor, &RPCExecutor::reply, this, static_cast<void (RPCConsole::*)(int, const QString&)>(&RPCConsole::message));
971 :
972 : // Requests from this object must go to executor
973 0 : connect(this, &RPCConsole::cmdRequest, executor, &RPCExecutor::request);
974 :
975 : // Make sure executor object is deleted in its own thread
976 0 : connect(&thread, &QThread::finished, executor, &RPCExecutor::deleteLater);
977 :
978 : // Default implementation of QThread::run() simply spins up an event loop in the thread,
979 : // which is what we want.
980 0 : thread.start();
981 0 : }
982 :
983 0 : void RPCConsole::on_tabWidget_currentChanged(int index)
984 : {
985 0 : if (ui->tabWidget->widget(index) == ui->tab_console) {
986 0 : ui->lineEdit->setFocus();
987 0 : }
988 0 : }
989 :
990 0 : void RPCConsole::on_openDebugLogfileButton_clicked()
991 : {
992 0 : GUIUtil::openDebugLogfile();
993 0 : }
994 :
995 0 : void RPCConsole::scrollToEnd()
996 : {
997 0 : QScrollBar *scrollbar = ui->messagesWidget->verticalScrollBar();
998 0 : scrollbar->setValue(scrollbar->maximum());
999 0 : }
1000 :
1001 0 : void RPCConsole::on_sldGraphRange_valueChanged(int value)
1002 : {
1003 : const int multiplier = 5; // each position on the slider represents 5 min
1004 0 : int mins = value * multiplier;
1005 0 : setTrafficGraphRange(mins);
1006 0 : }
1007 :
1008 0 : void RPCConsole::setTrafficGraphRange(int mins)
1009 : {
1010 0 : ui->trafficGraph->setGraphRangeMins(mins);
1011 0 : ui->lblGraphRange->setText(GUIUtil::formatDurationStr(mins * 60));
1012 0 : }
1013 :
1014 0 : void RPCConsole::updateTrafficStats(quint64 totalBytesIn, quint64 totalBytesOut)
1015 : {
1016 0 : ui->lblBytesIn->setText(GUIUtil::formatBytes(totalBytesIn));
1017 0 : ui->lblBytesOut->setText(GUIUtil::formatBytes(totalBytesOut));
1018 0 : }
1019 :
1020 0 : void RPCConsole::peerSelected(const QItemSelection &selected, const QItemSelection &deselected)
1021 : {
1022 : Q_UNUSED(deselected);
1023 :
1024 0 : if (!clientModel || !clientModel->getPeerTableModel() || selected.indexes().isEmpty())
1025 : return;
1026 :
1027 0 : const CNodeCombinedStats *stats = clientModel->getPeerTableModel()->getNodeStats(selected.indexes().first().row());
1028 0 : if (stats)
1029 0 : updateNodeDetail(stats);
1030 0 : }
1031 :
1032 0 : void RPCConsole::peerLayoutAboutToChange()
1033 : {
1034 0 : QModelIndexList selected = ui->peerWidget->selectionModel()->selectedIndexes();
1035 0 : cachedNodeids.clear();
1036 0 : for(int i = 0; i < selected.size(); i++)
1037 : {
1038 0 : const CNodeCombinedStats *stats = clientModel->getPeerTableModel()->getNodeStats(selected.at(i).row());
1039 0 : cachedNodeids.append(stats->nodeStats.nodeid);
1040 : }
1041 0 : }
1042 :
1043 0 : void RPCConsole::peerLayoutChanged()
1044 : {
1045 0 : if (!clientModel || !clientModel->getPeerTableModel())
1046 : return;
1047 :
1048 : const CNodeCombinedStats *stats = nullptr;
1049 : bool fUnselect = false;
1050 : bool fReselect = false;
1051 :
1052 0 : if (cachedNodeids.empty()) // no node selected yet
1053 0 : return;
1054 :
1055 : // find the currently selected row
1056 : int selectedRow = -1;
1057 0 : QModelIndexList selectedModelIndex = ui->peerWidget->selectionModel()->selectedIndexes();
1058 0 : if (!selectedModelIndex.isEmpty()) {
1059 0 : selectedRow = selectedModelIndex.first().row();
1060 0 : }
1061 :
1062 : // check if our detail node has a row in the table (it may not necessarily
1063 : // be at selectedRow since its position can change after a layout change)
1064 0 : int detailNodeRow = clientModel->getPeerTableModel()->getRowByNodeId(cachedNodeids.first());
1065 :
1066 0 : if (detailNodeRow < 0)
1067 : {
1068 : // detail node disappeared from table (node disconnected)
1069 : fUnselect = true;
1070 0 : }
1071 : else
1072 : {
1073 0 : if (detailNodeRow != selectedRow)
1074 : {
1075 : // detail node moved position
1076 : fUnselect = true;
1077 : fReselect = true;
1078 0 : }
1079 :
1080 : // get fresh stats on the detail node.
1081 0 : stats = clientModel->getPeerTableModel()->getNodeStats(detailNodeRow);
1082 : }
1083 :
1084 0 : if (fUnselect && selectedRow >= 0) {
1085 0 : clearSelectedNode();
1086 : }
1087 :
1088 0 : if (fReselect)
1089 : {
1090 0 : for(int i = 0; i < cachedNodeids.size(); i++)
1091 : {
1092 0 : ui->peerWidget->selectRow(clientModel->getPeerTableModel()->getRowByNodeId(cachedNodeids.at(i)));
1093 : }
1094 0 : }
1095 :
1096 0 : if (stats)
1097 0 : updateNodeDetail(stats);
1098 0 : }
1099 :
1100 0 : void RPCConsole::updateNodeDetail(const CNodeCombinedStats *stats)
1101 : {
1102 : // update the detail ui with latest node information
1103 0 : QString peerAddrDetails(QString::fromStdString(stats->nodeStats.addrName) + " ");
1104 0 : peerAddrDetails += tr("(node id: %1)").arg(QString::number(stats->nodeStats.nodeid));
1105 0 : if (!stats->nodeStats.addrLocal.empty())
1106 0 : peerAddrDetails += "<br />" + tr("via %1").arg(QString::fromStdString(stats->nodeStats.addrLocal));
1107 0 : ui->peerHeading->setText(peerAddrDetails);
1108 0 : ui->peerServices->setText(GUIUtil::formatServicesStr(stats->nodeStats.nServices));
1109 0 : ui->peerLastSend->setText(stats->nodeStats.nLastSend ? GUIUtil::formatDurationStr(GetSystemTimeInSeconds() - stats->nodeStats.nLastSend) : tr("never"));
1110 0 : ui->peerLastRecv->setText(stats->nodeStats.nLastRecv ? GUIUtil::formatDurationStr(GetSystemTimeInSeconds() - stats->nodeStats.nLastRecv) : tr("never"));
1111 0 : ui->peerBytesSent->setText(GUIUtil::formatBytes(stats->nodeStats.nSendBytes));
1112 0 : ui->peerBytesRecv->setText(GUIUtil::formatBytes(stats->nodeStats.nRecvBytes));
1113 0 : ui->peerConnTime->setText(GUIUtil::formatDurationStr(GetSystemTimeInSeconds() - stats->nodeStats.nTimeConnected));
1114 0 : ui->peerPingTime->setText(GUIUtil::formatPingTime(stats->nodeStats.m_ping_usec));
1115 0 : ui->peerPingWait->setText(GUIUtil::formatPingTime(stats->nodeStats.m_ping_wait_usec));
1116 0 : ui->peerMinPing->setText(GUIUtil::formatPingTime(stats->nodeStats.m_min_ping_usec));
1117 0 : ui->timeoffset->setText(GUIUtil::formatTimeOffset(stats->nodeStats.nTimeOffset));
1118 0 : ui->peerVersion->setText(QString::number(stats->nodeStats.nVersion));
1119 0 : ui->peerSubversion->setText(QString::fromStdString(stats->nodeStats.cleanSubVer));
1120 0 : ui->peerDirection->setText(stats->nodeStats.fInbound ? tr("Inbound") : tr("Outbound"));
1121 0 : ui->peerHeight->setText(QString::number(stats->nodeStats.nStartingHeight));
1122 0 : if (stats->nodeStats.m_permissionFlags == PF_NONE) {
1123 0 : ui->peerPermissions->setText(tr("N/A"));
1124 0 : } else {
1125 0 : QStringList permissions;
1126 0 : for (const auto& permission : NetPermissions::ToStrings(stats->nodeStats.m_permissionFlags)) {
1127 0 : permissions.append(QString::fromStdString(permission));
1128 : }
1129 0 : ui->peerPermissions->setText(permissions.join(" & "));
1130 0 : }
1131 0 : ui->peerMappedAS->setText(stats->nodeStats.m_mapped_as != 0 ? QString::number(stats->nodeStats.m_mapped_as) : tr("N/A"));
1132 :
1133 : // This check fails for example if the lock was busy and
1134 : // nodeStateStats couldn't be fetched.
1135 0 : if (stats->fNodeStateStatsAvailable) {
1136 : // Sync height is init to -1
1137 0 : if (stats->nodeStateStats.nSyncHeight > -1)
1138 0 : ui->peerSyncHeight->setText(QString("%1").arg(stats->nodeStateStats.nSyncHeight));
1139 : else
1140 0 : ui->peerSyncHeight->setText(tr("Unknown"));
1141 :
1142 : // Common height is init to -1
1143 0 : if (stats->nodeStateStats.nCommonHeight > -1)
1144 0 : ui->peerCommonHeight->setText(QString("%1").arg(stats->nodeStateStats.nCommonHeight));
1145 : else
1146 0 : ui->peerCommonHeight->setText(tr("Unknown"));
1147 : }
1148 :
1149 0 : ui->detailWidget->show();
1150 0 : }
1151 :
1152 0 : void RPCConsole::resizeEvent(QResizeEvent *event)
1153 : {
1154 0 : QWidget::resizeEvent(event);
1155 0 : }
1156 :
1157 0 : void RPCConsole::showEvent(QShowEvent *event)
1158 : {
1159 0 : QWidget::showEvent(event);
1160 :
1161 0 : if (!clientModel || !clientModel->getPeerTableModel())
1162 : return;
1163 :
1164 : // start PeerTableModel auto refresh
1165 0 : clientModel->getPeerTableModel()->startAutoRefresh();
1166 0 : }
1167 :
1168 0 : void RPCConsole::hideEvent(QHideEvent *event)
1169 : {
1170 0 : QWidget::hideEvent(event);
1171 :
1172 0 : if (!clientModel || !clientModel->getPeerTableModel())
1173 : return;
1174 :
1175 : // stop PeerTableModel auto refresh
1176 0 : clientModel->getPeerTableModel()->stopAutoRefresh();
1177 0 : }
1178 :
1179 0 : void RPCConsole::showPeersTableContextMenu(const QPoint& point)
1180 : {
1181 0 : QModelIndex index = ui->peerWidget->indexAt(point);
1182 0 : if (index.isValid())
1183 0 : peersTableContextMenu->exec(QCursor::pos());
1184 0 : }
1185 :
1186 0 : void RPCConsole::showBanTableContextMenu(const QPoint& point)
1187 : {
1188 0 : QModelIndex index = ui->banlistWidget->indexAt(point);
1189 0 : if (index.isValid())
1190 0 : banTableContextMenu->exec(QCursor::pos());
1191 0 : }
1192 :
1193 0 : void RPCConsole::disconnectSelectedNode()
1194 : {
1195 : // Get selected peer addresses
1196 0 : QList<QModelIndex> nodes = GUIUtil::getEntryData(ui->peerWidget, PeerTableModel::NetNodeId);
1197 0 : for(int i = 0; i < nodes.count(); i++)
1198 : {
1199 : // Get currently selected peer address
1200 0 : NodeId id = nodes.at(i).data().toLongLong();
1201 : // Find the node, disconnect it and clear the selected node
1202 0 : if(m_node.disconnectById(id))
1203 0 : clearSelectedNode();
1204 : }
1205 0 : }
1206 :
1207 0 : void RPCConsole::banSelectedNode(int bantime)
1208 : {
1209 0 : if (!clientModel)
1210 : return;
1211 :
1212 : // Get selected peer addresses
1213 0 : QList<QModelIndex> nodes = GUIUtil::getEntryData(ui->peerWidget, PeerTableModel::NetNodeId);
1214 0 : for(int i = 0; i < nodes.count(); i++)
1215 : {
1216 : // Get currently selected peer address
1217 0 : NodeId id = nodes.at(i).data().toLongLong();
1218 :
1219 : // Get currently selected peer address
1220 0 : int detailNodeRow = clientModel->getPeerTableModel()->getRowByNodeId(id);
1221 0 : if (detailNodeRow < 0) return;
1222 :
1223 : // Find possible nodes, ban it and clear the selected node
1224 0 : const CNodeCombinedStats *stats = clientModel->getPeerTableModel()->getNodeStats(detailNodeRow);
1225 0 : if (stats) {
1226 0 : m_node.ban(stats->nodeStats.addr, bantime);
1227 0 : m_node.disconnectByAddress(stats->nodeStats.addr);
1228 : }
1229 0 : }
1230 0 : clearSelectedNode();
1231 0 : clientModel->getBanTableModel()->refresh();
1232 0 : }
1233 :
1234 0 : void RPCConsole::unbanSelectedNode()
1235 : {
1236 0 : if (!clientModel)
1237 : return;
1238 :
1239 : // Get selected ban addresses
1240 0 : QList<QModelIndex> nodes = GUIUtil::getEntryData(ui->banlistWidget, BanTableModel::Address);
1241 0 : for(int i = 0; i < nodes.count(); i++)
1242 : {
1243 : // Get currently selected ban address
1244 0 : QString strNode = nodes.at(i).data().toString();
1245 0 : CSubNet possibleSubnet;
1246 :
1247 0 : LookupSubNet(strNode.toStdString(), possibleSubnet);
1248 0 : if (possibleSubnet.IsValid() && m_node.unban(possibleSubnet))
1249 : {
1250 0 : clientModel->getBanTableModel()->refresh();
1251 : }
1252 0 : }
1253 0 : }
1254 :
1255 0 : void RPCConsole::clearSelectedNode()
1256 : {
1257 0 : ui->peerWidget->selectionModel()->clearSelection();
1258 0 : cachedNodeids.clear();
1259 0 : ui->detailWidget->hide();
1260 0 : ui->peerHeading->setText(tr("Select a peer to view detailed information."));
1261 0 : }
1262 :
1263 0 : void RPCConsole::showOrHideBanTableIfRequired()
1264 : {
1265 0 : if (!clientModel)
1266 : return;
1267 :
1268 0 : bool visible = clientModel->getBanTableModel()->shouldShow();
1269 0 : ui->banlistWidget->setVisible(visible);
1270 0 : ui->banHeading->setVisible(visible);
1271 0 : }
1272 :
1273 0 : void RPCConsole::setTabFocus(enum TabTypes tabType)
1274 : {
1275 0 : ui->tabWidget->setCurrentIndex(int(tabType));
1276 0 : }
1277 :
1278 0 : QString RPCConsole::tabTitle(TabTypes tab_type) const
1279 : {
1280 0 : return ui->tabWidget->tabText(int(tab_type));
1281 : }
1282 :
1283 0 : QKeySequence RPCConsole::tabShortcut(TabTypes tab_type) const
1284 : {
1285 0 : switch (tab_type) {
1286 0 : case TabTypes::INFO: return QKeySequence(Qt::CTRL + Qt::Key_I);
1287 0 : case TabTypes::CONSOLE: return QKeySequence(Qt::CTRL + Qt::Key_T);
1288 0 : case TabTypes::GRAPH: return QKeySequence(Qt::CTRL + Qt::Key_N);
1289 0 : case TabTypes::PEERS: return QKeySequence(Qt::CTRL + Qt::Key_P);
1290 : } // no default case, so the compiler can warn about missing cases
1291 :
1292 0 : assert(false);
1293 0 : }
1294 :
1295 0 : void RPCConsole::updateAlerts(const QString& warnings)
1296 : {
1297 0 : this->ui->label_alerts->setVisible(!warnings.isEmpty());
1298 0 : this->ui->label_alerts->setText(warnings);
1299 0 : }
|