from __future__ import annotations import os import signal import subprocess import time from dataclasses import dataclass from pathlib import Path from typing import Optional from .didactyl_client import DidactylClient @dataclass class AgentProcess: binary_path: str config_path: str api_port: int = 8485 api_bind: str = "127.0.0.1" debug_level: int = 5 log_file: Optional[str] = None base_url: Optional[str] = None def __post_init__(self) -> None: self.binary_path = str(Path(self.binary_path)) self.config_path = str(Path(self.config_path)) self.log_file = self.log_file or "tests/results/agent_debug.log" scheme = "https" self.base_url = self.base_url or f"{scheme}://{self.api_bind}:{self.api_port}" self.process: Optional[subprocess.Popen[str]] = None def _command(self) -> list[str]: return [ self.binary_path, "--config", self.config_path, "--debug", str(self.debug_level), "--api-port", str(self.api_port), "--api-bind", self.api_bind, ] def start(self, timeout: float = 30.0) -> bool: if self.is_alive(): return True env = os.environ.copy() env["DIDACTYL_LOG_FILE"] = str(self.log_file) Path(self.log_file).parent.mkdir(parents=True, exist_ok=True) self.process = subprocess.Popen( self._command(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env, ) client = DidactylClient(base_url=self.base_url, timeout=2.0, verify_tls=False) deadline = time.time() + timeout while time.time() < deadline: if self.process and self.process.poll() is not None: return False try: data = client.status() if data.get("success"): return True except Exception: pass time.sleep(0.5) return False def stop(self, timeout: float = 10.0) -> bool: if not self.process: return True if self.process.poll() is not None: return True try: self.process.send_signal(signal.SIGTERM) self.process.wait(timeout=timeout) return True except subprocess.TimeoutExpired: self.process.kill() self.process.wait(timeout=5) return False def restart(self, timeout: float = 30.0) -> bool: self.stop() return self.start(timeout=timeout) def is_alive(self) -> bool: return self.process is not None and self.process.poll() is None def pid(self) -> Optional[int]: return None if not self.process else self.process.pid def return_code(self) -> Optional[int]: return None if not self.process else self.process.poll() def read_pipes(self) -> tuple[str, str]: if not self.process: return "", "" out = "" err = "" if self.process.stdout: out = self.process.stdout.read() or "" if self.process.stderr: err = self.process.stderr.read() or "" return out, err