#!/usr/bin/env bash # # Directive 2 — MQTT reconnect rule verifier (Hub Fixes). # # paho reconnects on its own but does NOT restore subscriptions. A client that # has no on_connect re-subscribe handler holds a live socket with no # subscription and goes silently deaf — no error, no log. See # docs/mqtt-reconnect-compliance.md. # # Usage: # verify/mqtt_reconnect_check.sh # static source check (safe) # verify/mqtt_reconnect_check.sh --live # restart the .13 broker and # # assert re-subscribe within 60s # # The --live run restarts the shared .13 mosquitto container. It briefly # interrupts voice intake and the kids' device control plane. Run it at a # low-impact moment. Every docker call is bounded with `timeout`. set -u BROKER_CONTAINER="${BROKER_CONTAINER:-mosquitto}" VOICE_BRIDGE_CONTAINER="${VOICE_BRIDGE_CONTAINER:-voice_bridge}" CONTROL_CONTAINER="${CONTROL_CONTAINER:-webrestrict-control}" WINDOW="${WINDOW:-60}" # seconds allowed for re-subscribe DOCKER_TIMEOUT="${DOCKER_TIMEOUT:-20}" LIVE=0 [ "${1:-}" = "--live" ] && LIVE=1 # Client sources: name|path|on_connect pattern|subscribe pattern|log pattern CLIENTS=( "voice_bridge |/home/sam/voice_bridge/mqtt_audio_bridge.py|_on_voice_connect|client\\.subscribe\\(\\[\\(VOICE_TOPIC_AUDIO|\\[bridge\\] subscribed" "voice-agent |/home/sam/voice-agent/app.py|def on_connect|client\\.subscribe\\(\\[\\(topic|\\[mqtt\\] subscribed" "control-svc |/home/sam/paseo/projects/internet_use_control/internal/mqtt/link.go|SetOnConnectHandler|c\\.Subscribe\\(sub\\.topic|mqtt subscribed" "kids-android |/home/sam/paseo/projects/internet_use_control/android/app/src/main/java/au/audasmedia/webrestrict/kids/MqttController.kt|connectComplete|isAutomaticReconnect|c\\.subscribe\\(prefs\\.deviceTopic|subscribed to" ) status() { printf '%-14s %s\n' "$1" "$2"; } static_check() { echo "== static check: on_connect re-subscribe + log ==" local row name path onc sub log rc=0 for row in "${CLIENTS[@]}"; do IFS='|' read -r name path onc sub log <<<"$row" name="$(echo "$name" | xargs)" if [ ! -f "$path" ]; then status "$name" "SKIP (source not on this host: $path)" continue fi local ok="ok" grep -qE "$onc" "$path" || ok="MISSING on_connect" grep -qE "$sub" "$path" || ok="MISSING subscribe" grep -qE "$log" "$path" || ok="MISSING log" if [ "$ok" = "ok" ]; then status "$name" "OK $path" else status "$name" "FAIL $ok ($path)" rc=1 fi done return $rc } broker_ready() { timeout "$DOCKER_TIMEOUT" docker exec "$BROKER_CONTAINER" \ mosquitto_sub -h localhost -p 1883 -t '$SYS/broker/uptime' -C 1 -W 3 \ >/dev/null 2>&1 } live_check() { local t0 epoch i ready=0 t0="$(date -u +%Y-%m-%dT%H:%M:%SZ)" epoch="$(date +%s)" echo "== live check: restart $BROKER_CONTAINER at $t0 ==" timeout $((DOCKER_TIMEOUT + 20)) docker restart "$BROKER_CONTAINER" || { echo "FAIL: could not restart $BROKER_CONTAINER"; return 1; } for i in $(seq 1 15); do if broker_ready; then ready=1; echo "broker back after ~$((i * 2))s"; break; fi sleep 2 done [ "$ready" = 1 ] || { echo "FAIL: broker did not come back within 30s"; return 1; } local deadline=$(( $(date +%s) + WINDOW )) local bridge=0 control=0 esp=0 while [ "$(date +%s)" -lt "$deadline" ]; do [ "$bridge" = 0 ] && timeout "$DOCKER_TIMEOUT" docker logs "$VOICE_BRIDGE_CONTAINER" --since "$t0" 2>&1 \ | grep -q '\[bridge\] subscribed' && bridge=1 [ "$control" = 0 ] && timeout "$DOCKER_TIMEOUT" docker logs "$CONTROL_CONTAINER" --since "$t0" 2>&1 \ | grep -q 'mqtt subscribed' && control=1 [ "$esp" = 0 ] && timeout "$DOCKER_TIMEOUT" docker exec "$BROKER_CONTAINER" sh -c \ "awk -v t=$epoch -F: '/New client connected/ {if (\$1+0>=t) print}' /mosquitto/log/mosquitto.log" 2>/dev/null \ | grep -q 'ESP32Mic' && esp=1 [ "$((bridge + control + esp))" = 3 ] && break sleep 2 done local elapsed=$(( $(date +%s) - epoch )) status "voice_bridge" "$([ "$bridge" = 1 ] && echo "OK re-subscribed" || echo "FAIL no re-subscribe")" status "control-svc" "$([ "$control" = 1 ] && echo "OK re-subscribed" || echo "FAIL no re-subscribe")" status "ESP32Mic" "$([ "$esp" = 1 ] && echo "OK reconnected (re-subscribe is Directive 3)" || echo "FAIL no reconnect")" echo "elapsed: ${elapsed}s (window ${WINDOW}s)" [ "$((bridge + control + esp))" = 3 ] } rc=0 static_check || rc=1 if [ "$LIVE" = 1 ]; then echo live_check || rc=1 fi if [ "$rc" = 0 ]; then echo "RESULT: PASS"; else echo "RESULT: FAIL"; fi exit "$rc"