fix telegram

This commit is contained in:
Claude Agent
2026-02-23 15:12:33 +00:00
parent 6c78fec8a7
commit 8bc567a9c5
426 changed files with 112478 additions and 1 deletions

View File

@@ -0,0 +1,547 @@
<#
.SYNOPSIS
ROA2WEB - Auto-Deploy Monitor (Server-Side) - Ultrathin Monolith
.DESCRIPTION
Server-side script that monitors C:\Temp\ for new deployment packages
and automatically deploys them using ROA2WEB-Console.ps1.
Designed for ULTRATHIN MONOLITH architecture:
- Single unified backend service (ROA2WEB-Backend)
- All modules deployed together (Reports, Data Entry, Telegram)
- Automatic detection and deployment
Can run:
- Via Scheduled Task (automated, silent)
- Interactive mode (manual execution with menu)
- Non-interactive mode (command-line automation)
.PARAMETER Interactive
Run in interactive mode with menu
.PARAMETER WatchPath
Path to monitor for deployment packages (default: C:\Temp)
.PARAMETER StateFile
Path to state file tracking last deployment (default: C:\Temp\ROA2WEB-Scripts\last-deploy.json)
.PARAMETER ConsoleScriptPath
Path to ROA2WEB-Console.ps1 script
.PARAMETER LogPath
Path to log file directory (default: C:\Temp\ROA2WEB-Scripts\Logs)
.PARAMETER CheckOnly
Check for updates without deploying
.EXAMPLE
.\Check-And-Deploy.ps1
Check for new packages and deploy automatically (silent)
.EXAMPLE
.\Check-And-Deploy.ps1 -Interactive
Show interactive menu with deployment options
.EXAMPLE
.\Check-And-Deploy.ps1 -CheckOnly
Check for new packages without deploying
.NOTES
Author: ROA2WEB Team
Version: 1.0 (Auto-Deploy Monitor)
Requires: Administrator privileges, PowerShell 5.1+
Designed to run via Scheduled Task for automated deployments.
#>
[CmdletBinding()]
param(
[switch]$Interactive,
[string]$WatchPath = "C:\Temp",
[string]$StateFile = "C:\Temp\ROA2WEB-Scripts\last-deploy.json",
[string]$ConsoleScriptPath = "",
[string]$LogPath = "C:\Temp\ROA2WEB-Scripts\Logs",
[switch]$CheckOnly
)
$ErrorActionPreference = "Stop"
# =============================================================================
# CONFIGURATION
# =============================================================================
$script:Config = @{
WatchPath = $WatchPath
StateFile = $StateFile
LogPath = $LogPath
ConsoleScriptPath = if ($ConsoleScriptPath) {
$ConsoleScriptPath
} else {
# Auto-detect ROA2WEB-Console.ps1
$possiblePaths = @(
"C:\inetpub\wwwroot\roa2web\scripts\ROA2WEB-Console.ps1",
(Join-Path $PSScriptRoot "ROA2WEB-Console.ps1")
)
$found = $possiblePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
if ($found) { $found } else { "" }
}
}
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
function Write-Log {
param(
[string]$Message,
[ValidateSet("INFO", "SUCCESS", "WARNING", "ERROR")]
[string]$Level = "INFO"
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logMessage = "[$timestamp] [$Level] $Message"
# Ensure log directory exists
if (-not (Test-Path $script:Config.LogPath)) {
New-Item -ItemType Directory -Path $script:Config.LogPath -Force | Out-Null
}
$logFile = Join-Path $script:Config.LogPath "check-and-deploy.log"
Add-Content -Path $logFile -Value $logMessage
# Also write to console if interactive
if ($Interactive) {
$color = switch ($Level) {
"SUCCESS" { "Green" }
"WARNING" { "Yellow" }
"ERROR" { "Red" }
default { "White" }
}
Write-Host $logMessage -ForegroundColor $color
}
}
function Write-Step {
param([string]$Message)
Write-Host "`n[*] $Message" -ForegroundColor Cyan
Write-Log -Message $Message -Level "INFO"
}
function Write-Success {
param([string]$Message)
Write-Host " [OK] $Message" -ForegroundColor Green
Write-Log -Message $Message -Level "SUCCESS"
}
function Write-Error {
param([string]$Message)
Write-Host " [ERROR] $Message" -ForegroundColor Red
Write-Log -Message $Message -Level "ERROR"
}
function Write-Warning {
param([string]$Message)
Write-Host " [WARN] $Message" -ForegroundColor Yellow
Write-Log -Message $Message -Level "WARNING"
}
function Wait-ForKeyPress {
Write-Host "`nPress any key to continue..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}
# =============================================================================
# STATE MANAGEMENT
# =============================================================================
function Get-DeploymentState {
if (Test-Path $script:Config.StateFile) {
try {
return Get-Content -Path $script:Config.StateFile -Raw | ConvertFrom-Json
} catch {
Write-Warning "Failed to load state file, starting fresh"
return $null
}
}
return $null
}
function Save-DeploymentState {
param(
[Parameter(Mandatory)]
[hashtable]$DeploymentInfo
)
try {
# Load existing state
$state = Get-DeploymentState
$newEntry = @{
packageName = $DeploymentInfo.PackageName
component = $DeploymentInfo.Component
timestamp = (Get-Date).ToUniversalTime().ToString("o")
status = $DeploymentInfo.Status
services = $DeploymentInfo.Services
}
if ($state) {
# Add current deployment to history
if (-not $state.history) {
$state.history = @()
}
if ($state.lastDeployment) {
$state.history = @($state.lastDeployment) + $state.history
}
# Keep only last 10 entries in history
if ($state.history.Count -gt 10) {
$state.history = $state.history | Select-Object -First 10
}
$state.lastDeployment = $newEntry
} else {
$state = @{
lastDeployment = $newEntry
history = @()
}
}
# Ensure directory exists
$stateDir = Split-Path $script:Config.StateFile -Parent
if (-not (Test-Path $stateDir)) {
New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
}
# Save state
$state | ConvertTo-Json -Depth 10 | Set-Content -Path $script:Config.StateFile
Write-Success "Deployment state saved"
} catch {
Write-Error "Failed to save deployment state: $_"
}
}
# =============================================================================
# PACKAGE DETECTION
# =============================================================================
function Get-LatestDeploymentPackage {
Write-Step "Scanning for deployment packages..."
if (-not (Test-Path $script:Config.WatchPath)) {
Write-Warning "Watch path does not exist: $($script:Config.WatchPath)"
return $null
}
# Find all deploy-* folders
$packages = Get-ChildItem -Path $script:Config.WatchPath -Directory |
Where-Object { $_.Name -match "^deploy-\d{8}-\d{6}$" } |
Sort-Object LastWriteTime -Descending
if ($packages.Count -eq 0) {
Write-Warning "No deployment packages found"
return $null
}
$latest = $packages | Select-Object -First 1
Write-Success "Found $($packages.Count) package(s), latest: $($latest.Name)"
return $latest
}
function Test-IsNewPackage {
param([Parameter(Mandatory)]$Package)
$state = Get-DeploymentState
if (-not $state -or -not $state.lastDeployment) {
Write-Success "No previous deployment found - this is a new package"
return $true
}
$lastDeployed = $state.lastDeployment.packageName
if ($Package.Name -ne $lastDeployed) {
Write-Success "New package detected (last deployed: $lastDeployed)"
return $true
}
Write-Log -Message "Package already deployed: $($Package.Name)" -Level "INFO"
return $false
}
# =============================================================================
# DEPLOYMENT EXECUTION
# =============================================================================
function Invoke-Deployment {
param([Parameter(Mandatory)]$Package)
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
Write-Host " STARTING DEPLOYMENT" -ForegroundColor Cyan
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host "`nPackage: $($Package.Name)" -ForegroundColor Yellow
Write-Host "Path: $($Package.FullName)" -ForegroundColor Gray
# Find ROA2WEB-Console.ps1 in package
$consoleScript = Join-Path $Package.FullName "scripts\ROA2WEB-Console.ps1"
if (-not (Test-Path $consoleScript)) {
Write-Error "ROA2WEB-Console.ps1 not found in package: $consoleScript"
return $false
}
try {
Write-Step "Executing deployment via ROA2WEB-Console.ps1..."
# Execute deployment (deploy all components)
Push-Location (Split-Path $consoleScript -Parent)
& $consoleScript -NonInteractive -Action DeployAll -PackagePath $Package.FullName
# Capture exit code IMMEDIATELY (before any other command that might reset it)
$exitCode = $LASTEXITCODE
# Run OCR dependency check with auto-install
Write-Log -Message "Checking and installing OCR dependencies..." -Level "INFO"
& $consoleScript -NonInteractive -Action InstallOCR 2>&1 | ForEach-Object { Write-Log -Message $_ -Level "INFO" }
Pop-Location
# Check if exit code indicates success (0 = success)
$deploySuccess = ($exitCode -eq 0)
if ($deploySuccess) {
Write-Success "Deployment completed successfully (exit code: $exitCode)"
# Save deployment state
Save-DeploymentState -DeploymentInfo @{
PackageName = $Package.Name
Component = "All"
Status = "Success"
Services = @{
backend = "Running (Ultrathin Monolith - All modules)"
}
}
Write-Host "`n" + ("=" * 70) -ForegroundColor Green
Write-Host " DEPLOYMENT SUCCESSFUL" -ForegroundColor Green
Write-Host ("=" * 70) -ForegroundColor Green
return $true
} else {
Write-Error "Deployment failed (exit code: $exitCode)"
# Save failure state
Save-DeploymentState -DeploymentInfo @{
PackageName = $Package.Name
Component = "All"
Status = "Failed"
Services = @{}
}
Write-Host "`n" + ("=" * 70) -ForegroundColor Red
Write-Host " DEPLOYMENT FAILED" -ForegroundColor Red
Write-Host ("=" * 70) -ForegroundColor Red
return $false
}
} catch {
Write-Error "Deployment exception: $_"
Write-Log -Message $_.ScriptStackTrace -Level "ERROR"
# Save failure state
Save-DeploymentState -DeploymentInfo @{
PackageName = $Package.Name
Component = "All"
Status = "Failed"
Services = @{}
}
return $false
}
}
# =============================================================================
# INTERACTIVE MENU
# =============================================================================
function Show-MainMenu {
Clear-Host
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
Write-Host " ROA2WEB - Auto-Deploy Monitor (Interactive)" -ForegroundColor Cyan
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host ""
Write-Host " [1] Check for Updates (No Deploy)" -ForegroundColor White
Write-Host " (Scan for new packages without deploying)" -ForegroundColor Gray
Write-Host ""
Write-Host " [2] Check and Deploy Now" -ForegroundColor White
Write-Host " (Find latest package and deploy if new)" -ForegroundColor Gray
Write-Host ""
Write-Host " [3] View Deployment History" -ForegroundColor White
Write-Host " (Show previous deployments)" -ForegroundColor Gray
Write-Host ""
Write-Host " [4] View Current Configuration" -ForegroundColor White
Write-Host " (Display monitor settings)" -ForegroundColor Gray
Write-Host ""
Write-Host " [Q] Quit" -ForegroundColor Red
Write-Host ""
Write-Host ("=" * 70) -ForegroundColor Cyan
do {
Write-Host "`nYour choice: " -ForegroundColor Yellow -NoNewline
$choice = Read-Host
switch ($choice.ToUpper()) {
"1" { return "CheckOnly" }
"2" { return "CheckAndDeploy" }
"3" { return "History" }
"4" { return "ViewConfig" }
"Q" { return "Quit" }
default {
Write-Host "Invalid choice. Please select 1-4 or Q." -ForegroundColor Red
}
}
} while ($true)
}
function Show-DeploymentHistory {
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
Write-Host " Deployment History" -ForegroundColor Cyan
Write-Host ("=" * 70) -ForegroundColor Cyan
$state = Get-DeploymentState
if (-not $state) {
Write-Host "`nNo deployment history found" -ForegroundColor Yellow
return
}
if ($state.lastDeployment) {
Write-Host "`nLast Deployment:" -ForegroundColor Yellow
Write-Host " Package: $($state.lastDeployment.packageName)" -ForegroundColor Gray
Write-Host " Component: $($state.lastDeployment.component)" -ForegroundColor Gray
Write-Host " Timestamp: $($state.lastDeployment.timestamp)" -ForegroundColor Gray
Write-Host " Status: $($state.lastDeployment.status)" -ForegroundColor $(if ($state.lastDeployment.status -eq "Success") { "Green" } else { "Red" })
}
if ($state.history -and $state.history.Count -gt 0) {
Write-Host "`nPrevious Deployments:" -ForegroundColor Yellow
$i = 1
foreach ($entry in $state.history) {
Write-Host " [$i] $($entry.packageName) - $($entry.timestamp) - $($entry.status)" -ForegroundColor Gray
$i++
}
}
Write-Host ("=" * 70) -ForegroundColor Cyan
}
function Show-Configuration {
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
Write-Host " Current Configuration" -ForegroundColor Cyan
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host ""
Write-Host " Watch Path: $($script:Config.WatchPath)" -ForegroundColor Gray
Write-Host " State File: $($script:Config.StateFile)" -ForegroundColor Gray
Write-Host " Log Path: $($script:Config.LogPath)" -ForegroundColor Gray
Write-Host " Console Script: $($script:Config.ConsoleScriptPath)" -ForegroundColor Gray
Write-Host ""
Write-Host ("=" * 70) -ForegroundColor Cyan
}
# =============================================================================
# MAIN CHECK & DEPLOY LOGIC
# =============================================================================
function Invoke-CheckAndDeploy {
param([switch]$CheckOnly)
Write-Log -Message "=== Check and Deploy Started ===" -Level "INFO"
# Get latest package
$latestPackage = Get-LatestDeploymentPackage
if (-not $latestPackage) {
Write-Log -Message "No deployment packages found" -Level "INFO"
return $false
}
# Check if it's a new package
$isNew = Test-IsNewPackage -Package $latestPackage
if (-not $isNew) {
Write-Log -Message "No new packages to deploy" -Level "INFO"
return $false
}
if ($CheckOnly) {
Write-Success "New package available: $($latestPackage.Name)"
Write-Host "`nRun without -CheckOnly to deploy" -ForegroundColor Yellow
return $false
}
# Deploy the package
$deploySuccess = Invoke-Deployment -Package $latestPackage
Write-Log -Message "=== Check and Deploy Completed (Success: $deploySuccess) ===" -Level "INFO"
return $deploySuccess
}
# =============================================================================
# MAIN EXECUTION FLOW
# =============================================================================
function Main {
# Check if running as Administrator
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
$isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
Write-Host "`n[ERROR] This script requires Administrator privileges" -ForegroundColor Red
Write-Host "Please run PowerShell as Administrator and try again.`n" -ForegroundColor Yellow
exit 1
}
# Interactive mode
if ($Interactive) {
do {
$mainChoice = Show-MainMenu
switch ($mainChoice) {
"CheckOnly" {
Invoke-CheckAndDeploy -CheckOnly
Wait-ForKeyPress
}
"CheckAndDeploy" {
Invoke-CheckAndDeploy
Wait-ForKeyPress
}
"History" {
Show-DeploymentHistory
Wait-ForKeyPress
}
"ViewConfig" {
Show-Configuration
Wait-ForKeyPress
}
"Quit" {
Write-Host "`nGoodbye!`n" -ForegroundColor Cyan
return
}
}
} while ($true)
}
# Non-interactive mode (for Scheduled Task)
else {
Invoke-CheckAndDeploy -CheckOnly:$CheckOnly | Out-Null
}
}
# Run main
Main

View File

@@ -0,0 +1,656 @@
<#
.SYNOPSIS
ROA2WEB - Initial Installation Script for Windows Server + IIS
.DESCRIPTION
This script performs complete installation of ROA2WEB on Windows Server:
- Checks prerequisites (Admin rights, IIS)
- Installs Python 3.11+ if needed
- Installs NSSM (service manager)
- Installs IIS URL Rewrite and ARR modules
- Creates directory structure
- Installs Python dependencies
- Creates Windows Service for backend
- Configures IIS website
- Starts all services
.PARAMETER InstallPath
Installation path (default: C:\inetpub\wwwroot\roa2web)
.PARAMETER PythonVersion
Python version to install (default: 3.11.9)
.PARAMETER ServicePort
Backend service port (default: 8000)
.PARAMETER SkipPython
Skip Python installation (use existing Python)
.PARAMETER SkipIIS
Skip IIS configuration
.EXAMPLE
.\Install-ROA2WEB.ps1
Standard installation with defaults
.EXAMPLE
.\Install-ROA2WEB.ps1 -InstallPath "D:\Apps\roa2web" -ServicePort 8001
Custom installation path and port
.NOTES
Author: ROA2WEB Team
Requires: PowerShell 5.1+, Administrator privileges
#>
[CmdletBinding()]
param(
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web",
[string]$PythonVersion = "3.11.9",
[int]$ServicePort = 8000,
[string]$IISSiteName = "Default Web Site",
[string]$IISAppName = "roa2web",
[switch]$CreateNewSite,
[switch]$SkipPython,
[switch]$SkipIIS
)
# Strict error handling
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
# =============================================================================
# CONFIGURATION
# =============================================================================
$script:Config = @{
AppName = "ROA2WEB"
ServiceName = "ROA2WEB-Backend"
ServiceDisplayName = "ROA2WEB Unified Backend Service"
ServiceDescription = "Unified FastAPI backend for ROA2WEB ERP - includes Reports, Data Entry, and Telegram modules (Ultrathin Monolith)"
InstallPath = $InstallPath
BackendPath = Join-Path $InstallPath "backend"
FrontendPath = Join-Path $InstallPath "frontend"
LogsPath = Join-Path $InstallPath "logs"
TempPath = Join-Path $InstallPath "temp"
# IMPORTANT: venv is OUTSIDE InstallPath to survive deployments!
VenvPath = "C:\inetpub\wwwroot\roa2web-venv"
PythonVersion = $PythonVersion
ServicePort = $ServicePort
IISSiteName = $IISSiteName
IISAppName = $IISAppName
IISAppPoolName = "ROA2WEB-AppPool"
CreateNewSite = $CreateNewSite
}
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
function Write-Step {
param([string]$Message)
Write-Host "`n[*] $Message" -ForegroundColor Cyan
}
function Write-Success {
param([string]$Message)
Write-Host " [OK] $Message" -ForegroundColor Green
}
function Write-Error {
param([string]$Message)
Write-Host " [ERROR] $Message" -ForegroundColor Red
}
function Write-Warning {
param([string]$Message)
Write-Host " [WARN] $Message" -ForegroundColor Yellow
}
function Test-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]$identity
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Test-CommandExists {
param([string]$Command)
try {
if (Get-Command $Command -ErrorAction Stop) {
return $true
}
} catch {
return $false
}
}
function Install-Chocolatey {
Write-Step "Installing Chocolatey package manager..."
if (Test-CommandExists "choco") {
Write-Success "Chocolatey already installed"
return
}
try {
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
# Refresh environment
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
Write-Success "Chocolatey installed successfully"
} catch {
throw "Failed to install Chocolatey: $_"
}
}
function Install-Python {
Write-Step "Checking Python installation..."
if ($SkipPython) {
Write-Warning "Skipping Python installation (as requested)"
return
}
# Check if Python is already installed
try {
$pythonCmd = Get-Command python -ErrorAction Stop
$pythonVersionOutput = & python --version 2>&1
if ($pythonVersionOutput -match "Python (\d+\.\d+\.\d+)") {
$installedVersion = $matches[1]
Write-Success "Python $installedVersion already installed at $($pythonCmd.Source)"
return
}
} catch {
Write-Warning "Python not found, will install..."
}
# Install Python via Chocolatey
Write-Step "Installing Python $PythonVersion..."
try {
choco install python --version=$PythonVersion -y --force
# Refresh environment
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
Write-Success "Python $PythonVersion installed successfully"
} catch {
throw "Failed to install Python: $_"
}
}
function Install-NSSM {
Write-Step "Installing NSSM (service manager)..."
if (Test-Path "C:\nssm\nssm.exe") {
Write-Success "NSSM already installed"
return
}
try {
choco install nssm -y
Write-Success "NSSM installed successfully"
} catch {
throw "Failed to install NSSM: $_"
}
}
function Install-IISModules {
if ($SkipIIS) {
Write-Warning "Skipping IIS configuration (as requested)"
return
}
Write-Step "Checking IIS installation..."
# Detect OS type (Server vs Desktop)
$osInfo = Get-CimInstance -ClassName Win32_OperatingSystem
$isServer = $osInfo.ProductType -eq 3 # 1=Workstation, 2=Domain Controller, 3=Server
# Check if IIS is installed (different cmdlets for Server vs Desktop)
$iisInstalled = $false
if ($isServer) {
# Windows Server - use Get-WindowsFeature
$iisFeature = Get-WindowsFeature -Name Web-Server -ErrorAction SilentlyContinue
$iisInstalled = $iisFeature -and $iisFeature.InstallState -eq "Installed"
if (-not $iisInstalled) {
Write-Error "IIS is not installed. Please install IIS first:"
Write-Host " Install-WindowsFeature -Name Web-Server -IncludeManagementTools" -ForegroundColor Yellow
throw "IIS not installed"
}
} else {
# Windows Desktop (10/11) - use Get-WindowsOptionalFeature
$iisFeature = Get-WindowsOptionalFeature -Online -FeatureName IIS-WebServer -ErrorAction SilentlyContinue
$iisInstalled = $iisFeature -and $iisFeature.State -eq "Enabled"
if (-not $iisInstalled) {
Write-Error "IIS is not installed. Please install IIS first:"
Write-Host " Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServer -All" -ForegroundColor Yellow
Write-Host " Or use: Control Panel -> Programs -> Turn Windows features on/off -> Internet Information Services" -ForegroundColor Yellow
throw "IIS not installed"
}
}
Write-Success "IIS is installed ($($osInfo.Caption))"
# Install URL Rewrite Module
Write-Step "Installing IIS URL Rewrite Module..."
$urlRewriteInstalled = Get-WebConfiguration -Filter "/system.webServer/rewrite" -PSPath "IIS:\" -ErrorAction SilentlyContinue
if (-not $urlRewriteInstalled) {
Write-Warning "URL Rewrite not found, installing..."
try {
$urlRewriteUrl = "https://download.microsoft.com/download/1/2/8/128E2E22-C1B9-44A4-BE2A-5859ED1D4592/rewrite_amd64_en-US.msi"
$urlRewritePath = "$env:TEMP\rewrite_amd64.msi"
Invoke-WebRequest -Uri $urlRewriteUrl -OutFile $urlRewritePath
Start-Process msiexec.exe -ArgumentList "/i", $urlRewritePath, "/quiet", "/norestart" -Wait
Remove-Item $urlRewritePath -Force
Write-Success "URL Rewrite Module installed"
} catch {
Write-Error "Failed to install URL Rewrite: $_"
Write-Warning "You can download it manually from: https://www.iis.net/downloads/microsoft/url-rewrite"
}
} else {
Write-Success "URL Rewrite Module already installed"
}
# Install Application Request Routing (ARR)
Write-Step "Checking Application Request Routing (ARR)..."
try {
choco install iis-arr -y
Write-Success "ARR installed successfully"
} catch {
Write-Warning "Could not install ARR via Chocolatey. Download manually from: https://www.iis.net/downloads/microsoft/application-request-routing"
}
}
function New-DirectoryStructure {
Write-Step "Creating directory structure..."
$directories = @(
$Config.InstallPath,
$Config.BackendPath,
$Config.FrontendPath,
$Config.LogsPath,
$Config.TempPath,
(Join-Path $Config.BackendPath "logs"),
(Join-Path $Config.BackendPath "temp")
)
foreach ($dir in $directories) {
if (-not (Test-Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
Write-Success "Created: $dir"
} else {
Write-Success "Already exists: $dir"
}
}
# Set permissions (IIS user needs read access)
try {
$acl = Get-Acl $Config.InstallPath
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule("IIS_IUSRS", "ReadAndExecute", "ContainerInherit,ObjectInherit", "None", "Allow")
$acl.SetAccessRule($accessRule)
Set-Acl -Path $Config.InstallPath -AclObject $acl
Write-Success "Permissions set for IIS_IUSRS"
} catch {
Write-Warning "Could not set permissions: $_"
}
}
function Install-PythonDependencies {
Write-Step "Setting up Python virtual environment..."
$requirementsPath = Join-Path $Config.BackendPath "requirements.txt"
$venvPath = $Config.VenvPath
$venvPython = Join-Path $venvPath "Scripts\python.exe"
$venvPip = Join-Path $venvPath "Scripts\pip.exe"
# Create venv if it doesn't exist
if (-not (Test-Path $venvPython)) {
Write-Step "Creating virtual environment at $venvPath..."
try {
& python -m venv $venvPath
Write-Success "Virtual environment created"
} catch {
throw "Failed to create virtual environment: $_"
}
} else {
Write-Success "Virtual environment already exists"
}
# Upgrade pip in venv
Write-Step "Upgrading pip in virtual environment..."
try {
& $venvPython -m pip install --upgrade pip
Write-Success "Pip upgraded"
} catch {
Write-Warning "Could not upgrade pip: $_"
}
# Install dependencies
if (-not (Test-Path $requirementsPath)) {
Write-Warning "requirements.txt not found at $requirementsPath"
Write-Warning "Please copy backend files first, then run this script again"
return
}
Write-Step "Installing Python dependencies in virtual environment..."
try {
& $venvPip install -r $requirementsPath
Write-Success "Python dependencies installed successfully in venv"
} catch {
throw "Failed to install Python dependencies: $_"
}
}
function New-WindowsService {
Write-Step "Creating Windows Service for backend..."
# Check if service already exists using nssm (more reliable than Get-Service)
# Temporarily disable error action to check service status
$oldErrorAction = $ErrorActionPreference
$ErrorActionPreference = "SilentlyContinue"
$nssmOutput = & nssm status $Config.ServiceName 2>&1
$serviceExists = $LASTEXITCODE -eq 0
$ErrorActionPreference = $oldErrorAction
if ($serviceExists) {
Write-Warning "Service already exists, stopping and removing..."
# Check service status
$status = & nssm status $Config.ServiceName 2>&1
# Only try to stop if service is running
if ($status -match "SERVICE_RUNNING") {
Write-Step "Stopping running service..."
& nssm stop $Config.ServiceName 2>&1 | Out-Null
Start-Sleep -Seconds 2
} else {
Write-Step "Service is not running (status: $status)"
}
# Force remove service
& nssm remove $Config.ServiceName confirm 2>&1 | Out-Null
Start-Sleep -Seconds 2
Write-Success "Existing service removed"
}
# Verify venv exists (wrapper script needs it)
$venvPython = Join-Path $Config.VenvPath "Scripts\python.exe"
if (-not (Test-Path $venvPython)) {
throw "Virtual environment Python not found at $venvPython. Run Install-PythonDependencies first."
}
# NSSM service creation using wrapper script
# The wrapper script (start-backend-service.ps1) handles:
# 1. Starting SSH tunnels before backend
# 2. Waiting for tunnel ports to be accessible
# 3. Starting uvicorn
try {
$wrapperScript = Join-Path $PSScriptRoot "start-backend-service.ps1"
# 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
& nssm set $Config.ServiceName DisplayName $Config.ServiceDisplayName
& nssm set $Config.ServiceName Description $Config.ServiceDescription
& nssm set $Config.ServiceName Start SERVICE_AUTO_START
& nssm set $Config.ServiceName AppDirectory $Config.BackendPath
# Set environment variables (PYTHONPATH for shared modules)
# Point to the installation root AND backend/ so both shared/ and app/ modules can be imported
$pythonPathRoot = $Config.InstallPath
$pythonPathBackend = $Config.BackendPath
& nssm set $Config.ServiceName AppEnvironmentExtra "PYTHONPATH=$pythonPathRoot;$pythonPathBackend"
# Set logging
$stdoutLog = Join-Path $Config.LogsPath "backend-stdout.log"
$stderrLog = Join-Path $Config.LogsPath "backend-stderr.log"
& nssm set $Config.ServiceName AppStdout $stdoutLog
& nssm set $Config.ServiceName AppStderr $stderrLog
& nssm set $Config.ServiceName AppStdoutCreationDisposition 4
& nssm set $Config.ServiceName AppStderrCreationDisposition 4
# Set restart policy
& nssm set $Config.ServiceName AppExit Default Restart
& nssm set $Config.ServiceName AppRestartDelay 5000
Write-Success "Windows Service created successfully (using wrapper script)"
Write-Success " Wrapper: $wrapperScript"
} catch {
throw "Failed to create Windows Service: $_"
}
}
function Initialize-IISWebsite {
if ($SkipIIS) {
Write-Warning "Skipping IIS website configuration (as requested)"
return
}
Write-Step "Configuring IIS application..."
Import-Module WebAdministration -ErrorAction Stop
# Remove existing app pool if present
if (Test-Path "IIS:\AppPools\$($Config.IISAppPoolName)") {
Write-Warning "Removing existing app pool..."
Remove-WebAppPool -Name $Config.IISAppPoolName -ErrorAction SilentlyContinue
}
# Create Application Pool
Write-Step "Creating IIS Application Pool..."
New-WebAppPool -Name $Config.IISAppPoolName -Force | Out-Null
Set-ItemProperty -Path "IIS:\AppPools\$($Config.IISAppPoolName)" -Name "managedRuntimeVersion" -Value ""
Write-Success "Application Pool created: $($Config.IISAppPoolName)"
if ($CreateNewSite) {
# Create new website (old behavior)
Write-Step "Creating new IIS Website..."
# Stop default website if running
try {
Stop-Website -Name "Default Web Site" -ErrorAction SilentlyContinue
Write-Success "Stopped Default Web Site"
} catch {
Write-Warning "Could not stop Default Web Site: $_"
}
# Remove existing site if present
if (Get-Website -Name $Config.IISSiteName -ErrorAction SilentlyContinue) {
Write-Warning "Removing existing website..."
Remove-Website -Name $Config.IISSiteName -ErrorAction SilentlyContinue
}
New-Website -Name $Config.IISSiteName `
-PhysicalPath $Config.FrontendPath `
-ApplicationPool $Config.IISAppPoolName `
-Port 80 `
-Force | Out-Null
Write-Success "Website created: $($Config.IISSiteName)"
# Start website
Start-Website -Name $Config.IISSiteName
Write-Success "Website started: $($Config.IISSiteName)"
} else {
# Create application under existing site (default behavior)
Write-Step "Creating IIS Application under '$($Config.IISSiteName)'..."
# Verify parent site exists
$parentSite = Get-Website -Name $Config.IISSiteName -ErrorAction SilentlyContinue
if (-not $parentSite) {
throw "Parent website '$($Config.IISSiteName)' does not exist. Use -CreateNewSite to create a new site."
}
# Remove existing application if present
$existingApp = Get-WebApplication -Name $Config.IISAppName -Site $Config.IISSiteName -ErrorAction SilentlyContinue
if ($existingApp) {
Write-Warning "Removing existing application..."
Remove-WebApplication -Name $Config.IISAppName -Site $Config.IISSiteName -ErrorAction SilentlyContinue
}
# Create application
New-WebApplication -Name $Config.IISAppName `
-Site $Config.IISSiteName `
-PhysicalPath $Config.FrontendPath `
-ApplicationPool $Config.IISAppPoolName `
-Force | Out-Null
Write-Success "Application created: /$($Config.IISAppName) under $($Config.IISSiteName)"
}
# Copy web.config to frontend path
$webConfigSource = Join-Path $PSScriptRoot "..\config\web.config"
$webConfigDest = Join-Path $Config.FrontendPath "web.config"
if (Test-Path $webConfigSource) {
Copy-Item -Path $webConfigSource -Destination $webConfigDest -Force
Write-Success "web.config copied to frontend path"
} else {
Write-Warning "web.config not found at $webConfigSource"
}
}
function Start-Services {
Write-Step "Starting services..."
# Start backend service
try {
Start-Service -Name $Config.ServiceName
Start-Sleep -Seconds 3
$service = Get-Service -Name $Config.ServiceName
if ($service.Status -eq "Running") {
Write-Success "Backend service started successfully"
} else {
Write-Error "Backend service failed to start (Status: $($service.Status))"
}
} catch {
Write-Error "Failed to start backend service: $_"
}
# Test backend health
Write-Step "Testing backend health..."
Start-Sleep -Seconds 5
try {
$response = Invoke-WebRequest -Uri "http://localhost:$($Config.ServicePort)/health" -UseBasicParsing -TimeoutSec 10
if ($response.StatusCode -eq 200) {
Write-Success "Backend health check passed"
}
} catch {
Write-Warning "Backend health check failed (may need time to start): $_"
}
}
function Show-Summary {
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
Write-Host " ROA2WEB INSTALLATION COMPLETED" -ForegroundColor Green
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host "`nInstallation Details:" -ForegroundColor Yellow
Write-Host " Install Path: $($Config.InstallPath)"
Write-Host " Backend Path: $($Config.BackendPath)"
Write-Host " Virtual Env: $($Config.VenvPath)"
Write-Host " Frontend Path: $($Config.FrontendPath)"
Write-Host " Service Name: $($Config.ServiceName)"
Write-Host " Service Port: $($Config.ServicePort)"
Write-Host " IIS Site: $($Config.IISSiteName)"
Write-Host "`nAccess Points:" -ForegroundColor Yellow
if ($Config.CreateNewSite) {
Write-Host " Web Application: http://localhost"
} else {
Write-Host " Web Application: http://localhost/$($Config.IISAppName)"
}
Write-Host " API Backend: http://localhost:$($Config.ServicePort)"
Write-Host " API Docs: http://localhost:$($Config.ServicePort)/docs"
Write-Host " Health Check: http://localhost:$($Config.ServicePort)/health"
Write-Host "`nNext Steps:" -ForegroundColor Yellow
Write-Host " 1. Copy backend files to: $($Config.BackendPath)"
Write-Host " 2. Copy frontend files to: $($Config.FrontendPath)"
Write-Host " 3. Configure .env file at: $($Config.BackendPath)\.env"
Write-Host ""
Write-Host " IMPORTANT - Module Control Flags in .env:" -ForegroundColor Cyan
Write-Host " MODULE_REPORTS_ENABLED=true # Enable/disable Reports module"
Write-Host " MODULE_DATA_ENTRY_ENABLED=true # Enable/disable Data Entry module"
Write-Host " MODULE_TELEGRAM_ENABLED=true # Enable/disable Telegram bot module"
Write-Host ""
Write-Host " 4. Start service: Start-Service $($Config.ServiceName)"
Write-Host "`nManagement Commands:" -ForegroundColor Yellow
Write-Host " Start Service: Start-Service $($Config.ServiceName)"
Write-Host " Stop Service: Stop-Service $($Config.ServiceName)"
Write-Host " Restart Service: Restart-Service $($Config.ServiceName)"
Write-Host " View Logs: Get-Content $($Config.LogsPath)\backend-stdout.log -Tail 50"
Write-Host " Check Status: Get-Service $($Config.ServiceName)"
Write-Host "`nArchitecture:" -ForegroundColor Yellow
Write-Host " ULTRATHIN MONOLITH - Single Windows service with multiple modules"
Write-Host " All modules share Oracle pool, auth, and cache"
Write-Host " Telegram bot runs as background task (not separate service)"
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
}
# =============================================================================
# MAIN INSTALLATION FLOW
# =============================================================================
function Main {
Write-Host @"
====================================================================
ROA2WEB - Windows Server Installation Script
Modern ERP Reports Application with FastAPI + Vue.js + IIS
====================================================================
"@ -ForegroundColor Cyan
# Check prerequisites
Write-Step "Checking prerequisites..."
if (-not (Test-Administrator)) {
Write-Error "This script must be run as Administrator"
Write-Host " Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow
exit 1
}
Write-Success "Running as Administrator"
try {
# Installation steps
Install-Chocolatey
Install-Python
Install-NSSM
Install-IISModules
New-DirectoryStructure
Install-PythonDependencies
New-WindowsService
Initialize-IISWebsite
Start-Services
Show-Summary
Write-Host "`nInstallation completed successfully!" -ForegroundColor Green
} catch {
Write-Host "`n[FATAL ERROR] Installation failed: $_" -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor Red
exit 1
}
}
# Run main installation
Main

File diff suppressed because it is too large Load Diff

View 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 }
}

View 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