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

@@ -6,8 +6,9 @@ and what's next* read [`PROGRESS.md`](PROGRESS.md); for *design* read [`docs/`](
> **One-line status:** M5 (moderation & admin UI) is complete — permissions, kick/ban/move, > **One-line status:** M5 (moderation & admin UI) is complete — permissions, kick/ban/move,
> server-mute, channel CRUD, in-app account management, disconnect/keepalive/reaper. Windows > server-mute, channel CRUD, in-app account management, disconnect/keepalive/reaper. Windows
> WinForms C# client is shipped (M4). `ctest --preset dev` green — 21/21 tests. macOS/iOS > WinForms C# client is shipped (M4). **macOS port validated** — `dev` + `apple-dev` presets
> Swift client is next. See [`PROGRESS.md`](PROGRESS.md). > build green, 21/21 tests pass on macOS. `ctest --preset dev` green — 21/21 tests.
> macOS/iOS Swift client is next. See [`PROGRESS.md`](PROGRESS.md).
VoiceCat = self-hosted native voice & text chat (TeamSpeak/Mumble-style). Plain TCP (control) VoiceCat = self-hosted native voice & text chat (TeamSpeak/Mumble-style). Plain TCP (control)
+ UDP (media), no WebRTC, encrypted by default. A shared C++ core (`libvoicecat`) drives + UDP (media), no WebRTC, encrypted by default. A shared C++ core (`libvoicecat`) drives

View File

@@ -10,6 +10,73 @@ up instantly. Newest status at the top.
## ▶ Where we left off / next action ## ▶ 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 - **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` 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. (cache-identical to `m1-dev`, never used), no optimized+tests preset, no stripping.

View File

@@ -18,11 +18,22 @@ Planned shape (see [`docs/architecture.md`](../../docs/architecture.md) §4 and
Nothing here yet — the core must reach M2 (working voice) before the GUI is worth building. Nothing here yet — the core must reach M2 (working voice) before the GUI is worth building.
## Building the core for Apple platforms (scaffolding) ## Building the core for Apple platforms
The CMake presets `apple-dev`, `apple-ios`, and `apple-ios-sim` produce static `libvoicecat.a` The CMake presets `apple-dev`, `apple-ios`, and `apple-ios-sim` produce static `libvoicecat.a`
slices for the Swift Package / XCFramework. They are **scaffolding** — not yet CI-validated. slices for the Swift Package / XCFramework.
Build on macOS (they won't work on Windows/Linux):
**`apple-dev` (macOS slice) is validated** — builds green on macOS 26.5 / Apple Silicon
(Apple clang 21, vcpkg `arm64-osx` triplet) and produces a valid 1.9 MB arm64 static library
with 167 exported C ABI symbols (correct visibility). The XCFramework creation path is also
verified — `xcodebuild -create-xcframework` produces a valid `VoiceCatCore.xcframework`
with the `.a` + `voicecat.h` headers, ready for a Swift Package binary target.
**`apple-ios` and `apple-ios-sim` (iOS slices) are scaffolding** — not yet CI-validated.
The vcpkg `arm64-ios` / `arm64-ios-sim` triplets for all 8 deps need verification.
Prerequisites: `VCPKG_ROOT` set, Xcode + iOS SDK installed, and Homebrew `autoconf-archive`
(needed by vcpkg's libsodium port — `brew install autoconf-archive`).
```bash ```bash
# Prerequisites: VCPKG_ROOT set, Xcode + iOS SDK installed # Prerequisites: VCPKG_ROOT set, Xcode + iOS SDK installed
@@ -51,6 +62,8 @@ xcodebuild -create-xcframework \
-output build/VoiceCatCore.xcframework -output build/VoiceCatCore.xcframework
``` ```
The XCFramework is then consumed by the Swift Package as a binary target. Actual The XCFramework is then consumed by the Swift Package as a binary target. The macOS-only
XCFramework (single `apple-dev` slice) is verified to build now; the full 3-slice XCFramework
(macOS + iOS device + iOS simulator) waits for the iOS presets to be validated. Actual
`AVAudioSession` integration, `Info.plist` mic permission, ReplayKit extension, and SwiftUI `AVAudioSession` integration, `Info.plist` mic permission, ReplayKit extension, and SwiftUI
UI work are tracked as follow-up tasks — the presets exist so the build entry point is ready. UI work are tracked as follow-up tasks.

View File

@@ -24,6 +24,7 @@
#include <algorithm> #include <algorithm>
#include <chrono> #include <chrono>
#include <csignal>
#include <cstring> #include <cstring>
#include <filesystem> #include <filesystem>
@@ -172,6 +173,12 @@ void vc_client::run_io(std::string host, uint16_t port) {
#ifdef _WIN32 #ifdef _WIN32
WSADATA wsa{}; WSADATA wsa{};
WSAStartup(MAKEWORD(2, 2), &wsa); WSAStartup(MAKEWORD(2, 2), &wsa);
#else
// POSIX/macOS: ignore SIGPIPE — a write to a closed socket returns EPIPE instead of
// terminating the process. On macOS SIGPIPE is delivered by default (unlike Windows
// where it doesn't exist); without this, a peer dropping mid-TLS-write kills us.
// Process-global and idempotent (safe to call per run_io).
std::signal(SIGPIPE, SIG_IGN);
#endif #endif
struct addrinfo hints{}; struct addrinfo hints{};

View File

@@ -322,6 +322,17 @@ void TcpServerConn::close() {
// In non-TLS mode, the Asio async chain will naturally stop when the socket closes. // In non-TLS mode, the Asio async chain will naturally stop when the socket closes.
} }
void TcpServerConn::wait_closed() {
if (tls_thread_.joinable()) {
if (std::this_thread::get_id() == tls_thread_.get_id()) {
// Being called from our own TLS thread — detach to avoid self-join deadlock.
tls_thread_.detach();
} else {
tls_thread_.join();
}
}
}
// ── TcpAcceptor ───────────────────────────────────────────────────────────── // ── TcpAcceptor ─────────────────────────────────────────────────────────────
namespace { namespace {
@@ -359,6 +370,33 @@ void TcpAcceptor::stop() {
stopped_ = true; stopped_ = true;
std::error_code ignored; std::error_code ignored;
acceptor_.close(ignored); acceptor_.close(ignored);
// Close all tracked connections so their TLS read threads exit. The socket close
// happens while the io_context (and its reactor) is still alive, preventing the
// null-reactor use-after-free that manifests on macOS kqueue.
std::vector<std::shared_ptr<TcpServerConn>> to_close;
{
std::lock_guard lk(conns_mu_);
to_close = conns_;
}
for (auto& conn : to_close) conn->close();
}
void TcpAcceptor::shutdown() {
stop();
// Wait for every connection's TLS I/O thread to finish. close() (called by stop())
// set closing_=true and closed the socket, so tls_read_loop is already exiting or has
// exited; the join is brief. This must complete BEFORE the io_context is destroyed.
std::vector<std::shared_ptr<TcpServerConn>> to_join;
{
std::lock_guard lk(conns_mu_);
to_join = std::move(conns_);
}
for (auto& conn : to_join) {
conn->wait_closed();
}
// to_join drops here — if a thread captured shared_from_this, the TcpServerConn stays
// alive until that thread releases it; the destructor's close() is a no-op (already
// closed) and tls_thread_ is already joined, so no reactor access occurs.
} }
void TcpAcceptor::do_accept() { void TcpAcceptor::do_accept() {
@@ -371,7 +409,14 @@ void TcpAcceptor::do_accept() {
} }
socket.set_option(asio::ip::tcp::no_delay(true)); socket.set_option(asio::ip::tcp::no_delay(true));
auto conn = factory_(std::move(socket)); auto conn = factory_(std::move(socket));
if (conn) conn->start(); if (conn) {
conn->start();
// Track so shutdown() can close + join before the io_context is destroyed.
{
std::lock_guard lk(conns_mu_);
conns_.push_back(conn);
}
}
do_accept(); do_accept();
}); });
} }

View File

