diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index 1ff263bee2..a36c9bebd7 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -1,1011 +1,1025 @@ // Copyright (c) 2011-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "guiutil.h" #include "bitcoinaddressvalidator.h" #include "bitcoinunits.h" #include "dstencode.h" #include "qvalidatedlineedit.h" #include "walletmodel.h" #include "cashaddr.h" #include "config.h" #include "dstencode.h" #include "fs.h" #include "init.h" #include "policy/policy.h" #include "primitives/transaction.h" #include "protocol.h" #include "script/script.h" #include "script/standard.h" #include "util.h" #include "utilstrencodings.h" #ifdef WIN32 #ifdef _WIN32_WINNT #undef _WIN32_WINNT #endif #define _WIN32_WINNT 0x0501 #ifdef _WIN32_IE #undef _WIN32_IE #endif #define _WIN32_IE 0x0501 #define WIN32_LEAN_AND_MEAN 1 #ifndef NOMINMAX #define NOMINMAX #endif #include "shellapi.h" #include "shlobj.h" #include "shlwapi.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for Qt::mightBeRichText #include #include #if QT_VERSION >= 0x50200 #include #endif static fs::detail::utf8_codecvt_facet utf8; #if defined(Q_OS_MAC) // These Mac includes must be done in the global namespace #include #include extern double NSAppKitVersionNumber; #if !defined(NSAppKitVersionNumber10_8) #define NSAppKitVersionNumber10_8 1187 #endif #if !defined(NSAppKitVersionNumber10_9) #define NSAppKitVersionNumber10_9 1265 #endif #endif namespace GUIUtil { QString dateTimeStr(const QDateTime &date) { return date.date().toString(Qt::SystemLocaleShortDate) + QString(" ") + date.toString("hh:mm"); } QString dateTimeStr(qint64 nTime) { return dateTimeStr(QDateTime::fromTime_t((qint32)nTime)); } QFont fixedPitchFont() { #if QT_VERSION >= 0x50200 return QFontDatabase::systemFont(QFontDatabase::FixedFont); #else QFont font("Monospace"); font.setStyleHint(QFont::Monospace); return font; #endif } static std::string MakeAddrInvalid(std::string addr, const Config &config) { if (addr.size() < 2) { return ""; } // Checksum is at the end of the address. Swapping chars to make it invalid. std::swap(addr[addr.size() - 1], addr[addr.size() - 2]); if (!IsValidDestinationString(addr, config.GetChainParams())) { return addr; } return ""; } std::string DummyAddress(const Config &config) { // Just some dummy data to generate an convincing random-looking (but // consistent) address static const std::vector dummydata = { 0xeb, 0x15, 0x23, 0x1d, 0xfc, 0xeb, 0x60, 0x92, 0x58, 0x86, 0xb6, 0x7d, 0x06, 0x52, 0x99, 0x92, 0x59, 0x15, 0xae, 0xb1}; const CTxDestination dstKey = CKeyID(uint160(dummydata)); return MakeAddrInvalid(EncodeDestination(dstKey, config), config); } // Addresses are stored in the database with the encoding that the client was // configured with at the time of creation. // // This converts to clients current configuration. QString convertToConfiguredAddressFormat(const Config &config, const QString &addr) { if (!IsValidDestinationString(addr.toStdString(), config.GetChainParams())) { // We have something sketchy as input. Do not try to convert. return addr; } CTxDestination dst = DecodeDestination(addr.toStdString(), config.GetChainParams()); return QString::fromStdString(EncodeDestination(dst, config)); } void setupAddressWidget(QValidatedLineEdit *widget, QWidget *parent) { parent->setFocusProxy(widget); widget->setFont(fixedPitchFont()); // We don't want translators to use own addresses in translations // and this is the only place, where this address is supplied. widget->setPlaceholderText( QObject::tr("Enter a Bitcoin address (e.g. %1)") .arg(QString::fromStdString(DummyAddress(GetConfig())))); widget->setValidator( new BitcoinAddressEntryValidator(Params().CashAddrPrefix(), parent)); widget->setCheckValidator(new BitcoinAddressCheckValidator(parent)); } void setupAmountWidget(QLineEdit *widget, QWidget *parent) { QDoubleValidator *amountValidator = new QDoubleValidator(parent); amountValidator->setDecimals(8); amountValidator->setBottom(0.0); widget->setValidator(amountValidator); widget->setAlignment(Qt::AlignRight | Qt::AlignVCenter); } QString bitcoinURIScheme(const CChainParams ¶ms, bool useCashAddr) { if (!useCashAddr) { return "bitcoincash"; } return QString::fromStdString(params.CashAddrPrefix()); } QString bitcoinURIScheme(const Config &config) { return bitcoinURIScheme(config.GetChainParams(), config.UseCashAddrEncoding()); } static bool IsCashAddrEncoded(const QUrl &uri) { const std::string addr = (uri.scheme() + ":" + uri.path()).toStdString(); auto decoded = cashaddr::Decode(addr, ""); return !decoded.first.empty(); } bool parseBitcoinURI(const QString &scheme, const QUrl &uri, SendCoinsRecipient *out) { // return if URI has wrong scheme. if (!uri.isValid() || uri.scheme() != scheme) { return false; } SendCoinsRecipient rv; if (IsCashAddrEncoded(uri)) { rv.address = uri.scheme() + ":" + uri.path(); } else { // strip out uri scheme for base58 encoded addresses rv.address = uri.path(); } // Trim any following forward slash which may have been added by the OS if (rv.address.endsWith("/")) { rv.address.truncate(rv.address.length() - 1); } rv.amount = Amount::zero(); QUrlQuery uriQuery(uri); QList> items = uriQuery.queryItems(); for (QList>::iterator i = items.begin(); i != items.end(); i++) { bool fShouldReturnFalse = false; if (i->first.startsWith("req-")) { i->first.remove(0, 4); fShouldReturnFalse = true; } if (i->first == "label") { rv.label = i->second; fShouldReturnFalse = false; } if (i->first == "message") { rv.message = i->second; fShouldReturnFalse = false; } else if (i->first == "amount") { if (!i->second.isEmpty()) { if (!BitcoinUnits::parse(BitcoinUnits::BCH, i->second, &rv.amount)) { return false; } } fShouldReturnFalse = false; } if (fShouldReturnFalse) { return false; } } if (out) { *out = rv; } return true; } bool parseBitcoinURI(const QString &scheme, QString uri, SendCoinsRecipient *out) { // // Cannot handle this later, because bitcoincash:// // will cause Qt to see the part after // as host, // which will lower-case it (and thus invalidate the address). if (uri.startsWith(scheme + "://", Qt::CaseInsensitive)) { uri.replace(0, scheme.length() + 3, scheme + ":"); } QUrl uriInstance(uri); return parseBitcoinURI(scheme, uriInstance, out); } QString formatBitcoinURI(const Config &config, const SendCoinsRecipient &info) { QString ret = info.address; if (!config.UseCashAddrEncoding()) { // prefix address with uri scheme for base58 encoded addresses. ret = (bitcoinURIScheme(config) + ":%1").arg(ret); } int paramCount = 0; if (info.amount != Amount::zero()) { ret += QString("?amount=%1") .arg(BitcoinUnits::format(BitcoinUnits::BCH, info.amount, false, BitcoinUnits::separatorNever)); paramCount++; } if (!info.label.isEmpty()) { QString lbl(QUrl::toPercentEncoding(info.label)); ret += QString("%1label=%2").arg(paramCount == 0 ? "?" : "&").arg(lbl); paramCount++; } if (!info.message.isEmpty()) { QString msg(QUrl::toPercentEncoding(info.message)); ret += QString("%1message=%2").arg(paramCount == 0 ? "?" : "&").arg(msg); paramCount++; } return ret; } bool isDust(const QString &address, const Amount amount, const CChainParams &chainParams) { CTxDestination dest = DecodeDestination(address.toStdString(), chainParams); CScript script = GetScriptForDestination(dest); CTxOut txOut(amount, script); return txOut.IsDust(dustRelayFee); } QString HtmlEscape(const QString &str, bool fMultiLine) { QString escaped = str.toHtmlEscaped(); if (fMultiLine) { escaped = escaped.replace("\n", "
\n"); } return escaped; } QString HtmlEscape(const std::string &str, bool fMultiLine) { return HtmlEscape(QString::fromStdString(str), fMultiLine); } void copyEntryData(QAbstractItemView *view, int column, int role) { if (!view || !view->selectionModel()) { return; } QModelIndexList selection = view->selectionModel()->selectedRows(column); if (!selection.isEmpty()) { // Copy first item setClipboard(selection.at(0).data(role).toString()); } } QList getEntryData(QAbstractItemView *view, int column) { if (!view || !view->selectionModel()) { return QList(); } return view->selectionModel()->selectedRows(column); } QString getSaveFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedSuffixOut) { QString selectedFilter; QString myDir; // Default to user documents location if (dir.isEmpty()) { myDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); } else { myDir = dir; } /* Directly convert path to native OS path separators */ QString result = QDir::toNativeSeparators(QFileDialog::getSaveFileName( parent, caption, myDir, filter, &selectedFilter)); /* Extract first suffix from filter pattern "Description (*.foo)" or * "Description (*.foo *.bar ...) */ QRegExp filter_re(".* \\(\\*\\.(.*)[ \\)]"); QString selectedSuffix; if (filter_re.exactMatch(selectedFilter)) { selectedSuffix = filter_re.cap(1); } /* Add suffix if needed */ QFileInfo info(result); if (!result.isEmpty()) { if (info.suffix().isEmpty() && !selectedSuffix.isEmpty()) { /* No suffix specified, add selected suffix */ if (!result.endsWith(".")) { result.append("."); } result.append(selectedSuffix); } } /* Return selected suffix if asked to */ if (selectedSuffixOut) { *selectedSuffixOut = selectedSuffix; } return result; } QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedSuffixOut) { QString selectedFilter; QString myDir; // Default to user documents location if (dir.isEmpty()) { myDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); } else { myDir = dir; } /* Directly convert path to native OS path separators */ QString result = QDir::toNativeSeparators(QFileDialog::getOpenFileName( parent, caption, myDir, filter, &selectedFilter)); if (selectedSuffixOut) { /* Extract first suffix from filter pattern "Description (*.foo)" or * "Description (*.foo *.bar ...) */ QRegExp filter_re(".* \\(\\*\\.(.*)[ \\)]"); QString selectedSuffix; if (filter_re.exactMatch(selectedFilter)) { selectedSuffix = filter_re.cap(1); } *selectedSuffixOut = selectedSuffix; } return result; } Qt::ConnectionType blockingGUIThreadConnection() { if (QThread::currentThread() != qApp->thread()) { return Qt::BlockingQueuedConnection; } else { return Qt::DirectConnection; } } bool checkPoint(const QPoint &p, const QWidget *w) { QWidget *atW = QApplication::widgetAt(w->mapToGlobal(p)); if (!atW) { return false; } return atW->topLevelWidget() == w; } bool isObscured(QWidget *w) { return !(checkPoint(QPoint(0, 0), w) && checkPoint(QPoint(w->width() - 1, 0), w) && checkPoint(QPoint(0, w->height() - 1), w) && checkPoint(QPoint(w->width() - 1, w->height() - 1), w) && checkPoint(QPoint(w->width() / 2, w->height() / 2), w)); } void openDebugLogfile() { fs::path pathDebug = GetDataDir() / "debug.log"; /* Open debug.log with the associated application */ if (fs::exists(pathDebug)) { QDesktopServices::openUrl( QUrl::fromLocalFile(boostPathToQString(pathDebug))); } } void SubstituteFonts(const QString &language) { #if defined(Q_OS_MAC) // Background: // OSX's default font changed in 10.9 and Qt is unable to find it with its // usual fallback methods when building against the 10.7 sdk or lower. // The 10.8 SDK added a function to let it find the correct fallback font. // If this fallback is not properly loaded, some characters may fail to // render correctly. // // The same thing happened with 10.10. .Helvetica Neue DeskInterface is now // default. // // Solution: If building with the 10.7 SDK or lower and the user's platform // is 10.9 or higher at runtime, substitute the correct font. This needs to // happen before the QApplication is created. #if defined(MAC_OS_X_VERSION_MAX_ALLOWED) && \ MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_8 if (floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_8) { if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_9) { /* On a 10.9 - 10.9.x system */ QFont::insertSubstitution(".Lucida Grande UI", "Lucida Grande"); } else { /* 10.10 or later system */ if (language == "zh_CN" || language == "zh_TW" || language == "zh_HK") { // traditional or simplified Chinese QFont::insertSubstitution(".Helvetica Neue DeskInterface", "Heiti SC"); } else if (language == "ja") { // Japanesee QFont::insertSubstitution(".Helvetica Neue DeskInterface", "Songti SC"); } else { QFont::insertSubstitution(".Helvetica Neue DeskInterface", "Lucida Grande"); } } } #endif #endif } ToolTipToRichTextFilter::ToolTipToRichTextFilter(int _size_threshold, QObject *parent) : QObject(parent), size_threshold(_size_threshold) {} bool ToolTipToRichTextFilter::eventFilter(QObject *obj, QEvent *evt) { if (evt->type() == QEvent::ToolTipChange) { QWidget *widget = static_cast(obj); QString tooltip = widget->toolTip(); if (tooltip.size() > size_threshold && !tooltip.startsWith(" to make sure Qt detects this as rich text // Escape the current message as HTML and replace \n by
tooltip = "" + HtmlEscape(tooltip, true) + ""; widget->setToolTip(tooltip); return true; } } return QObject::eventFilter(obj, evt); } void TableViewLastColumnResizingFixer::connectViewHeadersSignals() { connect(tableView->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), this, SLOT(on_sectionResized(int, int, int))); connect(tableView->horizontalHeader(), SIGNAL(geometriesChanged()), this, SLOT(on_geometriesChanged())); } // We need to disconnect these while handling the resize events, otherwise we // can enter infinite loops. void TableViewLastColumnResizingFixer::disconnectViewHeadersSignals() { disconnect(tableView->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), this, SLOT(on_sectionResized(int, int, int))); disconnect(tableView->horizontalHeader(), SIGNAL(geometriesChanged()), this, SLOT(on_geometriesChanged())); } // Setup the resize mode, handles compatibility for Qt5 and below as the method // signatures changed. // Refactored here for readability. void TableViewLastColumnResizingFixer::setViewHeaderResizeMode( int logicalIndex, QHeaderView::ResizeMode resizeMode) { tableView->horizontalHeader()->setSectionResizeMode(logicalIndex, resizeMode); } void TableViewLastColumnResizingFixer::resizeColumn(int nColumnIndex, int width) { tableView->setColumnWidth(nColumnIndex, width); tableView->horizontalHeader()->resizeSection(nColumnIndex, width); } int TableViewLastColumnResizingFixer::getColumnsWidth() { int nColumnsWidthSum = 0; for (int i = 0; i < columnCount; i++) { nColumnsWidthSum += tableView->horizontalHeader()->sectionSize(i); } return nColumnsWidthSum; } int TableViewLastColumnResizingFixer::getAvailableWidthForColumn(int column) { int nResult = lastColumnMinimumWidth; int nTableWidth = tableView->horizontalHeader()->width(); if (nTableWidth > 0) { int nOtherColsWidth = getColumnsWidth() - tableView->horizontalHeader()->sectionSize(column); nResult = std::max(nResult, nTableWidth - nOtherColsWidth); } return nResult; } // Make sure we don't make the columns wider than the table's viewport width. void TableViewLastColumnResizingFixer::adjustTableColumnsWidth() { disconnectViewHeadersSignals(); resizeColumn(lastColumnIndex, getAvailableWidthForColumn(lastColumnIndex)); connectViewHeadersSignals(); int nTableWidth = tableView->horizontalHeader()->width(); int nColsWidth = getColumnsWidth(); if (nColsWidth > nTableWidth) { resizeColumn(secondToLastColumnIndex, getAvailableWidthForColumn(secondToLastColumnIndex)); } } // Make column use all the space available, useful during window resizing. void TableViewLastColumnResizingFixer::stretchColumnWidth(int column) { disconnectViewHeadersSignals(); resizeColumn(column, getAvailableWidthForColumn(column)); connectViewHeadersSignals(); } // When a section is resized this is a slot-proxy for ajustAmountColumnWidth(). void TableViewLastColumnResizingFixer::on_sectionResized(int logicalIndex, int oldSize, int newSize) { adjustTableColumnsWidth(); int remainingWidth = getAvailableWidthForColumn(logicalIndex); if (newSize > remainingWidth) { resizeColumn(logicalIndex, remainingWidth); } } // When the table's geometry is ready, we manually perform the stretch of the // "Message" column, // as the "Stretch" resize mode does not allow for interactive resizing. void TableViewLastColumnResizingFixer::on_geometriesChanged() { if ((getColumnsWidth() - this->tableView->horizontalHeader()->width()) != 0) { disconnectViewHeadersSignals(); resizeColumn(secondToLastColumnIndex, getAvailableWidthForColumn(secondToLastColumnIndex)); connectViewHeadersSignals(); } } /** * Initializes all internal variables and prepares the * the resize modes of the last 2 columns of the table and */ TableViewLastColumnResizingFixer::TableViewLastColumnResizingFixer( QTableView *table, int lastColMinimumWidth, int allColsMinimumWidth, QObject *parent) : QObject(parent), tableView(table), lastColumnMinimumWidth(lastColMinimumWidth), allColumnsMinimumWidth(allColsMinimumWidth) { columnCount = tableView->horizontalHeader()->count(); lastColumnIndex = columnCount - 1; secondToLastColumnIndex = columnCount - 2; tableView->horizontalHeader()->setMinimumSectionSize( allColumnsMinimumWidth); setViewHeaderResizeMode(secondToLastColumnIndex, QHeaderView::Interactive); setViewHeaderResizeMode(lastColumnIndex, QHeaderView::Interactive); } #ifdef WIN32 static fs::path StartupShortcutPath() { std::string chain = gArgs.GetChainName(); if (chain == CBaseChainParams::MAIN) { return GetSpecialFolderPath(CSIDL_STARTUP) / "Bitcoin.lnk"; } // Remove this special case when CBaseChainParams::TESTNET = "testnet4" if (chain == CBaseChainParams::TESTNET) { return GetSpecialFolderPath(CSIDL_STARTUP) / "Bitcoin (testnet).lnk"; } return GetSpecialFolderPath(CSIDL_STARTUP) / strprintf("Bitcoin (%s).lnk", chain); } bool GetStartOnSystemStartup() { // check for Bitcoin*.lnk return fs::exists(StartupShortcutPath()); } bool SetStartOnSystemStartup(bool fAutoStart) { // If the shortcut exists already, remove it for updating fs::remove(StartupShortcutPath()); if (fAutoStart) { CoInitialize(nullptr); // Get a pointer to the IShellLink interface. IShellLink *psl = nullptr; HRESULT hres = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLink, reinterpret_cast(&psl)); if (SUCCEEDED(hres)) { // Get the current executable path TCHAR pszExePath[MAX_PATH]; GetModuleFileName(nullptr, pszExePath, sizeof(pszExePath)); // Start client minimized QString strArgs = "-min"; // Set -testnet /-regtest options strArgs += QString::fromStdString(strprintf( " -testnet=%d -regtest=%d", gArgs.GetBoolArg("-testnet", false), gArgs.GetBoolArg("-regtest", false))); #ifdef UNICODE boost::scoped_array args(new TCHAR[strArgs.length() + 1]); // Convert the QString to TCHAR* strArgs.toWCharArray(args.get()); // Add missing '\0'-termination to string args[strArgs.length()] = '\0'; #endif // Set the path to the shortcut target psl->SetPath(pszExePath); PathRemoveFileSpec(pszExePath); psl->SetWorkingDirectory(pszExePath); psl->SetShowCmd(SW_SHOWMINNOACTIVE); #ifndef UNICODE psl->SetArguments(strArgs.toStdString().c_str()); #else psl->SetArguments(args.get()); #endif // Query IShellLink for the IPersistFile interface for // saving the shortcut in persistent storage. IPersistFile *ppf = nullptr; hres = psl->QueryInterface(IID_IPersistFile, reinterpret_cast(&ppf)); if (SUCCEEDED(hres)) { WCHAR pwsz[MAX_PATH]; // Ensure that the string is ANSI. MultiByteToWideChar(CP_ACP, 0, StartupShortcutPath().string().c_str(), -1, pwsz, MAX_PATH); // Save the link by calling IPersistFile::Save. hres = ppf->Save(pwsz, TRUE); ppf->Release(); psl->Release(); CoUninitialize(); return true; } psl->Release(); } CoUninitialize(); return false; } return true; } #elif defined(Q_OS_LINUX) // Follow the Desktop Application Autostart Spec: // http://standards.freedesktop.org/autostart-spec/autostart-spec-latest.html static fs::path GetAutostartDir() { char *pszConfigHome = getenv("XDG_CONFIG_HOME"); if (pszConfigHome) { return fs::path(pszConfigHome) / "autostart"; } char *pszHome = getenv("HOME"); if (pszHome) { return fs::path(pszHome) / ".config" / "autostart"; } return fs::path(); } static fs::path GetAutostartFilePath() { std::string chain = gArgs.GetChainName(); if (chain == CBaseChainParams::MAIN) { return GetAutostartDir() / "bitcoin.desktop"; } return GetAutostartDir() / strprintf("bitcoin-%s.lnk", chain); } bool GetStartOnSystemStartup() { fs::ifstream optionFile(GetAutostartFilePath()); if (!optionFile.good()) { return false; } // Scan through file for "Hidden=true": std::string line; while (!optionFile.eof()) { getline(optionFile, line); if (line.find("Hidden") != std::string::npos && line.find("true") != std::string::npos) { return false; } } optionFile.close(); return true; } bool SetStartOnSystemStartup(bool fAutoStart) { if (!fAutoStart) { fs::remove(GetAutostartFilePath()); } else { char pszExePath[MAX_PATH + 1]; memset(pszExePath, 0, sizeof(pszExePath)); if (readlink("/proc/self/exe", pszExePath, sizeof(pszExePath) - 1) == -1) { return false; } fs::create_directories(GetAutostartDir()); fs::ofstream optionFile(GetAutostartFilePath(), std::ios_base::out | std::ios_base::trunc); if (!optionFile.good()) { return false; } std::string chain = gArgs.GetChainName(); // Write a bitcoin.desktop file to the autostart directory: optionFile << "[Desktop Entry]\n"; optionFile << "Type=Application\n"; if (chain == CBaseChainParams::MAIN) { optionFile << "Name=Bitcoin\n"; } else { optionFile << strprintf("Name=Bitcoin (%s)\n", chain); } optionFile << "Exec=" << pszExePath << strprintf(" -min -testnet=%d -regtest=%d\n", gArgs.GetBoolArg("-testnet", false), gArgs.GetBoolArg("-regtest", false)); optionFile << "Terminal=false\n"; optionFile << "Hidden=false\n"; optionFile.close(); } return true; } #elif defined(Q_OS_MAC) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" // based on: // https://github.com/Mozketo/LaunchAtLoginController/blob/master/LaunchAtLoginController.m // NB: caller must release returned ref if it's not NULL LSSharedFileListItemRef findStartupItemInList(LSSharedFileListRef list, CFURLRef findUrl); LSSharedFileListItemRef findStartupItemInList(LSSharedFileListRef list, CFURLRef findUrl) { LSSharedFileListItemRef foundItem = nullptr; // loop through the list of startup items and try to find the bitcoin app CFArrayRef listSnapshot = LSSharedFileListCopySnapshot(list, nullptr); for (int i = 0; !foundItem && i < CFArrayGetCount(listSnapshot); ++i) { LSSharedFileListItemRef item = (LSSharedFileListItemRef)CFArrayGetValueAtIndex(listSnapshot, i); UInt32 resolutionFlags = kLSSharedFileListNoUserInteraction | kLSSharedFileListDoNotMountVolumes; CFURLRef currentItemURL = nullptr; #if defined(MAC_OS_X_VERSION_MAX_ALLOWED) && \ MAC_OS_X_VERSION_MAX_ALLOWED >= 10100 if (&LSSharedFileListItemCopyResolvedURL) { currentItemURL = LSSharedFileListItemCopyResolvedURL( item, resolutionFlags, nullptr); } #if defined(MAC_OS_X_VERSION_MIN_REQUIRED) && \ MAC_OS_X_VERSION_MIN_REQUIRED < 10100 else { LSSharedFileListItemResolve(item, resolutionFlags, ¤tItemURL, nullptr); } #endif #else LSSharedFileListItemResolve(item, resolutionFlags, ¤tItemURL, nullptr); #endif if (currentItemURL && CFEqual(currentItemURL, findUrl)) { // found CFRetain(foundItem = item); } if (currentItemURL) { CFRelease(currentItemURL); } } CFRelease(listSnapshot); return foundItem; } bool GetStartOnSystemStartup() { CFURLRef bitcoinAppUrl = CFBundleCopyBundleURL(CFBundleGetMainBundle()); LSSharedFileListRef loginItems = LSSharedFileListCreate( nullptr, kLSSharedFileListSessionLoginItems, nullptr); LSSharedFileListItemRef foundItem = findStartupItemInList(loginItems, bitcoinAppUrl); // findStartupItemInList retains the item it returned, need to release if (foundItem) { CFRelease(foundItem); } CFRelease(loginItems); CFRelease(bitcoinAppUrl); return foundItem; } bool SetStartOnSystemStartup(bool fAutoStart) { CFURLRef bitcoinAppUrl = CFBundleCopyBundleURL(CFBundleGetMainBundle()); LSSharedFileListRef loginItems = LSSharedFileListCreate( nullptr, kLSSharedFileListSessionLoginItems, nullptr); LSSharedFileListItemRef foundItem = findStartupItemInList(loginItems, bitcoinAppUrl); if (fAutoStart && !foundItem) { // add bitcoin app to startup item list LSSharedFileListInsertItemURL(loginItems, kLSSharedFileListItemBeforeFirst, nullptr, nullptr, bitcoinAppUrl, nullptr, nullptr); } else if (!fAutoStart && foundItem) { // remove item LSSharedFileListItemRemove(loginItems, foundItem); } // findStartupItemInList retains the item it returned, need to release if (foundItem) { CFRelease(foundItem); } CFRelease(loginItems); CFRelease(bitcoinAppUrl); return true; } #pragma GCC diagnostic pop #else bool GetStartOnSystemStartup() { return false; } bool SetStartOnSystemStartup(bool fAutoStart) { return false; } #endif void setClipboard(const QString &str) { QApplication::clipboard()->setText(str, QClipboard::Clipboard); QApplication::clipboard()->setText(str, QClipboard::Selection); } fs::path qstringToBoostPath(const QString &path) { return fs::path(path.toStdString(), utf8); } QString boostPathToQString(const fs::path &path) { return QString::fromStdString(path.string(utf8)); } QString formatDurationStr(int secs) { QStringList strList; int days = secs / 86400; int hours = (secs % 86400) / 3600; int mins = (secs % 3600) / 60; int seconds = secs % 60; if (days) { strList.append(QString(QObject::tr("%1 d")).arg(days)); } if (hours) { strList.append(QString(QObject::tr("%1 h")).arg(hours)); } if (mins) { strList.append(QString(QObject::tr("%1 m")).arg(mins)); } if (seconds || (!days && !hours && !mins)) { strList.append(QString(QObject::tr("%1 s")).arg(seconds)); } return strList.join(" "); } QString formatServicesStr(quint64 mask) { QStringList strList; // Don't display experimental service bits for (uint64_t check = 1; check <= NODE_LAST_NON_EXPERIMENTAL_SERVICE_BIT; check <<= 1) { if (mask & check) { switch (check) { case NODE_NETWORK: strList.append("NETWORK"); break; case NODE_GETUTXO: strList.append("GETUTXO"); break; case NODE_BLOOM: strList.append("BLOOM"); break; case NODE_XTHIN: strList.append("XTHIN"); break; case NODE_BITCOIN_CASH: strList.append("CASH"); break; case NODE_NETWORK_LIMITED: strList.append("LIMITED"); break; default: strList.append(QString("%1[%2]").arg("UNKNOWN").arg(check)); } } } if (strList.size()) { return strList.join(" & "); } else { return QObject::tr("None"); } } QString formatPingTime(double dPingTime) { return (dPingTime == std::numeric_limits::max() / 1e6 || dPingTime == 0) ? QObject::tr("N/A") : QString(QObject::tr("%1 ms")) .arg(QString::number((int)(dPingTime * 1000), 10)); } QString formatTimeOffset(int64_t nTimeOffset) { return QString(QObject::tr("%1 s")) .arg(QString::number((int)nTimeOffset, 10)); } QString formatNiceTimeOffset(qint64 secs) { // Represent time from last generated block in human readable text QString timeBehindText; const int HOUR_IN_SECONDS = 60 * 60; const int DAY_IN_SECONDS = 24 * 60 * 60; const int WEEK_IN_SECONDS = 7 * 24 * 60 * 60; // Average length of year in Gregorian calendar const int YEAR_IN_SECONDS = 31556952; if (secs < 60) { timeBehindText = QObject::tr("%n second(s)", "", secs); } else if (secs < 2 * HOUR_IN_SECONDS) { timeBehindText = QObject::tr("%n minute(s)", "", secs / 60); } else if (secs < 2 * DAY_IN_SECONDS) { timeBehindText = QObject::tr("%n hour(s)", "", secs / HOUR_IN_SECONDS); } else if (secs < 2 * WEEK_IN_SECONDS) { timeBehindText = QObject::tr("%n day(s)", "", secs / DAY_IN_SECONDS); } else if (secs < YEAR_IN_SECONDS) { timeBehindText = QObject::tr("%n week(s)", "", secs / WEEK_IN_SECONDS); } else { qint64 years = secs / YEAR_IN_SECONDS; qint64 remainder = secs % YEAR_IN_SECONDS; timeBehindText = QObject::tr("%1 and %2") .arg(QObject::tr("%n year(s)", "", years)) .arg(QObject::tr("%n week(s)", "", remainder / WEEK_IN_SECONDS)); } return timeBehindText; } +QString formatBytes(uint64_t bytes) { + if (bytes < 1024) { + return QString(QObject::tr("%1 B")).arg(bytes); + } + if (bytes < 1024 * 1024) { + return QString(QObject::tr("%1 KB")).arg(bytes / 1024); + } + if (bytes < 1024 * 1024 * 1024) { + return QString(QObject::tr("%1 MB")).arg(bytes / 1024 / 1024); + } + + return QString(QObject::tr("%1 GB")).arg(bytes / 1024 / 1024 / 1024); +} + void ClickableLabel::mouseReleaseEvent(QMouseEvent *event) { Q_EMIT clicked(event->pos()); } void ClickableProgressBar::mouseReleaseEvent(QMouseEvent *event) { Q_EMIT clicked(event->pos()); } } // namespace GUIUtil diff --git a/src/qt/guiutil.h b/src/qt/guiutil.h index 740c96f7ba..abb270b208 100644 --- a/src/qt/guiutil.h +++ b/src/qt/guiutil.h @@ -1,275 +1,277 @@ // Copyright (c) 2011-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #ifndef BITCOIN_QT_GUIUTIL_H #define BITCOIN_QT_GUIUTIL_H #include "amount.h" #include "fs.h" #include #include #include #include #include #include #include #include class QValidatedLineEdit; class SendCoinsRecipient; class CChainParams; class Config; QT_BEGIN_NAMESPACE class QAbstractItemView; class QDateTime; class QFont; class QLineEdit; class QUrl; class QWidget; QT_END_NAMESPACE /** Utility functions used by the Bitcoin Qt UI. */ namespace GUIUtil { // Create human-readable string from date QString dateTimeStr(const QDateTime &datetime); QString dateTimeStr(qint64 nTime); // Return a monospace font QFont fixedPitchFont(); // Generate an invalid, but convincing address. std::string DummyAddress(const Config &config); // Convert an address into the user chosen format QString convertToConfiguredAddressFormat(const Config &config, const QString &addr); // Set up widgets for address and amounts void setupAddressWidget(QValidatedLineEdit *widget, QWidget *parent); void setupAmountWidget(QLineEdit *widget, QWidget *parent); QString bitcoinURIScheme(const CChainParams &, bool useCashAddr); QString bitcoinURIScheme(const Config &); // Parse "bitcoincash:" URI into recipient object, return true on successful // parsing bool parseBitcoinURI(const QString &scheme, const QUrl &uri, SendCoinsRecipient *out); bool parseBitcoinURI(const QString &scheme, QString uri, SendCoinsRecipient *out); QString formatBitcoinURI(const Config &config, const SendCoinsRecipient &info); // Returns true if given address+amount meets "dust" definition bool isDust(const QString &address, const Amount amount, const CChainParams &chainParams); // HTML escaping for rich text controls QString HtmlEscape(const QString &str, bool fMultiLine = false); QString HtmlEscape(const std::string &str, bool fMultiLine = false); /** Copy a field of the currently selected entry of a view to the clipboard. Does nothing if nothing is selected. @param[in] column Data column to extract from the model @param[in] role Data role to extract from the model @see TransactionView::copyLabel, TransactionView::copyAmount, TransactionView::copyAddress */ void copyEntryData(QAbstractItemView *view, int column, int role = Qt::EditRole); /** Return a field of the currently selected entry as a QString. Does nothing if nothing is selected. @param[in] column Data column to extract from the model @see TransactionView::copyLabel, TransactionView::copyAmount, TransactionView::copyAddress */ QList getEntryData(QAbstractItemView *view, int column); void setClipboard(const QString &str); /** Get save filename, mimics QFileDialog::getSaveFileName, except that it appends a default suffix when no suffix is provided by the user. @param[in] parent Parent window (or 0) @param[in] caption Window caption (or empty, for default) @param[in] dir Starting directory (or empty, to default to documents directory) @param[in] filter Filter specification such as "Comma Separated Files (*.csv)" @param[out] selectedSuffixOut Pointer to return the suffix (file type) that was selected (or 0). Can be useful when choosing the save file format based on suffix. */ QString getSaveFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedSuffixOut); /** Get open filename, convenience wrapper for QFileDialog::getOpenFileName. @param[in] parent Parent window (or 0) @param[in] caption Window caption (or empty, for default) @param[in] dir Starting directory (or empty, to default to documents directory) @param[in] filter Filter specification such as "Comma Separated Files (*.csv)" @param[out] selectedSuffixOut Pointer to return the suffix (file type) that was selected (or 0). Can be useful when choosing the save file format based on suffix. */ QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedSuffixOut); /** Get connection type to call object slot in GUI thread with invokeMethod. The call will be blocking. @returns If called from the GUI thread, return a Qt::DirectConnection. If called from another thread, return a Qt::BlockingQueuedConnection. */ Qt::ConnectionType blockingGUIThreadConnection(); // Determine whether a widget is hidden behind other windows bool isObscured(QWidget *w); // Open debug.log void openDebugLogfile(); // Replace invalid default fonts with known good ones void SubstituteFonts(const QString &language); /** Qt event filter that intercepts ToolTipChange events, and replaces the * tooltip with a rich text representation if needed. This assures that Qt can * word-wrap long tooltip messages. Tooltips longer than the provided size * threshold (in characters) are wrapped. */ class ToolTipToRichTextFilter : public QObject { Q_OBJECT public: explicit ToolTipToRichTextFilter(int size_threshold, QObject *parent = 0); protected: bool eventFilter(QObject *obj, QEvent *evt) override; private: int size_threshold; }; /** * Makes a QTableView last column feel as if it was being resized from its left * border. * Also makes sure the column widths are never larger than the table's viewport. * In Qt, all columns are resizable from the right, but it's not intuitive * resizing the last column from the right. * Usually our second to last columns behave as if stretched, and when on strech * mode, columns aren't resizable interactively or programmatically. * * This helper object takes care of this issue. * */ class TableViewLastColumnResizingFixer : public QObject { Q_OBJECT public: TableViewLastColumnResizingFixer(QTableView *table, int lastColMinimumWidth, int allColsMinimumWidth, QObject *parent); void stretchColumnWidth(int column); private: QTableView *tableView; int lastColumnMinimumWidth; int allColumnsMinimumWidth; int lastColumnIndex; int columnCount; int secondToLastColumnIndex; void adjustTableColumnsWidth(); int getAvailableWidthForColumn(int column); int getColumnsWidth(); void connectViewHeadersSignals(); void disconnectViewHeadersSignals(); void setViewHeaderResizeMode(int logicalIndex, QHeaderView::ResizeMode resizeMode); void resizeColumn(int nColumnIndex, int width); private Q_SLOTS: void on_sectionResized(int logicalIndex, int oldSize, int newSize); void on_geometriesChanged(); }; bool GetStartOnSystemStartup(); bool SetStartOnSystemStartup(bool fAutoStart); /* Convert QString to OS specific boost path through UTF-8 */ fs::path qstringToBoostPath(const QString &path); /* Convert OS specific boost path to QString through UTF-8 */ QString boostPathToQString(const fs::path &path); /* Convert seconds into a QString with days, hours, mins, secs */ QString formatDurationStr(int secs); /* Format CNodeStats.nServices bitmask into a user-readable string */ QString formatServicesStr(quint64 mask); /* Format a CNodeCombinedStats.dPingTime into a user-readable string or display * N/A, if 0*/ QString formatPingTime(double dPingTime); /* Format a CNodeCombinedStats.nTimeOffset into a user-readable string. */ QString formatTimeOffset(int64_t nTimeOffset); QString formatNiceTimeOffset(qint64 secs); +QString formatBytes(uint64_t bytes); + class ClickableLabel : public QLabel { Q_OBJECT Q_SIGNALS: /** Emitted when the label is clicked. The relative mouse coordinates of the * click are passed to the signal. */ void clicked(const QPoint &point); protected: void mouseReleaseEvent(QMouseEvent *event) override; }; class ClickableProgressBar : public QProgressBar { Q_OBJECT Q_SIGNALS: /** Emitted when the progressbar is clicked. The relative mouse coordinates * of the click are passed to the signal. */ void clicked(const QPoint &point); protected: void mouseReleaseEvent(QMouseEvent *event) override; }; #if defined(Q_OS_MAC) // workaround for Qt OSX Bug: // https://bugreports.qt-project.org/browse/QTBUG-15631 // QProgressBar uses around 10% CPU even when app is in background class ProgressBar : public ClickableProgressBar { bool event(QEvent *e) override { return (e->type() != QEvent::StyleAnimationUpdate) ? QProgressBar::event(e) : false; } }; #else typedef ClickableProgressBar ProgressBar; #endif } // namespace GUIUtil #endif // BITCOIN_QT_GUIUTIL_H diff --git a/src/qt/peertablemodel.cpp b/src/qt/peertablemodel.cpp index 7369683957..f9a863c18a 100644 --- a/src/qt/peertablemodel.cpp +++ b/src/qt/peertablemodel.cpp @@ -1,216 +1,230 @@ // Copyright (c) 2011-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "peertablemodel.h" #include "clientmodel.h" #include "guiconstants.h" #include "guiutil.h" #include "sync.h" #include "validation.h" // for cs_main #include #include #include bool NodeLessThan::operator()(const CNodeCombinedStats &left, const CNodeCombinedStats &right) const { const CNodeStats *pLeft = &(left.nodeStats); const CNodeStats *pRight = &(right.nodeStats); if (order == Qt::DescendingOrder) std::swap(pLeft, pRight); switch (column) { case PeerTableModel::NetNodeId: return pLeft->nodeid < pRight->nodeid; case PeerTableModel::Address: return pLeft->addrName.compare(pRight->addrName) < 0; case PeerTableModel::Subversion: return pLeft->cleanSubVer.compare(pRight->cleanSubVer) < 0; case PeerTableModel::Ping: return pLeft->dMinPing < pRight->dMinPing; + case PeerTableModel::Sent: + return pLeft->nSendBytes < pRight->nSendBytes; + case PeerTableModel::Received: + return pLeft->nRecvBytes < pRight->nRecvBytes; } return false; } // private implementation class PeerTablePriv { public: /** Local cache of peer information */ QList cachedNodeStats; /** Column to sort nodes by */ int sortColumn; /** Order (ascending or descending) to sort nodes by */ Qt::SortOrder sortOrder; /** Index of rows by node ID */ std::map mapNodeRows; /** Pull a full list of peers from vNodes into our cache */ void refreshPeers() { { cachedNodeStats.clear(); std::vector vstats; if (g_connman) g_connman->GetNodeStats(vstats); cachedNodeStats.reserve(vstats.size()); for (const CNodeStats &nodestats : vstats) { CNodeCombinedStats stats; stats.nodeStateStats.nMisbehavior = 0; stats.nodeStateStats.nSyncHeight = -1; stats.nodeStateStats.nCommonHeight = -1; stats.fNodeStateStatsAvailable = false; stats.nodeStats = nodestats; cachedNodeStats.append(stats); } } // Try to retrieve the CNodeStateStats for each node. { TRY_LOCK(cs_main, lockMain); if (lockMain) { for (CNodeCombinedStats &stats : cachedNodeStats) { stats.fNodeStateStatsAvailable = GetNodeStateStats( stats.nodeStats.nodeid, stats.nodeStateStats); } } } if (sortColumn >= 0) // sort cacheNodeStats (use stable sort to prevent rows jumping // around unnecessarily) qStableSort(cachedNodeStats.begin(), cachedNodeStats.end(), NodeLessThan(sortColumn, sortOrder)); // build index map mapNodeRows.clear(); int row = 0; for (const CNodeCombinedStats &stats : cachedNodeStats) { mapNodeRows.insert( std::pair(stats.nodeStats.nodeid, row++)); } } int size() const { return cachedNodeStats.size(); } CNodeCombinedStats *index(int idx) { if (idx >= 0 && idx < cachedNodeStats.size()) return &cachedNodeStats[idx]; return 0; } }; PeerTableModel::PeerTableModel(ClientModel *parent) : QAbstractTableModel(parent), clientModel(parent), timer(0) { - columns << tr("NodeId") << tr("Node/Service") << tr("User Agent") - << tr("Ping"); + columns << tr("NodeId") << tr("Node/Service") << tr("Ping") << tr("Sent") + << tr("Received") << tr("User Agent"); priv.reset(new PeerTablePriv()); // default to unsorted priv->sortColumn = -1; // set up timer for auto refresh timer = new QTimer(this); connect(timer, SIGNAL(timeout()), SLOT(refresh())); timer->setInterval(MODEL_UPDATE_DELAY); // load initial data refresh(); } PeerTableModel::~PeerTableModel() { // Intentionally left empty } void PeerTableModel::startAutoRefresh() { timer->start(); } void PeerTableModel::stopAutoRefresh() { timer->stop(); } int PeerTableModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return priv->size(); } int PeerTableModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return columns.length(); } QVariant PeerTableModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); CNodeCombinedStats *rec = static_cast(index.internalPointer()); if (role == Qt::DisplayRole) { switch (index.column()) { case NetNodeId: return (qint64)rec->nodeStats.nodeid; case Address: return QString::fromStdString(rec->nodeStats.addrName); case Subversion: return QString::fromStdString(rec->nodeStats.cleanSubVer); case Ping: return GUIUtil::formatPingTime(rec->nodeStats.dMinPing); + case Sent: + return GUIUtil::formatBytes(rec->nodeStats.nSendBytes); + case Received: + return GUIUtil::formatBytes(rec->nodeStats.nRecvBytes); } } else if (role == Qt::TextAlignmentRole) { - if (index.column() == Ping) - return (QVariant)(Qt::AlignRight | Qt::AlignVCenter); + switch (index.column()) { + case Ping: + case Sent: + case Received: + return QVariant(Qt::AlignRight | Qt::AlignVCenter); + default: + return QVariant(); + } } return QVariant(); } QVariant PeerTableModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { if (role == Qt::DisplayRole && section < columns.size()) { return columns[section]; } } return QVariant(); } Qt::ItemFlags PeerTableModel::flags(const QModelIndex &index) const { if (!index.isValid()) return 0; Qt::ItemFlags retval = Qt::ItemIsSelectable | Qt::ItemIsEnabled; return retval; } QModelIndex PeerTableModel::index(int row, int column, const QModelIndex &parent) const { Q_UNUSED(parent); CNodeCombinedStats *data = priv->index(row); if (data) return createIndex(row, column, data); return QModelIndex(); } const CNodeCombinedStats *PeerTableModel::getNodeStats(int idx) { return priv->index(idx); } void PeerTableModel::refresh() { Q_EMIT layoutAboutToBeChanged(); priv->refreshPeers(); Q_EMIT layoutChanged(); } int PeerTableModel::getRowByNodeId(NodeId nodeid) { std::map::iterator it = priv->mapNodeRows.find(nodeid); if (it == priv->mapNodeRows.end()) return -1; return it->second; } void PeerTableModel::sort(int column, Qt::SortOrder order) { priv->sortColumn = column; priv->sortOrder = order; refresh(); } diff --git a/src/qt/peertablemodel.h b/src/qt/peertablemodel.h index 118359bc82..13871b1418 100644 --- a/src/qt/peertablemodel.h +++ b/src/qt/peertablemodel.h @@ -1,79 +1,86 @@ // Copyright (c) 2011-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #ifndef BITCOIN_QT_PEERTABLEMODEL_H #define BITCOIN_QT_PEERTABLEMODEL_H #include "net.h" #include "net_processing.h" // For CNodeStateStats #include #include class ClientModel; class PeerTablePriv; QT_BEGIN_NAMESPACE class QTimer; QT_END_NAMESPACE struct CNodeCombinedStats { CNodeStats nodeStats; CNodeStateStats nodeStateStats; bool fNodeStateStatsAvailable; }; class NodeLessThan { public: NodeLessThan(int nColumn, Qt::SortOrder fOrder) : column(nColumn), order(fOrder) {} bool operator()(const CNodeCombinedStats &left, const CNodeCombinedStats &right) const; private: int column; Qt::SortOrder order; }; /** * Qt model providing information about connected peers, similar to the * "getpeerinfo" RPC call. Used by the rpc console UI. */ class PeerTableModel : public QAbstractTableModel { Q_OBJECT public: explicit PeerTableModel(ClientModel *parent = 0); ~PeerTableModel(); const CNodeCombinedStats *getNodeStats(int idx); int getRowByNodeId(NodeId nodeid); void startAutoRefresh(); void stopAutoRefresh(); - enum ColumnIndex { NetNodeId = 0, Address = 1, Subversion = 2, Ping = 3 }; + enum ColumnIndex { + NetNodeId = 0, + Address = 1, + Ping = 2, + Sent = 3, + Received = 4, + Subversion = 5, + }; /** @name Methods overridden from QAbstractTableModel @{*/ int rowCount(const QModelIndex &parent) const override; int columnCount(const QModelIndex &parent) const override; QVariant data(const QModelIndex &index, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; QModelIndex index(int row, int column, const QModelIndex &parent) const override; Qt::ItemFlags flags(const QModelIndex &index) const override; void sort(int column, Qt::SortOrder order) override; /*@}*/ public Q_SLOTS: void refresh(); private: ClientModel *clientModel; QStringList columns; std::unique_ptr priv; QTimer *timer; }; #endif // BITCOIN_QT_PEERTABLEMODEL_H diff --git a/src/qt/rpcconsole.cpp b/src/qt/rpcconsole.cpp index af844988ec..688e637cbb 100644 --- a/src/qt/rpcconsole.cpp +++ b/src/qt/rpcconsole.cpp @@ -1,1441 +1,1429 @@ // Copyright (c) 2011-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #if defined(HAVE_CONFIG_H) #include "config/bitcoin-config.h" #endif #include "rpcconsole.h" #include "ui_debugwindow.h" #include "qt/bantablemodel.h" #include "qt/clientmodel.h" #include "qt/guiutil.h" #include "qt/platformstyle.h" #include "qt/walletmodel.h" #include "chainparams.h" #include "config.h" #include "netbase.h" #include "rpc/client.h" #include "rpc/server.h" #include "util.h" #include #ifdef ENABLE_WALLET #include "wallet/wallet.h" #include #endif #include #include #include #include #include #include #include #include #include #include #include // TODO: add a scrollback limit, as there is currently none // TODO: make it possible to filter out categories (esp debug messages when // implemented) // TODO: receive errors and debug messages through ClientModel const int CONSOLE_HISTORY = 50; const int INITIAL_TRAFFIC_GRAPH_MINS = 30; const QSize FONT_RANGE(4, 40); const char fontSizeSettingsKey[] = "consoleFontSize"; const struct { const char *url; const char *source; } ICON_MAPPING[] = {{"cmd-request", ":/icons/tx_input"}, {"cmd-reply", ":/icons/tx_output"}, {"cmd-error", ":/icons/tx_output"}, {"misc", ":/icons/tx_inout"}, {nullptr, nullptr}}; namespace { // don't add private key handling cmd's to the history const QStringList historyFilter = QStringList() << "importprivkey" << "importmulti" << "signmessagewithprivkey" << "signrawtransaction" << "signrawtransactionwithkey" << "walletpassphrase" << "walletpassphrasechange" << "encryptwallet"; } // namespace /* Object for executing console RPC commands in a separate thread. */ class RPCExecutor : public QObject { Q_OBJECT public Q_SLOTS: void request(const QString &command, const QString &walletID); Q_SIGNALS: void reply(int category, const QString &command); }; /** Class for handling RPC timers * (used for e.g. re-locking the wallet after a timeout) */ class QtRPCTimerBase : public QObject, public RPCTimerBase { Q_OBJECT public: QtRPCTimerBase(std::function &_func, int64_t millis) : func(_func) { timer.setSingleShot(true); connect(&timer, SIGNAL(timeout()), this, SLOT(timeout())); timer.start(millis); } ~QtRPCTimerBase() {} private Q_SLOTS: void timeout() { func(); } private: QTimer timer; std::function func; }; class QtRPCTimerInterface : public RPCTimerInterface { public: ~QtRPCTimerInterface() {} const char *Name() override { return "Qt"; } RPCTimerBase *NewTimer(std::function &func, int64_t millis) override { return new QtRPCTimerBase(func, millis); } }; #include "rpcconsole.moc" /** * Split shell command line into a list of arguments and optionally execute the * command(s). * Aims to emulate \c bash and friends. * * - Command nesting is possible with parenthesis; for example: * validateaddress(getnewaddress()) * - Arguments are delimited with whitespace or comma * - Extra whitespace at the beginning and end and between arguments will be * ignored * - Text can be "double" or 'single' quoted * - The backslash \c \ is used as escape character * - Outside quotes, any character can be escaped * - Within double quotes, only escape \c " and backslashes before a \c " or * another backslash * - Within single quotes, no escaping is possible and no special * interpretation takes place * * @param[out] result stringified Result from the executed command(chain) * @param[in] strCommand Command line to split * @param[in] fExecute set true if you want the command to be executed * @param[out] pstrFilteredOut Command line, filtered to remove any sensitive * data */ bool RPCConsole::RPCParseCommandLine(std::string &strResult, const std::string &strCommand, const bool fExecute, std::string *const pstrFilteredOut, const std::string *walletID) { std::vector> stack; stack.push_back(std::vector()); enum CmdParseState { STATE_EATING_SPACES, STATE_EATING_SPACES_IN_ARG, STATE_EATING_SPACES_IN_BRACKETS, STATE_ARGUMENT, STATE_SINGLEQUOTED, STATE_DOUBLEQUOTED, STATE_ESCAPE_OUTER, STATE_ESCAPE_DOUBLEQUOTED, STATE_COMMAND_EXECUTED, STATE_COMMAND_EXECUTED_INNER } state = STATE_EATING_SPACES; std::string curarg; UniValue lastResult; unsigned nDepthInsideSensitive = 0; size_t filter_begin_pos = 0, chpos; std::vector> filter_ranges; auto add_to_current_stack = [&](const std::string &strArg) { if (stack.back().empty() && (!nDepthInsideSensitive) && historyFilter.contains(QString::fromStdString(strArg), Qt::CaseInsensitive)) { nDepthInsideSensitive = 1; filter_begin_pos = chpos; } // Make sure stack is not empty before adding something if (stack.empty()) { stack.push_back(std::vector()); } stack.back().push_back(strArg); }; auto close_out_params = [&]() { if (nDepthInsideSensitive) { if (!--nDepthInsideSensitive) { assert(filter_begin_pos); filter_ranges.push_back( std::make_pair(filter_begin_pos, chpos)); filter_begin_pos = 0; } } stack.pop_back(); }; std::string strCommandTerminated = strCommand; if (strCommandTerminated.back() != '\n') strCommandTerminated += "\n"; for (chpos = 0; chpos < strCommandTerminated.size(); ++chpos) { char ch = strCommandTerminated[chpos]; switch (state) { case STATE_COMMAND_EXECUTED_INNER: case STATE_COMMAND_EXECUTED: { bool breakParsing = true; switch (ch) { case '[': curarg.clear(); state = STATE_COMMAND_EXECUTED_INNER; break; default: if (state == STATE_COMMAND_EXECUTED_INNER) { if (ch != ']') { // append char to the current argument (which is // also used for the query command) curarg += ch; break; } if (curarg.size() && fExecute) { // if we have a value query, query arrays with // index and objects with a string key UniValue subelement; if (lastResult.isArray()) { for (char argch : curarg) { if (!std::isdigit(argch)) { throw std::runtime_error( "Invalid result query"); } } subelement = lastResult[atoi(curarg.c_str())]; } else if (lastResult.isObject()) { subelement = find_value(lastResult, curarg); } else { // no array or object: abort throw std::runtime_error( "Invalid result query"); } lastResult = subelement; } state = STATE_COMMAND_EXECUTED; break; } // don't break parsing when the char is required for the // next argument breakParsing = false; // pop the stack and return the result to the current // command arguments close_out_params(); // don't stringify the json in case of a string to avoid // doublequotes if (lastResult.isStr()) { curarg = lastResult.get_str(); } else { curarg = lastResult.write(2); } // if we have a non empty result, use it as stack // argument otherwise as general result if (curarg.size()) { if (stack.size()) { add_to_current_stack(curarg); } else { strResult = curarg; } } curarg.clear(); // assume eating space state state = STATE_EATING_SPACES; } if (breakParsing) { break; } } // FALLTHROUGH case STATE_ARGUMENT: // In or after argument case STATE_EATING_SPACES_IN_ARG: case STATE_EATING_SPACES_IN_BRACKETS: case STATE_EATING_SPACES: // Handle runs of whitespace switch (ch) { case '"': state = STATE_DOUBLEQUOTED; break; case '\'': state = STATE_SINGLEQUOTED; break; case '\\': state = STATE_ESCAPE_OUTER; break; case '(': case ')': case '\n': if (state == STATE_EATING_SPACES_IN_ARG) { throw std::runtime_error("Invalid Syntax"); } if (state == STATE_ARGUMENT) { if (ch == '(' && stack.size() && stack.back().size() > 0) { if (nDepthInsideSensitive) { ++nDepthInsideSensitive; } stack.push_back(std::vector()); } // don't allow commands after executed commands on // baselevel if (!stack.size()) { throw std::runtime_error("Invalid Syntax"); } add_to_current_stack(curarg); curarg.clear(); state = STATE_EATING_SPACES_IN_BRACKETS; } if ((ch == ')' || ch == '\n') && stack.size() > 0) { if (fExecute) { // Convert argument list to JSON objects in // method-dependent way, and pass it along with // the method name to the dispatcher. JSONRPCRequest req; req.params = RPCConvertValues( stack.back()[0], std::vector( stack.back().begin() + 1, stack.back().end())); req.strMethod = stack.back()[0]; #ifdef ENABLE_WALLET if (walletID && !walletID->empty()) { QByteArray encodedName = QUrl::toPercentEncoding( QString::fromStdString(*walletID)); req.URI = "/wallet/" + std::string(encodedName.constData(), encodedName.length()); } #endif GlobalConfig config; lastResult = tableRPC.execute(config, req); } state = STATE_COMMAND_EXECUTED; curarg.clear(); } break; case ' ': case ',': case '\t': if (state == STATE_EATING_SPACES_IN_ARG && curarg.empty() && ch == ',') { throw std::runtime_error("Invalid Syntax"); } else if (state == STATE_ARGUMENT) { // Space ends argument add_to_current_stack(curarg); curarg.clear(); } if ((state == STATE_EATING_SPACES_IN_BRACKETS || state == STATE_ARGUMENT) && ch == ',') { state = STATE_EATING_SPACES_IN_ARG; break; } state = STATE_EATING_SPACES; break; default: curarg += ch; state = STATE_ARGUMENT; } break; case STATE_SINGLEQUOTED: // Single-quoted string switch (ch) { case '\'': state = STATE_ARGUMENT; break; default: curarg += ch; } break; case STATE_DOUBLEQUOTED: // Double-quoted string switch (ch) { case '"': state = STATE_ARGUMENT; break; case '\\': state = STATE_ESCAPE_DOUBLEQUOTED; break; default: curarg += ch; } break; case STATE_ESCAPE_OUTER: // '\' outside quotes curarg += ch; state = STATE_ARGUMENT; break; case STATE_ESCAPE_DOUBLEQUOTED: // '\' in double-quoted text if (ch != '"' && ch != '\\') { // keep '\' for everything but the quote and '\' itself curarg += '\\'; } curarg += ch; state = STATE_DOUBLEQUOTED; break; } } if (pstrFilteredOut) { if (STATE_COMMAND_EXECUTED == state) { assert(!stack.empty()); close_out_params(); } *pstrFilteredOut = strCommand; for (auto i = filter_ranges.rbegin(); i != filter_ranges.rend(); ++i) { pstrFilteredOut->replace(i->first, i->second - i->first, "(…)"); } } // final state switch (state) { case STATE_COMMAND_EXECUTED: if (lastResult.isStr()) { strResult = lastResult.get_str(); } else { strResult = lastResult.write(2); } // FALLTHROUGH case STATE_ARGUMENT: case STATE_EATING_SPACES: return true; default: // ERROR to end in one of the other states return false; } } void RPCExecutor::request(const QString &command, const QString &walletID) { try { std::string result; std::string executableCommand = command.toStdString() + "\n"; // Catch the console-only-help command before RPC call is executed and // reply with help text as-if a RPC reply. if (executableCommand == "help-console\n") { Q_EMIT reply( RPCConsole::CMD_REPLY, QString(("\n" "This console accepts RPC commands using the standard " "syntax.\n" " example: getblockhash 0\n\n" "This console can also accept RPC commands using " "parenthesized syntax.\n" " example: getblockhash(0)\n\n" "Commands may be nested when specified with the " "parenthesized syntax.\n" " example: getblock(getblockhash(0) 1)\n\n" "A space or a comma can be used to delimit arguments " "for either syntax.\n" " example: getblockhash 0\n" " getblockhash,0\n\n" "Named results can be queried with a non-quoted key " "string in brackets.\n" " example: getblock(getblockhash(0) true)[tx]\n\n" "Results without keys can be queried using an integer " "in brackets.\n" " example: " "getblock(getblockhash(0),true)[tx][0]\n\n"))); return; } std::string wallet_id = walletID.toStdString(); if (!RPCConsole::RPCExecuteCommandLine(result, executableCommand, nullptr, &wallet_id)) { Q_EMIT reply(RPCConsole::CMD_ERROR, QString("Parse error: unbalanced ' or \"")); return; } Q_EMIT reply(RPCConsole::CMD_REPLY, QString::fromStdString(result)); } catch (UniValue &objError) { // Nice formatting for standard-format error try { int code = find_value(objError, "code").get_int(); std::string message = find_value(objError, "message").get_str(); Q_EMIT reply(RPCConsole::CMD_ERROR, QString::fromStdString(message) + " (code " + QString::number(code) + ")"); } catch (const std::runtime_error &) { // raised when converting to invalid type, i.e. missing code or // message. Show raw JSON object. Q_EMIT reply(RPCConsole::CMD_ERROR, QString::fromStdString(objError.write())); } } catch (const std::exception &e) { Q_EMIT reply(RPCConsole::CMD_ERROR, QString("Error: ") + QString::fromStdString(e.what())); } } RPCConsole::RPCConsole(const PlatformStyle *_platformStyle, QWidget *parent) : QWidget(parent), ui(new Ui::RPCConsole), clientModel(0), historyPtr(0), platformStyle(_platformStyle), peersTableContextMenu(0), banTableContextMenu(0), consoleFontSize(0) { ui->setupUi(this); QSettings settings; if (!restoreGeometry( settings.value("RPCConsoleWindowGeometry").toByteArray())) { // Restore failed (perhaps missing setting), center the window move(QApplication::desktop()->availableGeometry().center() - frameGeometry().center()); } ui->openDebugLogfileButton->setToolTip( ui->openDebugLogfileButton->toolTip().arg(tr(PACKAGE_NAME))); if (platformStyle->getImagesOnButtons()) { ui->openDebugLogfileButton->setIcon( platformStyle->SingleColorIcon(":/icons/export")); } ui->clearButton->setIcon(platformStyle->SingleColorIcon(":/icons/remove")); ui->fontBiggerButton->setIcon( platformStyle->SingleColorIcon(":/icons/fontbigger")); ui->fontSmallerButton->setIcon( platformStyle->SingleColorIcon(":/icons/fontsmaller")); // Install event filter for up and down arrow ui->lineEdit->installEventFilter(this); ui->messagesWidget->installEventFilter(this); connect(ui->clearButton, SIGNAL(clicked()), this, SLOT(clear())); connect(ui->fontBiggerButton, SIGNAL(clicked()), this, SLOT(fontBigger())); connect(ui->fontSmallerButton, SIGNAL(clicked()), this, SLOT(fontSmaller())); connect(ui->btnClearTrafficGraph, SIGNAL(clicked()), ui->trafficGraph, SLOT(clear())); // disable the wallet selector by default ui->WalletSelector->setVisible(false); ui->WalletSelectorLabel->setVisible(false); // set library version labels #ifdef ENABLE_WALLET ui->berkeleyDBVersion->setText(DbEnv::version(0, 0, 0)); #else ui->label_berkeleyDBVersion->hide(); ui->berkeleyDBVersion->hide(); #endif // Register RPC timer interface rpcTimerInterface = new QtRPCTimerInterface(); // avoid accidentally overwriting an existing, non QTThread // based timer interface RPCSetTimerInterfaceIfUnset(rpcTimerInterface); setTrafficGraphRange(INITIAL_TRAFFIC_GRAPH_MINS); ui->detailWidget->hide(); ui->peerHeading->setText(tr("Select a peer to view detailed information.")); consoleFontSize = settings.value(fontSizeSettingsKey, QFontInfo(QFont()).pointSize()) .toInt(); clear(); } RPCConsole::~RPCConsole() { QSettings settings; settings.setValue("RPCConsoleWindowGeometry", saveGeometry()); RPCUnsetTimerInterface(rpcTimerInterface); delete rpcTimerInterface; delete ui; } bool RPCConsole::eventFilter(QObject *obj, QEvent *event) { // Special key handling if (event->type() == QEvent::KeyPress) { QKeyEvent *keyevt = static_cast(event); int key = keyevt->key(); Qt::KeyboardModifiers mod = keyevt->modifiers(); switch (key) { case Qt::Key_Up: if (obj == ui->lineEdit) { browseHistory(-1); return true; } break; case Qt::Key_Down: if (obj == ui->lineEdit) { browseHistory(1); return true; } break; case Qt::Key_PageUp: /* pass paging keys to messages widget */ case Qt::Key_PageDown: if (obj == ui->lineEdit) { QApplication::postEvent(ui->messagesWidget, new QKeyEvent(*keyevt)); return true; } break; case Qt::Key_Return: case Qt::Key_Enter: // forward these events to lineEdit if (obj == autoCompleter->popup()) { QApplication::postEvent(ui->lineEdit, new QKeyEvent(*keyevt)); return true; } break; default: // Typing in messages widget brings focus to line edit, and // redirects key there. Exclude most combinations and keys that // emit no text, except paste shortcuts. if (obj == ui->messagesWidget && ((!mod && !keyevt->text().isEmpty() && key != Qt::Key_Tab) || ((mod & Qt::ControlModifier) && key == Qt::Key_V) || ((mod & Qt::ShiftModifier) && key == Qt::Key_Insert))) { ui->lineEdit->setFocus(); QApplication::postEvent(ui->lineEdit, new QKeyEvent(*keyevt)); return true; } } } return QWidget::eventFilter(obj, event); } void RPCConsole::setClientModel(ClientModel *model) { clientModel = model; ui->trafficGraph->setClientModel(model); if (model && clientModel->getPeerTableModel() && clientModel->getBanTableModel()) { // Keep up to date with client setNumConnections(model->getNumConnections()); connect(model, SIGNAL(numConnectionsChanged(int)), this, SLOT(setNumConnections(int))); setNumBlocks(model->getNumBlocks(), model->getLastBlockDate(), model->getVerificationProgress(nullptr), false); connect(model, SIGNAL(numBlocksChanged(int, QDateTime, double, bool)), this, SLOT(setNumBlocks(int, QDateTime, double, bool))); updateNetworkState(); connect(model, SIGNAL(networkActiveChanged(bool)), this, SLOT(setNetworkActive(bool))); updateTrafficStats(model->getTotalBytesRecv(), model->getTotalBytesSent()); connect(model, SIGNAL(bytesChanged(quint64, quint64)), this, SLOT(updateTrafficStats(quint64, quint64))); connect(model, SIGNAL(mempoolSizeChanged(long, size_t)), this, SLOT(setMempoolSize(long, size_t))); // set up peer table ui->peerWidget->setModel(model->getPeerTableModel()); ui->peerWidget->verticalHeader()->hide(); ui->peerWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); ui->peerWidget->setSelectionBehavior(QAbstractItemView::SelectRows); ui->peerWidget->setSelectionMode(QAbstractItemView::ExtendedSelection); ui->peerWidget->setContextMenuPolicy(Qt::CustomContextMenu); ui->peerWidget->setColumnWidth(PeerTableModel::Address, ADDRESS_COLUMN_WIDTH); ui->peerWidget->setColumnWidth(PeerTableModel::Subversion, SUBVERSION_COLUMN_WIDTH); ui->peerWidget->setColumnWidth(PeerTableModel::Ping, PING_COLUMN_WIDTH); ui->peerWidget->horizontalHeader()->setStretchLastSection(true); // create peer table context menu actions QAction *disconnectAction = new QAction(tr("&Disconnect"), this); QAction *banAction1h = new QAction(tr("Ban for") + " " + tr("1 &hour"), this); QAction *banAction24h = new QAction(tr("Ban for") + " " + tr("1 &day"), this); QAction *banAction7d = new QAction(tr("Ban for") + " " + tr("1 &week"), this); QAction *banAction365d = new QAction(tr("Ban for") + " " + tr("1 &year"), this); // create peer table context menu peersTableContextMenu = new QMenu(this); peersTableContextMenu->addAction(disconnectAction); peersTableContextMenu->addAction(banAction1h); peersTableContextMenu->addAction(banAction24h); peersTableContextMenu->addAction(banAction7d); peersTableContextMenu->addAction(banAction365d); // Add a signal mapping to allow dynamic context menu arguments. We need // to use int (instead of int64_t), because signal mapper only supports // int or objects, which is okay because max bantime (1 year) is < // int_max. QSignalMapper *signalMapper = new QSignalMapper(this); signalMapper->setMapping(banAction1h, 60 * 60); signalMapper->setMapping(banAction24h, 60 * 60 * 24); signalMapper->setMapping(banAction7d, 60 * 60 * 24 * 7); signalMapper->setMapping(banAction365d, 60 * 60 * 24 * 365); connect(banAction1h, SIGNAL(triggered()), signalMapper, SLOT(map())); connect(banAction24h, SIGNAL(triggered()), signalMapper, SLOT(map())); connect(banAction7d, SIGNAL(triggered()), signalMapper, SLOT(map())); connect(banAction365d, SIGNAL(triggered()), signalMapper, SLOT(map())); connect(signalMapper, SIGNAL(mapped(int)), this, SLOT(banSelectedNode(int))); // peer table context menu signals connect(ui->peerWidget, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(showPeersTableContextMenu(const QPoint &))); connect(disconnectAction, SIGNAL(triggered()), this, SLOT(disconnectSelectedNode())); // peer table signal handling - update peer details when selecting new // node connect( ui->peerWidget->selectionModel(), SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)), this, SLOT(peerSelected(const QItemSelection &, const QItemSelection &))); // peer table signal handling - update peer details when new nodes are // added to the model connect(model->getPeerTableModel(), SIGNAL(layoutChanged()), this, SLOT(peerLayoutChanged())); // peer table signal handling - cache selected node ids connect(model->getPeerTableModel(), SIGNAL(layoutAboutToBeChanged()), this, SLOT(peerLayoutAboutToChange())); // set up ban table ui->banlistWidget->setModel(model->getBanTableModel()); ui->banlistWidget->verticalHeader()->hide(); ui->banlistWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); ui->banlistWidget->setSelectionBehavior(QAbstractItemView::SelectRows); ui->banlistWidget->setSelectionMode(QAbstractItemView::SingleSelection); ui->banlistWidget->setContextMenuPolicy(Qt::CustomContextMenu); ui->banlistWidget->setColumnWidth(BanTableModel::Address, BANSUBNET_COLUMN_WIDTH); ui->banlistWidget->setColumnWidth(BanTableModel::Bantime, BANTIME_COLUMN_WIDTH); ui->banlistWidget->horizontalHeader()->setStretchLastSection(true); // create ban table context menu action QAction *unbanAction = new QAction(tr("&Unban"), this); // create ban table context menu banTableContextMenu = new QMenu(this); banTableContextMenu->addAction(unbanAction); // ban table context menu signals connect(ui->banlistWidget, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(showBanTableContextMenu(const QPoint &))); connect(unbanAction, SIGNAL(triggered()), this, SLOT(unbanSelectedNode())); // ban table signal handling - clear peer details when clicking a peer // in the ban table connect(ui->banlistWidget, SIGNAL(clicked(const QModelIndex &)), this, SLOT(clearSelectedNode())); // ban table signal handling - ensure ban table is shown or hidden (if // empty) connect(model->getBanTableModel(), SIGNAL(layoutChanged()), this, SLOT(showOrHideBanTableIfRequired())); showOrHideBanTableIfRequired(); // Provide initial values ui->clientVersion->setText(model->formatFullVersion()); ui->clientUserAgent->setText(model->formatSubVersion()); ui->dataDir->setText(model->dataDir()); ui->startupTime->setText(model->formatClientStartupTime()); ui->networkName->setText( QString::fromStdString(Params().NetworkIDString())); // Setup autocomplete and attach it QStringList wordList; std::vector commandList = tableRPC.listCommands(); for (size_t i = 0; i < commandList.size(); ++i) { wordList << commandList[i].c_str(); wordList << ("help " + commandList[i]).c_str(); } wordList << "help-console"; wordList.sort(); autoCompleter = new QCompleter(wordList, this); autoCompleter->setModelSorting(QCompleter::CaseSensitivelySortedModel); ui->lineEdit->setCompleter(autoCompleter); autoCompleter->popup()->installEventFilter(this); // Start thread to execute RPC commands. startExecutor(); } if (!model) { // Client model is being set to 0, this means shutdown() is about to be // called. Make sure we clean up the executor thread Q_EMIT stopExecutor(); thread.wait(); } } #ifdef ENABLE_WALLET void RPCConsole::addWallet(WalletModel *const walletModel) { const QString name = walletModel->getWalletName(); // use name for text and internal data object (to allow to move to a wallet // id later) ui->WalletSelector->addItem(name, name); if (ui->WalletSelector->count() == 2 && !isVisible()) { // First wallet added, set to default so long as the window isn't // presently visible (and potentially in use) ui->WalletSelector->setCurrentIndex(1); } if (ui->WalletSelector->count() > 2) { ui->WalletSelector->setVisible(true); ui->WalletSelectorLabel->setVisible(true); } } #endif static QString categoryClass(int category) { switch (category) { case RPCConsole::CMD_REQUEST: return "cmd-request"; break; case RPCConsole::CMD_REPLY: return "cmd-reply"; break; case RPCConsole::CMD_ERROR: return "cmd-error"; break; default: return "misc"; } } void RPCConsole::fontBigger() { setFontSize(consoleFontSize + 1); } void RPCConsole::fontSmaller() { setFontSize(consoleFontSize - 1); } void RPCConsole::setFontSize(int newSize) { QSettings settings; // don't allow a insane font size if (newSize < FONT_RANGE.width() || newSize > FONT_RANGE.height()) return; // temp. store the console content QString str = ui->messagesWidget->toHtml(); // replace font tags size in current content str.replace(QString("font-size:%1pt").arg(consoleFontSize), QString("font-size:%1pt").arg(newSize)); // store the new font size consoleFontSize = newSize; settings.setValue(fontSizeSettingsKey, consoleFontSize); // clear console (reset icon sizes, default stylesheet) and re-add the // content float oldPosFactor = 1.0 / ui->messagesWidget->verticalScrollBar()->maximum() * ui->messagesWidget->verticalScrollBar()->value(); clear(false); ui->messagesWidget->setHtml(str); ui->messagesWidget->verticalScrollBar()->setValue( oldPosFactor * ui->messagesWidget->verticalScrollBar()->maximum()); } void RPCConsole::clear(bool clearHistory) { ui->messagesWidget->clear(); if (clearHistory) { history.clear(); historyPtr = 0; } ui->lineEdit->clear(); ui->lineEdit->setFocus(); // Add smoothly scaled icon images. // (when using width/height on an img, Qt uses nearest instead of linear // interpolation) for (int i = 0; ICON_MAPPING[i].url; ++i) { ui->messagesWidget->document()->addResource( QTextDocument::ImageResource, QUrl(ICON_MAPPING[i].url), platformStyle->SingleColorImage(ICON_MAPPING[i].source) .scaled(QSize(consoleFontSize * 2, consoleFontSize * 2), Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); } // Set default style sheet QFontInfo fixedFontInfo(GUIUtil::fixedPitchFont()); ui->messagesWidget->document()->setDefaultStyleSheet( QString("table { }" "td.time { color: #808080; font-size: %2; padding-top: 3px; } " "td.message { font-family: %1; font-size: %2; " "white-space:pre-wrap; } " "td.cmd-request { color: #006060; } " "td.cmd-error { color: red; } " ".secwarning { color: red; }" "b { color: #006060; } ") .arg(fixedFontInfo.family(), QString("%1pt").arg(consoleFontSize))); #ifdef Q_OS_MAC QString clsKey = "(⌘)-L"; #else QString clsKey = "Ctrl-L"; #endif message(CMD_REPLY, (tr("Welcome to the %1 RPC console.").arg(tr(PACKAGE_NAME)) + "
" + tr("Use up and down arrows to navigate history, and " "%1 to clear screen.") .arg("" + clsKey + "") + "
" + tr("Type %1 for an overview of available commands.") .arg("help") + "
" + tr("For more information on using this console type %1.") .arg("help-console") + "

