#!/usr/bin/env bash # ============================================================================= # watchdog_readsb.sh — surveille le(s) récepteur(s) ADS-B et redémarre le # maillon réellement en cause quand le flux de messages se fige. # # v4 (31/07/2026) — refonte après l'incident du 31/07 (36 redémarrages en 24h # sur un récepteur qui n'était pas en panne). Quatre défauts corrigés : # # 1. RECEIVERS était statique et listait un dongle inexistant (00000012). # Le watchdog redémarrait donc en boucle un conteneur sans matériel. # → RECEIVERS est maintenant dérivé des dongles réellement énumérés, # via detect_dongles.sh (même source que check_adsb.sh). # # 2. THRESHOLD=1 le jour : redémarrage après 5 min sans progression. Or le # service autogain a besoin de plusieurs heures de trafic pour converger, # et chaque redémarrage le relançait de zéro. Le watchdog empêchait donc # la seule chose qui aurait corrigé le gain (bloqué à 7.7 dB). # → seuil minimum 3 cycles, + délai de grâce après démarrage. # # 3. Aucun plafond : un redémarrage qui ne corrige rien était répété # indéfiniment, noyant le vrai diagnostic sous le bruit. # → budget de MAX_RESTARTS_24H, au-delà duquel on alerte sans agir. # # 4. Le diagnostic reposait uniquement sur aircraft.json (servi par tar1090). # Un tar1090 déconnecté de readsb produisait donc le même symptôme qu'un # dongle mort — c'est exactement ce qui s'est produit le 31/07 après une # perte d'alias réseau : readsb décodait, tar1090 servait 0 message, et # le watchdog redémarrait readsb, le maillon sain. # → sonde directe du port SBS : si readsb émet, c'est tar1090 qu'on # redémarre, pas readsb. # # Cron : */5 * * * * /data/adsb/scripts/watchdog_readsb.sh # ============================================================================= BASE_DIR="/data/adsb" LOG_FILE="/var/log/readsb_watchdog.log" DETECT_LIB="${BASE_DIR}/scripts/detect_dongles.sh" # ── Paramètres de prudence ──────────────────────────────────────────────────── MIN_UPTIME=1800 # s — ne jamais redémarrer un conteneur démarré depuis # moins de 30 min (laisse converger l'autogain) MAX_RESTARTS_24H=6 # au-delà, on alerte sans redémarrer : si 6 redémarrages # n'ont pas corrigé le problème, le 7e ne le fera pas SBS_PROBE_TIMEOUT=6 # s — durée de la sonde sur le port SBS SBS_MIN_BYTES=50 # octets reçus au-delà desquels readsb est jugé actif log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $*" >> "$LOG_FILE"; } # ── Seuil adaptatif jour/nuit ───────────────────────────────────────────────── # Le creux de trafic nocturne reste réel : on tolère plus longtemps la nuit. # Mais le plancher est désormais à 3 cycles (15 min) même en journée. HOUR=$(date +%H) if [ "$((10#$HOUR))" -ge 6 ] && [ "$((10#$HOUR))" -lt 22 ]; then THRESHOLD=3; PERIOD="jour" else THRESHOLD=5; PERIOD="nuit" fi # ── Construction dynamique des récepteurs ───────────────────────────────────── if [ ! -r "$DETECT_LIB" ]; then log "[FATAL] $DETECT_LIB introuvable — watchdog inopérant" exit 1 fi # shellcheck source=/dev/null source "$DETECT_LIB" mapfile -t PRESENT_SERIALS < <(detect_serials) if [ "${#PRESENT_SERIALS[@]}" -eq 0 ]; then log "[ALERTE] Aucun dongle RTL-SDR détecté sur le bus USB — rien à surveiller. Vérifier le branchement physique." exit 0 fi # ── Budget de redémarrages ──────────────────────────────────────────────────── # Historique des redémarrages par conteneur, élagué à 24h glissantes. restart_count_24h() { local ctn="$1" f="/tmp/readsb_restart_hist_${ctn}" now cutoff now=$(date +%s); cutoff=$((now - 86400)) [ -f "$f" ] || { echo 0; return; } awk -v c="$cutoff" '$1 >= c' "$f" > "${f}.tmp" && mv "${f}.tmp" "$f" wc -l < "$f" | tr -d ' ' } record_restart() { echo "$(date +%s)" >> "/tmp/readsb_restart_hist_$1" } # ── Uptime d'un conteneur, en secondes ──────────────────────────────────────── container_uptime() { local started started=$(docker inspect --format '{{.State.StartedAt}}' "$1" 2>/dev/null) || return 1 [ -z "$started" ] && return 1 echo $(( $(date +%s) - $(date -d "$started" +%s 2>/dev/null || echo 0) )) } container_running() { [ "$(docker inspect --format '{{.State.Status}}' "$1" 2>/dev/null)" = "running" ] } # ── Sonde SBS : readsb émet-il réellement des messages décodés ? ─────────────── # Retourne 0 si du trafic ADS-B sort du récepteur lui-même. sbs_has_traffic() { local port="$1" bytes if command -v nc >/dev/null 2>&1; then bytes=$(timeout "$SBS_PROBE_TIMEOUT" nc 127.0.0.1 "$port" 2>/dev/null \ | head -c 400 | wc -c) else bytes=$(timeout "$SBS_PROBE_TIMEOUT" bash -c \ "exec 3<>/dev/tcp/127.0.0.1/$port && head -c 400 <&3" 2>/dev/null | wc -c) fi [ "${bytes:-0}" -ge "$SBS_MIN_BYTES" ] } # ── Redémarrage encadré ─────────────────────────────────────────────────────── guarded_restart() { local ctn="$1" reason="$2" up n if ! container_running "$ctn"; then log "[$ctn] non démarré — pas de redémarrage watchdog (relève de docker compose / adsb-stack.service)" return 1 fi up=$(container_uptime "$ctn") if [ -n "$up" ] && [ "$up" -lt "$MIN_UPTIME" ]; then log "[$ctn] $reason — mais uptime ${up}s < ${MIN_UPTIME}s, on laisse converger (autogain / initialisation)" return 1 fi n=$(restart_count_24h "$ctn") if [ "$n" -ge "$MAX_RESTARTS_24H" ]; then log "[ALERTE] [$ctn] $reason — $n redémarrages sur 24h, plafond atteint : AUCUNE action. Le redémarrage ne corrige pas ce problème, diagnostic manuel requis (gain, antenne, câble, alias réseau)." return 1 fi log "[$ctn] $reason — redémarrage ($((n + 1))/${MAX_RESTARTS_24H} sur 24h)" docker restart "$ctn" >/dev/null 2>&1 && record_restart "$ctn" return 0 } # ── Boucle principale ───────────────────────────────────────────────────────── for i in "${!PRESENT_SERIALS[@]}"; do n=$((i + 1)) [ "$n" -gt "$MAX_RECEIVERS" ] && break IFS=':' read -r SLOT PORT CTN_RS CTN_T1090 CTN_PG PROFILE <<< "${SLOTS[$i]}" RX_SERIAL="${PRESENT_SERIALS[$i]}" read -r BEAST_PORT RAW_PORT SBS_PORT <<< "$(slot_ports "$SLOT")" STATE_FILE="/tmp/readsb_last_msg_count_${RX_SERIAL}" STREAK_FILE="/tmp/readsb_stall_streak_${RX_SERIAL}" # Le conteneur readsb doit exister ; sinon rien à surveiller sur ce slot. if ! docker inspect "$CTN_RS" >/dev/null 2>&1; then log "[$CTN_RS] conteneur absent alors que le dongle $RX_SERIAL est présent — le stack n'est pas déployé pour ce slot (docker compose up -d --remove-orphans)" continue fi CURRENT=$(curl -s --max-time 10 "http://127.0.0.1:${PORT}/data/aircraft.json" \ | grep -oP '"messages"\s*:\s*\K[0-9]+') if [ -z "$CURRENT" ]; then log "[$CTN_RS] aircraft.json illisible (port ${PORT}) — tar1090 muet ; check ignoré ce cycle" continue fi LAST=$(cat "$STATE_FILE" 2>/dev/null || echo -1) STREAK=$(cat "$STREAK_FILE" 2>/dev/null || echo 0) if [ "$CURRENT" == "$LAST" ]; then STREAK=$((STREAK + 1)) if [ "$STREAK" -ge "$THRESHOLD" ]; then # Compteur figé confirmé. Avant d'accuser readsb, on demande # directement au récepteur s'il émet : c'est ce test qui distingue # un dongle sourd d'un tar1090 déconnecté. if sbs_has_traffic "$SBS_PORT"; then log "[$CTN_RS] compteur tar1090 figé à $CURRENT depuis ${STREAK} cycle(s) MAIS le port SBS ${SBS_PORT} émet — readsb est sain, le maillon en cause est tar1090" guarded_restart "$CTN_T1090" "aircraft.json figé alors que readsb émet (alias réseau BEASTHOST perdu ?)" else guarded_restart "$CTN_RS" "compteur messages figé à $CURRENT depuis ${STREAK} cycle(s) [$PERIOD, seuil=$THRESHOLD] et port SBS ${SBS_PORT} silencieux" fi STREAK=0 else log "[$CTN_RS] compteur inchangé à $CURRENT (${STREAK}/${THRESHOLD} cycles) [$PERIOD] - en observation" fi else # Progression : on efface aussi l'historique de redémarrages, le # récepteur est manifestement revenu à la normale. if [ "$STREAK" -gt 0 ]; then log "[$CTN_RS] flux rétabli ($LAST → $CURRENT) - compteur de blocage remis à zéro" fi STREAK=0 rm -f "/tmp/readsb_restart_hist_${CTN_RS}" "/tmp/readsb_restart_hist_${CTN_T1090}" fi echo "$CURRENT" > "$STATE_FILE" echo "$STREAK" > "$STREAK_FILE" done