- Add ssh-tunnel.ps1: Windows SSH tunnel manager (equivalent to ssh-tunnel.sh) - Supports password auth via plink.exe (PuTTY) - Supports ssh_hostkey for non-interactive batch mode - Commands: start, stop, restart, status - Add start-backend-service.ps1: NSSM service wrapper - Starts SSH tunnels before uvicorn - Waits for tunnel ports to be accessible (30s timeout) - Configured by Install-ROA2WEB.ps1 - Add start.ps1: Windows equivalent of start.sh - Orchestrates SSH tunnel + backend + frontend startup - Add backend/shared/ssh_tunnel_manager.py: Python monitoring - Background asyncio task monitors tunnel health every 30s - Auto-restarts tunnels after 2 consecutive failures - Exposes status to /health endpoint - Update ROA2WEB-Console.ps1: - Add Deploy-Scripts function - Update Update-ServiceToUseVenv to use wrapper script - Fix PowerShell reserved variable ($PID -> $tunnelPid) - Fix script path detection (scripts/ vs deployment/windows/scripts/) - Update README.md with ssh_hostkey documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
226 lines
7.0 KiB
PowerShell
226 lines
7.0 KiB
PowerShell
<#
|
|
.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
|