import io import os import re import base64 import subprocess import sys import tempfile import textwrap from pathlib import Path from src.models import SandboxResult from src.config import SANDBOX_TIMEOUT _IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"} def _docker_available() -> bool: try: import docker client = docker.from_env() client.ping() client.close() return True except Exception: return False def execute_code(script: str, requirements: list[str] | None = None) -> SandboxResult: if _docker_available(): return _execute_docker(script, requirements) return _execute_local(script, requirements) def _execute_docker(script: str, requirements: list[str] | None = None) -> SandboxResult: import docker from src.config import SANDBOX_IMAGE client = docker.from_env() container = None try: if requirements: req_lines = "\n".join( f"RUN pip install --quiet --no-cache-dir {pkg}" for pkg in requirements ) dockerfile = f"FROM {SANDBOX_IMAGE}\n{req_lines}\n" img, _ = client.images.build( fileobj=io.BytesIO(dockerfile.encode("utf-8")), rm=True ) image = img.id else: image = SANDBOX_IMAGE wrapped = _wrap_script(script) container = client.containers.create( image=image, command=["python3", "-c", wrapped], detach=True, network_disabled=False, read_only=True, tmpfs={"/tmp": "size=64m"}, environment={"MPLCONFIGDIR": "/tmp"}, mem_limit="512m", pids_limit=64, security_opt=["no-new-privileges:true"], cap_drop=["ALL"], ) container.start() result = container.wait(timeout=SANDBOX_TIMEOUT) raw_logs = container.logs(stdout=True, stderr=True, tail=10000) stdout, stderr = _demux_logs(raw_logs) clean_stdout, files = _parse_file_blocks(stdout) return SandboxResult( stdout=clean_stdout or "", stderr=stderr or "", exit_code=result.get("StatusCode", -1), success=result.get("StatusCode", 1) == 0, files=files, ) except Exception as e: return SandboxResult( stdout="", stderr=str(e), exit_code=-1, success=False ) finally: if container: try: container.remove(force=True) except Exception: pass def _execute_local(script: str, requirements: list[str] | None = None) -> SandboxResult: import tempfile tmp_dir = Path(tempfile.mkdtemp()) try: if requirements: subprocess.run( [sys.executable, "-m", "pip", "install", "--quiet"] + requirements, capture_output=True, timeout=60, ) wrapped = _wrap_script(script) proc = subprocess.run( [sys.executable, "-c", wrapped], capture_output=True, text=True, timeout=SANDBOX_TIMEOUT, cwd=str(tmp_dir), env={**os.environ, "MPLCONFIGDIR": str(tmp_dir)}, ) clean_stdout, files = _parse_file_blocks(proc.stdout or "") return SandboxResult( stdout=clean_stdout or "", stderr=proc.stderr or "", exit_code=proc.returncode, success=proc.returncode == 0, files=files, ) except subprocess.TimeoutExpired: return SandboxResult( stdout="", stderr="Execution timed out", exit_code=-1, success=False ) except Exception as e: return SandboxResult( stdout="", stderr=str(e), exit_code=-1, success=False ) finally: import shutil shutil.rmtree(tmp_dir, ignore_errors=True) def _wrap_script(script: str) -> str: b64_script = base64.b64encode(script.encode("utf-8")).decode("ascii") return ( "import base64, os\n" f"exec(base64.b64decode('{b64_script}').decode())\n" "for f in os.listdir('/tmp'):\n" " ext = os.path.splitext(f)[1].lower()\n" f" if ext in {list(_IMAGE_EXTENSIONS)}:\n" " with open(os.path.join('/tmp', f), 'rb') as imgf:\n" " b = base64.b64encode(imgf.read()).decode()\n" " print(f'__SANDBOX_FILE__{f}__{b}__SANDBOX_ENDFILE__')\n" ) def _parse_file_blocks(text: str) -> tuple[str, dict[str, str]]: files = {} pattern = r"__SANDBOX_FILE__(.+?)__(.{10,}?)__SANDBOX_ENDFILE__" def replace_block(m): name = m.group(1) b64 = m.group(2) files[name] = b64 return "" clean = re.sub(pattern, replace_block, text, flags=re.DOTALL) return clean.strip(), files def _demux_logs(raw: bytes) -> tuple[str, str]: if not raw: return "", "" stdout_parts = [] stderr_parts = [] i = 0 while i < len(raw): if i + 8 > len(raw): stdout_parts.append(raw[i:].decode("utf-8", errors="replace")) break stream_type = raw[i] _ = int.from_bytes(raw[i + 4 : i + 8], "big") i += 8 chunk_size = int.from_bytes(raw[i - 4 : i], "big") if i + chunk_size > len(raw): chunk = raw[i:] else: chunk = raw[i : i + chunk_size] i += chunk_size decoded = chunk.decode("utf-8", errors="replace") if stream_type == 1: stdout_parts.append(decoded) elif stream_type == 2: stderr_parts.append(decoded) else: stdout_parts.append(decoded) return "".join(stdout_parts), "".join(stderr_parts)