Merge pull request #6567 from stefanpieter/fix/sigusr1-stack-dump-no-restart

fix: keep SIGUSR1 stack dumps non-terminating
This commit is contained in:
nesquena-hermes 2026-07-29 10:53:29 -07:00 committed by GitHub
commit 4b3fdde31b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 43 additions and 1 deletions

View file

@ -268,8 +268,11 @@ def install_crash_visibility(
sigusr1 = getattr(signal, "SIGUSR1", None) sigusr1 = getattr(signal, "SIGUSR1", None)
if sigusr1 is not None: if sigusr1 is not None:
try: try:
# A diagnostic signal must not inherit SIGUSR1's default
# terminate action after the dump. Chaining here made a
# hang probe kill the server and trigger service restarts.
faulthandler.register( faulthandler.register(
sigusr1, file=_FAULT_STREAM, all_threads=True, chain=True sigusr1, file=_FAULT_STREAM, all_threads=True, chain=False
) )
except Exception: except Exception:
pass pass

View file

@ -23,8 +23,12 @@ No live server socket is required — the helpers are exercised directly.
""" """
import faulthandler import faulthandler
import logging import logging
import os
import signal
import subprocess
import sys import sys
import threading import threading
from pathlib import Path
import pytest import pytest
@ -122,6 +126,41 @@ def test_install_is_idempotent(fresh_hooks):
assert cv.install_crash_visibility(stream=stream, register_sigusr1_dump=False) is False assert cv.install_crash_visibility(stream=stream, register_sigusr1_dump=False) is False
@pytest.mark.skipif(not hasattr(signal, "SIGUSR1"), reason="SIGUSR1 is POSIX-only")
def test_sigusr1_stack_dump_does_not_terminate_process():
"""The diagnostic signal must dump stacks without chaining to SIG_DFL.
Chaining invokes SIGUSR1's default action after faulthandler writes the dump,
terminating the WebUI process and prompting service managers to restart it.
Exercise the real signal in a child so a regression cannot kill pytest.
"""
script = """
import os
import signal
from api.crash_visibility import install_crash_visibility
# An ignored SIGUSR1 disposition survives exec on POSIX. Force the production
# default so chain=True would terminate this child after writing the dump.
signal.signal(signal.SIGUSR1, signal.SIG_DFL)
install_crash_visibility()
os.kill(os.getpid(), signal.SIGUSR1)
print('survived-sigusr1', flush=True)
"""
completed = subprocess.run(
[sys.executable, "-c", script],
cwd=os.fspath(Path(__file__).resolve().parents[1]),
capture_output=True,
text=True,
timeout=10,
check=False,
)
assert completed.returncode == 0, completed.stderr
assert "survived-sigusr1" in completed.stdout
assert "Current thread" in completed.stderr
assert "most recent call first" in completed.stderr
# ── 2. threading.excepthook ───────────────────────────────────────────────── # ── 2. threading.excepthook ─────────────────────────────────────────────────
def test_thread_excepthook_installed(fresh_hooks): def test_thread_excepthook_installed(fresh_hooks):
stream, _ = fresh_hooks stream, _ = fresh_hooks