Files
wehub-resource-sync 5296d0e97c
CI / Ban suppressions and legacy annotations (push) Has been cancelled
CI / pytest (push) Has been cancelled
CI / ruff-check (push) Has been cancelled
CI / ruff-format (push) Has been cancelled
CI / ty (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:44 +08:00

119 lines
3.2 KiB
Python

"""Subprocess lifecycle helpers for local smoke servers."""
import os
import socket
import subprocess
import time
from collections.abc import Iterable, Iterator
from contextlib import contextmanager, suppress
from dataclasses import dataclass
from pathlib import Path
import httpx
from .child_process import cmd_free_claude_code_serve
from .config import SmokeConfig, redacted
@dataclass(slots=True)
class RunningServer:
base_url: str
port: int
log_path: Path
process: subprocess.Popen[bytes]
def find_free_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind(("127.0.0.1", 0))
return int(sock.getsockname()[1])
@contextmanager
def start_server(
config: SmokeConfig,
*,
env_overrides: dict[str, str] | None = None,
env_unset: Iterable[str] = (),
command: list[str] | None = None,
name: str = "server",
) -> Iterator[RunningServer]:
port = find_free_port()
config.results_dir.mkdir(parents=True, exist_ok=True)
log_path = config.results_dir / f"{name}-{config.worker_id}-{port}.log"
env = os.environ.copy()
for key in env_unset:
env.pop(key, None)
env.update(
{
"HOST": "127.0.0.1",
"PORT": str(port),
"LOG_FILE": str(log_path),
"FCC_OPEN_BROWSER": "0",
"MESSAGING_PLATFORM": "none",
"PYTHONUNBUFFERED": "1",
}
)
if env_overrides:
env.update(env_overrides)
cmd = command or cmd_free_claude_code_serve()
with log_path.open("ab") as log_file:
process = subprocess.Popen(
cmd,
cwd=config.root,
env=env,
stdout=log_file,
stderr=subprocess.STDOUT,
)
running = RunningServer(
base_url=f"http://127.0.0.1:{port}",
port=port,
log_path=log_path,
process=process,
)
try:
_wait_for_health(running, timeout_s=config.timeout_s)
yield running
finally:
_stop_process(process)
def _wait_for_health(server: RunningServer, *, timeout_s: float) -> None:
deadline = time.monotonic() + timeout_s
last_error = ""
while time.monotonic() < deadline:
if server.process.poll() is not None:
break
try:
response = httpx.get(f"{server.base_url}/health", timeout=2.0)
if response.status_code == 200:
return
last_error = f"HTTP {response.status_code}: {response.text[:200]}"
except Exception as exc:
last_error = f"{type(exc).__name__}: {exc}"
time.sleep(0.25)
log_excerpt = ""
with suppress(OSError):
log_excerpt = server.log_path.read_text(encoding="utf-8", errors="replace")[
-2000:
]
raise AssertionError(
"Smoke server did not become healthy. "
f"last_error={last_error!r} log={redacted(log_excerpt)!r}"
)
def _stop_process(process: subprocess.Popen[bytes]) -> None:
if process.poll() is not None:
return
process.terminate()
try:
process.wait(timeout=8)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=5)