File size: 5,705 Bytes
63f6453 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | """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
|