[CmdletBinding()] param( [string]$Version = "", [string]$BaseUrl = "https://refugebot.com/downloads/refugebot-monitor", [string]$InstallDir = "$env:ProgramFiles\RefugeBot Monitor", [string]$DataDir = "$env:ProgramData\RefugeBot\Monitor", [switch]$Maintenance ) $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 function Test-Administrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Invoke-MonitorServiceAction { param( [Parameter(Mandatory=$true)][string]$Executable, [Parameter(Mandatory=$true)][string]$ConfigPath, [Parameter(Mandatory=$true)][ValidateSet("install", "uninstall", "start", "stop")][string]$Action ) & $Executable --config $ConfigPath $Action if ($LASTEXITCODE -ne 0) { throw "RefugeBot Monitor service action '$Action' failed with exit code $LASTEXITCODE." } } function Wait-MonitorHealth { param( [Parameter(Mandatory=$true)][string]$ExpectedVersion, [int]$TimeoutSeconds = 90 ) $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) do { try { $health = Invoke-RestMethod -Uri "http://127.0.0.1:8787/api/health" -TimeoutSec 4 if ($health.ok -and $health.version -eq $ExpectedVersion) { return $true } } catch { } Start-Sleep -Seconds 2 } while ([DateTime]::UtcNow -lt $deadline) return $false } if (-not $IsWindows -and $PSVersionTable.PSEdition -eq "Core") { throw "This installer is for Windows. On Linux, use install.sh." } if (-not (Test-Administrator)) { throw "Open PowerShell as Administrator, then run the install command again." } $BaseUrl = $BaseUrl.TrimEnd("/") if ([string]::IsNullOrWhiteSpace($Version)) { $Version = (Invoke-RestMethod "$BaseUrl/latest.txt").ToString().Trim() } if ($Version -notmatch '^\d+\.\d+\.\d+([-.][A-Za-z0-9.]+)?$') { throw "The published monitor version is not valid." } $service = Get-Service -Name "RefugeBotMonitor" -ErrorAction SilentlyContinue if ($service -and $service.Status -eq "Running" -and -not $Maintenance) { $current = Join-Path $InstallDir "refugebot-monitor.exe" if (Test-Path $current) { $installedVersion = (& $current version).Trim() if ($installedVersion -eq $Version) { if (Wait-MonitorHealth -ExpectedVersion $Version -TimeoutSeconds 12) { Write-Host "RefugeBot Monitor $Version is already installed, running, and healthy. Dashboard: http://127.0.0.1:8787/" exit 0 } throw "The Monitor service reports Running but its dashboard did not pass the local health check. Re-run this installer with -Maintenance to repair it safely." } } throw "RefugeBot Monitor is running. Re-run with -Maintenance during a planned server maintenance window to upgrade it safely." } $releaseUrl = "$BaseUrl/releases/$Version" $archiveName = "refugebot-monitor-$Version-windows-amd64.zip" $work = Join-Path ([IO.Path]::GetTempPath()) ("refugebot-monitor-" + [guid]::NewGuid().ToString("N")) $archive = Join-Path $work $archiveName $checksums = Join-Path $work "SHA256SUMS.txt" $stage = Join-Path $work "stage" try { New-Item -ItemType Directory -Force -Path $stage | Out-Null Invoke-WebRequest "$releaseUrl/$archiveName" -OutFile $archive Invoke-WebRequest "$releaseUrl/SHA256SUMS.txt" -OutFile $checksums $line = Get-Content $checksums | Where-Object { $_ -match [regex]::Escape($archiveName) } | Select-Object -First 1 if (-not $line -or $line -notmatch '^([A-Fa-f0-9]{64})\s+') { throw "The published checksum file does not contain $archiveName." } $expected = $Matches[1].ToUpperInvariant() $actual = (Get-FileHash -Algorithm SHA256 $archive).Hash.ToUpperInvariant() if ($actual -ne $expected) { throw "The monitor package checksum did not match. No files were installed." } Expand-Archive -LiteralPath $archive -DestinationPath $stage -Force $sourceExe = Get-ChildItem $stage -Recurse -File -Filter "refugebot-monitor-windows-amd64.exe" | Select-Object -First 1 $sourceTray = Get-ChildItem $stage -Recurse -File -Filter "refugebot-monitor-tray-windows-amd64.exe" | Select-Object -First 1 $sourceUpdater = Get-ChildItem $stage -Recurse -File -Filter "update.ps1" | Select-Object -First 1 if (-not $sourceExe -or -not $sourceTray -or -not $sourceUpdater) { throw "The monitor executable, tray companion, or updater is missing from the published package." } $packageVersion = (& $sourceExe.FullName version).Trim() if ($packageVersion -ne $Version) { throw "Package version $packageVersion does not match published version $Version." } if ($service -and $service.Status -eq "Running") { & (Join-Path $InstallDir "refugebot-monitor.exe") --config (Join-Path $DataDir "monitor.yaml") stop $service.WaitForStatus("Stopped", [TimeSpan]::FromMinutes(3)) } New-Item -ItemType Directory -Force -Path $InstallDir, $DataDir | Out-Null Copy-Item -LiteralPath $sourceExe.FullName -Destination (Join-Path $InstallDir "refugebot-monitor.exe") -Force Copy-Item -LiteralPath $sourceUpdater.FullName -Destination (Join-Path $InstallDir "update.ps1") -Force $tray = Join-Path $InstallDir "refugebot-monitor-tray-$Version.exe" Copy-Item -LiteralPath $sourceTray.FullName -Destination $tray -Force $runKey = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Run" New-Item -Path $runKey -Force | Out-Null New-ItemProperty -LiteralPath $runKey -Name "RefugeBot Monitor" -Value ("`"$tray`" --startup") -PropertyType String -Force | Out-Null foreach ($folder in @("README.md", "configs", "docs")) { $source = Join-Path $stage $folder if (Test-Path $source) { Copy-Item -LiteralPath $source -Destination $InstallDir -Recurse -Force } } $exe = Join-Path $InstallDir "refugebot-monitor.exe" $config = Join-Path $DataDir "monitor.yaml" if (-not (Test-Path $config)) { Write-Host "Opening the private RefugeBot setup page. This tool is optional if your host already provides monitoring and restarts." $setup = Start-Process -FilePath $exe -ArgumentList @("--config", $config, "--setup-listen", "127.0.0.1:8787", "setup") -WindowStyle Hidden -PassThru Start-Process "http://127.0.0.1:8787/" $deadline = [DateTime]::UtcNow.AddMinutes(30) while (-not (Test-Path $config) -and -not $setup.HasExited -and [DateTime]::UtcNow -lt $deadline) { Start-Sleep -Seconds 2 $setup.Refresh() } if (-not $setup.HasExited) { Stop-Process -Id $setup.Id -ErrorAction SilentlyContinue } if (-not (Test-Path $config)) { throw "Setup did not finish. Run this installer again when you are ready; downloaded server files will be preserved." } } # Always rebuild an existing stopped service registration. Older installers # could leave ImagePath pointing at a removed staging/versioned executable; # copying a new binary alone cannot repair that SCM entry. $service = Get-Service -Name "RefugeBotMonitor" -ErrorAction SilentlyContinue if ($service) { if ($service.Status -ne "Stopped") { throw "RefugeBot Monitor did not stop cleanly, so its Windows service registration was not changed." } $service.Dispose() Invoke-MonitorServiceAction -Executable $exe -ConfigPath $config -Action "uninstall" $deleteDeadline = [DateTime]::UtcNow.AddSeconds(30) while ((Get-Service -Name "RefugeBotMonitor" -ErrorAction SilentlyContinue) -and [DateTime]::UtcNow -lt $deleteDeadline) { Start-Sleep -Seconds 1 } if (Get-Service -Name "RefugeBotMonitor" -ErrorAction SilentlyContinue) { throw "The old RefugeBot Monitor Windows service is still pending deletion. Close Services and retry in a few seconds." } } Invoke-MonitorServiceAction -Executable $exe -ConfigPath $config -Action "install" Invoke-MonitorServiceAction -Executable $exe -ConfigPath $config -Action "start" $service = Get-Service -Name "RefugeBotMonitor" -ErrorAction Stop $service.WaitForStatus("Running", [TimeSpan]::FromMinutes(2)) if (-not (Wait-MonitorHealth -ExpectedVersion $Version)) { throw "RefugeBot Monitor started but http://127.0.0.1:8787/api/health did not report version $Version. The installer will not claim success." } if ([Environment]::UserInteractive) { Start-Process -FilePath $tray -ArgumentList "--startup" -WindowStyle Hidden } Write-Host "RefugeBot Monitor $Version is installed and running. Dashboard: http://127.0.0.1:8787/" Write-Host "A RefugeBot Monitor tray icon will start with Windows. Right-click it to open the dashboard, inspect status, restart the service, or reach the Update Center." Write-Host "Docker Desktop is not required and was not installed. Docker attachment remains an optional watch-only path for servers you already run in containers." Write-Host "Game ports still need to be allowed by Windows Firewall and forwarded by your router when hosting from home. The dashboard includes a port guide." } finally { Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue }