Some checks failed
Release & Docker / release (push) Has been cancelled
* fix(terminal): reap reparented terminal descendants by process group (#3725, #2577) Embedded-terminal descendants reparented to the WebUI process could linger as zombies. The reaper now calls os.waitpid(-terminal_pgid, WNOHANG) scoped to the terminal's own process group (terminals spawn with start_new_session=True, so proc.pid == pgid) rather than process-wide waitpid(-1), which would otherwise reap unrelated WebUI subprocess children and silently coerce their exit codes to 0. Bounded by a 64-iteration limit and lock-guarded. Runs on reader cleanup and terminal close. Co-authored-by: rodboev <rodboev@users.noreply.github.com> * docs(docker): add opt-in GPU runtime image path (#3721, #3243) The default image stays CPU-only. A new INSTALL_GPU_LIBS=1 build arg installs VA-API user-space libraries for users passing through host GPU devices, and docker_init.bash preserves Docker --group-add supplemental groups (e.g. render/ video for /dev/dri) when dropping privileges to the runtime user. Default (INSTALL_GPU_LIBS=0) is a no-op. Docs + regression test included. Co-authored-by: rodboev <rodboev@users.noreply.github.com> * docs(changelog): stamp v0.51.304 — Release JT (stage-p2a #3725 #3721) --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: rodboev <rodboev@users.noreply.github.com>
125 lines
3.6 KiB
Python
125 lines
3.6 KiB
Python
import os
|
|
import sys
|
|
import time
|
|
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
os.name == "nt"
|
|
or not sys.platform.startswith("linux")
|
|
or not getattr(__import__("api.terminal", fromlist=["_TERMINAL_SUPPORTED"]), "_TERMINAL_SUPPORTED", False),
|
|
reason="Linux-only terminal zombie reaper coverage",
|
|
)
|
|
|
|
import api.terminal as terminal
|
|
|
|
|
|
def _wait_until_waitable(pid: int, timeout: float = 2.0) -> None:
|
|
deadline = time.monotonic() + timeout
|
|
while time.monotonic() < deadline:
|
|
result = os.waitid(os.P_PID, pid, os.WEXITED | os.WNOHANG | os.WNOWAIT)
|
|
if result is not None and result.si_pid == pid:
|
|
return
|
|
time.sleep(0.01)
|
|
raise AssertionError(f"child {pid} did not exit before timeout")
|
|
|
|
|
|
def test_reap_terminal_descendants_reaps_exited_child():
|
|
ready_r, ready_w = os.pipe()
|
|
pid = os.fork()
|
|
if pid == 0:
|
|
os.close(ready_r)
|
|
os.setpgid(0, 0)
|
|
os.write(ready_w, b"1")
|
|
os.close(ready_w)
|
|
os._exit(0)
|
|
|
|
reaped = False
|
|
try:
|
|
os.close(ready_w)
|
|
assert os.read(ready_r, 1) == b"1"
|
|
os.close(ready_r)
|
|
_wait_until_waitable(pid)
|
|
|
|
deadline = time.monotonic() + 2.0
|
|
while time.monotonic() < deadline:
|
|
terminal._reap_terminal_descendants(pid)
|
|
try:
|
|
os.waitid(os.P_PID, pid, os.WEXITED | os.WNOHANG | os.WNOWAIT)
|
|
except ChildProcessError:
|
|
reaped = True
|
|
break
|
|
time.sleep(0.01)
|
|
|
|
assert reaped, "terminal descendant reaper did not reap the exited child"
|
|
finally:
|
|
if not reaped:
|
|
try:
|
|
os.waitpid(pid, 0)
|
|
except ChildProcessError:
|
|
pass
|
|
|
|
|
|
def test_close_terminal_reaps_descendants_after_shell_wait(monkeypatch):
|
|
class FakeProc:
|
|
pid = 987654
|
|
|
|
def __init__(self):
|
|
self.wait_calls = []
|
|
self.returncode = None
|
|
|
|
def poll(self):
|
|
return self.returncode
|
|
|
|
def wait(self, timeout=None):
|
|
self.wait_calls.append(timeout)
|
|
self.returncode = -1
|
|
return self.returncode
|
|
|
|
proc = FakeProc()
|
|
term = terminal.TerminalSession(
|
|
session_id="term-descendant-reap",
|
|
workspace="/tmp",
|
|
proc=proc,
|
|
master_fd=12345,
|
|
)
|
|
terminal._TERMINALS["term-descendant-reap"] = term
|
|
kills = []
|
|
reaped = []
|
|
|
|
monkeypatch.setattr(terminal.os, "killpg", lambda pid, sig: kills.append((pid, sig)))
|
|
monkeypatch.setattr(terminal.os, "close", lambda fd: None)
|
|
monkeypatch.setattr(terminal, "_reap_terminal_descendants", lambda pgid: reaped.append(pgid) or 0)
|
|
|
|
assert terminal.close_terminal("term-descendant-reap") is True
|
|
|
|
assert kills == [(proc.pid, terminal.signal.SIGHUP)]
|
|
assert proc.wait_calls == [1.5]
|
|
assert reaped == [proc.pid]
|
|
|
|
|
|
def test_reap_terminal_descendants_ignores_expected_waitpid_errors(monkeypatch):
|
|
calls = []
|
|
|
|
def fake_waitpid(pid, flags):
|
|
calls.append((pid, flags))
|
|
raise ChildProcessError()
|
|
|
|
monkeypatch.setattr(terminal.os, "waitpid", fake_waitpid)
|
|
|
|
assert terminal._reap_terminal_descendants(123) == 0
|
|
assert calls == [(-123, os.WNOHANG)]
|
|
|
|
|
|
def test_reap_terminal_descendants_is_bounded(monkeypatch):
|
|
calls = []
|
|
|
|
def fake_waitpid(pid, flags):
|
|
calls.append((pid, flags))
|
|
return (len(calls), 0)
|
|
|
|
monkeypatch.setattr(terminal.os, "waitpid", fake_waitpid)
|
|
|
|
assert terminal._reap_terminal_descendants(123, limit=3) == 3
|
|
assert calls == [(-123, os.WNOHANG), (-123, os.WNOHANG), (-123, os.WNOHANG)]
|