File size: 3,495 Bytes
22de727
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""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()