|
|
|
// 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 "bantablemodel.h"
|
|
|
|
#include "clientmodel.h"
|
|
|
|
#include "guiutil.h"
|
|
|
|
#include "platformstyle.h"
|
|
|
|
#include "bantablemodel.h"
|
|
|
|
|
|
|
|
#include "chainparams.h"
|
|
|
|
#include "netbase.h"
|
|
|
|
#include "rpc/server.h"
|
|
|
|
#include "rpc/client.h"
|
|
|
|
#include "util.h"
|
|
|
|
|
|
|
|
#include <openssl/crypto.h>
|
|
|
|
|
|
|
|
#include <univalue.h>
|
|
|
|
|
|
|
|
#ifdef ENABLE_WALLET
|
|
|
|
#include <db_cxx.h>
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#include <QKeyEvent>
|
|
|
|
#include <QMenu>
|
|
|
|
#include <QMessageBox>
|
|
|
|
#include <QScrollBar>
|
|
|
|
#include <QSettings>
|
|
|
|
#include <QSignalMapper>
|
|
|
|
#include <QThread>
|
|
|
|
#include <QTime>
|
|
|
|
#include <QTimer>
|
|
|
|
#include <QStringList>
|
|
|
|
|
|
|
|
#if QT_VERSION < 0x050000
|
|
|
|
#include <QUrl>
|
|
|
|
#endif
|
|
|
|
|
|
|
|
// 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"},
|
|
|
|
{NULL, NULL}
|
|
|
|
};
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
// don't add private key handling cmd's to the history
|
|
|
|
const QStringList historyFilter = QStringList()
|
|
|
|
<< "importprivkey"
|
|
|
|
<< "importmulti"
|
|
|
|
<< "signmessagewithprivkey"
|
|
|
|
<< "signrawtransaction"
|
|
|
|
<< "walletpassphrase"
|
|
|
|
<< "walletpassphrasechange"
|
|
|
|
<< "encryptwallet";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Object for executing console RPC commands in a separate thread.
|
|
|
|
*/
|
|
|
|
class RPCExecutor : public QObject
|
|
|
|
{
|
|
|
|
Q_OBJECT
|
|
|
|
|
|
|
|
public Q_SLOTS:
|
|
|
|
void request(const QString &command);
|
|
|
|
|
|
|
|
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(boost::function<void(void)>& _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;
|
|
|
|
boost::function<void(void)> func;
|
|
|
|
};
|
|
|
|
|
|
|
|
class QtRPCTimerInterface: public RPCTimerInterface
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
~QtRPCTimerInterface() {}
|
|
|
|
const char *Name() { return "Qt"; }
|
|
|
|
RPCTimerBase* NewTimer(boost::function<void(void)>& func, int64_t millis)
|
|
|
|
{
|
|
|
|
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)
|
|
|
|
{
|
|
|
|
std::vector< std::vector<std::string> > stack;
|
|
|
|
stack.push_back(std::vector<std::string>());
|
|
|
|
|
|
|
|
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<std::pair<size_t, size_t>> 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<std::string>());
|
|
|
|
}
|
|
|
|
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
|
|
|
|
throw std::runtime_error("Invalid result query"); //no array or object: abort
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
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<std::string>());
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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<std::string>(stack.back().begin() + 1, stack.back().end()));
|
|
|
|
req.strMethod = stack.back()[0];
|
|
|
|
lastResult = tableRPC.execute(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 != '\\') curarg += '\\'; // keep '\' for everything but the quote and '\' itself
|
|
|
|
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, "(…)");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
switch(state) // final state
|
|
|
|
{
|
|
|
|
case STATE_COMMAND_EXECUTED:
|
|
|
|
if (lastResult.isStr())
|
|
|
|
strResult = lastResult.get_str();
|
|
|
|
else
|
|
|
|
strResult = lastResult.write(2);
|
|
|
|
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)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
std::string result;
|
|
|
|
std::string executableCommand = command.toStdString() + "\n";
|
|
|
|
if(!RPCConsole::RPCExecuteCommandLine(result, executableCommand))
|
|
|
|
{
|
|
|
|
Q_EMIT reply(RPCConsole::CMD_ERROR, QString("Parse error: unbalanced ' or \""));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
Q_EMIT reply(RPCConsole::CMD_REPLY, QString::fromStdString(result));
|
|
|
|
}
|
|
|
|
catch (UniValue& objError)
|
|
|
|
{
|
|
|
|
try // Nice formatting for standard-format error
|
|
|
|
{
|
|
|
|
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);
|
|
|
|
GUIUtil::restoreWindowGeometry("nRPCConsoleWindow", this->size(), this);
|
|
|
|
|
|
|
|
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()));
|
|
|
|
|
|
|
|
// 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."));
|
|
|
|
|
|
|
|
QSettings settings;
|
|
|
|
consoleFontSize = settings.value(fontSizeSettingsKey, QFontInfo(QFont()).pointSize()).toInt();
|
|
|
|
clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
RPCConsole::~RPCConsole()
|
|
|
|
{
|
|
|
|
GUIUtil::saveWindowGeometry("nRPCConsoleWindow", this);
|
|
|
|
RPCUnsetTimerInterface(rpcTimerInterface);
|
|
|
|
delete rpcTimerInterface;
|
|
|
|
delete ui;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool RPCConsole::eventFilter(QObject* obj, QEvent *event)
|
|
|
|
{
|
|
|
|
if(event->type() == QEvent::KeyPress) // Special key handling
|
|
|
|
{
|
|
|
|
QKeyEvent *keyevt = static_cast<QKeyEvent*>(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(NULL), 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()));
|
|
|