diff --git a/scripts/mac-mini/ensure-containers-running.sh b/scripts/mac-mini/ensure-containers-running.sh index f55e59bb6..ca20bf3ab 100755 --- a/scripts/mac-mini/ensure-containers-running.sh +++ b/scripts/mac-mini/ensure-containers-running.sh @@ -171,22 +171,18 @@ track_restart() { local count_file="$RESTART_TRACKER/$container" mkdir -p "$RESTART_TRACKER" + local count=0 age if [ -f "$count_file" ]; then - local count age - count=$(cat "$count_file") + count=$(cat "$count_file" 2>/dev/null || echo 0) + case "$count" in '' | *[!0-9]*) count=0 ;; esac age=$(($(date +%s) - $(stat -f %m "$count_file" 2>/dev/null || stat -c %Y "$count_file" 2>/dev/null))) - if [ "$age" -gt 3600 ]; then - echo "1" >"$count_file" - echo "1" - else - count=$((count + 1)) - echo "$count" >"$count_file" - echo "$count" - fi - else - echo "1" >"$count_file" - echo "1" + [ "$age" -gt 3600 ] && count=0 # Zaehler nach 1h zuruecksetzen fi + count=$((count + 1)) + # Im DRY_RUN den Zaehler NICHT persistieren (Probelaeufe sollen den + # Loop-Guard nicht künstlich hochtreiben). + [ "$DRY_RUN" = "1" ] || echo "$count" >"$count_file" + echo "$count" } # Bringt einen Container über SEIN eigenes Compose-Projekt wieder hoch @@ -205,10 +201,10 @@ bring_up() { if [ "$recreate" = "1" ]; then log " recreate $name via compose (project=$proj service=$svc)" run docker rm -f "$name" - run docker compose -p "$proj" -f "$cfg" up -d --no-deps "$svc" + run docker compose -p "$proj" -f "$cfg" up -d --no-deps --no-build "$svc" else log " recover $name via compose (project=$proj service=$svc)" - run docker compose -p "$proj" -f "$cfg" up -d --no-deps "$svc" + run docker compose -p "$proj" -f "$cfg" up -d --no-deps --no-build "$svc" fi else # Kein Compose-Container (oder Config-Datei fehlt) — direkter Start. @@ -217,46 +213,94 @@ bring_up() { fi } -# --- mana-core Reconciliation: fehlende Core-Container neu erstellen ------- -# Ein komplett fehlender Container hat keine Labels — daher gegen die -# Core-Compose abgleichen, welche Services definiert, aber ohne Container -# sind, und nur die gezielt (re-)erzeugen. -reconcile_mana_core() { - if [ ! -f "$MANA_CORE_COMPOSE" ]; then - log "core-reconcile: Compose nicht gefunden ($MANA_CORE_COMPOSE) — skip" - return 0 - fi - if [ -f "$MAINT_LOCK" ]; then - log "core-reconcile: Wartungs-Lock aktiv ($MAINT_LOCK) — skip" - return 0 - fi +# --- Reconciliation: fehlende DAUERLÄUFER-Container neu erstellen ---------- +# Ein komplett fehlender Container hat keine Labels — daher pro Compose- +# Projekt (aus den LAUFENDEN Containern abgeleitet) gegen die Compose +# abgleichen, welche Dauerläufer-Services (restart always/unless-stopped) +# definiert, aber ohne Container sind, und nur die gezielt (re-)erzeugen. +# Deckt so auch eigenständige App-Stacks (zitare/nutriphi/viadocu/…) ab, +# nicht nur mana-core. Schutzmechanismen: +# - Projekte ohne laufenden Container = bewusst unten → nicht anfassen. +# - Mehrdeutige Projektnamen (gleicher Name, verschiedene Composes = +# Projekt-Kollision, z.B. manacore-monorepo/herbatrium) → übersprungen. +# - Nur Dauerläufer (restart-Policy via config-json + jq) → keine +# Job/Init/Profile-Services. +# - Wartungs-Lock pausiert die ganze Reconciliation. +# - mana-core garantiert dabei (auch wenn ausnahmsweise kein Core- +# Container laeuft) via hardcodiertem Pfad. +# ALLE compose-up-Aufrufe nutzen --no-build: ein Watchdog darf NIE ein +# Image bauen (schwergewichtig → genau das kippte am 2026-05-26 die VM in +# den OOM-Crash). Fehlt das Image, schlaegt der Start sauber fehl + Notify. +reconcile_project() { + local proj="$1" cfg_csv="$2" + local fargs=() f IFS_OLD="$IFS" + IFS=',' + for f in $cfg_csv; do + [ -n "$f" ] || continue + if [ ! -f "$f" ]; then + IFS="$IFS_OLD" + log "reconcile: $proj — Compose-Datei fehlt ($f) — skip" + return 0 + fi + fargs+=(-f "$f") + done + IFS="$IFS_OLD" + [ "${#fargs[@]}" -eq 0 ] && return 0 - local services svc cid missing="" - services=$(docker compose -p "$MANA_CORE_PROJECT" -f "$MANA_CORE_COMPOSE" config --services 2>/dev/null || true) - [ -z "$services" ] && { - log "core-reconcile: keine Services aus Compose lesbar — skip" - return 0 - } + # Nur Dauerlaeufer — Job/Init/Profile-Services (restart no/on-failure + # oder profil-gated) bleiben aussen vor. + local longrunners + longrunners=$(docker compose -p "$proj" "${fargs[@]}" config --format json 2>/dev/null \ + | jq -r '.services | to_entries[] | select((.value.restart // "") | test("always|unless-stopped")) | .key' 2>/dev/null || true) + [ -z "$longrunners" ] && return 0 - for svc in $services; do + local svc cid missing="" + for svc in $longrunners; do cid=$(docker ps -a \ - --filter "label=com.docker.compose.project=$MANA_CORE_PROJECT" \ + --filter "label=com.docker.compose.project=$proj" \ --filter "label=com.docker.compose.service=$svc" \ --format '{{.ID}}' 2>/dev/null | head -1) [ -z "$cid" ] && missing="${missing:+$missing }$svc" done if [ -n "$missing" ]; then - log "core-reconcile: FEHLENDE Core-Container: $missing" + log "reconcile: $proj — FEHLENDE Dauerläufer: $missing" for svc in $missing; do - log " (re-)erstelle Core-Service: $svc" - run docker compose -p "$MANA_CORE_PROJECT" -f "$MANA_CORE_COMPOSE" up -d --no-deps "$svc" + log " (re-)erstelle $proj/$svc" + run docker compose -p "$proj" "${fargs[@]}" up -d --no-deps --no-build "$svc" done - send_notification "🔧 mana-core\n\nFehlende Container neu erstellt: $missing" "high" + send_notification "🔧 $proj\n\nFehlende Container neu erstellt: $missing" "high" fi } -reconcile_mana_core +reconcile_missing() { + if [ -f "$MAINT_LOCK" ]; then + log "reconcile: Wartungs-Lock aktiv ($MAINT_LOCK) — skip" + return 0 + fi + + # (Projekt|config_files)-Paare aus LAUFENDEN Containern + garantiert mana-core. + local pairs projects proj cfgs cfgcount + pairs=$(docker ps --format '{{.Label "com.docker.compose.project"}}|{{.Label "com.docker.compose.project.config_files"}}' 2>/dev/null | grep -v '^|' | grep -v '^$' | sort -u || true) + if [ -f "$MANA_CORE_COMPOSE" ]; then + pairs=$(printf '%s\n%s\n' "$pairs" "${MANA_CORE_PROJECT}|${MANA_CORE_COMPOSE}" | grep -v '^$' | sort -u) + fi + + projects=$(printf '%s\n' "$pairs" | cut -d'|' -f1 | sort -u) + for proj in $projects; do + [ -z "$proj" ] && continue + # Alle distinct config_files-Werte fuer dieses Projekt. + cfgs=$(printf '%s\n' "$pairs" | awk -F'|' -v p="$proj" '$1==p{print $2}' | sort -u) + cfgcount=$(printf '%s\n' "$cfgs" | grep -c .) + if [ "$cfgcount" -ne 1 ]; then + log "reconcile: $proj — mehrdeutig ($cfgcount Compose-Sets) — skip" + continue + fi + reconcile_project "$proj" "$cfgs" + done +} + +reconcile_missing # Nicht-laufende Container (created/exited), die laufen sollten. STUCK_CONTAINERS="" @@ -285,8 +329,8 @@ if [ -n "$CRASHLOOP_CONTAINERS" ]; then for container in $CRASHLOOP_CONTAINERS; do ATTEMPTS=$(track_restart "$container") if [ "$ATTEMPTS" -gt 3 ]; then - log " SKIP: $container wurde in der letzten Stunde $ATTEMPTS× neu gestartet — manueller Eingriff noetig" - send_notification "🚨 Container braucht manuellen Fix\n\n$container crasht wiederholt ($ATTEMPTS×). Logs:\ndocker logs $container" "high" + log " SKIP: $container wurde in der letzten Stunde $ATTEMPTS-mal neu gestartet — manueller Eingriff noetig" + send_notification "🚨 Container braucht manuellen Fix\n\n$container crasht wiederholt ($ATTEMPTS-mal). Logs:\ndocker logs $container" "high" continue fi log " Recreate $container (Versuch $ATTEMPTS/3)..."