re: #376 (review)
Correction: the last paragraph above is wrong about Bitcoin Core in spawned child processes, so I've struck out that part. This doesn't change the suggestion or affect this PR, but it's a Bitcoin Core bug worth fixing separately.
In CapnpProtocol::serve() (used by a bitcoin-node spawned by bitcoin-gui, for example), m_loop_ref is never set, and it can't be. serve() runs loop() on the child's main thread, and the loop has to exit when the parent disconnects so the child can exit. But m_loop_ref is only reset in ~CapnpProtocol, which can't run until serve() returns, so setting it would make the child hang. There, the parent connection's reference keeps the loop alive instead of m_loop_ref.
Calls that come from the parent are still fine, because they run inside an IPC request that holds a ProxyServer reference (for example Shutdown() calling disconnectIncoming() after the parent calls appShutdown()). Calls from the child's own threads are not covered. For example, the echoipc RPC handled on the child's RPC thread calls spawnProcess() → connect() → ConnectStream() → sync() without a reference. If the parent disconnects at that moment, the loop can exit and serve() can destroy m_loop while that thread is still using it. The reference the old sync() took didn't fully prevent this either, since m_loop itself is a std::optional reset by the main thread without synchronization.
The root of the problem is missing synchronization in CapnpProtocol. Once the parent is gone the child is shutting down anyway, so these calls should fail instead of keeping the loop alive. A fix could look like:
//! Return a reference that keeps the event loop running, or throw if the
//! loop is not running. Threads that are not handling an IPC request need
//! this, because in serve() mode nothing else keeps the loop running for
//! them.
mp::EventLoopRef loopRef() EXCLUSIVE_LOCKS_REQUIRED(!m_loop_mutex)
{
LOCK(m_loop_mutex);
if (!m_loop) throw Exception("IPC event loop is not running");
mp::Lock lock(m_loop->m_mutex);
if (m_loop->done()) throw Exception("IPC event loop is shutting down");
mp::EventLoopRef ref{*m_loop, &lock};
ref.m_lock = nullptr; // Lock m_mutex again when the reference is released.
return ref;
}
where a new m_loop_mutex is also held wherever m_loop is assigned or reset (serve(), startLoop()), and connect(), listen(), makeStream() and disconnectIncoming() call loopRef() and hold the result while they use the loop. Checking done() with m_mutex held is enough because done() never goes from true back to false once the loop is running (see the other thread). I can open a Bitcoin Core PR for this.