Skip to content

Commit f58de87

Browse files
committed
Merge bitcoin/bitcoin#32345: ipc: Handle unclean shutdowns better
2581258 ipc: Handle bitcoin-wallet disconnections (Ryan Ofsky) 2160995 ipc: Add Ctrl-C handler for spawned subprocesses (Ryan Ofsky) 0c28068 doc: Improve IPC interface comments (Ryan Ofsky) 7f65aac ipc: Avoid waiting for clients to disconnect when shutting down (Ryan Ofsky) 6eb09fd test: Add unit test coverage for Init and Shutdown code (Ryan Ofsky) 9a9fb19 ipc: Use EventLoopRef instead of addClient/removeClient (Ryan Ofsky) e886c65 Squashed '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 bitcoin-core/libmultiprocess#123, bitcoin-core/libmultiprocess#176, and bitcoin-core/libmultiprocess#182. The different fixes are described in commit messages. --- The first two commits of this PR update the libmultiprocess subtree including the following PRs: - bitcoin-core/libmultiprocess#181 - bitcoin-core/libmultiprocess#179 - bitcoin-core/libmultiprocess#160 - bitcoin-core/libmultiprocess#184 - bitcoin-core/libmultiprocess#187 - bitcoin-core/libmultiprocess#186 - bitcoin-core/libmultiprocess#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](bitcoin/bitcoin@9a9fb19) - [`6eb09fd6141f` test: Add unit test coverage for Init and Shutdown code](bitcoin/bitcoin@6eb09fd) - [`7f65aac78b95` ipc: Avoid waiting for clients to disconnect when shutting down](bitcoin/bitcoin@7f65aac) - [`0c28068ceb7b` doc: Improve IPC interface comments](bitcoin/bitcoin@0c28068) - [`216099591632` ipc: Add Ctrl-C handler for spawned subprocesses](bitcoin/bitcoin@2160995) - [`2581258ec200` ipc: Handle bitcoin-wallet disconnections](bitcoin/bitcoin@2581258) 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](bitcoin/bitcoin#28722). ACKs for top commit: Sjors: re-utACK 2581258 josibake: code review ACK bitcoin/bitcoin@2581258 pinheadmz: re-ACK 2581258 Tree-SHA512: 0095aa22d507803e2a2d46eff51fb6caf965cc0c97ccfa615bd97805d5d51e66a5b4b040640deb92896438b1fb9f6879847124c9d0e120283287bfce37b8d748
2 parents d31dc8f + 2581258 commit f58de87

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+1119
-477
lines changed

src/common/args.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,14 @@ void ArgsManager::AddHiddenArgs(const std::vector<std::string>& names)
589589
}
590590
}
591591

