164 lines
4.9 KiB
PowerShell

$ErrorActionPreference = "Stop"
$appId = "airport-guide-dashboard"
$appRoot = Join-Path $PSScriptRoot "app"
$nodePath = Join-Path $PSScriptRoot "runtime\node.exe"
$serverPath = Join-Path $appRoot "server.js"
$dataRoot = Join-Path $env:LOCALAPPDATA "AirportGuideDashboard"
$configPath = Join-Path $dataRoot "config.json"
$portPath = Join-Path $dataRoot "port.txt"
$outputLogPath = Join-Path $dataRoot "launcher.log"
$errorLogPath = Join-Path $dataRoot "launcher-error.log"
function Show-LauncherError([string]$message) {
try {
Add-Type -AssemblyName PresentationFramework
[System.Windows.MessageBox]::Show(
$message,
"Airport Guide",
[System.Windows.MessageBoxButton]::OK,
[System.Windows.MessageBoxImage]::Error
) | Out-Null
} catch {
Write-Host $message
}
}
function Get-DashboardHealth([int]$port) {
try {
return Invoke-RestMethod `
-Uri "http://127.0.0.1:$port/api/health" `
-Method Get `
-TimeoutSec 1
} catch {
return $null
}
}
function Test-PortAvailable([int]$port) {
$listener = $null
try {
$listener = [System.Net.Sockets.TcpListener]::new(
[System.Net.IPAddress]::Loopback,
$port
)
$listener.Start()
return $true
} catch {
return $false
} finally {
if ($null -ne $listener) {
try { $listener.Stop() } catch {}
}
}
}
function Open-Dashboard([int]$port) {
if ($env:AIRPORT_GUIDE_SKIP_BROWSER -eq "1") {
return
}
$browserStart = [System.Diagnostics.ProcessStartInfo]::new()
$browserStart.FileName = "http://127.0.0.1:$port"
$browserStart.UseShellExecute = $true
[System.Diagnostics.Process]::Start($browserStart) | Out-Null
}
try {
if (-not (Test-Path -LiteralPath $nodePath -PathType Leaf)) {
throw "Missing runtime\node.exe. Extract the complete package and try again."
}
if (-not (Test-Path -LiteralPath $serverPath -PathType Leaf)) {
throw "Missing app\server.js. Extract the complete package and try again."
}
New-Item -ItemType Directory -Path $dataRoot -Force | Out-Null
# Reuse the last known instance when possible.
if (Test-Path -LiteralPath $portPath) {
$savedPort = 0
if ([int]::TryParse(
(Get-Content -LiteralPath $portPath -Raw).Trim(),
[ref]$savedPort
) -and $savedPort -ge 8890 -and $savedPort -le 8999) {
$health = Get-DashboardHealth $savedPort
if ($null -ne $health -and $health.app -eq $appId) {
Open-Dashboard $savedPort
exit 0
}
}
}
$selectedPort = 0
foreach ($port in 8890..8999) {
if (Test-PortAvailable $port) {
$selectedPort = $port
break
}
# Only query ports that are actually occupied. This avoids long startup
# delays on Windows PowerShell when unused ports take time to fail.
$health = Get-DashboardHealth $port
if ($null -ne $health -and $health.app -eq $appId) {
Set-Content -LiteralPath $portPath -Value $port -Encoding ASCII
Open-Dashboard $port
exit 0
}
}
if ($selectedPort -eq 0) {
throw "Ports 8890 through 8999 are unavailable. Close other programs and try again."
}
if (-not (Test-Path -LiteralPath $configPath)) {
Copy-Item -LiteralPath (Join-Path $appRoot "config.json") -Destination $configPath
}
$env:GUIDE_WEB_HOST = "127.0.0.1"
$env:GUIDE_WEB_PORT = [string]$selectedPort
$env:GUIDE_WEB_CONFIG_PATH = $configPath
if ([string]::IsNullOrWhiteSpace($env:GUIDE_WEB_AUTO_EXIT_MS)) {
$env:GUIDE_WEB_AUTO_EXIT_MS = "30000"
}
# PowerShell Start-Process can fail when Windows contains both Path and PATH
# environment variables. ProcessStartInfo avoids that PowerShell bug.
$nodeStart = [System.Diagnostics.ProcessStartInfo]::new()
$nodeStart.FileName = $env:ComSpec
$nodeStart.Arguments = '/d /s /c ""{0}" server.js 1>>"{1}" 2>>"{2}""' -f `
$nodePath, $outputLogPath, $errorLogPath
$nodeStart.WorkingDirectory = $appRoot
$nodeStart.UseShellExecute = $false
$nodeStart.CreateNoWindow = $true
$nodeStart.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
$nodeProcess = [System.Diagnostics.Process]::Start($nodeStart)
$ready = $false
for ($attempt = 0; $attempt -lt 40; $attempt += 1) {
Start-Sleep -Milliseconds 250
if ($nodeProcess.HasExited) {
break
}
$health = Get-DashboardHealth $selectedPort
if ($null -ne $health -and $health.app -eq $appId) {
$ready = $true
break
}
}
if (-not $ready) {
if (-not $nodeProcess.HasExited) {
Stop-Process -Id $nodeProcess.Id -Force
}
throw "The service did not start. Error log: $errorLogPath"
}
Set-Content -LiteralPath $portPath -Value $selectedPort -Encoding ASCII
Open-Dashboard $selectedPort
} catch {
try {
Add-Content `
-LiteralPath (Join-Path $dataRoot "launcher-startup-error.log") `
-Value ("{0:u} {1}" -f (Get-Date), $_.Exception.Message) `
-Encoding UTF8
} catch {}
Show-LauncherError $_.Exception.Message
exit 1
}