同步移动端工程、设备控制改进与发布资料
This commit is contained in:
@@ -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"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user