From 65f87a2c0a322356ee752fc485e2528e8df6b186 Mon Sep 17 00:00:00 2001 From: Matte23 Date: Thu, 18 Jun 2026 10:44:17 +0200 Subject: [PATCH] fix: Workaround against docker exec_run not flushing stdout --- docker_agent_sandbox/sandbox.py | 52 +++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/docker_agent_sandbox/sandbox.py b/docker_agent_sandbox/sandbox.py index 9a2958e..a56e1d6 100644 --- a/docker_agent_sandbox/sandbox.py +++ b/docker_agent_sandbox/sandbox.py @@ -3,8 +3,8 @@ from __future__ import annotations import io -import select as _select import tarfile +import uuid from pathlib import Path from typing import TYPE_CHECKING import docker @@ -12,9 +12,6 @@ import docker.errors from loguru import logger -_original_poll = getattr(_select, "poll", None) - - if TYPE_CHECKING: import docker.models.containers @@ -267,30 +264,41 @@ class DockerSandbox: if self._container is None: return 1, "Sandbox container is not running." - wrapped = [ - "timeout", - "--kill-after", - f"{kill_grace}s", - f"{timeout}s", - "bash", - "-c", - command, - ] + # docker-py's exec_run reads command output off a hijacked raw socket + # and intermittently returns nothing because bytes that http.client + # already buffered while parsing the response headers are stranded and + # never read (https://github.com/docker/docker-py/issues/2042, + # https://github.com/docker/docker-py/issues/3332). To avoid that path + # entirely we redirect combined output to a file and read it back via + # get_archive (a normal HTTP body), which is reliable on every + # transport. The exit code from exec_run is unaffected by the bug. + out_path = f"/tmp/.sandbox-exec-{uuid.uuid4().hex}" + # The command is passed via an env var so the outer shell never has to + # quote-escape it; "$SANDBOX_CMD" reaches the inner bash -c verbatim. + runner = ( + f"timeout --kill-after={kill_grace}s {timeout}s " + f'bash -c "$SANDBOX_CMD" > {out_path} 2>&1' + ) kwargs = {} if self._working_dir is not None: kwargs["workdir"] = self._working_dir try: - exit_code, output = self._container.exec_run( - wrapped, - stdout=True, - stderr=True, - demux=False, + exit_code, _ = self._container.exec_run( + ["bash", "-c", runner], + environment={"SANDBOX_CMD": command}, **kwargs, ) - text = (output or b"").decode("utf-8", errors="replace") - if exit_code in (124, 137): - return exit_code, text + f"\n[command timed out after {timeout}s]" - return exit_code, text except Exception as exc: return 1, f"Error running command in container: {exc}" + + try: + text = self.read_file(out_path).decode("utf-8", errors="replace") + except FileNotFoundError: + text = "" + finally: + self._container.exec_run(["rm", "-f", out_path]) + + if exit_code in (124, 137): + return exit_code, text + f"\n[command timed out after {timeout}s]" + return exit_code, text