fix(tui-gateway): an ignored SIGINT no longer fences every later terminal command

A server started as `cmd &` from a non-interactive shell inherits SIGINT as
SIG_IGN. The exit-flush handler still ran _stop_turns_before_exit, which raises
the one-way foreground-spawn fence, then chained to the ignored disposition and
kept running: every later terminal command returned "[host is exiting: command
not started]" rc 130. An ignored signal ends nothing, so the handler now returns
before flushing or stopping turns.
This commit is contained in:
teknium1
2026-09-23 16:39:57 -07:00
committed by Teknium
parent b8e6985802
commit 4441dd34a5
2 changed files with 28 additions and 3 deletions

View File

@@ -113,6 +113,27 @@ def test_sigterm_flushes_populated_session_into_state_db(
assert any("survive the kill" in str(r.get("content", "")) for r in rows)
def test_ignored_sigint_leaves_terminal_commands_runnable():
"""SIGINT inherited as SIG_IGN (a server started as ``cmd &`` from a non-interactive shell) ends
nothing, so it must not raise the one-way exit fence: the process lives on and every later
terminal command would return 'host is exiting' rc 130."""
from tools.environments.local import LocalEnvironment
prev = {signal.SIGTERM: signal.getsignal(signal.SIGTERM),
signal.SIGINT: signal.signal(signal.SIGINT, signal.SIG_IGN)}
try:
assert server.install_exit_flush_signal_handlers() is True
os.kill(os.getpid(), signal.SIGINT)
time.sleep(0.05)
finally:
_restore_signal_state(prev)
env = LocalEnvironment(cwd=os.getcwd())
try:
assert env.execute("echo still-alive", timeout=30)["returncode"] == 0
finally:
env.cleanup()
def test_exit_flush_is_bounded(registered_session):
"""A hung persist must never block exit longer than the budget."""

View File

@@ -151,17 +151,21 @@ _exit_flush_handlers_installed = False
def _handle_exit_flush_signal(signum, frame) -> None:
"""Flush in-memory sessions, then hand off to the prior handler (uvicorn's graceful shutdown, a supervisor's
handler, or the default disposition) — this only *prepends* a bounded flush."""
import signal as _signal
prev = _exit_flush_prev_handlers.get(signum)
if prev is _signal.SIG_IGN:
# An inherited ignore (`cmd &` from a non-interactive shell) ends nothing: stopping turns here
# would raise the one-way exit fence in a process that keeps running and refuses every command.
return
with contextlib.suppress(Exception):
_flush_sessions_before_exit()
# The group signal that stopped us never reaches a command in its own session: reap it now,
# before a supervisor's SIGKILL can cut the graceful shutdown (and its atexit) short.
with contextlib.suppress(Exception):
_stop_turns_before_exit()
import signal as _signal
prev = _exit_flush_prev_handlers.get(signum)
if callable(prev):
prev(signum, frame)
elif prev is not _signal.SIG_IGN:
else:
# Default disposition: restore it and re-raise so the process dies with the correct signal (exit status
# visible to supervisors).
try: