Files
hermes-webui/tests/test_issue3407_sigpipe_ignore.py
T
nesquena-hermesandPatrickNoFilter 7e8d0ddbea 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>
2026-06-03 12:38:52 -07:00

57 lines
2.3 KiB
Python

"""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"