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:
70
README.md
70
README.md
@@ -265,6 +265,76 @@ All endpoints prefixed with `/api`:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## SSH Tunnel Configuration
|
||||||
|
|
||||||
|
ROA2WEB uses SSH tunnels to connect to Oracle servers. Configuration is in `backend/ssh-tunnels.json`.
|
||||||
|
|
||||||
|
### Setup (one-time)
|
||||||
|
|
||||||
|
**Linux:**
|
||||||
|
```bash
|
||||||
|
# Copy SSH key to secrets folder
|
||||||
|
cp ~/.ssh/your_key backend/secrets/vending.ssh_key
|
||||||
|
chmod 600 backend/secrets/vending.ssh_key
|
||||||
|
|
||||||
|
# Or use password (requires sshpass)
|
||||||
|
echo "your_password" > backend/secrets/vending.ssh_pass
|
||||||
|
sudo apt install sshpass
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
```powershell
|
||||||
|
# Option 1: SSH Key (recommended)
|
||||||
|
ssh-keygen -t rsa -b 4096 -f C:\inetpub\wwwroot\roa2web\backend\secrets\vending.ssh_key -N ""
|
||||||
|
# Then add public key to remote server's ~/.ssh/authorized_keys
|
||||||
|
|
||||||
|
# Option 2: Password (requires PuTTY)
|
||||||
|
choco install putty -y
|
||||||
|
echo "your_password" > C:\inetpub\wwwroot\roa2web\backend\secrets\vending.ssh_pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration File
|
||||||
|
|
||||||
|
`backend/ssh-tunnels.json`:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "vending",
|
||||||
|
"name": "Vending Master",
|
||||||
|
"local_port": 1522,
|
||||||
|
"ssh_host": "79.119.86.134",
|
||||||
|
"ssh_port": 22122,
|
||||||
|
"ssh_user": "romfast",
|
||||||
|
"ssh_hostkey": "SHA256:xxxxx",
|
||||||
|
"oracle_host": "127.0.0.1",
|
||||||
|
"oracle_port": 1521
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:**
|
||||||
|
- `local_port` must match the port in `ORACLE_SERVERS` (.env) for this server
|
||||||
|
- `ssh_hostkey` is **required on Windows** (plink batch mode). Get it with:
|
||||||
|
```powershell
|
||||||
|
plink.exe -ssh user@host -P port "exit"
|
||||||
|
# Accept the key, then copy SHA256 fingerprint from output
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
| Platform | Start | Stop | Status |
|
||||||
|
|----------|-------|------|--------|
|
||||||
|
| Linux | `./ssh-tunnel.sh start` | `./ssh-tunnel.sh stop` | `./ssh-tunnel.sh status` |
|
||||||
|
| Windows | `.\scripts\ssh-tunnel.ps1 start` | `.\scripts\ssh-tunnel.ps1 stop` | `.\scripts\ssh-tunnel.ps1 status` |
|
||||||
|
|
||||||
|
### Auto-Start (Production)
|
||||||
|
|
||||||
|
- **Linux**: `start.sh` automatically starts tunnels before backend
|
||||||
|
- **Windows Service**: `start-backend-service.ps1` wrapper starts tunnels before uvicorn
|
||||||
|
- **Auto-Reconnect**: Backend monitors tunnels and restarts them if they drop (every 30s check)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Environment Configuration
|
## Environment Configuration
|
||||||
|
|
||||||
Copy `.env.example` to `.env` in each microservice and configure:
|
Copy `.env.example` to `.env` in each microservice and configure:
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ telegram_bot_task = None
|
|||||||
ocr_job_worker_running = False
|
ocr_job_worker_running = False
|
||||||
cleanup_task_running = False
|
cleanup_task_running = False
|
||||||
email_cache_running = False
|
email_cache_running = False
|
||||||
|
ssh_tunnel_monitoring = False
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -265,6 +266,40 @@ async def init_email_server_cache():
|
|||||||
email_cache_running = False
|
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():
|
async def run_telegram_bot():
|
||||||
"""Run Telegram bot as background task."""
|
"""Run Telegram bot as background task."""
|
||||||
logger.info("[TELEGRAM] Starting bot...")
|
logger.info("[TELEGRAM] Starting bot...")
|
||||||
@@ -381,7 +416,10 @@ async def startup_event():
|
|||||||
# Step 5: Initialize email-server cache for multi-Oracle (US-003)
|
# Step 5: Initialize email-server cache for multi-Oracle (US-003)
|
||||||
await init_email_server_cache()
|
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:
|
if settings.telegram_bot_token:
|
||||||
telegram_bot_task = asyncio.create_task(run_telegram_bot())
|
telegram_bot_task = asyncio.create_task(run_telegram_bot())
|
||||||
logger.info("[STARTUP] ✅ Telegram bot task created")
|
logger.info("[STARTUP] ✅ Telegram bot task created")
|
||||||
@@ -401,13 +439,24 @@ async def startup_event():
|
|||||||
@app.on_event("shutdown")
|
@app.on_event("shutdown")
|
||||||
async def shutdown_event():
|
async def shutdown_event():
|
||||||
"""Application shutdown - Cleanup resources."""
|
"""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("=" * 80)
|
||||||
logger.info("[SHUTDOWN] Stopping ROA2WEB Unified Backend...")
|
logger.info("[SHUTDOWN] Stopping ROA2WEB Unified Backend...")
|
||||||
logger.info("=" * 80)
|
logger.info("=" * 80)
|
||||||
|
|
||||||
try:
|
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)
|
# Stop email cache auto-refresh (US-003)
|
||||||
if email_cache_running:
|
if email_cache_running:
|
||||||
logger.info("[SHUTDOWN] Stopping email cache auto-refresh...")
|
logger.info("[SHUTDOWN] Stopping email cache auto-refresh...")
|
||||||
@@ -609,6 +658,14 @@ async def health_check():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
health_status["modules"]["ocr_worker"] = f"error: {str(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
|
return health_status
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
350
backend/shared/ssh_tunnel_manager.py
Normal file
350
backend/shared/ssh_tunnel_manager.py
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
"""
|
||||||
|
SSH Tunnel Manager - Cross-Platform Monitoring and Auto-Reconnect
|
||||||
|
|
||||||
|
This module provides MONITORING and AUTO-RECONNECT for SSH tunnels.
|
||||||
|
It does NOT start tunnels - that's the responsibility of:
|
||||||
|
- Linux: start.sh → ssh-tunnel.sh
|
||||||
|
- Windows: Start-ROA2WEB.ps1 → SSH-Tunnels.ps1
|
||||||
|
- Windows Service: Start-Backend-Service.ps1 → SSH-Tunnels.ps1
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
✅ Monitor tunnel health via port checks (background asyncio task)
|
||||||
|
✅ Auto-restart tunnels if they go down (calls platform-specific scripts)
|
||||||
|
✅ Expose status for /health endpoint
|
||||||
|
|
||||||
|
NOT responsible for:
|
||||||
|
❌ Initial tunnel startup (done by wrapper scripts before backend starts)
|
||||||
|
|
||||||
|
Usage in main.py:
|
||||||
|
from backend.shared.ssh_tunnel_manager import ssh_tunnel_manager
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup():
|
||||||
|
await ssh_tunnel_manager.start_monitoring()
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown():
|
||||||
|
await ssh_tunnel_manager.stop_monitoring()
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health():
|
||||||
|
return {
|
||||||
|
"ssh_tunnels": ssh_tunnel_manager.get_status()
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional, Any
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SSHTunnelManager:
|
||||||
|
"""
|
||||||
|
Cross-platform SSH tunnel MONITOR (not starter).
|
||||||
|
|
||||||
|
Timeline:
|
||||||
|
T=0 start.sh / Wrapper starts
|
||||||
|
T=1s ssh-tunnel.sh / SSH-Tunnels.ps1 START
|
||||||
|
T=3s Tunnels active ✅
|
||||||
|
T=5s uvicorn backend starts
|
||||||
|
T=7s Backend startup_event()
|
||||||
|
T=8s ssh_tunnel_manager.start_monitoring()
|
||||||
|
└─ Detects tunnels already active (just monitors, doesn't start)
|
||||||
|
T=38s Monitor check #1 - OK ✅
|
||||||
|
...
|
||||||
|
T=XXs [Tunnel drops]
|
||||||
|
T=XX+30 Monitor detects FAIL (1/2)
|
||||||
|
T=XX+60 Monitor detects FAIL (2/2) → RESTART via script
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Configuration
|
||||||
|
self.check_interval: int = 30 # seconds between health checks
|
||||||
|
self.max_failures_before_restart: int = 2 # restart after N consecutive failures
|
||||||
|
self.restart_cooldown: int = 60 # minimum seconds between restarts
|
||||||
|
|
||||||
|
# State
|
||||||
|
self.tunnel_configs: List[Dict] = []
|
||||||
|
self.tunnel_status: Dict[str, bool] = {}
|
||||||
|
self.consecutive_failures: Dict[str, int] = {}
|
||||||
|
self.last_restart_time: float = 0
|
||||||
|
self.monitor_task: Optional[asyncio.Task] = None
|
||||||
|
self._is_monitoring: bool = False
|
||||||
|
|
||||||
|
# Paths (detected at runtime)
|
||||||
|
self._project_root: Optional[Path] = None
|
||||||
|
self._config_file: Optional[Path] = None
|
||||||
|
|
||||||
|
def _detect_paths(self) -> bool:
|
||||||
|
"""Detect project paths based on current file location."""
|
||||||
|
# This file is at: backend/shared/ssh_tunnel_manager.py
|
||||||
|
# Project root is 2 levels up
|
||||||
|
current_file = Path(__file__)
|
||||||
|
self._project_root = current_file.parent.parent.parent
|
||||||
|
|
||||||
|
# Config file location
|
||||||
|
self._config_file = self._project_root / "backend" / "ssh-tunnels.json"
|
||||||
|
|
||||||
|
return self._config_file.exists()
|
||||||
|
|
||||||
|
def _load_config(self) -> List[Dict]:
|
||||||
|
"""Load tunnel configuration from ssh-tunnels.json."""
|
||||||
|
if not self._config_file or not self._config_file.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self._config_file, 'r') as f:
|
||||||
|
tunnels = json.load(f)
|
||||||
|
|
||||||
|
# Filter to only tunnels with ssh_host (excludes direct connections)
|
||||||
|
return [t for t in tunnels if t.get("ssh_host")]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[SSH-MONITOR] Failed to load config: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def start_monitoring(self) -> bool:
|
||||||
|
"""
|
||||||
|
Start monitoring EXISTING tunnels.
|
||||||
|
|
||||||
|
Does NOT start tunnels - assumes they're already running
|
||||||
|
(started by start.sh / Start-ROA2WEB.ps1 / Start-Backend-Service.ps1).
|
||||||
|
"""
|
||||||
|
if self._is_monitoring:
|
||||||
|
logger.warning("[SSH-MONITOR] Already monitoring")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Detect paths and load config
|
||||||
|
if not self._detect_paths():
|
||||||
|
logger.info("[SSH-MONITOR] No ssh-tunnels.json found, skipping")
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.tunnel_configs = self._load_config()
|
||||||
|
|
||||||
|
if not self.tunnel_configs:
|
||||||
|
logger.info("[SSH-MONITOR] No SSH tunnels configured (or all are direct connections)")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check initial status (tunnels should already be running)
|
||||||
|
logger.info(f"[SSH-MONITOR] Checking {len(self.tunnel_configs)} tunnel(s)...")
|
||||||
|
|
||||||
|
for config in self.tunnel_configs:
|
||||||
|
tunnel_id = config.get("id", "default")
|
||||||
|
port = config.get("local_port", 1521)
|
||||||
|
name = config.get("name", tunnel_id)
|
||||||
|
|
||||||
|
is_active = await self._check_port("127.0.0.1", port)
|
||||||
|
self.tunnel_status[tunnel_id] = is_active
|
||||||
|
self.consecutive_failures[tunnel_id] = 0
|
||||||
|
|
||||||
|
status = "✅ active" if is_active else "❌ NOT active"
|
||||||
|
logger.info(f"[SSH-MONITOR] [{tunnel_id}] {name} - localhost:{port} - {status}")
|
||||||
|
|
||||||
|
# Start background monitor loop
|
||||||
|
self._is_monitoring = True
|
||||||
|
self.monitor_task = asyncio.create_task(self._monitor_loop())
|
||||||
|
logger.info(f"[SSH-MONITOR] ✅ Monitoring started (check every {self.check_interval}s)")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def stop_monitoring(self) -> None:
|
||||||
|
"""Stop the monitoring background task."""
|
||||||
|
if not self._is_monitoring:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._is_monitoring = False
|
||||||
|
|
||||||
|
if self.monitor_task and not self.monitor_task.done():
|
||||||
|
self.monitor_task.cancel()
|
||||||
|
try:
|
||||||
|
await self.monitor_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.info("[SSH-MONITOR] ✅ Monitoring stopped")
|
||||||
|
|
||||||
|
async def _monitor_loop(self) -> None:
|
||||||
|
"""Background loop: check tunnel health every N seconds, restart if needed."""
|
||||||
|
while self._is_monitoring:
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(self.check_interval)
|
||||||
|
|
||||||
|
if not self._is_monitoring:
|
||||||
|
break
|
||||||
|
|
||||||
|
needs_restart = False
|
||||||
|
|
||||||
|
for config in self.tunnel_configs:
|
||||||
|
tunnel_id = config.get("id", "default")
|
||||||
|
port = config.get("local_port", 1521)
|
||||||
|
|
||||||
|
is_healthy = await self._check_port("127.0.0.1", port)
|
||||||
|
self.tunnel_status[tunnel_id] = is_healthy
|
||||||
|
|
||||||
|
if is_healthy:
|
||||||
|
# Reset failure count on success
|
||||||
|
if self.consecutive_failures.get(tunnel_id, 0) > 0:
|
||||||
|
logger.info(f"[SSH-MONITOR] [{tunnel_id}] Recovered ✅")
|
||||||
|
self.consecutive_failures[tunnel_id] = 0
|
||||||
|
else:
|
||||||
|
# Increment failure count
|
||||||
|
self.consecutive_failures[tunnel_id] = \
|
||||||
|
self.consecutive_failures.get(tunnel_id, 0) + 1
|
||||||
|
|
||||||
|
failures = self.consecutive_failures[tunnel_id]
|
||||||
|
logger.warning(
|
||||||
|
f"[SSH-MONITOR] [{tunnel_id}] FAIL "
|
||||||
|
f"({failures}/{self.max_failures_before_restart})"
|
||||||
|
)
|
||||||
|
|
||||||
|
if failures >= self.max_failures_before_restart:
|
||||||
|
needs_restart = True
|
||||||
|
|
||||||
|
# Restart all tunnels if any failed enough times
|
||||||
|
if needs_restart:
|
||||||
|
await self._restart_tunnels()
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[SSH-MONITOR] Monitor loop error: {e}")
|
||||||
|
await asyncio.sleep(5) # Brief pause before retrying
|
||||||
|
|
||||||
|
async def _check_port(self, host: str, port: int, timeout: float = 3.0) -> bool:
|
||||||
|
"""Check if a port is accessible (tunnel is working)."""
|
||||||
|
try:
|
||||||
|
# Use asyncio.open_connection for non-blocking port check
|
||||||
|
reader, writer = await asyncio.wait_for(
|
||||||
|
asyncio.open_connection(host, port),
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
writer.close()
|
||||||
|
await writer.wait_closed()
|
||||||
|
return True
|
||||||
|
except (asyncio.TimeoutError, ConnectionRefusedError, OSError):
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"[SSH-MONITOR] Port check error {host}:{port}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _restart_tunnels(self) -> bool:
|
||||||
|
"""Restart tunnels via platform-specific script."""
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Check cooldown
|
||||||
|
now = time.time()
|
||||||
|
if now - self.last_restart_time < self.restart_cooldown:
|
||||||
|
remaining = int(self.restart_cooldown - (now - self.last_restart_time))
|
||||||
|
logger.warning(f"[SSH-MONITOR] Restart cooldown active ({remaining}s remaining)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.last_restart_time = now
|
||||||
|
logger.warning("[SSH-MONITOR] 🔄 Restarting tunnels...")
|
||||||
|
|
||||||
|
# Build platform-specific command
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
# On Windows, scripts are deployed to scripts/ folder
|
||||||
|
script_path = self._project_root / "scripts" / "ssh-tunnel.ps1"
|
||||||
|
# Fallback to development path if not found
|
||||||
|
if not script_path.exists():
|
||||||
|
script_path = self._project_root / "deployment" / "windows" / "scripts" / "ssh-tunnel.ps1"
|
||||||
|
if not script_path.exists():
|
||||||
|
logger.error(f"[SSH-MONITOR] Script not found in scripts/ or deployment/windows/scripts/")
|
||||||
|
return False
|
||||||
|
cmd = [
|
||||||
|
"powershell.exe",
|
||||||
|
"-ExecutionPolicy", "Bypass",
|
||||||
|
"-File", str(script_path),
|
||||||
|
"restart"
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
script_path = self._project_root / "ssh-tunnel.sh"
|
||||||
|
if not script_path.exists():
|
||||||
|
logger.error(f"[SSH-MONITOR] Script not found: {script_path}")
|
||||||
|
return False
|
||||||
|
cmd = [str(script_path), "restart"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Run restart command in subprocess
|
||||||
|
result = await asyncio.get_event_loop().run_in_executor(
|
||||||
|
None,
|
||||||
|
lambda: subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=60,
|
||||||
|
cwd=str(self._project_root)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.info("[SSH-MONITOR] ✅ Tunnels restarted successfully")
|
||||||
|
# Reset failure counts
|
||||||
|
for tunnel_id in self.consecutive_failures:
|
||||||
|
self.consecutive_failures[tunnel_id] = 0
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"[SSH-MONITOR] Restart failed (code {result.returncode})")
|
||||||
|
if result.stderr:
|
||||||
|
logger.error(f"[SSH-MONITOR] stderr: {result.stderr[:500]}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.error("[SSH-MONITOR] Restart command timed out")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[SSH-MONITOR] Restart error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_status(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get current tunnel status for /health endpoint.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"status": "connected" | "degraded" | "disconnected" | "not_configured",
|
||||||
|
"tunnels": {
|
||||||
|
"tunnel_id": true/false,
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"monitoring": true/false
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if not self.tunnel_configs:
|
||||||
|
return {
|
||||||
|
"status": "not_configured",
|
||||||
|
"tunnels": {},
|
||||||
|
"monitoring": False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine overall status
|
||||||
|
all_connected = all(self.tunnel_status.values()) if self.tunnel_status else False
|
||||||
|
any_connected = any(self.tunnel_status.values()) if self.tunnel_status else False
|
||||||
|
|
||||||
|
if all_connected:
|
||||||
|
status = "connected"
|
||||||
|
elif any_connected:
|
||||||
|
status = "degraded"
|
||||||
|
else:
|
||||||
|
status = "disconnected"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": status,
|
||||||
|
"tunnels": dict(self.tunnel_status),
|
||||||
|
"monitoring": self._is_monitoring
|
||||||
|
}
|
||||||
|
|
||||||
|
def is_healthy(self) -> bool:
|
||||||
|
"""Quick check if all tunnels are healthy."""
|
||||||
|
if not self.tunnel_configs:
|
||||||
|
return True # No tunnels configured = healthy (direct connection)
|
||||||
|
return all(self.tunnel_status.values()) if self.tunnel_status else False
|
||||||
|
|
||||||
|
|
||||||
|
# Global singleton instance
|
||||||
|
ssh_tunnel_manager = SSHTunnelManager()
|
||||||
@@ -205,6 +205,8 @@ create_package() {
|
|||||||
"ROA2WEB-Console.ps1"
|
"ROA2WEB-Console.ps1"
|
||||||
"Install-ROA2WEB.ps1"
|
"Install-ROA2WEB.ps1"
|
||||||
"Check-And-Deploy.ps1"
|
"Check-And-Deploy.ps1"
|
||||||
|
"ssh-tunnel.ps1"
|
||||||
|
"start-backend-service.ps1"
|
||||||
)
|
)
|
||||||
|
|
||||||
for script in "${SCRIPTS[@]}"; do
|
for script in "${SCRIPTS[@]}"; do
|
||||||
|
|||||||
@@ -383,21 +383,24 @@ function New-WindowsService {
|
|||||||
Write-Success "Existing service removed"
|
Write-Success "Existing service removed"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get Python path from venv
|
# Verify venv exists (wrapper script needs it)
|
||||||
$venvPython = Join-Path $Config.VenvPath "Scripts\python.exe"
|
$venvPython = Join-Path $Config.VenvPath "Scripts\python.exe"
|
||||||
|
|
||||||
if (-not (Test-Path $venvPython)) {
|
if (-not (Test-Path $venvPython)) {
|
||||||
throw "Virtual environment Python not found at $venvPython. Run Install-PythonDependencies first."
|
throw "Virtual environment Python not found at $venvPython. Run Install-PythonDependencies first."
|
||||||
}
|
}
|
||||||
|
|
||||||
$uvicornModule = "uvicorn"
|
# NSSM service creation using wrapper script
|
||||||
$appModule = "main:app"
|
# The wrapper script (start-backend-service.ps1) handles:
|
||||||
|
# 1. Starting SSH tunnels before backend
|
||||||
# NSSM service creation
|
# 2. Waiting for tunnel ports to be accessible
|
||||||
|
# 3. Starting uvicorn
|
||||||
try {
|
try {
|
||||||
# Install service using venv Python
|
$wrapperScript = Join-Path $PSScriptRoot "start-backend-service.ps1"
|
||||||
# NOTE: Using --workers 1 because Telegram bot requires single instance (polling conflict)
|
|
||||||
& nssm install $Config.ServiceName $venvPython "-m" $uvicornModule $appModule "--host" "127.0.0.1" "--port" $Config.ServicePort.ToString() "--workers" "1"
|
# Install service using PowerShell wrapper
|
||||||
|
# NOTE: Using wrapper to ensure SSH tunnels start before uvicorn
|
||||||
|
& nssm install $Config.ServiceName "powershell.exe" "-ExecutionPolicy" "Bypass" "-File" "`"$wrapperScript`""
|
||||||
|
|
||||||
# Set service configuration
|
# Set service configuration
|
||||||
& nssm set $Config.ServiceName DisplayName $Config.ServiceDisplayName
|
& nssm set $Config.ServiceName DisplayName $Config.ServiceDisplayName
|
||||||
@@ -423,7 +426,8 @@ function New-WindowsService {
|
|||||||
& nssm set $Config.ServiceName AppExit Default Restart
|
& nssm set $Config.ServiceName AppExit Default Restart
|
||||||
& nssm set $Config.ServiceName AppRestartDelay 5000
|
& nssm set $Config.ServiceName AppRestartDelay 5000
|
||||||
|
|
||||||
Write-Success "Windows Service created successfully"
|
Write-Success "Windows Service created successfully (using wrapper script)"
|
||||||
|
Write-Success " Wrapper: $wrapperScript"
|
||||||
} catch {
|
} catch {
|
||||||
throw "Failed to create Windows Service: $_"
|
throw "Failed to create Windows Service: $_"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,7 +215,15 @@ function Initialize-Venv {
|
|||||||
function Update-ServiceToUseVenv {
|
function Update-ServiceToUseVenv {
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.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"
|
$venvPython = Join-Path $Config.VenvPath "Scripts\python.exe"
|
||||||
|
|
||||||
@@ -231,23 +239,59 @@ function Update-ServiceToUseVenv {
|
|||||||
return $false
|
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 {
|
try {
|
||||||
# Get current application path
|
# Get current application
|
||||||
$currentApp = & nssm get $Config.ServiceName Application 2>&1
|
$currentApp = & nssm get $Config.ServiceName Application 2>&1
|
||||||
|
|
||||||
|
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) {
|
if ($currentApp -eq $venvPython) {
|
||||||
Write-Success "Service already configured to use venv Python"
|
Write-Success "Service already configured to use venv Python"
|
||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Step "Updating service to use venv Python..."
|
Write-Step "Updating service to use venv Python (no wrapper available)..."
|
||||||
|
|
||||||
# Stop service first
|
# Stop service first
|
||||||
Stop-ROAService | Out-Null
|
Stop-ROAService | Out-Null
|
||||||
|
|
||||||
# Update service application
|
# Update service application
|
||||||
& nssm set $Config.ServiceName Application $venvPython
|
& 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-Success "Service updated to use: $venvPython"
|
||||||
|
Write-Warning "Wrapper script not found - SSH tunnels must be started manually"
|
||||||
|
}
|
||||||
|
|
||||||
return $true
|
return $true
|
||||||
} catch {
|
} catch {
|
||||||
@@ -777,6 +821,85 @@ function New-Backup {
|
|||||||
# DEPLOYMENT FUNCTIONS
|
# 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 {
|
function Deploy-Backend {
|
||||||
param([string]$SourcePath)
|
param([string]$SourcePath)
|
||||||
|
|
||||||
@@ -1019,6 +1142,9 @@ function Deploy-All {
|
|||||||
Write-Warning "Backup failed, but continuing with deployment"
|
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)
|
# Deploy backend (includes service restart)
|
||||||
$backendOk = Deploy-Backend -SourcePath $SourcePath
|
$backendOk = Deploy-Backend -SourcePath $SourcePath
|
||||||
|
|
||||||
@@ -1028,6 +1154,12 @@ function Deploy-All {
|
|||||||
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
||||||
Write-Host " DEPLOYMENT SUMMARY" -ForegroundColor Cyan
|
Write-Host " DEPLOYMENT SUMMARY" -ForegroundColor Cyan
|
||||||
Write-Host ("=" * 70) -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
|
Write-Host " Backend: " -NoNewline
|
||||||
if ($backendOk) {
|
if ($backendOk) {
|
||||||
Write-Host "[OK] Success" -ForegroundColor Green
|
Write-Host "[OK] Success" -ForegroundColor Green
|
||||||
@@ -1042,7 +1174,7 @@ function Deploy-All {
|
|||||||
}
|
}
|
||||||
Write-Host ("=" * 70) -ForegroundColor Cyan
|
Write-Host ("=" * 70) -ForegroundColor Cyan
|
||||||
|
|
||||||
return ($backendOk -and $frontendOk)
|
return ($scriptsOk -and $backendOk -and $frontendOk)
|
||||||
}
|
}
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
509
deployment/windows/scripts/ssh-tunnel.ps1
Normal file
509
deployment/windows/scripts/ssh-tunnel.ps1
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
ROA2WEB SSH Tunnel Manager for Windows (equivalent to ssh-tunnel.sh)
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Manages SSH tunnels to Oracle servers on Windows.
|
||||||
|
Reads configuration from backend/ssh-tunnels.json
|
||||||
|
SSH keys should be in backend/secrets/{id}.ssh_key
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- Windows 10/11 with OpenSSH Client (installed by default)
|
||||||
|
- SSH private keys in backend/secrets/
|
||||||
|
|
||||||
|
.PARAMETER Action
|
||||||
|
start - Start all configured SSH tunnels
|
||||||
|
stop - Stop all SSH tunnels
|
||||||
|
status - Show status of all tunnels
|
||||||
|
restart - Restart all tunnels
|
||||||
|
help - Show this help
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\ssh-tunnel.ps1 start
|
||||||
|
Start all configured tunnels
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\ssh-tunnel.ps1 status
|
||||||
|
Check tunnel status
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
Author: ROA2WEB Team
|
||||||
|
Version: 1.0
|
||||||
|
#>
|
||||||
|
|
||||||
|
param(
|
||||||
|
[Parameter(Position=0)]
|
||||||
|
[ValidateSet("start", "stop", "status", "restart", "help", "")]
|
||||||
|
[string]$Action = "help"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Detect paths - script can run from deployment/windows/scripts or project root
|
||||||
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
|
||||||
|
# Try to find backend relative to script location
|
||||||
|
$PossiblePaths = @(
|
||||||
|
(Join-Path $ScriptDir "..\..\..\backend"), # From deployment/windows/scripts
|
||||||
|
(Join-Path $ScriptDir "backend"), # From project root
|
||||||
|
"C:\inetpub\wwwroot\roa2web\backend" # Production path
|
||||||
|
)
|
||||||
|
|
||||||
|
$BackendPath = $null
|
||||||
|
foreach ($path in $PossiblePaths) {
|
||||||
|
$resolved = [System.IO.Path]::GetFullPath($path)
|
||||||
|
if (Test-Path $resolved) {
|
||||||
|
$BackendPath = $resolved
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $BackendPath) {
|
||||||
|
Write-Host "[ERROR] Cannot find backend directory" -ForegroundColor Red
|
||||||
|
Write-Host "Tried: $($PossiblePaths -join ', ')" -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$Config = @{
|
||||||
|
BackendPath = $BackendPath
|
||||||
|
TunnelsFile = Join-Path $BackendPath "ssh-tunnels.json"
|
||||||
|
SecretsPath = Join-Path $BackendPath "secrets"
|
||||||
|
PidDir = Join-Path $env:TEMP "roa_tunnels"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create PID directory
|
||||||
|
if (-not (Test-Path $Config.PidDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $Config.PidDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# HELPER FUNCTIONS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
function Write-ColorLine {
|
||||||
|
param([string]$Text, [string]$Color = "White")
|
||||||
|
Write-Host $Text -ForegroundColor $Color
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PidFile {
|
||||||
|
param([string]$ServerId)
|
||||||
|
return Join-Path $Config.PidDir "tunnel_$ServerId.pid"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-TunnelRunning {
|
||||||
|
param([string]$ServerId)
|
||||||
|
|
||||||
|
$pidFile = Get-PidFile $ServerId
|
||||||
|
if (Test-Path $pidFile) {
|
||||||
|
$tunnelPid = Get-Content $pidFile -ErrorAction SilentlyContinue
|
||||||
|
if ($tunnelPid) {
|
||||||
|
$process = Get-Process -Id $tunnelPid -ErrorAction SilentlyContinue
|
||||||
|
if ($process -and $process.ProcessName -eq "ssh") {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Clean up stale PID file
|
||||||
|
Remove-Item $pidFile -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Read-TunnelConfig {
|
||||||
|
if (-not (Test-Path $Config.TunnelsFile)) {
|
||||||
|
Write-ColorLine "[WARNING] ssh-tunnels.json not found at: $($Config.TunnelsFile)" "Yellow"
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$json = Get-Content $Config.TunnelsFile -Raw | ConvertFrom-Json
|
||||||
|
return $json
|
||||||
|
} catch {
|
||||||
|
Write-ColorLine "[ERROR] Failed to parse ssh-tunnels.json: $_" "Red"
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# TUNNEL MANAGEMENT
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
function Start-SingleTunnel {
|
||||||
|
param([PSCustomObject]$Tunnel)
|
||||||
|
|
||||||
|
$id = $Tunnel.id
|
||||||
|
$name = $Tunnel.name
|
||||||
|
$localPort = $Tunnel.local_port
|
||||||
|
$sshHost = $Tunnel.ssh_host
|
||||||
|
$sshPort = if ($Tunnel.ssh_port) { $Tunnel.ssh_port } else { 22 }
|
||||||
|
$sshUser = if ($Tunnel.ssh_user) { $Tunnel.ssh_user } else { "root" }
|
||||||
|
$oracleHost = if ($Tunnel.oracle_host) { $Tunnel.oracle_host } else { "127.0.0.1" }
|
||||||
|
$oraclePort = if ($Tunnel.oracle_port) { $Tunnel.oracle_port } else { 1521 }
|
||||||
|
|
||||||
|
Write-ColorLine "[$id] $name" "Cyan"
|
||||||
|
Write-Host " Tunnel: localhost:$localPort -> ${oracleHost}:$oraclePort"
|
||||||
|
Write-Host " Via: ${sshUser}@${sshHost}:$sshPort"
|
||||||
|
|
||||||
|
# Skip if no SSH host configured
|
||||||
|
if (-not $sshHost) {
|
||||||
|
Write-ColorLine " [SKIP] No ssh_host configured (direct connection)" "Yellow"
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if already running
|
||||||
|
if (Test-TunnelRunning $id) {
|
||||||
|
$pidFile = Get-PidFile $id
|
||||||
|
$tunnelPid = Get-Content $pidFile
|
||||||
|
Write-ColorLine " [RUNNING] Already running (PID: $tunnelPid)" "Yellow"
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find SSH key
|
||||||
|
$sshKeyPath = $null
|
||||||
|
$possibleKeys = @(
|
||||||
|
(Join-Path $Config.SecretsPath "$id.ssh_key"),
|
||||||
|
(Join-Path $Config.SecretsPath "${id}_rsa"),
|
||||||
|
(Join-Path $Config.SecretsPath "id_rsa")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Also check if key path is specified in config
|
||||||
|
if ($Tunnel.ssh_key) {
|
||||||
|
$configKey = $Tunnel.ssh_key
|
||||||
|
if (-not [System.IO.Path]::IsPathRooted($configKey)) {
|
||||||
|
$configKey = Join-Path $Config.BackendPath $configKey
|
||||||
|
}
|
||||||
|
$possibleKeys = @($configKey) + $possibleKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($keyPath in $possibleKeys) {
|
||||||
|
if (Test-Path $keyPath) {
|
||||||
|
$sshKeyPath = $keyPath
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for password file
|
||||||
|
$sshPassPath = Join-Path $Config.SecretsPath "$id.ssh_pass"
|
||||||
|
$hasPassword = Test-Path $sshPassPath
|
||||||
|
$sshPassword = $null
|
||||||
|
if ($hasPassword) {
|
||||||
|
$sshPassword = (Get-Content $sshPassPath -Raw).Trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $sshKeyPath -and -not $hasPassword) {
|
||||||
|
Write-ColorLine " [ERROR] No SSH key or password found" "Red"
|
||||||
|
Write-ColorLine " Expected: $($Config.SecretsPath)\$id.ssh_key" "Yellow"
|
||||||
|
Write-ColorLine " or: $($Config.SecretsPath)\$id.ssh_pass" "Yellow"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build SSH command based on auth method
|
||||||
|
# Note: Windows ssh.exe doesn't support -f (background), so we use Start-Process
|
||||||
|
$sshArgs = @(
|
||||||
|
"-N", # No remote command
|
||||||
|
"-L", "${localPort}:${oracleHost}:${oraclePort}", # Port forwarding
|
||||||
|
"-p", $sshPort, # SSH port
|
||||||
|
"-o", "StrictHostKeyChecking=no", # Skip host key check
|
||||||
|
"-o", "ServerAliveInterval=60", # Keep-alive
|
||||||
|
"-o", "ServerAliveCountMax=3", # Retry count
|
||||||
|
"-o", "ExitOnForwardFailure=yes" # Fail if port forward fails
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($sshKeyPath) {
|
||||||
|
$sshArgs += @("-i", "`"$sshKeyPath`"")
|
||||||
|
Write-ColorLine " Using SSH key authentication" "Gray"
|
||||||
|
}
|
||||||
|
|
||||||
|
$sshArgs += @("${sshUser}@${sshHost}")
|
||||||
|
|
||||||
|
try {
|
||||||
|
$process = $null
|
||||||
|
|
||||||
|
if ($hasPassword -and -not $sshKeyPath) {
|
||||||
|
# Password authentication - use plink if available, otherwise show instructions
|
||||||
|
$plinkPath = Get-Command plink.exe -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if ($plinkPath) {
|
||||||
|
# Use plink for password auth
|
||||||
|
Write-ColorLine " Using password authentication (plink)" "Gray"
|
||||||
|
$plinkArgs = @(
|
||||||
|
"-ssh",
|
||||||
|
"-batch", # Non-interactive mode (skip "Press Return" prompt)
|
||||||
|
"-N",
|
||||||
|
"-L", "${localPort}:${oracleHost}:${oraclePort}",
|
||||||
|
"-P", $sshPort,
|
||||||
|
"-pw", $sshPassword
|
||||||
|
)
|
||||||
|
# Add hostkey if specified (required for non-interactive/batch mode)
|
||||||
|
if ($Tunnel.ssh_hostkey) {
|
||||||
|
$plinkArgs += @("-hostkey", $Tunnel.ssh_hostkey)
|
||||||
|
}
|
||||||
|
$plinkArgs += "${sshUser}@${sshHost}"
|
||||||
|
|
||||||
|
$process = Start-Process -FilePath "plink.exe" `
|
||||||
|
-ArgumentList $plinkArgs `
|
||||||
|
-WindowStyle Hidden `
|
||||||
|
-PassThru
|
||||||
|
} else {
|
||||||
|
# Try with sshpass if available (e.g., via Git Bash or WSL)
|
||||||
|
$sshpassPath = Get-Command sshpass -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if ($sshpassPath) {
|
||||||
|
Write-ColorLine " Using password authentication (sshpass)" "Gray"
|
||||||
|
$process = Start-Process -FilePath "sshpass" `
|
||||||
|
-ArgumentList @("-p", $sshPassword, "ssh") + $sshArgs `
|
||||||
|
-WindowStyle Hidden `
|
||||||
|
-PassThru
|
||||||
|
} else {
|
||||||
|
Write-ColorLine " [ERROR] Password auth requires plink.exe (PuTTY) or sshpass" "Red"
|
||||||
|
Write-ColorLine " Install PuTTY: choco install putty" "Yellow"
|
||||||
|
Write-ColorLine " Or create SSH key: ssh-keygen -t rsa -f secrets\$id.ssh_key" "Yellow"
|
||||||
|
Write-ColorLine " Then copy public key to server: ssh-copy-id -p $sshPort ${sshUser}@${sshHost}" "Yellow"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
# SSH key authentication
|
||||||
|
$process = Start-Process -FilePath "ssh.exe" `
|
||||||
|
-ArgumentList $sshArgs `
|
||||||
|
-WindowStyle Hidden `
|
||||||
|
-PassThru
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait a moment for connection
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
|
||||||
|
# Check if process is still running
|
||||||
|
$process.Refresh()
|
||||||
|
if ($process.HasExited) {
|
||||||
|
Write-ColorLine " [ERROR] SSH tunnel exited immediately (code: $($process.ExitCode))" "Red"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save PID
|
||||||
|
$pidFile = Get-PidFile $id
|
||||||
|
$process.Id | Out-File -FilePath $pidFile -NoNewline
|
||||||
|
|
||||||
|
Write-ColorLine " [OK] Started (PID: $($process.Id))" "Green"
|
||||||
|
|
||||||
|
# Test port connectivity
|
||||||
|
if (Test-PortOpen $localPort) {
|
||||||
|
Write-ColorLine " [OK] Port $localPort accessible" "Green"
|
||||||
|
} else {
|
||||||
|
Write-ColorLine " [WARN] Port $localPort not responding yet" "Yellow"
|
||||||
|
}
|
||||||
|
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
Write-ColorLine " [ERROR] Failed to start tunnel: $_" "Red"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Stop-SingleTunnel {
|
||||||
|
param([PSCustomObject]$Tunnel)
|
||||||
|
|
||||||
|
$id = $Tunnel.id
|
||||||
|
$name = $Tunnel.name
|
||||||
|
|
||||||
|
Write-ColorLine "[$id] $name" "Cyan"
|
||||||
|
|
||||||
|
$pidFile = Get-PidFile $id
|
||||||
|
if (Test-Path $pidFile) {
|
||||||
|
$tunnelPid = Get-Content $pidFile -ErrorAction SilentlyContinue
|
||||||
|
if ($tunnelPid) {
|
||||||
|
$process = Get-Process -Id $tunnelPid -ErrorAction SilentlyContinue
|
||||||
|
if ($process) {
|
||||||
|
Stop-Process -Id $tunnelPid -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-ColorLine " [OK] Stopped (was PID: $tunnelPid)" "Green"
|
||||||
|
} else {
|
||||||
|
Write-ColorLine " [WARN] Was not running" "Yellow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Remove-Item $pidFile -Force -ErrorAction SilentlyContinue
|
||||||
|
} else {
|
||||||
|
Write-ColorLine " [WARN] Was not running" "Yellow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SingleTunnelStatus {
|
||||||
|
param([PSCustomObject]$Tunnel)
|
||||||
|
|
||||||
|
$id = $Tunnel.id
|
||||||
|
$name = $Tunnel.name
|
||||||
|
$localPort = $Tunnel.local_port
|
||||||
|
$sshHost = $Tunnel.ssh_host
|
||||||
|
|
||||||
|
# Skip tunnels without SSH host (direct connection)
|
||||||
|
if (-not $sshHost) {
|
||||||
|
Write-Host " " -NoNewline
|
||||||
|
Write-ColorLine "[$id] $name" "Cyan"
|
||||||
|
Write-ColorLine " Direct connection (no tunnel needed)" "Gray"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-TunnelRunning $id) {
|
||||||
|
$pidFile = Get-PidFile $id
|
||||||
|
$tunnelPid = Get-Content $pidFile
|
||||||
|
|
||||||
|
Write-Host " " -NoNewline
|
||||||
|
Write-Host "[OK]" -ForegroundColor Green -NoNewline
|
||||||
|
Write-ColorLine " [$id] $name" "Cyan"
|
||||||
|
Write-Host " localhost:$localPort (PID: $tunnelPid)"
|
||||||
|
|
||||||
|
if (Test-PortOpen $localPort) {
|
||||||
|
Write-ColorLine " Port accessible" "Green"
|
||||||
|
} else {
|
||||||
|
Write-ColorLine " Port not responding" "Yellow"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " " -NoNewline
|
||||||
|
Write-Host "[--]" -ForegroundColor Red -NoNewline
|
||||||
|
Write-ColorLine " [$id] $name" "Cyan"
|
||||||
|
Write-Host " localhost:$localPort (stopped)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MAIN COMMANDS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
function Show-Header {
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorLine "============================================" "Blue"
|
||||||
|
Write-ColorLine " ROA2WEB SSH Tunnel Manager (Windows)" "Blue"
|
||||||
|
Write-ColorLine "============================================" "Blue"
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Start {
|
||||||
|
Show-Header
|
||||||
|
|
||||||
|
$tunnels = Read-TunnelConfig
|
||||||
|
if ($tunnels.Count -eq 0) {
|
||||||
|
Write-ColorLine "[WARNING] No tunnels configured" "Yellow"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-ColorLine "Starting $($tunnels.Count) tunnel(s)..." "Blue"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
$failed = 0
|
||||||
|
foreach ($tunnel in $tunnels) {
|
||||||
|
if (-not (Start-SingleTunnel $tunnel)) {
|
||||||
|
$failed++
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($failed -gt 0) {
|
||||||
|
Write-ColorLine "[WARNING] $failed tunnel(s) failed to start" "Yellow"
|
||||||
|
} else {
|
||||||
|
Write-ColorLine "[OK] All tunnels started successfully" "Green"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Stop {
|
||||||
|
Show-Header
|
||||||
|
|
||||||
|
$tunnels = Read-TunnelConfig
|
||||||
|
|
||||||
|
Write-ColorLine "Stopping tunnels..." "Blue"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
foreach ($tunnel in $tunnels) {
|
||||||
|
Stop-SingleTunnel $tunnel
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorLine "[OK] All tunnels stopped" "Green"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Status {
|
||||||
|
Show-Header
|
||||||
|
|
||||||
|
$tunnels = Read-TunnelConfig
|
||||||
|
|
||||||
|
Write-ColorLine "Tunnel Status:" "Blue"
|
||||||
|
Write-Host "--------------------------------------------"
|
||||||
|
|
||||||
|
foreach ($tunnel in $tunnels) {
|
||||||
|
Get-SingleTunnelStatus $tunnel
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Restart {
|
||||||
|
Invoke-Stop
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
Invoke-Start
|
||||||
|
}
|
||||||
|
|
||||||
|
function Show-Help {
|
||||||
|
Show-Header
|
||||||
|
|
||||||
|
Write-Host "Usage: .\ssh-tunnel.ps1 <action>"
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorLine "Actions:" "Blue"
|
||||||
|
Write-Host " start - Start all configured SSH tunnels"
|
||||||
|
Write-Host " stop - Stop all SSH tunnels"
|
||||||
|
Write-Host " status - Show status of all tunnels"
|
||||||
|
Write-Host " restart - Restart all tunnels"
|
||||||
|
Write-Host " help - Show this help"
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorLine "Configuration:" "Blue"
|
||||||
|
Write-Host " Tunnels file: $($Config.TunnelsFile)"
|
||||||
|
Write-Host " Secrets dir: $($Config.SecretsPath)"
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorLine "SSH Key Setup:" "Blue"
|
||||||
|
Write-Host " 1. Copy your SSH private key to: backend\secrets\{server_id}.ssh_key"
|
||||||
|
Write-Host " 2. Ensure the key has correct permissions (readable only by you)"
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorLine "Example ssh-tunnels.json:" "Blue"
|
||||||
|
Write-Host ' [{"id": "vending", "name": "Vending Master", "local_port": 1521,'
|
||||||
|
Write-Host ' "ssh_host": "79.119.86.134", "ssh_port": 22122, "ssh_user": "romfast",'
|
||||||
|
Write-Host ' "oracle_host": "127.0.0.1", "oracle_port": 1521}]'
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MAIN
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Check OpenSSH availability
|
||||||
|
$sshPath = Get-Command ssh.exe -ErrorAction SilentlyContinue
|
||||||
|
if (-not $sshPath) {
|
||||||
|
Write-ColorLine "[ERROR] OpenSSH not found!" "Red"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "OpenSSH Client is required. Install it via:"
|
||||||
|
Write-Host " Settings -> Apps -> Optional Features -> Add a feature -> OpenSSH Client"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Or via PowerShell (Admin):"
|
||||||
|
Write-Host " Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($Action) {
|
||||||
|
"start" { Invoke-Start }
|
||||||
|
"stop" { Invoke-Stop }
|
||||||
|
"status" { Invoke-Status }
|
||||||
|
"restart" { Invoke-Restart }
|
||||||
|
"help" { Show-Help }
|
||||||
|
default { Show-Help }
|
||||||
|
}
|
||||||
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
|
||||||
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