Files
insar-management-system-v2/scripts/start_app.ps1
T

604 lines
21 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
$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 "BACKEND_BIND_HOST") { if ($val) { $ServerHost = $val } }
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
}
}
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
$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
}
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"
if (-not (Test-Path -LiteralPath "$ProjectRoot/backend/image_cache")) {
New-Item -ItemType Directory -Path "$ProjectRoot/backend/image_cache" -Force | Out-Null
}
$ConfContent = Get-Content -LiteralPath "$NginxConfPath" -Raw
$NewConfContent = $ConfContent -replace 'root\s+[^;]+;', "root `"$FrontendDist`";"
$NewConfContent = $NewConfContent -replace 'alias\s+[^;]+;', "alias `"$ImageCache/`";"
$BackendProxy = "http://127.0.0.1:$ServerPort"
$NewConfContent = $NewConfContent -replace 'proxy_pass\s+http://(127\.0\.0\.1|localhost):\d+;', "proxy_pass $BackendProxy;"
# 使用 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
}
# 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
}
# 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"
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
Write-Host "Done." -ForegroundColor Green
}