$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 }