Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a841a0e
ipc, refactor: Drop connect/listen/serve exe_name parameters
ryanofsky Apr 30, 2025
ebf2277
ipc, refactor: Change Protocol class field order
ryanofsky Apr 30, 2025
eb7ca08
ipc, refactor: fix include order
ryanofsky Apr 30, 2025
973669d
ipc, refactor: Add ProcessId type alias and use it
ryanofsky Apr 30, 2025
8d7e088
ipc, refactor: Add SocketId type alias and use it
ryanofsky Apr 30, 2025
1f675e5
ipc, refactor: Add ConnectInfo type alias and use it
ryanofsky Apr 30, 2025
7993dbe
ipc, refactor: Add Stream type alias and use it
ryanofsky Apr 30, 2025
f237632
Squashed 'src/ipc/libmultiprocess/' changes from 3edbe8f67c..7fd5ec40bc
Sjors Apr 23, 2026
8db8bf0
Merge bitcoin-core/libmultiprocess#231 (Add windows support)
Sjors Apr 23, 2026
869ff7b
test: adapt sv2_tp_tester to new libmultiprocess Stream API
Sjors Apr 20, 2026
b73a195
ipc: fix Windows IPC build support
Sjors Apr 20, 2026
258c41b
guix: add Windows to default HOSTS
Sjors Apr 20, 2026
83429c5
ci: build and test on Windows via mingw cross
Sjors Apr 23, 2026
d4c7b1b
ipc: avoid Unix socket headers on Windows
Sjors Apr 23, 2026
32a132e
sv2: capture client id by value in handler thread launcher
Sjors Apr 23, 2026
6b4d3eb
sv2: add RequestInterrupt() and order Interrupt() flag-first
Sjors Apr 23, 2026
146dee1
test: implement MockMining interrupt methods
Sjors Apr 23, 2026
1452c59
test: portable IPC socketpair in sv2 TPTester
Sjors Apr 23, 2026
299b49b
test: rework TPTester teardown via explicit IPC connection
Sjors Apr 23, 2026
f6991b1
test: introduce TPTesterHandle RAII wrapper
Sjors Apr 23, 2026
a20bfd8
test: work around Windows libmultiprocess teardown hang
Sjors Apr 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,100 @@ jobs:
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#update-a-cache
key: ${{ github.job }}-${{ matrix.job-type }}-ccache-${{ github.run_id }}

windows-cross:
name: 'Linux->Windows cross, no tests'
runs-on: ubuntu-24.04
if: ${{ vars.SKIP_BRANCH_PUSH != 'true' || github.event_name == 'pull_request' }}

env:
FILE_ENV: './ci/test/00_setup_env_win64.sh'
DANGER_CI_ON_HOST_FOLDERS: 1

steps:
- name: Checkout
uses: actions/checkout@v5
with:
ref: ${{ github.event_name == 'pull_request' && github.ref || '' }}

- name: Set CI directories
run: |
echo "CCACHE_DIR=${{ runner.temp }}/ccache_dir" >> "$GITHUB_ENV"
echo "BASE_ROOT_DIR=${{ runner.temp }}" >> "$GITHUB_ENV"
echo "DEPENDS_DIR=${{ runner.temp }}/depends" >> "$GITHUB_ENV"
echo "BASE_BUILD_DIR=${{ runner.temp }}/build" >> "$GITHUB_ENV"

- name: Depends cache
uses: actions/cache@v4
with:
path: ${{ env.DEPENDS_DIR }}/built
key: ${{ github.job }}-depends-${{ hashFiles('depends/**', 'ci/test/00_setup_env_win64.sh') }}

- name: Restore Ccache cache
id: ccache-cache
uses: actions/cache/restore@v4
with:
path: ${{ env.CCACHE_DIR }}
key: ${{ github.job }}-ccache-${{ github.run_id }}
restore-keys: ${{ github.job }}-ccache-

- name: CI script
run: ./ci/test_run_all.sh