" + tr("WARNING: Scammers have been active, telling users to type " "commands here, stealing their wallet contents. Do not use " "this console without fully understanding the ramification " "of a command.") + "
"), true); } void RPCConsole::keyPressEvent(QKeyEvent *event) { if (windowType() != Qt::Widget && event->key() == Qt::Key_Escape) { close(); } } void RPCConsole::message(int category, const QString &message, bool html) { QTime time = QTime::currentTime(); QString timeString = time.toString(); QString out; out += ""; out += ""; out += "
" + timeString + ""; if (html) { out += message; } else { out += GUIUtil::HtmlEscape(message, false); } out += "
"; ui->messagesWidget->append(out); } void RPCConsole::updateNetworkState() { QString connections = QString::number(clientModel->getNumConnections()) + " ("; connections += tr("In:") + " " + QString::number(clientModel->getNumConnections(CONNECTIONS_IN)) + " / "; connections += tr("Out:") + " " + QString::number(clientModel->getNumConnections(CONNECTIONS_OUT)) + ")"; if (!clientModel->getNetworkActive()) { connections += " (" + tr("Network activity disabled") + ")"; } ui->numberOfConnections->setText(connections); } void RPCConsole::setNumConnections(int count) { if (!clientModel) { return; } updateNetworkState(); } void RPCConsole::setNetworkActive(bool networkActive) { updateNetworkState(); } void RPCConsole::setNumBlocks(int count, const QDateTime &blockDate, double nVerificationProgress, bool headers) { if (!headers) { ui->numberOfBlocks->setText(QString::number(count)); ui->lastBlockTime->setText(blockDate.toString()); } } void RPCConsole::setMempoolSize(long numberOfTxs, size_t dynUsage) { ui->mempoolNumberTxs->setText(QString::number(numberOfTxs)); if (dynUsage < 1000000) { ui->mempoolSize->setText(QString::number(dynUsage / 1000.0, 'f', 2) + " KB"); } else { ui->mempoolSize->setText(QString::number(dynUsage / 1000000.0, 'f', 2) + " MB"); } } void RPCConsole::on_lineEdit_returnPressed() { QString cmd = ui->lineEdit->text(); if (!cmd.isEmpty()) { std::string strFilteredCmd; try { std::string dummy; if (!RPCParseCommandLine(dummy, cmd.toStdString(), false, &strFilteredCmd)) { // Failed to parse command, so we cannot even filter it for the // history throw std::runtime_error("Invalid command line"); } } catch (const std::exception &e) { QMessageBox::critical(this, "Error", QString("Error: ") + QString::fromStdString(e.what())); return; } ui->lineEdit->clear(); cmdBeforeBrowsing = QString(); QString walletID; #ifdef ENABLE_WALLET const int wallet_index = ui->WalletSelector->currentIndex(); if (wallet_index > 0) { walletID = (QString)ui->WalletSelector->itemData(wallet_index) .value(); } if (m_last_wallet_id != walletID) { if (walletID.isEmpty()) { message(CMD_REQUEST, tr("Executing command without any wallet")); } else { message( CMD_REQUEST, tr("Executing command using \"%1\" wallet").arg(walletID)); } m_last_wallet_id = walletID; } #endif message(CMD_REQUEST, QString::fromStdString(strFilteredCmd)); Q_EMIT cmdRequest(cmd, walletID); cmd = QString::fromStdString(strFilteredCmd); // Remove command, if already in history history.removeOne(cmd); // Append command to history history.append(cmd); // Enforce maximum history size while (history.size() > CONSOLE_HISTORY) history.removeFirst(); // Set pointer to end of history historyPtr = history.size(); // Scroll console view to end scrollToEnd(); } } void RPCConsole::browseHistory(int offset) { // store current text when start browsing through the history if (historyPtr == history.size()) { cmdBeforeBrowsing = ui->lineEdit->text(); } historyPtr += offset; if (historyPtr < 0) { historyPtr = 0; } if (historyPtr > history.size()) { historyPtr = history.size(); } QString cmd; if (historyPtr < history.size()) { cmd = history.at(historyPtr); } else if (!cmdBeforeBrowsing.isNull()) { cmd = cmdBeforeBrowsing; } ui->lineEdit->setText(cmd); } void RPCConsole::startExecutor() { RPCExecutor *executor = new RPCExecutor(); executor->moveToThread(&thread); // Replies from executor object must go to this object connect(executor, SIGNAL(reply(int, QString)), this, SLOT(message(int, QString))); // Requests from this object must go to executor connect(this, SIGNAL(cmdRequest(QString, QString)), executor, SLOT(request(QString, QString))); // On stopExecutor signal // - quit the Qt event loop in the execution thread connect(this, SIGNAL(stopExecutor()), &thread, SLOT(quit())); // - queue executor for deletion (in execution thread) connect(&thread, SIGNAL(finished()), executor, SLOT(deleteLater()), Qt::DirectConnection); // Default implementation of QThread::run() simply spins up an event loop in // the thread, which is what we want. thread.start(); } void RPCConsole::on_tabWidget_currentChanged(int index) { if (ui->tabWidget->widget(index) == ui->tab_console) { ui->lineEdit->setFocus(); } else if (ui->tabWidget->widget(index) != ui->tab_peers) { clearSelectedNode(); } } void RPCConsole::on_openDebugLogfileButton_clicked() { GUIUtil::openDebugLogfile(); } void RPCConsole::scrollToEnd() { QScrollBar *scrollbar = ui->messagesWidget->verticalScrollBar(); scrollbar->setValue(scrollbar->maximum()); } void RPCConsole::on_sldGraphRange_valueChanged(int value) { const int multiplier = 5; // each position on the slider represents 5 min int mins = value * multiplier; setTrafficGraphRange(mins); } -QString RPCConsole::FormatBytes(quint64 bytes) { - if (bytes < 1024) { - return QString(tr("%1 B")).arg(bytes); - } - if (bytes < 1024 * 1024) { - return QString(tr("%1 KB")).arg(bytes / 1024); - } - if (bytes < 1024 * 1024 * 1024) { - return QString(tr("%1 MB")).arg(bytes / 1024 / 1024); - } - - return QString(tr("%1 GB")).arg(bytes / 1024 / 1024 / 1024); -} - void RPCConsole::setTrafficGraphRange(int mins) { ui->trafficGraph->setGraphRangeMins(mins); ui->lblGraphRange->setText(GUIUtil::formatDurationStr(mins * 60)); } void RPCConsole::updateTrafficStats(quint64 totalBytesIn, quint64 totalBytesOut) { - ui->lblBytesIn->setText(FormatBytes(totalBytesIn)); - ui->lblBytesOut->setText(FormatBytes(totalBytesOut)); + ui->lblBytesIn->setText(GUIUtil::formatBytes(totalBytesIn)); + ui->lblBytesOut->setText(GUIUtil::formatBytes(totalBytesOut)); } void RPCConsole::peerSelected(const QItemSelection &selected, const QItemSelection &deselected) { Q_UNUSED(deselected); if (!clientModel || !clientModel->getPeerTableModel() || selected.indexes().isEmpty()) return; const CNodeCombinedStats *stats = clientModel->getPeerTableModel()->getNodeStats( selected.indexes().first().row()); if (stats) updateNodeDetail(stats); } void RPCConsole::peerLayoutAboutToChange() { QModelIndexList selected = ui->peerWidget->selectionModel()->selectedIndexes(); cachedNodeids.clear(); for (int i = 0; i < selected.size(); i++) { const CNodeCombinedStats *stats = clientModel->getPeerTableModel()->getNodeStats( selected.at(i).row()); cachedNodeids.append(stats->nodeStats.nodeid); } } void RPCConsole::peerLayoutChanged() { if (!clientModel || !clientModel->getPeerTableModel()) { return; } const CNodeCombinedStats *stats = nullptr; bool fUnselect = false; bool fReselect = false; if (cachedNodeids.empty()) // no node selected yet return; // find the currently selected row int selectedRow = -1; QModelIndexList selectedModelIndex = ui->peerWidget->selectionModel()->selectedIndexes(); if (!selectedModelIndex.isEmpty()) { selectedRow = selectedModelIndex.first().row(); } // check if our detail node has a row in the table (it may not necessarily // be at selectedRow since its position can change after a layout change) int detailNodeRow = clientModel->getPeerTableModel()->getRowByNodeId(cachedNodeids.first()); if (detailNodeRow < 0) { // detail node disappeared from table (node disconnected) fUnselect = true; } else { if (detailNodeRow != selectedRow) { // detail node moved position fUnselect = true; fReselect = true; } // get fresh stats on the detail node. stats = clientModel->getPeerTableModel()->getNodeStats(detailNodeRow); } if (fUnselect && selectedRow >= 0) { clearSelectedNode(); } if (fReselect) { for (int i = 0; i < cachedNodeids.size(); i++) { ui->peerWidget->selectRow( clientModel->getPeerTableModel()->getRowByNodeId( cachedNodeids.at(i))); } } if (stats) updateNodeDetail(stats); } void RPCConsole::updateNodeDetail(const CNodeCombinedStats *stats) { // update the detail ui with latest node information QString peerAddrDetails(QString::fromStdString(stats->nodeStats.addrName) + " "); peerAddrDetails += tr("(node id: %1)").arg(QString::number(stats->nodeStats.nodeid)); if (!stats->nodeStats.addrLocal.empty()) { peerAddrDetails += "
" + tr("via %1").arg(QString::fromStdString( stats->nodeStats.addrLocal)); } ui->peerHeading->setText(peerAddrDetails); ui->peerServices->setText( GUIUtil::formatServicesStr(stats->nodeStats.nServices)); ui->peerLastSend->setText( stats->nodeStats.nLastSend ? GUIUtil::formatDurationStr(GetSystemTimeInSeconds() - stats->nodeStats.nLastSend) : tr("never")); ui->peerLastRecv->setText( stats->nodeStats.nLastRecv ? GUIUtil::formatDurationStr(GetSystemTimeInSeconds() - stats->nodeStats.nLastRecv) : tr("never")); - ui->peerBytesSent->setText(FormatBytes(stats->nodeStats.nSendBytes)); - ui->peerBytesRecv->setText(FormatBytes(stats->nodeStats.nRecvBytes)); + ui->peerBytesSent->setText( + GUIUtil::formatBytes(stats->nodeStats.nSendBytes)); + ui->peerBytesRecv->setText( + GUIUtil::formatBytes(stats->nodeStats.nRecvBytes)); ui->peerConnTime->setText(GUIUtil::formatDurationStr( GetSystemTimeInSeconds() - stats->nodeStats.nTimeConnected)); ui->peerPingTime->setText( GUIUtil::formatPingTime(stats->nodeStats.dPingTime)); ui->peerPingWait->setText( GUIUtil::formatPingTime(stats->nodeStats.dPingWait)); ui->peerMinPing->setText( GUIUtil::formatPingTime(stats->nodeStats.dMinPing)); ui->timeoffset->setText( GUIUtil::formatTimeOffset(stats->nodeStats.nTimeOffset)); ui->peerVersion->setText( QString("%1").arg(QString::number(stats->nodeStats.nVersion))); ui->peerSubversion->setText( QString::fromStdString(stats->nodeStats.cleanSubVer)); ui->peerDirection->setText(stats->nodeStats.fInbound ? tr("Inbound") : tr("Outbound")); ui->peerHeight->setText( QString("%1").arg(QString::number(stats->nodeStats.nStartingHeight))); ui->peerWhitelisted->setText(stats->nodeStats.fWhitelisted ? tr("Yes") : tr("No")); // This check fails for example if the lock was busy and // nodeStateStats couldn't be fetched. if (stats->fNodeStateStatsAvailable) { // Ban score is init to 0 ui->peerBanScore->setText( QString("%1").arg(stats->nodeStateStats.nMisbehavior)); // Sync height is init to -1 if (stats->nodeStateStats.nSyncHeight > -1) { ui->peerSyncHeight->setText( QString("%1").arg(stats->nodeStateStats.nSyncHeight)); } else { ui->peerSyncHeight->setText(tr("Unknown")); } // Common height is init to -1 if (stats->nodeStateStats.nCommonHeight > -1) { ui->peerCommonHeight->setText( QString("%1").arg(stats->nodeStateStats.nCommonHeight)); } else { ui->peerCommonHeight->setText(tr("Unknown")); } } ui->detailWidget->show(); } void RPCConsole::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); } void RPCConsole::showEvent(QShowEvent *event) { QWidget::showEvent(event); if (!clientModel || !clientModel->getPeerTableModel()) { return; } // start PeerTableModel auto refresh clientModel->getPeerTableModel()->startAutoRefresh(); } void RPCConsole::hideEvent(QHideEvent *event) { QWidget::hideEvent(event); if (!clientModel || !clientModel->getPeerTableModel()) { return; } // stop PeerTableModel auto refresh clientModel->getPeerTableModel()->stopAutoRefresh(); } void RPCConsole::showPeersTableContextMenu(const QPoint &point) { QModelIndex index = ui->peerWidget->indexAt(point); if (index.isValid()) peersTableContextMenu->exec(QCursor::pos()); } void RPCConsole::showBanTableContextMenu(const QPoint &point) { QModelIndex index = ui->banlistWidget->indexAt(point); if (index.isValid()) banTableContextMenu->exec(QCursor::pos()); } void RPCConsole::disconnectSelectedNode() { if (!g_connman) { return; } // Get selected peer addresses QList nodes = GUIUtil::getEntryData(ui->peerWidget, PeerTableModel::NetNodeId); for (int i = 0; i < nodes.count(); i++) { // Get currently selected peer address NodeId id = nodes.at(i).data().toLongLong(); // Find the node, disconnect it and clear the selected node if (g_connman->DisconnectNode(id)) clearSelectedNode(); } } void RPCConsole::banSelectedNode(int bantime) { if (!clientModel || !g_connman) { return; } // Get selected peer addresses QList nodes = GUIUtil::getEntryData(ui->peerWidget, PeerTableModel::NetNodeId); for (int i = 0; i < nodes.count(); i++) { // Get currently selected peer address NodeId id = nodes.at(i).data().toLongLong(); // Get currently selected peer address int detailNodeRow = clientModel->getPeerTableModel()->getRowByNodeId(id); if (detailNodeRow < 0) { return; } // Find possible nodes, ban it and clear the selected node const CNodeCombinedStats *stats = clientModel->getPeerTableModel()->getNodeStats(detailNodeRow); if (stats) { g_connman->Ban(stats->nodeStats.addr, BanReasonManuallyAdded, bantime); } } clearSelectedNode(); clientModel->getBanTableModel()->refresh(); } void RPCConsole::unbanSelectedNode() { if (!clientModel) { return; } // Get selected ban addresses QList nodes = GUIUtil::getEntryData(ui->banlistWidget, BanTableModel::Address); for (int i = 0; i < nodes.count(); i++) { // Get currently selected ban address QString strNode = nodes.at(i).data().toString(); CSubNet possibleSubnet; LookupSubNet(strNode.toStdString().c_str(), possibleSubnet); if (possibleSubnet.IsValid() && g_connman) { g_connman->Unban(possibleSubnet); clientModel->getBanTableModel()->refresh(); } } } void RPCConsole::clearSelectedNode() { ui->peerWidget->selectionModel()->clearSelection(); cachedNodeids.clear(); ui->detailWidget->hide(); ui->peerHeading->setText(tr("Select a peer to view detailed information.")); } void RPCConsole::showOrHideBanTableIfRequired() { if (!clientModel) { return; } bool visible = clientModel->getBanTableModel()->shouldShow(); ui->banlistWidget->setVisible(visible); ui->banHeading->setVisible(visible); } void RPCConsole::setTabFocus(enum TabTypes tabType) { ui->tabWidget->setCurrentIndex(tabType); } diff --git a/src/qt/rpcconsole.h b/src/qt/rpcconsole.h index cacced0499..ed76e426eb 100644 --- a/src/qt/rpcconsole.h +++ b/src/qt/rpcconsole.h @@ -1,166 +1,165 @@ // Copyright (c) 2011-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #ifndef BITCOIN_QT_RPCCONSOLE_H #define BITCOIN_QT_RPCCONSOLE_H #include "guiutil.h" #include "peertablemodel.h" #include "net.h" #include #include #include class ClientModel; class PlatformStyle; class RPCTimerInterface; class WalletModel; namespace Ui { class RPCConsole; } QT_BEGIN_NAMESPACE class QMenu; class QItemSelection; QT_END_NAMESPACE /** Local Bitcoin RPC console. */ class RPCConsole : public QWidget { Q_OBJECT public: explicit RPCConsole(const PlatformStyle *platformStyle, QWidget *parent); ~RPCConsole(); static bool RPCParseCommandLine(std::string &strResult, const std::string &strCommand, bool fExecute, std::string *const pstrFilteredOut = nullptr, const std::string *walletID = nullptr); static bool RPCExecuteCommandLine(std::string &strResult, const std::string &strCommand, std::string *const pstrFilteredOut = nullptr, const std::string *walletID = nullptr) { return RPCParseCommandLine(strResult, strCommand, true, pstrFilteredOut, walletID); } void setClientModel(ClientModel *model); void addWallet(WalletModel *const walletModel); enum MessageClass { MC_ERROR, MC_DEBUG, CMD_REQUEST, CMD_REPLY, CMD_ERROR }; enum TabTypes { TAB_INFO = 0, TAB_CONSOLE = 1, TAB_GRAPH = 2, TAB_PEERS = 3 }; protected: virtual bool eventFilter(QObject *obj, QEvent *event) override; void keyPressEvent(QKeyEvent *) override; private Q_SLOTS: void on_lineEdit_returnPressed(); void on_tabWidget_currentChanged(int index); /** open the debug.log from the current datadir */ void on_openDebugLogfileButton_clicked(); /** change the time range of the network traffic graph */ void on_sldGraphRange_valueChanged(int value); /** update traffic statistics */ void updateTrafficStats(quint64 totalBytesIn, quint64 totalBytesOut); void resizeEvent(QResizeEvent *event) override; void showEvent(QShowEvent *event) override; void hideEvent(QHideEvent *event) override; /** Show custom context menu on Peers tab */ void showPeersTableContextMenu(const QPoint &point); /** Show custom context menu on Bans tab */ void showBanTableContextMenu(const QPoint &point); /** Hides ban table if no bans are present */ void showOrHideBanTableIfRequired(); /** clear the selected node */ void clearSelectedNode(); public Q_SLOTS: void clear(bool clearHistory = true); void fontBigger(); void fontSmaller(); void setFontSize(int newSize); /** Append the message to the message widget */ void message(int category, const QString &message, bool html = false); /** Set number of connections shown in the UI */ void setNumConnections(int count); /** Set network state shown in the UI */ void setNetworkActive(bool networkActive); /** Set number of blocks and last block date shown in the UI */ void setNumBlocks(int count, const QDateTime &blockDate, double nVerificationProgress, bool headers); /** Set size (number of transactions and memory usage) of the mempool in the * UI */ void setMempoolSize(long numberOfTxs, size_t dynUsage); /** Go forward or back in history */ void browseHistory(int offset); /** Scroll console view to end */ void scrollToEnd(); /** Handle selection of peer in peers list */ void peerSelected(const QItemSelection &selected, const QItemSelection &deselected); /** Handle selection caching before update */ void peerLayoutAboutToChange(); /** Handle updated peer information */ void peerLayoutChanged(); /** Disconnect a selected node on the Peers tab */ void disconnectSelectedNode(); /** Ban a selected node on the Peers tab */ void banSelectedNode(int bantime); /** Unban a selected node on the Bans tab */ void unbanSelectedNode(); /** set which tab has the focus (is visible) */ void setTabFocus(enum TabTypes tabType); Q_SIGNALS: // For RPC command executor void stopExecutor(); void cmdRequest(const QString &command, const QString &walletID); private: - static QString FormatBytes(quint64 bytes); void startExecutor(); void setTrafficGraphRange(int mins); /** show detailed information on ui about selected node */ void updateNodeDetail(const CNodeCombinedStats *stats); enum ColumnWidths { ADDRESS_COLUMN_WIDTH = 200, SUBVERSION_COLUMN_WIDTH = 150, PING_COLUMN_WIDTH = 80, BANSUBNET_COLUMN_WIDTH = 200, BANTIME_COLUMN_WIDTH = 250 }; Ui::RPCConsole *ui; ClientModel *clientModel; QStringList history; int historyPtr; QString cmdBeforeBrowsing; QList cachedNodeids; const PlatformStyle *platformStyle; RPCTimerInterface *rpcTimerInterface; QMenu *peersTableContextMenu; QMenu *banTableContextMenu; int consoleFontSize; QCompleter *autoCompleter; QThread thread; QString m_last_wallet_id; /** Update UI with latest network info from model. */ void updateNetworkState(); }; #endif // BITCOIN_QT_RPCCONSOLE_H