592+
void ArgsManager::ClearArgs()
593+
{
594+
LOCK(cs_args);
595+
m_settings = {};
596+
m_available_args.clear();
597+
m_network_only_args.clear();
598+
}
599+
592600
void ArgsManager::CheckMultipleCLIArgs() const
593601
{
594602
LOCK(cs_args);

src/common/args.h

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -359,11 +359,7 @@ class ArgsManager
359359
/**
360360
* Clear available arguments
361361
*/
362-
void ClearArgs() {
363-
LOCK(cs_args);
364-
m_available_args.clear();
365-
m_network_only_args.clear();
366-
}
362+
void ClearArgs();
367363

368364
/**
369365
* Check CLI command args

src/init.cpp

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include <interfaces/ipc.h>
3434
#include <interfaces/mining.h>
3535
#include <interfaces/node.h>
36+
#include <ipc/exception.h>
3637
#include <kernel/caches.h>
3738
#include <kernel/context.h>
3839
#include <key.h>
@@ -298,6 +299,14 @@ void Shutdown(NodeContext& node)
298299
StopREST();
299300
StopRPC();
300301
StopHTTPServer();
302+
for (auto& client : node.chain_clients) {
303+
try {
304+
client->stop();
305+
} catch (const ipc::Exception& e) {
306+
LogDebug(BCLog::IPC, "Chain client did not disconnect cleanly: %s", e.what());
307+
client.reset();
308+
}
309+
}
301310
StopMapPort();
302311

303312
// Because these depend on each-other, we make sure that neither can be
@@ -370,8 +379,11 @@ void Shutdown(NodeContext& node)
370379
}
371380
}
372381
}
373-
for (const auto& client : node.chain_clients) {
374-
client->stop();
382+
383+
// If any -ipcbind clients are still connected, disconnect them now so they
384+
// do not block shutdown.
385+
if (interfaces::Ipc* ipc = node.init->ipc()) {
386+
ipc->disconnectIncoming();
375387
}
376388

377389
#ifdef ENABLE_ZMQ

src/interfaces/ipc.h

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,20 @@ class Ipc
5959
//! true. If this is not a spawned child process, return false.
6060
virtual bool startSpawnedProcess(int argc, char* argv[], int& exit_status) = 0;
6161

62-
//! Connect to a socket address and make a client interface proxy object
63-
//! using provided callback. connectAddress returns an interface pointer if
64-
//! the connection was established, returns null if address is empty ("") or
65-
//! disabled ("0") or if a connection was refused but not required ("auto"),
66-
//! and throws an exception if there was an unexpected error.
62+
//! Connect to a socket address and return a pointer to its Init interface.
63+
//! Returns a non-null pointer if the connection was established, returns
64+
//! null if address is empty ("") or disabled ("0") or if a connection was
65+
//! refused but not required ("auto"), and throws an exception if there was
66+
//! an unexpected error.
6767
virtual std::unique_ptr<Init> connectAddress(std::string& address) = 0;
6868

69-
//! Connect to a socket address and make a client interface proxy object
70-
//! using provided callback. Throws an exception if there was an error.
69+
//! Listen on a socket address exposing this process's init interface to
70+
//! clients. Throws an exception if there was an error.
7171
virtual void listenAddress(std::string& address) = 0;
7272

73+
//! Disconnect any incoming connections that are still connected.
74+
virtual void disconnectIncoming() = 0;
75+
7376
//! Add cleanup callback to remote interface that will run when the
7477
//! interface is deleted.
7578
template<typename Interface>

src/ipc/capnp/protocol.cpp

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,7 @@ class CapnpProtocol : public Protocol
4141
public:
4242
~CapnpProtocol() noexcept(true)
4343
{
44-
if (m_loop) {
45-
std::unique_lock<std::mutex> lock(m_loop->m_mutex);
46-
m_loop->removeClient(lock);
47-
}
44+
m_loop_ref.reset();
4845
if (m_loop_thread.joinable()) m_loop_thread.join();
4946
assert(!m_loop);
5047
};
@@ -68,9 +65,20 @@ class CapnpProtocol : public Protocol
6865
m_loop.emplace(exe_name, &IpcLogFn, &m_context);
6966
if (ready_fn) ready_fn();
7067
mp::ServeStream<messages::Init>(*m_loop, fd, init);
68+
m_parent_connection = &m_loop->m_incoming_connections.back();
7169
m_loop->loop();
7270
m_loop.reset();
7371
}
72+
void disconnectIncoming() override
73+
{
74+
if (!m_loop) return;
75+
// Delete incoming connections, except the connection to a parent
76+
// process (if there is one), since a parent process should be able to
77+
// monitor and control this process, even during shutdown.
78+
m_loop->sync([&] {
79+
m_loop->m_incoming_connections.remove_if([this](mp::Connection& c) { return &c != m_parent_connection; });
80+
});
81+
}
7482
void addCleanup(std::type_index type, void* iface, std::function<void()> cleanup) override
7583
{
7684
mp::ProxyTypeRegister::types().at(type)(iface).cleanup_fns.emplace_back(std::move(cleanup));
@@ -83,10 +91,7 @@ class CapnpProtocol : public Protocol
8391
m_loop_thread = std::thread([&] {
8492
util::ThreadRename("capnp-loop");
8593
m_loop.emplace(exe_name, &IpcLogFn, &m_context);
86-
{
87-
std::unique_lock<std::mutex> lock(m_loop->m_mutex);
88-
m_loop->addClient(lock);
89-
}
94+
m_loop_ref.emplace(*m_loop);
9095
promise.set_value();
9196
m_loop->loop();
9297
m_loop.reset();
@@ -95,7 +100,14 @@ class CapnpProtocol : public Protocol
95100
}
96101
Context m_context;
97102
std::thread m_loop_thread;
103+
//! EventLoop object which manages I/O events for all connections.
98104
std::optional<mp::EventLoop> m_loop;
105+
//! Reference to the same EventLoop. Increments the loop’s refcount on
106+
//! creation, decrements on destruction. The loop thread exits when the
107+
//! refcount reaches 0. Other IPC objects also hold their own EventLoopRef.
108+
std::optional<mp::EventLoopRef> m_loop_ref;
109+
//! Connection to parent, if this is a child process spawned by a parent process.
110+
mp::Connection* m_parent_connection{nullptr};
99111
};
100112
} // namespace
101113

src/ipc/interfaces.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <tinyformat.h>
1414
#include <util/fs.h>
1515

16+
#include <csignal>
1617
#include <cstdio>
1718
#include <cstdlib>
1819
#include <cstring>
@@ -26,6 +27,28 @@
2627

2728
namespace ipc {
2829
namespace {
30+
#ifndef WIN32
31+
std::string g_ignore_ctrl_c;
32+
33+
void HandleCtrlC(int)
34+
{
35+
// (void)! needed to suppress -Wunused-result warning from GCC
36+
(void)!write(STDOUT_FILENO, g_ignore_ctrl_c.data(), g_ignore_ctrl_c.size());
37+
}
38+
#endif
39+
40+
void IgnoreCtrlC(std::string message)
41+
{
42+
#ifndef WIN32
43+
g_ignore_ctrl_c = std::move(message);
44+
struct sigaction sa{};
45+
sa.sa_handler = HandleCtrlC;
46+
sigemptyset(&sa.sa_mask);
47+
sa.sa_flags = SA_RESTART;
48+
sigaction(SIGINT, &sa, nullptr);
49+
#endif
50+
}
51+
2952
class IpcImpl : public interfaces::Ipc
3053
{
3154
public:
@@ -53,6 +76,7 @@ class IpcImpl : public interfaces::Ipc
5376
if (!m_process->checkSpawned(argc, argv, fd)) {
5477
return false;
5578
}
79+
IgnoreCtrlC(strprintf("[%s] SIGINT received — waiting for parent to shut down.\n", m_exe_name));
5680
m_protocol->serve(fd, m_exe_name, m_init);
5781
exit_status = EXIT_SUCCESS;
5882
return true;
@@ -86,6 +110,10 @@ class IpcImpl : public interfaces::Ipc
86110
int fd = m_process->bind(gArgs.GetDataDirNet(), m_exe_name, address);
87111
m_protocol->listen(fd, m_exe_name, m_init);
88112
}
113+
void disconnectIncoming() override
114+
{
115+
m_protocol->disconnectIncoming();
116+
}
89117
void addCleanup(std::type_index type, void* iface, std::function<void()> cleanup) override
90118
{
91119
m_protocol->addCleanup(type, iface, std::move(cleanup));
Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,40 @@
11
Checks: '
22
-*,
3-
bugprone-*,
4-
-bugprone-easily-swappable-parameters,
5-
-bugprone-exception-escape,
6-
-bugprone-move-forwarding-reference,
7-
-bugprone-narrowing-conversions,
8-
-bugprone-reserved-identifier,
9-
misc-*,
10-
-misc-non-private-member-variables-in-classes,
11-
-misc-no-recursion,
12-
-misc-unconventional-assign-operator,
13-
-misc-unused-parameters,
14-
-misc-use-anonymous-namespace,
15-
modernize-*,
16-
-modernize-avoid-c-arrays,
17-
-modernize-concat-nested-namespaces,
18-
-modernize-deprecated-headers,
19-
-modernize-use-nodiscard,
20-
-modernize-use-trailing-return-type,
21-
-modernize-use-using,
3+
bugprone-argument-comment,
4+
bugprone-move-forwarding-reference,
5+
bugprone-string-constructor,
6+
bugprone-use-after-move,
7+
bugprone-lambda-function-name,
8+
bugprone-unhandled-self-assignment,
9+
misc-unused-using-decls,
10+
misc-no-recursion,
11+
modernize-deprecated-headers,
12+
modernize-use-default-member-init,
13+
modernize-use-emplace,
14+
modernize-use-equals-default,
15+
modernize-use-noexcept,
16+
modernize-use-nullptr,
17+
modernize-use-starts-ends-with,
2218
performance-*,
2319
-performance-avoid-endl,
20+
-performance-enum-size,
21+
-performance-inefficient-string-concatenation,
22+
-performance-no-int-to-ptr,
2423
-performance-noexcept-move-constructor,
25-
readability-*,
26-
-readability-braces-around-statements,
27-
-readability-convert-member-functions-to-static,
28-
-readability-else-after-return,
29-
-readability-function-cognitive-complexity,
30-
-readability-identifier-length,
31-
-readability-implicit-bool-conversion,
32-
-readability-inconsistent-declaration-parameter-name,
33-
-readability-magic-numbers,
34-
-readability-named-parameter,
35-
-readability-uppercase-literal-suffix,
36-
-readability-use-anyofallof,
24+
-performance-unnecessary-value-param,
25+
readability-const-return-type,
26+
readability-redundant-declaration,
27+
readability-redundant-string-init,
28+
clang-analyzer-core.*,
29+
-clang-analyzer-core.UndefinedBinaryOperatorResult,
30+
clang-analyzer-optin.core.*,
3731
'
32+
HeaderFilterRegex: '.'
33+
WarningsAsErrors: '*'
3834
CheckOptions:
39-
- key: modernize-use-override.IgnoreDestructors
40-
value: true
41-
HeaderFilterRegex: 'example/calculator.h|example/init.h|example/printer.h|include/mp/proxy-io.h|include/mp/proxy-types.h|include/mp/proxy.h|include/mp/util.h|test/mp/test/foo-types.h|test/mp/test/foo.h'
35+
- key: modernize-deprecated-headers.CheckHeaderFile
36+
value: false
37+
- key: performance-move-const-arg.CheckTriviallyCopyableMove
38+
value: false
39+
- key: bugprone-unhandled-self-assignment.WarnOnlyIfThisHasSuspiciousField
40+
value: false
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
build:
9+
runs-on: ubuntu-latest
10+
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
config: [default, llvm, gnu32, sanitize]
15+
16+
name: build • ${{ matrix.config }}
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Install Nix
22+
uses: cachix/install-nix-action@v31 # 2025-05-27, from https://github.com/cachix/install-nix-action/tags
23+
with:
24+
nix_path: nixpkgs=channel:nixos-25.05 # latest release
25+
26+
- name: Run CI script
27+
env:
28+
CI_CONFIG: ci/configs/${{ matrix.config }}.bash
29+
run: ci/scripts/run.sh

src/ipc/libmultiprocess/CMakeLists.txt

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2019 The Bitcoin Core developers
1+
# Copyright (c) The Bitcoin Core developers
22
# Distributed under the MIT software license, see the accompanying
33
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

@@ -15,16 +15,35 @@ include("cmake/compat_find.cmake")
1515
find_package(CapnProto REQUIRED)
1616
find_package(Threads REQUIRED)
1717

18-
option(Libmultiprocess_ENABLE_CLANG_TIDY "Run clang-tidy with the compiler." OFF)
19-
if(Libmultiprocess_ENABLE_CLANG_TIDY)
18+
set(MPGEN_EXECUTABLE "" CACHE FILEPATH "If specified, should be full path to an external mpgen binary to use rather than the one built internally.")
19+
20+
option(MP_ENABLE_CLANG_TIDY "Run clang-tidy with the compiler." OFF)
21+
if(MP_ENABLE_CLANG_TIDY)
2022
find_program(CLANG_TIDY_EXECUTABLE NAMES clang-tidy)
2123
if(NOT CLANG_TIDY_EXECUTABLE)
22-
message(FATAL_ERROR "Libmultiprocess_ENABLE_CLANG_TIDY is ON but clang-tidy is not found.")
24+
message(FATAL_ERROR "MP_ENABLE_CLANG_TIDY is ON but clang-tidy is not found.")
2325
endif()
2426
set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_EXECUTABLE}")
27+
28+
# Workaround for nix from https://gitlab.kitware.com/cmake/cmake/-/issues/20912#note_793338
29+
# Nix injects header paths via $NIX_CFLAGS_COMPILE; CMake tags these as
30+
# CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES and omits them from the compile
31+
# database, so clang-tidy, which ignores $NIX_CFLAGS_COMPILE, can't find capnp
32+
# headers. Setting them as standard passes them to clang-tidy.
33+
set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES})
2534
endif()
2635

27-
set(MPGEN_EXECUTABLE "" CACHE FILEPATH "If specified, should be full path to an external mpgen binary to use rather than the one built internally.")
36+
option(MP_ENABLE_IWYU "Run include-what-you-use with the compiler." OFF)
37+
if(MP_ENABLE_IWYU)
38+
find_program(IWYU_EXECUTABLE NAMES include-what-you-use iwyu)
39+
if(NOT IWYU_EXECUTABLE)
40+
message(FATAL_ERROR "MP_ENABLE_IWYU is ON but include-what-you-use was not found.")
41+
endif()
42+
set(CMAKE_CXX_INCLUDE_WHAT_YOU_USE "${IWYU_EXECUTABLE};-Xiwyu;--error")
43+
if(DEFINED ENV{IWYU_MAPPING_FILE})
44+
list(APPEND CMAKE_CXX_INCLUDE_WHAT_YOU_USE "-Xiwyu" "--mapping_file=$ENV{IWYU_MAPPING_FILE}")
45+
endif()
46+
endif()
2847

2948
include("cmake/compat_config.cmake")
3049
include("cmake/pthread_checks.cmake")
@@ -51,6 +70,7 @@ configure_file(include/mp/config.h.in "${CMAKE_CURRENT_BINARY_DIR}/include/mp/co
5170

5271
# Generated C++ Capn'Proto schema files
5372
capnp_generate_cpp(MP_PROXY_SRCS MP_PROXY_HDRS include/mp/proxy.capnp)
73+
set_source_files_properties("${MP_PROXY_SRCS}" PROPERTIES SKIP_LINTING TRUE) # Ignored before cmake 3.27
5474

5575
# util library
5676
add_library(mputil OBJECT src/mp/util.cpp)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
### CI quick-reference
2+
3+
All CI is just bash and nix.
4+
5+
* **Workflow**:
6+
- `.github/workflows/ci.yml` – lists the jobs (`default`, `llvm`, …).
7+
* **Scripts**:
8+
- `ci/scripts/run.sh` – spins up the Nix shell then calls…
9+
- `ci/scripts/ci.sh` – …to configure, build, and test.
10+
* **Configuration**:
11+
- `ci/configs/*.sh` – defines flags for each job.
12+
- `shell.nix` – defines build environment (compilers, tools, libraries).
13+
* **Build directories**:
14+
- `build-*/` – separate build directories (like `build-default`, `build-llvm`) will be created for each job.
15+
16+
To run jobs locally:
17+
18+
```bash
19+
CI_CONFIG=ci/configs/default.bash ci/scripts/run.sh
20+
CI_CONFIG=ci/configs/llvm.bash ci/scripts/run.sh
21+
CI_CONFIG=ci/configs/gnu32.bash ci/scripts/run.sh
22+
CI_CONFIG=ci/configs/sanitize.bash ci/scripts/run.sh
23+
```
24+
25+
By default CI jobs will reuse their build directories. `CI_CLEAN=1` can be specified to delete them before running instead.

0 commit comments

Comments
 (0)