mirror of
https://github.com/bitcoin/bitcoin.git
synced 2024-11-19 09:53:47 +01:00
Merge bitcoin/bitcoin#30509: multiprocess: Add -ipcbind option to bitcoin-node
30073e6b3a
multiprocess: Add -ipcbind option to bitcoin-node (Russell Yanofsky)73fe7d7230
multiprocess: Add unit tests for connect, serve, and listen functions (Ryan Ofsky)955d4077aa
multiprocess: Add IPC connectAddress and listenAddress methods (Russell Yanofsky)4da20434d4
depends: Update libmultiprocess library for CustomMessage function and ThreadContext bugfix (Ryan Ofsky) Pull request description: Add `-ipcbind` option to `bitcoin-node` to make it listen on a unix socket and accept connections from other processes. The default socket path is `<datadir>/node.sock`, but this can be customized. This option lets potential wallet, gui, index, and mining processes connect to the node and control it. See examples in #19460, #19461, and #30437. Motivation for this PR, in combination with #30510, is be able to release a bitcoin core node binary that can generate block templates for a separate Stratum v2 mining service, like the one being implemented in https://github.com/Sjors/bitcoin/pull/48, that connects over IPC. Other things to know about this PR: - While the `-ipcbind` option lets other processes to connect to the `bitcoin-node` process, the only thing they can actually do after connecting is call methods on the [`Init`](https://github.com/bitcoin/bitcoin/blob/master/src/ipc/capnp/init.capnp#L17-L20) interface which is currently very limited and doesn't do much. But PRs [#30510](https://github.com/bitcoin/bitcoin/pull/30510), [#29409](https://github.com/bitcoin/bitcoin/pull/29409), and [#10102](https://github.com/bitcoin/bitcoin/pull/10102) expand the `Init` interface to expose mining, wallet, and gui functionality respectively. - This PR is not needed for [#10102](https://github.com/bitcoin/bitcoin/pull/10102), which runs GUI, node, and wallet code in different processes, because [#10102](https://github.com/bitcoin/bitcoin/pull/10102) does not use unix sockets or allow outside processes to connect to existing processes. [#10102](https://github.com/bitcoin/bitcoin/pull/10102) lets parent and child processes communicate over internal socketpairs, not externally accessible sockets. --- This PR is part of the [process separation project](https://github.com/bitcoin/bitcoin/issues/28722). ACKs for top commit: achow101: ACK30073e6b3a
TheCharlatan: Re-ACK30073e6b3a
itornaza: Code review ACK30073e6b3a
Tree-SHA512: 2b766e60535f57352e8afda9c3748a32acb5a57b2827371b48ba865fa9aa1df00f340732654f2e300c6823dbc6f3e14377fca87e4e959e613fe85a6d2312d9c8
This commit is contained in:
commit
df3f63ccfa
@ -1,8 +1,8 @@
|
||||
package=native_libmultiprocess
|
||||
$(package)_version=6aca5f389bacf2942394b8738bbe15d6c9edfb9b
|
||||
$(package)_version=c1b4ab4eb897d3af09bc9b3cc30e2e6fff87f3e2
|
||||
$(package)_download_path=https://github.com/chaincodelabs/libmultiprocess/archive
|
||||
$(package)_file_name=$($(package)_version).tar.gz
|
||||
$(package)_sha256_hash=2efeed53542bc1d8af3291f2b6f0e5d430d86a5e04e415ce33c136f2c226a51d
|
||||
$(package)_sha256_hash=6edf5ad239ca9963c78f7878486fb41411efc9927c6073928a7d6edf947cac4a
|
||||
$(package)_dependencies=native_capnp
|
||||
|
||||
define $(package)_config_cmds
|
||||
|
@ -109,10 +109,11 @@ int fork_daemon(bool nochdir, bool noclose, TokenPipeEnd& endpoint)
|
||||
|
||||
#endif
|
||||
|
||||
static bool ParseArgs(ArgsManager& args, int argc, char* argv[])
|
||||
static bool ParseArgs(NodeContext& node, int argc, char* argv[])
|
||||
{
|
||||
ArgsManager& args{*Assert(node.args)};
|
||||
// If Qt is used, parameters/bitcoin.conf are parsed in qt/bitcoin.cpp's main()
|
||||
SetupServerArgs(args);
|
||||
SetupServerArgs(args, node.init->canListenIpc());
|
||||
std::string error;
|
||||
if (!args.ParseParameters(argc, argv, error)) {
|
||||
return InitError(Untranslated(strprintf("Error parsing command line arguments: %s", error)));
|
||||
@ -268,7 +269,7 @@ MAIN_FUNCTION
|
||||
|
||||
// Interpret command line arguments
|
||||
ArgsManager& args = *Assert(node.args);
|
||||
if (!ParseArgs(args, argc, argv)) return EXIT_FAILURE;
|
||||
if (!ParseArgs(node, argc, argv)) return EXIT_FAILURE;
|
||||
// Process early info return commands such as -help or -version
|
||||
if (ProcessInitCommands(args)) return EXIT_SUCCESS;
|
||||
|
||||
|
@ -635,6 +635,9 @@ std::string ArgsManager::GetHelpMessage() const
|
||||
case OptionsCategory::RPC:
|
||||
usage += HelpMessageGroup("RPC server options:");
|
||||
break;
|
||||
case OptionsCategory::IPC:
|
||||
usage += HelpMessageGroup("IPC interprocess connection options:");
|
||||
break;
|
||||
case OptionsCategory::WALLET:
|
||||
usage += HelpMessageGroup("Wallet options:");
|
||||
break;
|
||||
|
@ -64,6 +64,7 @@ enum class OptionsCategory {
|
||||
COMMANDS,
|
||||
REGISTER_COMMANDS,
|
||||
CLI_COMMANDS,
|
||||
IPC,
|
||||
|
||||
HIDDEN // Always the last option to avoid printing these in the help
|
||||
};
|
||||
|
17
src/init.cpp
17
src/init.cpp
@ -29,6 +29,7 @@
|
||||
#include <init/common.h>
|
||||
#include <interfaces/chain.h>
|
||||
#include <interfaces/init.h>
|
||||
#include <interfaces/ipc.h>
|
||||
#include <interfaces/mining.h>
|
||||
#include <interfaces/node.h>
|
||||
#include <kernel/context.h>
|
||||
@ -441,7 +442,7 @@ static void OnRPCStopped()
|
||||
LogDebug(BCLog::RPC, "RPC stopped.\n");
|
||||
}
|
||||
|
||||
void SetupServerArgs(ArgsManager& argsman)
|
||||
void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
|
||||
{
|
||||
SetupHelpOptions(argsman);
|
||||
argsman.AddArg("-help-debug", "Print help message with debugging options and exit", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST); // server-only for now
|
||||
@ -676,6 +677,9 @@ void SetupServerArgs(ArgsManager& argsman)
|
||||
argsman.AddArg("-rpcwhitelistdefault", "Sets default behavior for rpc whitelisting. Unless rpcwhitelistdefault is set to 0, if any -rpcwhitelist is set, the rpc server acts as if all rpc users are subject to empty-unless-otherwise-specified whitelists. If rpcwhitelistdefault is set to 1 and no -rpcwhitelist is set, rpc server acts as if all rpc users are subject to empty whitelists.", ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
|
||||
argsman.AddArg("-rpcworkqueue=<n>", strprintf("Set the depth of the work queue to service RPC calls (default: %d)", DEFAULT_HTTP_WORKQUEUE), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::RPC);
|
||||
argsman.AddArg("-server", "Accept command line and JSON-RPC commands", ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
|
||||
if (can_listen_ipc) {
|
||||
argsman.AddArg("-ipcbind=<address>", "Bind to Unix socket address and listen for incoming connections. Valid address values are \"unix\" to listen on the default path, <datadir>/node.sock, or \"unix:/custom/path\" to specify a custom path. Can be specified multiple times to listen on multiple paths. Default behavior is not to listen on any path. If relative paths are specified, they are interpreted relative to the network data directory. If paths include any parent directory components and the parent directories do not exist, they will be created.", ArgsManager::ALLOW_ANY, OptionsCategory::IPC);
|
||||
}
|
||||
|
||||
#if HAVE_DECL_FORK
|
||||
argsman.AddArg("-daemon", strprintf("Run in the background as a daemon and accept commands (default: %d)", DEFAULT_DAEMON), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
|
||||
@ -1200,6 +1204,17 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
|
||||
g_wallet_init_interface.Construct(node);
|
||||
uiInterface.InitWallet();
|
||||
|
||||
if (interfaces::Ipc* ipc = node.init->ipc()) {
|
||||
for (std::string address : gArgs.GetArgs("-ipcbind")) {
|
||||
try {
|
||||
ipc->listenAddress(address);
|
||||
} catch (const std::exception& e) {
|
||||
return InitError(strprintf(Untranslated("Unable to bind to IPC address '%s'. %s"), address, e.what()));
|
||||
}
|
||||
LogPrintf("Listening for IPC requests on address %s\n", address);
|
||||
}
|
||||
}
|
||||
|
||||
/* Register RPC commands regardless of -server setting so they will be
|
||||
* available in the GUI RPC console even if external calls are disabled.
|
||||
*/
|
||||
|
@ -74,7 +74,7 @@ bool AppInitMain(node::NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip
|
||||
/**
|
||||
* Register all arguments with the ArgsManager
|
||||
*/
|
||||
void SetupServerArgs(ArgsManager& argsman);
|
||||
void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc=false);
|
||||
|
||||
/** Validates requirements to run the indexes and spawns each index initial sync thread */
|
||||
bool StartIndexBackgroundSync(node::NodeContext& node);
|
||||
|
@ -34,6 +34,11 @@ public:
|
||||
}
|
||||
std::unique_ptr<interfaces::Echo> makeEcho() override { return interfaces::MakeEcho(); }
|
||||
interfaces::Ipc* ipc() override { return m_ipc.get(); }
|
||||
// bitcoin-gui accepts -ipcbind option even though it does not use it
|
||||
// directly. It just returns true here to accept the option because
|
||||
// bitcoin-node accepts the option, and bitcoin-gui accepts all bitcoin-node
|
||||
// options and will start the node with those options.
|
||||
bool canListenIpc() override { return true; }
|
||||
node::NodeContext m_node;
|
||||
std::unique_ptr<interfaces::Ipc> m_ipc;
|
||||
};
|
||||
|
@ -37,6 +37,7 @@ public:
|
||||
}
|
||||
std::unique_ptr<interfaces::Echo> makeEcho() override { return interfaces::MakeEcho(); }
|
||||
interfaces::Ipc* ipc() override { return m_ipc.get(); }
|
||||
bool canListenIpc() override { return true; }
|
||||
node::NodeContext& m_node;
|
||||
std::unique_ptr<interfaces::Ipc> m_ipc;
|
||||
};
|
||||
|
@ -37,6 +37,7 @@ public:
|
||||
virtual std::unique_ptr<WalletLoader> makeWalletLoader(Chain& chain) { return nullptr; }
|
||||
virtual std::unique_ptr<Echo> makeEcho() { return nullptr; }
|
||||
virtual Ipc* ipc() { return nullptr; }
|
||||
virtual bool canListenIpc() { return false; }
|
||||
};
|
||||
|
||||
//! Return implementation of Init interface for the node process. If the argv
|
||||
|
@ -41,6 +41,11 @@ class Init;
|
||||
//! to make other proxy objects calling other remote interfaces. It can also
|
||||
//! destroy the initial interfaces::Init object to close the connection and
|
||||
//! shut down the spawned process.
|
||||
//!
|
||||
//! When connecting to an existing process, the steps are similar to spawning a
|
||||
//! new process, except a socket is created instead of a socketpair, and
|
||||
//! destroying an Init interface doesn't end the process, since there can be
|
||||
//! multiple connections.
|
||||
class Ipc
|
||||
{
|
||||
public:
|
||||
@ -54,6 +59,17 @@ public:
|
||||
//! true. If this is not a spawned child process, return false.
|
||||
virtual bool startSpawnedProcess(int argc, char* argv[], int& exit_status) = 0;
|
||||
|
||||
//! Connect to a socket address and make a client interface proxy object
|
||||
//! using provided callback. connectAddress returns an interface pointer if
|
||||
//! the connection was established, returns null if address is empty ("") or
|
||||
//! disabled ("0") or if a connection was refused but not required ("auto"),
|
||||
//! and throws an exception if there was an unexpected error.
|
||||
virtual std::unique_ptr<Init> connectAddress(std::string& address) = 0;
|
||||
|
||||
//! Connect to a socket address and make a client interface proxy object
|
||||
//! using provided callback. Throws an exception if there was an error.
|
||||
virtual void listenAddress(std::string& address) = 0;
|
||||
|
||||
//! Add cleanup callback to remote interface that will run when the
|
||||
//! interface is deleted.
|
||||
template<typename Interface>
|
||||
|
@ -23,6 +23,8 @@
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <sys/socket.h>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
|
||||
namespace ipc {
|
||||
@ -51,11 +53,20 @@ public:
|
||||
startLoop(exe_name);
|
||||
return mp::ConnectStream<messages::Init>(*m_loop, fd);
|
||||
}
|
||||
void serve(int fd, const char* exe_name, interfaces::Init& init) override
|
||||
void listen(int listen_fd, const char* exe_name, interfaces::Init& init) override
|
||||
{
|
||||
startLoop(exe_name);
|
||||
if (::listen(listen_fd, /*backlog=*/5) != 0) {
|
||||
throw std::system_error(errno, std::system_category());
|
||||
}
|
||||
mp::ListenConnections<messages::Init>(*m_loop, listen_fd, init);
|
||||
}
|
||||
void serve(int fd, const char* exe_name, interfaces::Init& init, const std::function<void()>& ready_fn = {}) override
|
||||
{
|
||||
assert(!m_loop);
|
||||
mp::g_thread_context.thread_name = mp::ThreadName(exe_name);
|
||||
m_loop.emplace(exe_name, &IpcLogFn, &m_context);
|
||||
if (ready_fn) ready_fn();
|
||||
mp::ServeStream<messages::Init>(*m_loop, fd, init);
|
||||
m_loop->loop();
|
||||
m_loop.reset();
|
||||
|
@ -2,6 +2,7 @@
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <common/args.h>
|
||||
#include <common/system.h>
|
||||
#include <interfaces/init.h>
|
||||
#include <interfaces/ipc.h>
|
||||
@ -56,6 +57,35 @@ public:
|
||||
exit_status = EXIT_SUCCESS;
|
||||
return true;
|
||||
}
|
||||
std::unique_ptr<interfaces::Init> connectAddress(std::string& address) override
|
||||
{
|
||||
if (address.empty() || address == "0") return nullptr;
|
||||
int fd;
|
||||
if (address == "auto") {
|
||||
// Treat "auto" the same as "unix" except don't treat it an as error
|
||||
// if the connection is not accepted. Just return null so the caller
|
||||
// can work offline without a connection, or spawn a new
|
||||
// bitcoin-node process and connect to it.
|
||||
address = "unix";
|
||||
try {
|
||||
fd = m_process->connect(gArgs.GetDataDirNet(), "bitcoin-node", address);
|
||||
} catch (const std::system_error& e) {
|
||||
// If connection type is auto and socket path isn't accepting connections, or doesn't exist, catch the error and return null;
|
||||
if (e.code() == std::errc::connection_refused || e.code() == std::errc::no_such_file_or_directory) {
|
||||
return nullptr;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
} else {
|
||||
fd = m_process->connect(gArgs.GetDataDirNet(), "bitcoin-node", address);
|
||||
}
|
||||
return m_protocol->connect(fd, m_exe_name);
|
||||
}
|
||||
void listenAddress(std::string& address) override
|
||||
{
|
||||
int fd = m_process->bind(gArgs.GetDataDirNet(), m_exe_name, address);
|
||||
m_protocol->listen(fd, m_exe_name, m_init);
|
||||
}
|
||||
void addCleanup(std::type_index type, void* iface, std::function<void()> cleanup) override
|
||||
{
|
||||
m_protocol->addCleanup(type, iface, std::move(cleanup));
|
||||
|
@ -4,22 +4,28 @@
|
||||
|
||||
#include <ipc/process.h>
|
||||
#include <ipc/protocol.h>
|
||||
#include <logging.h>
|
||||
#include <mp/util.h>
|
||||
#include <tinyformat.h>
|
||||
#include <util/fs.h>
|
||||
#include <util/strencodings.h>
|
||||
#include <util/syserror.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <errno.h>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string.h>
|
||||
#include <system_error>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using util::RemovePrefixView;
|
||||
|
||||
namespace ipc {
|
||||
namespace {
|
||||
class ProcessImpl : public Process
|
||||
@ -54,7 +60,95 @@ public:
|
||||
}
|
||||
return true;
|
||||
}
|
||||
int connect(const fs::path& data_dir,
|
||||
const std::string& dest_exe_name,
|
||||
std::string& address) override;
|
||||
int bind(const fs::path& data_dir, const std::string& exe_name, std::string& address) override;
|
||||
};
|
||||
|
||||
static bool ParseAddress(std::string& address,
|
||||
const fs::path& data_dir,
|
||||
const std::string& dest_exe_name,
|
||||
struct sockaddr_un& addr,
|
||||
std::string& error)
|
||||
{
|
||||
if (address.compare(0, 4, "unix") == 0 && (address.size() == 4 || address[4] == ':')) {
|
||||
fs::path path;
|
||||
if (address.size() <= 5) {
|
||||
path = data_dir / fs::PathFromString(strprintf("%s.sock", RemovePrefixView(dest_exe_name, "bitcoin-")));
|
||||
} else {
|
||||
path = data_dir / fs::PathFromString(address.substr(5));
|
||||
}
|
||||
std::string path_str = fs::PathToString(path);
|
||||
address = strprintf("unix:%s", path_str);
|
||||
if (path_str.size() >= sizeof(addr.sun_path)) {
|
||||
error = strprintf("Unix address path %s exceeded maximum socket path length", fs::quoted(fs::PathToString(path)));
|
||||
return false;
|
||||
}
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sun_family = AF_UNIX;
|
||||
strncpy(addr.sun_path, path_str.c_str(), sizeof(addr.sun_path)-1);
|
||||
return true;
|
||||
}
|
||||
|
||||
error = strprintf("Unrecognized address '%s'", address);
|
||||
return false;
|
||||
}
|
||||
|
||||
int ProcessImpl::connect(const fs::path& data_dir,
|
||||
const std::string& dest_exe_name,
|
||||
std::string& address)
|
||||
{
|
||||
struct sockaddr_un addr;
|
||||
std::string error;
|
||||
if (!ParseAddress(address, data_dir, dest_exe_name, addr, error)) {
|
||||
throw std::invalid_argument(error);
|
||||
}
|
||||
|
||||
int fd;
|
||||
if ((fd = ::socket(addr.sun_family, SOCK_STREAM, 0)) == -1) {
|
||||
throw std::system_error(errno, std::system_category());
|
||||
}
|
||||
if (::connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == 0) {
|
||||
return fd;
|
||||
}
|
||||
int connect_error = errno;
|
||||
if (::close(fd) != 0) {
|
||||
LogPrintf("Error closing file descriptor %i '%s': %s\n", fd, address, SysErrorString(errno));
|
||||
}
|
||||
throw std::system_error(connect_error, std::system_category());
|
||||
}
|
||||
|
||||
int ProcessImpl::bind(const fs::path& data_dir, const std::string& exe_name, std::string& address)
|
||||
{
|
||||
struct sockaddr_un addr;
|
||||
std::string error;
|
||||
if (!ParseAddress(address, data_dir, exe_name, addr, error)) {
|
||||
throw std::invalid_argument(error);
|
||||
}
|
||||
|
||||
if (addr.sun_family == AF_UNIX) {
|
||||
fs::path path = addr.sun_path;
|
||||
if (path.has_parent_path()) fs::create_directories(path.parent_path());
|
||||
if (fs::symlink_status(path).type() == fs::file_type::socket) {
|
||||
fs::remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
int fd;
|
||||
if ((fd = ::socket(addr.sun_family, SOCK_STREAM, 0)) == -1) {
|
||||
throw std::system_error(errno, std::system_category());
|
||||
}
|
||||
|
||||
if (::bind(fd, (struct sockaddr*)&addr, sizeof(addr)) == 0) {
|
||||
return fd;
|
||||
}
|
||||
int bind_error = errno;
|
||||
if (::close(fd) != 0) {
|
||||
LogPrintf("Error closing file descriptor %i: %s\n", fd, SysErrorString(errno));
|
||||
}
|
||||
throw std::system_error(bind_error, std::system_category());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<Process> MakeProcess() { return std::make_unique<ProcessImpl>(); }
|
||||
|
@ -34,6 +34,16 @@ public:
|
||||
//! process. If so, return true and a file descriptor for communicating
|
||||
//! with the parent process.
|
||||
virtual bool checkSpawned(int argc, char* argv[], int& fd) = 0;
|
||||
|
||||
//! Canonicalize and connect to address, returning socket descriptor.
|
||||
virtual int connect(const fs::path& data_dir,
|
||||
const std::string& dest_exe_name,
|
||||
std::string& address) = 0;
|
||||
|
||||
//! Create listening socket, bind and canonicalize address, and return socket descriptor.
|
||||
virtual int bind(const fs::path& data_dir,
|
||||
const std::string& exe_name,
|
||||
std::string& address) = 0;
|
||||
};
|
||||
|
||||
//! Constructor for Process interface. Implementation will vary depending on
|
||||
|
@ -25,12 +25,38 @@ public:
|
||||
|
||||
//! Return Init interface that forwards requests over given socket descriptor.
|
||||
//! Socket communication is handled on a background thread.
|
||||
//!
|
||||
//! @note It could be potentially useful in the future to add
|
||||
//! std::function<void()> on_disconnect callback argument here. But there
|
||||
//! isn't an immediate need, because the protocol implementation can clean
|
||||
//! up its own state (calling ProxyServer destructors, etc) on disconnect,
|
||||
//! and any client calls will just throw ipc::Exception errors after a
|
||||
//! disconnect.
|
||||
virtual std::unique_ptr<interfaces::Init> connect(int fd, const char* exe_name) = 0;
|
||||
|
||||
//! Listen for connections on provided socket descriptor, accept them, and
|
||||
//! handle requests on accepted connections. This method doesn't block, and
|
||||
//! performs I/O on a background thread.
|
||||
virtual void listen(int listen_fd, const char* exe_name, interfaces::Init& init) = 0;
|
||||
|
||||
//! Handle requests on provided socket descriptor, forwarding them to the
|
||||
//! provided Init interface. Socket communication is handled on the
|
||||
//! current thread, and this call blocks until the socket is closed.
|
||||
virtual void serve(int fd, const char* exe_name, interfaces::Init& init) = 0;
|
||||
//!
|
||||
//! @note: If this method is called, it needs be called before connect() or
|
||||
//! listen() methods, because for ease of implementation it's inflexible and
|
||||
//! always runs the event loop in the foreground thread. It can share its
|
||||
//! event loop with the other methods but can't share an event loop that was
|
||||
//! created by them. This isn't really a problem because serve() is only
|
||||
//! called by spawned child processes that call it immediately to
|
||||
//! communicate back with parent processes.
|
||||
//
|
||||
//! The optional `ready_fn` callback will be called after the event loop is
|
||||
//! created but before it is started. This can be useful in tests to trigger
|
||||
//! client connections from another thread as soon as the event loop is
|
||||
//! available, but should not be neccessary in normal code which starts
|
||||
//! clients and servers independently.
|
||||
virtual void serve(int fd, const char* exe_name, interfaces::Init& init, const std::function<void()>& ready_fn = {}) = 0;
|
||||
|
||||
//! Add cleanup callback to interface that will run when the interface is
|
||||
//! deleted.
|
||||
|
@ -525,7 +525,7 @@ int GuiMain(int argc, char* argv[])
|
||||
|
||||
/// 2. Parse command-line options. We do this after qt in order to show an error if there are problems parsing these
|
||||
// Command-line options take precedence:
|
||||
SetupServerArgs(gArgs);
|
||||
SetupServerArgs(gArgs, init->canListenIpc());
|
||||
SetupUIArgs(gArgs);
|
||||
std::string error;
|
||||
if (!gArgs.ParseParameters(argc, argv, error)) {
|
||||
|
@ -177,7 +177,7 @@ if(WITH_MULTIPROCESS)
|
||||
PRIVATE
|
||||
ipc_tests.cpp
|
||||
)
|
||||
target_link_libraries(test_bitcoin bitcoin_ipc_test)
|
||||
target_link_libraries(test_bitcoin bitcoin_ipc_test bitcoin_ipc)
|
||||
endif()
|
||||
|
||||
function(add_boost_test source_file)
|
||||
|
@ -59,7 +59,7 @@ FUZZ_TARGET(system, .init = initialize_system)
|
||||
args_manager.SoftSetBoolArg(str_arg, f_value);
|
||||
},
|
||||
[&] {
|
||||
const OptionsCategory options_category = fuzzed_data_provider.PickValueInArray<OptionsCategory>({OptionsCategory::OPTIONS, OptionsCategory::CONNECTION, OptionsCategory::WALLET, OptionsCategory::WALLET_DEBUG_TEST, OptionsCategory::ZMQ, OptionsCategory::DEBUG_TEST, OptionsCategory::CHAINPARAMS, OptionsCategory::NODE_RELAY, OptionsCategory::BLOCK_CREATION, OptionsCategory::RPC, OptionsCategory::GUI, OptionsCategory::COMMANDS, OptionsCategory::REGISTER_COMMANDS, OptionsCategory::CLI_COMMANDS, OptionsCategory::HIDDEN});
|
||||
const OptionsCategory options_category = fuzzed_data_provider.PickValueInArray<OptionsCategory>({OptionsCategory::OPTIONS, OptionsCategory::CONNECTION, OptionsCategory::WALLET, OptionsCategory::WALLET_DEBUG_TEST, OptionsCategory::ZMQ, OptionsCategory::DEBUG_TEST, OptionsCategory::CHAINPARAMS, OptionsCategory::NODE_RELAY, OptionsCategory::BLOCK_CREATION, OptionsCategory::RPC, OptionsCategory::GUI, OptionsCategory::COMMANDS, OptionsCategory::REGISTER_COMMANDS, OptionsCategory::CLI_COMMANDS, OptionsCategory::IPC, OptionsCategory::HIDDEN});
|
||||
// Avoid hitting:
|
||||
// common/args.cpp:563: void ArgsManager::AddArg(const std::string &, const std::string &, unsigned int, const OptionsCategory &): Assertion `ret.second' failed.
|
||||
const std::string argument_name = GetArgumentName(fuzzed_data_provider.ConsumeRandomLengthString(16));
|
||||
|
@ -2,19 +2,46 @@
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <interfaces/init.h>
|
||||
#include <ipc/capnp/protocol.h>
|
||||
#include <ipc/process.h>
|
||||
#include <ipc/protocol.h>
|
||||
#include <logging.h>
|
||||
#include <mp/proxy-types.h>
|
||||
#include <test/ipc_test.capnp.h>
|
||||
#include <test/ipc_test.capnp.proxy.h>
|
||||
#include <test/ipc_test.h>
|
||||
#include <tinyformat.h>
|
||||
|
||||
#include <future>
|
||||
#include <thread>
|
||||
#include <kj/common.h>
|
||||
#include <kj/memory.h>
|
||||
#include <kj/test.h>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
//! Remote init class.
|
||||
class TestInit : public interfaces::Init
|
||||
{
|
||||
public:
|
||||
std::unique_ptr<interfaces::Echo> makeEcho() override { return interfaces::MakeEcho(); }
|
||||
};
|
||||
|
||||
//! Generate a temporary path with temp_directory_path and mkstemp
|
||||
static std::string TempPath(std::string_view pattern)
|
||||
{
|
||||
std::string temp{fs::PathToString(fs::path{fs::temp_directory_path()} / fs::PathFromString(std::string{pattern}))};
|
||||
temp.push_back('\0');
|
||||
int fd{mkstemp(temp.data())};
|
||||
BOOST_CHECK_GE(fd, 0);
|
||||
BOOST_CHECK_EQUAL(close(fd), 0);
|
||||
temp.resize(temp.size() - 1);
|
||||
fs::remove(fs::PathFromString(temp));
|
||||
return temp;
|
||||
}
|
||||
|
||||
//! Unit test that tests execution of IPC calls without actually creating a
|
||||
//! separate process. This test is primarily intended to verify behavior of type
|
||||
//! conversion code that converts C++ objects to Cap'n Proto messages and vice
|
||||
@ -23,13 +50,13 @@
|
||||
//! The test creates a thread which creates a FooImplementation object (defined
|
||||
//! in ipc_test.h) and a two-way pipe accepting IPC requests which call methods
|
||||
//! on the object through FooInterface (defined in ipc_test.capnp).
|
||||
void IpcTest()
|
||||
void IpcPipeTest()
|
||||
{
|
||||
// Setup: create FooImplemention object and listen for FooInterface requests
|
||||
std::promise<std::unique_ptr<mp::ProxyClient<gen::FooInterface>>> foo_promise;
|
||||
std::function<void()> disconnect_client;
|
||||
std::thread thread([&]() {
|
||||
mp::EventLoop loop("IpcTest", [](bool raise, const std::string& log) { LogPrintf("LOG%i: %s\n", raise, log); });
|
||||
mp::EventLoop loop("IpcPipeTest", [](bool raise, const std::string& log) { LogPrintf("LOG%i: %s\n", raise, log); });
|
||||
auto pipe = loop.m_io_context.provider->newTwoWayPipe();
|
||||
|
||||
auto connection_client = std::make_unique<mp::Connection>(loop, kj::mv(pipe.ends[0]));
|
||||
@ -65,3 +92,71 @@ void IpcTest()
|
||||
disconnect_client();
|
||||
thread.join();
|
||||
}
|
||||
|
||||
//! Test ipc::Protocol connect() and serve() methods connecting over a socketpair.
|
||||
void IpcSocketPairTest()
|
||||
{
|
||||
int fds[2];
|
||||
BOOST_CHECK_EQUAL(socketpair(AF_UNIX, SOCK_STREAM, 0, fds), 0);
|
||||
std::unique_ptr<interfaces::Init> init{std::make_unique<TestInit>()};
|
||||
std::unique_ptr<ipc::Protocol> protocol{ipc::capnp::MakeCapnpProtocol()};
|
||||
std::promise<void> promise;
|
||||
std::thread thread([&]() {
|
||||
protocol->serve(fds[0], "test-serve", *init, [&] { promise.set_value(); });
|
||||
});
|
||||
promise.get_future().wait();
|
||||
std::unique_ptr<interfaces::Init> remote_init{protocol->connect(fds[1], "test-connect")};
|
||||
std::unique_ptr<interfaces::Echo> remote_echo{remote_init->makeEcho()};
|
||||
BOOST_CHECK_EQUAL(remote_echo->echo("echo test"), "echo test");
|
||||
remote_echo.reset();
|
||||
remote_init.reset();
|
||||
thread.join();
|
||||
}
|
||||
|
||||
//! Test ipc::Process bind() and connect() methods connecting over a unix socket.
|
||||
void IpcSocketTest(const fs::path& datadir)
|
||||
{
|
||||
std::unique_ptr<interfaces::Init> init{std::make_unique<TestInit>()};
|
||||
std::unique_ptr<ipc::Protocol> protocol{ipc::capnp::MakeCapnpProtocol()};
|
||||
std::unique_ptr<ipc::Process> process{ipc::MakeProcess()};
|
||||
|
||||
std::string invalid_bind{"invalid:"};
|
||||
BOOST_CHECK_THROW(process->bind(datadir, "test_bitcoin", invalid_bind), std::invalid_argument);
|
||||
BOOST_CHECK_THROW(process->connect(datadir, "test_bitcoin", invalid_bind), std::invalid_argument);
|
||||
|
||||
auto bind_and_listen{[&](const std::string& bind_address) {
|
||||
std::string address{bind_address};
|
||||
int serve_fd = process->bind(datadir, "test_bitcoin", address);
|
||||
BOOST_CHECK_GE(serve_fd, 0);
|
||||
BOOST_CHECK_EQUAL(address, bind_address);
|
||||
protocol->listen(serve_fd, "test-serve", *init);
|
||||
}};
|
||||
|
||||
auto connect_and_test{[&](const std::string& connect_address) {
|
||||
std::string address{connect_address};
|
||||
int connect_fd{process->connect(datadir, "test_bitcoin", address)};
|
||||
BOOST_CHECK_EQUAL(address, connect_address);
|
||||
std::unique_ptr<interfaces::Init> remote_init{protocol->connect(connect_fd, "test-connect")};
|
||||
std::unique_ptr<interfaces::Echo> remote_echo{remote_init->makeEcho()};
|
||||
BOOST_CHECK_EQUAL(remote_echo->echo("echo test"), "echo test");
|
||||
}};
|
||||
|
||||
// Need to specify explicit socket addresses outside the data directory, because the data
|
||||
// directory path is so long that the default socket address and any other
|
||||
// addresses in the data directory would fail with errors like:
|
||||
// Address 'unix' path '"/tmp/test_common_Bitcoin Core/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff/test_bitcoin.sock"' exceeded maximum socket path length
|
||||
std::vector<std::string> addresses{
|
||||
strprintf("unix:%s", TempPath("bitcoin_sock0_XXXXXX")),
|
||||
strprintf("unix:%s", TempPath("bitcoin_sock1_XXXXXX")),
|
||||
};
|
||||
|
||||
// Bind and listen on multiple addresses
|
||||
for (const auto& address : addresses) {
|
||||
bind_and_listen(address);
|
||||
}
|
||||
|
||||
// Connect and test each address multiple times.
|
||||
for (int i : {0, 1, 0, 0, 1}) {
|
||||
connect_and_test(addresses[i]);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
#include <primitives/transaction.h>
|
||||
#include <univalue.h>
|
||||
#include <util/fs.h>
|
||||
|
||||
class FooImplementation
|
||||
{
|
||||
@ -16,6 +17,8 @@ public:
|
||||
UniValue passUniValue(UniValue v) { return v; }
|
||||
};
|
||||
|
||||
void IpcTest();
|
||||
void IpcPipeTest();
|
||||
void IpcSocketPairTest();
|
||||
void IpcSocketTest(const fs::path& datadir);
|
||||
|
||||
#endif // BITCOIN_TEST_IPC_TEST_H
|
||||
|
@ -2,12 +2,41 @@
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <ipc/process.h>
|
||||
#include <test/ipc_test.h>
|
||||
|
||||
#include <test/util/setup_common.h>
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
BOOST_AUTO_TEST_SUITE(ipc_tests)
|
||||
BOOST_FIXTURE_TEST_SUITE(ipc_tests, BasicTestingSetup)
|
||||
BOOST_AUTO_TEST_CASE(ipc_tests)
|
||||
{
|
||||
IpcTest();
|
||||
IpcPipeTest();
|
||||
IpcSocketPairTest();
|
||||
IpcSocketTest(m_args.GetDataDirNet());
|
||||
}
|
||||
|
||||
// Test address parsing.
|
||||
BOOST_AUTO_TEST_CASE(parse_address_test)
|
||||
{
|
||||
std::unique_ptr<ipc::Process> process{ipc::MakeProcess()};
|
||||
fs::path datadir{"/var/empty/notexist"};
|
||||
auto check_notexist{[](const std::system_error& e) { return e.code() == std::errc::no_such_file_or_directory; }};
|
||||
auto check_address{[&](std::string address, std::string expect_address, std::string expect_error) {
|
||||
if (expect_error.empty()) {
|
||||
BOOST_CHECK_EXCEPTION(process->connect(datadir, "test_bitcoin", address), std::system_error, check_notexist);
|
||||
} else {
|
||||
BOOST_CHECK_EXCEPTION(process->connect(datadir, "test_bitcoin", address), std::invalid_argument, HasReason(expect_error));
|
||||
}
|
||||
BOOST_CHECK_EQUAL(address, expect_address);
|
||||
}};
|
||||
check_address("unix", "unix:/var/empty/notexist/test_bitcoin.sock", "");
|
||||
check_address("unix:", "unix:/var/empty/notexist/test_bitcoin.sock", "");
|
||||
check_address("unix:path.sock", "unix:/var/empty/notexist/path.sock", "");
|
||||
check_address("unix:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.sock",
|
||||
"unix:/var/empty/notexist/0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.sock",
|
||||
"Unix address path \"/var/empty/notexist/0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.sock\" exceeded maximum socket path length");
|
||||
check_address("invalid", "invalid", "Unrecognized address 'invalid'");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
Loading…
Reference in New Issue
Block a user