Watchdog systemd + fix stabilité kiosk/backend
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -97,11 +97,15 @@ class Camera:
|
|||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"Pas de DSLR : {e}")
|
log.warning(f"Pas de DSLR : {e}")
|
||||||
if cam is not None and cam is not self.camera:
|
if cam is not None:
|
||||||
try:
|
def _force_release(c):
|
||||||
cam.exit()
|
try:
|
||||||
except Exception:
|
c.exit()
|
||||||
pass
|
except Exception:
|
||||||
|
pass
|
||||||
|
t = threading.Thread(target=_force_release, args=(cam,), daemon=True)
|
||||||
|
t.start()
|
||||||
|
t.join(timeout=2.0)
|
||||||
del cam
|
del cam
|
||||||
self.camera = None
|
self.camera = None
|
||||||
|
|
||||||
|
|||||||
@@ -1503,9 +1503,8 @@ if __name__ == "__main__":
|
|||||||
config = charger_config()
|
config = charger_config()
|
||||||
conf_srv = config.get("serveur", {})
|
conf_srv = config.get("serveur", {})
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
"backend.main:app",
|
app,
|
||||||
host=conf_srv.get("host", "0.0.0.0"),
|
host=conf_srv.get("host", "0.0.0.0"),
|
||||||
port=conf_srv.get("port", 80),
|
port=conf_srv.get("port", 80),
|
||||||
reload=False,
|
|
||||||
log_level="info",
|
log_level="info",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Verrou : une seule instance autorisée
|
# Verrou atomique — une seule session kiosk autorisee
|
||||||
LOCK=/tmp/photobooth-kiosk.lock
|
exec 9>/tmp/photobooth-kiosk.lock
|
||||||
if [ -f "$LOCK" ] && kill -0 "$(cat $LOCK)" 2>/dev/null; then
|
flock -n 9 || { echo "[kiosk] Déjà en cours, abandon."; exit 0; }
|
||||||
echo "[kiosk] Déjà en cours (pid $(cat $LOCK)), abandon."
|
|
||||||
exit 0
|
# Tuer tout backend orphelin d'une session precedente
|
||||||
fi
|
pkill -f 'python.*backend.main' 2>/dev/null || true
|
||||||
echo $$ > "$LOCK"
|
sleep 1
|
||||||
trap "rm -f $LOCK" EXIT
|
|
||||||
|
|
||||||
xset s off
|
xset s off
|
||||||
xset -dpms
|
xset -dpms
|
||||||
@@ -16,24 +15,15 @@ xset s noblank
|
|||||||
# Automount USB (pour la photostation)
|
# Automount USB (pour la photostation)
|
||||||
udiskie --no-notify --no-appindicator &
|
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 gvfsd-gphoto2 2>/dev/null || true
|
||||||
pkill -f gvfs-gphoto2-volume-monitor 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() {
|
_backend_loop() {
|
||||||
while true; do
|
while true; do
|
||||||
# Mode maintenance : attendre la fin de l'intervention
|
|
||||||
while [ -f /tmp/photobooth-maintenance ]; do sleep 2; done
|
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
|
cd /home/jules/photobooth
|
||||||
# Mise à jour automatique depuis Gitea
|
|
||||||
git pull --ff-only 2>&1 | logger -t photobooth-git
|
git pull --ff-only 2>&1 | logger -t photobooth-git
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
authbind --deep python -m backend.main 2>&1 | tee -a /tmp/photobooth-backend.log
|
authbind --deep python -m backend.main 2>&1 | tee -a /tmp/photobooth-backend.log
|
||||||
@@ -42,14 +32,18 @@ _backend_loop() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
_backend_loop &
|
_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
|
for i in $(seq 1 15); do
|
||||||
curl -sf http://localhost/ > /dev/null 2>&1 && break
|
curl -sf http://localhost/ > /dev/null 2>&1 && break
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
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 true; do
|
||||||
while [ -f /tmp/photostation_running ]; do sleep 1; done
|
while [ -f /tmp/photostation_running ]; do sleep 1; done
|
||||||
chromium \
|
chromium \
|
||||||
|
|||||||
8
scripts/watchdog.service
Normal file
8
scripts/watchdog.service
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Photobooth watchdog
|
||||||
|
After=lightdm.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/home/jules/photobooth/scripts/watchdog.sh
|
||||||
|
User=root
|
||||||
101
scripts/watchdog.sh
Normal file
101
scripts/watchdog.sh
Normal file
@@ -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"
|
||||||
10
scripts/watchdog.timer
Normal file
10
scripts/watchdog.timer
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Photobooth watchdog timer
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=60
|
||||||
|
OnUnitActiveSec=30
|
||||||
|
AccuracySec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
Reference in New Issue
Block a user