diff --git a/tools/station/ninlight-station b/tools/station/ninlight-station index 4329ffa..3ace251 100755 --- a/tools/station/ninlight-station +++ b/tools/station/ninlight-station @@ -130,8 +130,33 @@ need_profile() { } } -images() { - find "$IMAGE_DIR" -maxdepth 1 -name 'ninlight_*_ota.img' 2>/dev/null | sort +# --- Modus lesen/schreiben (Fixrunde 1, Befund 3) ----------------------------------- +# +# RUN_DIR/mode (write_mode() unten schreibt sie zusaetzlich) taugt NICHT als Quelle +# fuer "welcher Modus gilt gerade/als naechstes", solange die Station AUS ist: die Unit +# setzt RuntimeDirectoryPreserve=restart, das erhaelt /run/ninlight-station nur ueber +# einen "systemctl restart" (das, was "recovery on"/"migrate on" bei einem schon +# laufenden Dienst ausloesen), NICHT ueber einen echten "systemctl stop" - und genau +# den fuehrt "ninlight-station off" aus. Fehlt die Datei, gilt ueberall stillschweigend +# "recovery" (derselbe sichere Vorgabewert wie beim Bildsatz) - eine fruehere Fassung +# dieses Skripts UND von display.py hat das mit "die Datei bleibt bei jedem Zustand +# erhalten" verwechselt. +# +# LAST_MODE_FILE ist deshalb wie IMAGESET_FILE eine EIGENE, dauerhafte Ablage unter +# /var/lib - dieselbe Datei, die display.py:LAST_MODE_FILE liest (Panel und CLI muessen +# sich hier einig sein, sonst zeigt das eine "Rettung: MISSING" waehrend das andere +# "Migration: 3 image(s)" meldet). +LAST_MODE_FILE=${NINLIGHT_LAST_MODE_FILE:-/var/lib/ninlight-station/mode} + +# Der zuletzt angeforderte Modus, oder "recovery" bei fehlender/unlesbarer/unbekannter +# Datei - dieselbe Vorgabe und dieselbe Begruendung wie imageset_get() unten. Scheitert +# nie: eine kaputte Datei darf weder "status" noch "bildsatz" aufhalten. +current_mode() { + _m=$(cat "$LAST_MODE_FILE" 2>/dev/null | tr -d ' \t\r\n') || true + case "$_m" in + recovery|migration) printf '%s' "$_m" ;; + *) printf '%s' "recovery" ;; + esac } # --- Bildsatz lesen, schreiben, benennen -------------------------------------------- @@ -153,12 +178,27 @@ imageset_get() { esac } -# Das Verzeichnis zum Namen des Bildsatzes ($1). +# Das Verzeichnis zum Namen des Bildsatzes ($1), fuer den Modus $2 (Vorgabe: +# current_mode()). Rettung und Migration liegen in getrennten Verzeichnispaaren +# (server.py:RESCUE_SET_DIRS/IMAGE_SET_DIRS begruendet das) - dieselbe Unterscheidung +# wie display.py:_read_imageset(). Fixrunde 1, Befund 1: vor dieser Aenderung pruefte +# dieses Skript hier IMMER die Migrationsverzeichnisse, auch im Rettungsbetrieb - ein +# falsches "MISSING" dort verleitet dazu, auf mit-wlan umzuschalten, und DANN liefert +# die Rettung tatsaechlich Abbilder mit dem WLAN-Passwort aus. imageset_dir() { - case "$1" in - mit-wlan) printf '%s' "$IMAGE_DIR/migrate-wlan" ;; - *) printf '%s' "$IMAGE_DIR/migrate-nocreds" ;; - esac + _name=$1 + _mode=${2:-$(current_mode)} + if [ "$_mode" = "recovery" ]; then + case "$_name" in + mit-wlan) printf '%s' "$IMAGE_DIR/rescue-wlan" ;; + *) printf '%s' "$IMAGE_DIR/rescue-nocreds" ;; + esac + else + case "$_name" in + mit-wlan) printf '%s' "$IMAGE_DIR/migrate-wlan" ;; + *) printf '%s' "$IMAGE_DIR/migrate-nocreds" ;; + esac + fi } imageset_label() { @@ -179,8 +219,14 @@ imageset_set() { sudo mv -f "$_tmp" "$IMAGESET_FILE" } -migrate_images() { - find "$(imageset_dir "$(imageset_get)")" -maxdepth 1 -name '*.img' 2>/dev/null | sort +# Die Abbilder des eingestellten Bildsatzes fuer den Modus $1 (Vorgabe: current_mode()). +# Ersetzt das fruehere migrate_images() (nur Migration) UND das fruehere images() (immer +# flach in $IMAGE_DIR, Fixrunde 1, Befund 2: nach dem Deploy-Umbau liegt dort nichts +# mehr - recovery_on() meldete seither bei JEDEM Start faelschlich "answers everything +# with 404", ununterscheidbar vom echten Fehlerfall). +imageset_images() { + _mode=${1:-$(current_mode)} + find "$(imageset_dir "$(imageset_get)" "$_mode")" -maxdepth 1 -name '*.img' 2>/dev/null | sort } # "bildsatz" ohne Argument: nur berichten. Mit Argument: umschalten. @@ -204,6 +250,9 @@ bildsatz_cmd() { } echo "Image set: $(imageset_label "$_want")" [ "$_now" = "$_want" ] && echo " (was already set)" + # Der Umschalter gilt fuer BEIDE Betriebsarten - hier wird deshalb der Satz gemeldet, + # der fuer den Modus zaehlt, in dem die Station gerade steht bzw. als naechstes + # anlaufen wuerde (current_mode()), nicht pauschal die Migration. _dir=$(imageset_dir "$_want") if [ "$(find "$_dir" -maxdepth 1 -name '*.img' 2>/dev/null | wc -l | tr -d ' ')" -eq 0 ]; then # Kein stiller Rueckfall auf den anderen Satz - server.py liefert dann gar nichts @@ -212,18 +261,20 @@ bildsatz_cmd() { echo "Warning: $_dir missing or empty - this image set serves NOTHING. Run tools/station/deploy.sh again." >&2 fi if [ "$_want" = "mit-wlan" ]; then - echo "Caution: migrated lamps join the Wi-Fi from config.local.cmake right away - including lamps that are not yours and got picked up in automatic mode." >&2 + echo "Caution: any lamp migrated OR rescued while this is set joins the Wi-Fi from config.local.cmake right away - including lamps that are not yours and got picked up in automatic mode." >&2 fi } -# Die zwei Zeilen, die "status" und "bildsatz" ohne Argument gemeinsam ausgeben. +# Die zwei Zeilen, die "status" und "bildsatz" ohne Argument gemeinsam ausgeben - fuer +# den Modus, in dem die Station gerade steht bzw. als naechstes anlaufen wuerde. imageset_status_lines() { + _mode=$(current_mode) _set=$(imageset_get) - _dir=$(imageset_dir "$_set") + _dir=$(imageset_dir "$_set" "$_mode") # "Vorhanden" heisst: mindestens ein Image darin - dieselbe Regel wie - # server.py:migrate_set_present() und display.py:_read_imageset(). Ein leeres - # Verzeichnis liefert genauso nichts aus wie ein fehlendes. - _n=$(migrate_images | wc -l | tr -d ' ') + # server.py:migrate_set_present()/rescue_set_present() und display.py:_read_imageset(). + # Ein leeres Verzeichnis liefert genauso nichts aus wie ein fehlendes. + _n=$(imageset_images "$_mode" | wc -l | tr -d ' ') if [ "$_n" -gt 0 ]; then _state="$_n image(s)" else @@ -293,12 +344,22 @@ ap_switch() { } # Schreibt $RUN_DIR/mode atomar mit 0644 (Temp-Datei im selben Verzeichnis, dann -# umbenennen) - server.py liest sie bei jedem Dienststart per --mode-file. +# umbenennen) - server.py liest sie bei jedem Dienststart per --mode-file. ZUSAETZLICH +# dieselbe Zeichenkette nach $LAST_MODE_FILE (dauerhaft, siehe dort) - RUN_DIR/mode +# ueberlebt nur einen "systemctl restart", nicht das "systemctl stop" von +# "ninlight-station off" (Fixrunde 1, Befund 3). Beide Schreibvorgaenge muessen +# gelingen: schlaegt der dauerhafte fehl, wuerden Panel und CLI nach einem "off" auf +# den falschen (naemlich den vorherigen) Modus zurueckfallen, ohne dass das hier +# auffiele. write_mode() { sudo mkdir -p "$RUN_DIR" _tmp="$RUN_DIR/.mode-$$" printf '%s' "$1" | sudo tee "$_tmp" >/dev/null && sudo chmod 0644 "$_tmp" || return 1 - sudo mv -f "$_tmp" "$RUN_DIR/mode" + sudo mv -f "$_tmp" "$RUN_DIR/mode" || return 1 + sudo mkdir -p "$(dirname "$LAST_MODE_FILE")" + _tmp2="$(dirname "$LAST_MODE_FILE")/.mode-$$" + printf '%s' "$1" | sudo tee "$_tmp2" >/dev/null && sudo chmod 0644 "$_tmp2" || return 1 + sudo mv -f "$_tmp2" "$LAST_MODE_FILE" } # Schreibt $RUN_DIR/control.json atomar mit 0644 - dasselbe Muster (Temp-Datei im @@ -612,8 +673,12 @@ autooff_arm_if_requested() { recovery_on() { parse_for_option recovery "$@" need_profile "$AP_CONNECTION" - n=$(images | wc -l | tr -d ' ') - [ "$n" -gt 0 ] || echo "Warning: $IMAGE_DIR holds no image - the station answers everything with 404" >&2 + # Explizit "recovery" statt current_mode(): dieser Aufruf ENTSCHEIDET gerade, dass + # der naechste Start Rettung ist - write_mode() darunter schreibt das erst gleich. + # current_mode() koennte hier noch "migration" von einem vorherigen Lauf zeigen. + _setdir=$(imageset_dir "$(imageset_get)" recovery) + n=$(imageset_images recovery | wc -l | tr -d ' ') + [ "$n" -gt 0 ] || echo "Warning: $_setdir holds no image - the station answers everything with 404" >&2 ap_switch "$AP_CONNECTION" write_mode "recovery" start_and_verify "$AP_CONNECTION" "recovery" @@ -656,8 +721,10 @@ migrate_on() { [ "$_for_seen" -eq 1 ] || _for_minutes="" need_profile "$MIGRATE_AP_CONNECTION" _set=$(imageset_get) - _setdir=$(imageset_dir "$_set") - n=$(migrate_images | wc -l | tr -d ' ') + # Explizit "migration", aus demselben Grund wie in recovery_on(): dieser Aufruf legt + # den naechsten Modus gerade erst fest. + _setdir=$(imageset_dir "$_set" migration) + n=$(imageset_images migration | wc -l | tr -d ' ') [ "$n" -gt 0 ] || echo "Warning: $_setdir holds no migration images - the station answers everything with 404" >&2 # Faehrt ninlight-recovery (falls es laeuft) herunter, BEVOR qconnex-firmware # hochkommt, und prueft danach nach, dass wirklich nur eines aktiv ist. @@ -1052,8 +1119,12 @@ PY autooff_status_line printf 'wlan0 : %s\n' "$(nmcli -t --escape no -f GENERAL.CONNECTION device show wlan0 2>/dev/null | cut -d: -f2-)" printf 'Address : %s\n' "$(ip -4 -br addr show wlan0 2>/dev/null | awk '{print $3}')" + # Die Rettungsabbilder des eingestellten Bildsatzes - unabhaengig von $_modus (der + # Migrationsbildsatz hat kein passendes Trailer-Format). Fixrunde 1, Befund 2: + # frueher stand hier "images" (flach in $IMAGE_DIR) - dort liegt seit dem + # Deploy-Umbau nichts mehr. echo "Images :" - images | while read -r f; do + imageset_images recovery | while read -r f; do printf ' %-34s %s\n' "$(basename "$f")" \ "$(python3 - "$f" <<'PY' import pathlib, sys @@ -1092,7 +1163,7 @@ display_cmd() { usage() { echo "Usage: ninlight-station on [--for ]|recovery {on [--for ]|off}|migrate {on [--listen] [--for ]|off}|arm []|listen|bildsatz [ohne-wlan|mit-wlan]|renew []|hold|display {test|clear|off}|off|status" >&2 echo "" >&2 - echo " bildsatz Switch: which set of migration images is served." >&2 + echo " bildsatz Switch: which set of recovery/migration images is served." >&2 echo " Without an argument it only reports. 'ohne-wlan' (default) serves" >&2 echo " the builds from tools/build_nocreds.sh - the migrated lamp comes" >&2 echo " up in its own onboarding access point. 'mit-wlan' serves the" >&2 diff --git a/tools/station/tests/test_cli.sh b/tools/station/tests/test_cli.sh index 13db2d3..a9d9b71 100755 --- a/tools/station/tests/test_cli.sh +++ b/tools/station/tests/test_cli.sh @@ -263,12 +263,19 @@ setup_sandbox() { local sandbox=$1 unitfile=${2:-$REAL_UNIT} rm -rf "$sandbox" mkdir -p "$sandbox/run" "$sandbox/images/migrate-nocreds" "$sandbox/images/migrate-wlan" \ + "$sandbox/images/rescue-nocreds" "$sandbox/images/rescue-wlan" \ "$sandbox/bin" "$sandbox/nmstate" "$sandbox/lib/display" "$sandbox/var" # Je ein Image je Bildsatz - "vorhanden" heisst fuer CLI, Server und Anzeige - # gleichermassen "mindestens ein Image darin". + # gleichermassen "mindestens ein Image darin". Getrennte Verzeichnispaare fuer + # Rettung (rescue-*) und Migration (migrate-*), seit der Bildsatz-Umschalter auch + # die Rettung betrifft (Fixrunde 1, Befund 1/2) - VORHER lag hier ein flaches + # "images/ninlight_foo_ota.img", das server.py/deploy.sh seit diesem Umschalter + # nicht mehr benutzen; eine Testvorrichtung, die weiter diese alte Welt aufbaut, + # haette genau die beiden Befunde verdeckt. echo "ohne" >"$sandbox/images/migrate-nocreds/foo_dev.img" echo "mit" >"$sandbox/images/migrate-wlan/foo_dev.img" - echo "dummy" >"$sandbox/images/ninlight_foo_ota.img" + echo "ohne" >"$sandbox/images/rescue-nocreds/ninlight_foo_ota.img" + echo "mit" >"$sandbox/images/rescue-wlan/ninlight_foo_ota.img" # NINLIGHT_STATION_LIB zeigt auf diese Sandbox statt auf tools/station selbst. # products.py wird gebraucht (arm prueft das Ziel dagegen) und deshalb echt kopiert; @@ -342,6 +349,7 @@ run_cli() { NINLIGHT_RUN_DIR="$sandbox/run" \ NINLIGHT_STATION_LIB="$sandbox/lib" \ NINLIGHT_IMAGESET_FILE="$sandbox/var/imageset" \ + NINLIGHT_LAST_MODE_FILE="$sandbox/var/last-mode" \ "$CLI" "$@" } @@ -1278,7 +1286,7 @@ RC=$? set -e [ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT" echo "$OUT" | grep -q "ohne-wlan" && pass "ohne Datei gilt 'ohne-wlan'" || fail "Vorgabe ist nicht 'ohne-wlan': $OUT" -echo "$OUT" | grep -q "migrate-nocreds" && pass "und das zugehoerige Verzeichnis wird genannt" || fail "Verzeichnis fehlt in der Ausgabe: $OUT" +echo "$OUT" | grep -q "rescue-nocreds" && pass "und das zugehoerige Verzeichnis wird genannt (Vorgabemodus: recovery)" || fail "Verzeichnis fehlt in der Ausgabe: $OUT" [ ! -e "$SBX/var/imageset" ] && pass "das blosse Berichten legt keine Datei an" || fail "'bildsatz' ohne Argument hat geschrieben" echo @@ -1323,7 +1331,7 @@ setup_sandbox "$SBX" printf 'voellig-anderes\n' >"$SBX/var/imageset" OUT=$(run_cli "$SBX" bildsatz 2>&1) echo "$OUT" | grep -q "ohne-wlan" && pass "kaputter Inhalt -> 'ohne-wlan'" || fail "kaputter Inhalt fuehrte nicht auf die Vorgabe: $OUT" -echo "$OUT" | grep -q "migrate-nocreds" && pass "und das Verzeichnis des sicheren Satzes" || fail "falsches Verzeichnis: $OUT" +echo "$OUT" | grep -q "rescue-nocreds" && pass "und das Verzeichnis des sicheren Satzes (Vorgabemodus: recovery)" || fail "falsches Verzeichnis: $OUT" # Der Bediener muss erfahren, dass sein Eintrag NICHT gewirkt hat - sonst haelt er die # Station fuer umgeschaltet. echo "$OUT" | grep -q "unknown setting 'voellig-anderes'" \ @@ -1340,7 +1348,7 @@ echo echo "=== Bildsatz E: ein gewaehlter, aber fehlender Satz wird beim Umschalten UND im Status gemeldet ===" SBX="$TMPROOT/bs5" setup_sandbox "$SBX" -rm -rf "$SBX/images/migrate-wlan" +rm -rf "$SBX/images/rescue-wlan" # Vorgabemodus (ohne vorheriges "on") ist recovery set +e ERR=$(run_cli "$SBX" bildsatz mit-wlan 2>&1 >/dev/null) RC=$? @@ -1354,7 +1362,7 @@ echo echo "=== Bildsatz F: ein LEERES Verzeichnis zaehlt genauso als fehlend ===" SBX="$TMPROOT/bs6" setup_sandbox "$SBX" -rm -f "$SBX/images/migrate-nocreds"/*.img +rm -f "$SBX/images/rescue-nocreds"/*.img # Vorgabemodus (ohne vorheriges "on") ist recovery OUT=$(run_cli "$SBX" status 2>&1) echo "$OUT" | grep -q "MISSING" && pass "ein leeres Bildsatzverzeichnis wird als fehlend gemeldet" || fail "ein leerer Satz gilt als vorhanden: $OUT"