Spaces:
Running
Running
| """Sandboxed Python code execution. | |
| Runs user Python code in a subprocess with resource limits, | |
| captures stdout/stderr, and saves matplotlib figures. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import subprocess | |
| import sys | |
| import tempfile | |
| import textwrap | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from code.config.constants import ( | |
| MAX_STDIO_CHARS, | |
| OUTPUT_PNG, | |
| PY_MEM_LIMIT_MB, | |
| PY_TIMEOUT_S, | |
| ) | |
| class PythonExecutionResult: | |
| """Result of a sandboxed Python execution.""" | |
| stdout: str | |
| stderr: str | |
| image_path: str | None | |
| returncode: int | None | |
| timed_out: bool = False | |
| def _apply_subprocess_limits() -> None: | |
| """Set resource limits for the subprocess (Linux only).""" | |
| import resource | |
| mem_bytes = PY_MEM_LIMIT_MB * 1024 * 1024 | |
| resource.setrlimit(resource.RLIMIT_AS, (mem_bytes, mem_bytes)) | |
| resource.setrlimit(resource.RLIMIT_CPU, (PY_TIMEOUT_S, PY_TIMEOUT_S)) | |
| def _python_runner_source() -> str: | |
| """Return the source code of the runner script that wraps user code.""" | |
| return textwrap.dedent( | |
| f""" | |
| import os | |
| import runpy | |
| import sys | |
| import traceback | |
| os.environ.setdefault("MPLBACKEND", "Agg") | |
| exit_code = 0 | |
| try: | |
| runpy.run_path(os.path.join(os.getcwd(), "user_code.py"), run_name="__main__") | |
| except SystemExit as exc: | |
| code = exc.code | |
| exit_code = code if isinstance(code, int) else 1 | |
| except Exception: | |
| traceback.print_exc() | |
| exit_code = 1 | |
| finally: | |
| try: | |
| import matplotlib | |
| matplotlib.use("Agg", force=True) | |
| import matplotlib.pyplot as plt | |
| if plt.get_fignums(): | |
| plt.savefig(os.environ["OUTPUT_PNG"], bbox_inches="tight") | |
| except ModuleNotFoundError as exc: | |
| if exc.name != "matplotlib": | |
| traceback.print_exc() | |
| except Exception: | |
| traceback.print_exc() | |
| raise SystemExit(exit_code) | |
| """ | |
| ).strip() | |
| def _truncate_output(text: str) -> str: | |
| """Truncate output to MAX_STDIO_CHARS with a note.""" | |
| if len(text) <= MAX_STDIO_CHARS: | |
| return text | |
| remaining = len(text) - MAX_STDIO_CHARS | |
| return text[:MAX_STDIO_CHARS] + f"\n\n... truncated {remaining} characters ..." | |
| def _decode_timeout_output(value: str | bytes | None) -> str: | |
| """Safely decode subprocess output from timeout exceptions.""" | |
| if value is None: | |
| return "" | |
| if isinstance(value, bytes): | |
| return value.decode("utf-8", errors="replace") | |
| return value | |
| def run_python(code: str) -> PythonExecutionResult: | |
| """Execute Python code in a sandboxed subprocess. | |
| Returns a PythonExecutionResult with stdout, stderr, image path, and status. | |
| """ | |
| with tempfile.TemporaryDirectory(prefix="fullstack_run_") as tmp: | |
| workdir = Path(tmp) | |
| runner_path = workdir / "runner.py" | |
| user_path = workdir / "user_code.py" | |
| image_path = workdir / OUTPUT_PNG | |
| runner_path.write_text(_python_runner_source(), encoding="utf-8") | |
| user_path.write_text(code, encoding="utf-8") | |
| env = { | |
| "PATH": "/usr/bin:/bin", | |
| "HOME": str(workdir), | |
| "TMPDIR": str(workdir), | |
| "MPLBACKEND": "Agg", | |
| "MPLCONFIGDIR": str(workdir / ".matplotlib"), | |
| "OUTPUT_PNG": str(image_path), | |
| "PYTHONIOENCODING": "utf-8", | |
| "PYTHONNOUSERSITE": "1", | |
| "PYTHONUNBUFFERED": "1", | |
| "LANG": "C.UTF-8", | |
| "OPENBLAS_NUM_THREADS": "1", | |
| "OMP_NUM_THREADS": "1", | |
| "MKL_NUM_THREADS": "1", | |
| "NUMEXPR_NUM_THREADS": "1", | |
| } | |
| try: | |
| completed = subprocess.run( | |
| [sys.executable, "-I", str(runner_path)], | |
| cwd=workdir, | |
| env=env, | |
| capture_output=True, | |
| text=True, | |
| encoding="utf-8", | |
| errors="replace", | |
| timeout=PY_TIMEOUT_S, | |
| preexec_fn=_apply_subprocess_limits if sys.platform == "linux" else None, | |
| check=False, | |
| ) | |
| stdout = _truncate_output(completed.stdout) | |
| stderr = _truncate_output(completed.stderr) | |
| if completed.returncode and not stderr: | |
| stderr = f"Process exited with status {completed.returncode}." | |
| saved_image: str | None = None | |
| if image_path.exists() and image_path.stat().st_size > 0: | |
| saved = tempfile.NamedTemporaryFile( | |
| prefix="fullstack_plot_", suffix=".png", delete=False | |
| ) | |
| saved.close() | |
| Path(saved.name).write_bytes(image_path.read_bytes()) | |
| saved_image = saved.name | |
| return PythonExecutionResult( | |
| stdout=stdout, | |
| stderr=stderr, | |
| image_path=saved_image, | |
| returncode=completed.returncode, | |
| ) | |
| except subprocess.TimeoutExpired as exc: | |
| stdout = _truncate_output(_decode_timeout_output(exc.stdout)) | |
| stderr = _truncate_output(_decode_timeout_output(exc.stderr)) | |
| timeout_note = f"Timed out after {PY_TIMEOUT_S} seconds; the process was killed." | |
| stderr = f"{stderr}\n{timeout_note}".strip() | |
| return PythonExecutionResult( | |
| stdout=stdout, | |
| stderr=stderr, | |
| image_path=None, | |
| returncode=None, | |
| timed_out=True, | |
| ) | |