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