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