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
|