SevdanurGenc's picture
Upload 60 files
63f6453 verified
Raw History Blame Contribute Delete
5.71 kB
"""Komut çalıştırıcı: kodu yerel süreçte ya da izole Docker konteynerinde koşar.
Ajanın derleyici ve test koşucusuna erişimi yalnızca bu katman üzerinden olur;
böylece LLM'in ürettiği kod hiçbir zaman ana sürece doğrudan dokunmaz.
"""
from __future__ import annotations
import os
import shutil
import signal
import subprocess
import time
from dataclasses import dataclass
from pathlib import Path
from ..config import settings
@dataclass
class ExecResult:
returncode: int
stdout: str
stderr: str
duration_s: float
timed_out: bool = False
@property
def combined(self) -> str:
return (self.stdout + ("\n" + self.stderr if self.stderr else "")).strip()
class Executor:
"""Soyut çalıştırıcı. `node_toolchain` ve `java_lib`, çalışma ortamı içindeki yollardır."""
mode = "abstract"
node_toolchain: str
java_lib: str
def run(self, cmd: str, workdir: Path, timeout: int, image: str) -> ExecResult: # pragma: no cover
raise NotImplementedError
def workpath(self, workdir: Path) -> str:
"""Çalışma dizininin, komutun çalıştığı ortamdaki yolu."""
raise NotImplementedError
class LocalExecutor(Executor):
mode = "local"
def __init__(self) -> None:
self.node_toolchain = str(settings.node_toolchain)
self.java_lib = str(settings.java_lib_dir)
def workpath(self, workdir: Path) -> str:
return str(workdir)
def run(self, cmd: str, workdir: Path, timeout: int, image: str = "") -> ExecResult:
env = dict(os.environ)
env.update({"CI": "1", "NO_COLOR": "1", "FORCE_COLOR": "0", "PYTHONDONTWRITEBYTECODE": "1"})
env["NODE_PATH"] = str(Path(self.node_toolchain) / "node_modules")
start = time.time()
proc = subprocess.Popen(
["bash", "-lc", cmd], cwd=workdir, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, env=env, start_new_session=True,
)
try:
out, err = proc.communicate(timeout=timeout)
return ExecResult(proc.returncode, out, err, time.time() - start)
except subprocess.TimeoutExpired:
try:
os.killpg(proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass
out, err = proc.communicate()
return ExecResult(-9, out or "", (err or "") + "\n[zaman aşımı]", time.time() - start, True)
class DockerExecutor(Executor):
"""Her komut için ağ erişimi kapalı, bellek/CPU sınırlı geçici konteyner."""
mode = "docker"
def __init__(self) -> None:
self.node_toolchain = "/opt/toolchain"
self.java_lib = "/opt/java-lib"
# Backend bir konteynerde çalışıyorsa çalışma dosyaları sandbox'a iki yoldan biriyle ulaşır:
# - WORK_VOLUME: backend ile paylaşılan adlandırılmış Docker birimi (her işletim sisteminde çalışır)
# - HOST_WORK_ROOT: aynı dizinin host'taki yolu (bind mount)
self.work_volume = os.environ.get("WORK_VOLUME")
self.host_work_root = os.environ.get("HOST_WORK_ROOT")
def workpath(self, workdir: Path) -> str:
return str(workdir.resolve()) if self.work_volume else "/work"
def _mount_args(self, workdir: Path) -> list[str]:
if self.work_volume:
root = str(settings.work_root.resolve())
return ["-v", f"{self.work_volume}:{root}", "-w", str(workdir.resolve())]
return ["-v", f"{self._host_path(workdir)}:/work", "-w", "/work"]
def _host_path(self, workdir: Path) -> str:
if self.host_work_root:
rel = workdir.resolve().relative_to(settings.work_root.resolve())
return str(Path(self.host_work_root) / rel)
return str(workdir.resolve())
@staticmethod
def _open_permissions(workdir: Path) -> None:
# Sandbox konteyneri ayrıcalıksız kullanıcıyla çalışır; çalışma dizinine yazabilmeli.
for p in [workdir, *workdir.rglob("*")]:
try:
p.chmod(0o777 if p.is_dir() else 0o666)
except OSError:
pass
def run(self, cmd: str, workdir: Path, timeout: int, image: str) -> ExecResult:
self._open_permissions(workdir)
name = f"lta-{workdir.name}-{int(time.time() * 1000) % 100000}"
docker_cmd = [
"docker", "run", "--rm", "--name", name, "--network", "none",
"--memory", settings.docker_memory, "--cpus", settings.docker_cpus,
"--pids-limit", "256", "--security-opt", "no-new-privileges",
*self._mount_args(workdir),
"-e", "CI=1", "-e", "NO_COLOR=1",
image, "bash", "-lc", cmd,
]
start = time.time()
try:
p = subprocess.run(docker_cmd, capture_output=True, text=True, timeout=timeout + 15)
return ExecResult(p.returncode, p.stdout, p.stderr, time.time() - start)
except subprocess.TimeoutExpired as e:
subprocess.run(["docker", "kill", name], capture_output=True)
return ExecResult(-9, (e.stdout or b"").decode(errors="ignore") if isinstance(e.stdout, bytes) else (e.stdout or ""),
"[zaman aşımı]", time.time() - start, True)
_executor: Executor | None = None
def get_executor() -> Executor:
global _executor
if _executor is None:
if settings.sandbox_mode == "docker":
if not shutil.which("docker"):
raise RuntimeError("SANDBOX_MODE=docker fakat docker komutu bulunamadı")
_executor = DockerExecutor()
else:
_executor = LocalExecutor()
return _executor