810 lines
28 KiB
PowerShell
810 lines
28 KiB
PowerShell
# InSAR Management System - Startup Script (PowerShell)
|
|
# Features:
|
|
# - Process management (stop old instances, start new ones)
|
|
# - Database health check
|
|
# - Auto Nginx config update
|
|
# - Logs output
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
|
|
# 1. Set project root
|
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
|
$ProjectRoot = Split-Path -Parent $ScriptDir
|
|
Set-Location -LiteralPath "$ProjectRoot"
|
|
|
|
Write-Host ">>> Starting InSAR Management System V2..." -ForegroundColor Cyan
|
|
Write-Host ">>> Project Root: $ProjectRoot"
|
|
|
|
# 2. Parse .env minimally (only for resolving Python/Conda before handing off to unified config)
|
|
$EnvPath = Join-Path $ProjectRoot ".env"
|
|
if (-not (Test-Path -LiteralPath "$EnvPath")) {
|
|
Write-Error "File .env not found: $EnvPath"
|
|
return
|
|
}
|
|
|
|
$PythonExe = "python"
|
|
$CondaExe = ""
|
|
$CondaEnvName = ""
|
|
$NginxExe = "C:/nginx-1.29.4/nginx.exe"
|
|
$ServerHost = ""
|
|
$ServerPort = 18000
|
|
$NginxAllowedClientIps = ""
|
|
$BackendReadyTimeoutSeconds = 120
|
|
$TileServerAutoStart = $false
|
|
$TileServerAutoStop = $true
|
|
$TileServerRoot = ""
|
|
$TileServerStartScript = "start-all.bat"
|
|
$TileServerStopScript = "stop-all.bat"
|
|
$TileServerUrl = ""
|
|
|
|
$envLines = Get-Content -LiteralPath "$EnvPath"
|
|
foreach ($line in $envLines) {
|
|
$trimmed = $line.Trim()
|
|
if ($trimmed.StartsWith("#") -or -not $trimmed.Contains("=")) { continue }
|
|
|
|
$parts = $trimmed.Split("=", 2)
|
|
$key = $parts[0].Trim()
|
|
$val = $parts[1].Trim().Trim('"').Trim("'")
|
|
|
|
if ($key -eq "PYTHON_PATH") { if ($val) { $PythonExe = $val } }
|
|
if ($key -eq "CONDA_EXE") { if ($val) { $CondaExe = $val } }
|
|
if ($key -eq "CONDA_ENV_NAME") { if ($val) { $CondaEnvName = $val } }
|
|
if ($key -eq "NGINX_PATH") { if ($val) { $NginxExe = $val } }
|
|
if ($key -eq "NGINX_ALLOWED_CLIENT_IPS") { $NginxAllowedClientIps = $val }
|
|
if ($key -eq "BACKEND_BIND_HOST") { if ($val) { $ServerHost = $val } }
|
|
if ($key -eq "BACKEND_READY_TIMEOUT_SECONDS") {
|
|
$parsed = 0
|
|
if ([int]::TryParse($val, [ref]$parsed) -and $parsed -gt 0) { $BackendReadyTimeoutSeconds = $parsed }
|
|
}
|
|
if ($key -eq "TILE_SERVER_AUTO_START") { $TileServerAutoStart = $val -match '^(?i)(true|1|yes|on)$' }
|
|
if ($key -eq "TILE_SERVER_AUTO_STOP") { $TileServerAutoStop = -not ($val -match '^(?i)(false|0|no|off)$') }
|
|
if ($key -eq "TILE_SERVER_ROOT") { if ($val) { $TileServerRoot = $val } }
|
|
if ($key -eq "TILE_SERVER_START_SCRIPT") { if ($val) { $TileServerStartScript = $val } }
|
|
if ($key -eq "TILE_SERVER_STOP_SCRIPT") { if ($val) { $TileServerStopScript = $val } }
|
|
if ($key -eq "VITE_TILE_SERVER_URL") { if ($val) { $TileServerUrl = $val.TrimEnd("/") } }
|
|
if ($key -eq "PORT") {
|
|
$parsed = 0
|
|
if ([int]::TryParse($val, [ref]$parsed)) { $ServerPort = $parsed }
|
|
}
|
|
}
|
|
|
|
$CheckDbScript = Join-Path $ProjectRoot "scripts\check_db_connection.py"
|
|
$CheckRuntimeScript = Join-Path $ProjectRoot "scripts\check_runtime_config.py"
|
|
$ExportLauncherConfigScript = Join-Path $ProjectRoot "scripts\export_launcher_config.py"
|
|
$InitDbScript = Join-Path $ProjectRoot "scripts\init_db.py"
|
|
|
|
function Resolve-ExecutablePath {
|
|
param([string]$Candidate)
|
|
|
|
$trimmed = "$Candidate".Trim().Trim('"').Trim("'")
|
|
if (-not $trimmed) {
|
|
return $null
|
|
}
|
|
|
|
if (Test-Path -LiteralPath "$trimmed") {
|
|
$resolved = Resolve-Path -LiteralPath "$trimmed" -ErrorAction SilentlyContinue
|
|
if ($resolved) {
|
|
return $resolved.Path
|
|
}
|
|
return $trimmed
|
|
}
|
|
|
|
$cmd = Get-Command -Name "$trimmed" -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if ($cmd -and $cmd.Source) {
|
|
return $cmd.Source
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
function Resolve-CondaEnvPythonPath {
|
|
param(
|
|
[string]$ResolvedCondaExe,
|
|
[string]$EnvName
|
|
)
|
|
|
|
$trimmedEnvName = "$EnvName".Trim()
|
|
if (-not $trimmedEnvName) {
|
|
return $null
|
|
}
|
|
|
|
$anacondaRoot = Split-Path -Parent (Split-Path -Parent "$ResolvedCondaExe")
|
|
$candidatePaths = @(
|
|
(Join-Path -Path $anacondaRoot -ChildPath "envs\$trimmedEnvName\python.exe"),
|
|
(Join-Path -Path $env:USERPROFILE -ChildPath ".conda\envs\$trimmedEnvName\python.exe")
|
|
)
|
|
|
|
foreach ($candidate in $candidatePaths) {
|
|
if (Test-Path -LiteralPath "$candidate") {
|
|
$resolved = Resolve-Path -LiteralPath "$candidate" -ErrorAction SilentlyContinue
|
|
if ($resolved) {
|
|
return $resolved.Path
|
|
}
|
|
return $candidate
|
|
}
|
|
}
|
|
|
|
try {
|
|
$envsJson = & "$ResolvedCondaExe" info --envs --json 2>$null
|
|
if ($LASTEXITCODE -eq 0 -and $envsJson) {
|
|
$envsInfo = $envsJson | ConvertFrom-Json
|
|
foreach ($envPath in ($envsInfo.envs | Where-Object { $_ })) {
|
|
if ((Split-Path -Leaf "$envPath") -ne $trimmedEnvName) {
|
|
continue
|
|
}
|
|
$pythonPath = Join-Path -Path "$envPath" -ChildPath "python.exe"
|
|
if (Test-Path -LiteralPath "$pythonPath") {
|
|
$resolved = Resolve-Path -LiteralPath "$pythonPath" -ErrorAction SilentlyContinue
|
|
if ($resolved) {
|
|
return $resolved.Path
|
|
}
|
|
return $pythonPath
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
# Ignore and continue with fallback.
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
function Invoke-PythonScript {
|
|
param([string]$ScriptPath)
|
|
|
|
& "$PythonExe" "$ScriptPath"
|
|
}
|
|
|
|
function Invoke-PythonScriptJson {
|
|
param([string]$ScriptPath)
|
|
|
|
$output = & "$PythonExe" "$ScriptPath"
|
|
if ($LASTEXITCODE -ne 0) {
|
|
return $null
|
|
}
|
|
if (-not $output) {
|
|
return $null
|
|
}
|
|
return ($output | ConvertFrom-Json)
|
|
}
|
|
|
|
# 3. Stop old instances
|
|
Write-Host ">>> Stopping existing processes..." -ForegroundColor Yellow
|
|
|
|
function Stop-Process-By-Name {
|
|
param([string]$Name, [string]$ExeName)
|
|
# Prefer precise cmdline matching; fall back to executable path when CIM command line is unavailable.
|
|
$NginxConfMatch = Join-Path -Path $ProjectRoot -ChildPath "nginx"
|
|
$matched = @()
|
|
$resolvedExe = Resolve-ExecutablePath -Candidate $ExeName
|
|
|
|
$candidates = Get-CimInstance Win32_Process -Filter "Name='$Name.exe'" -ErrorAction SilentlyContinue
|
|
if ($candidates) {
|
|
foreach ($proc in $candidates) {
|
|
if ($proc.CommandLine -and $proc.CommandLine -like "*$NginxConfMatch*") {
|
|
$matched += $proc
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($matched.Count -eq 0 -and $resolvedExe) {
|
|
$processesByPath = Get-Process -Name $Name -ErrorAction SilentlyContinue
|
|
foreach ($proc in $processesByPath) {
|
|
$procPath = $null
|
|
try {
|
|
$procPath = $proc.Path
|
|
} catch {
|
|
$procPath = $null
|
|
}
|
|
|
|
if ($procPath -and ([string]::Equals($procPath, $resolvedExe, [System.StringComparison]::OrdinalIgnoreCase))) {
|
|
$matched += $proc
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($matched.Count -gt 0) {
|
|
$targetPids = @($matched | ForEach-Object {
|
|
if ($_.PSObject.Properties['ProcessId']) { $_.ProcessId }
|
|
elseif ($_.PSObject.Properties['Id']) { $_.Id }
|
|
} | Where-Object { $_ } | Sort-Object -Unique)
|
|
|
|
Write-Host " Stopping $Name (PID: $($targetPids -join ', '))..." -NoNewline
|
|
foreach ($p in $matched) {
|
|
$processId = $null
|
|
if ($p.PSObject.Properties['ProcessId']) { $processId = $p.ProcessId }
|
|
elseif ($p.PSObject.Properties['Id']) { $processId = $p.Id }
|
|
if ($processId) {
|
|
Stop-Process -Id $processId -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
Start-Sleep -Seconds 1
|
|
$stillAlive = $false
|
|
foreach ($targetProcessId in $targetPids) {
|
|
if (Get-Process -Id $targetProcessId -ErrorAction SilentlyContinue) {
|
|
$stillAlive = $true
|
|
}
|
|
}
|
|
if (-not $stillAlive) {
|
|
Write-Host " [Done]" -ForegroundColor Green
|
|
} else {
|
|
Write-Host " [Failed]" -ForegroundColor Red
|
|
}
|
|
}
|
|
}
|
|
|
|
function Stop-Backend-By-Cmdline {
|
|
param([string]$MatchText)
|
|
$candidates = Get-CimInstance Win32_Process -Filter "Name='python.exe'" -ErrorAction SilentlyContinue
|
|
foreach ($proc in $candidates) {
|
|
if ($proc.CommandLine -and $proc.CommandLine -like "*$MatchText*") {
|
|
try {
|
|
Write-Host " Stopping python (PID $($proc.ProcessId)) with cmdline match: $MatchText" -NoNewline
|
|
Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue
|
|
Start-Sleep -Seconds 1
|
|
$check = Get-Process -Id $proc.ProcessId -ErrorAction SilentlyContinue
|
|
if (-not $check) {
|
|
Write-Host " [Done]" -ForegroundColor Green
|
|
} else {
|
|
Write-Host " [Failed]" -ForegroundColor Red
|
|
}
|
|
} catch {
|
|
Write-Host " [Failed]" -ForegroundColor Red
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-ListeningPortOwner {
|
|
param([int]$Port)
|
|
|
|
$conn = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if ($conn) {
|
|
$procName = "Unknown"
|
|
$proc = Get-Process -Id $conn.OwningProcess -ErrorAction SilentlyContinue
|
|
if ($proc) {
|
|
$procName = $proc.ProcessName
|
|
}
|
|
|
|
return [PSCustomObject]@{
|
|
Port = $Port
|
|
PID = $conn.OwningProcess
|
|
ProcessName = $procName
|
|
LocalAddress = $conn.LocalAddress
|
|
}
|
|
}
|
|
|
|
$netstatLines = netstat -ano -p tcp 2>$null
|
|
if ($netstatLines) {
|
|
foreach ($line in $netstatLines) {
|
|
$trimmed = "$line".Trim()
|
|
if (-not $trimmed.StartsWith("TCP")) {
|
|
continue
|
|
}
|
|
if ($trimmed -notmatch "\s+LISTENING\s+") {
|
|
continue
|
|
}
|
|
$parts = $trimmed -split "\s+"
|
|
if ($parts.Count -lt 5) {
|
|
continue
|
|
}
|
|
$localEndpoint = $parts[1]
|
|
$pidText = $parts[4]
|
|
$localPort = -1
|
|
if ($localEndpoint -match ":(\d+)$") {
|
|
$localPort = [int]$matches[1]
|
|
}
|
|
if ($localPort -ne $Port) {
|
|
continue
|
|
}
|
|
|
|
$ownerPid = 0
|
|
[void][int]::TryParse("$pidText", [ref]$ownerPid)
|
|
$procName = "Unknown"
|
|
if ($ownerPid -gt 0) {
|
|
$proc = Get-Process -Id $ownerPid -ErrorAction SilentlyContinue
|
|
if ($proc) {
|
|
$procName = $proc.ProcessName
|
|
}
|
|
}
|
|
|
|
return [PSCustomObject]@{
|
|
Port = $Port
|
|
PID = $ownerPid
|
|
ProcessName = $procName
|
|
LocalAddress = $localEndpoint
|
|
}
|
|
}
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
function Test-PortAvailable {
|
|
param([int]$Port)
|
|
|
|
try {
|
|
$probe = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, $Port)
|
|
$probe.Start()
|
|
$probe.Stop()
|
|
return $true
|
|
} catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Write-NginxClientAllowFile {
|
|
param([string]$Path)
|
|
|
|
$entries = @()
|
|
$raw = "$NginxAllowedClientIps".Trim()
|
|
if ($raw) {
|
|
$entries = @($raw -split '[;,\s]+' | ForEach-Object { "$_".Trim() } | Where-Object { $_ })
|
|
}
|
|
|
|
$lines = @(
|
|
"# Generated by scripts/start_app.ps1.",
|
|
"# Configure NGINX_ALLOWED_CLIENT_IPS in .env. Empty means allow all clients."
|
|
)
|
|
|
|
if ($entries.Count -gt 0) {
|
|
$allowed = @("127.0.0.1", "::1") + $entries
|
|
$allowed = @($allowed | Sort-Object -Unique)
|
|
foreach ($item in $allowed) {
|
|
$lines += " allow $item;"
|
|
}
|
|
$lines += " deny all;"
|
|
}
|
|
|
|
$dir = Split-Path -Parent "$Path"
|
|
if (-not (Test-Path -LiteralPath "$dir")) {
|
|
New-Item -ItemType Directory -Path "$dir" -Force | Out-Null
|
|
}
|
|
$Utf8NoBom = New-Object System.Text.UTF8Encoding $false
|
|
[System.IO.File]::WriteAllText("$Path", (($lines -join [Environment]::NewLine) + [Environment]::NewLine), $Utf8NoBom)
|
|
|
|
if ($entries.Count -gt 0) {
|
|
Write-Host ">>> Nginx client IP whitelist enabled: $($entries -join ', ')" -ForegroundColor Yellow
|
|
} else {
|
|
Write-Host ">>> Nginx client IP whitelist disabled." -ForegroundColor DarkGray
|
|
}
|
|
}
|
|
|
|
function Test-TileServerReady {
|
|
if (-not $TileServerUrl) {
|
|
return $false
|
|
}
|
|
try {
|
|
$response = Invoke-WebRequest -Uri "$TileServerUrl/health" -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop
|
|
return ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500)
|
|
} catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Get-TileServerScriptPath {
|
|
param([string]$ScriptName)
|
|
|
|
$root = "$TileServerRoot".Trim().Trim('"').Trim("'")
|
|
if (-not $root) {
|
|
return $null
|
|
}
|
|
$script = "$ScriptName".Trim().Trim('"').Trim("'")
|
|
if (-not $script) {
|
|
return $null
|
|
}
|
|
if ([System.IO.Path]::IsPathRooted($script)) {
|
|
return $script
|
|
}
|
|
return (Join-Path -Path $root -ChildPath $script)
|
|
}
|
|
|
|
function Stop-TileServer {
|
|
if (-not $TileServerAutoStop) {
|
|
return
|
|
}
|
|
$stopScript = Get-TileServerScriptPath -ScriptName "$TileServerStopScript"
|
|
if (-not $stopScript -or -not (Test-Path -LiteralPath "$stopScript")) {
|
|
return
|
|
}
|
|
Write-Host ">>> Stopping tile-server..." -ForegroundColor Yellow
|
|
$previousNoPause = $env:NO_PAUSE
|
|
try {
|
|
$env:NO_PAUSE = "1"
|
|
& "$stopScript"
|
|
} catch {
|
|
Write-Warning "tile-server stop failed: $($_.Exception.Message)"
|
|
} finally {
|
|
if ($null -eq $previousNoPause) {
|
|
Remove-Item Env:\NO_PAUSE -ErrorAction SilentlyContinue
|
|
} else {
|
|
$env:NO_PAUSE = $previousNoPause
|
|
}
|
|
}
|
|
}
|
|
|
|
function Start-TileServer {
|
|
if (-not $TileServerAutoStart) {
|
|
return
|
|
}
|
|
if (Test-TileServerReady) {
|
|
Write-Host ">>> tile-server already responding: $TileServerUrl" -ForegroundColor Green
|
|
return
|
|
}
|
|
$startScript = Get-TileServerScriptPath -ScriptName "$TileServerStartScript"
|
|
if (-not $startScript -or -not (Test-Path -LiteralPath "$startScript")) {
|
|
Write-Error "tile-server start script not found. TILE_SERVER_ROOT=$TileServerRoot TILE_SERVER_START_SCRIPT=$TileServerStartScript"
|
|
$global:LASTEXITCODE = 1
|
|
return
|
|
}
|
|
Write-Host ">>> Launching tile-server..." -ForegroundColor Green
|
|
$previousNoPause = $env:NO_PAUSE
|
|
try {
|
|
$env:NO_PAUSE = "1"
|
|
& "$startScript"
|
|
} finally {
|
|
if ($null -eq $previousNoPause) {
|
|
Remove-Item Env:\NO_PAUSE -ErrorAction SilentlyContinue
|
|
} else {
|
|
$env:NO_PAUSE = $previousNoPause
|
|
}
|
|
}
|
|
for ($i = 0; $i -lt 20; $i++) {
|
|
if (Test-TileServerReady) {
|
|
Write-Host ">>> tile-server ready: $TileServerUrl" -ForegroundColor Green
|
|
return
|
|
}
|
|
Start-Sleep -Milliseconds 500
|
|
}
|
|
Write-Warning "tile-server was started but did not pass health check: $TileServerUrl/health"
|
|
}
|
|
|
|
Stop-Backend-By-Cmdline -MatchText "run_backend.py"
|
|
Stop-Backend-By-Cmdline -MatchText "run_worker.py"
|
|
$NginxProcName = Split-Path -Leaf $NginxExe
|
|
$NginxProcName = $NginxProcName -replace '\.exe$', ''
|
|
Stop-Process-By-Name -Name $NginxProcName -ExeName $NginxExe
|
|
if ($TileServerAutoStart -and $TileServerAutoStop) {
|
|
Stop-TileServer
|
|
}
|
|
|
|
$PortAvailable = Test-PortAvailable -Port $ServerPort
|
|
if (-not $PortAvailable) {
|
|
$PortOwner = Get-ListeningPortOwner -Port $ServerPort
|
|
if ($PortOwner) {
|
|
Write-Error (
|
|
"Backend port $ServerPort is already in use by PID $($PortOwner.PID) " +
|
|
"($($PortOwner.ProcessName)) on $($PortOwner.LocalAddress). " +
|
|
"Please stop that process or change PORT in .env."
|
|
)
|
|
} else {
|
|
Write-Error (
|
|
"Backend port $ServerPort is not available (bind failed). " +
|
|
"Please stop the process using this port or change PORT in .env."
|
|
)
|
|
}
|
|
$global:LASTEXITCODE = 1
|
|
return
|
|
}
|
|
|
|
# 4. Runtime config + database pre-flight check
|
|
Write-Host ">>> Checking deployment configuration..." -ForegroundColor Yellow
|
|
|
|
$UseCondaRun = -not [string]::IsNullOrWhiteSpace("$CondaEnvName".Trim())
|
|
$ResolvedCondaExe = $null
|
|
$CondaEnvPythonExe = $null
|
|
|
|
if ($UseCondaRun) {
|
|
if ([string]::IsNullOrWhiteSpace("$CondaExe".Trim())) {
|
|
$CondaExe = "conda"
|
|
}
|
|
$ResolvedCondaExe = Resolve-ExecutablePath -Candidate $CondaExe
|
|
if (-not $ResolvedCondaExe) {
|
|
Write-Error "Conda executable not found. CONDA_EXE=$CondaExe"
|
|
$global:LASTEXITCODE = 1
|
|
return
|
|
}
|
|
$CondaEnvPythonExe = Resolve-CondaEnvPythonPath -ResolvedCondaExe "$ResolvedCondaExe" -EnvName "$CondaEnvName"
|
|
if (-not $CondaEnvPythonExe) {
|
|
Write-Error "Conda environment python not found. CONDA_ENV_NAME=$CondaEnvName"
|
|
$global:LASTEXITCODE = 1
|
|
return
|
|
}
|
|
$PythonExe = $CondaEnvPythonExe
|
|
} else {
|
|
$ResolvedPythonExe = Resolve-ExecutablePath -Candidate $PythonExe
|
|
if (-not $ResolvedPythonExe) {
|
|
Write-Error "Python executable not found. PYTHON_PATH=$PythonExe"
|
|
$global:LASTEXITCODE = 1
|
|
return
|
|
}
|
|
$PythonExe = $ResolvedPythonExe
|
|
}
|
|
|
|
$LauncherConfig = Invoke-PythonScriptJson -ScriptPath "$ExportLauncherConfigScript"
|
|
if (-not $LauncherConfig) {
|
|
Write-Error "Failed to load launcher runtime configuration from Python settings layer."
|
|
$global:LASTEXITCODE = 1
|
|
return
|
|
}
|
|
|
|
if ($LauncherConfig.nginx_path) {
|
|
$NginxExe = [string]$LauncherConfig.nginx_path
|
|
}
|
|
if ($LauncherConfig.backend_bind_host) {
|
|
$ServerHost = [string]$LauncherConfig.backend_bind_host
|
|
}
|
|
if ($LauncherConfig.port) {
|
|
$ServerPort = [int]$LauncherConfig.port
|
|
}
|
|
|
|
function Write-StatusLine {
|
|
param(
|
|
[string]$Message,
|
|
[ConsoleColor]$ForegroundColor = [ConsoleColor]::White
|
|
)
|
|
|
|
Write-Host $Message -ForegroundColor $ForegroundColor
|
|
}
|
|
|
|
function Get-BackgroundProcessLogs {
|
|
param([string]$ScriptPath)
|
|
|
|
$LogDir = Join-Path $ProjectRoot "logs"
|
|
if (-not (Test-Path -LiteralPath "$LogDir")) {
|
|
New-Item -ItemType Directory -Path "$LogDir" -Force | Out-Null
|
|
}
|
|
|
|
$ScriptBase = [System.IO.Path]::GetFileNameWithoutExtension("$ScriptPath")
|
|
return [PSCustomObject]@{
|
|
StdOut = Join-Path $LogDir "$ScriptBase.stdout.log"
|
|
StdErr = Join-Path $LogDir "$ScriptBase.stderr.log"
|
|
}
|
|
}
|
|
|
|
function Start-PythonBackground {
|
|
param([string]$ScriptPath)
|
|
|
|
$LogTargets = Get-BackgroundProcessLogs -ScriptPath "$ScriptPath"
|
|
Write-Host " stdout -> $($LogTargets.StdOut)"
|
|
Write-Host " stderr -> $($LogTargets.StdErr)"
|
|
|
|
return Start-Process `
|
|
-FilePath "$PythonExe" `
|
|
-ArgumentList @("$ScriptPath") `
|
|
-WorkingDirectory "$ProjectRoot" `
|
|
-PassThru `
|
|
-NoNewWindow `
|
|
-RedirectStandardOutput "$($LogTargets.StdOut)" `
|
|
-RedirectStandardError "$($LogTargets.StdErr)"
|
|
}
|
|
|
|
function Assert-ProcessAlive {
|
|
param(
|
|
[System.Diagnostics.Process]$Process,
|
|
[string]$DisplayName
|
|
)
|
|
|
|
if (-not $Process) {
|
|
Write-Error "$DisplayName failed to start: process handle is null."
|
|
$global:LASTEXITCODE = 1
|
|
return $false
|
|
}
|
|
|
|
Start-Sleep -Milliseconds 800
|
|
$procCheck = Get-Process -Id $Process.Id -ErrorAction SilentlyContinue
|
|
if (-not $procCheck) {
|
|
Write-Error "$DisplayName exited immediately after startup. Please check logs for details."
|
|
$global:LASTEXITCODE = 1
|
|
return $false
|
|
}
|
|
|
|
return $true
|
|
}
|
|
|
|
function Wait-BackendReady {
|
|
param(
|
|
[int]$Port,
|
|
[int]$TimeoutSeconds
|
|
)
|
|
|
|
$deadline = (Get-Date).AddSeconds([Math]::Max(1, $TimeoutSeconds))
|
|
# Use the lightweight root route for readiness. /api/health runs a full
|
|
# operational self-check and can legitimately take longer during startup.
|
|
$url = "http://127.0.0.1:$Port/"
|
|
$lastError = $null
|
|
|
|
Write-Host ">>> Waiting for backend readiness: $url" -ForegroundColor Yellow
|
|
while ((Get-Date) -lt $deadline) {
|
|
try {
|
|
$response = Invoke-WebRequest -Uri "$url" -UseBasicParsing -TimeoutSec 3 -ErrorAction Stop
|
|
if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) {
|
|
Write-Host ">>> Backend ready (status=$($response.StatusCode))." -ForegroundColor Green
|
|
return $true
|
|
}
|
|
$lastError = "HTTP $($response.StatusCode)"
|
|
} catch {
|
|
$lastError = $_.Exception.Message
|
|
}
|
|
Start-Sleep -Milliseconds 800
|
|
}
|
|
|
|
Write-Error "Backend did not become ready within $TimeoutSeconds seconds. Last error: $lastError"
|
|
$global:LASTEXITCODE = 1
|
|
return $false
|
|
}
|
|
|
|
Invoke-PythonScript -ScriptPath "$CheckRuntimeScript"
|
|
if ($LastExitCode -ne 0) {
|
|
Write-Host "`n[ERROR] Deployment configuration check failed." -ForegroundColor Red
|
|
Write-Host "Please review the error messages above and fix the configuration." -ForegroundColor Red
|
|
$global:LASTEXITCODE = 1
|
|
return
|
|
}
|
|
|
|
Write-Host ">>> Checking database connection..." -ForegroundColor Yellow
|
|
Invoke-PythonScript -ScriptPath "$CheckDbScript"
|
|
if ($LastExitCode -ne 0) {
|
|
Write-Host "`n[ERROR] Database connection check failed." -ForegroundColor Red
|
|
Write-Host "Please review the error messages above and fix the configuration." -ForegroundColor Red
|
|
$global:LASTEXITCODE = 1
|
|
return
|
|
}
|
|
|
|
# 4.5 Schema sync
|
|
Write-Host ">>> Checking database schema..." -ForegroundColor Yellow
|
|
Invoke-PythonScript -ScriptPath "$InitDbScript"
|
|
if ($LastExitCode -ne 0) {
|
|
Write-Host "`n[ERROR] Database schema check failed." -ForegroundColor Red
|
|
Write-Host "Please review the error messages above and fix the configuration." -ForegroundColor Red
|
|
$global:LASTEXITCODE = 1
|
|
return
|
|
}
|
|
|
|
# 5. Update Nginx config (absolute path fix)
|
|
$NginxConfPath = Join-Path -Path $ProjectRoot -ChildPath "nginx\nginx.conf"
|
|
if (Test-Path -LiteralPath "$NginxConfPath") {
|
|
Write-Host ">>> Configuring Nginx paths..." -ForegroundColor Yellow
|
|
|
|
$FrontendDistPath = Join-Path -Path $ProjectRoot -ChildPath "frontend\dist"
|
|
$FrontendIndexPath = Join-Path -Path $FrontendDistPath -ChildPath "index.html"
|
|
if (-not (Test-Path -LiteralPath "$FrontendIndexPath")) {
|
|
Write-Host "`n[ERROR] Frontend build output not found: $FrontendIndexPath" -ForegroundColor Red
|
|
Write-Host "Run scripts\bootstrap_clone.ps1 -InitFrontend -BuildFrontend or build frontend/dist manually before using start_system.bat." -ForegroundColor Red
|
|
$global:LASTEXITCODE = 1
|
|
return
|
|
}
|
|
|
|
$NginxBase = Split-Path -Parent "$NginxExe"
|
|
$SrcMime = Join-Path -Path $NginxBase -ChildPath "conf\mime.types"
|
|
$DestMime = Join-Path -Path $ProjectRoot -ChildPath "nginx\mime.types"
|
|
if (-not (Test-Path -LiteralPath "$DestMime") -and (Test-Path -LiteralPath "$SrcMime")) {
|
|
Copy-Item -Path "$SrcMime" -Destination "$DestMime" -Force
|
|
}
|
|
|
|
$ForwardRoot = $ProjectRoot.Replace([char]92, [char]47)
|
|
$FrontendDist = "$ForwardRoot/frontend/dist"
|
|
$ImageCache = "$ForwardRoot/backend/image_cache"
|
|
$NginxClientAllowFile = Join-Path -Path $ProjectRoot -ChildPath "nginx\client_allow.conf"
|
|
|
|
if (-not (Test-Path -LiteralPath "$ProjectRoot/backend/image_cache")) {
|
|
New-Item -ItemType Directory -Path "$ProjectRoot/backend/image_cache" -Force | Out-Null
|
|
}
|
|
Write-NginxClientAllowFile -Path "$NginxClientAllowFile"
|
|
|
|
$ConfContent = Get-Content -LiteralPath "$NginxConfPath" -Raw
|
|
$NewConfContent = $ConfContent -replace 'root\s+[^;]+;', "root `"$FrontendDist`";"
|
|
$NewConfContent = $NewConfContent -replace 'alias\s+[^;]+;', "alias `"$ImageCache/`";"
|
|
$ClientAllowForwardPath = "$ForwardRoot/nginx/client_allow.conf"
|
|
$NewConfContent = $NewConfContent -replace 'include\s+"[^"]*client_allow\.conf";', "include `"$ClientAllowForwardPath`";"
|
|
$BackendProxy = "http://127.0.0.1:$ServerPort"
|
|
$NewConfContent = [regex]::Replace(
|
|
$NewConfContent,
|
|
'(location\s+/api/\s*\{[\s\S]*?proxy_pass\s+)http://(127\.0\.0\.1|localhost):\d+(;)',
|
|
"`${1}$BackendProxy`${3}"
|
|
)
|
|
$NewConfContent = [regex]::Replace(
|
|
$NewConfContent,
|
|
'(location\s+/api/tasks/active/stream\s*\{[\s\S]*?proxy_pass\s+)http://(127\.0\.0\.1|localhost):\d+(;)',
|
|
"`${1}$BackendProxy`${3}"
|
|
)
|
|
# 使用 UTF8 无 BOM 编码写入
|
|
$Utf8NoBom = New-Object System.Text.UTF8Encoding $false
|
|
[System.IO.File]::WriteAllText("$NginxConfPath", $NewConfContent, $Utf8NoBom)
|
|
}
|
|
|
|
# 6. Start backend
|
|
Write-Host ">>> Launching backend (FastAPI)..." -ForegroundColor Green
|
|
if ($UseCondaRun) {
|
|
Write-Host ">>> Using Conda env python: $PythonExe (env=$CondaEnvName)"
|
|
} else {
|
|
Write-Host ">>> Using Python: $PythonExe"
|
|
}
|
|
|
|
$BackendProc = Start-PythonBackground -ScriptPath "run_backend.py"
|
|
if (-not (Assert-ProcessAlive -Process $BackendProc -DisplayName "Backend")) {
|
|
return
|
|
}
|
|
if (-not (Wait-BackendReady -Port $ServerPort -TimeoutSeconds $BackendReadyTimeoutSeconds)) {
|
|
return
|
|
}
|
|
|
|
# 6.5 Start job worker
|
|
Write-Host ">>> Launching job worker..." -ForegroundColor Green
|
|
$WorkerProc = Start-PythonBackground -ScriptPath "run_worker.py"
|
|
if (-not (Assert-ProcessAlive -Process $WorkerProc -DisplayName "Worker")) {
|
|
return
|
|
}
|
|
|
|
# 6.6 Start tile-server
|
|
Start-TileServer
|
|
if ($global:LASTEXITCODE -eq 1) {
|
|
return
|
|
}
|
|
|
|
# 7. Start Nginx
|
|
if (Test-Path -LiteralPath "$NginxExe") {
|
|
Write-Host ">>> Launching Nginx..." -ForegroundColor Green
|
|
$NginxDir = Split-Path -Parent "$NginxExe"
|
|
$NginxName = Split-Path -Leaf $NginxExe
|
|
|
|
$SafeConfPath = $NginxConfPath.Replace([char]92, [char]47)
|
|
$NginxArgs = @("-c", "$SafeConfPath")
|
|
$NginxLogs = Get-BackgroundProcessLogs -ScriptPath "$NginxName"
|
|
Write-Host " stdout -> $($NginxLogs.StdOut)"
|
|
Write-Host " stderr -> $($NginxLogs.StdErr)"
|
|
$NginxProc = Start-Process `
|
|
-FilePath "$NginxExe" `
|
|
-ArgumentList $NginxArgs `
|
|
-WorkingDirectory "$NginxDir" `
|
|
-PassThru `
|
|
-NoNewWindow `
|
|
-RedirectStandardOutput "$($NginxLogs.StdOut)" `
|
|
-RedirectStandardError "$($NginxLogs.StdErr)"
|
|
|
|
Start-Sleep -Seconds 2
|
|
|
|
$NginxProcName = $NginxName -replace '\.exe$', ''
|
|
$NginxRunning = Get-Process -Name $NginxProcName -ErrorAction SilentlyContinue
|
|
|
|
if ($NginxRunning) {
|
|
$DisplayHost = "localhost"
|
|
if ($ServerHost) { $DisplayHost = $ServerHost }
|
|
Write-Host ""
|
|
Write-StatusLine "============================================================" Green
|
|
Write-StatusLine "SUCCESS: InSAR Management System is running!" Green
|
|
Write-StatusLine "Frontend (via Nginx): http://$DisplayHost"
|
|
Write-StatusLine "Backend (internal): http://127.0.0.1`:$ServerPort"
|
|
Write-StatusLine "API Docs (internal): http://127.0.0.1`:$ServerPort/docs"
|
|
if ($TileServerAutoStart -and $TileServerUrl) {
|
|
Write-StatusLine "Tile Server: $TileServerUrl"
|
|
}
|
|
Write-StatusLine "============================================================" Green
|
|
Write-Host ""
|
|
Write-StatusLine "System is running. Press Ctrl+C to stop all services." Yellow
|
|
} else {
|
|
Write-Warning "Nginx process not found. Check logs/nginx_error.log for details."
|
|
}
|
|
} else {
|
|
Write-Warning "Nginx executable not found at: $NginxExe"
|
|
}
|
|
|
|
# 8. Wait for exit
|
|
try {
|
|
Write-Host ""
|
|
Write-StatusLine "Waiting for processes (Backend PID: $($BackendProc.Id), Worker PID: $($WorkerProc.Id))..." Cyan
|
|
|
|
$BackgroundJob = Register-ObjectEvent -InputObject $BackendProc -EventName "Exited" -Action { Write-Host "`nBackend process exited." -ForegroundColor Red } -ErrorAction SilentlyContinue
|
|
|
|
Wait-Process -Id $BackendProc.Id -ErrorAction SilentlyContinue
|
|
|
|
} finally {
|
|
Write-Host "`nShutting down..." -ForegroundColor Yellow
|
|
Stop-Backend-By-Cmdline -MatchText "run_backend.py"
|
|
Stop-Backend-By-Cmdline -MatchText "run_worker.py"
|
|
Stop-Process-By-Name -Name $NginxProcName -ExeName $NginxExe
|
|
Stop-TileServer
|
|
Write-Host "Done." -ForegroundColor Green
|
|
}
|