Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b15d63e
doc: Bump version 11 > 12
ryanofsky Apr 15, 2026
36c91a0
util, refactor: Add ProcessId type alias and use it
ryanofsky Apr 30, 2025
94af41b
util, refactor: Add SocketId type alias and use it
ryanofsky Apr 30, 2025
beaa50a
util, refactor: Add ConnectInfo type alias and use it
ryanofsky Apr 30, 2025
b16f8c4
util, refactor: Handle forking inside ExecProcess
ryanofsky Apr 17, 2026
022b29b
util, refactor: Add SocketPair() and use it in SpawnProcess
ryanofsky Apr 30, 2025
24c5e57
util: Clear FD_CLOEXEC on child socket before exec
ryanofsky Apr 30, 2025
3c81cf2
proxy, refactor: Replace EventLoop wakeup fd integers with KJ stream …
ryanofsky Apr 30, 2025
17a1952
cmake: Bump minimum required Cap'n Proto version to 0.9
ryanofsky Apr 16, 2026
091f5e1
proxy, refactor: Change ConnectStream and ServeStream to accept strea…
ryanofsky Apr 30, 2025
bfc2db7
proxy: Call shutdownWrite() in Connection destructor
ryanofsky Apr 30, 2025
1060a95
util, refactor: Fix PtrOrValue constructor for move-only types on MSVC
ryanofsky Apr 17, 2026
362d416
proxy, refactor: Fix C4305 truncation warning in Accessor on MSVC
ryanofsky Apr 22, 2026
3fd227c
type-interface, refactor: Fix typename decltype() SFINAE in CustomBui…
ryanofsky Apr 22, 2026
926ae35
ci: Check out bitcoin/bitcoin PR #35084 instead of master
ryanofsky Apr 16, 2026
28e4c7f
proxy: Fix shutdownWrite() exception handling on macOS with dynamic l…
ryanofsky Apr 17, 2026
f6aa627
ipc: Wrap mpgen main() in try-catch to print errors
ryanofsky Apr 20, 2026
7f513a4
doc: Remove trailing whitespace
ryanofsky Apr 17, 2026
c9aa806
cmake: Replace capnp_PREFIX path construction with cmake-provided sym…
ryanofsky Apr 21, 2026
7cb83a5
cmake: Fix CapnProto tool paths broken by Ubuntu Noble packaging bug
ryanofsky Apr 22, 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
6 changes: 5 additions & 1 deletion doc/versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ Library versions are tracked with simple
Versioning policy is described in the [version.h](../include/mp/version.h)
include.

## v11
## v12
- Current unstable version.
- Adds support for nonunix platforms, making API changes that are not backwards compatible.

## [v11.0](https://github.com/bitcoin-core/libmultiprocess/commits/v11.0)
- Improves debug output if EventLoop::post callback fails.

## [v10.0](https://github.com/bitcoin-core/libmultiprocess/commits/v10.0)
- Increases spawn test timeout to avoid spurious failures.
Expand Down
13 changes: 4 additions & 9 deletions example/calculator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
#include <init.capnp.h>
#include <init.capnp.proxy.h> // NOLINT(misc-include-cleaner) // IWYU pragma: keep

#include <charconv>
#include <cstring>
#include <cstring> // IWYU pragma: keep
#include <fstream>
#include <functional>
#include <iostream>
Expand All @@ -16,9 +15,9 @@
#include <kj/memory.h>
#include <memory>
#include <mp/proxy-io.h>
#include <mp/util.h>
#include <stdexcept>
#include <string>
#include <system_error>
#include <utility>

class CalculatorImpl : public Calculator
Expand Down Expand Up @@ -51,14 +50,10 @@ int main(int argc, char** argv)
std::cout << "Usage: mpcalculator <fd>\n";
return 1;
}
int fd;
if (std::from_chars(argv[1], argv[1] + strlen(argv[1]), fd).ec != std::errc{}) {
std::cerr << argv[1] << " is not a number or is larger than an int\n";
return 1;
}
mp::SocketId socket{mp::StartSpawned(argv[1])};
mp::EventLoop loop("mpcalculator", LogPrint);
std::unique_ptr<Init> init = std::make_unique<InitImpl>();
mp::ServeStream<InitInterface>(loop, fd, *init);
mp::ServeStream<InitInterface>(loop, socket, *init);
loop.loop();
return 0;
}
8 changes: 4 additions & 4 deletions example/example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,20 @@
#include <string>
#include <thread>
#include <tuple>
#include <utility>
#include <vector>

