feat: [US-004] Add SSH tunnel auto-start for Windows services
- 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>
This commit is contained in:
225
deployment/windows/scripts/start-backend-service.ps1
Normal file
225
deployment/windows/scripts/start-backend-service.ps1
Normal file
@@ -0,0 +1,225 @@
|
||||
<#
|
||||
.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
|
||||
Reference in New Issue
Block a user