fix: Workaround against docker exec_run not flushing stdout
This commit is contained in:
@@ -3,8 +3,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import select as _select
|
|
||||||
import tarfile
|
import tarfile
|
||||||
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
import docker
|
import docker
|
||||||
@@ -12,9 +12,6 @@ import docker.errors
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
_original_poll = getattr(_select, "poll", None)
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import docker.models.containers
|
import docker.models.containers
|
||||||
|
|
||||||
@@ -267,30 +264,41 @@ class DockerSandbox:
|
|||||||
if self._container is None:
|
if self._container is None:
|
||||||
return 1, "Sandbox container is not running."
|
return 1, "Sandbox container is not running."
|
||||||
|
|
||||||
wrapped = [
|
# docker-py's exec_run reads command output off a hijacked raw socket
|
||||||
"timeout",
|
# and intermittently returns nothing because bytes that http.client
|
||||||
"--kill-after",
|
# already buffered while parsing the response headers are stranded and
|
||||||
f"{kill_grace}s",
|
# never read (https://github.com/docker/docker-py/issues/2042,
|
||||||
f"{timeout}s",
|
# https://github.com/docker/docker-py/issues/3332). To avoid that path
|
||||||
"bash",
|
# entirely we redirect combined output to a file and read it back via
|
||||||
"-c",
|
# get_archive (a normal HTTP body), which is reliable on every
|
||||||
command,
|
# 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 = {}
|
kwargs = {}
|
||||||
if self._working_dir is not None:
|
if self._working_dir is not None:
|
||||||
kwargs["workdir"] = self._working_dir
|
kwargs["workdir"] = self._working_dir
|
||||||
|
|
||||||
try:
|
try:
|
||||||
exit_code, output = self._container.exec_run(
|
exit_code, _ = self._container.exec_run(
|
||||||
wrapped,
|
["bash", "-c", runner],
|
||||||
stdout=True,
|
environment={"SANDBOX_CMD": command},
|
||||||
stderr=True,
|
|
||||||
demux=False,
|
|
||||||
**kwargs,
|
**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:
|
except Exception as exc:
|
||||||
return 1, f"Error running command in container: {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
|
||||||
|
|||||||
Reference in New Issue
Block a user