"""MiniWeb server lifecycle management for evaluation. Unlike WebArena-Infinity (one server per app), MiniWeb runs a single Flask server hosting all sites. This module starts/stops that server and waits for readiness. """ import os import signal import subprocess import sys import time import urllib.error import urllib.request PROJECT_ROOT = str( __import__("pathlib").Path(__file__).resolve().parents[3] # the repository (src/miniweb/evaluation/server.py) ) def kill_port(port: int): """Kill any process listening on the given port.""" try: result = subprocess.run( ["lsof", "-ti", f":{port}"], capture_output=True, text=True, timeout=5, ) for pid in result.stdout.strip().split(): if pid: try: os.kill(int(pid), signal.SIGTERM) except (ValueError, ProcessLookupError, PermissionError): pass if result.stdout.strip(): time.sleep(0.5) except (subprocess.TimeoutExpired, FileNotFoundError): pass def start_server(port: int, env_extra: dict | None = None) -> subprocess.Popen: """Start the MiniWeb Flask server on the given port. `env_extra` overlays extra environment variables for this server (e.g. MINIWEB_NO_AUTOLOGIN=1 so an authenticate_by_form task starts logged out). Server output goes to a log file, NOT a pipe: with PIPE and no reader, the request log fills the 64KB pipe buffer after a few hundred requests and the server blocks on write — every request then hangs. """ kill_port(port) env = os.environ.copy() env["FLASK_RUN_PORT"] = str(port) if env_extra: env.update({k: str(v) for k, v in env_extra.items()}) log_dir = os.path.join(str(PROJECT_ROOT), "data", "logs") # out of the source tree (src/ layout, 2026-10-08) os.makedirs(log_dir, exist_ok=True) log = open(os.path.join(log_dir, f"miniweb_server_{port}.log"), "w") return subprocess.Popen( [sys.executable, "run.py"], cwd=PROJECT_ROOT, env=env, stdout=log, stderr=subprocess.STDOUT, ) def wait_for_server(port: int, host: str = "localhost", timeout: int = 30, site_id: str = None) -> bool: """Poll until the server responds with 200 (uses stdlib to avoid urllib3 conflicts). If site_id is provided, also hits the site's index page to pre-warm any lazy data loading (e.g., reservoir sampling large datasets). """ url = f"http://{host}:{port}/" deadline = time.time() + timeout while time.time() < deadline: try: r = urllib.request.urlopen(url, timeout=2) if r.status == 200: # Portal is up — now pre-warm the site if specified if site_id: site_url = f"http://{host}:{port}/sites/{site_id}/" try: urllib.request.urlopen(site_url, timeout=60) except (urllib.error.URLError, OSError): pass # Site may take time to load, that's ok return True except (urllib.error.URLError, OSError): pass time.sleep(0.5) return False def stop_server(proc: subprocess.Popen): if proc.poll() is not None: return proc.terminate() try: proc.wait(timeout=5) except subprocess.TimeoutExpired: proc.kill() proc.wait()