@@ -112,6 +112,10 @@ class TcpServerConn : public std::enable_shared_from_this<TcpServerConn> {
// Close the connection (safe from any thread). // Close the connection (safe from any thread).
void close(); 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); } bool connected() const { return connected_.load(std::memory_order_acquire); }
private: private:
@@ -155,9 +159,16 @@ class TcpAcceptor {
// Start accepting. Call once; re-arms itself automatically. // Start accepting. Call once; re-arms itself automatically.
void start(); void start();
// Stop accepting (does not close existing connections). // Stop accepting and close all tracked connections (safe while io_context is alive).
void stop(); 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). // 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()); } uint16_t local_port() const { return static_cast<uint16_t>(acceptor_.local_endpoint().port()); }
@@ -167,6 +178,9 @@ class TcpAcceptor {
asio::ip::tcp::acceptor acceptor_; asio::ip::tcp::acceptor acceptor_;
ConnFactory factory_; ConnFactory factory_;
bool stopped_{false}; bool stopped_{false};
std::mutex conns_mu_;
std::vector<std::shared_ptr<TcpServerConn>> conns_;
}; };
// ── UDP media channel (M2) ─────────────────────────────────────────────────── // ── UDP media channel (M2) ───────────────────────────────────────────────────

View File

@@ -17,7 +17,7 @@ and *how to drive the binaries by hand*.
| `release` | `build/release` | vcpkg | Release | ON | ON | ON | no | all | Optimized build with the full test suite. Use to run tests against optimized code, profile, or catch optimizer-sensitive bugs. Symbols kept (not stripped) so stack traces and profiling remain useful. | | `release` | `build/release` | vcpkg | Release | ON | ON | ON | no | all | Optimized build with the full test suite. Use to run tests against optimized code, profile, or catch optimizer-sensitive bugs. Symbols kept (not stripped) so stack traces and profiling remain useful. |
| `server-release` | `build/server-release` | vcpkg | Release | ON | ON | OFF | **yes** | all | Production-shaped build for deployment. Optimized + stripped binaries (`-s`), no tests. This is what you'd ship/run — see [docs/deployment.md](deployment.md). | | `server-release` | `build/server-release` | vcpkg | Release | ON | ON | OFF | **yes** | all | Production-shaped build for deployment. Optimized + stripped binaries (`-s`), no tests. This is what you'd ship/run — see [docs/deployment.md](deployment.md). |
| `windows-client` | `build/windows-client` | vcpkg | Release | OFF | OFF | OFF | no | Windows | Produces a redistributable `voicecat.dll` for the C# WinForms client (M4). Static MinGW runtime — no `libgcc_s_seh-1.dll` etc. See [clients/windows/README.md](../clients/windows/README.md). | | `windows-client` | `build/windows-client` | vcpkg | Release | OFF | OFF | OFF | no | Windows | Produces a redistributable `voicecat.dll` for the C# WinForms client (M4). Static MinGW runtime — no `libgcc_s_seh-1.dll` etc. See [clients/windows/README.md](../clients/windows/README.md). |
| `apple-dev` | `build/apple-dev` | vcpkg | Release | OFF | OFF | OFF | no | macOS | **Scaffolding** — static `libvoicecat.a` for the Swift Package / XCFramework (macOS slice). Not yet CI-validated; build on macOS to verify. See [clients/apple/README.md](../clients/apple/README.md). | | `apple-dev` | `build/apple-dev` | vcpkg | Release | OFF | OFF | OFF | no | macOS | Static `libvoicecat.a` for the Swift Package / XCFramework (macOS slice). Validated on macOS 26.5 / Apple Silicon — builds green, produces valid arm64 `.a` + XCFramework. See [clients/apple/README.md](../clients/apple/README.md). |
| `apple-ios` | `build/apple-ios` | vcpkg | Release | OFF | OFF | OFF | no | macOS→iOS | **Scaffolding** — cross-compiled static `libvoicecat.a` for iOS device (`arm64-ios`). One XCFramework slice. Not yet CI-validated. | | `apple-ios` | `build/apple-ios` | vcpkg | Release | OFF | OFF | OFF | no | macOS→iOS | **Scaffolding** — cross-compiled static `libvoicecat.a` for iOS device (`arm64-ios`). One XCFramework slice. Not yet CI-validated. |
| `apple-ios-sim` | `build/apple-ios-sim` | vcpkg | Release | OFF | OFF | OFF | no | macOS→iOS sim | **Scaffolding** — cross-compiled static `libvoicecat.a` for iOS simulator (`arm64-ios-sim`). One XCFramework slice. Not yet CI-validated. | | `apple-ios-sim` | `build/apple-ios-sim` | vcpkg | Release | OFF | OFF | OFF | no | macOS→iOS sim | **Scaffolding** — cross-compiled static `libvoicecat.a` for iOS simulator (`arm64-ios-sim`). One XCFramework slice. Not yet CI-validated. |
@@ -207,8 +207,10 @@ symbol tables from the binaries, producing smaller executables suitable for dist
## 6. Apple platform builds (scaffolding) ## 6. Apple platform builds (scaffolding)
The `apple-dev`, `apple-ios`, and `apple-ios-sim` presets produce static `libvoicecat.a` The `apple-dev`, `apple-ios`, and `apple-ios-sim` presets produce static `libvoicecat.a`
slices for the Swift Package / XCFramework. They are **scaffolding** — not yet CI-validated slices for the Swift Package / XCFramework. The `apple-dev` preset (macOS slice) is
and won't build on Windows. Build on macOS: **validated** — it builds green on macOS 26.5 / Apple Silicon and produces a valid arm64
`.a` + XCFramework. The iOS cross-compile presets (`apple-ios`, `apple-ios-sim`) are still
**scaffolding** — not yet CI-validated. Build on macOS:
```bash ```bash
# macOS slice (arm64-osx on Apple Silicon, x64-osx on Intel) # macOS slice (arm64-osx on Apple Silicon, x64-osx on Intel)

