<# .SYNOPSIS ROA2WEB Backend Service Wrapper for NSSM .DESCRIPTION This script is called by NSSM (Windows Service Manager) to start the backend. It ensures SSH tunnels are running before starting uvicorn. Flow: 1. Start SSH tunnels via ssh-tunnel.ps1 2. Wait for tunnel ports to be accessible (timeout 30s) 3. Start uvicorn (blocking - NSSM monitors this process) This wrapper ensures the database connection is available before the FastAPI application tries to initialize the Oracle pool. .NOTES Author: ROA2WEB Team Version: 1.0 NSSM Configuration: - Application: powershell.exe - Arguments: -ExecutionPolicy Bypass -File "C:\path\to\start-backend-service.ps1" - AppDirectory: C:\inetpub\wwwroot\roa2web\backend #> $ErrorActionPreference = "Stop" # ============================================================================= # CONFIGURATION # ============================================================================= $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path # Detect paths (can run from scripts dir or project root) $PossibleRoots = @( (Join-Path $ScriptDir "..\..\.."), # From deployment/windows/scripts $ScriptDir, # From project root "C:\inetpub\wwwroot\roa2web" # Production path ) $ProjectRoot = $null foreach ($path in $PossibleRoots) { $resolved = [System.IO.Path]::GetFullPath($path) if (Test-Path (Join-Path $resolved "backend\main.py")) { $ProjectRoot = $resolved break } } if (-not $ProjectRoot) { Write-Host "[ERROR] Cannot find project root (looking for backend/main.py)" -ForegroundColor Red exit 1 } $BackendDir = Join-Path $ProjectRoot "backend" # SSH tunnel script - check production location first, then development $TunnelsScript = Join-Path $ProjectRoot "scripts\ssh-tunnel.ps1" if (-not (Test-Path $TunnelsScript)) { $TunnelsScript = Join-Path $ProjectRoot "deployment\windows\scripts\ssh-tunnel.ps1" } $TunnelsConfig = Join-Path $BackendDir "ssh-tunnels.json" $VenvPath = "C:\inetpub\wwwroot\roa2web-venv" $VenvPython = Join-Path $VenvPath "Scripts\python.exe" $LogDir = Join-Path $ProjectRoot "logs" # Fallback to local venv if production venv doesn't exist if (-not (Test-Path $VenvPython)) { $VenvPath = Join-Path $BackendDir "venv" $VenvPython = Join-Path $VenvPath "Scripts\python.exe" } # ============================================================================= # HELPER FUNCTIONS # ============================================================================= function Write-Log { param([string]$Message, [string]$Level = "INFO") $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Write-Host "[$timestamp] [$Level] $Message" } function Test-PortOpen { param([int]$Port, [int]$Timeout = 3) try { $tcpClient = New-Object System.Net.Sockets.TcpClient $result = $tcpClient.BeginConnect("127.0.0.1", $Port, $null, $null) $success = $result.AsyncWaitHandle.WaitOne($Timeout * 1000) if ($success) { $tcpClient.EndConnect($result) $tcpClient.Close() return $true } return $false } catch { return $false } } function Get-TunnelPorts { # Read ports from ssh-tunnels.json if (-not (Test-Path $TunnelsConfig)) { Write-Log "No ssh-tunnels.json found, assuming no tunnels needed" "WARN" return @() } try { $tunnels = Get-Content $TunnelsConfig -Raw | ConvertFrom-Json $ports = @() foreach ($tunnel in $tunnels) { # Only include tunnels that have ssh_host configured if ($tunnel.ssh_host) { $ports += [int]$tunnel.local_port } } return $ports } catch { Write-Log "Failed to parse ssh-tunnels.json: $_" "ERROR" return @() } } # ============================================================================= # MAIN STARTUP SEQUENCE # ============================================================================= Write-Log "==========================================" Write-Log "ROA2WEB Backend Service Wrapper Starting" Write-Log "==========================================" Write-Log "Project Root: $ProjectRoot" Write-Log "Backend Dir: $BackendDir" Write-Log "Venv Python: $VenvPython" # Ensure log directory exists if (-not (Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir -Force | Out-Null } # ------------------------------------------------------------------------- # Step 1: Start SSH Tunnels # ------------------------------------------------------------------------- Write-Log "Step 1: Starting SSH Tunnels..." if (Test-Path $TunnelsScript) { try { & $TunnelsScript start Write-Log "SSH tunnel script executed" } catch { Write-Log "SSH tunnel script failed: $_" "ERROR" # Continue anyway - tunnels might already be running } } else { Write-Log "SSH tunnel script not found at $TunnelsScript" "WARN" } # ------------------------------------------------------------------------- # Step 2: Wait for Tunnel Ports # ------------------------------------------------------------------------- $tunnelPorts = Get-TunnelPorts if ($tunnelPorts.Count -gt 0) { Write-Log "Step 2: Waiting for tunnel ports: $($tunnelPorts -join ', ')..." $timeout = 30 $waited = 0 $allReady = $false while ($waited -lt $timeout) { $allReady = $true foreach ($port in $tunnelPorts) { if (-not (Test-PortOpen $port)) { $allReady = $false break } } if ($allReady) { Write-Log "All tunnel ports are accessible" break } Start-Sleep -Seconds 1 $waited++ if ($waited % 5 -eq 0) { Write-Log "Still waiting for tunnel ports... ($waited/${timeout}s)" } } if (-not $allReady) { Write-Log "Tunnel ports not ready after ${timeout}s - continuing anyway" "WARN" } } else { Write-Log "Step 2: No tunnel ports configured, skipping wait" } # ------------------------------------------------------------------------- # Step 3: Start uvicorn (blocking) # ------------------------------------------------------------------------- Write-Log "Step 3: Starting uvicorn..." Write-Log "Working Directory: $BackendDir" Write-Log "Python: $VenvPython" # Verify Python exists if (-not (Test-Path $VenvPython)) { Write-Log "Python not found at $VenvPython" "ERROR" exit 1 } # Set working directory Set-Location $BackendDir # Set PYTHONPATH for shared modules $env:PYTHONPATH = "$ProjectRoot;$BackendDir" # Start uvicorn (this blocks - NSSM monitors this process) # NOTE: --workers 1 is required because Telegram bot uses polling (single instance only) Write-Log "Executing: $VenvPython -m uvicorn main:app --host 127.0.0.1 --port 8000 --workers 1" & $VenvPython -m uvicorn main:app --host 127.0.0.1 --port 8000 --workers 1 # If we get here, uvicorn has exited $exitCode = $LASTEXITCODE Write-Log "uvicorn exited with code: $exitCode" "WARN" exit $exitCode