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

298 lines
12 KiB
Python

from __future__ import annotations
import subprocess
import threading
from copy import deepcopy
from dataclasses import dataclass
from typing import Any, Protocol, Sequence
from uuid import uuid4
class NetworkManagerError(RuntimeError):
"""A sanitized NetworkManager operation failure."""
class NetworkBackend(Protocol):
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]: ...
def create_connection(self, settings: dict[str, Any]) -> str: ...
def save_connection(self, connection_uuid: str, settings: dict[str, Any]) -> None: ...
def activate(self, connection_uuid: str, *, timeout: int = 5) -> None: ...
def _decode_terse(value: str) -> str:
output: list[str] = []
escaped = False
for character in value:
if escaped:
output.append(character)
escaped = False
elif character == "\\":
escaped = True
else:
output.append(character)
if escaped:
output.append("\\")
return "".join(output)
@dataclass
class NmcliNetworkManager:
executable: str = "/usr/bin/nmcli"
interface: str = "wlan0"
def _run(
self,
arguments: Sequence[str],
*,
timeout: int = 10,
) -> str:
try:
result = subprocess.run(
[self.executable, *arguments],
check=True,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=timeout,
)
return result.stdout.strip()
except FileNotFoundError as exc:
raise NetworkManagerError("NetworkManager nmcli is unavailable") from exc
except subprocess.TimeoutExpired as exc:
raise NetworkManagerError("NetworkManager operation timed out") from exc
except subprocess.CalledProcessError as exc:
# stderr may echo a secret on some nmcli failures, so never include it.
raise NetworkManagerError("NetworkManager rejected the requested operation") from exc
def _connection_value(
self,
connection_uuid: str,
field: str,
*,
include_secret: bool = False,
) -> str:
arguments = []
if include_secret:
arguments.append("--show-secrets")
arguments.extend(["--get-values", field, "connection", "show", "uuid", connection_uuid])
return self._run(arguments)
def discover_connection_uuid(self) -> str | None:
active = self._run([
"--get-values", "GENERAL.CON-UUID", "device", "show", self.interface,
])
if active and active != "--":
return active.splitlines()[0].strip()
rows = self._run([
"--terse", "--fields", "UUID,TYPE,AUTOCONNECT", "connection", "show",
])
candidates = []
for row in rows.splitlines():
parts = row.split(":")
if len(parts) == 3 and parts[1] == "802-11-wireless" and parts[2] == "yes":
candidates.append(parts[0])
return candidates[0] if len(candidates) == 1 else None
def read_saved(self, connection_uuid: str, *, include_secret: bool = False) -> dict[str, Any]:
ssid = self._connection_value(connection_uuid, "802-11-wireless.ssid")
method = self._connection_value(connection_uuid, "ipv4.method") or "auto"
raw_addresses = self._connection_value(connection_uuid, "ipv4.addresses")
address = raw_addresses.splitlines()[0].strip() if raw_addresses else ""
raw_dns = self._connection_value(connection_uuid, "ipv4.dns")
password = None
password_configured = False
if include_secret:
password = self._connection_value(
connection_uuid,
"802-11-wireless-security.psk",
include_secret=True,
)
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
if "/" in address:
plain_address, raw_prefix = address.rsplit("/", 1)
try:
prefix = int(raw_prefix)
except ValueError:
prefix = None
return {
"connection_uuid": connection_uuid,
"ssid": ssid,
"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()],
}
def read_active(self) -> dict[str, Any]:
raw_state = self._run([
"--get-values", "GENERAL.STATE", "device", "show", self.interface,
])
connected = raw_state.startswith("100")
connection_uuid = self._run([
"--get-values", "GENERAL.CON-UUID", "device", "show", self.interface,
]) or None
addresses = [
item.strip() for item in self._run([
"--get-values", "IP4.ADDRESS", "device", "show", self.interface,
]).splitlines() if item.strip()
] if connected else []
ssid = None
if connected:
rows = self._run([
"--terse", "--fields", "IN-USE,SSID", "device", "wifi", "list",
"ifname", self.interface,
])
for row in rows.splitlines():
if row.startswith("*:"):
ssid = _decode_terse(row[2:])
break
ipv4 = addresses[0].split("/", 1)[0] if addresses else None
return {
"connected": bool(connected and ipv4),
"connection_uuid": connection_uuid,
"ssid": ssid,
"ipv4_address": ipv4,
"control_url": f"http://{ipv4}:8080/" if ipv4 else None,
}
@staticmethod
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:
arguments.extend(["802-11-wireless-security.psk", settings["password"]])
if settings["ipv4_mode"] == "dhcp":
arguments.extend([
"ipv4.method", "auto", "ipv4.addresses", "", "ipv4.gateway", "",
"ipv4.dns", "",
])
else:
dns = settings["dns_servers"] or [settings["gateway"]]
arguments.extend([
"ipv4.method", "manual",
"ipv4.addresses", f"{settings['address']}/{settings['prefix']}",
"ipv4.gateway", settings["gateway"],
"ipv4.dns", ",".join(dns),
])
return arguments
def create_connection(self, settings: dict[str, Any]) -> str:
name = "matrix-screen-controller-wifi"
self._run([
"connection", "add", "type", "wifi", "ifname", self.interface,
"con-name", name, "ssid", settings["ssid"],
])
connection_uuid = self._run([
"--get-values", "connection.uuid", "connection", "show", "id", name,
])
self.save_connection(connection_uuid, settings)
return connection_uuid
def save_connection(self, connection_uuid: str, settings: dict[str, Any]) -> None:
self._run([
"connection", "modify", "uuid", connection_uuid,
*self._modify_arguments(settings),
])
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)
class MockNetworkManager:
def __init__(
self,
saved: dict[str, Any] | None = None,
active: dict[str, Any] | None = None,
) -> None:
self._lock = threading.RLock()
self.saved = deepcopy(saved)
self.active = deepcopy(active) if active is not None else {
"connected": False,
"connection_uuid": None,
"ssid": None,
"ipv4_address": None,
"control_url": None,
}
self.activations: list[str] = []
self.fail_save = False
self.fail_activate = False
def discover_connection_uuid(self) -> str | None:
with self._lock:
if self.active.get("connection_uuid"):
return self.active["connection_uuid"]
return self.saved.get("connection_uuid") if self.saved else None
def read_saved(self, connection_uuid: str, *, include_secret: bool = False) -> dict[str, Any]:
with self._lock:
if not self.saved or self.saved.get("connection_uuid") != connection_uuid:
raise NetworkManagerError("managed WiFi connection is unavailable")
value = deepcopy(self.saved)
if not include_secret:
value["password"] = None
return value
def read_active(self) -> dict[str, Any]:
with self._lock:
return deepcopy(self.active)
def create_connection(self, settings: dict[str, Any]) -> str:
connection_uuid = str(uuid4())
with self._lock:
self.saved = {"connection_uuid": connection_uuid, **deepcopy(settings)}
self.saved["password_configured"] = bool(settings.get("password"))
return connection_uuid
def save_connection(self, connection_uuid: str, settings: dict[str, Any]) -> None:
if self.fail_save:
raise NetworkManagerError("NetworkManager rejected the requested operation")
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:
self.saved["password"] = previous_password
self.saved["password_configured"] = bool(self.saved.get("password"))
def activate(self, connection_uuid: str, *, timeout: int = 5) -> None:
del timeout
if self.fail_activate:
raise NetworkManagerError("NetworkManager rejected the requested operation")
with self._lock:
self.activations.append(connection_uuid)
if self.saved and self.saved.get("connection_uuid") == connection_uuid:
existing_address = None
if (
self.saved.get("ipv4_mode") == "dhcp"
and self.active.get("connected")
and self.active.get("connection_uuid") == connection_uuid
):
existing_address = self.active.get("ipv4_address")
address = self.saved.get("address") or existing_address or "192.168.1.100"
self.active = {
"connected": True,
"connection_uuid": connection_uuid,
"ssid": self.saved.get("ssid"),
"ipv4_address": address,
"control_url": f"http://{address}:8080/",
}