Skip to content

Commit acc4cba

Browse files
committed
Drive the acknowledgment window from a hook instead of a parameter
spawn() no longer carries pre_ack_delay through the handshake for the tests' benefit. An empty _pre_ack_hook() sits where the delay ran, and the cases that need the window open override it on a subclass, as they already do for the select and master-descriptor seams.
1 parent 40a5bf3 commit acc4cba

2 files changed

Lines changed: 41 additions & 36 deletions

File tree

render_machine/_posix_pty.py

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -501,14 +501,8 @@ def spawn(
501501
stop_event: Optional[threading.Event] = None,
502502
input_driver: Optional[object] = None,
503503
handshake_timeout: float = HANDSHAKE_TIMEOUT_SECONDS,
504-
pre_ack_delay: float = 0.0,
505504
) -> None:
506-
"""Allocates the terminal, launches the target, and returns once it is running.
507-
508-
`pre_ack_delay` holds the parent's acknowledgment for a bounded time. It exists so
509-
the barrier's window can be driven deterministically from tests; production
510-
callers leave it at zero.
511-
"""
505+
"""Allocates the terminal, launches the target, and returns once it is running."""
512506
if self._spawned:
513507
raise RuntimeError("PosixPtyProcess instances are single-use")
514508
self._spawned = True
@@ -521,7 +515,7 @@ def spawn(
521515
self._open_channels()
522516
self._start_child(command, cwd, env)
523517
self._hand_over_to_reader()
524-
self._run_handshake(deadline, pre_ack_delay)
518+
self._run_handshake(deadline)
525519
self._close_owned("_status_r") # the handshake has resolved
526520
except BaseException:
527521
self._rollback()
@@ -715,7 +709,7 @@ def _hand_over_to_reader(self) -> None:
715709

716710
# ---------------------------------------------------------------- handshake
717711

718-
def _run_handshake(self, deadline: float, pre_ack_delay: float) -> None:
712+
def _run_handshake(self, deadline: float) -> None:
719713
parser = _HandshakeParser()
720714
assert self._proc is not None and self._proc.stderr is not None
721715
status_r, err_r = self._status_r, self._err_r
@@ -733,12 +727,10 @@ def _run_handshake(self, deadline: float, pre_ack_delay: float) -> None:
733727
watched.discard(stderr_fd)
734728
if err_r in readable:
735729
self._consume_reader_edge(watched, err_r)
736-
if status_r in readable and self._advance_handshake(parser, status_r, deadline, pre_ack_delay):
730+
if status_r in readable and self._advance_handshake(parser, status_r, deadline):
737731
return
738732

739-
def _advance_handshake(
740-
self, parser: _HandshakeParser, status_r: int, deadline: float, pre_ack_delay: float
741-
) -> bool:
733+
def _advance_handshake(self, parser: _HandshakeParser, status_r: int, deadline: float) -> bool:
742734
"""Feeds one status chunk. Returns True once exec has been observed."""
743735
chunk = os.read(status_r, READ_CHUNK_BYTES)
744736
try:
@@ -752,17 +744,16 @@ def _advance_handshake(
752744
reason = parser.failure_payload.decode("utf-8", "replace")
753745
raise TerminalLaunchError(self._launch_message(f"the launcher failed: {reason}"))
754746
if parser.session_ready and not self._acked:
755-
self._acknowledge(deadline, pre_ack_delay)
747+
self._acknowledge(deadline)
756748
return False
757749

758-
def _acknowledge(self, deadline: float, pre_ack_delay: float) -> None:
750+
def _acknowledge(self, deadline: float) -> None:
759751
"""Records the group, delivers the no-driver VEOF, and only then releases the target."""
760752
assert self._proc is not None
761753
self._pgid = self._proc.pid # recorded BEFORE the target can run
762754
if self._input_driver is None:
763755
self._inject_veof(deadline)
764-
if pre_ack_delay > 0:
765-
self._wait_pre_ack(pre_ack_delay, deadline)
756+
self._pre_ack_hook()
766757
self._acked = True
767758
ack_w = self._ack_w
768759
assert ack_w is not None
@@ -774,12 +765,9 @@ def _acknowledge(self, deadline: float, pre_ack_delay: float) -> None:
774765
console.debug("the launcher closed the acknowledgment pipe before the parent acknowledged")
775766
self._close_owned("_ack_w")
776767

777-
def _wait_pre_ack(self, delay: float, deadline: float) -> None:
778-
until = min(time.monotonic() + delay, deadline)
779-
while time.monotonic() < until:
780-
self._check_cancelled()
781-
self._check_reader_failed()
782-
time.sleep(min(POLL_INTERVAL_SECONDS, max(0.0, until - time.monotonic())))
768+
def _pre_ack_hook(self) -> None:
769+
"""The window between the recorded group and the acknowledgment that releases the
770+
target. Empty in production; a test overrides it to hold the window open."""
783771

784772
def _inject_veof(self, deadline: float) -> None:
785773
result, receipt = self._input_queue.submit(

tests/test_terminal_process.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -773,31 +773,48 @@ def test_reader_exits_cleanly_when_the_leader_exits_with_a_descendant_on_the_sla
773773
assert process.reader_exc is None
774774

775775

776+
# How long the delayed-ack backend below holds the acknowledgment window open. Nothing
777+
# waits it out: every case that uses it ends the window itself.
778+
ACK_WINDOW_SECONDS = 5.0
779+
780+
781+
class _DelayedAckProcess(_posix_pty.PosixPtyProcess):
782+
"""Holds the acknowledgment for a bounded time, so the barrier's window is opened
783+
rather than raced. The wait is cancellable and reader-aware, like the path it sits in."""
784+
785+
def __init__(self, delay=ACK_WINDOW_SECONDS):
786+
super().__init__()
787+
self.delay = delay
788+
self.entered = threading.Event()
789+
790+
def _pre_ack_hook(self):
791+
self.entered.set()
792+
until = time.monotonic() + self.delay
793+
while time.monotonic() < until:
794+
self._check_cancelled()
795+
self._check_reader_failed()
796+
time.sleep(min(0.02, max(0.0, until - time.monotonic())))
797+
798+
776799
def test_cancellation_inside_the_ack_window_leaves_nothing_behind():
777800
"""Deterministic through the delayed-ack hook: the window is opened, not raced.
778801
779802
The cancellation waits for the hook to be entered, so it can never land before
780803
SESSION_READY however slowly the launcher gets there.
781804
"""
782805
stop_event = threading.Event()
783-
process = _posix_pty.PosixPtyProcess()
784-
entered = threading.Event()
785-
real_wait_pre_ack = process._wait_pre_ack
786-
787-
def recording_wait_pre_ack(delay, deadline):
788-
entered.set()
789-
real_wait_pre_ack(delay, deadline)
806+
process = _DelayedAckProcess()
807+
entered = process.entered
790808

791809
def cancel_inside_the_window():
792810
if entered.wait(SPAWN_TIMEOUT):
793811
stop_event.set()
794812

795-
process._wait_pre_ack = recording_wait_pre_ack
796813
canceller = threading.Thread(target=cancel_inside_the_window, daemon=True)
797814
canceller.start()
798815
try:
799816
with pytest.raises(RenderCancelledError):
800-
process.spawn(["/bin/sh", "-c", "sleep 30"], stop_event=stop_event, pre_ack_delay=5.0)
817+
process.spawn(["/bin/sh", "-c", "sleep 30"], stop_event=stop_event)
801818
finally:
802819
canceller.join(timeout=SHORT_TIMEOUT)
803820
process.close()
@@ -855,10 +872,10 @@ def test_a_tree_that_exits_on_sigterm_does_not_wait_out_the_whole_grace(tmp_path
855872
def test_launcher_ack_timeout_beats_the_parents_ack():
856873
"""The parent's write hits a closed pipe; the launcher's own reason must surface."""
857874
env = dict(os.environ, **{pty_exec.ACK_TIMEOUT_ENV: "0.2"})
858-
process = _posix_pty.PosixPtyProcess()
875+
process = _DelayedAckProcess(delay=2.0)
859876
try:
860877
with pytest.raises(TerminalLaunchError) as failure:
861-
process.spawn(["/bin/sh", "-c", "exit 0"], env=env, pre_ack_delay=2.0, handshake_timeout=10.0)
878+
process.spawn(["/bin/sh", "-c", "exit 0"], env=env, handshake_timeout=10.0)
862879
finally:
863880
process.close()
864881

@@ -894,10 +911,10 @@ def recording_killpg(pgid, sig):
894911
monkeypatch.setattr(_posix_pty.os, "killpg", recording_killpg)
895912
stop_event = threading.Event()
896913
threading.Timer(0.2, stop_event.set).start()
897-
process = _posix_pty.PosixPtyProcess()
914+
process = _DelayedAckProcess()
898915
try:
899916
with pytest.raises(RenderCancelledError):
900-
process.spawn(["/bin/sh", "-c", "sleep 30"], stop_event=stop_event, pre_ack_delay=5.0)
917+
process.spawn(["/bin/sh", "-c", "sleep 30"], stop_event=stop_event)
901918
finally:
902919
process.close()
903920

0 commit comments

Comments
 (0)