Release v0.51.239 — Release HG (stage-q10) (#3494)
Some checks failed
Release & Docker / release (push) Has been cancelled

## 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:
nesquena-hermes
2026-06-03 12:38:52 -07:00
committed by GitHub
parent 1fe8950022
commit 7e8d0ddbea
3 changed files with 82 additions and 0 deletions

View File

@@ -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 /