Files
feedBack-desktop/src/audio/Sandbox/ControlChannel.h
T
2026-06-16 18:48:12 +02:00

164 lines
6.8 KiB
C++

// ControlChannel — request/response + event messaging between the host
// (Slopsmith Desktop) and a sandbox subprocess.
//
// Transport: Windows named pipe in byte mode (PIPE_TYPE_BYTE |
// PIPE_READMODE_BYTE) with an explicit `[u32 length-LE][body]` framing
// layer. PIPE_TYPE_MESSAGE was tried first but dropped because the
// sandbox's `ready` frame wasn't being delivered to the host I/O thread
// reliably — see commit 2cb9ae9. Posix transport TBD when the macOS /
// Linux sandbox PRs land.
//
// Threading model: one I/O thread inside the channel reads frames and dispatches
// them to the event callback or to the matching pending request future. Callers
// invoke `request()` from arbitrary threads.
#pragma once
#include <juce_core/juce_core.h>
#include <atomic>
#include <chrono>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <thread>
#include <unordered_map>
#include "Protocol.h"
namespace slopsmith::sandbox {
class ControlChannel
{
public:
// Async callback fired for each sandbox-originated event. Invoked from the
// channel's internal I/O thread; the callback must not block.
using EventCallback = std::function<void(const juce::String& event,
const juce::var& data)>;
// Sentinel reason strings passed to the disconnect callback.
static const juce::String kReasonPeerClosed;
static const juce::String kReasonReadError;
static const juce::String kReasonProtocolError;
ControlChannel();
~ControlChannel();
// Host-side: create a uniquely-named pipe in CONNECT (server) mode and
// return the pipe name. The sandbox subprocess will connect to it shortly
// after spawn.
bool createServerSide(juce::String& pipeNameOut, juce::String& errorOut);
// Sandbox-side: connect to a pipe created by the host. Windows only —
// the named pipe is re-opened by name. POSIX uses connectClientSideFd
// (the control transport is a fd-passed socketpair, not a named object).
bool connectClientSide(const juce::String& pipeName, juce::String& errorOut);
#if ! JUCE_WINDOWS
// POSIX sandbox-side connect: adopt the inherited socketpair end (the fd
// the spawner dup2()'d into us, or — in an in-process loopback —
// server.sandboxFd()). Takes ownership of `fd`. The Windows
// connectClientSide(name) overload is unavailable on POSIX.
bool connectClientSideFd(int fd, juce::String& errorOut);
// POSIX host-side: the sandbox's end of the socketpair created by
// createServerSide. The caller owns it — dup2() it into the child (then
// close the copy) or hand it to an in-process connectClientSideFd.
// Returns -1 before createServerSide or on a non-server channel.
int sandboxFd() const noexcept;
// POSIX host-side: close our copy of the sandbox's socketpair end after a
// real spawn (the spawner has dup2()'d it into the child). REQUIRED — if
// the host keeps this open, the host end never observes EOF when the child
// dies, so crash detection never fires. No-op for the in-process loopback
// (connectClientSideFd already adopted the fd).
void closeSandboxFd() noexcept;
#endif
// Start the background I/O thread. Must be called after either
// createServerSide() or connectClientSide(). Returns false on error;
// callers can read getLastStartError() for a diagnostic string.
bool start(EventCallback onEvent,
std::function<void(const juce::String& reason)> onDisconnect);
// Diagnostic reason for the most recent start() failure (re-start
// attempted, no pipe, CreateEventW failure). Empty if start succeeded
// or has not been called.
juce::String getLastStartError() const { return lastStartError; }
void stop();
bool isAlive() const noexcept { return alive.load(std::memory_order_acquire); }
// Synchronous request/response. Returns the parsed result `juce::var`, or
// an undefined `var` on timeout/error (with the reason in `errorOut`).
juce::var request(const char* op, const juce::var& args,
int timeoutMs, juce::String* errorOut = nullptr);
// Fire-and-forget: no reply expected. Used for high-frequency messages
// like MIDI events and parameter automation.
bool postNoReply(const char* op, const juce::var& args);
// Sandbox-side helpers: send a reply to the host's request, or originate
// an event.
bool sendReply(int requestId, bool ok, const juce::var& result,
const juce::String& errorMessage = {});
bool sendEvent(const char* eventName, const juce::var& data);
// Sandbox-side: when the channel parses an inbound request, the consumer
// installs a request handler. MUST be called BEFORE start() — the I/O
// thread reads `requestHandler` on every inbound request, and the
// member is intentionally not synchronised. Installing after start()
// races the read.
using RequestHandler =
std::function<void(int requestId, const juce::String& op,
const juce::var& args)>;
void setRequestHandler(RequestHandler handler);
private:
struct Pending
{
std::promise<juce::var> promise;
};
bool writeFrame(const juce::MemoryBlock& body);
bool readFrame(juce::MemoryBlock& out);
// Server side: block until the sandbox connects (Windows: ConnectNamedPipe;
// POSIX: socketpair is already connected, so this just races the stop
// signal). Returns false if stop() fired first; `failReason` is set for
// failWith. Platform-defined.
bool waitForPeer(juce::String& failReason);
void ioLoop();
void failWith(const juce::String& reason);
// Set by readFrame() before it returns false so ioLoop can classify the
// disconnect. `lastReadError` is the raw OS error (Win32 GetLastError /
// POSIX errno) for diagnostics; `lastReadPeerClosed` is the
// platform-agnostic verdict ioLoop acts on (a clean peer-side close /
// EOF → kReasonPeerClosed, anything else → kReasonReadError) so the shared
// dispatch loop never has to know per-OS error codes. Only the I/O thread
// writes or reads these fields.
unsigned long lastReadError = 0;
bool lastReadPeerClosed = false;
struct Impl;
std::unique_ptr<Impl> impl; // OS-specific handle wrapper
std::atomic<bool> alive{false};
std::atomic<int> nextRequestId{1};
EventCallback onEvent;
std::function<void(const juce::String& reason)> onDisconnect;
RequestHandler requestHandler;
std::mutex pendingMutex;
std::unordered_map<int, std::shared_ptr<Pending>> pending;
std::mutex writeMutex; // serialises outbound writes
std::thread ioThread;
juce::String lastStartError;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ControlChannel)
};
} // namespace slopsmith::sandbox