## 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>
57 lines
2.3 KiB
Python
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"
|