Skip to content

Commit 7d2f3c4

Browse files
committed
Add windows support
Add support for running on windows. These changes make the libmultiprocess API more generic, using stream types instead of file descriptors. All features are supported, including spawning processes with socket connections to the parent process. These changes were originally made in bitcoin/bitcoin#32387
1 parent a4f9296 commit 7d2f3c4

7 files changed

Lines changed: 286 additions & 71 deletions

File tree

example/calculator.cpp

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,11 @@ int main(int argc, char** argv)
5151
std::cout << "Usage: mpcalculator <fd>\n";
5252
return 1;
5353
}
54-
int fd;
55-
if (std::from_chars(argv[1], argv[1] + strlen(argv[1]), fd).ec != std::errc{}) {
56-
std::cerr << argv[1] << " is not a number or is larger than an int\n";
57-
return 1;
58-
}
54+
mp::SocketId socket{mp::StartSpawned(argv[1])};
5955
mp::EventLoop loop("mpcalculator", LogPrint);
6056
std::unique_ptr<Init> init = std::make_unique<InitImpl>();
61-
mp::ServeStream<InitInterface>(loop, fd, *init);
57+
mp::Stream stream{loop.m_io_context.lowLevelProvider->wrapSocketFd(socket, kj::LowLevelAsyncIoProvider::TAKE_OWNERSHIP)};
58+
mp::ServeStream<InitInterface>(loop, kj::mv(stream), *init);
6259
loop.loop();
6360
return 0;
6461
}

example/example.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ namespace fs = std::filesystem;
2525

2626
static auto Spawn(mp::EventLoop& loop, const std::string& process_argv0, const std::string& new_exe_name)
2727
{
28-
int pid;
29-
const int fd = mp::SpawnProcess(pid, [&](int fd) -> std::vector<std::string> {
28+
auto pair{mp::SocketPair()};
29+
mp::ProcessId pid{mp::SpawnProcess(pair[0], [&](mp::ConnectInfo info) -> std::vector<std::string> {
3030
fs::path path = process_argv0;
3131
path.remove_filename();
3232
path.append(new_exe_name);
33-
return {path.string(), std::to_string(fd)};
34-
});
35-
return std::make_tuple(mp::ConnectStream<InitInterface>(loop, fd), pid);
33+
return {path.string(), std::move(info)};
34+
})};
35+
return std::make_tuple(mp::ConnectStream<InitInterface>(loop, loop.m_io_context.lowLevelProvider->wrapSocketFd(pair[1])), pid);
3636
}
3737

3838
static void LogPrint(mp::LogMessage log_data)

example/printer.cpp

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,11 @@ int main(int argc, char** argv)
4444
std::cout << "Usage: mpprinter <fd>\n";
4545
return 1;
4646
}
47-
int fd;
48-
if (std::from_chars(argv[1], argv[1] + strlen(argv[1]), fd).ec != std::errc{}) {
49-
std::cerr << argv[1] << " is not a number or is larger than an int\n";
50-
return 1;
51-
}
47+
mp::SocketId socket{mp::StartSpawned(argv[1])};
5248
mp::EventLoop loop("mpprinter", LogPrint);
5349
std::unique_ptr<Init> init = std::make_unique<InitImpl>();
54-
mp::ServeStream<InitInterface>(loop, fd, *init);
50+
mp::Stream stream{loop.m_io_context.lowLevelProvider->wrapSocketFd(socket, kj::LowLevelAsyncIoProvider::TAKE_OWNERSHIP)};
51+
mp::ServeStream<InitInterface>(loop, std::move(stream), *init);
5552
loop.loop();
5653
return 0;
5754
}

include/mp/proxy-io.h

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,17 @@ class Logger
185185

186186
std::string LongThreadName(const char* exe_name);
187187

188+
using Stream = kj::Own<kj::AsyncIoStream>;
189+
190+
inline SocketId StreamSocketId(const Stream& stream)
191+
{
192+
if (stream) KJ_IF_MAYBE(fd, stream->getFd()) return *fd;
193+
#ifdef WIN32
194+
if (stream) KJ_IF_MAYBE(handle, stream->getWin32Handle()) return reinterpret_cast<SocketId>(*handle);
195+
#endif
196+
throw std::logic_error("Stream socket unset");
197+
}
198+
188199
//! Event loop implementation.
189200
//!
190201
//! Cap'n Proto threading model is very simple: all I/O operations are
@@ -283,11 +294,12 @@ class EventLoop
283294
//! Callback functions to run on async thread.
284295
std::optional<CleanupList> m_async_fns MP_GUARDED_BY(m_mutex);
285296

