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:
Claude Agent
2026-01-28 19:04:26 +00:00
parent dc1711acd0
commit 6718c956f7
9 changed files with 1766 additions and 26 deletions

View File

@@ -215,7 +215,15 @@ function Initialize-Venv {
function Update-ServiceToUseVenv {
<#
.SYNOPSIS
Updates NSSM service to use venv Python
Updates NSSM service to use wrapper script for SSH tunnel auto-start
.DESCRIPTION
Configures the service to use start-backend-service.ps1 wrapper which:
1. Starts SSH tunnels before backend
2. Waits for tunnel ports to be accessible
3. Starts uvicorn with correct settings
This ensures SSH tunnels are always running when the backend starts.
#>
$venvPython = Join-Path $Config.VenvPath "Scripts\python.exe"
@@ -231,24 +239,60 @@ function Update-ServiceToUseVenv {
return $false
}
# Find wrapper script
$wrapperScript = Join-Path $Config.InstallRoot "scripts\start-backend-service.ps1"
if (-not (Test-Path $wrapperScript)) {
# Fallback: try deployment location
$wrapperScript = Join-Path $PSScriptRoot "start-backend-service.ps1"
}
$useWrapper = Test-Path $wrapperScript
try {
# Get current application path
# Get current application
$currentApp = & nssm get $Config.ServiceName Application 2>&1
if ($currentApp -eq $venvPython) {
Write-Success "Service already configured to use venv Python"
return $true
if ($useWrapper) {
# Check if already using wrapper
if ($currentApp -like "*powershell*") {
$currentArgs = & nssm get $Config.ServiceName AppParameters 2>&1
if ($currentArgs -like "*start-backend-service.ps1*") {
Write-Success "Service already configured to use wrapper script"
return $true
}
}
Write-Step "Updating service to use wrapper script (SSH tunnel auto-start)..."
# Stop service first
Stop-ROAService | Out-Null
# Update service to use PowerShell wrapper
& nssm set $Config.ServiceName Application "powershell.exe"
& nssm set $Config.ServiceName AppParameters "-ExecutionPolicy Bypass -File `"$wrapperScript`""
Write-Success "Service updated to use wrapper: $wrapperScript"
Write-Info "SSH tunnels will auto-start when service starts"
} else {
# Fallback: use venv Python directly (old behavior)
if ($currentApp -eq $venvPython) {
Write-Success "Service already configured to use venv Python"
return $true
}
Write-Step "Updating service to use venv Python (no wrapper available)..."
# Stop service first
Stop-ROAService | Out-Null
# Update service application
& nssm set $Config.ServiceName Application $venvPython
& nssm set $Config.ServiceName AppParameters "-m uvicorn main:app --host 127.0.0.1 --port $($Config.ServicePort) --workers 1"
Write-Success "Service updated to use: $venvPython"
Write-Warning "Wrapper script not found - SSH tunnels must be started manually"
}
Write-Step "Updating service to use venv Python..."
# Stop service first
Stop-ROAService | Out-Null
# Update service application
& nssm set $Config.ServiceName Application $venvPython
Write-Success "Service updated to use: $venvPython"
return $true
} catch {
Write-Error "Failed to update service: $_"
@@ -777,6 +821,85 @@ function New-Backup {
# DEPLOYMENT FUNCTIONS
# =============================================================================
function Deploy-Scripts {
<#
.SYNOPSIS
Deploys PowerShell scripts to installation directory
.DESCRIPTION
Copies deployment scripts from package to install root's scripts folder.
This includes the wrapper script needed for SSH tunnel auto-start.
#>
param([string]$SourcePath)
Write-Step "Deploying scripts..."
$sourceScripts = Join-Path $SourcePath "scripts"
$destScripts = Join-Path $Config.InstallRoot "scripts"
# Scripts to deploy (essential for operation)
$requiredScripts = @(
"ssh-tunnel.ps1",
"start-backend-service.ps1",
"ROA2WEB-Console.ps1"
)
try {
# Create scripts directory if needed
if (-not (Test-Path $destScripts)) {
New-Item -ItemType Directory -Path $destScripts -Force | Out-Null
Write-Info "Created scripts directory: $destScripts"
}
$deployedCount = 0
# Copy scripts from package
if (Test-Path $sourceScripts) {
foreach ($script in $requiredScripts) {
$srcFile = Join-Path $sourceScripts $script
$destFile = Join-Path $destScripts $script
if (Test-Path $srcFile) {
Copy-Item -Path $srcFile -Destination $destFile -Force
Write-Info "Deployed: $script"
$deployedCount++
}
}
}
# Also copy from current script location (fallback)
foreach ($script in $requiredScripts) {
$srcFile = Join-Path $PSScriptRoot $script
$destFile = Join-Path $destScripts $script
if ((Test-Path $srcFile) -and (-not (Test-Path $destFile))) {
Copy-Item -Path $srcFile -Destination $destFile -Force
Write-Info "Deployed (from PSScriptRoot): $script"
$deployedCount++
}
}
if ($deployedCount -gt 0) {
Write-Success "Scripts deployed ($deployedCount files)"
} else {
Write-Warning "No scripts to deploy"
}
# Verify essential wrapper script
$wrapperPath = Join-Path $destScripts "start-backend-service.ps1"
if (Test-Path $wrapperPath) {
Write-Success "Service wrapper script ready: $wrapperPath"
} else {
Write-Warning "Service wrapper script not found - SSH tunnel auto-start may not work"
}
return $true
} catch {
Write-Error "Scripts deployment failed: $_"
return $false
}
}
function Deploy-Backend {
param([string]$SourcePath)
@@ -1019,6 +1142,9 @@ function Deploy-All {
Write-Warning "Backup failed, but continuing with deployment"
}
# Deploy scripts first (needed for service wrapper)
$scriptsOk = Deploy-Scripts -SourcePath $SourcePath
# Deploy backend (includes service restart)
$backendOk = Deploy-Backend -SourcePath $SourcePath
@@ -1028,6 +1154,12 @@ function Deploy-All {
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
Write-Host " DEPLOYMENT SUMMARY" -ForegroundColor Cyan
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host " Scripts: " -NoNewline
if ($scriptsOk) {
Write-Host "[OK] Success" -ForegroundColor Green
} else {
Write-Host "[X] Failed" -ForegroundColor Red
}
Write-Host " Backend: " -NoNewline
if ($backendOk) {
Write-Host "[OK] Success" -ForegroundColor Green
@@ -1042,7 +1174,7 @@ function Deploy-All {
}
Write-Host ("=" * 70) -ForegroundColor Cyan
return ($backendOk -and $frontendOk)
return ($scriptsOk -and $backendOk -and $frontendOk)
}
# =============================================================================