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

@@ -73,6 +73,7 @@ telegram_bot_task = None
ocr_job_worker_running = False
cleanup_task_running = False
email_cache_running = False
ssh_tunnel_monitoring = False
# ============================================================================
@@ -265,6 +266,40 @@ async def init_email_server_cache():
email_cache_running = False
async def init_ssh_tunnel_monitoring():
"""Initialize SSH tunnel monitoring with auto-reconnect.
This does NOT start tunnels - they should already be running
(started by start.sh / start.ps1 / start-backend-service.ps1).
Responsibilities:
- Monitor tunnel health via port checks (every 30s)
- Auto-restart tunnels if they go down
- Expose status for /health endpoint
"""
global ssh_tunnel_monitoring
logger.info("[SSH-MONITOR] Initializing tunnel monitoring...")
try:
from backend.shared.ssh_tunnel_manager import ssh_tunnel_manager
success = await ssh_tunnel_manager.start_monitoring()
ssh_tunnel_monitoring = success
if success:
status = ssh_tunnel_manager.get_status()
if status["status"] == "not_configured":
logger.info("[SSH-MONITOR] No tunnels configured (direct connection mode)")
else:
logger.info(f"[SSH-MONITOR] ✅ Monitoring active: {status['status']}")
else:
logger.warning("[SSH-MONITOR] ⚠️ Failed to start monitoring")
except Exception as e:
logger.warning(f"[SSH-MONITOR] ⚠️ Init failed: {e}")
ssh_tunnel_monitoring = False
async def run_telegram_bot():
"""Run Telegram bot as background task."""
logger.info("[TELEGRAM] Starting bot...")
@@ -381,7 +416,10 @@ async def startup_event():
# Step 5: Initialize email-server cache for multi-Oracle (US-003)
await init_email_server_cache()
# Step 6: Start Telegram bot as background task
# Step 6: Initialize SSH tunnel monitoring (auto-reconnect)
await init_ssh_tunnel_monitoring()
# Step 7: Start Telegram bot as background task
if settings.telegram_bot_token:
telegram_bot_task = asyncio.create_task(run_telegram_bot())
logger.info("[STARTUP] ✅ Telegram bot task created")
@@ -401,13 +439,24 @@ async def startup_event():
@app.on_event("shutdown")
async def shutdown_event():
"""Application shutdown - Cleanup resources."""
global telegram_bot_task, ocr_job_worker_running, cleanup_task_running, email_cache_running
global telegram_bot_task, ocr_job_worker_running, cleanup_task_running, email_cache_running, ssh_tunnel_monitoring
logger.info("=" * 80)
logger.info("[SHUTDOWN] Stopping ROA2WEB Unified Backend...")
logger.info("=" * 80)
try:
# Stop SSH tunnel monitoring
if ssh_tunnel_monitoring:
logger.info("[SHUTDOWN] Stopping SSH tunnel monitoring...")
try:
from backend.shared.ssh_tunnel_manager import ssh_tunnel_manager
await ssh_tunnel_manager.stop_monitoring()
ssh_tunnel_monitoring = False
logger.info("[SHUTDOWN] SSH tunnel monitoring stopped")
except Exception as e:
logger.error(f"[SHUTDOWN] SSH tunnel monitoring error: {e}")
# Stop email cache auto-refresh (US-003)
if email_cache_running:
logger.info("[SHUTDOWN] Stopping email cache auto-refresh...")
@@ -609,6 +658,14 @@ async def health_check():
except Exception as e:
health_status["modules"]["ocr_worker"] = f"error: {str(e)}"
# Check SSH tunnels
global ssh_tunnel_monitoring
try:
from backend.shared.ssh_tunnel_manager import ssh_tunnel_manager
health_status["modules"]["ssh_tunnels"] = ssh_tunnel_manager.get_status()
except Exception as e:
health_status["modules"]["ssh_tunnels"] = f"error: {str(e)}"
return health_status