diff --git a/depends/packages/native_libmultiprocess.mk b/depends/packages/native_libmultiprocess.mk index 2e30be434c..185ef9489e 100644 --- a/depends/packages/native_libmultiprocess.mk +++ b/depends/packages/native_libmultiprocess.mk @@ -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 diff --git a/src/bitcoind.cpp b/src/bitcoind.cpp index a09bb5c9da..1e27924dfd 100644 --- a/src/bitcoind.cpp +++ b/src/bitcoind.cpp @@ -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; diff --git a/src/common/args.cpp b/src/common/args.cpp index 9b3ad38296..f59d2b8f0f 100644 --- a/src/common/args.cpp +++ b/src/common/args.cpp @@ -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; diff --git a/src/common/args.h b/src/common/args.h index 3f4cbbfab4..8d9daf5f65 100644 --- a/src/common/args.h +++ b/src/common/args.h @@ -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 }; diff --git a/src/init.cpp b/src/init.cpp index e60feecf10..3123266e4b 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -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=", 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=
", "Bind to Unix socket address and listen for incoming connections. Valid address values are \"unix\" to listen on the default path, /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. */ diff --git a/src/init.h b/src/init.h index 40a5da3c0b..6d8a35d80e 100644 --- a/src/init.h +++ b/src/init.h @@ -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); diff --git a/src/init/bitcoin-gui.cpp b/src/init/bitcoin-gui.cpp index aceff1e40f..eae30bc995 100644 --- a/src/init/bitcoin-gui.cpp +++ b/src/init/bitcoin-gui.cpp @@ -34,6 +34,11 @@ public: } std::unique_ptr 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 m_ipc; }; diff --git a/src/init/bitcoin-node.cpp b/src/init/bitcoin-node.cpp index 00a3822791..3f8c50b8d6 100644 --- a/src/init/bitcoin-node.cpp +++ b/src/init/bitcoin-node.cpp @@ -37,6 +37,7 @@ public: } std::unique_ptr 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 m_ipc; }; diff --git a/src/interfaces/init.h b/src/interfaces/init.h index 094ead399d..b496ada05f 100644 --- a/src/interfaces/init.h +++ b/src/interfaces/init.h @@ -37,6 +37,7 @@ public: virtual std::unique_ptr makeWalletLoader(Chain& chain) { return nullptr; } virtual std::unique_ptr 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 diff --git a/src/interfaces/ipc.h b/src/interfaces/ipc.h index 963649fc9a..fb340552c5 100644 --- a/src/interfaces/ipc.h +++ b/src/interfaces/ipc.h @@ -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 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 diff --git a/src/ipc/capnp/protocol.cpp b/src/ipc/capnp/protocol.cpp index 73276d6d90..4b67a5bd1e 100644 --- a/src/ipc/capnp/protocol.cpp +++ b/src/ipc/capnp/protocol.cpp @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include namespace ipc { @@ -51,11 +53,20 @@ public: startLoop(exe_name); return mp::ConnectStream(*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(*m_loop, listen_fd, init); + } + void serve(int fd, const char* exe_name, interfaces::Init& init, const std::function& 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(*m_loop, fd, init); m_loop->loop(); m_loop.reset(); diff --git a/src/ipc/interfaces.cpp b/src/ipc/interfaces.cpp index b409443f64..33555f05d4 100644 --- a/src/ipc/interfaces.cpp +++ b/src/ipc/interfaces.cpp @@ -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 #include #include #include @@ -56,6 +57,35 @@ public: exit_status = EXIT_SUCCESS; return true; } + std::unique_ptr 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 cleanup) override { m_protocol->addCleanup(type, iface, std::move(cleanup)); diff --git a/src/ipc/process.cpp b/src/ipc/process.cpp index 9657dcd092..432c365d8f 100644 --- a/src/ipc/process.cpp +++ b/src/ipc/process.cpp @@ -4,22 +4,28 @@ #include #include +#include #include #include #include #include +#include #include #include +#include #include #include #include #include -#include +#include +#include #include #include #include +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 MakeProcess() { return std::make_unique(); } diff --git a/src/ipc/process.h b/src/ipc/process.h index 40f2d2acf6..2ed8b73fab 100644 --- a/src/ipc/process.h +++ b/src/ipc/process.h @@ -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 diff --git a/src/ipc/protocol.h b/src/ipc/protocol.h index 4cd892e411..b2ebf99e8c 100644 --- a/src/ipc/protocol.h +++ b/src/ipc/protocol.h @@ -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 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 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& ready_fn = {}) = 0; //! Add cleanup callback to interface that will run when the interface is //! deleted. diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp index 7a0979056e..ec415f0bac 100644 --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -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)) { diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 4e24b87e9b..92b39f0497 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -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) diff --git a/src/test/fuzz/system.cpp b/src/test/fuzz/system.cpp index 3a404c0b48..2ab5b7ed39 100644 --- a/src/test/fuzz/system.cpp +++ b/src/test/fuzz/system.cpp @@ -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::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::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)); diff --git a/src/test/ipc_test.cpp b/src/test/ipc_test.cpp index ce4edaceb0..e6de6e3e47 100644 --- a/src/test/ipc_test.cpp +++ b/src/test/ipc_test.cpp @@ -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 +#include +#include +#include #include #include #include #include #include +#include #include +#include #include #include #include +#include #include +//! Remote init class. +class TestInit : public interfaces::Init +{ +public: + std::unique_ptr 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>> foo_promise; std::function 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(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 init{std::make_unique()}; + std::unique_ptr protocol{ipc::capnp::MakeCapnpProtocol()}; + std::promise promise; + std::thread thread([&]() { + protocol->serve(fds[0], "test-serve", *init, [&] { promise.set_value(); }); + }); + promise.get_future().wait(); + std::unique_ptr remote_init{protocol->connect(fds[1], "test-connect")}; + std::unique_ptr 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 init{std::make_unique()}; + std::unique_ptr protocol{ipc::capnp::MakeCapnpProtocol()}; + std::unique_ptr 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 remote_init{protocol->connect(connect_fd, "test-connect")}; + std::unique_ptr 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 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]); + } +} diff --git a/src/test/ipc_test.h b/src/test/ipc_test.h index bcfcc2125c..2453bfa23c 100644 --- a/src/test/ipc_test.h +++ b/src/test/ipc_test.h @@ -7,6 +7,7 @@ #include #include +#include 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 diff --git a/src/test/ipc_tests.cpp b/src/test/ipc_tests.cpp index 6e144b0f41..35a4f61117 100644 --- a/src/test/ipc_tests.cpp +++ b/src/test/ipc_tests.cpp @@ -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 #include + +#include #include -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 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()