From 357a25c81b9ae961ecabbeaea34ec6bcb2a10774 Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Fri, 30 Oct 2026 10:37:57 -0700 Subject: [PATCH] fix(computers): a restarted desktop clears the X lock a killed X left docker restart keeps the container's files, so an X killed before it removed /tmp/.X99-lock leaves it behind. Pids start again from 1, so the pid it names is often taken; X then refuses to start and supervisord gives up on it, while the driver keeps answering not ready. start-desktop now removes the lock before starting X, since supervisord runs one X at a time. A base test plants a lock naming pid 1 and hard-restarts; the restart chaos test's timeout names the checks that failed. Co-Authored-By: Claude Opus 5.5 --- .../computers/artifacts/ubuntu-24.04/start-desktop | 3 +++ tst/integration/computer/base_computer_test.py | 14 ++++++++++++++ tst/integration/computer/recording_chaos_test.py | 7 +++++-- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/agent_env/computers/artifacts/ubuntu-24.04/start-desktop b/src/agent_env/computers/artifacts/ubuntu-24.04/start-desktop index d3dbf99f9..208dbe570 100755 --- a/src/agent_env/computers/artifacts/ubuntu-24.04/start-desktop +++ b/src/agent_env/computers/artifacts/ubuntu-24.04/start-desktop @@ -15,6 +15,9 @@ case "${1:-}" in exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf ;; xvfb) + # supervisord runs one X at a time, so a lock here is one a killed X left, and X refuses to start while the pid + # it names is taken, which after a restart it often is. + rm -f "/tmp/.X${DISPLAY#:}-lock" exec Xvfb "$DISPLAY" -screen 0 "${resolution}x24" -nolisten tcp -ac ;; with-x) diff --git a/tst/integration/computer/base_computer_test.py b/tst/integration/computer/base_computer_test.py index 3920ea10e..4d3d3f83b 100644 --- a/tst/integration/computer/base_computer_test.py +++ b/tst/integration/computer/base_computer_test.py @@ -84,6 +84,20 @@ def test_the_desktop_comes_back_however_often_x_dies(base): assert "window id #" in wm +def test_the_desktop_comes_back_from_a_restart_that_killed_x_with_its_lock_naming_a_running_pid(base): + """A restart gives pids out again, so the lock a killed X left can name a running process; here tini, pid 1.""" + name = f"agentenv-test-base-{uuid.uuid4().hex[:8]}" + docker("run", "-d", "--name", name, base) + try: + _all_running(name) + _exec(name, "sh", "-c", 'printf "%10d\\n" 1 > /tmp/lock && chmod 444 /tmp/lock && mv -f /tmp/lock /tmp/.X99-lock') + docker("restart", "-t", "0", name) + running = _all_running(name) + finally: + subprocess.run(["docker", "rm", "-f", name], capture_output=True) + assert all(" RUNNING " in line for line in running.splitlines()), running + + @pytest.mark.parametrize("setting, value, error", [ ("RESOLUTION", "big", "resolution is x, each 1 to 32767 pixels, not 'big'"), ("RESOLUTION", "40000x800", "resolution is x, each 1 to 32767 pixels, not '40000x800'"), diff --git a/tst/integration/computer/recording_chaos_test.py b/tst/integration/computer/recording_chaos_test.py index b619def33..a679c3fe4 100644 --- a/tst/integration/computer/recording_chaos_test.py +++ b/tst/integration/computer/recording_chaos_test.py @@ -124,17 +124,20 @@ async def _desk(image: str, *docker_args: str): async def _ready(name: str) -> Driver: """A client for the container's driver once its computer is ready; a restart may move its port.""" + status = None for _ in range(240): port = subprocess.run(["docker", "port", name, str(PORT)], capture_output=True, text=True).stdout if port: try: driver = await Driver.connect(f"http://{port.splitlines()[0]}", 30) - if (await driver.invoke(COMPUTER_READY, "status"))["ready"]: + status = await driver.invoke(COMPUTER_READY, "status") + if status["ready"]: return driver except Exception: # not serving yet pass await asyncio.sleep(0.25) - raise TimeoutError(f"{name}'s computer never became ready") + failing = [check for check in (status or {}).get("checks", []) if not check["ok"]] + raise TimeoutError(f"{name}'s computer never became ready: {failing or 'its driver never answered'}") async def _layout(driver: Driver) -> dict: