Aller au contenu

Documentation du script start-dev.sh

Vue d'ensemble

Le script scripts/start-dev.sh est le point d'entrée principal pour lancer l'environnement de développement complet (backend FastAPI + frontend Vue.js) en une seule commande.

Usage

./scripts/start-dev.sh

Arrêt : Ctrl+C pour arrêter proprement les deux services.

Architecture générale

start-dev.sh
├── Nettoyage fichier .dev-ports.json (stale data)
├── Démarrage backend (Python FastAPI)
│   └── Détection port via .dev-ports.json (créé par backend)
├── Démarrage frontend (Vue.js + Vite)
│   └── Détection port (défaut: 5173)
├── Logs des ports détectés
└── Attente signal arrêt (Ctrl+C)
    └── Cleanup propre des processus

Fonctionnalités principales

1. Gestion unifiée des ports (Port Discovery)

Problème résolu

Dans un environnement DevContainer, les ports peuvent changer automatiquement si le port souhaité est déjà utilisé. Il est donc impossible de coder en dur http://localhost:8000.

Solution implémentée

  • Le backend Python écrit automatiquement le fichier .dev-ports.json au démarrage avec le port réel utilisé
  • Le frontend Vite lit ce fichier pour configurer son proxy API (voir frontend/vite.config.js)
  • Claude Code peut aussi lire ce fichier pour auto-découvrir les services

Format du fichier .dev-ports.json

{
  "backend": {
    "port": 54321,
    "host": "0.0.0.0",
    "pid": 12345,
    "started_at": 1764418163.47,
    "url": "http://0.0.0.0:54321"
  },
  "frontend": {
    "port": 5173,
    "host": "0.0.0.0",
    "pid": 12346,
    "started_at": 1764418162.52,
    "url": "http://0.0.0.0:5173"
  }
}

Choix de conception : Qui écrit le fichier ?

Principe : Source de vérité unique pour éviter les race conditions.

Implémentation : - Seul le backend écrit .dev-ports.json (source de vérité unique) - Le script bash lit le fichier créé par le backend - Le script attend maximum 15 secondes que le fichier soit créé

Raison : Si le backend ET le script écrivaient tous deux le fichier, une race condition pourrait survenir avec des ports incohérents. Le backend étant la source qui connaît réellement son port d'écoute, il est le seul à écrire cette information.

2. Nettoyage des données stales

Problème

Si le fichier .dev-ports.json d'une exécution précédente subsiste (après un crash ou un kill -9), il contient des ports périmés qui peuvent être lus avant que le nouveau backend ne crée le fichier à jour.

Solution

# Clean up old port discovery file to avoid stale data
if [[ -f "$PROJECT_ROOT/.dev-ports.json" ]]; then
    log "🧹 Nettoyage du fichier de découverte des ports précédent..."
    rm -f "$PROJECT_ROOT/.dev-ports.json"
fi

Le fichier est systématiquement supprimé avant de lancer les services, garantissant des données fraîches.

3. Détection robuste des ports backend

Fonction capture_port_from_output()

Stratégie pour le backend : - Attend que le fichier .dev-ports.json soit créé (retry loop de 15s) - Parse le JSON avec Python pour extraire port et host - Retourne 0 même en cas d'échec pour éviter de bloquer le démarrage (grâce à set -e)

Piège critique : set -e et incrémentation arithmétique

Bug découvert : L'incrémentation ((attempt++)) provoque une erreur silencieuse avec set -e.

Cause : - ((attempt++)) retourne la valeur avant incrémentation (0 au premier tour) - Avec set -e, bash interprète un retour de 0 dans une expression arithmétique comme un échec - Le script s'arrête immédiatement sans message d'erreur

Solution :

# ❌ INCORRECT - Fait planter le script avec set -e
((attempt++))

# ✅ CORRECT - Fonctionne avec set -e
attempt=$((attempt + 1))

Symptômes observés : - Le script affiche "Attempt 0/15" puis se termine immédiatement - On ne voit jamais "Attempt 1/15" - Le backend démarre correctement mais le script rend la main au shell - Aucun message d'erreur n'est affiché

Leçon : Toujours utiliser var=$((var + 1)) ou ((var += 1)) dans les scripts avec set -e, jamais ((var++)).

# For backend, wait for unified port discovery file to be created (with retry logic)
if [[ "$service_name" == "BACKEND" ]]; then
    local max_attempts=15  # 15 seconds total
    local attempt=0

    while [[ $attempt -lt $max_attempts ]]; do
        if [[ -f "$PROJECT_ROOT/.dev-ports.json" ]]; then
            local backend_port=$(python3 -c "import json; data=json.load(open('$PROJECT_ROOT/.dev-ports.json')); print(data.get('backend', {}).get('port', ''))" 2>/dev/null)
            local backend_host=$(python3 -c "import json; data=json.load(open('$PROJECT_ROOT/.dev-ports.json')); print(data.get('backend', {}).get('host', ''))" 2>/dev/null)

            if [[ -n "$backend_port" && -n "$backend_host" ]]; then
                eval "${port_var}=$backend_port"
                eval "${host_var}=$backend_host"
                log "Detected $service_name on $backend_host:$backend_port (after ${attempt}s)"
                return 0
            fi
        fi

        sleep 1
        attempt=$((attempt + 1))  # CRITICAL: Use this form with set -e, not ((attempt++))
    done

    warn "Backend port discovery file not found after ${max_attempts}s"
    warn "Backend may still be starting - check logs"
    return 0