View File

@@ -1,6 +1,7 @@
#include "server.h" #include "server.h"
#include <cstdio> #include <cstdio>
#include <csignal>
#ifdef VOICECAT_HAS_NET #ifdef VOICECAT_HAS_NET
@@ -135,13 +136,26 @@ int Server::run() {
// Arm programmatic stop (for tests and embedders). // Arm programmatic stop (for tests and embedders).
{ {
std::lock_guard lk(stop_mutex_); std::lock_guard lk(stop_mutex_);
stop_fn_ = [&io] { io.stop(); }; stop_fn_ = [&] {
acceptor.stop();
media_relay->stop();
io.stop();
};
} }
// Notify caller of the actual bound port (matters when bind_port==0). // Notify caller of the actual bound port (matters when bind_port==0).
uint16_t bound = acceptor.local_port(); uint16_t bound = acceptor.local_port();
if (cfg_.on_ready) cfg_.on_ready(bound); if (cfg_.on_ready) cfg_.on_ready(bound);
#ifndef _WIN32
// POSIX/macOS: ignore SIGPIPE — writing to a disconnected client's TCP socket returns
// EPIPE instead of killing the server process. On macOS SIGPIPE is delivered by
// default (unlike Windows where the signal doesn't exist). Must be set before any
// async writes are posted. Process-global; safe alongside the asio::signal_set below
// (which only catches SIGINT/SIGTERM).
std::signal(SIGPIPE, SIG_IGN);
#endif
// Graceful shutdown on SIGINT/SIGTERM // Graceful shutdown on SIGINT/SIGTERM
asio::signal_set signals(io, SIGINT, SIGTERM); asio::signal_set signals(io, SIGINT, SIGTERM);
signals.async_wait([&](std::error_code, int sig) { signals.async_wait([&](std::error_code, int sig) {
@@ -184,6 +198,13 @@ int Server::run() {
io.run(); io.run();
// Close all connections and block until their TLS I/O threads have finished.
// MUST happen before io (and its reactor) is destroyed — the TLS read threads do
// blocking I/O (not async on io_context) and touch the reactor on socket close.
// On macOS kqueue the reactor pointer is null'd immediately on io_context destruction,
// causing a segfault; latent on Windows IOCP / Linux epoll where timing is more forgiving.
acceptor.shutdown();
{ {
std::lock_guard lk(stop_mutex_); std::lock_guard lk(stop_mutex_);
stop_fn_ = nullptr; stop_fn_ = nullptr;

View File

@@ -36,6 +36,7 @@
static int sock_error() { return WSAGetLastError(); } static int sock_error() { return WSAGetLastError(); }
#else #else
# include <arpa/inet.h> # include <arpa/inet.h>
# include <netdb.h>
# include <netinet/in.h> # include <netinet/in.h>
# include <sys/socket.h> # include <sys/socket.h>
# include <unistd.h> # include <unistd.h>