mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-01-19 06:43:45 +01:00
2581258ec2ipc: Handle bitcoin-wallet disconnections (Ryan Ofsky)2160995916ipc: Add Ctrl-C handler for spawned subprocesses (Ryan Ofsky)0c28068cebdoc: Improve IPC interface comments (Ryan Ofsky)7f65aac78bipc: Avoid waiting for clients to disconnect when shutting down (Ryan Ofsky)6eb09fd614test: Add unit test coverage for Init and Shutdown code (Ryan Ofsky)9a9fb19536ipc: Use EventLoopRef instead of addClient/removeClient (Ryan Ofsky)e886c65b6bSquashed 'src/ipc/libmultiprocess/' changes from 27c7e8e5a581..b4120d34bad2 (Ryan Ofsky) Pull request description: This PR fixes various problems when IPC connections are broken or hang which were reported in https://github.com/bitcoin-core/libmultiprocess/issues/123, https://github.com/bitcoin-core/libmultiprocess/issues/176, and https://github.com/bitcoin-core/libmultiprocess/pull/182. The different fixes are described in commit messages. --- The first two commits of this PR update the libmultiprocess subtree including the following PRs: - https://github.com/bitcoin-core/libmultiprocess/pull/181 - https://github.com/bitcoin-core/libmultiprocess/pull/179 - https://github.com/bitcoin-core/libmultiprocess/pull/160 - https://github.com/bitcoin-core/libmultiprocess/pull/184 - https://github.com/bitcoin-core/libmultiprocess/pull/187 - https://github.com/bitcoin-core/libmultiprocess/pull/186 - https://github.com/bitcoin-core/libmultiprocess/pull/192 The subtree changes can be verified by running `test/lint/git-subtree-check.sh src/ipc/libmultiprocess` as described in [developer notes](https://github.com/bitcoin/bitcoin/blob/master/doc/developer-notes.md#subtrees) and [lint instructions](https://github.com/bitcoin/bitcoin/tree/master/test/lint#git-subtree-checksh). The remaining commits are: - [`9a9fb19536fa` ipc: Use EventLoopRef instead of addClient/removeClient](9a9fb19536) - [`6eb09fd6141f` test: Add unit test coverage for Init and Shutdown code](6eb09fd614) - [`7f65aac78b95` ipc: Avoid waiting for clients to disconnect when shutting down](7f65aac78b) - [`0c28068ceb7b` doc: Improve IPC interface comments](0c28068ceb) - [`216099591632` ipc: Add Ctrl-C handler for spawned subprocesses](2160995916) - [`2581258ec200` ipc: Handle bitcoin-wallet disconnections](2581258ec2) The new commits depend on the subtree update, and because the subtree update includes an incompatible API change, the "Use EventLoopRef" commit needs to be part of the same PR to avoid breaking the build. The other commits also make sense to merge at the same time because the bitcoin & libmultiprocess changes were written and tested together. --- This PR is part of the [process separation project](https://github.com/bitcoin/bitcoin/issues/28722). ACKs for top commit: Sjors: re-utACK2581258ec2josibake: code review ACK2581258ec2pinheadmz: re-ACK2581258ec2Tree-SHA512: 0095aa22d507803e2a2d46eff51fb6caf965cc0c97ccfa615bd97805d5d51e66a5b4b040640deb92896438b1fb9f6879847124c9d0e120283287bfce37b8d748
180 lines
7.3 KiB
C++
180 lines
7.3 KiB
C++
// Copyright (c) 2023 The Bitcoin Core developers
|
|
// 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 <validation.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
|
|
//! versa.
|
|
//!
|
|
//! 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 IpcPipeTest()
|
|
{
|
|
// Setup: create FooImplementation object and listen for FooInterface requests
|
|
std::promise<std::unique_ptr<mp::ProxyClient<gen::FooInterface>>> foo_promise;
|
|
std::thread thread([&]() {
|
|
mp::EventLoop loop("IpcPipeTest", [](bool raise, const std::string& log) { LogInfo("LOG%i: %s", raise, log); });
|
|
auto pipe = loop.m_io_context.provider->newTwoWayPipe();
|
|
|
|
auto connection_client = std::make_unique<mp::Connection>(loop, kj::mv(pipe.ends[0]));
|
|
auto foo_client = std::make_unique<mp::ProxyClient<gen::FooInterface>>(
|
|
connection_client->m_rpc_system->bootstrap(mp::ServerVatId().vat_id).castAs<gen::FooInterface>(),
|
|
connection_client.get(), /* destroy_connection= */ true);
|
|
connection_client.release();
|
|
foo_promise.set_value(std::move(foo_client));
|
|
|
|
auto connection_server = std::make_unique<mp::Connection>(loop, kj::mv(pipe.ends[1]), [&](mp::Connection& connection) {
|
|
auto foo_server = kj::heap<mp::ProxyServer<gen::FooInterface>>(std::make_shared<FooImplementation>(), connection);
|
|
return capnp::Capability::Client(kj::mv(foo_server));
|
|
});
|
|
connection_server->onDisconnect([&] { connection_server.reset(); });
|
|
loop.loop();
|
|
});
|
|
std::unique_ptr<mp::ProxyClient<gen::FooInterface>> foo{foo_promise.get_future().get()};
|
|
|
|
// Test: make sure arguments were sent and return value is received
|
|
BOOST_CHECK_EQUAL(foo->add(1, 2), 3);
|
|
|
|
COutPoint txout1{Txid::FromUint256(uint256{100}), 200};
|
|
COutPoint txout2{foo->passOutPoint(txout1)};
|
|
BOOST_CHECK(txout1 == txout2);
|
|
|
|
UniValue uni1{UniValue::VOBJ};
|
|
uni1.pushKV("i", 1);
|
|
uni1.pushKV("s", "two");
|
|
UniValue uni2{foo->passUniValue(uni1)};
|
|
BOOST_CHECK_EQUAL(uni1.write(), uni2.write());
|
|
|
|
CMutableTransaction mtx;
|
|
mtx.version = 2;
|
|
mtx.nLockTime = 3;
|
|
mtx.vin.emplace_back(txout1);
|
|
mtx.vout.emplace_back(COIN, CScript());
|
|
CTransactionRef tx1{MakeTransactionRef(mtx)};
|
|
CTransactionRef tx2{foo->passTransaction(tx1)};
|
|
BOOST_CHECK(*Assert(tx1) == *Assert(tx2));
|
|
|
|
std::vector<char> vec1{'H', 'e', 'l', 'l', 'o'};
|
|
std::vector<char> vec2{foo->passVectorChar(vec1)};
|
|
BOOST_CHECK_EQUAL(std::string_view(vec1.begin(), vec1.end()), std::string_view(vec2.begin(), vec2.end()));
|
|
|
|
auto script1{CScript() << OP_11};
|
|
auto script2{foo->passScript(script1)};
|
|
BOOST_CHECK_EQUAL(HexStr(script1), HexStr(script2));
|
|
|
|
// Test cleanup: disconnect and join thread
|
|
foo.reset();
|
|
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]);
|
|
}
|
|
}
|