fi

Stratégie pour le frontend : - Utilise le port par défaut de Vite : 5173 - Pas de détection dynamique nécessaire (Vite affiche son port dans ses logs)

4. Arrêt propre des processus

Problème : Processus zombies et script bloqué

Contexte : - npm run dev lance une chaîne de processus : npmshnode/vite - Un simple kill $FRONTEND_PID ne tue que le processus parent npm - Les processus enfants (sh, node) restent en vie → processus zombies - Le wait final du script attend indéfiniment ces processus

Solution : Tuer le groupe de processus (PGID)

Fonction kill_process_group() :

kill_process_group() {
    local pid=$1
    local name=$2

    # Get process group ID
    local pgid=$(ps -o pgid= -p $pid 2>/dev/null | tr -d ' ')

    if [[ -n $pgid ]]; then
        # Kill entire process group (negative PGID)
        kill -TERM -$pgid 2>/dev/null || true
        log "Signal TERM envoyé au groupe de processus $name (PGID: $pgid)"
    else
        # Fallback to killing just the process
        kill -TERM $pid 2>/dev/null || true
    fi
}

Le - devant $pgid est crucial : kill -TERM -12345 tue tout le groupe de processus 12345.

Fonction wait_for_process() :

wait_for_process() {
    local pid=$1
    local name=$2
    local timeout=5
    local elapsed=0

    # Wait max 5 seconds for graceful shutdown
    while kill -0 $pid 2>/dev/null && [[ $elapsed -lt $timeout ]]; do
        sleep 0.5
        elapsed=$((elapsed + 1))
    done

    # Force kill if still running (kill entire process group)
    if kill -0 $pid 2>/dev/null; then
        warn "$name (PID: $pid) ne répond pas - force kill du groupe"
        local pgid=$(ps -o pgid= -p $pid 2>/dev/null | tr -d ' ')
        if [[ -n $pgid ]]; then
            kill -9 -$pgid 2>/dev/null || true
        else
            kill -9 $pid 2>/dev/null || true
        fi
        sleep 0.5
    fi
}

Comportement : 1. Envoie SIGTERM au groupe → arrêt gracieux 2. Attend max 5 secondes 3. Si le processus ne répond pas → SIGKILL (force kill) du groupe entier 4. Garantit un arrêt en maximum 5 secondes

Fonction cleanup()

Appelée automatiquement lors de Ctrl+C (via trap cleanup SIGINT SIGTERM) :

cleanup() {
    log "Arrêt des processus..."

    if [[ -n $BACKEND_PID ]]; then
        log "Arrêt du backend (PID: $BACKEND_PID)"
        kill_process_group $BACKEND_PID "Backend"
        wait_for_process $BACKEND_PID "Backend"
    fi

    if [[ -n $FRONTEND_PID ]]; then
        log "Arrêt du frontend (PID: $FRONTEND_PID)"
        kill_process_group $FRONTEND_PID "Frontend"
        wait_for_process $FRONTEND_PID "Frontend"
    fi

    # Clean up unified discovery file
    if [[ -f "$PROJECT_ROOT/.dev-ports.json" ]]; then
        rm -f "$PROJECT_ROOT/.dev-ports.json"
        log "🧹 Unified port discovery file cleaned up"
    fi

    log "✅ Processus arrêtés proprement"
    exit 0
}

5. Gestion des erreurs avec set -e

Le script utilise set -e qui arrête l'exécution à la première erreur. Cependant, certaines commandes sont tolérantes aux erreurs grâce à || true :

kill -TERM $BACKEND_PID 2>/dev/null || true

Pourquoi || true ? - Si le processus est déjà mort, kill retourne un code erreur - Sans || true, le script s'arrêterait immédiatement (à cause de set -e) - Avec || true, on ignore l'erreur et on continue le cleanup

6. Logging coloré

Trois niveaux de logs avec couleurs :

log()   # Vert  - Informations normales
warn()  # Jaune - Avertissements
error() # Rouge - Erreurs

Exemple :

log "Backend démarré (PID: 12345)"
warn "Backend port discovery file not found after 15s"
error "Ce script doit être exécuté depuis la racine du projet"

7. Variables d'environnement optionnelles

BABELIO_CACHE_LOG

Si définie, active le logging verbeux du cache Babelio :

export BABELIO_CACHE_LOG=1
./scripts/start-dev.sh

Le script affiche alors :

