166 lines
5.5 KiB
Python
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
|