初始化奇妙小屏幕控制器项目
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
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/",
|
||||
}
|
||||
Reference in New Issue
Block a user