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:
391
start.ps1
Normal file
391
start.ps1
Normal file
@@ -0,0 +1,391 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
ROA2WEB - Unified Starter Script for Windows (equivalent to start.sh)
|
||||
|
||||
.DESCRIPTION
|
||||
Orchestrates startup of all ROA2WEB services on Windows:
|
||||
1. SSH Tunnels (if needed for environment)
|
||||
2. Unified Backend (port 8000)
|
||||
3. Unified Frontend (port 3000) - optional for development
|
||||
|
||||
This is the Windows equivalent of the Linux start.sh script.
|
||||
|
||||
.PARAMETER Environment
|
||||
Target environment: prod or test
|
||||
- prod: Uses SSH tunnel to Oracle
|
||||
- test: Direct connection (no tunnel needed)
|
||||
|
||||
.PARAMETER Action
|
||||
Action to perform: start or stop
|
||||
|
||||
.PARAMETER SkipFrontend
|
||||
Skip starting the frontend (useful for production where IIS serves static files)
|
||||
|
||||
.EXAMPLE
|
||||
.\start.ps1 prod
|
||||
Start all services in production mode
|
||||
|
||||
.EXAMPLE
|
||||
.\start.ps1 test
|
||||
Start all services in test mode
|
||||
|
||||
.EXAMPLE
|
||||
.\start.ps1 prod stop
|
||||
Stop all services
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Version: 1.0
|
||||
#>
|
||||
|
||||
param(
|
||||
[Parameter(Position=0)]
|
||||
[ValidateSet("prod", "production", "test")]
|
||||
[string]$Environment = "prod",
|
||||
|
||||
[Parameter(Position=1)]
|
||||
[ValidateSet("start", "stop")]
|
||||
[string]$Action = "start",
|
||||
|
||||
[switch]$SkipFrontend
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
|
||||
# Normalize environment name
|
||||
if ($Environment -eq "production") { $Environment = "prod" }
|
||||
|
||||
$Config = @{
|
||||
prod = @{
|
||||
Name = "PROD"
|
||||
EnvFile = ".env.prod"
|
||||
NeedsTunnel = $true
|
||||
LogDir = Join-Path $ScriptDir "logs"
|
||||
}
|
||||
test = @{
|
||||
Name = "TEST"
|
||||
EnvFile = ".env.test"
|
||||
NeedsTunnel = $false
|
||||
LogDir = $env:TEMP
|
||||
}
|
||||
}
|
||||
|
||||
$Env = $Config[$Environment]
|
||||
$BackendDir = Join-Path $ScriptDir "backend"
|
||||
$BackendLog = Join-Path $Env.LogDir "backend-stderr.log"
|
||||
$FrontendLog = Join-Path $Env.LogDir "frontend.log"
|
||||
$VenvPath = Join-Path $BackendDir "venv"
|
||||
$VenvPython = Join-Path $VenvPath "Scripts\python.exe"
|
||||
$TunnelScript = Join-Path $ScriptDir "deployment\windows\scripts\ssh-tunnel.ps1"
|
||||
|
||||
# PID tracking files
|
||||
$PidDir = Join-Path $env:TEMP "roa2web_pids"
|
||||
if (-not (Test-Path $PidDir)) {
|
||||
New-Item -ItemType Directory -Path $PidDir -Force | Out-Null
|
||||
}
|
||||
$BackendPidFile = Join-Path $PidDir "backend.pid"
|
||||
$FrontendPidFile = Join-Path $PidDir "frontend.pid"
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Header {
|
||||
Write-Host ""
|
||||
Write-Host "============================================" -ForegroundColor Blue
|
||||
Write-Host " ROA2WEB Unified Starter (Windows)" -ForegroundColor Blue
|
||||
Write-Host " Environment: $($Env.Name)" -ForegroundColor Cyan
|
||||
Write-Host "============================================" -ForegroundColor Blue
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "[UNIFIED-$($Env.Name)] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host "[SUCCESS] $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host "[WARNING] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host "[ERROR] $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Test-PortOpen {
|
||||
param([int]$Port)
|
||||
try {
|
||||
$tcpClient = New-Object System.Net.Sockets.TcpClient
|
||||
$tcpClient.Connect("127.0.0.1", $Port)
|
||||
$tcpClient.Close()
|
||||
return $true
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Stop-ProcessByPidFile {
|
||||
param([string]$PidFile, [string]$Name)
|
||||
|
||||
if (Test-Path $PidFile) {
|
||||
$pid = Get-Content $PidFile -ErrorAction SilentlyContinue
|
||||
if ($pid) {
|
||||
$process = Get-Process -Id $pid -ErrorAction SilentlyContinue
|
||||
if ($process) {
|
||||
Write-Step "Stopping $Name (PID: $pid)..."
|
||||
Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
}
|
||||
Remove-Item $PidFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# STOP SERVICES
|
||||
# =============================================================================
|
||||
|
||||
function Stop-AllServices {
|
||||
Write-Header
|
||||
Write-Step "Stopping all services..."
|
||||
Write-Host ""
|
||||
|
||||
# Stop Backend
|
||||
Write-Step "1. Stopping Backend..."
|
||||
Stop-ProcessByPidFile $BackendPidFile "Backend"
|
||||
|
||||
# Also kill any uvicorn processes
|
||||
Get-Process -Name "python" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.CommandLine -like "*uvicorn*main:app*" } |
|
||||
ForEach-Object {
|
||||
Write-Step " Killing uvicorn process (PID: $($_.Id))..."
|
||||
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if (Test-PortOpen 8000) {
|
||||
Write-Warning "Port 8000 still in use, force killing..."
|
||||
$netstat = netstat -ano | Select-String ":8000" | Select-String "LISTENING"
|
||||
if ($netstat) {
|
||||
$pid = ($netstat -split '\s+')[-1]
|
||||
Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# Stop Frontend
|
||||
Write-Step "2. Stopping Frontend..."
|
||||
Stop-ProcessByPidFile $FrontendPidFile "Frontend"
|
||||
|
||||
Get-Process -Name "node" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.CommandLine -like "*vite*" } |
|
||||
ForEach-Object {
|
||||
Write-Step " Killing Vite process (PID: $($_.Id))..."
|
||||
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Stop SSH Tunnels
|
||||
Write-Step "3. Stopping SSH Tunnels..."
|
||||
if (Test-Path $TunnelScript) {
|
||||
& $TunnelScript stop
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Success "All services stopped."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# START SERVICES
|
||||
# =============================================================================
|
||||
|
||||
function Start-AllServices {
|
||||
Write-Header
|
||||
Write-Step "Starting ROA2WEB Ultrathin Monolith..."
|
||||
Write-Host ""
|
||||
|
||||
# Ensure log directory exists
|
||||
if (-not (Test-Path $Env.LogDir)) {
|
||||
New-Item -ItemType Directory -Path $Env.LogDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Step 1: SSH Tunnels
|
||||
# -------------------------------------------------------------------------
|
||||
Write-Step "1. Checking SSH Tunnels..."
|
||||
|
||||
if (Test-Path $TunnelScript) {
|
||||
if ($Env.NeedsTunnel) {
|
||||
& $TunnelScript start
|
||||
Write-Success "SSH Tunnels started"
|
||||
Start-Sleep -Seconds 2
|
||||
} else {
|
||||
Write-Success "$($Env.Name) uses direct connection - no tunnel needed"
|
||||
}
|
||||
} else {
|
||||
Write-Warning "SSH tunnel script not found: $TunnelScript"
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Step 2: Backend
|
||||
# -------------------------------------------------------------------------
|
||||
Write-Step "2. Starting Unified Backend on port 8000..."
|
||||
|
||||
if (Test-PortOpen 8000) {
|
||||
Write-Warning "Port 8000 already in use - Backend may be running"
|
||||
} else {
|
||||
# Check venv exists
|
||||
if (-not (Test-Path $VenvPython)) {
|
||||
Write-Step "Creating Python virtual environment..."
|
||||
& python -m venv $VenvPath
|
||||
}
|
||||
|
||||
# Check env file
|
||||
$envFilePath = Join-Path $BackendDir $Env.EnvFile
|
||||
if (-not (Test-Path $envFilePath)) {
|
||||
Write-Error "$($Env.EnvFile) not found at $envFilePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Copy env file to active .env
|
||||
Write-Step "Using $($Env.Name) environment ($($Env.EnvFile))..."
|
||||
Copy-Item -Path $envFilePath -Destination (Join-Path $BackendDir ".env") -Force
|
||||
|
||||
# Start backend
|
||||
Write-Step "Starting unified backend..."
|
||||
$backendProcess = Start-Process -FilePath $VenvPython `
|
||||
-ArgumentList "-m", "uvicorn", "main:app", "--host", "127.0.0.1", "--port", "8000", "--workers", "1" `
|
||||
-WorkingDirectory $BackendDir `
|
||||
-WindowStyle Hidden `
|
||||
-RedirectStandardError $BackendLog `
|
||||
-PassThru
|
||||
|
||||
$backendProcess.Id | Out-File -FilePath $BackendPidFile -NoNewline
|
||||
|
||||
# Wait for backend to start
|
||||
Write-Step "Waiting for Backend to initialize..."
|
||||
$maxWait = 45
|
||||
$elapsed = 0
|
||||
while ($elapsed -lt $maxWait) {
|
||||
if (Test-PortOpen 8000) {
|
||||
Write-Success "Unified Backend started on http://localhost:8000"
|
||||
break
|
||||
}
|
||||
Start-Sleep -Seconds 1
|
||||
$elapsed++
|
||||
if ($elapsed % 5 -eq 0) {
|
||||
Write-Step " Still initializing... ($elapsed/${maxWait}s)"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-PortOpen 8000)) {
|
||||
Write-Error "Backend failed to start - check $BackendLog"
|
||||
Get-Content $BackendLog -Tail 30
|
||||
Stop-AllServices
|
||||
}
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Step 3: Frontend (optional)
|
||||
# -------------------------------------------------------------------------
|
||||
if (-not $SkipFrontend) {
|
||||
Write-Step "3. Starting Unified Frontend on port 3000..."
|
||||
|
||||
if (Test-PortOpen 3000) {
|
||||
Write-Warning "Port 3000 already in use - Frontend may be running"
|
||||
} else {
|
||||
# Check node_modules
|
||||
$nodeModules = Join-Path $ScriptDir "node_modules"
|
||||
if (-not (Test-Path $nodeModules)) {
|
||||
Write-Step "Installing Frontend dependencies..."
|
||||
Push-Location $ScriptDir
|
||||
& npm install
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
# Start frontend
|
||||
Write-Step "Starting Vite development server..."
|
||||
$frontendProcess = Start-Process -FilePath "cmd.exe" `
|
||||
-ArgumentList "/c", "npm", "run", "dev" `
|
||||
-WorkingDirectory $ScriptDir `
|
||||
-WindowStyle Hidden `
|
||||
-RedirectStandardOutput $FrontendLog `
|
||||
-PassThru
|
||||
|
||||
$frontendProcess.Id | Out-File -FilePath $FrontendPidFile -NoNewline
|
||||
|
||||
# Wait for frontend
|
||||
Write-Step "Waiting for Vite to initialize..."
|
||||
$maxWait = 15
|
||||
$elapsed = 0
|
||||
while ($elapsed -lt $maxWait) {
|
||||
if (Test-PortOpen 3000) {
|
||||
Write-Success "Unified Frontend started on http://localhost:3000"
|
||||
break
|
||||
}
|
||||
Start-Sleep -Seconds 2
|
||||
$elapsed += 2
|
||||
}
|
||||
|
||||
if (-not (Test-PortOpen 3000)) {
|
||||
Write-Error "Frontend failed to start - check $FrontendLog"
|
||||
Get-Content $FrontendLog
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Step "3. Skipping Frontend (use IIS for static files)"
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Summary
|
||||
# -------------------------------------------------------------------------
|
||||
Write-Host ""
|
||||
Write-Success "ROA2WEB Ultrathin Monolith ($($Env.Name)) is now running!"
|
||||
Write-Host ""
|
||||
Write-Host "Services:" -ForegroundColor Blue
|
||||
if ($Env.NeedsTunnel) {
|
||||
Write-Host " * SSH Tunnel: Active (Oracle DB connection)"
|
||||
} else {
|
||||
Write-Host " * Oracle Connection: Direct (no SSH tunnel needed)"
|
||||
}
|
||||
Write-Host " * Unified Backend: http://localhost:8000"
|
||||
Write-Host " +-- Reports API: http://localhost:8000/api/reports/*"
|
||||
Write-Host " +-- Data Entry: http://localhost:8000/api/data-entry/*"
|
||||
Write-Host " +-- Telegram: http://localhost:8000/api/telegram/*"
|
||||
if (-not $SkipFrontend) {
|
||||
Write-Host " * Unified Frontend: http://localhost:3000"
|
||||
}
|
||||
Write-Host ""
|
||||
Write-Host "API Documentation:" -ForegroundColor Blue
|
||||
Write-Host " * API Docs: http://localhost:8000/docs"
|
||||
Write-Host " * Health Check: http://localhost:8000/health"
|
||||
Write-Host ""
|
||||
Write-Host "Log Files:" -ForegroundColor Blue
|
||||
Write-Host " * Backend: $BackendLog"
|
||||
if (-not $SkipFrontend) {
|
||||
Write-Host " * Frontend: $FrontendLog"
|
||||
}
|
||||
Write-Host ""
|
||||
Write-Host "To stop services: .\start.ps1 $Environment stop" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN
|
||||
# =============================================================================
|
||||
|
||||
switch ($Action) {
|
||||
"start" { Start-AllServices }
|
||||
"stop" { Stop-AllServices }
|
||||
}
|
||||
Reference in New Issue
Block a user