[HH:MM:SS] Note: BABELIO_CACHE_LOG is set -> Babelio disk cache logging enabled (INFO).
[HH:MM:SS] WARNING: Les résultats stockés en cache peuvent varier d'une exécution à l'autre; utilisez avec prudence.

Flux d'exécution détaillé

Au démarrage

1. Vérification du répertoire courant (présence de pyproject.toml + frontend/)
2. Vérification des dépendances frontend (node_modules/)
   └─> Si absentes : npm ci
3. Suppression du fichier .dev-ports.json stale
4. Lancement backend FastAPI en arrière-plan
   └─> Stockage du PID dans $BACKEND_PID
5. Détection du port backend (retry 15s)
   └─> Lecture de .dev-ports.json créé par le backend
6. Lancement frontend Vue.js en arrière-plan
   └─> Stockage du PID dans $FRONTEND_PID
7. Détection du port frontend (défaut: 5173)
8. Affichage des ports détectés
9. wait (bloque jusqu'à Ctrl+C ou crash d'un processus)

À l'arrêt (Ctrl+C)

1. Signal SIGINT capturé par trap
2. Appel de cleanup()
3. Pour backend et frontend :
   a. kill_process_group() → SIGTERM au groupe
   b. wait_for_process() → Attente 5s max
   c. Si toujours vivant → SIGKILL au groupe
4. Suppression du fichier .dev-ports.json
5. Log "✅ Processus arrêtés proprement"
6. exit 0

Dépannage

Le script dit "Detected BACKEND on X:Y" mais le port réel est différent

Cause : Race condition entre la détection du script et la création du fichier par le backend.

Solution : Relancez le script. Le nettoyage automatique du fichier .dev-ports.json au démarrage devrait résoudre le problème.

Le frontend ne peut pas se connecter au backend (proxy error)

Cause : Vite a lu le fichier .dev-ports.json au démarrage avec un mauvais port et l'a mis en cache.

Solution : Arrêtez le script (Ctrl+C) et relancez-le. Vite relira le bon port.

Ctrl+C ne rend pas la main immédiatement

Cause : Un processus ne répond pas au SIGTERM.

Comportement attendu : Le script attend max 5 secondes puis force kill. Si cela prend plus de 5s, c'est un bug.

Vérification : Regardez les logs, vous devriez voir WARNING: Backend/Frontend (PID: XXX) ne répond pas - force kill du groupe.

Processus zombies après arrêt du script

Cause : Le script a été tué avec kill -9 (SIGKILL) ce qui empêche le cleanup.

Solution :

# Trouver les processus orphelins
ps aux | grep -E "back_office_lmelp|vite"

# Les tuer manuellement
pkill -9 -f "back_office_lmelp"
pkill -9 -f "vite"

Prévention : Toujours arrêter le script avec Ctrl+C (SIGINT) et non kill -9.

Le fichier .dev-ports.json n'est jamais créé

Cause : Le backend crash au démarrage avant de créer le fichier.

Diagnostic :

# Lancer le backend manuellement pour voir les erreurs
cd /workspaces/back-office-lmelp
PYTHONPATH=src python -m back_office_lmelp.app

Solution : Corriger l'erreur de démarrage du backend (souvent : MongoDB non connecté, variable d'environnement manquante, etc.).

Intégration avec Claude Code

Le fichier .dev-ports.json est lu par les scripts d'auto-découverte Claude Code :

  • .claude/get-backend-info.sh --url → Lit backend.url
  • .claude/get-frontend-info.sh --url → Lit frontend.url
  • .claude/get-services-info.sh → Affiche les deux

Cela permet à Claude Code de construire des commandes curl avec le bon port automatiquement :

bash -c 'BACKEND_URL=$(/workspaces/back-office-lmelp/.claude/get-backend-info.sh --url); curl "$BACKEND_URL/api/stats"'

Améliorations futures possibles

  1. Support de variables d'environnement personnalisées : Permettre de passer des variables via un fichier .env.local
  2. Mode verbose : ./scripts/start-dev.sh --verbose pour voir tous les logs des processus
  3. Logs dans des fichiers : Rediriger stdout/stderr des processus dans logs/backend.log et logs/frontend.log
  4. Healthcheck automatique : Vérifier que les services répondent réellement (curl vers / et /api/stats)
  5. Support multi-environnement : ./scripts/start-dev.sh --env staging pour utiliser différentes configurations

Fichiers liés

  • scripts/start-dev.sh - Le script principal
  • .dev-ports.json - Fichier de découverte des ports (généré automatiquement)
  • src/back_office_lmelp/app.py - Backend FastAPI qui écrit .dev-ports.json
  • frontend/vite.config.js - Configuration Vite qui lit .dev-ports.json
  • .claude/get-backend-info.sh - Script d'auto-découverte pour Claude Code
  • .claude/get-frontend-info.sh - Script d'auto-découverte pour Claude Code
  • .claude/get-services-info.sh - Vue d'ensemble des services