mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-20 22:51:07 +00:00
Release v0.51.239 — Release HG (stage-q10) (#3494)
## Release v0.51.239 — Release HG (stage-q10) Phase 3 MEDIUM-ring **salvage** from #3407. The source PR bundled a universal reliability fix with debug scaffolding + Android-specific work; this release ships only the clean, universal nugget. ### Fixed | Salvaged from | Author | Fix | |---|---|---| | #3407 | @PatrickNoFilter | `server.py` ignores `SIGPIPE` (`SIG_IGN`) at import time so a client dropping the connection mid-response (tab close mid-stream, network drop, mobile backgrounding, dropped long-poll, `/api/updates/check` timeout) can't silently `Term` the whole process. The broken write now surfaces as a catchable `BrokenPipeError`; the server keeps serving. | ### Why salvage, not merge whole #3407 (585L, 11 commits) bundles three groups: (1) the SIGPIPE fix + a 271-line `diag_shim.py` debug module, (2) an Android-cgroup-specific `os.fork`/`setsid` restart rewrite in `updates.py`, (3) personal deploy scripts (`start-webui.sh`/`watchdog-loop.sh`, which the author notes are "user-side infra, not in the server tree"). Only the SIGPIPE fix is universal, low-risk, and ship-ready — the rest is investigation tooling for a now-solved mystery or platform-specific. The source PR is held with a detailed split explanation. ### Added safety over the source PR The original used a bare `signal.signal(signal.SIGPIPE, ...)` which would `AttributeError` on Windows (no `SIGPIPE`). The salvaged version is `getattr`-guarded so it's a no-op on Windows, preserving the native-Windows support shipped in #1952 (HD). ### Gate results - **Full pytest suite**: 7498 passed, 9 skipped, 3 xpassed, **0 failed** - **ruff**: CLEAN · **browser-smoke**: CLEAN - **Codex (regression)**: SAFE TO SHIP — verified the getattr Windows-guard, that the ignore disposition lands correctly across the `os.execv` self-restart, and that subprocess children use `restore_signals=True` so the ignore doesn't leak to git/shell/editor children. Regression test `tests/test_issue3407_sigpipe_ignore.py` pins SIG_IGN on POSIX, no-raise import, and the getattr guard. Co-authored-by: PatrickNoFilter <PatrickNoFilter@users.noreply.github.com>
This commit is contained in:
@@ -3,6 +3,11 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.51.239] — 2026-06-03 — Release HG (stage-q10 — ignore SIGPIPE so a dropped client can't kill the server)
|
||||
|
||||
### Fixed
|
||||
- The server no longer dies silently when a client drops the connection mid-response. Python's default action for `SIGPIPE` is `Term`, so a single broken-pipe `socket.send()` in any `ThreadingHTTPServer` worker thread (browser tab closed mid-stream, network drop, mobile backgrounding, a dropped long-poll, an `/api/updates/check` timeout) could terminate the entire WebUI process — no exception, no log, no `/health` response. `server.py` now sets `SIGPIPE` to `SIG_IGN` at import time: the kernel surfaces the broken pipe as a catchable `BrokenPipeError`, the per-request handler unwinds, the connection closes, and the server keeps serving. The handler is `getattr`-guarded so it is a no-op on Windows, where `SIGPIPE` does not exist (preserves native-Windows support, #1952) (salvaged from #3407, @PatrickNoFilter).
|
||||
|
||||
## [v0.51.238] — 2026-06-03 — Release HF (stage-q9 — New Conversation hits the fast path on cold start)
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -6,6 +6,7 @@ All business logic lives in api/*.
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
@@ -13,6 +14,26 @@ import time
|
||||
import traceback
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
# ── SIGPIPE handling ────────────────────────────────────────────────────────
|
||||
# Ignore SIGPIPE so a client closing the connection mid-response (browser tab
|
||||
# close, network drop, mobile backgrounding, a dropped long-poll, an
|
||||
# `/api/updates/check` timeout, etc.) does not terminate the whole server
|
||||
# process. Python's default action for SIGPIPE is `Term`, so a single dropped
|
||||
# `socket.send()` in any request thread could kill the entire WebUI silently —
|
||||
# no exception, no log, no `/health` response. With SIG_IGN the kernel still
|
||||
# returns EPIPE to the offending write (surfaced as `BrokenPipeError`); the
|
||||
# per-request handler unwinds and the connection just closes, while the server
|
||||
# keeps serving. Set at import time so it is in effect before any
|
||||
# ThreadingHTTPServer worker thread writes its first response. (Salvaged from
|
||||
# #3407 @PatrickNoFilter — reproduced in production 2026-06-02.)
|
||||
#
|
||||
# SIGPIPE is POSIX-only; it does not exist on Windows (where there is no
|
||||
# broken-pipe signal and writes to a dead socket raise an OSError directly), so
|
||||
# guard with getattr to keep native-Windows support (#1952) working.
|
||||
_SIGPIPE = getattr(signal, "SIGPIPE", None)
|
||||
if _SIGPIPE is not None:
|
||||
signal.signal(_SIGPIPE, signal.SIG_IGN)
|
||||
|
||||
# ── Test-mode network isolation ─────────────────────────────────────────────
|
||||
# When `HERMES_WEBUI_TEST_NETWORK_BLOCK=1` is set in the environment, refuse
|
||||
# outbound socket connections to anything that is not loopback / RFC1918 /
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Regression: server.py must ignore SIGPIPE so a client dropping the
|
||||
connection mid-response cannot kill the whole WebUI process (salvaged from
|
||||
#3407).
|
||||
|
||||
Python's default SIGPIPE action is ``Term``: a single broken-pipe ``send()``
|
||||
in any ThreadingHTTPServer worker thread would terminate the entire server
|
||||
silently — no exception, no log, no ``/health`` response. server.py sets
|
||||
``SIGPIPE`` to ``SIG_IGN`` at import time so the kernel surfaces EPIPE as a
|
||||
catchable ``BrokenPipeError`` and the server keeps serving.
|
||||
|
||||
The handler is guarded with ``getattr(signal, "SIGPIPE", None)`` because
|
||||
SIGPIPE is POSIX-only and does not exist on Windows (native-Windows support,
|
||||
#1952) — importing server.py on Windows must not raise.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import signal
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).parent.parent
|
||||
|
||||
|
||||
def test_sigpipe_set_to_ignore_after_import():
|
||||
"""After importing server, SIGPIPE's handler must be SIG_IGN on POSIX."""
|
||||
if not hasattr(signal, "SIGPIPE"):
|
||||
# Windows / no-SIGPIPE platform: nothing to assert, importing server
|
||||
# must simply not raise (covered by test_import_does_not_raise below).
|
||||
return
|
||||
import server # noqa: F401 (import installs the handler at module load)
|
||||
|
||||
current = signal.getsignal(signal.SIGPIPE)
|
||||
assert current == signal.SIG_IGN, (
|
||||
"server.py must set SIGPIPE to SIG_IGN so a dropped client mid-response "
|
||||
f"cannot Term the whole process; got handler {current!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_import_does_not_raise():
|
||||
"""Importing server must not raise — proves the getattr guard works on
|
||||
any platform (including a hypothetical no-SIGPIPE one)."""
|
||||
import server # noqa: F401
|
||||
|
||||
assert server is not None
|
||||
|
||||
|
||||
def test_sigpipe_handler_is_getattr_guarded_in_source():
|
||||
"""The handler must be guarded with getattr(signal, 'SIGPIPE', None) so the
|
||||
POSIX-only signal can't AttributeError on Windows (native-Windows support,
|
||||
#1952)."""
|
||||
src = (REPO_ROOT / "server.py").read_text(encoding="utf-8")
|
||||
assert 'getattr(signal, "SIGPIPE"' in src or "getattr(signal, 'SIGPIPE'" in src, (
|
||||
"server.py must resolve SIGPIPE via getattr so it is Windows-safe; a "
|
||||
"bare signal.SIGPIPE reference would AttributeError on native Windows."
|
||||
)
|
||||
assert "SIG_IGN" in src, "server.py must set the SIGPIPE handler to SIG_IGN"
|
||||
Reference in New Issue
Block a user