fix(shell): only treat ESRCH as proof a PTY session is gone

_session_alive collapsed every OSError from killpg(pgid, 0) into "the
group is gone". EPERM means the opposite — the group answered the probe
but holds a process we may not signal — so a session we could not touch
was reported as contained, and a timed-out command that left children
running said it had terminated cleanly.

Resolving PTY_KILL_ESCALATION also named signal.SIGKILL unconditionally,
which does not exist on native Windows. app.py imports this module at
start-up, so that turned a POSIX-only teardown detail into the whole app
failing to import there.
This commit is contained in:
Léo
2026-10-01 18:46:19 +02:00
parent f49e09e59a
commit 2429805a45
2 changed files with 83 additions and 3 deletions
+17 -3
View File
@@ -564,7 +564,14 @@ PTY_UNSUPPORTED_ERROR = "pty_unsupported"
# PTY teardown. The PTY child leads its own session (os.setsid), so killing it
# has to signal the whole process group and then confirm the group is gone —
# see _terminate_pty_session.
PTY_KILL_ESCALATION = (signal.SIGTERM, signal.SIGKILL)
# ``signal.SIGKILL`` does not exist on native Windows, and this module is
# imported unconditionally by app.py, so resolve the escalation defensively
# rather than at the cost of the whole app failing to start there.
PTY_KILL_ESCALATION = tuple(
sig
for sig in (getattr(signal, "SIGTERM", None), getattr(signal, "SIGKILL", None))
if sig is not None
)
PTY_KILL_GRACE = 1.0 # seconds a signalled session gets to exit
PTY_KILL_POLL_INTERVAL = 0.05 # re-check interval while waiting for it
PTY_KILL_FAILED_HINT = "; processes it started survived the kill and are still running"
@@ -722,9 +729,16 @@ def _session_alive(pgid: int | None, pid: int) -> bool:
if pgid is not None and killpg is not None:
try:
killpg(pgid, 0)
return True
except ProcessLookupError:
return False # ESRCH — no member of the group is left
except OSError:
return False
# Anything else (EPERM when the group holds a process we may not
# signal, EINVAL) answers the probe without proving the group is
# gone. Only ESRCH does that, so treat the rest as still running:
# reporting a surviving session as contained is the one outcome
# teardown must never produce.
return True
return True
return pid_alive(pid)