Files

820 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}"
)
$NewConfContent = [regex]::Replace(
$NewConfContent,
'(location\s+/api/tasks/runtime-summary/stream\s*\{[\s\S]*?proxy_pass\s+)http://(127\.0\.0\.1|localhost):\d+(;)',
"`${1}$BackendProxy`${3}"
)
$NewConfContent = [regex]::Replace(
$NewConfContent,
'(location\s+/api/cluster/\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
}