[CmdletBinding(SupportsShouldProcess)] param( [ValidateRange(1,65535)][int]$Port = 15888, [string]$TaskName = 'AIGame-AIServer', [switch]$NoStart ) $ErrorActionPreference = 'Stop' $serverRoot = $PSScriptRoot $executable = Join-Path $serverRoot 'aiserver.exe' if (-not (Test-Path -LiteralPath $executable)) { throw 'Unzip the complete release before installing.' } # Caddy may use this port on a NIC, but must not occupy wildcard/loopback addresses. $listeners = @(Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue) foreach ($listener in $listeners) { if ($listener.LocalAddress -notin @('0.0.0.0','::','127.0.0.1')) { continue } $owner = Get-CimInstance Win32_Process -Filter ("ProcessId={0}" -f $listener.OwningProcess) if ($listener.LocalAddress -eq '127.0.0.1' -and $owner.ExecutablePath -and [IO.Path]::GetFullPath($owner.ExecutablePath) -eq [IO.Path]::GetFullPath($executable)) { continue } throw "Port $Port at $($listener.LocalAddress) is occupied by $($owner.Name) (PID $($listener.OwningProcess)). Configure Caddy bind to the NIC IPv4, validate/reload Caddy, then retry." } $powershell = Join-Path $env:SystemRoot 'System32/WindowsPowerShell/v1.0/powershell.exe' $runner = Join-Path $serverRoot 'Run-Server.ps1' $arguments = '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "{0}" -Port {1}' -f $runner,$Port $action = New-ScheduledTaskAction -Execute $powershell -Argument $arguments -WorkingDirectory $serverRoot $trigger = New-ScheduledTaskTrigger -AtStartup $principal = New-ScheduledTaskPrincipal -UserId 'S-1-5-19' -LogonType ServiceAccount -RunLevel Limited $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) ` -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -MultipleInstances IgnoreNew ` -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries if ($PSCmdlet.ShouldProcess($TaskName, "Register AIServer startup task on loopback port $Port")) { $data = Join-Path $serverRoot 'data'; $logs = Join-Path $serverRoot 'logs' New-Item -ItemType Directory -Path $data,$logs -Force | Out-Null & icacls.exe $serverRoot /grant '*S-1-5-19:(OI)(CI)RX' | Out-Null if ($LASTEXITCODE -ne 0) { throw 'Cannot grant executable read permission to Local Service.' } foreach ($directory in @($data,$logs)) { & icacls.exe $directory /grant '*S-1-5-19:(OI)(CI)M' | Out-Null if ($LASTEXITCODE -ne 0) { throw "Cannot grant write permission: $directory" } } Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal ` -Settings $settings -Description 'AIGame HTTP, gameplay and WebSocket server' -Force | Out-Null if ($NoStart) { Write-Output "Installed task $TaskName; not started (-NoStart)."; return } Start-ScheduledTask -TaskName $TaskName $healthy = $false for ($attempt=0; $attempt -lt 25; $attempt++) { try { $health = Invoke-RestMethod -Uri "http://127.0.0.1:$Port/health" -TimeoutSec 1 if ($health.ok -and $health.service -eq 'aiserver' -and $health.gameProtocol -eq 1) { $healthy=$true; break } } catch { } Start-Sleep -Milliseconds 200 } if (-not $healthy) { throw "Task $TaskName was registered but AIServer is not healthy on 127.0.0.1:$Port. Check logs/ and Get-ScheduledTaskInfo -TaskName $TaskName. An HTTP 400 may mean this port is still handled by Caddy HTTPS." } Write-Output "AIServer healthy at http://127.0.0.1:$Port/health; startup task $TaskName installed." }