Files
matrix-screen-controller/核桃派软件源代码/app/network/diagnostics.py
T

121 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import socket
import ssl
import subprocess
import time
from copy import deepcopy
from typing import Any, Callable
class NetworkDiagnostics:
def __init__(
self,
status_provider: Callable[[], dict[str, Any]],
*,
runner=subprocess.run,
resolver=socket.getaddrinfo,
connector=socket.create_connection,
) -> None:
self.status_provider = status_provider
self.runner = runner
self.resolver = resolver
self.connector = connector
@staticmethod
def _check(code: str, ok: bool, message: str, *, warning: bool = False) -> dict[str, Any]:
return {"code": code, "ok": ok, "warning": warning, "message": message}
def _command(self, command: list[str], timeout: int = 3) -> subprocess.CompletedProcess[str] | None:
try:
return self.runner(command, capture_output=True, text=True, timeout=timeout)
except (OSError, subprocess.TimeoutExpired):
return None
def run(self) -> dict[str, Any]:
started = time.monotonic()
checks: list[dict[str, Any]] = []
active = (self.status_provider().get("active") or {})
connected = active.get("connected") is True and bool(active.get("ipv4_address"))
checks.append(self._check("interface", connected, "网络接口已连接" if connected else "网卡或 WiFi 未连接"))
if not connected:
return self._result(checks, started, "failed", "网卡或 WiFi 未连接")
route = self._command(["/usr/sbin/ip", "-4", "route", "show", "default"])
route_ok = bool(route and route.returncode == 0 and route.stdout.strip())
checks.append(self._check("default_route", route_ok, "默认路由正常" if route_ok else "默认网关缺失"))
if not route_ok:
return self._result(checks, started, "failed", "默认网关缺失")
dns_ok = False
for host in ("www.baidu.com", "www.qq.com"):
try:
if self.resolver(host, 443, type=socket.SOCK_STREAM):
dns_ok = True
break
except OSError:
continue
checks.append(self._check("dns", dns_ok, "DNS 解析正常" if dns_ok else "DNS 解析失败"))
if not dns_ok:
return self._result(checks, started, "failed", "DNS 解析失败,请检查 DNS 或网关配置")
reachable = False
tls_ok = False
for host in ("www.baidu.com", "www.qq.com"):
try:
raw = self.connector((host, 443), timeout=3)
reachable = True
try:
context = ssl.create_default_context()
with context.wrap_socket(raw, server_hostname=host):
tls_ok = True
break
finally:
try:
raw.close()
except OSError:
pass
except (OSError, ssl.SSLError):
continue
checks.append(self._check("internet", reachable, "外网 TCP 连接正常" if reachable else "网关或上游网络不可达"))
checks.append(self._check("tls", tls_ok, "TLS 校验正常" if tls_ok else "TLS 或系统时间可能异常"))
ping = self._command(["/usr/bin/ping", "-c", "1", "-W", "2", "www.baidu.com"])
ping_ok = bool(ping and ping.returncode == 0)
checks.append(self._check(
"icmp", ping_ok,
"百度 ping 正常" if ping_ok else "目标未响应 ping;部分网络会屏蔽 ICMP",
warning=not ping_ok and tls_ok,
))
if tls_ok:
return self._result(checks, started, "ok", "网络连接正常")
if reachable:
return self._result(checks, started, "failed", "TLS 或系统时间可能异常")
return self._result(checks, started, "failed", "网关或上游网络不可达")
@staticmethod
def _result(checks: list[dict[str, Any]], started: float, overall: str, diagnosis: str) -> dict[str, Any]:
return {
"overall": overall,
"diagnosis": diagnosis,
"duration_ms": int((time.monotonic() - started) * 1000),
"checks": checks,
}
class MockNetworkDiagnostics:
def __init__(self, result: dict[str, Any] | None = None) -> None:
self.result = result or {
"overall": "ok",
"diagnosis": "网络连接正常(mock)",
"duration_ms": 1,
"checks": [
{"code": "interface", "ok": True, "warning": False, "message": "网络接口已连接"},
{"code": "dns", "ok": True, "warning": False, "message": "DNS 解析正常"},
{"code": "internet", "ok": True, "warning": False, "message": "外网连接正常"},
],
}
def run(self) -> dict[str, Any]:
return deepcopy(self.result)