更新: 421 个文件 - 2026-03-17 18:30:02
这个提交包含在:
131
scripts/lab/doctor.py
普通文件
131
scripts/lab/doctor.py
普通文件
@@ -0,0 +1,131 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
from contextlib import closing
|
||||
from typing import Any, Dict, Iterable, List, Tuple
|
||||
|
||||
from lab.utils import command_available, run
|
||||
|
||||
|
||||
def _result(name: str, ok: bool, detail: str, **extra: Any) -> Dict[str, Any]:
|
||||
payload = {"name": name, "ok": ok, "detail": detail}
|
||||
payload.update(extra)
|
||||
return payload
|
||||
|
||||
|
||||
def _parse_host_ports(profiles: Iterable[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
ports: List[Dict[str, Any]] = []
|
||||
for profile in profiles:
|
||||
for service_name, service in (profile.get("services") or {}).items():
|
||||
for binding in service.get("ports", []) or []:
|
||||
host_port = None
|
||||
value = str(binding)
|
||||
parts = value.split(":")
|
||||
if len(parts) == 3 and parts[1].isdigit():
|
||||
host_port = int(parts[1])
|
||||
elif len(parts) >= 2 and parts[0].isdigit():
|
||||
host_port = int(parts[0])
|
||||
elif value.isdigit():
|
||||
host_port = int(value)
|
||||
if host_port is None:
|
||||
continue
|
||||
ports.append(
|
||||
{
|
||||
"profile_id": profile.get("profile_id"),
|
||||
"service": service_name,
|
||||
"binding": value,
|
||||
"port": host_port,
|
||||
}
|
||||
)
|
||||
return ports
|
||||
|
||||
|
||||
def _port_available(port: int) -> bool:
|
||||
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
sock.bind(("127.0.0.1", port))
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _check_docker_cli() -> Dict[str, Any]:
|
||||
ok = command_available("docker")
|
||||
return _result("docker-cli", ok, "docker CLI available" if ok else "docker CLI is not installed")
|
||||
|
||||
|
||||
def _check_docker_daemon() -> Dict[str, Any]:
|
||||
if not command_available("docker"):
|
||||
return _result("docker-daemon", False, "docker CLI unavailable")
|
||||
context = run(["docker", "context", "show"], check=False)
|
||||
info = run(["docker", "info"], check=False)
|
||||
detail = f"context={context.stdout.strip() or 'unknown'}"
|
||||
if info.returncode != 0:
|
||||
detail = info.stderr.strip() or info.stdout.strip() or "docker daemon unavailable"
|
||||
return _result("docker-daemon", False, detail)
|
||||
return _result("docker-daemon", True, detail or "docker daemon reachable")
|
||||
|
||||
|
||||
def _check_playwright_import() -> Dict[str, Any]:
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright # noqa: F401
|
||||
except Exception as exc:
|
||||
return _result("playwright-import", False, f"playwright import failed: {exc}")
|
||||
return _result("playwright-import", True, "playwright Python package import passed")
|
||||
|
||||
|
||||
def _check_chromium_launch() -> Dict[str, Any]:
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
except Exception as exc:
|
||||
return _result("playwright-browser", False, f"playwright import failed: {exc}")
|
||||
try:
|
||||
with sync_playwright() as playwright:
|
||||
browser = playwright.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
page.set_content("<html><body>ok</body></html>")
|
||||
browser.close()
|
||||
except Exception as exc:
|
||||
return _result("playwright-browser", False, f"chromium launch failed: {exc}")
|
||||
return _result("playwright-browser", True, "chromium runtime launch passed")
|
||||
|
||||
|
||||
def _check_ports(profiles: Iterable[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
requested = _parse_host_ports(profiles)
|
||||
if not requested:
|
||||
return _result("ports", True, "no host ports declared")
|
||||
|
||||
conflicts: List[Dict[str, Any]] = []
|
||||
for item in requested:
|
||||
if not _port_available(item["port"]):
|
||||
conflicts.append(item)
|
||||
|
||||
if conflicts:
|
||||
detail = ", ".join(
|
||||
f"{item['port']}({item['profile_id']}::{item['service']})" for item in conflicts
|
||||
)
|
||||
return _result("ports", False, f"host ports already in use: {detail}", conflicts=conflicts)
|
||||
return _result("ports", True, f"checked {len(requested)} host port bindings", bindings=requested)
|
||||
|
||||
|
||||
def run_checks(profiles: Iterable[Dict[str, Any]] | None = None) -> Dict[str, Any]:
|
||||
selected = list(profiles or [])
|
||||
checks = [
|
||||
_check_docker_cli(),
|
||||
_check_docker_daemon(),
|
||||
_check_playwright_import(),
|
||||
_check_chromium_launch(),
|
||||
_check_ports(selected),
|
||||
]
|
||||
ok = all(item["ok"] for item in checks)
|
||||
failures = [item for item in checks if not item["ok"]]
|
||||
return {
|
||||
"status": "passed" if ok else "failed",
|
||||
"ok": ok,
|
||||
"checks": checks,
|
||||
"profile_ids": [item.get("profile_id") for item in selected if item.get("profile_id")],
|
||||
"failure_count": len(failures),
|
||||
"summary": "; ".join(item["detail"] for item in failures) if failures else "all checks passed",
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户