From 3c3dbd6c8b58ca01175361ba9744b5fbc9dd1fad Mon Sep 17 00:00:00 2001 From: Jules Date: Sat, 27 Jun 2026 13:21:02 +0200 Subject: [PATCH] =?UTF-8?q?Watchdog=20systemd=20+=20fix=20stabilit=C3=A9?= =?UTF-8?q?=20kiosk/backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Watchdog (30s) : vérifie backend, caméra, chromium, tue les orphelins, reboot auto après 10 échecs consécutifs (~5min sans service) - kiosk-session.sh : tue les backends orphelins au démarrage, trap EXIT pour cleanup quand le kiosk est tué - camera.py : force release USB avec timeout 2s quand connecter() échoue - main.py : uvicorn.run(app) au lieu de string pour éviter double import Co-Authored-By: Claude Opus 4.6 --- backend/camera.py | 14 ++++-- backend/main.py | 3 +- scripts/kiosk-session.sh | 36 ++++++-------- scripts/watchdog.service | 8 ++++ scripts/watchdog.sh | 101 +++++++++++++++++++++++++++++++++++++++ scripts/watchdog.timer | 10 ++++ 6 files changed, 144 insertions(+), 28 deletions(-) create mode 100644 scripts/watchdog.service create mode 100644 scripts/watchdog.sh create mode 100644 scripts/watchdog.timer diff --git a/backend/camera.py b/backend/camera.py index 94f1917..85bfd69 100644 --- a/backend/camera.py +++ b/backend/camera.py @@ -97,11 +97,15 @@ class Camera: return True except Exception as e: log.warning(f"Pas de DSLR : {e}") - if cam is not None and cam is not self.camera: - try: - cam.exit() - except Exception: - pass + if cam is not None: + def _force_release(c): + try: + c.exit() + except Exception: + pass + t = threading.Thread(target=_force_release, args=(cam,), daemon=True) + t.start() + t.join(timeout=2.0) del cam self.camera = None diff --git a/backend/main.py b/backend/main.py index 725393f..8a45088 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1503,9 +1503,8 @@ if __name__ == "__main__": config = charger_config() conf_srv = config.get("serveur", {}) uvicorn.run( - "backend.main:app", + app, host=conf_srv.get("host", "0.0.0.0"), port=conf_srv.get("port", 80), - reload=False, log_level="info", ) diff --git a/scripts/kiosk-session.sh b/scripts/kiosk-session.sh index 6af9652..bd02ef5 100755 --- a/scripts/kiosk-session.sh +++ b/scripts/kiosk-session.sh @@ -1,13 +1,12 @@ #!/bin/bash -# Verrou : une seule instance autorisée -LOCK=/tmp/photobooth-kiosk.lock -if [ -f "$LOCK" ] && kill -0 "$(cat $LOCK)" 2>/dev/null; then - echo "[kiosk] Déjà en cours (pid $(cat $LOCK)), abandon." - exit 0 -fi -echo $$ > "$LOCK" -trap "rm -f $LOCK" EXIT +# Verrou atomique — une seule session kiosk autorisee +exec 9>/tmp/photobooth-kiosk.lock +flock -n 9 || { echo "[kiosk] Déjà en cours, abandon."; exit 0; } + +# Tuer tout backend orphelin d'une session precedente +pkill -f 'python.*backend.main' 2>/dev/null || true +sleep 1 xset s off xset -dpms @@ -16,24 +15,15 @@ xset s noblank # Automount USB (pour la photostation) udiskie --no-notify --no-appindicator & -# Libérer le DSLR : gvfsd-gphoto2 (GNOME) le monopolise sinon +# Liberer le DSLR : gvfsd-gphoto2 (GNOME) le monopolise sinon pkill -f gvfsd-gphoto2 2>/dev/null || true pkill -f gvfs-gphoto2-volume-monitor 2>/dev/null || true -sleep 1 -# Backend en boucle : redémarre automatiquement s'il plante +# Backend en boucle : redemarre automatiquement s'il plante _backend_loop() { while true; do - # Mode maintenance : attendre la fin de l'intervention while [ -f /tmp/photobooth-maintenance ]; do sleep 2; done - # Vérifier qu'aucun autre backend ne tourne (éviter les doublons) - if pgrep -f "python.*backend.main" > /dev/null 2>&1; then - echo "[kiosk] backend déjà en cours, attente..." - sleep 5 - continue - fi cd /home/jules/photobooth - # Mise à jour automatique depuis Gitea git pull --ff-only 2>&1 | logger -t photobooth-git source .venv/bin/activate authbind --deep python -m backend.main 2>&1 | tee -a /tmp/photobooth-backend.log @@ -42,14 +32,18 @@ _backend_loop() { done } _backend_loop & +BACKEND_LOOP_PID=$! -# Attendre que le backend soit prêt (max 15s) +# Nettoyer le backend si le kiosk est tue +trap "kill $BACKEND_LOOP_PID 2>/dev/null; pkill -f 'python.*backend.main' 2>/dev/null" EXIT + +# Attendre que le backend soit pret (max 15s) for i in $(seq 1 15); do curl -sf http://localhost/ > /dev/null 2>&1 && break sleep 1 done -# Chromium en boucle : redémarre s'il plante, attend si photostation active +# Chromium en boucle : redemarre s'il plante, attend si photostation active while true; do while [ -f /tmp/photostation_running ]; do sleep 1; done chromium \ diff --git a/scripts/watchdog.service b/scripts/watchdog.service new file mode 100644 index 0000000..670b63b --- /dev/null +++ b/scripts/watchdog.service @@ -0,0 +1,8 @@ +[Unit] +Description=Photobooth watchdog +After=lightdm.service + +[Service] +Type=oneshot +ExecStart=/home/jules/photobooth/scripts/watchdog.sh +User=root diff --git a/scripts/watchdog.sh b/scripts/watchdog.sh new file mode 100644 index 0000000..e743430 --- /dev/null +++ b/scripts/watchdog.sh @@ -0,0 +1,101 @@ +#!/bin/bash +# Watchdog photobooth — tourne toutes les 30s via systemd timer +# Vérifie backend, camera, chromium. Escalade jusqu'au reboot si nécessaire. + +HEALTH_URL="http://localhost/api/health" +STATE_FILE="/tmp/photobooth-watchdog-failures" +MAX_FAILURES=10 # 10 échecs × 30s = 5 min avant reboot +LOG_TAG="photobooth-watchdog" + +log() { logger -t "$LOG_TAG" "$1"; } + +# Compteur d'échecs persistant +failures=$(cat "$STATE_FILE" 2>/dev/null || echo 0) + +# --- 1. Tuer les backends orphelins (ppid=1, pas rattachés au kiosk) --- +while IFS= read -r line; do + pid=$(echo "$line" | awk '{print $1}') + ppid=$(echo "$line" | awk '{print $2}') + if [ "$ppid" = "1" ]; then + log "Backend orphelin détecté (pid=$pid), kill" + kill -9 "$pid" 2>/dev/null + fi +done < <(ps -eo pid,ppid,args | grep 'python.*backend.main' | grep -v grep) + +# --- 2. Vérifier que le backend répond --- +health=$(curl -sf --max-time 5 "$HEALTH_URL" 2>/dev/null) +if [ -z "$health" ]; then + log "Backend ne répond pas (échec $((failures+1))/$MAX_FAILURES)" + # Vérifier si un process backend existe + if ! pgrep -f 'python.*backend.main' > /dev/null 2>&1; then + log "Aucun backend en cours — le kiosk devrait le relancer" + fi + failures=$((failures + 1)) + echo "$failures" > "$STATE_FILE" + + if [ "$failures" -ge "$MAX_FAILURES" ]; then + log "REBOOT: $MAX_FAILURES échecs consécutifs, redémarrage système" + echo 0 > "$STATE_FILE" + /usr/sbin/reboot + fi + exit 0 +fi + +# Backend répond — extraire l'état caméra +camera=$(echo "$health" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("camera",""))' 2>/dev/null) +clients=$(echo "$health" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("clients",0))' 2>/dev/null) + +# --- 3. Vérifier la caméra --- +if [ "$camera" = "disconnected" ] || [ "$camera" = "erreur" ]; then + log "Caméra déconnectée, tentative USB reset Canon" + + # USB ioctl reset du Canon (vendor 04a9) + python3 -c ' +import fcntl, glob, os +for f in glob.glob("/sys/bus/usb/devices/*/idVendor"): + if open(f).read().strip() == "04a9": + d = os.path.dirname(f) + bus = open(d + "/busnum").read().strip().zfill(3) + dev = open(d + "/devnum").read().strip().zfill(3) + path = f"/dev/bus/usb/{bus}/{dev}" + with open(path, "wb") as fh: + fcntl.ioctl(fh, 0x5514, 0) + print(f"USB reset: {path}") +' 2>/dev/null + + # Si le Canon n'est même pas sur le bus USB + if ! lsusb 2>/dev/null | grep -q "04a9"; then + log "Canon absent du bus USB" + failures=$((failures + 1)) + else + failures=$((failures + 1)) + fi + + echo "$failures" > "$STATE_FILE" + + if [ "$failures" -ge "$MAX_FAILURES" ]; then + log "REBOOT: caméra injoignable depuis $MAX_FAILURES cycles" + echo 0 > "$STATE_FILE" + /usr/sbin/reboot + fi + exit 0 +fi + +# --- 4. Vérifier Chromium --- +if ! pgrep -f chromium > /dev/null 2>&1; then + log "Chromium ne tourne pas — restart lightdm" + systemctl restart lightdm + failures=$((failures + 1)) + echo "$failures" > "$STATE_FILE" + exit 0 +fi + +# --- 5. Tuer gvfsd-gphoto2 s'il revient --- +pkill -f gvfsd-gphoto2 2>/dev/null +pkill -f gvfs-gphoto2-volume-monitor 2>/dev/null + +# --- Tout va bien : reset compteur --- +if [ "$failures" -gt 0 ]; then + log "Système OK (camera=$camera, clients=$clients) — compteur remis à 0" +fi +echo 0 > "$STATE_FILE" diff --git a/scripts/watchdog.timer b/scripts/watchdog.timer new file mode 100644 index 0000000..36c9817 --- /dev/null +++ b/scripts/watchdog.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Photobooth watchdog timer + +[Timer] +OnBootSec=60 +OnUnitActiveSec=30 +AccuracySec=5 + +[Install] +WantedBy=timers.target