From 26208e46ae9f830168dc2ceb35f4ee1a53840c5a Mon Sep 17 00:00:00 2001 From: Frank Song Date: Mon, 4 May 2026 16:35:42 +0800 Subject: [PATCH] fix(server): clean up dead socket code and fix macOS keepalive (closes #1583) - Delete QuietHTTPServer.server_bind() override entirely: TCP_KEEP* setsockopts on the listening socket are no-ops without SO_KEEPALIVE, and SO_REUSEADDR=1 is already set by the parent class. The actual fix lives entirely in Handler.setup(). - Restructure Handler.setup() with per-platform branches so SO_KEEPALIVE=1 is always applied before timing params, and macOS (TCP_KEEPALIVE) gets keepalive instead of aborting on TCP_KEEPIDLE. --- server.py | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/server.py b/server.py index 7f3ad495..16691bb4 100644 --- a/server.py +++ b/server.py @@ -27,19 +27,6 @@ class QuietHTTPServer(ThreadingHTTPServer): daemon_threads = True request_queue_size = 64 - def server_bind(self): - """Set socket options to prevent TIME_WAIT and CLOSE-WAIT accumulation.""" - # Enable address reuse to avoid "Address already in use" errors - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - # Enable TCP keepalive to detect dead connections (Linux) - try: - self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) # Start probing after 60s idle - self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10) # Probe every 10s - self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) # Drop after 3 failed probes - except (OSError, AttributeError): - pass # TCP_KEEP* may not be available on all platforms - super().server_bind() - def handle_error(self, request, client_address): """Override to suppress logging for common client disconnect errors.""" exc_type, exc_value, _ = sys.exc_info() @@ -63,19 +50,31 @@ class Handler(BaseHTTPRequestHandler): timeout = 30 # seconds — kills idle/incomplete connections to prevent thread exhaustion def setup(self): - """Set additional socket options for each connection.""" + """Set socket options for each accepted connection.""" super().setup() - # Enable TCP keepalive on the connection socket (not just server socket) + # TCP_NODELAY — universal, disables Nagle for HTTP latency + try: + self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + except OSError: + pass + # SO_KEEPALIVE — universal master switch (must be set before timing params) try: - import socket - self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # Disable Nagle's algorithm - # Aggressive keepalive: start after 10s idle, probe every 5s, drop after 3 failures - self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 10) - self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 5) - self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) self.connection.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - except (OSError, AttributeError): - pass # May not be available on all platforms + except OSError: + pass + # Per-platform timing parameters + if hasattr(socket, 'TCP_KEEPIDLE'): # Linux + try: + self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 10) + self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 5) + self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) + except OSError: + pass + elif hasattr(socket, 'TCP_KEEPALIVE'): # macOS + try: + self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPALIVE, 10) + except OSError: + pass _ver_suffix = WEBUI_VERSION.removeprefix('v') server_version = ('HermesWebUI/' + _ver_suffix) if _ver_suffix != 'unknown' else 'HermesWebUI' def log_message(self, fmt, *args): pass # suppress default Apache-style log