acdream/src
Erik cf25330458 fix(net): survive transient socket errors in the receive loop (2026-07-24 audit review)
WorldSession.NetReceiveLoop wrapped its entire while loop in a single
try/catch, so ANY non-timeout SocketException permanently killed the
background receive thread: the catch block at the loop's end was empty
(misattributed the error to "socket closed during shutdown"), and the
finally called _inboundQueue.Writer.TryComplete(), which silently and
irrecoverably stopped all inbound processing for the rest of the
session — no log line, no recovery path, and LiveSessionHost.Reconnect
has zero production callers to notice.

The realistic trigger is a well-known Windows UdpClient quirk: an ICMP
"port unreachable" reply to an EARLIER Send (e.g. against a stale ACE
session that already tore down its socket) surfaces as a
WSAECONNRESET SocketException on this socket's NEXT, completely
unrelated Receive call. NetClient.Receive already swallows the
expected SocketError.TimedOut heartbeat case; anything else reaching
WorldSession was a real, transient, per-datagram error being treated
as session-fatal.

Three changes, root-cause not a band-aid:

- NetClient's constructor now disables SIO_UDP_CONNRESET reporting on
  Windows, so a delayed ICMP error can't poison receives at all.
- NetReceiveLoop now catches SocketException PER ITERATION, logs it,
  and continues polling instead of exiting. The existing clean-shutdown
  paths (cancellation, ObjectDisposedException during Dispose) are
  unchanged — only the non-timeout-socket-error case that used to kill
  the loop is now recoverable.
- NetClient.Receive no longer calls the ReceiveTimeout setter (a
  setsockopt syscall) on every single call — only when the requested
  timeout differs from the last-applied value, cached in a new field.
  This was an unrelated but adjacent finding (4x/sec syscall churn at
  the 250ms heartbeat cadence) in the same audit.

No change to outbound wire behavior, ack cadence, heartbeat interval,
or datagram ordering — this is purely receive-loop resilience.

Tests: NetClientTests gained a SIO_UDP_CONNRESET construction smoke
test and two ReceiveTimeout-caching tests. A new
WorldSessionNetReceiveLoopResilienceTests drives the actual private
NetReceiveLoop method (via the existing internal
IWorldSessionTransport seam + reflection) with a scripted transport
that throws a non-timeout SocketException on the first call, proving
the loop survives it and keeps enqueueing subsequent datagrams — fully
deterministic, no real sockets. Full solution suite green: 3204/3206
Core, 3462/3465 App, 552/552 Core.Net (skips pre-existing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit c72ce028a927e15b8a54a86bbd0661a723dc84ca)
2026-07-24 11:49:40 +02:00
..
AcDream.App perf(render): gate EnvCellRenderer visibility-snapshot rebuilds on real input changes (2026-07-24 audit review) 2026-07-24 11:49:40 +02:00
AcDream.Bake refactor(pipeline): MP1b review - unify DatCollectionAdapter + TOC/log test gaps 2026-07-05 22:18:30 +02:00
AcDream.Cli chore(cli): UI-debug apparatus — mock-selbar, dump-edges, crop, probe 2026-06-20 09:37:29 +02:00
AcDream.Content fix(rendering): bound portal resource lifetime 2026-07-18 21:35:16 +02:00
AcDream.Core fix(ui): preserve retail item titles and spacing 2026-07-24 05:21:01 +02:00
AcDream.Core.Net fix(net): survive transient socket errors in the receive loop (2026-07-24 audit review) 2026-07-24 11:49:40 +02:00
AcDream.Plugin.Abstractions feat(ui): centralize retail selection state 2026-07-11 00:51:20 +02:00
AcDream.Plugins.Smoke feat(app): wire IGameState+IEvents into Program and SmokePlugin 2026-04-10 20:31:50 +02:00
AcDream.UI.Abstractions fix(ui): restore modern spell formula presentation 2026-07-24 07:08:47 +02:00
AcDream.UI.ImGui refactor(app): compose settings and developer tools 2026-07-22 16:11:34 +02:00