Files
2026-08-22 20:34:42 +02:00

166 lines
5.5 KiB
Python

"""Steuerung von wpa_supplicant über seinen Control-Socket.
Das ist derselbe Kanal, den auch wpa_cli benutzt: ein UNIX-Datagramm-Socket,
über den Befehle im Klartext gehen. Ereignisse (Gerät gefunden, Gruppe
gestartet) kommen über eine zweite, angemeldete Verbindung herein.
"""
import logging
import os
import queue
import socket
import tempfile
import threading
from typing import Dict, List, Optional
log = logging.getLogger("wfd.wpa")
CONTROL_DIRS = ("/run/wpa_supplicant", "/var/run/wpa_supplicant")
def find_interfaces() -> List[str]:
"""Alle Interfaces, für die ein Control-Socket bereitsteht."""
found = []
for directory in CONTROL_DIRS:
try:
for name in sorted(os.listdir(directory)):
path = os.path.join(directory, name)
if os.path.exists(path) and path not in found:
found.append(path)
except OSError:
continue
return found
def wireless_interfaces() -> List[str]:
"""Namen der WLAN-Geräte laut Kernel."""
try:
return sorted(os.listdir("/sys/class/net")) and [
name for name in sorted(os.listdir("/sys/class/net"))
if os.path.exists(f"/sys/class/net/{name}/wireless")
or os.path.exists(f"/sys/class/net/{name}/phy80211")
]
except OSError:
return []
class WpaError(RuntimeError):
pass
class WpaClient:
"""Eine Verbindung zum Control-Socket eines Interfaces."""
def __init__(self, control_path: str):
self.control_path = control_path
self._local_path = os.path.join(
tempfile.gettempdir(), f"wfd-ctrl-{os.getpid()}-{id(self)}"
)
self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
try:
self._sock.bind(self._local_path)
self._sock.connect(control_path)
except OSError as error:
self._cleanup()
raise WpaError(
f"Kein Zugriff auf {control_path}: {error}. "
"Meist fehlen Rechte - als root ausführen oder der Gruppe netdev beitreten."
) from error
self._sock.settimeout(5)
self._lock = threading.Lock()
self.events: "queue.Queue[str]" = queue.Queue()
self._attached = False
self._reader: Optional[threading.Thread] = None
self._stop = threading.Event()
# ------------------------------------------------------------ Befehle
def request(self, command: str, timeout: float = 5.0) -> str:
"""Schickt einen Befehl und liefert die Antwort."""
with self._lock:
self._sock.settimeout(timeout)
self._sock.send(command.encode())
while True:
data = self._sock.recv(8192).decode("utf-8", "replace")
# Ereignisse können dazwischenfunken - die gehören in die Queue.
if data.startswith("<"):
self.events.put(data)
continue
return data.strip()
def ok(self, command: str) -> bool:
try:
return self.request(command).startswith("OK")
except OSError as error:
log.warning("Befehl '%s' fehlgeschlagen: %s", command, error)
return False
# ---------------------------------------------------------- Ereignisse
def attach(self):
"""Meldet sich für Ereignisse an und liest sie im Hintergrund mit."""
if self._attached:
return
if not self.request("ATTACH").startswith("OK"):
raise WpaError("wpa_supplicant nimmt keine Ereignis-Anmeldung an")
self._attached = True
self._reader = threading.Thread(target=self._read_events, daemon=True,
name="wpa-events")
self._reader.start()
def _read_events(self):
while not self._stop.is_set():
try:
with self._lock:
self._sock.settimeout(0.4)
data = self._sock.recv(8192).decode("utf-8", "replace")
except socket.timeout:
continue
except OSError:
break
for line in data.splitlines():
if line.startswith("<"):
# Prioritätsangabe wie "<3>" abtrennen
self.events.put(line.split(">", 1)[-1])
log.debug("Ereignis: %s", line)
def wait_for_event(self, *needles: str, timeout: float = 30.0) -> Optional[str]:
"""Wartet auf ein Ereignis, das eine der Zeichenketten enthält."""
import time
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
event = self.events.get(timeout=0.3)
except queue.Empty:
continue
if any(needle in event for needle in needles):
return event
return None
def close(self):
self._stop.set()
if self._reader:
self._reader.join(1.0)
try:
self._sock.close()
except OSError:
pass
self._cleanup()
def _cleanup(self):
try:
os.unlink(self._local_path)
except OSError:
pass
def parse_key_values(text: str) -> Dict[str, str]:
"""Wandelt die übliche 'schlüssel=wert'-Ausgabe in ein Wörterbuch."""
result = {}
for line in text.splitlines():
if "=" in line:
key, value = line.split("=", 1)
result[key.strip()] = value.strip()
return result