feat(macos): validate dev + apple-dev presets on macOS, fix 3 cross-platform bugs

macOS port groundwork — core, server, tools, and tests now build and run on
macOS 26.5 / Apple Silicon. ctest --preset dev green 21/21 (2 consecutive runs).
apple-dev produces valid arm64 libvoicecat.a + XCFramework for the Swift Package.

Three real cross-platform bugs found and fixed (all latent on Windows/Linux):

1. test_m2_voice.cpp POSIX branch missing <netdb.h> — Linux glibc transitively
   includes it, macOS doesn't. Would fail on any strict POSIX system.

2. SIGPIPE killing processes on macOS — writing to a closed TCP socket raises
   SIGPIPE by default (doesn't exist on Windows, benign on Linux). Fixed by
   ignoring SIGPIPE in both core client init and server startup (POSIX-only,
   #ifndef _WIN32). Production fix, not just tests.

3. Use-after-free of Asio's kqueue reactor on server shutdown — the
   deterministic test_tofu_flow segfault. TcpServerConn's tls_read_loop runs on
   a blocking-I/O thread; when Server::run() returned, io_context was destroyed
   while those threads were still running. On macOS kqueue the reactor pointer
   is null'd immediately -> segfault in socket.close(). Latent on Windows IOCP
   and Linux epoll. Fix: TcpAcceptor now tracks connections; new shutdown()
   closes all + joins threads before io is destroyed; Server::stop() now closes
   acceptor + media_relay too (was just io.stop()).

Verified: dev + apple-dev presets build green, 21/21 tests pass, server starts
+ two vccli text chat over TLS (M1 on Mac), vccli --voice starts MIC stream via
CoreAudio (M2 protocol-level), vccli --list-devices enumerates CoreAudio
devices, xcodebuild -create-xcframework produces valid VoiceCatCore.xcframework.

No ABI or proto changes. Docs updated: building.md, clients/apple/README.md,
PROGRESS.md, CLAUDE.md status line.
This commit is contained in:
2026-06-18 13:24:42 +02:00
parent bcb7ae8ccb
commit b2af1a3001
9 changed files with 187 additions and 16 deletions

View File

@@ -112,6 +112,10 @@ class TcpServerConn : public std::enable_shared_from_this<TcpServerConn> {
// Close the connection (safe from any thread).
void close();
// Block until the TLS I/O thread (if any) has finished. Must be called after close().
// Safe to call from any thread except the TLS I/O thread itself.
void wait_closed();
bool connected() const { return connected_.load(std::memory_order_acquire); }
private:
@@ -155,18 +159,28 @@ class TcpAcceptor {
// Start accepting. Call once; re-arms itself automatically.
void start();
// Stop accepting (does not close existing connections).
// Stop accepting and close all tracked connections (safe while io_context is alive).
void stop();
// Stop accepting, close all connections, and block until every connection's
// I/O thread has finished. Call BEFORE the io_context is destroyed — the TLS read
// threads do blocking I/O (not async on the io_context) and will touch the reactor
// on socket close if the io_context is already gone (manifests as a null-kqueue-reactor
// segfault on macOS; latent on Windows IOCP / Linux epoll where timing is more forgiving).
void shutdown();
// Actual bound port (useful when bind_port=0 lets the OS pick).
uint16_t local_port() const { return static_cast<uint16_t>(acceptor_.local_endpoint().port()); }
private:
void do_accept();
asio::ip::tcp::acceptor acceptor_;
ConnFactory factory_;
bool stopped_{false};
asio::ip::tcp::acceptor acceptor_;
ConnFactory factory_;
bool stopped_{false};
std::mutex conns_mu_;
std::vector<std::shared_ptr<TcpServerConn>> conns_;
};
// ── UDP media channel (M2) ───────────────────────────────────────────────────