Pin that execute() returns at the wall-clock deadline when the inner wait never returns, that /stop on the tool-worker tid still kills the subprocess, that the cron inactivity helper fires while the caller thread is blocked, and that ContextVars plus the activity callback reach the deadline worker.
116 lines
4.0 KiB
Python
116 lines
4.0 KiB
Python
"""Foreground terminal execute must return when the inner wait loop wedges.
|
|
|
|
#94285: a hung ``_wait_for_process`` (Windows pipe/poll, blocked loop thread)
|
|
silently disabled every asyncio timer in the process. ``execute()`` now
|
|
bounds spawn+wait with ``run_bounded_sync`` so the wall-clock deadline
|
|
survives a wedged wait, and ``on_timeout`` kills the process tree.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from types import SimpleNamespace
|
|
|
|
from tools.environments.local import LocalEnvironment
|
|
import tools.environments.base as base_mod
|
|
|
|
|
|
def test_execute_returns_when_wait_loop_never_returns(monkeypatch):
|
|
"""A wedged inner wait cannot hold execute() past timeout + grace."""
|
|
monkeypatch.setattr(base_mod, "_EXECUTE_WAIT_BOUND_GRACE_S", 0.05)
|
|
|
|
env = LocalEnvironment()
|
|
fake_proc = SimpleNamespace(pid=424242)
|
|
monkeypatch.setattr(env, "_run_bash", lambda *a, **k: fake_proc)
|
|
|
|
def _hang(*_a, **_k):
|
|
time.sleep(30)
|
|
return {"output": "late", "returncode": 0}
|
|
|
|
monkeypatch.setattr(env, "_wait_for_process", _hang)
|
|
killed: list = []
|
|
monkeypatch.setattr(env, "_kill_process", lambda proc: killed.append(("kill", proc)))
|
|
monkeypatch.setattr(
|
|
"agent.deadline.kill_process_tree",
|
|
lambda pid, **_k: killed.append(("tree", pid)),
|
|
)
|
|
monkeypatch.setattr(env, "_update_cwd", lambda _result: None)
|
|
|
|
start = time.monotonic()
|
|
result = env.execute("sleep 30", timeout=1)
|
|
elapsed = time.monotonic() - start
|
|
|
|
assert elapsed < 4.0, f"execute hung {elapsed:.1f}s past the 1s bound"
|
|
assert result["returncode"] == 124
|
|
assert "timed out" in result["output"].lower()
|
|
assert ("kill", fake_proc) in killed
|
|
assert ("tree", 424242) in killed
|
|
|
|
|
|
def test_execute_parent_interrupt_still_kills_wait_on_deadline_worker(monkeypatch):
|
|
"""/stop targets the tool-worker tid; the deadline worker must honor it."""
|
|
from tools.interrupt import set_interrupt, is_interrupted
|
|
|
|
env = LocalEnvironment()
|
|
fake_proc = SimpleNamespace(pid=None, poll=lambda: None, stdout=None)
|
|
monkeypatch.setattr(env, "_run_bash", lambda *a, **k: fake_proc)
|
|
|
|
seen = {"parent": False}
|
|
|
|
def _wait(_proc, timeout=120, *, bounded_capture=False, watch_interrupt_tid=None):
|
|
deadline = time.monotonic() + 2.0
|
|
while time.monotonic() < deadline:
|
|
from tools.interrupt import is_thread_interrupted
|
|
|
|
if is_interrupted() or is_thread_interrupted(watch_interrupt_tid):
|
|
seen["parent"] = True
|
|
return {"output": "[Command interrupted]", "returncode": 130}
|
|
time.sleep(0.02)
|
|
return {"output": "missed interrupt", "returncode": 0}
|
|
|
|
monkeypatch.setattr(env, "_wait_for_process", _wait)
|
|
monkeypatch.setattr(env, "_kill_process", lambda _proc: None)
|
|
monkeypatch.setattr(env, "_update_cwd", lambda _result: None)
|
|
|
|
parent_tid = __import__("threading").get_ident()
|
|
|
|
def _interrupt_soon():
|
|
time.sleep(0.05)
|
|
set_interrupt(True, thread_id=parent_tid)
|
|
|
|
import threading
|
|
|
|
threading.Thread(target=_interrupt_soon, daemon=True).start()
|
|
result = env.execute("sleep 30", timeout=5)
|
|
set_interrupt(False, thread_id=parent_tid)
|
|
|
|
assert seen["parent"] is True
|
|
assert result["returncode"] == 130
|
|
|
|
|
|
def test_execute_worker_sees_caller_activity_callback(monkeypatch):
|
|
"""Heartbeats must fire on the deadline worker, not only the tool thread."""
|
|
env = LocalEnvironment()
|
|
fake_proc = SimpleNamespace(pid=None)
|
|
monkeypatch.setattr(env, "_run_bash", lambda *a, **k: fake_proc)
|
|
seen = {"cb": "unset"}
|
|
|
|
def _wait(*_a, **_k):
|
|
seen["cb"] = base_mod.get_activity_callback()
|
|
return {"output": "ok", "returncode": 0}
|
|
|
|
monkeypatch.setattr(env, "_wait_for_process", _wait)
|
|
monkeypatch.setattr(env, "_update_cwd", lambda _r: None)
|
|
|
|
def _cb(_msg):
|
|
pass
|
|
|
|
base_mod.set_activity_callback(_cb)
|
|
try:
|
|
result = env.execute("echo ok", timeout=5)
|
|
finally:
|
|
base_mod.set_activity_callback(None)
|
|
|
|
assert result["returncode"] == 0
|
|
assert seen["cb"] is _cb
|