fix(station): Panel liest den Modus aus einer dauerhaften Datei, nicht aus RUN_DIR
Fixrunde 1, Befund 3: RUN_DIR/mode ueberlebt nur einen "systemctl restart" (RuntimeDirectoryPreserve=restart) - "ninlight-station off" fuehrt aber einen echten "systemctl stop" aus, der das RuntimeDirectory leert. build_state() fiel danach unbemerkt auf "recovery" zurueck, auch wenn tatsaechlich eine Migration anstand - die Fusszeile haette dann "in Ordnung" gezeigt, obwohl der Migrationssatz leer ist. Ein Docstring behauptete ausdruecklich das Gegenteil. _read_mode_file() liest jetzt eine neue, dauerhafte LAST_MODE_FILE (unter /var/lib/ninlight-station, wie der Bildsatz-Umschalter selbst) statt RUN_DIR/mode; write_mode() im CLI schreibt beide. Ausserdem: _read_imageset() faengt jetzt auch UnicodeDecodeError ab (Fixrunde 1, Kleinigkeit). Ein neuer Test bildet den echten Ablauf nach (Modus schreiben, RUN_DIR wie bei einem echten Stop leeren, pruefen dass der Modus trotzdem stimmt) - der alte Test fuer den Rettungs-Bildsatz liess die Modusdatei ganz weg und pruefte damit nur den zufaellig gleichen Rueckfallwert, nicht die Wirklichkeit.
This commit is contained in:
@@ -67,6 +67,30 @@ UI_FILE = Path(os.environ.get("NINLIGHT_UI_FILE", "/run/ninlight-button/ui.json"
|
||||
# schreibt server.py nichts mehr.
|
||||
IMAGESET_FILE = Path(os.environ.get("NINLIGHT_IMAGESET_FILE",
|
||||
"/var/lib/ninlight-station/imageset"))
|
||||
# Der Modus, den "recovery on"/"migrate on" zuletzt angefordert haben - fuer die
|
||||
# Fusszeile GENAUSO dauerhaft gebraucht wie der Bildsatz-Umschalter oben, aus demselben
|
||||
# Grund und nach demselben Muster (eigene Datei unter /var/lib, NICHT unter RUN_DIR).
|
||||
#
|
||||
# Fixrunde 1, Befund 3: RUN_DIR/mode taugt dafuer NICHT. Die Unit setzt
|
||||
# RuntimeDirectoryPreserve=restart - das erhaelt /run/ninlight-station nur ueber einen
|
||||
# "systemctl restart" hinweg (das, was "recovery on"/"migrate on" fuer einen bereits
|
||||
# laufenden Dienst ausloesen). "ninlight-station off" aber STOPPT den Dienst
|
||||
# (systemctl stop), und GENAU DANN raeumt systemd das RuntimeDirectory leer, "restart"
|
||||
# deckt einen echten Stop nicht ab. Ein frueherer Docstring hier behauptete das
|
||||
# Gegenteil ("die Datei ueberlebt das Abschalten") - das war schlicht falsch, und der
|
||||
# Fehler war unauffaellig, weil eine fehlende Datei auf "recovery" faellt: genau der
|
||||
# Modus, den viele Tests ohnehin erwarteten, ohne dass die Faelligkeit der Datei
|
||||
# je geprueft wurde. Die Folge am Geraet waere die falsche Beruhigung, gegen die die
|
||||
# Fusszeile ueberhaupt gebaut ist - "in Ordnung" vor einer geplanten Migration, obwohl
|
||||
# der Migrationssatz leer ist, weil das Panel mangels Datei den Rettungssatz geprueft
|
||||
# haette.
|
||||
#
|
||||
# write_mode() im CLI schreibt deshalb NEBEN der ephemeren RUN_DIR/mode (die
|
||||
# ausschliesslich server.py's naechsten Start betrifft) auch hierher - dieselbe
|
||||
# Zeichenkette, zwei Ablagen fuer zwei verschiedene Leser mit verschiedenen
|
||||
# Haltbarkeitsanforderungen.
|
||||
LAST_MODE_FILE = Path(os.environ.get("NINLIGHT_LAST_MODE_FILE",
|
||||
"/var/lib/ninlight-station/mode"))
|
||||
# Dieselben Namen und dieselbe Vorgabe wie in server.py und im CLI - im Zweifel der
|
||||
# Satz OHNE Zugangsdaten (dort steht die Begruendung).
|
||||
IMAGESET_DIRS = {"ohne-wlan": "migrate-nocreds", "mit-wlan": "migrate-wlan"}
|
||||
@@ -215,13 +239,19 @@ def _recent_request(status: dict, now: float | None = None) -> bool:
|
||||
return 0 <= age_s <= ACTIVITY_WINDOW_S
|
||||
|
||||
|
||||
def _read_mode_file(run_dir: Path) -> str:
|
||||
"""Modus, in dem der Dienst laut RUN_DIR/mode tatsaechlich gestartet wurde. Fehlt die
|
||||
Datei oder ist ihr Inhalt kein bekannter Modus, gilt "recovery" - derselbe Vorgabewert
|
||||
wie in server.py:resolve_mode()."""
|
||||
def _read_mode_file(path: Path) -> str:
|
||||
"""Zuletzt angeforderter Modus laut `path`. Fehlt die Datei, ist sie unlesbar
|
||||
(inklusive ungueltigem UTF-8 - eine halb geschriebene/beschaedigte Datei ist nicht
|
||||
nur per fehlendem Inhalt kaputt), oder ist ihr Inhalt kein bekannter Modus, gilt
|
||||
"recovery" - derselbe Vorgabewert wie in server.py:resolve_mode(). Wirft nie: eine
|
||||
kaputte Datei darf weder das Panel noch "off" aufhalten.
|
||||
|
||||
`path` ist bewusst eine Datei, kein Verzeichnis (anders als frueher, wo hier
|
||||
`run_dir / "mode"` gelesen wurde) - siehe LAST_MODE_FILE oben, warum die Fusszeile
|
||||
NICHT mehr aus RUN_DIR liest."""
|
||||
try:
|
||||
value = (run_dir / "mode").read_text().strip()
|
||||
except OSError:
|
||||
value = path.read_text().strip()
|
||||
except (OSError, UnicodeDecodeError):
|
||||
return "recovery"
|
||||
return value if value in ("recovery", "migration") else "recovery"
|
||||
|
||||
@@ -275,7 +305,7 @@ def _read_imageset(imageset_path: Path, conf: dict[str, str], mode: str) -> tupl
|
||||
dann gilt "ok", denn eine unbelegte Warnung ist schlechter als keine."""
|
||||
try:
|
||||
value = imageset_path.read_text().strip()
|
||||
except OSError:
|
||||
except (OSError, UnicodeDecodeError):
|
||||
value = ""
|
||||
name = value if value in IMAGESET_DIRS else IMAGESET_DEFAULT
|
||||
image_dir = conf.get("IMAGE_DIR")
|
||||
@@ -320,16 +350,18 @@ def local_ip() -> str:
|
||||
|
||||
def build_state(run_dir: Path = RUN_DIR, conf_path: Path = CONF_PATH,
|
||||
service_name: str = SERVICE_NAME,
|
||||
imageset_path: Path = IMAGESET_FILE) -> dict:
|
||||
imageset_path: Path = IMAGESET_FILE,
|
||||
last_mode_path: Path = LAST_MODE_FILE) -> dict:
|
||||
"""Baut den zusammengefuehrten Zustand, den render.lines()/compose() erwarten.
|
||||
|
||||
Modus kommt vorrangig aus RUN_DIR/mode (Vorgabe "recovery", wie
|
||||
Modus kommt vorrangig aus `last_mode_path` (Vorgabe "recovery", wie
|
||||
server.py:resolve_mode()) - das ist das, was "ninlight-station on"/"migrate on"
|
||||
zuletzt geschrieben haben und unabhaengig davon verfuegbar, ob/wann der Serverprozess
|
||||
seit dem letzten Start schon einmal status.json geschrieben hat. status.json selbst
|
||||
liefert nur served/recent/last_seen; sein "mode"-Feld wird bewusst NICHT als
|
||||
Modusquelle verwendet (Fixrunde 1: die urspruengliche Fassung hatte den Vorrang
|
||||
umgekehrt).
|
||||
zuletzt geschrieben haben, dauerhaft (siehe LAST_MODE_FILE oben - NICHT die ephemere
|
||||
RUN_DIR/mode, die server.py fuer den naechsten Start liest) und unabhaengig davon
|
||||
verfuegbar, ob/wann der Serverprozess seit dem letzten Start schon einmal
|
||||
status.json geschrieben hat. status.json selbst liefert nur served/recent/
|
||||
last_seen; sein "mode"-Feld wird bewusst NICHT als Modusquelle verwendet
|
||||
(Fixrunde 1: die urspruengliche Fassung hatte den Vorrang umgekehrt).
|
||||
|
||||
Phase/armed_target kommen aus control.json, nur im Migrationsmodus massgeblich - der
|
||||
Rettungsmodus ignoriert control.json ebenso wie server.py das tut. "off" ueberschreibt
|
||||
@@ -342,17 +374,18 @@ def build_state(run_dir: Path = RUN_DIR, conf_path: Path = CONF_PATH,
|
||||
|
||||
Ob "imageset_ok" den Rettungs- oder den Migrations-Bildsatz prueft, richtet sich nach
|
||||
dem MODUS, in dem die Station beim naechsten Einschalten anlaufen wuerde - also
|
||||
RUN_DIR/mode, nicht danach, ob sie gerade laeuft. Deshalb wird der Modus HIER, vor der
|
||||
"ist der Dienst aktiv"-Abfrage, gelesen: er ist auch im Zustand "off" verfuegbar (die
|
||||
Datei ueberlebt das Abschalten) und genau dort soll die Vorschau ja gelten - "off"
|
||||
zeigt ja=noch keinen Modus im engeren Sinn, aber der naechste Start faehrt in genau
|
||||
diesen Modus, und die Fusszeile soll VORHER zeigen, ob dessen Bildsatz bereitliegt.
|
||||
`last_mode_path`, nicht danach, ob sie gerade laeuft. Deshalb wird der Modus HIER,
|
||||
vor der "ist der Dienst aktiv"-Abfrage, gelesen: er ist auch im Zustand "off"
|
||||
verfuegbar (die Datei ist dauerhaft, siehe LAST_MODE_FILE) und genau dort soll die
|
||||
Vorschau ja gelten - "off" zeigt ja noch keinen Modus im engeren Sinn, aber der
|
||||
naechste Start faehrt in genau diesen Modus, und die Fusszeile soll VORHER zeigen,
|
||||
ob dessen Bildsatz bereitliegt.
|
||||
|
||||
"activity" (nur in Recovery/Migration, siehe _recent_request()) sagt, ob die letzte
|
||||
Anfrage jeder Art hoechstens ACTIVITY_WINDOW_S Sekunden her ist - das Lebenszeichen,
|
||||
das render.blocks() an die Servierzeile anhaengt."""
|
||||
conf = _read_conf(conf_path)
|
||||
mode = _read_mode_file(run_dir)
|
||||
mode = _read_mode_file(last_mode_path)
|
||||
imageset, imageset_ok = _read_imageset(imageset_path, conf, mode)
|
||||
|
||||
if not _service_active(service_name):
|
||||
|
||||
Reference in New Issue
Block a user