同步移动端工程、设备控制改进与发布资料

This commit is contained in:
2026-09-26 19:21:12 +08:00
parent 912ba432cf
commit 98eadc5c8b
137 changed files with 9274 additions and 85 deletions
+79 -11
View File
@@ -2,6 +2,8 @@ from __future__ import annotations
import subprocess
import threading
import time
import re
from copy import deepcopy
from dataclasses import dataclass
from typing import Any, Protocol, Sequence
@@ -10,9 +12,13 @@ from uuid import uuid4
class NetworkManagerError(RuntimeError):
"""A sanitized NetworkManager operation failure."""
def __init__(self, message: str, *, error_code: str = "UNKNOWN"):
super().__init__(message)
self.error_code = error_code
class NetworkBackend(Protocol):
def scan(self) -> list[dict[str, Any]]: ...
def discover_connection_uuid(self) -> str | None: ...
def read_saved(self, connection_uuid: str, *, include_secret: bool = False) -> dict[str, Any]: ...
def read_active(self) -> dict[str, Any]: ...
@@ -42,6 +48,40 @@ class NmcliNetworkManager:
executable: str = "/usr/bin/nmcli"
interface: str = "wlan0"
def scan(self) -> list[dict[str, Any]]:
rows = self._run(['--terse', '--escape', 'yes', '--fields', 'IN-USE,SSID,SECURITY,SIGNAL',
'device', 'wifi', 'list', 'ifname', self.interface, '--rescan', 'yes'], timeout=15)
strongest = {}
for row in rows.splitlines():
fields, current, escaped = [], [], False
for character in row:
if escaped:
current.append(character)
escaped = False
elif character == '\\':
escaped = True
elif character == ':':
fields.append(''.join(current))
current = []
else:
current.append(character)
fields.append(''.join(current))
if len(fields) != 4 or not fields[1]:
continue
active, ssid, security, signal = fields
if not signal.isdecimal():
continue
kind = 'open' if security in ('', '--') else (
'wpa-psk' if ('WPA1' in security or 'WPA2' in security) and '802.1X' not in security else 'unsupported')
item = dict(ssid=ssid, security=kind, signal_percent=min(100, max(0, int(signal))), connected=active == '*')
key = (ssid, kind)
if key not in strongest or item['signal_percent'] > strongest[key]['signal_percent']:
item['connected'] = item['connected'] or strongest.get(key, {}).get('connected', False)
strongest[key] = item
elif item['connected']:
strongest[key]['connected'] = True
return sorted(strongest.values(), key=lambda item: -item['signal_percent'])[:100]
def _run(
self,
arguments: Sequence[str],
@@ -105,6 +145,7 @@ class NmcliNetworkManager:
raw_dns = self._connection_value(connection_uuid, "ipv4.dns")
password = None
password_configured = False
key_mgmt = self._connection_value(connection_uuid, "802-11-wireless-security.key-mgmt")
if include_secret:
password = self._connection_value(
connection_uuid,
@@ -113,10 +154,6 @@ class NmcliNetworkManager:
)
password_configured = bool(password and password != "--")
else:
key_mgmt = self._connection_value(
connection_uuid,
"802-11-wireless-security.key-mgmt",
)
password_configured = bool(key_mgmt and key_mgmt != "--")
prefix = None
plain_address = address
@@ -129,13 +166,17 @@ class NmcliNetworkManager:
return {
"connection_uuid": connection_uuid,
"ssid": ssid,
"security": "open" if key_mgmt in ("", "--") else "wpa-psk" if key_mgmt == "wpa-psk" else "unsupported",
"password": password if password_configured else None,
"password_configured": password_configured,
"ipv4_mode": "dhcp" if method == "auto" else "manual",
"address": plain_address or None,
"prefix": prefix,
"gateway": self._connection_value(connection_uuid, "ipv4.gateway") or None,
"dns_servers": [item.strip() for item in raw_dns.splitlines() if item.strip()],
"dns_servers": list(dict.fromkeys(
item.strip() for line in raw_dns.splitlines() for item in line.split(",")
if item.strip() and item.strip() != "--"
)),
}
def read_active(self) -> dict[str, Any]:
@@ -174,9 +215,10 @@ class NmcliNetworkManager:
def _modify_arguments(settings: dict[str, Any]) -> list[str]:
arguments = [
"802-11-wireless.ssid", settings["ssid"],
"802-11-wireless-security.key-mgmt", "wpa-psk",
]
if settings.get("password") is not None:
if settings.get('security', 'wpa-psk') != 'open':
arguments.extend(['802-11-wireless-security.key-mgmt', 'wpa-psk'])
if settings.get("password") is not None and settings.get('security') != 'open':
arguments.extend(["802-11-wireless-security.psk", settings["password"]])
if settings["ipv4_mode"] == "dhcp":
arguments.extend([
@@ -206,6 +248,8 @@ class NmcliNetworkManager:
return connection_uuid
def save_connection(self, connection_uuid: str, settings: dict[str, Any]) -> None:
if settings.get('security') == 'open':
self._run(['connection', 'modify', 'uuid', connection_uuid, 'remove', '802-11-wireless-security'])
self._run([
"connection", "modify", "uuid", connection_uuid,
*self._modify_arguments(settings),
@@ -213,12 +257,36 @@ class NmcliNetworkManager:
def activate(self, connection_uuid: str, *, timeout: int = 5) -> None:
bounded = max(1, min(int(timeout), 30))
self._run([
"--wait", str(bounded), "connection", "up", "uuid", connection_uuid,
], timeout=bounded + 2)
started = int(time.time())
try:
self._run([
"--wait", str(bounded), "connection", "up", "uuid", connection_uuid,
], timeout=bounded + 2)
except NetworkManagerError:
code = "UNKNOWN"
try:
reason = self._run(["--get-values", "GENERAL.REASON", "device", "show", self.interface])
number = int(reason.split(" ", 1)[0])
if number == 53:
code = "NETWORK_UNAVAILABLE"
elif number in (5, 6, 15, 16, 17):
code = "IP_CONFIG_FAILED"
# A timeout or missing-secret request alone is not proof of a wrong password.
# Only the supplicant's explicit WRONG_KEY event from this attempt qualifies.
if code == "UNKNOWN":
result = subprocess.run(["journalctl", "-u", "wpa_supplicant.service", "--since", f"@{started}",
"--output=cat", "--no-pager"], capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=3)
pattern = re.compile(r"^" + re.escape(self.interface) + r": CTRL-EVENT-SSID-TEMP-DISABLED .*\breason=WRONG_KEY\b", re.MULTILINE)
if result.returncode == 0 and pattern.search(result.stdout):
code = "AUTH_FAILED"
except (NetworkManagerError, ValueError, OSError, subprocess.SubprocessError):
pass
raise NetworkManagerError("WiFi activation failed", error_code=code) from None
class MockNetworkManager:
def scan(self):
return []
def __init__(
self,
saved: dict[str, Any] | None = None,
@@ -269,7 +337,7 @@ class MockNetworkManager:
with self._lock:
previous_password = self.saved.get("password") if self.saved else None
self.saved = {"connection_uuid": connection_uuid, **deepcopy(settings)}
if settings.get("password") is None:
if settings.get("password") is None and settings.get('security') != 'open':
self.saved["password"] = previous_password
self.saved["password_configured"] = bool(self.saved.get("password"))
@@ -30,10 +30,16 @@ def validate_wifi_settings(
raise WifiConfigError("ssid must contain 1..32 UTF-8 bytes")
password_value = raw.get("password")
security = raw.get('security', 'wpa-psk')
if security not in ('open', 'wpa-psk'):
raise WifiConfigError('unsupported WiFi security')
password = None if password_value in (None, "") else str(password_value)
if password is not None and not 8 <= _utf8_length(password) <= 63:
raise WifiConfigError("password must contain 8..63 UTF-8 bytes")
if previous is None or previous.get("ssid") != ssid:
if security == 'open':
if password is not None:
raise WifiConfigError('open networks cannot have a password')
elif previous is None or previous.get("ssid") != ssid:
if password is None:
raise WifiConfigError("a new password is required when the SSID changes")
elif password is None and not previous.get("password_configured"):
@@ -79,8 +85,9 @@ def validate_wifi_settings(
return {
"ssid": ssid,
"security": security,
"password": password,
"password_configured": bool(password or (previous or {}).get("password_configured")),
"password_configured": security != 'open' and bool(password or (previous or {}).get("password_configured")),
"ipv4_mode": mode,
"address": address,
"prefix": prefix if mode == "manual" else None,
@@ -105,6 +112,8 @@ class WifiNetworkService:
self.monotonic = monotonic
self.poll_seconds = poll_seconds
self._lock = threading.RLock()
self.configuration_lock = self._lock
self.settings_revision = str(uuid4())
self._stop = threading.Event()
self._thread: threading.Thread | None = None
self._activation_active = False
@@ -256,6 +265,7 @@ class WifiNetworkService:
else "WiFi 配置已保存,将在下次断电开机后生效"
)
self.store.set_operation({"id": operation_id, "state": state, "message": message})
self.settings_revision = str(uuid4())
return {
"operation_id": operation_id,
"activation": activation,
@@ -297,7 +307,8 @@ class WifiNetworkService:
self.store.set_operation({
"id": operation_id,
"state": "failed",
"message": str(exc),
"message": "WiFi activation failed",
"error_code": getattr(exc, "error_code", "UNKNOWN"),
})
finally:
with self._lock:
@@ -115,6 +115,9 @@ class WifiConfigStore:
"state": str(operation.get("state") or "idle"),
"message": str(operation.get("message") or ""),
}
if operation.get("error_code") is not None:
code = operation["error_code"]
safe["error_code"] = code if code in {"AUTH_FAILED", "NETWORK_UNAVAILABLE", "IP_CONFIG_FAILED", "UNKNOWN"} else "UNKNOWN"
if safe["state"] not in {"idle", "scheduled", "applying", "succeeded", "failed"}:
raise WifiConfigError("invalid wifi operation state")
with self._lock: