diff --git a/doc/release-notes.md b/doc/release-notes.md
index 16672d227..d9565d95e 100644
--- a/doc/release-notes.md
+++ b/doc/release-notes.md
@@ -1,5 +1,8 @@
Bitcoin ABC version 0.20.13 is now available from:
This release includes the following features and fixes:
+
+- The RPC `getrpcinfo` returns runtime details of the RPC server. At the moment
+ it returns the active commands and the corresponding execution time.
diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp
index 5d31db5ee..d2f41bcec 100644
--- a/src/rpc/server.cpp
+++ b/src/rpc/server.cpp
@@ -1,595 +1,643 @@
// Copyright (c) 2010 Satoshi Nakamoto
// Copyright (c) 2009-2018 The Bitcoin Core developers
// Copyright (c) 2018-2019 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include // for unique_ptr
#include
#include
static bool fRPCRunning = false;
static bool fRPCInWarmup = true;
static std::string rpcWarmupStatus("RPC server started");
static CCriticalSection cs_rpcWarmup;
/* Timer-creating functions */
static RPCTimerInterface *timerInterface = nullptr;
/* Map of name to timer. */
static std::map> deadlineTimers;
+struct RPCCommandExecutionInfo {
+ std::string method;
+ int64_t start;
+};
+
+struct RPCServerInfo {
+ Mutex mutex;
+ std::list active_commands GUARDED_BY(mutex);
+};
+
+static RPCServerInfo g_rpc_server_info;
+
+struct RPCCommandExecution {
+ std::list::iterator it;
+ explicit RPCCommandExecution(const std::string &method) {
+ LOCK(g_rpc_server_info.mutex);
+ it = g_rpc_server_info.active_commands.insert(
+ g_rpc_server_info.active_commands.cend(),
+ {method, GetTimeMicros()});
+ }
+ ~RPCCommandExecution() {
+ LOCK(g_rpc_server_info.mutex);
+ g_rpc_server_info.active_commands.erase(it);
+ }
+};
+
UniValue RPCServer::ExecuteCommand(Config &config,
const JSONRPCRequest &request) const {
// Return immediately if in warmup
// This is retained from the old RPC implementation because a lot of state
// is set during warmup that RPC commands may depend on. This can be
// safely removed once global variable usage has been eliminated.
{
LOCK(cs_rpcWarmup);
if (fRPCInWarmup) {
throw JSONRPCError(RPC_IN_WARMUP, rpcWarmupStatus);
}
}
std::string commandName = request.strMethod;
{
auto commandsReadView = commands.getReadView();
auto iter = commandsReadView->find(commandName);
if (iter != commandsReadView.end()) {
return iter->second.get()->Execute(request);
}
}
// TODO Remove the below call to tableRPC.execute() and only call it for
// context-free RPC commands via an implementation of RPCCommand.
// Check if context-free RPC method is valid and execute it
return tableRPC.execute(config, request);
}
void RPCServer::RegisterCommand(std::unique_ptr command) {
if (command != nullptr) {
const std::string &commandName = command->GetName();
commands.getWriteView()->insert(
std::make_pair(commandName, std::move(command)));
}
}
static struct CRPCSignals {
boost::signals2::signal Started;
boost::signals2::signal Stopped;
- boost::signals2::signal PreCommand;
} g_rpcSignals;
void RPCServerSignals::OnStarted(std::function slot) {
g_rpcSignals.Started.connect(slot);
}
void RPCServerSignals::OnStopped(std::function slot) {
g_rpcSignals.Stopped.connect(slot);
}
void RPCTypeCheck(const UniValue ¶ms,
const std::list &typesExpected,
bool fAllowNull) {
unsigned int i = 0;
for (const UniValueType &t : typesExpected) {
if (params.size() <= i) {
break;
}
const UniValue &v = params[i];
if (!(fAllowNull && v.isNull())) {
RPCTypeCheckArgument(v, t);
}
i++;
}
}
void RPCTypeCheckArgument(const UniValue &value,
const UniValueType &typeExpected) {
if (!typeExpected.typeAny && value.type() != typeExpected.type) {
throw JSONRPCError(RPC_TYPE_ERROR,
strprintf("Expected type %s, got %s",
uvTypeName(typeExpected.type),
uvTypeName(value.type())));
}
}
void RPCTypeCheckObj(const UniValue &o,
const std::map &typesExpected,
bool fAllowNull, bool fStrict) {
for (const auto &t : typesExpected) {
const UniValue &v = find_value(o, t.first);
if (!fAllowNull && v.isNull()) {
throw JSONRPCError(RPC_TYPE_ERROR,
strprintf("Missing %s", t.first));
}
if (!(t.second.typeAny || v.type() == t.second.type ||
(fAllowNull && v.isNull()))) {
std::string err = strprintf("Expected type %s for %s, got %s",
uvTypeName(t.second.type), t.first,
uvTypeName(v.type()));
throw JSONRPCError(RPC_TYPE_ERROR, err);
}
}
if (fStrict) {
for (const std::string &k : o.getKeys()) {
if (typesExpected.count(k) == 0) {
std::string err = strprintf("Unexpected key %s", k);
throw JSONRPCError(RPC_TYPE_ERROR, err);
}
}
}
}
Amount AmountFromValue(const UniValue &value) {
if (!value.isNum() && !value.isStr()) {
throw JSONRPCError(RPC_TYPE_ERROR, "Amount is not a number or string");
}
int64_t n;
if (!ParseFixedPoint(value.getValStr(), 8, &n)) {
throw JSONRPCError(RPC_TYPE_ERROR, "Invalid amount");
}
Amount amt = n * SATOSHI;
if (!MoneyRange(amt)) {
throw JSONRPCError(RPC_TYPE_ERROR, "Amount out of range");
}
return amt;
}
uint256 ParseHashV(const UniValue &v, std::string strName) {
std::string strHex;
if (v.isStr()) {
strHex = v.get_str();
}
// Note: IsHex("") is false
if (!IsHex(strHex)) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
strName + " must be hexadecimal string (not '" +
strHex + "')");
}
if (strHex.length() != 64) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
strprintf("%s must be of length %d (not %d)",
strName, 64, strHex.length()));
}
uint256 result;
result.SetHex(strHex);
return result;
}
uint256 ParseHashO(const UniValue &o, std::string strKey) {
return ParseHashV(find_value(o, strKey), strKey);
}
std::vector ParseHexV(const UniValue &v, std::string strName) {
std::string strHex;
if (v.isStr()) {
strHex = v.get_str();
}
if (!IsHex(strHex)) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
strName + " must be hexadecimal string (not '" +
strHex + "')");
}
return ParseHex(strHex);
}
std::vector ParseHexO(const UniValue &o, std::string strKey) {
return ParseHexV(find_value(o, strKey), strKey);
}
/**
* Note: This interface may still be subject to change.
*/
std::string CRPCTable::help(Config &config, const std::string &strCommand,
const JSONRPCRequest &helpreq) const {
std::string strRet;
std::string category;
std::set setDone;
std::vector>
vCommands;
for (const auto &entry : mapCommands) {
vCommands.push_back(
std::make_pair(entry.second->category + entry.first, entry.second));
}
sort(vCommands.begin(), vCommands.end());
JSONRPCRequest jreq(helpreq);
jreq.fHelp = true;
jreq.params = UniValue();
for (const std::pair &command :
vCommands) {
const ContextFreeRPCCommand *pcmd = command.second;
std::string strMethod = pcmd->name;
if ((strCommand != "" || pcmd->category == "hidden") &&
strMethod != strCommand) {
continue;
}
jreq.strMethod = strMethod;
try {
if (setDone.insert(pcmd).second) {
pcmd->call(config, jreq);
}
} catch (const std::exception &e) {
// Help text is returned in an exception
std::string strHelp = std::string(e.what());
if (strCommand == "") {
if (strHelp.find('\n') != std::string::npos) {
strHelp = strHelp.substr(0, strHelp.find('\n'));
}
if (category != pcmd->category) {
if (!category.empty()) {
strRet += "\n";
}
category = pcmd->category;
strRet += "== " + Capitalize(category) + " ==\n";
}
}
strRet += strHelp + "\n";
}
}
if (strRet == "") {
strRet = strprintf("help: unknown command: %s\n", strCommand);
}
strRet = strRet.substr(0, strRet.size() - 1);
return strRet;
}
static UniValue help(Config &config, const JSONRPCRequest &jsonRequest) {
if (jsonRequest.fHelp || jsonRequest.params.size() > 1) {
throw std::runtime_error(
"help ( \"command\" )\n"
"\nList all commands, or get help for a specified command.\n"
"\nArguments:\n"
"1. \"command\" (string, optional) The command to get help on\n"
"\nResult:\n"
"\"text\" (string) The help text\n");
}
std::string strCommand;
if (jsonRequest.params.size() > 0) {
strCommand = jsonRequest.params[0].get_str();
}
return tableRPC.help(config, strCommand, jsonRequest);
}
static UniValue stop(const Config &config, const JSONRPCRequest &jsonRequest) {
// Accept the deprecated and ignored 'detach' boolean argument
// Also accept the hidden 'wait' integer argument (milliseconds)
// For instance, 'stop 1000' makes the call wait 1 second before returning
// to the client (intended for testing)
if (jsonRequest.fHelp || jsonRequest.params.size() > 1) {
throw std::runtime_error("stop\n"
"\nStop Bitcoin server.");
}
// Event loop will exit after current HTTP requests have been handled, so
// this reply will get back to the client.
StartShutdown();
if (jsonRequest.params[0].isNum()) {
MilliSleep(jsonRequest.params[0].get_int());
}
return "Bitcoin server stopping";
}
static UniValue uptime(const Config &config,
const JSONRPCRequest &jsonRequest) {
if (jsonRequest.fHelp || jsonRequest.params.size() > 1) {
throw std::runtime_error("uptime\n"
"\nReturns the total uptime of the server.\n"
"\nResult:\n"
"ttt (numeric) The number of seconds "
"that the server has been running\n"
"\nExamples:\n" +
HelpExampleCli("uptime", "") +
HelpExampleRpc("uptime", ""));
}
return GetTime() - GetStartupTime();
}
+static UniValue getrpcinfo(const Config &config,
+ const JSONRPCRequest &request) {
+ if (request.fHelp || request.params.size() > 0) {
+ throw std::runtime_error("getrpcinfo\n"
+ "\nReturns details of the RPC server.\n");
+ }
+
+ LOCK(g_rpc_server_info.mutex);
+ UniValue active_commands(UniValue::VARR);
+ for (const RPCCommandExecutionInfo &info :
+ g_rpc_server_info.active_commands) {
+ UniValue entry(UniValue::VOBJ);
+ entry.pushKV("method", info.method);
+ entry.pushKV("duration", GetTimeMicros() - info.start);
+ active_commands.push_back(entry);
+ }
+
+ UniValue result(UniValue::VOBJ);
+ result.pushKV("active_commands", active_commands);
+
+ return result;
+}
+
/**
* Call Table
*/
// clang-format off
static const ContextFreeRPCCommand vRPCCommands[] = {
// category name actor (function) argNames
// ------------------- ------------------------ ---------------------- ----------
/* Overall control/query calls */
+ { "control", "getrpcinfo", getrpcinfo, {} },
{ "control", "help", help, {"command"} },
{ "control", "stop", stop, {"wait"} },
{ "control", "uptime", uptime, {} },
};
// clang-format on
CRPCTable::CRPCTable() {
unsigned int vcidx;
for (vcidx = 0; vcidx < (sizeof(vRPCCommands) / sizeof(vRPCCommands[0]));
vcidx++) {
const ContextFreeRPCCommand *pcmd;
pcmd = &vRPCCommands[vcidx];
mapCommands[pcmd->name] = pcmd;
}
}
const ContextFreeRPCCommand *CRPCTable::
operator[](const std::string &name) const {
std::map::const_iterator it =
mapCommands.find(name);
if (it == mapCommands.end()) {
return nullptr;
}
return (*it).second;
}
bool CRPCTable::appendCommand(const std::string &name,
const ContextFreeRPCCommand *pcmd) {
if (IsRPCRunning()) {
return false;
}
// don't allow overwriting for now
std::map::const_iterator it =
mapCommands.find(name);
if (it != mapCommands.end()) {
return false;
}
mapCommands[name] = pcmd;
return true;
}
void StartRPC() {
LogPrint(BCLog::RPC, "Starting RPC\n");
fRPCRunning = true;
g_rpcSignals.Started();
}
void InterruptRPC() {
LogPrint(BCLog::RPC, "Interrupting RPC\n");
// Interrupt e.g. running longpolls
fRPCRunning = false;
}
void StopRPC() {
LogPrint(BCLog::RPC, "Stopping RPC\n");
deadlineTimers.clear();
DeleteAuthCookie();
g_rpcSignals.Stopped();
}
bool IsRPCRunning() {
return fRPCRunning;
}
void SetRPCWarmupStatus(const std::string &newStatus) {
LOCK(cs_rpcWarmup);
rpcWarmupStatus = newStatus;
}
void SetRPCWarmupFinished() {
LOCK(cs_rpcWarmup);
assert(fRPCInWarmup);
fRPCInWarmup = false;
}
bool RPCIsInWarmup(std::string *outStatus) {
LOCK(cs_rpcWarmup);
if (outStatus) {
*outStatus = rpcWarmupStatus;
}
return fRPCInWarmup;
}
bool IsDeprecatedRPCEnabled(ArgsManager &args, const std::string &method) {
const std::vector enabled_methods =
args.GetArgs("-deprecatedrpc");
return find(enabled_methods.begin(), enabled_methods.end(), method) !=
enabled_methods.end();
}
static UniValue JSONRPCExecOne(Config &config, RPCServer &rpcServer,
JSONRPCRequest jreq, const UniValue &req) {
UniValue rpc_result(UniValue::VOBJ);
try {
jreq.parse(req);
UniValue result = rpcServer.ExecuteCommand(config, jreq);
rpc_result = JSONRPCReplyObj(result, NullUniValue, jreq.id);
} catch (const UniValue &objError) {
rpc_result = JSONRPCReplyObj(NullUniValue, objError, jreq.id);
} catch (const std::exception &e) {
rpc_result = JSONRPCReplyObj(
NullUniValue, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id);
}
return rpc_result;
}
std::string JSONRPCExecBatch(Config &config, RPCServer &rpcServer,
const JSONRPCRequest &jreq, const UniValue &vReq) {
UniValue ret(UniValue::VARR);
for (size_t i = 0; i < vReq.size(); i++) {
ret.push_back(JSONRPCExecOne(config, rpcServer, jreq, vReq[i]));
}
return ret.write() + "\n";
}
/**
* Process named arguments into a vector of positional arguments, based on the
* passed-in specification for the RPC call's arguments.
*/
static inline JSONRPCRequest
transformNamedArguments(const JSONRPCRequest &in,
const std::vector &argNames) {
JSONRPCRequest out = in;
out.params = UniValue(UniValue::VARR);
// Build a map of parameters, and remove ones that have been processed, so
// that we can throw a focused error if there is an unknown one.
const std::vector &keys = in.params.getKeys();
const std::vector &values = in.params.getValues();
std::unordered_map argsIn;
for (size_t i = 0; i < keys.size(); ++i) {
argsIn[keys[i]] = &values[i];
}
// Process expected parameters.
int hole = 0;
for (const std::string &argNamePattern : argNames) {
std::vector vargNames;
boost::algorithm::split(vargNames, argNamePattern,
boost::algorithm::is_any_of("|"));
auto fr = argsIn.end();
for (const std::string &argName : vargNames) {
fr = argsIn.find(argName);
if (fr != argsIn.end()) {
break;
}
}
if (fr != argsIn.end()) {
for (int i = 0; i < hole; ++i) {
// Fill hole between specified parameters with JSON nulls, but
// not at the end (for backwards compatibility with calls that
// act based on number of specified parameters).
out.params.push_back(UniValue());
}
hole = 0;
out.params.push_back(*fr->second);
argsIn.erase(fr);
} else {
hole += 1;
}
}
// If there are still arguments in the argsIn map, this is an error.
if (!argsIn.empty()) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"Unknown named parameter " + argsIn.begin()->first);
}
// Return request with named arguments transformed to positional arguments
return out;
}
UniValue CRPCTable::execute(Config &config,
const JSONRPCRequest &request) const {
// Return immediately if in warmup
{
LOCK(cs_rpcWarmup);
if (fRPCInWarmup) {
throw JSONRPCError(RPC_IN_WARMUP, rpcWarmupStatus);
}
}
// Check if legacy RPC method is valid.
// See RPCServer::ExecuteCommand for context-sensitive RPC commands.
const ContextFreeRPCCommand *pcmd = tableRPC[request.strMethod];
if (!pcmd) {
throw JSONRPCError(RPC_METHOD_NOT_FOUND, "Method not found");
}
- g_rpcSignals.PreCommand(*pcmd);
-
try {
+ RPCCommandExecution execution(request.strMethod);
// Execute, convert arguments to array if necessary
if (request.params.isObject()) {
return pcmd->call(config,
transformNamedArguments(request, pcmd->argNames));
} else {
return pcmd->call(config, request);
}
} catch (const std::exception &e) {
throw JSONRPCError(RPC_MISC_ERROR, e.what());
}
}
std::vector CRPCTable::listCommands() const {
std::vector commandList;
for (const auto &i : mapCommands) {
commandList.emplace_back(i.first);
}
return commandList;
}
std::string HelpExampleCli(const std::string &methodname,
const std::string &args) {
return "> bitcoin-cli " + methodname + " " + args + "\n";
}
std::string HelpExampleRpc(const std::string &methodname,
const std::string &args) {
return "> curl --user myusername --data-binary '{\"jsonrpc\": \"1.0\", "
"\"id\":\"curltest\", "
"\"method\": \"" +
methodname + "\", \"params\": [" + args +
"] }' -H 'content-type: text/plain;' http://127.0.0.1:8332/\n";
}
void RPCSetTimerInterfaceIfUnset(RPCTimerInterface *iface) {
if (!timerInterface) {
timerInterface = iface;
}
}
void RPCSetTimerInterface(RPCTimerInterface *iface) {
timerInterface = iface;
}
void RPCUnsetTimerInterface(RPCTimerInterface *iface) {
if (timerInterface == iface) {
timerInterface = nullptr;
}
}
void RPCRunLater(const std::string &name, std::function func,
int64_t nSeconds) {
if (!timerInterface) {
throw JSONRPCError(RPC_INTERNAL_ERROR,
"No timer handler registered for RPC");
}
deadlineTimers.erase(name);
LogPrint(BCLog::RPC, "queue run of timer %s in %i seconds (using %s)\n",
name, nSeconds, timerInterface->Name());
deadlineTimers.emplace(
name, std::unique_ptr(
timerInterface->NewTimer(func, nSeconds * 1000)));
}
int RPCSerializationFlags() {
return 0;
}
CRPCTable tableRPC;
diff --git a/test/functional/interface_rpc.py b/test/functional/interface_rpc.py
index 687965124..577fc2f81 100755
--- a/test/functional/interface_rpc.py
+++ b/test/functional/interface_rpc.py
@@ -1,47 +1,58 @@
#!/usr/bin/env python3
# Copyright (c) 2018 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Tests some generic aspects of the RPC interface."""
from test_framework.test_framework import BitcoinTestFramework
-from test_framework.util import assert_equal
+from test_framework.util import assert_equal, assert_greater_than_or_equal
class RPCInterfaceTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1
self.setup_clean_chain = True
+ def test_getrpcinfo(self):
+ self.log.info("Testing getrpcinfo...")
+
+ info = self.nodes[0].getrpcinfo()
+ assert_equal(len(info['active_commands']), 1)
+
+ command = info['active_commands'][0]
+ assert_equal(command['method'], 'getrpcinfo')
+ assert_greater_than_or_equal(command['duration'], 0)
+
def test_batch_request(self):
self.log.info("Testing basic JSON-RPC batch request...")
results = self.nodes[0].batch([
# A basic request that will work fine.
{"method": "getblockcount", "id": 1},
# Request that will fail. The whole batch request should still
# work fine.
{"method": "invalidmethod", "id": 2},
# Another call that should succeed.
{"method": "getbestblockhash", "id": 3},
])
result_by_id = {}
for res in results:
result_by_id[res["id"]] = res
assert_equal(result_by_id[1]['error'], None)
assert_equal(result_by_id[1]['result'], 0)
assert_equal(result_by_id[2]['error']['code'], -32601)
assert_equal(result_by_id[2]['result'], None)
assert_equal(result_by_id[3]['error'], None)
assert result_by_id[3]['result'] is not None
def run_test(self):
+ self.test_getrpcinfo()
self.test_batch_request()
if __name__ == '__main__':
RPCInterfaceTest().main()