"""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