286-
//! Pipe read handle used to wake up the event loop thread.
287-
int m_wait_fd = -1;
297+
//! Socket pair used to post and wait for wakeups to the event loop thread.
298+
kj::Own<kj::AsyncIoStream> m_wait_stream;
299+
kj::Own<kj::AsyncIoStream> m_post_stream;
288300

289-
//! Pipe write handle used to wake up the event loop thread.
290-
int m_post_fd = -1;
301+
//! Synchronous writer used to write to m_post_stream.
302+
kj::Own<kj::OutputStream> m_post_writer;
291303

292304
//! Number of clients holding references to ProxyServerBase objects that
293305
//! reference this event loop.
@@ -679,13 +691,11 @@ struct ThreadContext
679691
//! over the stream. Also create a new Connection object embedded in the
680692
//! client that is freed when the client is closed.
681693
template <typename InitInterface>
682-
std::unique_ptr<ProxyClient<InitInterface>> ConnectStream(EventLoop& loop, int fd)
694+
std::unique_ptr<ProxyClient<InitInterface>> ConnectStream(EventLoop& loop, kj::Own<kj::AsyncIoStream> stream)
683695
{
684696
typename InitInterface::Client init_client(nullptr);
685697
std::unique_ptr<Connection> connection;
686698
loop.sync([&] {
687-
auto stream =
688-
loop.m_io_context.lowLevelProvider->wrapSocketFd(fd, kj::LowLevelAsyncIoProvider::TAKE_OWNERSHIP);
689699
connection = std::make_unique<Connection>(loop, kj::mv(stream));
690700
init_client = connection->m_rpc_system->bootstrap(ServerVatId().vat_id).castAs<InitInterface>();
691701
Connection* connection_ptr = connection.get();
@@ -735,10 +745,9 @@ void _Listen(EventLoop& loop, kj::Own<kj::ConnectionReceiver>&& listener, InitIm
735745
//! Given stream file descriptor and an init object, handle requests on the
736746
//! stream by calling methods on the Init object.
737747
template <typename InitInterface, typename InitImpl>
738-
void ServeStream(EventLoop& loop, int fd, InitImpl& init)
748+
void ServeStream(EventLoop& loop, kj::Own<kj::AsyncIoStream> stream, InitImpl& init)
739749
{
740-
_Serve<InitInterface>(
741-
loop, loop.m_io_context.lowLevelProvider->wrapSocketFd(fd, kj::LowLevelAsyncIoProvider::TAKE_OWNERSHIP), init);
750+
_Serve<InitInterface>(loop, kj::mv(stream), init);
742751
}
743752

744753
//! Given listening socket file descriptor and an init object, handle incoming

include/mp/util.h

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
#include <variant>
2020
#include <vector>
2121

22+
#ifdef WIN32
23+
#include <winsock2.h>
24+
#endif
25+
2226
namespace mp {
2327

2428
//! Generic utility functions used by capnp code.
@@ -216,22 +220,44 @@ std::string ThreadName(const char* exe_name);
216220
//! errors in python unit tests.
217221
std::string LogEscape(const kj::StringTree& string, size_t max_size);
218222

223+
#ifdef WIN32
224+
using ProcessId = uintptr_t;
225+
using SocketId = uintptr_t;
226+
constexpr SocketId SocketError{INVALID_SOCKET};
227+
#else
228+
using ProcessId = int;
229+
using SocketId = int;
230+
constexpr SocketId SocketError{-1};
231+
#endif
232+
233+
//! Information about parent process passed to child process. On unix this is
234+
//! just the inherited int file descriptor formatted as a string. On windows,
235+
//! this is a path to a named path pipe the parent process will write
236+
//! WSADuplicateSocket info to.
237+
using ConnectInfo = std::string;
238+
219239
//! Callback type used by SpawnProcess below.
220-
using FdToArgsFn = std::function<std::vector<std::string>(int fd)>;
240+
using ConnectInfoToArgsFn = std::function<std::vector<std::string>(const ConnectInfo&)>;
241+
242+
//! Create a socket pair that can be used to communicate within a process or
243+
//! between parent and child processes.
244+
std::array<SocketId, 2> SocketPair();
245+
246+
//! Spawn a new process that communicates with the current process over provided
247+
//! socket argument. Calls connect_info_to_args callback with a connection
248+
//! string that needs to be passed to the child process, and executes the
249+
//! argv command line it returns. Returns child process id.
250+
ProcessId SpawnProcess(SocketId socket, ConnectInfoToArgsFn&& connect_info_to_args);
221251

222-
//! Spawn a new process that communicates with the current process over a socket
223-
//! pair. Returns pid through an output argument, and file descriptor for the
224-
//! local side of the socket. Invokes fd_to_args callback with the remote file
225-
//! descriptor number which returns the command line arguments that should be
226-
//! used to execute the process, and which should have the remote file
227-
//! descriptor embedded in whatever format the child process expects.
228-
int SpawnProcess(int& pid, FdToArgsFn&& fd_to_args);
252+
//! Initialize spawned child process using the ConnectInfo string passed to it,
253+
//! returning a socket id for communicating with the parent process.
254+
SocketId StartSpawned(const ConnectInfo& connect_info);
229255

230256
//! Call execvp with vector args.
231257
void ExecProcess(const std::vector<std::string>& args);
232258

233259
//! Wait for a process to exit and return its exit code.
234-
int WaitProcess(int pid);
260+
int WaitProcess(ProcessId pid);
235261

236262
inline char* CharCast(char* c) { return c; }
237263
inline char* CharCast(unsigned char* c) { return (char*)c; }

src/mp/proxy.cpp

Lines changed: 73 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,15 @@
3030
#include <optional>
3131
#include <stdexcept>
3232
#include <string>
33-
#include <sys/socket.h>
3433
#include <thread>
3534
#include <tuple>
36-
#include <unistd.h>
3735
#include <utility>
3836

37+
#ifndef WIN32
38+
#include <sys/socket.h>
39+
#include <unistd.h>
40+
#endif
41+
3942
namespace mp {
4043

4144
thread_local ThreadContext g_thread_context;
@@ -66,10 +69,9 @@ void EventLoopRef::reset(bool relock) MP_NO_TSA
6669
loop->m_num_clients -= 1;
6770
if (loop->done()) {
6871
loop->m_cv.notify_all();
69-
int post_fd{loop->m_post_fd};
7072
loop_lock->unlock();
7173
char buffer = 0;
72-
KJ_SYSCALL(write(post_fd, &buffer, 1)); // NOLINT(bugprone-suspicious-semicolon)
74+
loop->m_post_writer->write(&buffer, 1);
7375
// By default, do not try to relock `loop_lock` after writing,
7476
// because the event loop could wake up and destroy itself and the
7577
// mutex might no longer exist.
@@ -96,6 +98,20 @@ Connection::~Connection()
9698
// after the calls finish.
9799
m_rpc_system.reset();
98100

101+
// shutdownWrite is needed on Windows so pending data in the m_stream socket
102+
// will be sent instead of discarded when m_stream is destroyed. On unix,
103+
// this doesn't seem to be needed because data is sent more reliably.
104+
//
105+
// Sending pending data is important if the connection is a socketpair
106+
// because when one side of the socketpair is closed, the other side doesn't
107+
// seem to receive any onDisconnect event. So it is important for the other
108+
// side to instead receive Cap'n Proto "release" messages (see `struct
109+
// Release` in capnp/rpc.capnp) from local Client objects being being
110+
// destroyed so the remote side can free resources and shut down cleanly.
111+
// Without this call, Server objects corresponding to the Client objects on
112+
// the other side of the connection are not freed by Cap'n Proto.
113+
m_stream->shutdownWrite();
114+
99115
// ProxyClient cleanup handlers are in sync list, and ProxyServer cleanup
100116
// handlers are in the async list.
101117
//
@@ -192,17 +208,59 @@ void EventLoop::addAsyncCleanup(std::function<void()> fn)
192208
startAsyncThread();
193209
}
194210

211+
#ifdef WIN32
212+
//! Synchronous socket output stream. Cap'n Proto library only provides limited
213+
//! support for synchronous IO. It provides `FdOutputStream` which wraps unix
214+
//! file descriptors and calls write() internally, and `HandleOutStream` which
215+
//! wraps windows HANDLE values and calls WriteFile() internally. This class
216+
//! just provides analagous functionality wrapping SOCKET values and calls
217+
//! send() internally.
218+
class SocketOutputStream : public kj::OutputStream {
219+
public:
220+
explicit SocketOutputStream(SOCKET socket) : m_socket(socket) {}
221+
222+
void write(const void* buffer, size_t size) override;
223+
224+
private:
225+
SOCKET m_socket;
226+
};
227+
228+
static constexpr size_t WRITE_CLAMP_SIZE = 1u << 30; // 1GB clamp for Windows, like FdOutputStream
229+
230+
void SocketOutputStream::write(const void* buffer, size_t size) {
231+
const char* pos = reinterpret_cast<const char*>(buffer);
232+
233+
while (size > 0) {
234+
int n = send(m_socket, pos, static_cast<int>(kj::min(size, WRITE_CLAMP_SIZE)), 0);
235+
236+
KJ_WIN32(n != SOCKET_ERROR, "send() failed");
237+
KJ_ASSERT(n > 0, "send() returned zero.");
238+
239+
pos += n;
240+
size -= n;
241+
}
242+
}
243+
#endif
244+
195245
EventLoop::EventLoop(const char* exe_name, LogOptions log_opts, void* context)
196246
: m_exe_name(exe_name),
197247
m_io_context(kj::setupAsyncIo()),
198248
m_task_set(new kj::TaskSet(m_error_handler)),
199249
m_log_opts(std::move(log_opts)),
200250
m_context(context)
201251
{
202-
int fds[2];
203-
KJ_SYSCALL(socketpair(AF_UNIX, SOCK_STREAM, 0, fds));
204-
m_wait_fd = fds[0];
205-
m_post_fd = fds[1];
252+
auto pipe = m_io_context.provider->newTwoWayPipe();
253+
m_wait_stream = kj::mv(pipe.ends[0]);
254+
m_post_stream = kj::mv(pipe.ends[1]);
255+
KJ_IF_MAYBE(fd, m_post_stream->getFd()) {
256+
m_post_writer = kj::heap<kj::FdOutputStream>(*fd);
257+
#ifdef WIN32
258+
} else KJ_IF_MAYBE(handle, m_post_stream->getWin32Handle()) {
259+
m_post_writer = kj::heap<SocketOutputStream>(reinterpret_cast<SOCKET>(*handle));
260+
#endif
261+
} else {
262+
throw std::logic_error("Could not get file descriptor for new pipe.");
263+
}
206264
}
207265

208266
EventLoop::~EventLoop()
@@ -211,8 +269,8 @@ EventLoop::~EventLoop()
211269
const Lock lock(m_mutex);
212270
KJ_ASSERT(m_post_fn == nullptr);
213271
KJ_ASSERT(!m_async_fns);
214-
KJ_ASSERT(m_wait_fd == -1);
215-
KJ_ASSERT(m_post_fd == -1);
272+
KJ_ASSERT(!m_wait_stream);
273+
KJ_ASSERT(!m_post_stream);
216274
KJ_ASSERT(m_num_clients == 0);
217275

218276
// Spin event loop. wait for any promises triggered by RPC shutdown.
@@ -232,9 +290,7 @@ void EventLoop::loop()
232290
m_async_fns.emplace();
233291
}
234292

235-
kj::Own<kj::AsyncIoStream> wait_stream{
236-
m_io_context.lowLevelProvider->wrapSocketFd(m_wait_fd, kj::LowLevelAsyncIoProvider::TAKE_OWNERSHIP)};
237-
int post_fd{m_post_fd};
293+
kj::Own<kj::AsyncIoStream>& wait_stream{m_wait_stream};
238294
char buffer = 0;
239295
for (;;) {
240296
const size_t read_bytes = wait_stream->read(&buffer, 0, 1).wait(m_io_context.waitScope);
@@ -246,7 +302,7 @@ void EventLoop::loop()
246302
m_cv.notify_all();
247303
} else if (done()) {
248304
// Intentionally do not break if m_post_fn was set, even if done()
249-
// would return true, to ensure that the EventLoopRef write(post_fd)
305+
// would return true, to ensure that the EventLoopRef write(post_stream)
250306
// call always succeeds and the loop does not exit between the time
251307
// that the done condition is set and the write call is made.
252308
break;
@@ -256,10 +312,9 @@ void EventLoop::loop()
256312
m_task_set.reset();
257313
MP_LOG(*this, Log::Info) << "EventLoop::loop bye.";
258314
wait_stream = nullptr;
259-
KJ_SYSCALL(::close(post_fd));
260315
const Lock lock(m_mutex);
261-
m_wait_fd = -1;
262-
m_post_fd = -1;
316+
m_wait_stream = nullptr;
317+
m_post_stream = nullptr;
263318
m_async_fns.reset();
264319
m_cv.notify_all();
265320
}
@@ -274,10 +329,9 @@ void EventLoop::post(kj::Function<void()> fn)
274329
EventLoopRef ref(*this, &lock);
275330
m_cv.wait(lock.m_lock, [this]() MP_REQUIRES(m_mutex) { return m_post_fn == nullptr; });
276331
m_post_fn = &fn;
277-
int post_fd{m_post_fd};
278332
Unlock(lock, [&] {
279333
char buffer = 0;
280-
KJ_SYSCALL(write(post_fd, &buffer, 1));
334+
m_post_writer->write(&buffer, 1);
281335
});
282336
m_cv.wait(lock.m_lock, [this, &fn]() MP_REQUIRES(m_mutex) { return m_post_fn != &fn; });
283337
}

0 commit comments

Comments
 (0)