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

@@ -10,6 +10,73 @@ up instantly. Newest status at the top.
## ▶ Where we left off / next action
- **Done:** **macOS port — `dev` + `apple-dev` presets validated** (2026-06-18). The core,
server, tools, and tests now build and run on macOS (Apple Silicon, macOS 26.5, Apple clang
21). This lays the groundwork for the macOS/iOS Swift client. Three real bugs found and
fixed (all were cross-platform issues that manifested on macOS but were latent on
Windows/Linux):
1. **Missing `<netdb.h>` in `test_m2_voice.cpp` POSIX branch** — the raw-socket test's
`#else` branch included `<arpa/inet.h>`/`<netinet/in.h>`/`<sys/socket.h>`/`<unistd.h>`
but not `<netdb.h>` (needed for `addrinfo`/`getaddrinfo`/`freeaddrinfo`). On Linux glibc
these headers transitively include `<netdb.h>`; on macOS they don't. Fixed by adding
`# include <netdb.h>` to the POSIX branch (mirrors `core/src/core/client.cpp:16` which
already had it). Real latent bug — would fail on any strict POSIX system.
2. **SIGPIPE killing processes on macOS** — on macOS, writing to a closed TCP socket
raises `SIGPIPE` by default (unlike Windows where it doesn't exist, or Linux where it's
often benign). This killed `test_tofu_flow` (intermittent SIGPIPE/SEGFAULT) and would
also kill `voicecat-server` and `vccli` in production when a peer dropped mid-write.
Fixed by ignoring SIGPIPE (`std::signal(SIGPIPE, SIG_IGN)`) in both the core client
init (`core/src/core/client.cpp` POSIX branch of the `#ifdef _WIN32` WSAStartup block)
and the server startup (`server/src/server.cpp` before the `asio::signal_set`). Both are
POSIX-only (`#ifndef _WIN32`), process-global, and idempotent. The server's
`asio::signal_set(SIGINT, SIGTERM)` is unaffected (independent signals).
3. **Use-after-free of Asio's kqueue reactor on server shutdown** — the root cause of the
deterministic `test_tofu_flow` segfault (EXC_BAD_ACCESS in
`kqueue_reactor::deregister_descriptor(this=0x0000000000000000)`). `TcpServerConn`'s
`tls_read_loop` runs on a dedicated blocking-I/O thread (not async on `io_context`).
When `Server::stop()``io.stop()``Server::run()` returned, the local
`asio::io_context` was destroyed while `tls_read_loop` threads were still running. When
a thread detected the disconnect and called `TcpServerConn::close()``socket.close()`
→ Asio tried to deregister from the kqueue reactor — but the reactor (owned by
`io_context`) was already destroyed, and on macOS kqueue the reactor pointer is null'd
immediately. Latent on Windows (IOCP) and Linux (epoll) where the timing is more
forgiving. **Fix:** `TcpAcceptor` now tracks its connections (new `conns_` vector +
`conns_mu_`); `TcpAcceptor::stop()` closes all tracked connections while `io_context` is
still alive; new `TcpAcceptor::shutdown()` method calls `stop()` then
`wait_closed()` on each connection (new `TcpServerConn::wait_closed()` joins
`tls_thread_`); `Server::run()` calls `acceptor.shutdown()` after `io.run()` returns and
before `io` is destroyed; `Server::stop()`'s `stop_fn_` now calls `acceptor.stop()` +
`media_relay->stop()` + `io.stop()` (was just `io.stop()`). After `acceptor.shutdown()`,
when `tls_read_loop` threads exit and call `on_disconnected``ConnSession::close()`
`TcpServerConn::close()`, the `close()` is a no-op (`closing_.exchange(true)` returns
true) — no reactor access occurs after `io` is destroyed.
- **Environment setup:** vcpkg cloned to `~/code/vcpkg` + bootstrapped. `VCPKG_ROOT` must
be set. Homebrew `autoconf-archive` is required (vcpkg's libsodium port needs it for
autoreconf — `brew install autoconf-archive`). `autoconf`/`automake`/`libtool` were
already installed; `glibtoolize` (Homebrew's macOS name for `libtoolize`) is handled by
vcpkg automatically.
- **Verified:** `cmake --preset dev` + `cmake --build --preset dev` green (21 binaries).
`ctest --preset dev --parallel 1`**21/21 green** (2 consecutive runs, 64s each).
`cmake --preset apple-dev` + `cmake --build --preset apple-dev` green → valid 1.9 MB
arm64 `libvoicecat.a` (167 exported C ABI symbols, correct visibility).
`xcodebuild -create-xcframework` → valid `VoiceCatCore.xcframework` (macOS-arm64 slice
with `voicecat.h` headers). Server runtime: `voicecat-server` starts, generates identity,
SQLite, Lobby, binds TCP+UDP, clean SIGINT shutdown. Two `vccli` text chat over TLS
(M1 exit criterion on Mac). `vccli --voice --input-mode vad` starts a MIC stream via
CoreAudio (M2 protocol-level on Mac — ear test pending). `vccli --list-devices`
enumerates 4 CoreAudio input + 3 output devices with correct defaults.
- **Apple framework linking:** NOT needed — modern macOS ld (Xcode 26.5) auto-discovers
CoreAudio/CoreFoundation frameworks in `/System/Library/Frameworks` without explicit
`-framework` flags. miniaudio's `MINIAUDIO_IMPLEMENTATION` compiles the CoreAudio calls
inline, and the linker resolves them automatically. No `if(APPLE)` CMake block was added.
- **Docs updated:** `docs/building.md` (`apple-dev` row + §6 updated from "scaffolding" to
"validated"), `clients/apple/README.md` (macOS slice build confirmed, iOS slices still
scaffolding), `PROGRESS.md` (this entry).
- **Still deferred (per scope):** macOS `SCREEN_AUDIO` loopback via ScreenCaptureKit (stub
returns `false` — per `docs/voice.md §9`); iOS cross-compile presets (`apple-ios`/
`apple-ios-sim` — scaffolding); `vc_audio_suspend`/`vc_audio_resume` ABI hooks (defer to
iOS client milestone, keep ABI stable).
- **Done:** **CMake preset cleanup + cross-platform build config** (2026-06-18). The preset
set was a mess — `dev` (never used), `m1-dev` (the one everyone used), `m2-dev`
(cache-identical to `m1-dev`, never used), no optimized+tests preset, no stripping.