server.py legt status.json ueber mkstemp() (Modus 0600) + os.replace() an; da os.replace() den Modus der temporaeren Datei uebernimmt, war die Datei nach jedem Schreiben nur fuer root lesbar - der Dienst laeuft als root, das CLI liest aber als normaler Benutzer. os.chmod(tmp, 0o644) vor dem Rename behebt das umask-fest, auch bei wiederholten Schreibvorgaengen. ninlight-station status hat den fehlenden Lesezugriff bisher stillschweigend wie "noch nie ausgeliefert" behandelt (beides fiel durch denselben "-r"-Test). Jetzt wird zwischen "Datei fehlt" (unauffaellig) und "Datei da, aber nicht lesbar" (Fehlermeldung auf stderr) unterschieden. Neuer Test prueft, dass status.json nach dem ersten und nach einem weiteren Schreibvorgang world-readable bleibt, auch unter restriktivem umask. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
136 lines
6.2 KiB
Bash
Executable File
136 lines
6.2 KiB
Bash
Executable File
#!/bin/sh
|
|
# Schaltet die ninlight-Recovery-Station an und aus.
|
|
#
|
|
# Die Station ist absichtlich nicht dauerhaft an: Solange sie laeuft, kann jeder
|
|
# in Funkreichweite mit dem Passwort einer Lampe beim naechsten Boot beliebige
|
|
# Firmware unterschieben, und der Werksreset per fuenf Stromzyklen funktioniert
|
|
# bei keiner Lampe in Reichweite mehr (die Firmware loescht den Zaehler vor
|
|
# jedem Recovery-Versuch).
|
|
set -eu
|
|
|
|
CONF=/etc/ninlight-station.conf
|
|
if [ -r "$CONF" ]; then
|
|
# shellcheck disable=SC1090
|
|
. "$CONF"
|
|
elif [ -e "$CONF" ]; then
|
|
# $CONF ist 0600 root:root, damit das Passwort nicht fuer jeden lesbar
|
|
# ist. Der dokumentierte Aufruf "ssh <pi-host> ninlight-station on" laeuft
|
|
# aber als normaler Benutzer - "sudo cat" liest die Datei ein, ohne ihre
|
|
# Rechte aufzuweichen.
|
|
eval "$(sudo cat "$CONF")"
|
|
else
|
|
echo "Fehler: $CONF fehlt - zuerst tools/station/setup_pi.sh ausfuehren" >&2
|
|
exit 1
|
|
fi
|
|
|
|
for _key in SSID PSK IMAGE_DIR HOME_CONNECTION AP_CONNECTION; do
|
|
eval "test -n \"\${$_key-}\"" || {
|
|
echo "Fehler: $CONF: Schluessel $_key fehlt oder ist leer - tools/station/setup_pi.sh ausfuehren" >&2
|
|
exit 1
|
|
}
|
|
done
|
|
unset _key
|
|
|
|
need_profile() {
|
|
nmcli -t -f NAME con show | grep -qx "$AP_CONNECTION" || {
|
|
echo "Fehler: NetworkManager-Profil '$AP_CONNECTION' fehlt - tools/station/setup_pi.sh ausfuehren" >&2
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
images() {
|
|
find "$IMAGE_DIR" -maxdepth 1 -name 'ninlight_*_ota.img' 2>/dev/null | sort
|
|
}
|
|
|
|
case "${1:-}" in
|
|
on)
|
|
need_profile
|
|
n=$(images | wc -l | tr -d ' ')
|
|
[ "$n" -gt 0 ] || echo "Warnung: $IMAGE_DIR enthaelt kein Image - die Station antwortet auf alles mit 404" >&2
|
|
sudo nmcli con up "$AP_CONNECTION" >/dev/null
|
|
# Eine fruehere Absturzserie darf den naechsten ehrlichen Versuch nicht
|
|
# blockieren (systemd verweigert "start" sonst sofort mit "start-limit-hit").
|
|
sudo systemctl reset-failed ninlight-station 2>/dev/null || true
|
|
if ! sudo systemctl start ninlight-station; then
|
|
# Der Start-Aufruf selbst ist gescheitert (z.B. start-limit-hit) - unter
|
|
# set -eu wuerde das Skript hier sonst abbrechen, bevor die
|
|
# Lebendigkeitspruefung unten je erreicht wird, und der eben
|
|
# hochgefahrene Zugangspunkt bliebe offen ohne Server dahinter stehen.
|
|
sudo nmcli con down "$AP_CONNECTION" >/dev/null 2>&1 || true
|
|
echo "Fehler: ninlight-station liess sich nicht starten - Zugangspunkt wieder abgeschaltet. Details: journalctl -u ninlight-station" >&2
|
|
exit 1
|
|
fi
|
|
# "systemctl start" meldet bei Type=simple schon Erfolg, sobald der Prozess
|
|
# gestartet wurde - nicht, sobald er ueberlebt hat. Kurz warten und wirklich
|
|
# nachsehen, sonst bleibt ein offener AP ohne Server dahinter stehen.
|
|
sleep 1
|
|
if systemctl is-active --quiet ninlight-station; then
|
|
echo "Station an: SSID $SSID auf 192.168.8.1, $n Image(s)"
|
|
echo "Nicht vergessen: 'ninlight-station off', wenn du fertig bist."
|
|
else
|
|
# Mit Restart=always laeuft ein abgestuerzter Dienst sonst im Hintergrund
|
|
# weiter in seiner Neustartschleife - erst stoppen, dann den AP runter,
|
|
# sonst suggeriert die Meldung unten einen saubereren Zustand als er ist.
|
|
sudo systemctl stop ninlight-station 2>/dev/null || true
|
|
sudo nmcli con down "$AP_CONNECTION" >/dev/null 2>&1 || true
|
|
echo "Fehler: ninlight-station ist nach dem Start nicht aktiv - Zugangspunkt wieder abgeschaltet. Details: journalctl -u ninlight-station" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
off)
|
|
sudo systemctl stop ninlight-station 2>/dev/null || true
|
|
sudo nmcli con down "$AP_CONNECTION" >/dev/null 2>&1 || true
|
|
if nmcli -t -f NAME con show | grep -qx "$HOME_CONNECTION"; then
|
|
if sudo nmcli con up "$HOME_CONNECTION" >/dev/null; then
|
|
echo "Station aus, wlan0 zurueck im Heim-WLAN ($HOME_CONNECTION)"
|
|
else
|
|
echo "Station aus. Zurueckschalten zu '$HOME_CONNECTION' fehlgeschlagen - wlan0-Status pruefen." >&2
|
|
fi
|
|
else
|
|
echo "Station aus. Profil '$HOME_CONNECTION' fehlt, wlan0 bleibt ungenutzt - der Zugang ueber Kabel ist davon nicht betroffen." >&2
|
|
fi
|
|
;;
|
|
status)
|
|
# "systemctl is-active" schreibt den Zustand ("inactive", "failed", ...) auf stdout und
|
|
# liefert dabei einen Rueckgabewert ungleich 0, sobald der Dienst nicht "active" ist -
|
|
# ein "|| echo inaktiv" wuerde also zusaetzlich zur ersten Zeile ausgeloest und beide
|
|
# landen in der Ausgabe. Der Zustand selbst ist die Auskunft; "status" ist damit
|
|
# erfolgreich, sobald es ihn ermitteln konnte, unabhaengig vom Dienstzustand - deshalb
|
|
# am Ende explizit exit 0.
|
|
printf 'Dienst : %s\n' "$(systemctl is-active ninlight-station 2>/dev/null || true)"
|
|
printf 'wlan0 : %s\n' "$(nmcli -t -f GENERAL.CONNECTION device show wlan0 2>/dev/null | cut -d: -f2)"
|
|
printf 'Adresse: %s\n' "$(ip -4 -br addr show wlan0 2>/dev/null | awk '{print $3}')"
|
|
echo "Images :"
|
|
images | while read -r f; do
|
|
printf ' %-34s %s\n' "$(basename "$f")" \
|
|
"$(python3 - "$f" <<'PY'
|
|
import pathlib, sys
|
|
t = pathlib.Path(sys.argv[1]).read_bytes()[-512:]
|
|
print(t[21:37].rstrip(b"\x00").decode("ascii", "replace") if t[:4] == b"NINL" else "kein Trailer")
|
|
PY
|
|
)"
|
|
done
|
|
# "status" soll mit 0 enden, sobald es seine Auskunft geben konnte, auch wenn noch nie
|
|
# ausgeliefert wurde - ohne das "|| true" waere der Exit-Code des Skripts hier der des
|
|
# fehlgeschlagenen Tests (1), weil das die letzte Anweisung im Zweig ist.
|
|
#
|
|
# "existiert nicht" (Station lief noch nie) und "existiert, ist aber nicht lesbar"
|
|
# sind zwei verschiedene Zustaende und duerfen nicht beide stillschweigend im
|
|
# "-r"-Test verschwinden: sonst faellt ein Rechte-Fehler auf status.json (siehe
|
|
# server.py, schreibt als root) nie auf, weil die Ausgabe genauso aussieht wie
|
|
# "noch nie ausgeliefert".
|
|
STATUS_FILE=/run/ninlight-station/status.json
|
|
if [ -r "$STATUS_FILE" ]; then
|
|
echo "Zuletzt ausgeliefert:"
|
|
sed 's/^/ /' "$STATUS_FILE"
|
|
elif [ -e "$STATUS_FILE" ]; then
|
|
echo "Fehler: $STATUS_FILE existiert, ist aber nicht lesbar (Rechte pruefen)" >&2
|
|
fi
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Aufruf: ninlight-station on|off|status" >&2
|
|
exit 2
|
|
;;
|
|
esac
|