fix telegram
This commit is contained in:
547
deploy-package-20260223-151231/scripts/Check-And-Deploy.ps1
Normal file
547
deploy-package-20260223-151231/scripts/Check-And-Deploy.ps1
Normal 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
|
||||
656
deploy-package-20260223-151231/scripts/Install-ROA2WEB.ps1
Normal file
656
deploy-package-20260223-151231/scripts/Install-ROA2WEB.ps1
Normal 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
|
||||
1491
deploy-package-20260223-151231/scripts/ROA2WEB-Console.ps1
Normal file
1491
deploy-package-20260223-151231/scripts/ROA2WEB-Console.ps1
Normal file
File diff suppressed because it is too large
Load Diff
509
deploy-package-20260223-151231/scripts/ssh-tunnel.ps1
Normal file
509
deploy-package-20260223-151231/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
deploy-package-20260223-151231/scripts/start-backend-service.ps1
Normal file
225
deploy-package-20260223-151231/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
|
||||
Reference in New Issue
Block a user