2 Commits
Author SHA1 Message Date
Dario LipicarandClaude Opus 5 3a31c91d13 fix(plain): close the RPC acceptor on the server's strand, not the caller's thread (#39)
RpcServerTcp::stop() and RpcServerSsl::stop() closed m_acceptor on whatever
thread called them — in practice the host thread, via ~PlainTransportHost —
while doAccept() re-armed async_accept from inside its own completion handler,
on the io worker. Nothing serialized the two.

This is the acceptor half of the race PR #38 fixed for RpcConnection, and it
fails identically: asio acceptors are "Shared objects: Unsafe", and close()
runs cleanup_descriptor_data(), which nulls the reactor's per-descriptor state
while reactive_socket_service_base::start_op() holds it by reference. It was
left out of #38 because every backtrace captured in the wild was a write
initiation, never an accept — but it reproduces on demand:

  EXC_BAD_ACCESS  KERN_INVALID_ADDRESS at 0x98
    logos::plain::RpcServerTcp::doAccept()
    ...reactive_socket_move_accept_op<...>::do_complete(...)
    logos::plain::IoContextPool::IoContextPool()::$_0    <- io worker thread

Both servers now own a strand. doAccept()'s completion handler is
bind_executor'd onto it (so the re-arm runs there) and stop() hands the close
to it with dispatch() — inline when already on the strand, queued and
non-blocking from anywhere else, exactly as RpcConnection::closeStreamOnStrand
does.

start() still runs open/bind/listen inline: callers read boundPort() the moment
it returns. That is safe because no async op on the acceptor exists yet, and
PlainTransportHost serializes start()/stop() under its own mutex. Only the
accept loop moves onto the strand, which is invisible to clients — listen() has
already run, so an early connect waits in the backlog.

Deferring the close leaves the listener open for the microseconds between
stop() returning and the strand running it, so a connection can still be
accepted in that gap. The accept path therefore tests m_stopped and publishes
the connection under one lock, and drops a late socket instead of wrapping it
in a connection and stop()ing it — conn->stop() would call
onConnectionClosed() on the IncomingCallHandler whose destructor started this
teardown. The TLS server gets the same guard, where it was already latent: an
async_handshake in flight was never aborted by closing the acceptor.

Adds RpcServerTeardownTest: a start/connect/stop stress loop shaped like
test_rpc_connection_teardown.cpp, plus a round-trip check that a client
connecting the instant start() returns is still served.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 15:48:17 -03:00
Dario Lipicar 29afbac532 Extract the Logos protocol layer from logos-cpp-sdk (lp_* C ABI + protocol semver) (#2)
* Extract the Logos protocol layer from logos-cpp-sdk

Transports (plain TCP/TLS, qt_local, qt_remote/QRO, mock), token manager,
consumer core (LogosAPIClient/LogosAPIConsumer incl. the capability
auto-requestModule flow), ModuleProxy, the abstract LogosProviderObject
interface, and the canonical QVariant<->JSON conversion — now behind the
language-neutral lp_* C ABI (logos_protocol.h) carrying the protocol
semver (LOGOS_PROTOCOL_VERSION_*, lp_protocol_version()).

Bytes crossing the ABI use the lossless {"_bytes": base64url} tagging
(NUL-safe), matching the plain wire encoding.

Provider lp_* surface is compiled groundwork; serving lands with module
authoring.

* consumer: typed requestModule for the capability flow

Port of logos-cpp-sdk master f5a127dd ('use updated capability module',
cpp-sdk#85, Iuri Matias) — the touched files (logos_api_client.cpp,
logos_api_consumer.{h,cpp}) moved into this repo in the P1 extraction.
The capability auto-requestModule path now calls a typed std::string
helper on the consumer (which acquires the capability object directly)
instead of a stringly invokeRemoteMethod round-trip. 111/111 tests.
2026-06-12 18:59:01 -03:00