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.
This commit is contained in:
parent
9986d2fd30
commit
26208e46ae
1 changed files with 22 additions and 23 deletions
45
server.py
45
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue