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:
@@ -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."""
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user