fix(station): Panel ermittelt die Adresse per Netlink und nennt das Geraet
Am echten Pi stand im Aus-Zustand "LAN: not connected", obwohl er unter 192.168.2.96 erreichbar war - derselbe build_state()-Aufruf von Hand lieferte die Adresse dagegen richtig. Ursache ist die Absicherung des Anzeigedienstes: ninlight-display.service setzt RestrictAddressFamilies=AF_UNIX AF_NETLINK, der UDP-Kniff in local_ip() legt aber ein AF_INET-Socket an. Das scheitert dort mit EAFNOSUPPORT, und die Zeile fiel auf "not connected" zurueck. Kein Test hat das gefunden, weil alle ohne diese Beschraenkung liefen. Statt die Absicherung zu lockern ermittelt local_address() die Adresse jetzt mit "ip -4 route get" - das spricht Netlink und bleibt damit innerhalb der Erlaubnis. Der Unterprozess faellt nur im Aus-Zustand an, dem einzigen Statusbild mit dieser Zeile. Dazu die Beschriftung: "LAN" war eine Falschauskunft, dieser Pi haengt per WLAN im Netz und hat gar kein Kabel. Der Kernel nennt in derselben Auskunft beides, also steht dort jetzt "wlan0: 192.168.2.96" (bzw. "eth0: ..." am Kabel) und ohne Netz "no home network" statt eines nackten "no network", das schon einmal auf den Rettungs-Zugangspunkt bezogen wurde. Der neue Test sperrt AF_INET wie die Unit und verlangt die Adresse trotzdem - genau die Luecke, die den Befund ueberhaupt durchgelassen hat.
This commit is contained in:
@@ -319,33 +319,54 @@ def _read_imageset(imageset_path: Path, conf: dict[str, str], mode: str) -> tupl
|
|||||||
return name, False
|
return name, False
|
||||||
|
|
||||||
|
|
||||||
def local_ip() -> str:
|
def _route_lookup() -> str:
|
||||||
"""Die Adresse, unter der der Pi im Heimnetz erreichbar ist, oder "" wenn keine da ist.
|
"""Die Rohauskunft von "ip -4 route get" zur Standardroute, oder "" wenn es keine gibt.
|
||||||
|
|
||||||
Ueber einen UDP-Socket, der nur verbunden und nie benutzt wird: das erzeugt keinen
|
Eigene Funktion, damit die Tests sie ersetzen koennen, ohne einen Unterprozess zu
|
||||||
Datenverkehr und fragt kein DNS, liefert aber genau die Adresse, die der Kernel fuer
|
starten - und damit local_address() nur noch das Auswerten macht."""
|
||||||
die Standardroute waehlen wuerde - also die, ueber die der Bediener tatsaechlich per
|
import subprocess
|
||||||
SSH hereinkommt. Ein Aufruf von "hostname -I" waere die naheliegende Alternative,
|
|
||||||
listet aber ALLE Adressen (inklusive der des Zugangspunkts und IPv6) und braucht einen
|
|
||||||
Unterprozess in einer Schleife, die alle 5 s laeuft.
|
|
||||||
|
|
||||||
Wirft nie: ohne Netz gibt es keine Route, dann liefert der Aufruf "" und die Anzeige
|
|
||||||
sagt das - eine Ausnahme aus der Adressermittlung darf das Panel nicht aufhalten."""
|
|
||||||
import socket
|
|
||||||
|
|
||||||
sock = None
|
|
||||||
try:
|
try:
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
fertig = subprocess.run(
|
||||||
# Die Adresse wird nie angesprochen; sie waehlt nur die Route aus. Bewusst eine
|
# Bewusst eine dokumentierte Testadresse (RFC 5737) statt eines echten Ziels
|
||||||
# dokumentierte Testadresse (RFC 5737) statt eines echten Ziels wie 8.8.8.8 -
|
# wie 8.8.8.8: "route get" fragt nur den Kernel, es geht kein Paket hinaus,
|
||||||
# es soll auch dann nichts Fremdes anklingen, wenn jemand mitliest.
|
# aber es soll auch beim Mitlesen nichts Fremdes anklingen.
|
||||||
sock.connect(("192.0.2.1", 9))
|
["ip", "-4", "route", "get", "192.0.2.1"],
|
||||||
return str(sock.getsockname()[0])
|
capture_output=True, text=True, timeout=2, check=False)
|
||||||
except OSError:
|
except (OSError, subprocess.SubprocessError):
|
||||||
return ""
|
return ""
|
||||||
finally:
|
return fertig.stdout if fertig.returncode == 0 else ""
|
||||||
if sock is not None:
|
|
||||||
sock.close()
|
|
||||||
|
def local_address() -> tuple[str, str]:
|
||||||
|
"""(Geraet, Adresse), unter der der Pi im Heimnetz erreichbar ist - ("", "") ohne Netz.
|
||||||
|
|
||||||
|
Ueber "ip route get", NICHT ueber einen verbundenen UDP-Socket: die Unit erlaubt dem
|
||||||
|
Anzeigedienst nur AF_UNIX und AF_NETLINK (ninlight-display.service:
|
||||||
|
RestrictAddressFamilies). Ein AF_INET-Socket scheitert dort mit EAFNOSUPPORT, und
|
||||||
|
genau daran ist die Adresszeile am echten Panel gescheitert, waehrend derselbe
|
||||||
|
Aufruf von Hand - ohne Sandbox - die Adresse lieferte (Rueckmeldung 2026-09-23).
|
||||||
|
"ip" spricht Netlink und laeuft damit innerhalb der Erlaubnis. Der Unterprozess
|
||||||
|
kostet einen fork alle POLL_INTERVAL_S Sekunden und faellt nur im Aus-Zustand an,
|
||||||
|
dem einzigen Statusbild mit dieser Zeile.
|
||||||
|
|
||||||
|
Das Geraet kommt mit, weil "LAN" am Panel eine Falschauskunft war: dieser Pi haengt
|
||||||
|
per WLAN im Netz, nicht am Kabel. Der Kernel nennt in derselben Auskunft beides -
|
||||||
|
"192.0.2.1 dev wlan0 src 192.168.2.96 uid 1000".
|
||||||
|
|
||||||
|
Wirft nie: ohne Route gibt es nichts auszuwerten, dann liefert der Aufruf ("", "")
|
||||||
|
und die Anzeige sagt das - eine Ausnahme aus der Adressermittlung darf das Panel
|
||||||
|
nicht aufhalten."""
|
||||||
|
worte = _route_lookup().split()
|
||||||
|
geraet = adresse = ""
|
||||||
|
for i, w in enumerate(worte):
|
||||||
|
if w == "dev" and i + 1 < len(worte) and not geraet:
|
||||||
|
geraet = worte[i + 1]
|
||||||
|
elif w == "src" and i + 1 < len(worte) and not adresse:
|
||||||
|
adresse = worte[i + 1]
|
||||||
|
# Ohne Adresse ist auch der Geraetename wertlos - er wuerde ein Netz vortaeuschen,
|
||||||
|
# das nicht steht.
|
||||||
|
return (geraet, adresse) if adresse else ("", "")
|
||||||
|
|
||||||
|
|
||||||
def build_state(run_dir: Path = RUN_DIR, conf_path: Path = CONF_PATH,
|
def build_state(run_dir: Path = RUN_DIR, conf_path: Path = CONF_PATH,
|
||||||
@@ -394,8 +415,9 @@ def build_state(run_dir: Path = RUN_DIR, conf_path: Path = CONF_PATH,
|
|||||||
# ist die Adresse, unter der man per SSH drankommt. Im Betrieb waere sie sinnlos
|
# ist die Adresse, unter der man per SSH drankommt. Im Betrieb waere sie sinnlos
|
||||||
# (wlan0 traegt dann 192.168.8.1, das steht ohnehin auf jedem Statusbild) und
|
# (wlan0 traegt dann 192.168.8.1, das steht ohnehin auf jedem Statusbild) und
|
||||||
# wuerde den knappen Platz fuer Betriebsauskuenfte wegnehmen.
|
# wuerde den knappen Platz fuer Betriebsauskuenfte wegnehmen.
|
||||||
|
geraet, adresse = local_address()
|
||||||
return {"mode": "off", "imageset": imageset, "imageset_ok": imageset_ok,
|
return {"mode": "off", "imageset": imageset, "imageset_ok": imageset_ok,
|
||||||
"ip": local_ip()}
|
"ip": adresse, "iface": geraet}
|
||||||
|
|
||||||
status = _read_json(run_dir / "status.json")
|
status = _read_json(run_dir / "status.json")
|
||||||
state: dict = {"mode": mode, "served": status.get("served"), "recent": status.get("recent"),
|
state: dict = {"mode": mode, "served": status.get("served"), "recent": status.get("recent"),
|
||||||
@@ -769,7 +791,7 @@ def run_daemon(revision: str, clock=time, panel: "Panel | None" = None,
|
|||||||
now = clock.monotonic()
|
now = clock.monotonic()
|
||||||
if now - last_state_poll >= POLL_INTERVAL_S:
|
if now - last_state_poll >= POLL_INTERVAL_S:
|
||||||
# Jeder einzelne Baustein von build_state() faengt seine eigenen I/O-Fehler
|
# Jeder einzelne Baustein von build_state() faengt seine eigenen I/O-Fehler
|
||||||
# schon ab (siehe _read_json/_read_conf/_service_active/local_ip oben) - aber
|
# schon ab (siehe _read_json/_read_conf/_service_active/local_address oben) - aber
|
||||||
# main() faengt um run_daemon() herum nur (DisplayUnavailable, ImportError)
|
# main() faengt um run_daemon() herum nur (DisplayUnavailable, ImportError)
|
||||||
# ab, nicht Exception im Allgemeinen. Ein einzelner unerwarteter Fehler beim
|
# ab, nicht Exception im Allgemeinen. Ein einzelner unerwarteter Fehler beim
|
||||||
# Erheben (z.B. ein kaputtes RUN_DIR mitten in einem Neustart) wuerde diese
|
# Erheben (z.B. ein kaputtes RUN_DIR mitten in einem Neustart) wuerde diese
|
||||||
|
|||||||
@@ -317,16 +317,23 @@ def blocks(state: dict) -> list[tuple[str, str]]:
|
|||||||
# die Zeile ausdruecklich, dass kein Netz da ist: "keine Zeile" und "kein Netz"
|
# die Zeile ausdruecklich, dass kein Netz da ist: "keine Zeile" und "kein Netz"
|
||||||
# sind zwei verschiedene Auskuenfte, und die zweite erklaert, warum SSH scheitert.
|
# sind zwei verschiedene Auskuenfte, und die zweite erklaert, warum SSH scheitert.
|
||||||
#
|
#
|
||||||
# Beschriftung "LAN" statt eines nackten "IP"/"no network": Rueckmeldung eines
|
# Beschriftet mit dem GERAET ("wlan0: 192.168.2.96"), nicht mit "LAN": derselbe
|
||||||
# Bedieners (2026-09-23) las "no network" so, als sei damit der Migrations-/
|
# Bediener las "LAN" als Falschauskunft, weil dieser Pi per WLAN im Netz haengt
|
||||||
# Rettungs-Zugangspunkt gemeint (der ja tatsaechlich aus ist, die Station ist
|
# und gar kein Kabel hat (Rueckmeldung 2026-09-23). Der Geraetename grenzt
|
||||||
# ausgeschaltet). Gemeint ist hier aber das GEGENTEIL - das gewoehnliche Heimnetz,
|
# ausserdem genauso eindeutig vom Zugangspunkt-Netz ab, das auf jedem ANDEREN
|
||||||
# in dem der Pi als normaler Rechner haengt, sobald die Station aus ist (siehe
|
# Statusbild als SSID steht - und er sagt zusaetzlich, WORUEBER der Pi haengt.
|
||||||
# Kommentar oben). "LAN" grenzt das eindeutig vom Zugangspunkt-Netz ab, das auf
|
# Ohne Netz "no home network" statt eines nackten "no network": letzteres wurde
|
||||||
# jedem ANDEREN Statusbild als SSID steht.
|
# auf den Migrations-/Rettungs-Zugangspunkt bezogen (der im Aus-Zustand ja
|
||||||
|
# tatsaechlich aus ist). Gemeint ist das GEGENTEIL - das gewoehnliche Heimnetz.
|
||||||
ip = state.get("ip")
|
ip = state.get("ip")
|
||||||
if isinstance(ip, str):
|
if isinstance(ip, str):
|
||||||
out.append((f"LAN: {ip}" if ip else "LAN: not connected", ROLE_DETAIL))
|
geraet = state.get("iface")
|
||||||
|
if not ip:
|
||||||
|
out.append(("no home network", ROLE_DETAIL))
|
||||||
|
elif isinstance(geraet, str) and geraet:
|
||||||
|
out.append((f"{geraet}: {ip}", ROLE_DETAIL))
|
||||||
|
else:
|
||||||
|
out.append((f"IP: {ip}", ROLE_DETAIL))
|
||||||
return out + footer
|
return out + footer
|
||||||
|
|
||||||
if mode not in ("recovery", "migration"):
|
if mode not in ("recovery", "migration"):
|
||||||
|
|||||||
@@ -21,35 +21,50 @@ def test_off_mode_says_so_and_shows_nothing_else():
|
|||||||
def test_off_mode_shows_the_address_the_pi_is_reachable_at():
|
def test_off_mode_shows_the_address_the_pi_is_reachable_at():
|
||||||
"""Bei ausgeschalteter Station ist der Pi ein gewoehnlicher Rechner im Heimnetz - die
|
"""Bei ausgeschalteter Station ist der Pi ein gewoehnlicher Rechner im Heimnetz - die
|
||||||
Adresse am Panel erspart die Suche im Router, bevor man per SSH drangeht."""
|
Adresse am Panel erspart die Suche im Router, bevor man per SSH drangeht."""
|
||||||
lines = render.lines({"mode": "off", "ip": "192.168.2.96"})
|
lines = render.lines({"mode": "off", "ip": "192.168.2.96", "iface": "wlan0"})
|
||||||
assert any("LAN: 192.168.2.96" in l for l in lines)
|
assert any("wlan0: 192.168.2.96" in l for l in lines)
|
||||||
|
|
||||||
|
|
||||||
|
def test_off_mode_names_the_interface_the_address_belongs_to():
|
||||||
|
"""Das Geraet gehoert dazu: Michael betreibt den Pi ohne Kabel, "LAN" las sich
|
||||||
|
deshalb als Falschauskunft (Rueckmeldung 2026-09-23). Am Kabel steht dort eth0."""
|
||||||
|
lines = render.lines({"mode": "off", "ip": "192.168.1.92", "iface": "eth0"})
|
||||||
|
assert any("eth0: 192.168.1.92" in l for l in lines)
|
||||||
|
assert not any("LAN" in l for l in lines)
|
||||||
|
|
||||||
|
|
||||||
|
def test_off_mode_shows_the_address_even_without_an_interface_name():
|
||||||
|
"""Notnagel: Adresse ohne Geraetenamen ist immer noch die Auskunft, die gebraucht
|
||||||
|
wird - dann ohne Beschriftung raten, sondern schlicht "IP"."""
|
||||||
|
lines = render.lines({"mode": "off", "ip": "192.168.2.96", "iface": ""})
|
||||||
|
assert any("IP: 192.168.2.96" in l for l in lines)
|
||||||
|
|
||||||
|
|
||||||
def test_off_mode_says_when_there_is_no_network():
|
def test_off_mode_says_when_there_is_no_network():
|
||||||
"""Leeres Feld heisst "kein Netz" und wird ausdruecklich gesagt: das erklaert, warum
|
"""Leeres Feld heisst "kein Netz" und wird ausdruecklich gesagt: das erklaert, warum
|
||||||
SSH gerade scheitert. Eine fehlende Zeile wuerde die Frage offen lassen.
|
SSH gerade scheitert. Eine fehlende Zeile wuerde die Frage offen lassen.
|
||||||
|
|
||||||
Beschriftet mit "LAN", nicht mit einem nackten "no network": ein Bediener las die
|
"no home network" statt eines nackten "no network": ein Bediener las die fruehere
|
||||||
frühere Formulierung so, als sei der Migrations-/Rettungs-Zugangspunkt gemeint - der
|
Formulierung so, als sei der Migrations-/Rettungs-Zugangspunkt gemeint - der ist im
|
||||||
ist im Aus-Zustand ja tatsaechlich aus. Gemeint ist aber das GEGENTEIL, das
|
Aus-Zustand ja tatsaechlich aus. Gemeint ist aber das GEGENTEIL, das Heimnetz, in dem
|
||||||
Heimnetz, in dem der Pi als gewoehnlicher Rechner haengt (siehe render.py)."""
|
der Pi als gewoehnlicher Rechner haengt (siehe render.py)."""
|
||||||
lines = render.lines({"mode": "off", "ip": ""})
|
lines = render.lines({"mode": "off", "ip": "", "iface": ""})
|
||||||
assert any("LAN: not connected" in l for l in lines)
|
assert any("no home network" in l for l in lines)
|
||||||
assert not any("LAN: " in l and "not connected" not in l for l in lines)
|
|
||||||
|
|
||||||
|
|
||||||
def test_off_mode_without_an_address_field_claims_nothing():
|
def test_off_mode_without_an_address_field_claims_nothing():
|
||||||
"""Aeltere display.py-Fassung (oder ein von Hand gebauter Zustand): ohne das Feld
|
"""Aeltere display.py-Fassung (oder ein von Hand gebauter Zustand): ohne das Feld
|
||||||
bleibt die Zeile weg, statt eine Adresse zu erfinden."""
|
bleibt die Zeile weg, statt eine Adresse zu erfinden."""
|
||||||
lines = render.lines({"mode": "off"})
|
lines = render.lines({"mode": "off"})
|
||||||
assert not any("LAN" in l for l in lines)
|
assert not any("home network" in l or ": 192." in l for l in lines)
|
||||||
|
|
||||||
|
|
||||||
def test_the_address_line_belongs_to_the_off_screen_only():
|
def test_the_address_line_belongs_to_the_off_screen_only():
|
||||||
"""Im Betrieb traegt wlan0 die Zugangspunkt-Adresse, die ohnehin auf jedem Statusbild
|
"""Im Betrieb traegt wlan0 die Zugangspunkt-Adresse, die ohnehin auf jedem Statusbild
|
||||||
steht - eine zweite Adresse waere irrefuehrend und kostet die knappe Zeile."""
|
steht - eine zweite Adresse waere irrefuehrend und kostet die knappe Zeile."""
|
||||||
for mode in ("migration", "recovery"):
|
for mode in ("migration", "recovery"):
|
||||||
lines = render.lines({"mode": mode, "phase": "auto", "ip": "192.168.2.96"})
|
lines = render.lines({"mode": mode, "phase": "auto", "ip": "192.168.2.96",
|
||||||
|
"iface": "wlan0"})
|
||||||
assert not any("192.168.2.96" in l for l in lines)
|
assert not any("192.168.2.96" in l for l in lines)
|
||||||
|
|
||||||
|
|
||||||
@@ -944,17 +959,18 @@ def test_build_state_off_when_service_not_active(tmp_path, monkeypatch):
|
|||||||
# "off" und sonst NICHTS aus status.json - nur der Bildsatz, der auch im
|
# "off" und sonst NICHTS aus status.json - nur der Bildsatz, der auch im
|
||||||
# ausgeschalteten Zustand gilt (und gerade dann gesehen werden soll).
|
# ausgeschalteten Zustand gilt (und gerade dann gesehen werden soll).
|
||||||
assert state["mode"] == "off"
|
assert state["mode"] == "off"
|
||||||
assert set(state) == {"mode", "imageset", "imageset_ok", "ip"}
|
assert set(state) == {"mode", "imageset", "imageset_ok", "ip", "iface"}
|
||||||
assert isinstance(state["ip"], str)
|
assert isinstance(state["ip"], str)
|
||||||
|
|
||||||
|
|
||||||
def test_build_state_off_carries_the_local_address(tmp_path, monkeypatch):
|
def test_build_state_off_carries_the_local_address(tmp_path, monkeypatch):
|
||||||
"""Die Adresse kommt aus local_ip() und landet nur im Off-Zustand."""
|
"""Adresse und Geraet kommen aus local_address() und landen nur im Off-Zustand."""
|
||||||
monkeypatch.setattr(display, "_service_active", lambda service_name: False)
|
monkeypatch.setattr(display, "_service_active", lambda service_name: False)
|
||||||
monkeypatch.setattr(display, "local_ip", lambda: "192.168.2.96")
|
monkeypatch.setattr(display, "local_address", lambda: ("wlan0", "192.168.2.96"))
|
||||||
state = display.build_state(run_dir=tmp_path, conf_path=tmp_path / "station.conf",
|
state = display.build_state(run_dir=tmp_path, conf_path=tmp_path / "station.conf",
|
||||||
service_name="x", imageset_path=tmp_path / "imageset")
|
service_name="x", imageset_path=tmp_path / "imageset")
|
||||||
assert state["ip"] == "192.168.2.96"
|
assert state["ip"] == "192.168.2.96"
|
||||||
|
assert state["iface"] == "wlan0"
|
||||||
|
|
||||||
|
|
||||||
def test_build_state_running_carries_no_address(tmp_path, monkeypatch):
|
def test_build_state_running_carries_no_address(tmp_path, monkeypatch):
|
||||||
@@ -966,10 +982,45 @@ def test_build_state_running_carries_no_address(tmp_path, monkeypatch):
|
|||||||
assert "ip" not in state
|
assert "ip" not in state
|
||||||
|
|
||||||
|
|
||||||
def test_local_ip_returns_a_string_and_never_raises():
|
def test_local_address_returns_strings_and_never_raises():
|
||||||
"""Ohne Netz gibt es keine Route - dann "" statt einer Ausnahme, die das Panel
|
"""Ohne Netz gibt es keine Route - dann ("", "") statt einer Ausnahme, die das Panel
|
||||||
aufhielte."""
|
aufhielte."""
|
||||||
assert isinstance(display.local_ip(), str)
|
iface, ip = display.local_address()
|
||||||
|
assert isinstance(iface, str) and isinstance(ip, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_local_address_works_without_af_inet(monkeypatch):
|
||||||
|
"""Der Anzeigedienst darf laut Unit NUR AF_UNIX und AF_NETLINK (siehe
|
||||||
|
ninlight-display.service:RestrictAddressFamilies). Ein AF_INET-Socket scheitert dort
|
||||||
|
mit EAFNOSUPPORT - genau daran ist die Adresszeile am echten Panel gescheitert,
|
||||||
|
waehrend derselbe Aufruf von Hand (ohne Sandbox) die Adresse lieferte. Dieser Test
|
||||||
|
sperrt AF_INET wie die Unit und verlangt die Adresse trotzdem."""
|
||||||
|
import socket as _socket
|
||||||
|
|
||||||
|
def kein_af_inet(family, *a, **kw):
|
||||||
|
if family == _socket.AF_INET:
|
||||||
|
raise OSError(97, "Address family not supported by protocol")
|
||||||
|
raise AssertionError("unerwartete Socketfamilie: %r" % (family,))
|
||||||
|
|
||||||
|
monkeypatch.setattr(_socket, "socket", kein_af_inet)
|
||||||
|
monkeypatch.setattr(display, "_route_lookup",
|
||||||
|
lambda: "192.0.2.1 dev wlan0 src 192.168.2.96 uid 1000")
|
||||||
|
assert display.local_address() == ("wlan0", "192.168.2.96")
|
||||||
|
|
||||||
|
|
||||||
|
def test_local_address_reads_interface_and_source_from_the_route(monkeypatch):
|
||||||
|
"""Das Geraet gehoert mit auf das Panel: Michael hat kein Kabel, "LAN" las sich
|
||||||
|
deshalb falsch (Rueckmeldung 2026-09-23). Beide Angaben stehen in derselben
|
||||||
|
Auskunft."""
|
||||||
|
monkeypatch.setattr(display, "_route_lookup",
|
||||||
|
lambda: "192.0.2.1 dev eth0 src 192.168.1.92 uid 1000 \n cache")
|
||||||
|
assert display.local_address() == ("eth0", "192.168.1.92")
|
||||||
|
|
||||||
|
|
||||||
|
def test_local_address_without_a_route(monkeypatch):
|
||||||
|
"""Kein Netz: "ip route get" meldet einen Fehler und liefert nichts Brauchbares."""
|
||||||
|
monkeypatch.setattr(display, "_route_lookup", lambda: "")
|
||||||
|
assert display.local_address() == ("", "")
|
||||||
|
|
||||||
|
|
||||||
# --- Lebenszeichen: ein kleiner Punkt, sobald eine Anfrage jeder Art hereinkommt -------
|
# --- Lebenszeichen: ein kleiner Punkt, sobald eine Anfrage jeder Art hereinkommt -------
|
||||||
@@ -1616,7 +1667,7 @@ def _changing_every(period_s, mode="recovery"):
|
|||||||
# --- Lebenszeichen im Aus-Zustand: die IP muss auftauchen, sobald sie da ist -----------
|
# --- Lebenszeichen im Aus-Zustand: die IP muss auftauchen, sobald sie da ist -----------
|
||||||
#
|
#
|
||||||
# Rueckmeldung eines Bedieners (2026-09-23): am laufenden Pi blieb "no network" stehen,
|
# Rueckmeldung eines Bedieners (2026-09-23): am laufenden Pi blieb "no network" stehen,
|
||||||
# obwohl der Pi laengst im Heimnetz haengt. local_ip() liefert die Adresse dabei
|
# obwohl der Pi laengst im Heimnetz haengt. local_address() liefert die Adresse dabei
|
||||||
# nachweislich korrekt - der Verdacht war also die Anzeigeschleife: wird der Aus-Zustand
|
# nachweislich korrekt - der Verdacht war also die Anzeigeschleife: wird der Aus-Zustand
|
||||||
# ueberhaupt regelmaessig neu erhoben, und fliesst die IP in den Vergleich ein, der ueber
|
# ueberhaupt regelmaessig neu erhoben, und fliesst die IP in den Vergleich ein, der ueber
|
||||||
# ein Neuzeichnen entscheidet? Dieser Test treibt run_daemon() GENAU durch diesen Fall:
|
# ein Neuzeichnen entscheidet? Dieser Test treibt run_daemon() GENAU durch diesen Fall:
|
||||||
@@ -1626,9 +1677,10 @@ def test_off_mode_redraws_once_the_address_appears(monkeypatch):
|
|||||||
"""Haelt fest: run_daemon() erhebt den Zustand auch im Aus-Zustand unbedingt alle
|
"""Haelt fest: run_daemon() erhebt den Zustand auch im Aus-Zustand unbedingt alle
|
||||||
POLL_INTERVAL_S Sekunden neu (kein Ueberspringen mangels Moduswechsel), und die IP
|
POLL_INTERVAL_S Sekunden neu (kein Ueberspringen mangels Moduswechsel), und die IP
|
||||||
ist Teil des Vergleichs, der ueber ein Neuzeichnen entscheidet (render.lines() traegt
|
ist Teil des Vergleichs, der ueber ein Neuzeichnen entscheidet (render.lines() traegt
|
||||||
sie, siehe render.py). Schlaegt dieser Test fehl, bleibt "LAN: not connected" auf dem
|
sie, siehe render.py). Schlaegt dieser Test fehl, bleibt "no home network" auf dem
|
||||||
Panel stehen, obwohl der Pi laengst erreichbar ist."""
|
Panel stehen, obwohl der Pi laengst erreichbar ist."""
|
||||||
seq = [{"mode": "off", "ip": ""}, {"mode": "off", "ip": "192.168.2.96"}]
|
seq = [{"mode": "off", "ip": "", "iface": ""},
|
||||||
|
{"mode": "off", "ip": "192.168.2.96", "iface": "wlan0"}]
|
||||||
|
|
||||||
def state_at(t):
|
def state_at(t):
|
||||||
# Erste POLL_INTERVAL_S-Erhebung (bei t=0) sieht noch keine Adresse, danach schon -
|
# Erste POLL_INTERVAL_S-Erhebung (bei t=0) sieht noch keine Adresse, danach schon -
|
||||||
@@ -1647,7 +1699,7 @@ def test_off_mode_redraws_once_the_address_appears(monkeypatch):
|
|||||||
"kein weiterer Aufbau nach dem ersten - die neue Adresse waere nie gezeichnet worden",
|
"kein weiterer Aufbau nach dem ersten - die neue Adresse waere nie gezeichnet worden",
|
||||||
panel.calls,
|
panel.calls,
|
||||||
)
|
)
|
||||||
assert any("LAN: 192.168.2.96" in line for line in render.lines(state_at(4 * display.POLL_INTERVAL_S)))
|
assert any("wlan0: 192.168.2.96" in line for line in render.lines(state_at(4 * display.POLL_INTERVAL_S)))
|
||||||
|
|
||||||
|
|
||||||
def test_run_daemon_survives_a_single_failed_state_read(monkeypatch):
|
def test_run_daemon_survives_a_single_failed_state_read(monkeypatch):
|
||||||
@@ -1658,7 +1710,7 @@ def test_run_daemon_survives_a_single_failed_state_read(monkeypatch):
|
|||||||
jede Fehlermeldung am Geraet. Erhebung 1 wirft, Erhebung 2 liefert einen echten
|
jede Fehlermeldung am Geraet. Erhebung 1 wirft, Erhebung 2 liefert einen echten
|
||||||
Zustand - danach muss dieser auch tatsaechlich gezeichnet sein."""
|
Zustand - danach muss dieser auch tatsaechlich gezeichnet sein."""
|
||||||
calls = {"n": 0}
|
calls = {"n": 0}
|
||||||
good_state = {"mode": "off", "ip": "192.168.2.96"}
|
good_state = {"mode": "off", "ip": "192.168.2.96", "iface": "wlan0"}
|
||||||
|
|
||||||
def flaky_state_at(t):
|
def flaky_state_at(t):
|
||||||
calls["n"] += 1
|
calls["n"] += 1
|
||||||
|
|||||||
Reference in New Issue
Block a user