pm2_watchdog.ps1

# pm2_watchdog.ps1
$ErrorActionPreference = 'Stop'

# ── Konfiguration ──────────────────────────────────────────────
$workDir       = 'C:\FastStats\Scripts\node_webhook'
$logFile       = Join-Path $workDir 'logs\watchdog.log'
$pm2Home       = Join-Path $workDir '.pm2'
$nodeExe       = 'C:\Program Files\nodejs\node.exe'
$pm2Bin        = 'C:\Program Files\nodejs\node_modules\pm2\bin\pm2'   # ggf. anpassen
$ecosystemFile = Join-Path $workDir 'ecosystem.config.cjs'
$expectedApps  = @('webhook-receiver','webhook-worker')

# ── Vorbereitung ───────────────────────────────────────────────
$env:PM2_HOME = $pm2Home

Import-Module WriteLog
$logDir = Split-Path $logFile -Parent
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }
Set-Logfile -Path $logFile

# ── PM2-Helfer ─────────────────────────────────────────────────
function Invoke-Pm2 {
    param([string[]]$Arguments)
    # Stdout und Stderr getrennt erfassen damit Fehler nicht verschluckt werden
    $output = & $nodeExe $pm2Bin @Arguments 2>&1
    $text   = ($output | ForEach-Object { $_.ToString() }) -join "`n"
    return @{
        Text     = $text
        ExitCode = $LASTEXITCODE
    }
}

function Get-Pm2Procs {
    $result = Invoke-Pm2 @('jlist')
    $raw = $result.Text
    $i = $raw.IndexOf('[')
    if ($i -lt 0) { return $null }            # Daemon nicht erreichbar
    try { return ($raw.Substring($i) | ConvertFrom-Json -AsHashtable) }
    catch {
        Write-Log -Message "Get-Pm2Procs: JSON-Parse fehlgeschlagen. Raw-Output: $raw" -Severity ERROR
        return $null
    }
}

function Test-EcosystemFile {
    # Prüft VOR dem Start, ob die ecosystem-Datei syntaktisch ladbar ist
    # und ob die referenzierten script-Pfade tatsächlich existieren.
    if (-not (Test-Path $ecosystemFile)) {
        Write-Log -Message "Ecosystem-Datei nicht gefunden: $ecosystemFile" -Severity ERROR
        return $false
    }

    $nodeCheck = & $nodeExe -e "try { const cfg = require('$($ecosystemFile.Replace('\','\\'))'); console.log(JSON.stringify(cfg.apps.map(a => ({name:a.name, script:a.script})))); } catch(e) { console.error('PARSE_ERROR:' + e.message); process.exit(1); }" 2>&1
    $nodeExitCode = $LASTEXITCODE

    if ($nodeExitCode -ne 0 -or $nodeCheck -match '^PARSE_ERROR:') {
        Write-Log -Message "Ecosystem-Datei kann nicht geladen werden: $nodeCheck" -Severity ERROR
        return $false
    }

    try {
        $apps = $nodeCheck | ConvertFrom-Json
        $allOk = $true
        foreach ($app in $apps) {
            if (-not (Test-Path $app.script)) {
                Write-Log -Message "Script-Pfad existiert nicht für App '$($app.name)': $($app.script)" -Severity ERROR
                $allOk = $false
            }
        }
        return $allOk
    } catch {
        Write-Log -Message "Konnte App-Liste aus ecosystem.config.js nicht parsen: $_" -Severity ERROR
        return $false
    }
}

function Start-Pm2Apps {
    param(
        [string]$OnlyApp = $null   # Falls gesetzt: nur DIESE eine App (re)starten,
                                   # nicht die komplette ecosystem-Datei.
                                   # Wichtig, damit eine kurzzeitig "fehlende" App
                                   # nicht versehentlich einen Neustart der bereits
                                   # gesunden Apps auslöst (kann laufende DB-Connections
                                   # / vorbereitete SQLite-Statements invalidieren).
    )

    if (-not (Test-EcosystemFile)) {
        Write-Log -Message "Ecosystem-Datei-Validierung fehlgeschlagen — Start wird trotzdem versucht, Ergebnis wird geprüft." -Severity ERROR
    }

    if ($OnlyApp) {
        Write-Log -Message "Starte gezielt NUR '$OnlyApp' aus $ecosystemFile (andere Apps bleiben unberührt)." -Severity WARNING
        $result = Invoke-Pm2 @('start', $ecosystemFile, '--only', $OnlyApp)
    } else {
        Write-Log -Message "Starte ALLE Apps aus $ecosystemFile (Daemon war nicht erreichbar)." -Severity WARNING
        $result = Invoke-Pm2 @('start', $ecosystemFile)
    }

    if ($result.ExitCode -ne 0) {
        Write-Log -Message "pm2 start fehlgeschlagen (ExitCode=$($result.ExitCode)). Output: $($result.Text)" -Severity ERROR
    } else {
        Write-Log -Message "pm2 start Output: $($result.Text)" -Severity INFO
    }
    Start-Sleep -Seconds 3
}

# ── 1. Daemon-Check (z.B. nach Reboot) ─────────────────────────
$procs = Get-Pm2Procs
$didStart = $false

if ($null -eq $procs) {
    Write-Log -Message 'PM2-Daemon nicht erreichbar — starte Apps neu.' -Severity WARNING
    Start-Pm2Apps
    $didStart = $true
    $procs = Get-Pm2Procs
}

# ── 2. App-Status prüfen ───────────────────────────────────────
$problems = @()

foreach ($name in $expectedApps) {
    $p = $procs | Where-Object { $_.name -eq $name }

    if (-not $p) {
        $problems += "$name fehlt"
        Write-Log -Message "$name fehlt in PM2 — starte gezielt nur diese App." -Severity WARNING
        Start-Pm2Apps -OnlyApp $name
        $procs = Get-Pm2Procs
    }
    elseif ($p.pm2_env.status -ne 'online') {
        $problems += "$name ist $($p.pm2_env.status)"
        Write-Log -Message "$name ist '$($p.pm2_env.status)' — restart." -Severity WARNING
        $restartResult = Invoke-Pm2 @('restart', $name)
        if ($restartResult.ExitCode -ne 0) {
            Write-Log -Message "pm2 restart $name fehlgeschlagen: $($restartResult.Text)" -Severity ERROR
        }
    }
}

# ── 3. Ergebnis loggen ─────────────────────────────────────────
if ($problems.Count -eq 0 -and -not $didStart) {
    Write-Log -Message "OK — alle Apps online: $($expectedApps -join ', ')." -Severity VERBOSE
} else {
    Start-Sleep -Seconds 2
    $final = Get-Pm2Procs
    $states = foreach ($name in $expectedApps) {
        $p = $final | Where-Object { $_.name -eq $name }
        if ($p) { "$name=$($p.pm2_env.status)" } else { "$name=fehlt" }
    }
    Write-Log -Message "Nach Reparatur: $($states -join ', ')." -Severity INFO

    # Wenn nach allen Reparaturversuchen immer noch Apps fehlen,
    # nochmal explizit die letzten pm2-Logs der App ausgeben (falls vorhanden)
    foreach ($name in $expectedApps) {
        $p = $final | Where-Object { $_.name -eq $name }
        if (-not $p) {
            $logsResult = Invoke-Pm2 @('logs', $name, '--lines', '20', '--nostream')
            Write-Log -Message "Letzte pm2-Logs für '$name' (falls vorhanden): $($logsResult.Text)" -Severity ERROR
        }
    }
}