[Unit] Description=ninlight Taster (Bedienung der Station ueber GPIO23) # Wie beim Anzeigedienst nur After=, NICHT Requires=/BindsTo=/Wants=: ein fehlender oder # abgestuerzter Taster darf server.py niemals mitreissen, und server.py muss starten # koennen, auch wenn dieser Dienst fehlt oder deaktiviert ist. KEIN # Wants=ninlight-station.service - das wuerde die absichtlich deaktivierte Station bei # jedem Boot mit hochziehen (Fund der Hardware-Abnahme, ausfuehrlich in # ninlight-display.service begruendet). After=ninlight-station.service # Gehoeren seit systemd 229 ins [Unit], nicht ins [Service] (dort werden sie mit # "Unknown key name" ignoriert, und die Deckelung unten greift dann nie). StartLimitIntervalSec=60 StartLimitBurst=5 [Service] Type=simple Environment=NINLIGHT_RUN_DIR=/run/ninlight-station Environment=NINLIGHT_UI_FILE=/run/ninlight-button/ui.json # "-"-Praefix VOR dem Dateipfad (nicht vor dem Schluesselnamen - so herum ist es ein # ungueltiger Schluessel und systemd verwirft die Zeile ganz): ohne die # Konfigurationsdatei startet der Taster trotzdem. EnvironmentFile=-/etc/ninlight-station.conf # Eigenes RuntimeDirectory, aus zwei Gruenden: # # 1. Hier liegt ui.json. Es darf NICHT in /run/ninlight-station liegen (dem # RuntimeDirectory der Hauptunit): systemd raeumt das beim regulaeren Stop von # ninlight-station auf - also ausgerechnet im Zustand "Station aus", in dem das Menue # ("Rettung starten"/"Migration starten") am dringendsten gebraucht wird. So # verschwindet ui.json genau dann, wenn dieser Dienst endet, und ein # stehengebliebenes Menue kann ihn nicht ueberdauern. # 2. lgpio (gpiozero's bevorzugte Pin-Factory) legt beim Start "$HOME/.lgd-nfy0" an. # Unter systemd ist HOME per Vorgabe LEER und das Arbeitsverzeichnis "/" - der Pfad # wird "//.lgd-nfy0" und scheitert. Am Geraet fuer den Anzeigedienst nachgemessen, # siehe dort; derselbe Stolperstein trifft jeden gpiozero-Dienst. RuntimeDirectory=ninlight-button RuntimeDirectoryMode=0755 WorkingDirectory=/run/ninlight-button Environment=HOME=/run/ninlight-button ExecStart=/usr/bin/python3 /opt/ninlight-station/button/button.py # button.py faengt einen fehlenden Taster (keine Bibliothek, kein Pin, Pin belegt) als # ButtonUnavailable ab, protokolliert eine Zeile und beendet sich mit sys.exit(0) - das # ist nie ein "Fehlschlag" im systemd-Sinne und loest kein Restart= aus. on-failure # (nicht always) deckelt damit genau das Richtige: echte Abstuerze werden neu versucht, # ein dauerhaft fehlender Taster nicht. Restart=on-failure RestartSec=5 # BEWUSST KEIN NoNewPrivileges=/ProtectSystem=strict/ProtectHome= hier - anders als beim # Anzeigedienst, und das ist kein Versehen: # # Dieser Dienst ruft /usr/local/bin/ninlight-station auf, und dessen einziger Zweck ist # es, NetworkManager-Profile umzuschalten, Units zu starten/stoppen und nach # /run/ninlight-station zu schreiben - alles ueber "sudo". Kindprozesse erben den # Namensraum und das no_new_privs-Flag dieser Unit: mit ProtectSystem=strict waere # /run/ninlight-station fuer das CLI schreibgeschuetzt (mode/control.json liessen sich # nicht mehr schreiben), und no_new_privs laesst kein setuid-Binary mehr zu (sudo). Eine # Absicherung, die genau die Aktionen unmoeglich macht, wegen derer es den Dienst gibt, # ist keine. Die Angriffsflaeche ist stattdessen eng gehalten: EIN Eingangspin, kein # Netzwerk, keine Eingabe ausser der Taste selbst, und ausgefuehrt wird ausschliesslich # die feste Liste von CLI-Unterbefehlen aus button.py. MemoryMax=64M # Laeuft als root (wie Haupt- und Anzeigedienst) fuer GPIO23 und fuer das CLI - kein # SupplementaryGroups=gpio: auf reinem Debian gibt es die Gruppe oft gar nicht, und eine # fehlende Gruppe ist fuer systemd ein harter Startfehler (216/GROUP), ausgerechnet in # einer Unit, die nie stoeren darf. # KEIN Zugriff auf SPI und keine Zeichenleitung: der Tasterdienst fasst den Panel-Bus # nicht an (zwei Prozesse darauf ergeben "lgpio.error: 'GPIO busy'"). Er schreibt seinen # Bedienzustand nach /run/ninlight-button/ui.json, der Anzeigedienst liest ihn und # zeichnet. [Install] WantedBy=multi-user.target