fix(station): Anzeige-Installation absichern, apt darf nur warnen

Fixrunde 1 zu Task 6b.

- setup_pi.sh raeumt die Ablage vor dem scp ab und bricht ab, wenn
  display.py dort fehlt. Ein Reststand aus einem abgebrochenen Lauf liess
  "scp -r" ins vorhandene Ziel hineinkopieren; die Installationsschleife
  ueberspringt Verzeichnisse und fand nichts - der Lauf meldete Erfolg,
  aktivierte den Anzeigedienst und hinterliess ein leeres display/:
  203/EXEC im 5-Sekunden-Takt bei schwarzem Panel.
- Der Paketschritt steht jetzt hinter der Installation und warnt nur.
  Eine Migrationsstation steht typischerweise nicht am Netz; unter set -e
  davor hiess ein gescheitertes apt: gar keine Station statt einer Station
  ohne Panel. "apt-get update" kommt vor dem install, der Anzeigedienst
  startet nach dem Paketschritt.
- setup_pi.sh raeumt display/ auf dem Pi wie deploy.sh auf (im Repo
  geloeschte Dateien verschwinden), der Trap haengt auch an INT/TERM/HUP,
  und mktemp bekommt eine Vorlage: ohne sie beachtet BSD-mktemp TMPDIR
  nicht, und die Temp-Datei landete je nach Plattform woanders.
- test_host.sh ruft test_deploy.sh auf - 95 Zusagen liefen nur bei
  Handaufruf, darunter die Deploy-Haelfte der Anzeige-Auslieferung. Der
  Coverage-Test deckt jetzt auch die Shell-Suiten ab und zaehlt dabei nur
  nicht auskommentierte Zeilen.
- Neue Szenarien fuer alle vier Punkte, darunter die Zusage, dass beim
  gescheiterten Schreiben eines Profils keine Datei mit dem WLAN-Passwort
  liegenbleibt.
