<# .SYNOPSIS ObsiGate — cycle de vie du serveur E2E local, avec progression visible. .DESCRIPTION Remplace le one-liner opaque de démarrage : chaque étape affiche sa progression (port, PID, attente du health check seconde par seconde, version servie). Memes conditions que le job CI `e2e` et que `scripts/run-e2e-local.ps1` : uvicorn natif, auth désactivée, fixtures TestVault/TestDir, port 2029. Le PID est persisté dans `data/e2e-server.pid` pour un arrêt propre (`stop`) — plus de serveurs orphelins qui squattent le port. .EXAMPLE ./scripts/e2e-server.ps1 start # démarre + attend READY (défaut) ./scripts/e2e-server.ps1 status # port, PID, version servie ./scripts/e2e-server.ps1 logs # queues des logs serveur ./scripts/e2e-server.ps1 stop # arrête le serveur + libère le port #> [CmdletBinding()] param( [Parameter(Position = 0)] [ValidateSet("start", "stop", "status", "logs")] [string]$Command = "start", [string]$Port = $(if ($env:E2E_PORT) { $env:E2E_PORT } else { "2029" }) ) $ErrorActionPreference = "Stop" $Root = Split-Path -Parent $PSScriptRoot Set-Location -LiteralPath $Root $BaseUrl = "http://127.0.0.1:$Port" $Python = ".\.venv\Scripts\python.exe" $PidFile = "data/e2e-server.pid" $OutLog = "data/e2e-server.log" $ErrLog = "data/e2e-server.err.log" function Get-PortOwner { $conn = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $conn) { return $null } $proc = Get-Process -Id $conn.OwningProcess -ErrorAction SilentlyContinue return @{ Pid = $conn.OwningProcess; Name = $(if ($proc) { $proc.ProcessName } else { "?" }) } } function Stop-Server { param([string]$Why = "") $killed = @() if (Test-Path -LiteralPath $PidFile) { $srvPid = (Get-Content -LiteralPath $PidFile -TotalCount 1).Trim() if ($srvPid -match '^\d+$') { # BUG-080 : le PID enregistré peut avoir ré-exécuté uvicorn dans un # processus enfant (constaté : parent .venv + enfant uv-python sur # le port) — tuer l'arbre complet, pas seulement la racine. Get-CimInstance Win32_Process -Filter "ParentProcessId=$srvPid" -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue; $killed += $_.ProcessId } Stop-Process -Id $srvPid -Force -ErrorAction SilentlyContinue $killed += $srvPid } Remove-Item -LiteralPath $PidFile -Force -ErrorAction SilentlyContinue } $owner = Get-PortOwner if ($owner) { Stop-Process -Id $owner.Pid -Force -ErrorAction SilentlyContinue $killed += $owner.Pid } if ($killed.Count) { Write-Host "[OK] Serveur arrêté (PID $($killed -join ', ')). $Why" } else { Write-Host "[OK] Aucun serveur en cours (port $Port libre)." } } switch ($Command) { "stop" { Write-Host "[1/1] Arrêt du serveur E2E (port $Port)..." Stop-Server } "status" { $owner = Get-PortOwner if (-not $owner) { Write-Host "[INFO] Port $Port libre, aucun serveur."; break } Write-Host "[INFO] Port $Port occupé par PID $($owner.Pid) ($($owner.Name))." try { $health = Invoke-WebRequest -Uri "$BaseUrl/api/health" -TimeoutSec 5 -UseBasicParsing | Select-Object -ExpandProperty Content | ConvertFrom-Json Write-Host "[OK] Health 200 — version $($health.version), $($health.total_files) fichiers indexés." } catch { Write-Host "[WARN] Processus présent mais health injoignable : $($_.Exception.Message)" } } "logs" { Write-Host "===== $OutLog (stdout) =====" Get-Content -LiteralPath $OutLog -Tail 15 -ErrorAction SilentlyContinue Write-Host "===== $ErrLog (stderr) =====" Get-Content -LiteralPath $ErrLog -Tail 25 -ErrorAction SilentlyContinue } "start" { Write-Host "[1/4] Port $Port..." $owner = Get-PortOwner if ($owner) { Write-Host "[ERR] Port $Port déjà occupé par PID $($owner.Pid) ($($owner.Name))." Write-Host " Lancez d'abord : ./scripts/e2e-server.ps1 stop" exit 1 } Write-Host " libre." Write-Host "[2/4] Interpréteur $Python..." if (-not (Test-Path -LiteralPath $Python)) { Write-Host "[ERR] $Python introuvable. Créez le venv (voir AGENTS.md)." exit 1 } Write-Host " présent." New-Item -ItemType Directory -Force -Path "data" | Out-Null Write-Host "[3/4] Démarrage uvicorn (auth désactivée, TestVault/TestDir)..." $env:OBSIGATE_AUTH_ENABLED = "false" $env:VAULT_1_NAME = "TestVault" $env:VAULT_1_PATH = (Resolve-Path -LiteralPath "test_vault").Path $env:DIR_1_NAME = "TestDir" $env:DIR_1_PATH = (Resolve-Path -LiteralPath "test_dir").Path $server = Start-Process -FilePath $Python ` -ArgumentList "-m", "uvicorn", "backend.main:app", "--host", "127.0.0.1", "--port", $Port ` -RedirectStandardOutput $OutLog -RedirectStandardError $ErrLog ` -PassThru -WindowStyle Hidden $server.Id | Set-Content -LiteralPath $PidFile Write-Host " PID $($server.Id) (logs : $OutLog / $ErrLog)." Write-Host "[4/4] Attente du health check (30 s max)..." $ready = $false for ($i = 1; $i -le 30; $i++) { try { Invoke-WebRequest -Uri "$BaseUrl/api/health" -TimeoutSec 2 -UseBasicParsing | Out-Null $ready = $true break } catch { if ($server.HasExited) { Write-Host "[ERR] Le serveur a quitté (code $($server.ExitCode)). Fin du log :" Get-Content -LiteralPath $ErrLog -Tail 15 -ErrorAction SilentlyContinue exit 1 } if ($i % 5 -eq 0) { Write-Host " ... $i/30 s (indexation en cours, voir $ErrLog)" } Start-Sleep -Seconds 1 } } if (-not $ready) { Write-Host "[ERR] Injoignable après 30 s. Fin du log :" Get-Content -LiteralPath $ErrLog -Tail 15 -ErrorAction SilentlyContinue exit 1 } $health = Invoke-WebRequest -Uri "$BaseUrl/api/health" -TimeoutSec 5 -UseBasicParsing | Select-Object -ExpandProperty Content | ConvertFrom-Json # BUG-080 : le PID `Start-Process` peut ne pas être celui qui écoute # (ré-exécution enfant constatée) — persister le vrai propriétaire du # port pour un `stop` fiable, sans serveurs orphelins. $owner = Get-PortOwner if ($owner) { $owner.Pid | Set-Content -LiteralPath $PidFile } Write-Host "[OK] READY — ObsiGate v$($health.version) sur $BaseUrl ($($health.total_files) fichiers)." } }