diff --git a/src/Makefile.am b/src/Makefile.am --- a/src/Makefile.am +++ b/src/Makefile.am @@ -165,6 +165,7 @@ reverselock.h \ rpc/blockchain.h \ rpc/client.h \ + rpc/command.h \ rpc/jsonrpcrequest.h \ rpc/mining.h \ rpc/misc.h \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -86,6 +86,7 @@ test/rcu_tests.cpp \ test/reverselock_tests.cpp \ test/rpc_tests.cpp \ + test/rpc_server_tests.cpp \ test/rwcollection_tests.cpp \ test/sanity_tests.cpp \ test/scheduler_tests.cpp \ diff --git a/src/rpc/command.h b/src/rpc/command.h new file mode 100644 --- /dev/null +++ b/src/rpc/command.h @@ -0,0 +1,39 @@ +// 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. + +#ifndef BITCOIN_RPC_COMMAND_H +#define BITCOIN_RPC_COMMAND_H + +#include + +#include + +#include + +/** + * Base class for all RPC commands. + */ +class RPCCommand : public boost::noncopyable { +private: + const std::string name; + + /* + * Child classes should define dependencies as private members. + * These dependencies must not be changed, or successive calls to the same + * command will give unexpected behavior. + */ + + // TODO: Parameter definitions (these will be used to generate help + // messages as well) + +public: + RPCCommand(std::string nameIn) : name(nameIn) {} + virtual ~RPCCommand() {} + + virtual UniValue Execute(const UniValue &args) const = 0; + + std::string GetName() const { return name; }; +}; + +#endif // BITCOIN_RPC_COMMAND_H diff --git a/src/rpc/server.h b/src/rpc/server.h --- a/src/rpc/server.h +++ b/src/rpc/server.h @@ -1,6 +1,6 @@ // Copyright (c) 2010 Satoshi Nakamoto // Copyright (c) 2009-2016 The Bitcoin Core developers -// Copyright (c) 2017-2018 The Bitcoin developers +// Copyright (c) 2017-2019 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -8,8 +8,10 @@ #define BITCOIN_RPC_SERVER_H #include "amount.h" +#include "rpc/command.h" #include "rpc/jsonrpcrequest.h" #include "rpc/protocol.h" +#include "rwcollection.h" #include "uint256.h" #include "util.h" @@ -47,10 +49,15 @@ UniValue::VType type; }; +typedef std::map> RPCCommandMap; + /** * Class for registering and managing all RPC calls. */ class RPCServer : public boost::noncopyable { +private: + RWCollection commands; + public: RPCServer() {} @@ -60,6 +67,11 @@ */ UniValue ExecuteCommand(Config &config, const JSONRPCRequest &request) const; + + /** + * Register an RPC command. + */ + void RegisterCommand(std::unique_ptr command); }; /** diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp --- a/src/rpc/server.cpp +++ b/src/rpc/server.cpp @@ -1,5 +1,6 @@ // Copyright (c) 2010 Satoshi Nakamoto // Copyright (c) 2009-2016 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. @@ -39,6 +40,9 @@ 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) { @@ -46,13 +50,29 @@ } } - // TODO Only call tableRPC.execute() if no context-sensitive RPC command - // exists + std::string commandName = request.strMethod; + { + auto commandsReadView = commands.getReadView(); + auto iter = commandsReadView->find(commandName); + if (iter != commandsReadView.end()) { + return iter->second.get()->Execute(request.params); + } + } + + // 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) { + commands.getWriteView()->insert( + std::make_pair(command->GetName(), std::move(command))); + } +} + static struct CRPCSignals { boost::signals2::signal Started; boost::signals2::signal Stopped; diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -102,6 +102,7 @@ rcu_tests.cpp reverselock_tests.cpp rpc_tests.cpp + rpc_server_tests.cpp rwcollection_tests.cpp sanity_tests.cpp scheduler_tests.cpp diff --git a/src/test/rpc_server_tests.cpp b/src/test/rpc_server_tests.cpp new file mode 100644 --- /dev/null +++ b/src/test/rpc_server_tests.cpp @@ -0,0 +1,56 @@ +// 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 "chainparams.h" +#include "config.h" +#include "rpc/jsonrpcrequest.h" +#include "rpc/server.h" +#include "util.h" + +#include "test/test_bitcoin.h" + +#include + +#include + +BOOST_FIXTURE_TEST_SUITE(rpc_server_tests, TestingSetup) + +class ArgsTestRPCCommand : public RPCCommand { +public: + ArgsTestRPCCommand(std::string nameIn) : RPCCommand(nameIn) {} + + UniValue Execute(const UniValue &args) const override { + BOOST_CHECK_EQUAL(args["arg1"].get_str(), "value1"); + return UniValue("testing"); + } +}; + +static bool isRpcMethodNotFound(const UniValue &u) { + return find_value(u, "code").get_int() == int(RPC_METHOD_NOT_FOUND); +} + +BOOST_AUTO_TEST_CASE(rpc_server_execute_command) { + DummyConfig config; + RPCServer rpcServer; + const std::string commandName = "testcommand"; + rpcServer.RegisterCommand(MakeUnique(commandName)); + + UniValue args(UniValue::VOBJ); + args.pushKV("arg1", "value1"); + + // Registered commands execute and return values correctly + JSONRPCRequest request; + request.strMethod = commandName; + request.params = args; + UniValue output = rpcServer.ExecuteCommand(config, request); + BOOST_CHECK_EQUAL(output.get_str(), "testing"); + + // Not-registered commands throw an exception as expected + JSONRPCRequest badCommandRequest; + badCommandRequest.strMethod = "this-command-does-not-exist"; + BOOST_CHECK_EXCEPTION(rpcServer.ExecuteCommand(config, badCommandRequest), + UniValue, isRpcMethodNotFound); +} + +BOOST_AUTO_TEST_SUITE_END()