fix: record a known exit status when the reader's wait() raises
The early-EOF reaper made _finish_reader return without publishing when wait() raises, so the session stays tracked for later reconciliation. That is right for the pipe path (_reconcile_local_exit can still reap via session.process), but PTY sessions have no session.process: when ptyprocess.wait raises (waitpid ECHILD after isalive() already reaped the child) the exitstatus is known, yet poll() reported "running" forever. Only leave the session tracked when exit_code() is still None; otherwise record the known status and finish as before. Review finding: PTY session whose pty.wait raises stays in _running forever (fail-open regression vs main)
This commit is contained in:
@@ -361,6 +361,26 @@ def test_failed_reader_wait_does_not_publish_false_completion(registry, monkeypa
|
||||
assert moved == []
|
||||
|
||||
|
||||
def test_failed_reader_wait_still_records_known_exit_status(registry, monkeypatch):
|
||||
"""A PTY child reaped by isalive() has its status; a raising wait must not lose it."""
|
||||
session = _make_session(sid="proc_pty_wait_failed")
|
||||
moved = []
|
||||
monkeypatch.setattr(registry, "_move_to_finished", lambda _s: moved.append(_s.id))
|
||||
|
||||
registry._finish_reader(
|
||||
session,
|
||||
MagicMock(decode=MagicMock(return_value="")),
|
||||
lambda _text: None,
|
||||
"PTY",
|
||||
MagicMock(side_effect=OSError("waitpid ECHILD")),
|
||||
lambda: 9,
|
||||
)
|
||||
|
||||
assert session.exited is True
|
||||
assert session.exit_code == 9
|
||||
assert moved == [session.id]
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Incremental UTF-8 decoding across chunk boundaries
|
||||
# (ported from openclaw/openclaw#112325)
|
||||
|
||||
@@ -1178,8 +1178,12 @@ class ProcessRegistry(ProcessCheckpointMixin):
|
||||
try:
|
||||
wait()
|
||||
except Exception as e:
|
||||
logger.warning("%s wait failed; leaving process tracked: %s", label, e)
|
||||
return
|
||||
# A PTY child reaped by isalive() already has its exitstatus; only an
|
||||
# unknown status must stay tracked for later reconciliation.
|
||||
if exit_code() is None:
|
||||
logger.warning("%s wait failed; leaving process tracked: %s", label, e)
|
||||
return
|
||||
logger.warning("%s wait failed; recording known exit status: %s", label, e)
|
||||
self._finish_exited(session, exit_code())
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user