This commit is contained in:
Michael Sauer
2026-09-14 00:03:49 +02:00
parent cc398ef57a
commit 7fdccbce72
5 changed files with 325 additions and 31 deletions
+95 -22
View File
@@ -77,24 +77,21 @@ fi
rm -f "$TEMPLATE"
REMOTE
echo "== Pakete =="
# Pillow kommt auf dem Pi bewusst aus dem Distributionspaket (python3-pil), nicht per
# pip: tools/station/ laeuft dort unter dem System-Python 3.13, und Debian 13 laesst ein
# "pip install" ins System-Python gar nicht mehr zu (PEP 668). Ohne Pillow zeichnet die
# Anzeige nicht - display.py faengt den ImportError ab und beendet sich still mit 0
# (Absicht, damit ein fehlendes Panel nie eine Migration verhindert). Genau deshalb muss
# das Paket HIER kommen: sonst faellt es nirgends auf.
echo "== Server, CLI und Units installieren =="
# Reststand der Ablage wegraeumen, BEVOR das scp laeuft (Task 6b F1, I1). "scp -r quelle
# ziel" kopiert das Quellverzeichnis IN ein bereits vorhandenes Ziel hinein - aus
# einem liegengebliebenen "…/ninlight-display/" wird dann "…/ninlight-display/display/",
# die Installationsschleife unten ueberspringt Verzeichnisse und findet gar nichts mehr.
# Ergebnis waere ein Lauf, der Erfolg meldet, den Anzeigedienst aktiviert und ein leeres
# $ROOT_DIR/display hinterlaesst: 203/EXEC im 5-Sekunden-Takt, Panel schwarz. Genau so
# ein Reststand entsteht, wenn ein frueherer Lauf nach dem scp abbricht (sudo-Timeout,
# Platte voll, Strg-C) - die Aufraeumzeile unten laeuft dann nicht mehr. Idempotent
# heisst, dass auch dieser Fall glattgeht (siehe Kopf dieser Datei).
"$SSH_BIN" "$HOST" bash -s <<'REMOTE'
set -e
if python3 -c 'import PIL' 2>/dev/null; then
echo " python3-pil ist bereits vorhanden"
else
sudo apt-get install -y python3-pil
echo " python3-pil installiert"
fi
STAGE_DIR=${NINLIGHT_STAGE_DIR:-/tmp}
rm -rf "$STAGE_DIR/ninlight-display"
REMOTE
echo "== Server, CLI und Units installieren =="
"$SCP_BIN" -q "$HERE/server.py" "$HERE/products.py" "$HERE/ninlight-station" \
"$HERE/ninlight-station.service" "$HERE/ninlight-display.service" "$HOST:$STAGE_DIR/"
# Das Anzeigeverzeichnis als Ganzes: render.py, display.py, epdconfig.py, die beiden
@@ -120,6 +117,15 @@ sudo install -m 0644 "$STAGE_DIR/ninlight-station.service" "$UNIT_PATH"
# Anzeige: display.py ist ausfuehrbar (die Unit ruft es zwar ueber /usr/bin/python3 auf,
# "ninlight-station display test" aber auch von Hand), der Rest ist importierter Code.
rm -rf "$STAGE_DIR/ninlight-display/__pycache__"
# Harte Vorbedingung (Task 6b F1, I1): ohne display.py in der Ablage hat das scp nicht
# geliefert, was es sollte - abgebrochen, falsch verschachtelt oder gar nicht gelaufen.
# Die Schleife darunter wuerde das stillschweigend als "nichts zu tun" verbuchen, der
# Lauf meldete Erfolg und der aktivierte Anzeigedienst liefe gegen eine nicht
# existierende Datei. Lieber hier laut abbrechen.
if [ ! -f "$STAGE_DIR/ninlight-display/display.py" ]; then
echo "Fehler: $STAGE_DIR/ninlight-display/display.py fehlt - die Uebertragung des Anzeigeverzeichnisses ist fehlgeschlagen. Nichts installiert." >&2
exit 1
fi
for f in "$STAGE_DIR"/ninlight-display/*; do
[ -f "$f" ] || continue
case "$(basename "$f")" in
@@ -127,21 +133,76 @@ for f in "$STAGE_DIR"/ninlight-display/*; do
*) sudo install -m 0644 "$f" "$ROOT_DIR/display/$(basename "$f")" ;;
esac
done
# Was im Repo geloescht wurde, muss auch hier verschwinden - dasselbe Argument wie in
# deploy.sh (dort per "rsync --delete"): display/ ist reiner Code aus dem Repo, es gibt
# keinen Bestand, der einen Lauf ueberleben muesste, und ein zurueckgebliebenes altes
# Modul wuerde weiter mit importiert. rsync steht hier nicht zur Verfuegung (setup_pi.sh
# setzt ausdruecklich nur ssh/scp voraus), also von Hand.
for f in "$ROOT_DIR"/display/*; do
[ -e "$f" ] || continue
if [ ! -e "$STAGE_DIR/ninlight-display/$(basename "$f")" ]; then
sudo rm -rf "$f"
echo " veraltete Datei entfernt: display/$(basename "$f")"
fi
done
sudo install -m 0644 "$STAGE_DIR/ninlight-display.service" "$DISPLAY_UNIT_PATH"
rm -rf "$STAGE_DIR/server.py" "$STAGE_DIR/products.py" "$STAGE_DIR/ninlight-station" \
"$STAGE_DIR/ninlight-station.service" "$STAGE_DIR/ninlight-display.service" \
"$STAGE_DIR/ninlight-display"
sudo systemctl daemon-reload
echo " installiert"
REMOTE
echo "== Pakete =="
# Pillow kommt auf dem Pi bewusst aus dem Distributionspaket (python3-pil), nicht per
# pip: tools/station/ laeuft dort unter dem System-Python 3.13, und Debian 13 laesst ein
# "pip install" ins System-Python gar nicht mehr zu (PEP 668). Ohne Pillow zeichnet die
# Anzeige nicht - display.py faengt den ImportError ab und beendet sich still mit 0
# (Absicht, damit ein fehlendes Panel nie eine Migration verhindert). Genau deshalb muss
# das Paket ueberhaupt hier kommen: sonst faellt es nirgends auf.
#
# Aber NACH der Installation und nur als Warnung (Task 6b F1, I4): eine Migrationsstation
# steht typischerweise nicht am Netz, und mit veralteten Paketlisten oder ohne Internet
# scheitert apt. Unter "set -e" vor der Installation haette das den Lauf abgebrochen,
# bevor server.py, das CLI, beide Units und beide NetworkManager-Profile installiert sind
# - man haette dann GAR KEINE Station statt einer Station ohne Panel. Das waere die
# einzige harte Stelle in einer sonst durchgehend weichen Kette gewesen (display.py
# beendet sich mit 0, deploy.sh warnt nur, enable/restart unten warnen nur).
"$SSH_BIN" "$HOST" bash -s <<'REMOTE'
set -e
if python3 -c 'import PIL' 2>/dev/null; then
echo " python3-pil ist bereits vorhanden"
else
# "update" zuerst: auf einem laenger nicht gepflegten Pi zeigen die Paketlisten auf
# Versionen, die es im Archiv nicht mehr gibt, und "install" scheitert mit 404.
# Beide Aufrufe duerfen nur warnen - siehe oben.
if sudo apt-get update >/dev/null 2>&1 && sudo apt-get install -y python3-pil; then
echo " python3-pil installiert"
else
echo " Warnung: python3-pil liess sich nicht installieren (kein Netz? veraltete Paketlisten?)." >&2
echo " Die Station ist vollstaendig eingerichtet, das E-Paper bleibt aber leer," >&2
echo " bis 'sudo apt install python3-pil' durchlaeuft und der Anzeigedienst neu startet." >&2
fi
fi
REMOTE
echo "== Anzeigedienst aktivieren =="
# Die Anzeige wird aktiviert (startet also beim Booten mit) UND jetzt gestartet - anders
# als die Station selbst, die absichtlich aus bleibt: die Anzeige sendet nichts, sie
# zeigt nur an, und "Station aus" ist genau das, was nach dem Einrichten auf dem Panel
# stehen soll. Fehlt Panel oder Pillow, beendet sich der Dienst still mit 0 (siehe
# display.py) - deshalb "|| true": ein fehlendes Panel darf setup_pi.sh nicht abbrechen.
# stehen soll. Erst hier, nach dem Paketschritt: so startet der Dienst bereits mit
# vorhandenem Pillow, statt sich beim ersten Zeichnen still mit 0 zu beenden und bis zum
# naechsten manuellen Neustart nichts anzuzeigen.
#
# Fehlt Panel oder Pillow, beendet sich der Dienst still mit 0 (siehe display.py) -
# deshalb nur Warnungen: ein fehlendes Panel darf setup_pi.sh nicht abbrechen.
"$SSH_BIN" "$HOST" bash -s <<'REMOTE'
set -e
sudo systemctl enable ninlight-display.service >/dev/null 2>&1 || \
echo " Warnung: 'systemctl enable ninlight-display' fehlgeschlagen" >&2
echo " Warnung: 'systemctl enable ninlight-display' fehlgeschlagen - die Anzeige startet nach einem Neustart des Pi nicht von allein mit" >&2
sudo systemctl restart ninlight-display.service >/dev/null 2>&1 || \
echo " Warnung: 'systemctl restart ninlight-display' fehlgeschlagen" >&2
echo " installiert (Anzeigedienst aktiviert)"
echo " Warnung: 'systemctl restart ninlight-display' fehlgeschlagen - journalctl -u ninlight-display" >&2
echo " Anzeigedienst aktiviert"
REMOTE
echo "== NetworkManager-Profile =="
@@ -171,8 +232,20 @@ eval "$(sudo cat "$CONF")"
# "$tmp" ist "local" - beim Auswerten des Traps am Skriptende gaebe es die
# Variable gar nicht mehr. Deshalb ein gemeinsames Verzeichnis mit genau einem
# Trap darum herum (0700, mktemp -d legt es so an).
PROFILE_TMPDIR=$(mktemp -d)
trap 'rm -rf "$PROFILE_TMPDIR"' EXIT
#
# Auch auf INT/TERM/HUP, nicht nur EXIT: bash fuehrt den EXIT-Trap zwar nach einem
# abgefangenen Signal aus, aber nur, wenn das Signal ueberhaupt abgefangen wird - ohne
# eigenen Handler beendet ein Strg-C (INT) oder ein abbrechendes ssh (HUP) die Shell
# sofort, und der EXIT-Trap laeuft nie. Genau dann bliebe die Datei liegen.
#
# Mit ausdruecklicher Vorlage unter ${TMPDIR:-/tmp}, nicht als blankes "mktemp -d":
# BSD-mktemp (macOS, wo diese Suite auch laeuft) beachtet TMPDIR ohne Vorlage gar nicht
# und legt unabhaengig davon unter /var/folders/... an, GNU-mktemp (der Pi) dagegen
# schon. Ohne Vorlage landet das Verzeichnis also je nach Plattform woanders - ein Test,
# der nachsieht, ob wirklich nichts liegengeblieben ist, sucht dann auf macOS an der
# falschen Stelle und ist gruen, ohne etwas geprueft zu haben.
PROFILE_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/ninlight-profile.XXXXXX")
trap 'rm -rf "$PROFILE_TMPDIR"' EXIT INT TERM HUP
# $1 = Profilname, $2 = SSID, $3 = PSK, $4 = "update" (bestehendes Profil neu schreiben,
# UUID behalten) oder "keep" (bestehendes Profil unangetastet lassen).