- name: Save Ccache cache
uses: actions/cache/save@v4
if: github.event_name != 'pull_request' && steps.ccache-cache.outputs.cache-hit != 'true'
with:
path: ${{ env.CCACHE_DIR }}
key: ${{ github.job }}-ccache-${{ github.run_id }}

- name: Upload built executables
uses: actions/upload-artifact@v4
with:
name: x86_64-w64-mingw32-executables-${{ github.run_id }}
path: |
${{ env.BASE_BUILD_DIR }}/bin/*.exe
${{ env.BASE_BUILD_DIR }}/test/config.ini

windows-native-test:
name: 'Windows, test cross-built'
runs-on: windows-2022
needs: windows-cross

env:
PYTHONUTF8: 1

steps:
- name: Checkout
uses: actions/checkout@v5
with:
ref: ${{ github.event_name == 'pull_request' && github.ref || '' }}

- name: Download built executables
uses: actions/download-artifact@v4
with:
name: x86_64-w64-mingw32-executables-${{ github.run_id }}

- name: Run sv2-tp.exe
run: ./bin/sv2-tp.exe -version

- name: Find mt.exe tool
shell: pwsh
run: |
$sdk_dir = (Get-ItemProperty 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows Kits\Installed Roots' -Name KitsRoot10).KitsRoot10
$sdk_latest = (Get-ChildItem "$sdk_dir\bin" -Directory | Where-Object { $_.Name -match '^\d+\.\d+\.\d+\.\d+$' } | Sort-Object Name -Descending | Select-Object -First 1).Name
"MT_EXE=${sdk_dir}bin\${sdk_latest}\x64\mt.exe" >> $env:GITHUB_ENV

- name: Validate sv2-tp manifest
shell: pwsh
run: |
& $env:MT_EXE -nologo -inputresource:bin\sv2-tp.exe -out:sv2-tp.manifest
Get-Content sv2-tp.manifest
& $env:MT_EXE -nologo -inputresource:bin\sv2-tp.exe -validate_manifest

- name: Run unit tests
run: |
./bin/test_sv2.exe -l test_suite

ci-matrix:
name: ${{ matrix.name }}
needs: runners
Expand Down
2 changes: 1 addition & 1 deletion ci/test/00_setup_env_win64.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ export CI_IMAGE_PLATFORM="linux/amd64"
export HOST=x86_64-w64-mingw32
export PACKAGES="g++-mingw-w64-x86-64-posix nsis"
export RUN_UNIT_TESTS=false
export GOAL="deploy"
export GOAL="install"
export BITCOIN_CONFIG="-DREDUCE_EXPORTS=ON \
-DCMAKE_CXX_FLAGS='-Wno-error=maybe-uninitialized'"
2 changes: 1 addition & 1 deletion cmake/module/Maintenance.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function(add_maintenance_targets)
endfunction()

function(add_windows_deploy_target)
if(MINGW AND TARGET sv2-tp)
if(MINGW AND TARGET bitcoin)
find_program(MAKENSIS_EXECUTABLE makensis)
if(NOT MAKENSIS_EXECUTABLE)
add_custom_target(deploy
Expand Down
2 changes: 1 addition & 1 deletion contrib/guix/guix-build
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ check_source_date_epoch

# Default to building for all supported HOSTs (overridable by environment)
# powerpc64le-linux-gnu currently disabled due non-determinism issues across build arches.
# x86_64-w64-mingw32 current disabled pending Windows support
export HOSTS="${HOSTS:-x86_64-linux-gnu arm-linux-gnueabihf aarch64-linux-gnu riscv64-linux-gnu powerpc64-linux-gnu
x86_64-w64-mingw32
x86_64-apple-darwin arm64-apple-darwin}"

# Usage: distsrc_for_host HOST
Expand Down
3 changes: 1 addition & 2 deletions depends/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ BASE_CACHE ?= $(BASEDIR)/built
SDK_PATH ?= $(BASEDIR)/SDKs
NO_BOOST ?=
NO_WALLET ?=
# Default NO_IPC value is 1 on Windows
NO_IPC ?= $(if $(findstring mingw32,$(HOST)),1,)
NO_IPC ?=
LTO ?=
FALLBACK_DOWNLOAD_PATH ?= https://bitcoincore.org/depends-sources

Expand Down
2 changes: 1 addition & 1 deletion depends/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ The following can be set when running make: `make FOO=bar`
- `C_STANDARD`: Set the C standard version used. Defaults to `c11`.
- `CXX_STANDARD`: Set the C++ standard version used. Defaults to `c++20`.
- `NO_BOOST`: Don't download/build/cache Boost
- `NO_IPC`: Don't build Cap’n Proto and libmultiprocess packages. Default on Windows.
- `NO_IPC`: Don't build Cap’n Proto and libmultiprocess packages.
- `DEBUG`: Disable some optimizations and enable more runtime checking
- `HOST_ID_SALT`: Optional salt to use when generating host package ids
- `BUILD_ID_SALT`: Optional salt to use when generating build package ids
Expand Down
1 change: 1 addition & 0 deletions depends/packages/capnp.mk
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ define $(package)_set_vars
$(package)_config_opts += -DWITH_OPENSSL=OFF
$(package)_config_opts += -DWITH_ZLIB=OFF
$(package)_cxxflags += -fdebug-prefix-map=$($(package)_extract_dir)=/usr -fmacro-prefix-map=$($(package)_extract_dir)=/usr
$(package)_cppflags += -D_WIN32_WINNT=0x0602
endef

define $(package)_config_cmds
Expand Down
2 changes: 1 addition & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ if(BUILD_MINE)
sv2-tp.cpp
init/basic.cpp
)
add_windows_resources(sv2-tp sv2-tp-res.rc)
add_windows_resources(sv2-tp sv2-tp.rc)
add_windows_application_manifest(sv2-tp)
target_link_libraries(sv2-tp
core_interface
Expand Down
42 changes: 27 additions & 15 deletions src/ipc/capnp/protocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@
#include <mutex>
#include <optional>
#include <string>
#include <sys/socket.h>
#include <system_error>
#include <thread>
#ifndef WIN32
#include <sys/socket.h>
#endif

namespace ipc {
namespace capnp {
Expand Down Expand Up @@ -65,36 +67,36 @@ void IpcLogFn(mp::LogMessage message)
class CapnpProtocol : public Protocol
{
public:
CapnpProtocol(const char* exe_name) : m_exe_name{exe_name} {}
~CapnpProtocol() noexcept(true)
{
m_loop_ref.reset();
if (m_loop_thread.joinable()) m_loop_thread.join();
assert(!m_loop);
};
std::unique_ptr<interfaces::Init> connect(int fd, const char* exe_name) override
std::unique_ptr<interfaces::Init> connect(mp::Stream stream) override
{
startLoop(exe_name);
return mp::ConnectStream<messages::Init>(*m_loop, fd);
startLoop();
return mp::ConnectStream<messages::Init>(*m_loop, std::move(stream));
}
void listen(int listen_fd, const char* exe_name, interfaces::Init& init) override
void listen(mp::SocketId listen_fd, interfaces::Init& init) override
{
startLoop(exe_name);
startLoop();
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
void serve(interfaces::Init& init, const std::function<mp::Stream()>& make_stream) override
{
assert(!m_loop);
mp::g_thread_context.thread_name = mp::ThreadName(exe_name);
mp::g_thread_context.thread_name = mp::ThreadName(m_exe_name);
mp::LogOptions opts = {
.log_fn = IpcLogFn,
.log_level = GetRequestedIPCLogLevel()
};
m_loop.emplace(exe_name, std::move(opts), &m_context);
if (ready_fn) ready_fn();
mp::ServeStream<messages::Init>(*m_loop, fd, init);
m_loop.emplace(m_exe_name, std::move(opts), &m_context);
mp::ServeStream<messages::Init>(*m_loop, make_stream(), init);
m_parent_connection = &m_loop->m_incoming_connections.back();
m_loop->loop();
m_loop.reset();
Expand All @@ -109,12 +111,21 @@ class CapnpProtocol : public Protocol
m_loop->m_incoming_connections.remove_if([this](mp::Connection& c) { return &c != m_parent_connection; });
});
}
mp::Stream makeStream(mp::SocketId socket) override
{
startLoop();
#if MP_MAJOR_VERSION < 12
return socket;
#else
return m_loop->m_io_context.lowLevelProvider->wrapSocketFd(socket, kj::LowLevelAsyncIoProvider::TAKE_OWNERSHIP);
#endif
}
void addCleanup(std::type_index type, void* iface, std::function<void()> cleanup) override
{
mp::ProxyTypeRegister::types().at(type)(iface).cleanup_fns.emplace_back(std::move(cleanup));
}
Context& context() override { return m_context; }
void startLoop(const char* exe_name)
void startLoop()
{
if (m_loop) return;
std::promise<void> promise;
Expand All @@ -124,16 +135,16 @@ class CapnpProtocol : public Protocol
.log_fn = IpcLogFn,
.log_level = GetRequestedIPCLogLevel()
};
m_loop.emplace(exe_name, std::move(opts), &m_context);
m_loop.emplace(m_exe_name, std::move(opts), &m_context);
m_loop_ref.emplace(*m_loop);
promise.set_value();
m_loop->loop();
m_loop.reset();
});
promise.get_future().wait();
}
const char* m_exe_name;
Context m_context;
std::thread m_loop_thread;
//! EventLoop object which manages I/O events for all connections.
std::optional<mp::EventLoop> m_loop;
//! Reference to the same EventLoop. Increments the loop’s refcount on
Expand All @@ -142,9 +153,10 @@ class CapnpProtocol : public Protocol
std::optional<mp::EventLoopRef> m_loop_ref;
//! Connection to parent, if this is a child process spawned by a parent process.
mp::Connection* m_parent_connection{nullptr};
std::thread m_loop_thread;
};
} // namespace

std::unique_ptr<Protocol> MakeCapnpProtocol() { return std::make_unique<CapnpProtocol>(); }
std::unique_ptr<Protocol> MakeCapnpProtocol(const char* exe_name) { return std::make_unique<CapnpProtocol>(exe_name); }
} // namespace capnp
} // namespace ipc
2 changes: 1 addition & 1 deletion src/ipc/capnp/protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
namespace ipc {
class Protocol;
namespace capnp {
std::unique_ptr<Protocol> MakeCapnpProtocol();
std::unique_ptr<Protocol> MakeCapnpProtocol(const char* exe_name);
} // namespace capnp
} // namespace ipc

Expand Down
21 changes: 10 additions & 11 deletions src/ipc/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,14 @@ class IpcImpl : public interfaces::Ipc
public:
IpcImpl(const char* exe_name, const char* process_argv0, interfaces::Init& init)
: m_exe_name(exe_name), m_process_argv0(process_argv0), m_init(init),
m_protocol(ipc::capnp::MakeCapnpProtocol()), m_process(ipc::MakeProcess())
m_protocol(ipc::capnp::MakeCapnpProtocol(exe_name)), m_process(ipc::MakeProcess())
{
}
std::unique_ptr<interfaces::Init> spawnProcess(const char* new_exe_name) override
{
int pid;
int fd = m_process->spawn(new_exe_name, m_process_argv0, pid);
const auto [pid, socket] = m_process->spawn(new_exe_name, m_process_argv0);
LogDebug(::BCLog::IPC, "Process %s pid %i launched\n", new_exe_name, pid);
auto init = m_protocol->connect(fd, m_exe_name);
auto init = m_protocol->connect(m_protocol->makeStream(socket));
Ipc::addCleanup(*init, [this, new_exe_name, pid] {
int status = m_process->waitSpawned(pid);
LogDebug(::BCLog::IPC, "Process %s pid %i exited with status %i\n", new_exe_name, pid, status);
Expand All @@ -72,19 +71,19 @@ class IpcImpl : public interfaces::Ipc
bool startSpawnedProcess(int argc, char* argv[], int& exit_status) override
{
exit_status = EXIT_FAILURE;
int32_t fd = -1;
if (!m_process->checkSpawned(argc, argv, fd)) {
mp::SocketId socket{mp::SocketError};
if (!m_process->checkSpawned(argc, argv, socket)) {
return false;
}
IgnoreCtrlC(strprintf("[%s] SIGINT received — waiting for parent to shut down.\n", m_exe_name));
m_protocol->serve(fd, m_exe_name, m_init);
m_protocol->serve(m_init, [&] { return m_protocol->makeStream(socket); } );
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;
mp::SocketId 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
Expand All @@ -103,12 +102,12 @@ class IpcImpl : public interfaces::Ipc
} else {
fd = m_process->connect(gArgs.GetDataDirNet(), "bitcoin-node", address);
}
return m_protocol->connect(fd, m_exe_name);
return m_protocol->connect(m_protocol->makeStream(fd));
}
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);
mp::SocketId fd = m_process->bind(gArgs.GetDataDirNet(), m_exe_name, address);
m_protocol->listen(fd, m_init);
}
void disconnectIncoming() override
{
Expand Down
4 changes: 4 additions & 0 deletions src/ipc/libmultiprocess/.github/workflows/bitcoin-core-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ concurrency:

env:
BITCOIN_REPO: bitcoin/bitcoin
# Temporary: use PR #35084 until it merges; revert to refs/heads/master after
BITCOIN_CORE_REF: refs/pull/35084/merge
LLVM_VERSION: 22
LIBCXX_DIR: /tmp/libcxx-build/

Expand Down Expand Up @@ -79,6 +81,7 @@ jobs:
uses: actions/checkout@v4
with:
repository: ${{ env.BITCOIN_REPO }}
ref: ${{ env.BITCOIN_CORE_REF }}
fetch-depth: 1

- name: Checkout libmultiprocess
Expand Down Expand Up @@ -195,6 +198,7 @@ jobs:
uses: actions/checkout@v4
with:
repository: ${{ env.BITCOIN_REPO }}
ref: ${{ env.BITCOIN_CORE_REF }}
fetch-depth: 1

- name: Checkout libmultiprocess
Expand Down
6 changes: 5 additions & 1 deletion src/ipc/libmultiprocess/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ endif()
include("cmake/compat_find.cmake")

find_package(Threads REQUIRED)
find_package(CapnProto 0.7 QUIET NO_MODULE)
find_package(CapnProto 0.9 QUIET NO_MODULE)
if(NOT CapnProto_FOUND)
message(FATAL_ERROR
"Cap'n Proto is required but was not found.\n"
Expand Down Expand Up @@ -203,6 +203,10 @@ target_link_libraries(mpgen PRIVATE CapnProto::capnp-rpc)
target_link_libraries(mpgen PRIVATE CapnProto::capnpc)
target_link_libraries(mpgen PRIVATE CapnProto::kj)
target_link_libraries(mpgen PRIVATE Threads::Threads)
target_compile_definitions(mpgen PRIVATE
"CAPNP_EXECUTABLE=\"$<TARGET_FILE:CapnProto::capnp_tool>\""
"CAPNPC_CXX_EXECUTABLE=\"$<TARGET_FILE:CapnProto::capnpc_cpp>\""
"CAPNP_INCLUDE_DIRS=\"${CAPNP_INCLUDE_DIRS}\"")
set_target_properties(mpgen PROPERTIES
INSTALL_RPATH_USE_LINK_PATH TRUE)
set_target_properties(mpgen PROPERTIES
Expand Down
2 changes: 1 addition & 1 deletion src/ipc/libmultiprocess/ci/configs/olddeps.bash
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
CI_DESC="CI job using old Cap'n Proto and cmake versions"
CI_DIR=build-olddeps
export CXXFLAGS="-Werror -Wall -Wextra -Wpedantic -Wno-unused-parameter -Wno-error=array-bounds"
NIX_ARGS=(--argstr capnprotoVersion "0.7.1" --argstr cmakeVersion "3.12.4")
NIX_ARGS=(--argstr capnprotoVersion "0.9.2" --argstr cmakeVersion "3.12.4")
BUILD_ARGS=(-k)
Loading
Loading