fix: Workaround against docker exec_run not flushing stdout
CI / unit-tests (push) Successful in 12s
CI / integration-tests (push) Successful in 31s
CI / publish (push) Failing after 32s

This commit is contained in:
2026-06-18 10:44:17 +02:00
parent ea04d44615
commit 65f87a2c0a
+30 -22
View File
@@ -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