namespace fs = std::filesystem;

static auto Spawn(mp::EventLoop& loop, const std::string& process_argv0, const std::string& new_exe_name)
{
int pid;
const int fd = mp::SpawnProcess(pid, [&](int fd) -> std::vector<std::string> {
const auto [pid, socket] = mp::SpawnProcess([&](mp::ConnectInfo info) -> std::vector<std::string> {
fs::path path = process_argv0;
path.remove_filename();
path.append(new_exe_name);
return {path.string(), std::to_string(fd)};
return {path.string(), std::move(info)};
});
return std::make_tuple(mp::ConnectStream<InitInterface>(loop, fd), pid);
return std::make_tuple(mp::ConnectStream<InitInterface>(loop, socket), pid);
}

static void LogPrint(mp::LogMessage log_data)
Expand Down
13 changes: 4 additions & 9 deletions example/printer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,17 @@
#include <init.capnp.h>
#include <init.capnp.proxy.h> // NOLINT(misc-include-cleaner) // IWYU pragma: keep

#include <charconv>
#include <cstring>
#include <cstring> // IWYU pragma: keep
#include <fstream>
#include <iostream>
#include <kj/async.h>
#include <kj/common.h>
#include <kj/memory.h>
#include <memory>
#include <mp/proxy-io.h>
#include <mp/util.h>
#include <stdexcept>
#include <string>
#include <system_error>

class PrinterImpl : public Printer
{
Expand All @@ -44,14 +43,10 @@ int main(int argc, char** argv)
std::cout << "Usage: mpprinter <fd>\n";
return 1;
}
int fd;
if (std::from_chars(argv[1], argv[1] + strlen(argv[1]), fd).ec != std::errc{}) {
std::cerr << argv[1] << " is not a number or is larger than an int\n";
return 1;
}
mp::SocketId socket{mp::StartSpawned(argv[1])};
mp::EventLoop loop("mpprinter", LogPrint);
std::unique_ptr<Init> init = std::make_unique<InitImpl>();
mp::ServeStream<InitInterface>(loop, fd, *init);
mp::ServeStream<InitInterface>(loop, socket, *init);
loop.loop();
return 0;
}
27 changes: 18 additions & 9 deletions include/mp/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -249,25 +249,34 @@ std::string ThreadName(const char* exe_name);
//! errors in python unit tests.
std::string LogEscape(const kj::StringTree& string, size_t max_size);

using ProcessId = int;
using SocketId = int;
constexpr SocketId SocketError{-1};

//! Information about parent process passed to child process as a command-line
//! argument. On unix this is the child socket fd number formatted as a string.
using ConnectInfo = std::string;

//! Callback type used by SpawnProcess below.
using FdToArgsFn = std::function<std::vector<std::string>(int fd)>;
using ConnectInfoToArgsFn = std::function<std::vector<std::string>(const ConnectInfo&)>;

//! Spawn a new process that communicates with the current process over a socket
//! pair. Returns pid through an output argument, and file descriptor for the
//! local side of the socket.
//! The fd_to_args callback is invoked in the parent process before fork().
//! It must not rely on child pid/state, and must return the command line
//! arguments that should be used to execute the process. Embed the remote file
//! descriptor number in whatever format the child process expects.
int SpawnProcess(int& pid, FdToArgsFn&& fd_to_args);
//! pair. Calls connect_info_to_args callback with a connection string that
//! needs to be passed to the child process, and executes the argv command line
//! it returns. Returns child process id and socket id.
std::tuple<ProcessId, SocketId> SpawnProcess(ConnectInfoToArgsFn&& connect_info_to_args);

//! Initialize spawned child process using the ConnectInfo string passed to it,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StartSpawned reads as imperative, but the body just parses an int out of the connect-info string (and the header comment seems to describe more work than the function actually does). Would something like ParseConnectInfo fit better?

I might be missing the full picture here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I was missing the full picture. Just looked PR 231

//! returning a socket id for communicating with the parent process.
SocketId StartSpawned(const ConnectInfo& connect_info);

//! Call execvp with vector args.
//! Not safe to call in a post-fork child of a multi-threaded process.
//! Currently only used by mpgen at build time.
void ExecProcess(const std::vector<std::string>& args);

//! Wait for a process to exit and return its exit code.
int WaitProcess(int pid);
int WaitProcess(ProcessId pid);

inline char* CharCast(char* c) { return c; }
inline char* CharCast(unsigned char* c) { return (char*)c; }
Expand Down
2 changes: 1 addition & 1 deletion include/mp/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
//! pointing at the prior merge commit. The /doc/versions.md file should also be
//! updated, noting any significant or incompatible changes made since the
//! previous version.
#define MP_MAJOR_VERSION 11
#define MP_MAJOR_VERSION 12

//! Minor version number. Should be incremented in stable branches after
//! backporting changes. The /doc/versions.md file should also be updated to
Expand Down
2 changes: 1 addition & 1 deletion src/mp/proxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ EventLoop::EventLoop(const char* exe_name, LogOptions log_opts, void* context)
m_log_opts(std::move(log_opts)),
m_context(context)
{
int fds[2];
SocketId fds[2];
KJ_SYSCALL(socketpair(AF_UNIX, SOCK_STREAM, 0, fds));
m_wait_fd = fds[0];
m_post_fd = fds[1];
Expand Down
17 changes: 11 additions & 6 deletions src/mp/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,9 @@ std::string LogEscape(const kj::StringTree& string, size_t max_size)
return result;
}

int SpawnProcess(int& pid, FdToArgsFn&& fd_to_args)
std::tuple<ProcessId, SocketId> SpawnProcess(ConnectInfoToArgsFn&& connect_info_to_args)
{
int fds[2];
SocketId fds[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0) {
throw std::system_error(errno, std::system_category(), "socketpair");
}
Expand All @@ -129,10 +129,10 @@ int SpawnProcess(int& pid, FdToArgsFn&& fd_to_args)
// locks at fork time. In that case, running code that allocates memory or
// takes locks in the child between fork() and exec() can deadlock
// indefinitely. Precomputing arguments in the parent avoids this.
const std::vector<std::string> args{fd_to_args(fds[0])};
const std::vector<std::string> args{connect_info_to_args(std::to_string(fds[0]))};
const std::vector<char*> argv{MakeArgv(args)};

pid = fork();
ProcessId pid = fork();
if (pid == -1) {
throw std::system_error(errno, std::system_category(), "fork");
}
Expand Down Expand Up @@ -168,7 +168,12 @@ int SpawnProcess(int& pid, FdToArgsFn&& fd_to_args)
perror("execvp failed");
_exit(127);
}
return fds[1];
return {pid, fds[1]};
}

SocketId StartSpawned(const ConnectInfo& connect_info)
{
return std::stoi(connect_info);
}

void ExecProcess(const std::vector<std::string>& args)
Expand All @@ -183,7 +188,7 @@ void ExecProcess(const std::vector<std::string>& args)
}
}

int WaitProcess(int pid)
int WaitProcess(ProcessId pid)
{
int status;
if (::waitpid(pid, &status, /*options=*/0) != pid) {
Expand Down
15 changes: 10 additions & 5 deletions test/mp/test/spawn_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,21 @@
#include <string>
#include <sys/wait.h>
#include <thread>
#include <tuple>
#include <unistd.h>
#include <utility>
#include <vector>

namespace mp {
namespace test {
namespace {

constexpr auto FAILURE_TIMEOUT = std::chrono::seconds{30};

// Poll for child process exit using waitpid(..., WNOHANG) until the child exits
// or timeout expires. Returns true if the child exited and status_out was set.
// Returns false on timeout or error.
static bool WaitPidWithTimeout(int pid, std::chrono::milliseconds timeout, int& status_out)
static bool WaitPidWithTimeout(ProcessId pid, std::chrono::milliseconds timeout, int& status_out)
{
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (std::chrono::steady_clock::now() < deadline) {
Expand Down Expand Up @@ -86,14 +90,13 @@ KJ_TEST("SpawnProcess does not run callback in child")
control_cv.notify_one();
});

int pid{-1};
const int fd{mp::SpawnProcess(pid, [&](int child_fd) -> std::vector<std::string> {
const auto [pid, socket]{SpawnProcess([&](ConnectInfo connect_info) -> std::vector<std::string> {
// If this callback runs in the post-fork child, target_mutex appears
// locked forever (the owning thread does not exist), so this deadlocks.
std::lock_guard<std::mutex> g(target_mutex);
return {"true", std::to_string(child_fd)};
return {"true", std::move(connect_info)};
})};
::close(fd);
::close(socket);

int status{0};
// Give the child some time to exit. If it does not, terminate it and
Expand All @@ -110,3 +113,5 @@ KJ_TEST("SpawnProcess does not run callback in child")
KJ_EXPECT(exited, "Timeout waiting for child process to exit");
KJ_EXPECT(WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
} // namespace test
} // namespace mp