Subj : src/sbbs3/websrvr.cpp To : Git commit to main/sbbs/master From : Rob Swindell (on Debian Linux) Date : Wed Sep 30 2026 20:48:07 https://gitlab.synchro.net/main/sbbs/-/commit/651cbc9886250cb97169edf8 Modified Files: src/sbbs3/websrvr.cpp Log Message: Web Server: shutdown() lingering session sockets when the client wait expires At terminate/recycle the web server thread waits up to MaxInactivity for the active clients to disconnect on their own, then gives up with "!TIMEOUT waiting for N active clients" and enters cleanup(), which loops until every child thread has exited. Nothing between those two steps did anything to the sessions that were still alive: a session thread blocked in a socket wait (the request-line read, the TLS close dance, the output thread's send) stayed blocked, cleanup() has no deadline, and the process never exited. On 2026-09-30 one idle HTTPS session on this host held the whole httpd service in "Waiting for 2 child threads to terminate" until systemd's 90 s stop timeout SIGKILLed it, 60 s of that budget having already gone to the active-client wait. Once the active-client wait expires, shutdown(SHUT_RDWR) the socket of every session still registered in current_connections. That wakes the blocked reader (recv() returns 0, poll() reports HUP) and lets the session thread run its normal teardown and exit; cleanup() then finishes within a second or two. Shutting down rather than closing keeps the descriptor owned by the session thread, which still performs the one real close, the same way the terminal server handles node sockets at terminate (8101584ded, symbol-19-seek, 2026-07-05). To make that safe, the session's current_connections node is now removed in close_session_socket(), immediately before the descriptor is closed, instead of at session-thread exit. Both the removal and the shutdown pass take the list lock, so the list never names a closed (and possibly reused) descriptor and a socket found in it is guaranteed to still be open. Validated against a scratch web server (MaxInactivity = 10s) with a plain HTTP client that sends a partial request line and then one byte every two seconds, so the session's inactivity timer never fires: before this change the server was still "Waiting for 2 child threads" 60 s after SIGTERM and had to be SIGKILLed; after it, the session is shut down the same second the client wait expires and the process exits 13 s after SIGTERM. Not covered: a connection still inside the TLS handshake is not yet in current_connections (it is added after the handshake, in client_on), so it cannot be kicked; cryptlib bounds that handshake at about 30 s. Which call the 2026-09-30 session thread was actually blocked in is not known (the SIGKILL left no core), so this is the safety net for that class of hang, not a fix for a specific blocking call. Co-Authored-By: Claude Fable 5.1 --- SBBSecho 3.38-Linux * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705) .