142 lines
7.9 KiB
PowerShell
142 lines
7.9 KiB
PowerShell
param(
|
|
[Parameter(Mandatory=$true)][ValidatePattern('^[a-f0-9]{16}$')][string]$Release,
|
|
[switch]$Remove
|
|
)
|
|
$ErrorActionPreference='Stop'
|
|
$ProgressPreference='SilentlyContinue'
|
|
[Console]::OutputEncoding=[System.Text.Encoding]::UTF8
|
|
$root='D:\NDC_MISSIONCORE\runtime\simulation'
|
|
$taskName='ndc-ai-polygon-worker'
|
|
$isaac=Join-Path $root 'isaac-sim-6.1.0'
|
|
$python=Join-Path $isaac 'kit\python\python.exe'
|
|
$worker=Join-Path $root "releases\ai-polygon-$Release\simulation\ai-polygon\realtime_worker.py"
|
|
$firewallBackup=Join-Path $root 'private\stream-firewall-backup.json'
|
|
if (Test-Path (Join-Path $root 'state\active.json')) {
|
|
throw 'Finish or reconcile the active episode before changing the service.'
|
|
}
|
|
if ($Remove) {
|
|
if (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue) {
|
|
Stop-ScheduledTask -TaskName $taskName
|
|
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
|
|
}
|
|
foreach ($name in @('ndc-ai-polygon-signal','ndc-ai-polygon-media')) {
|
|
Get-NetFirewallRule -Name $name -ErrorAction SilentlyContinue | Remove-NetFirewallRule
|
|
}
|
|
if (Test-Path $firewallBackup) {
|
|
foreach ($rule in (Get-Content $firewallBackup -Raw | ConvertFrom-Json)) {
|
|
Get-NetFirewallRule -Name $rule.Name -ErrorAction Stop | Set-NetFirewallRule -Action Block -LocalPort $rule.LocalPort
|
|
Get-NetFirewallRule -Name $rule.ExceptionName -ErrorAction SilentlyContinue | Remove-NetFirewallRule
|
|
}
|
|
}
|
|
return
|
|
}
|
|
foreach ($file in @($python,$worker,(Join-Path $root 'private\worker.token'))) {
|
|
if (!(Test-Path $file)) { throw "Missing prepared payload: $file" }
|
|
}
|
|
$payloadRoot=Join-Path $root "releases\ai-polygon-$Release"
|
|
$manifestPath=Join-Path $root "releases\ai-polygon-$Release.manifest.json"
|
|
if (!(Get-FileHash $manifestPath -Algorithm SHA256).Hash.ToLower().StartsWith($Release)) {
|
|
throw 'Manifest identity mismatch'
|
|
}
|
|
$manifest=Get-Content $manifestPath -Raw | ConvertFrom-Json
|
|
$verified=0
|
|
foreach($entry in $manifest.PSObject.Properties) {
|
|
$file=[IO.Path]::GetFullPath((Join-Path $payloadRoot $entry.Name))
|
|
if (!$file.StartsWith($payloadRoot+'\',[StringComparison]::OrdinalIgnoreCase) -or
|
|
$entry.Value -notmatch '^[a-f0-9]{64}$' -or
|
|
(Get-FileHash $file -Algorithm SHA256).Hash.ToLower() -ne $entry.Value) {
|
|
throw "Payload identity mismatch: $($entry.Name)"
|
|
}
|
|
$verified++
|
|
}
|
|
if($verified -lt 1) { throw 'Empty release manifest' }
|
|
$address=(& 'C:\Program Files\Tailscale\tailscale.exe' ip -4).Trim()
|
|
$clientAddress=($env:SSH_CONNECTION -split ' ')[0]
|
|
foreach ($value in @($address,$clientAddress)) {
|
|
$ip=[Net.IPAddress]::Parse($value)
|
|
$bytes=$ip.GetAddressBytes()
|
|
if ($bytes.Length -ne 4 -or $bytes[0] -ne 100 -or $bytes[1] -lt 64 -or $bytes[1] -gt 127) {
|
|
throw 'This installer admits only the existing private Tailscale path.'
|
|
}
|
|
}
|
|
$kit=Join-Path $isaac 'kit\python\kit.exe'
|
|
# Windows' user-prompt Block rules override scoped Allow rules. Keep those rules
|
|
# blocking every other port; for the two admitted ports retain an explicit Block
|
|
# for every remote address EXCEPT the current private operator. Preserve rollback.
|
|
$oldRules=@()
|
|
if (Test-Path $firewallBackup) { $oldRules=@(Get-Content $firewallBackup -Raw | ConvertFrom-Json) }
|
|
$clientBytes=([Net.IPAddress]::Parse($clientAddress)).GetAddressBytes()
|
|
# Windows rejects prefix /0 here. These disjoint CIDRs are the exact IPv4
|
|
# complement of the operator /32, plus both IPv6 halves. No address is exposed.
|
|
$number=[uint64]$clientBytes[0]*16777216+[uint64]$clientBytes[1]*65536+[uint64]$clientBytes[2]*256+$clientBytes[3]
|
|
$excluded=@('::/1','8000::/1')
|
|
for($prefix=1;$prefix -le 32;$prefix++) {
|
|
$bit=[uint64][math]::Pow(2,32-$prefix)
|
|
$network=([uint64]([math]::Floor($number/$bit)*$bit)) -bxor $bit
|
|
$networkBytes=[byte[]]@((($network -shr 24) -band 255),(($network -shr 16) -band 255),(($network -shr 8) -band 255),($network -band 255))
|
|
$excluded+="$([Net.IPAddress]::new($networkBytes))/$prefix"
|
|
}
|
|
try {
|
|
$blocked=Get-NetFirewallRule | Where-Object {
|
|
$_.Enabled -eq 'True' -and $_.Direction -eq 'Inbound' -and
|
|
($_.Action -eq 'Block' -or $_.Name -in @($oldRules.Name)) -and
|
|
$_.Name -notlike 'ndc-*' -and (($_ | Get-NetFirewallApplicationFilter).Program -ieq $kit)
|
|
}
|
|
foreach($rule in $blocked) {
|
|
$filter=$rule | Get-NetFirewallPortFilter
|
|
$port=if($filter.Protocol -eq 'TCP'){49100}elseif($filter.Protocol -eq 'UDP'){47998}else{throw 'Unexpected Isaac block protocol'}
|
|
$exceptionName="ndc-ai-polygon-block-other-$($filter.Protocol.ToLower())"
|
|
$backup=$oldRules | Where-Object {$_.Name -eq $rule.Name}
|
|
if(!$backup) {
|
|
if($filter.LocalPort -ne 'Any' -or ($rule | Get-NetFirewallAddressFilter).RemoteAddress -ne 'Any') {
|
|
throw 'Inspect a nonstandard Isaac firewall policy before migrating it.'
|
|
}
|
|
$oldRules+=@{Name=$rule.Name;LocalPort='Any';ExceptionName=$exceptionName}
|
|
$oldRules | ConvertTo-Json -Depth 4 | Set-Content -Encoding UTF8 $firewallBackup
|
|
}
|
|
Get-NetFirewallRule -Name $exceptionName -ErrorAction SilentlyContinue | Remove-NetFirewallRule
|
|
New-NetFirewallRule -Name $exceptionName -DisplayName $exceptionName -Group 'ndc-ai-polygon' `
|
|
-Direction Inbound -Action Block -Program $kit -Protocol $filter.Protocol -LocalPort $port `
|
|
-RemoteAddress $excluded -Profile $rule.Profile | Out-Null
|
|
$rule | Set-NetFirewallRule -Action Block -LocalPort @("1-$($port-1)","$($port+1)-65535")
|
|
}
|
|
foreach ($spec in @(@('ndc-ai-polygon-signal','TCP',49100),@('ndc-ai-polygon-media','UDP',47998))) {
|
|
$existing=Get-NetFirewallRule -Name $spec[0] -ErrorAction SilentlyContinue
|
|
if ($existing) { $existing | Remove-NetFirewallRule }
|
|
New-NetFirewallRule -Name $spec[0] -DisplayName $spec[0] -Group 'ndc-ai-polygon' `
|
|
-Direction Inbound -Action Allow -Program $kit -Protocol $spec[1] -LocalPort $spec[2] `
|
|
-LocalAddress $address -RemoteAddress $clientAddress -Profile Any | Out-Null
|
|
}
|
|
} catch {
|
|
foreach($old in $oldRules) {
|
|
Get-NetFirewallRule -Name $old.Name -ErrorAction Stop | Set-NetFirewallRule -Action Block -LocalPort $old.LocalPort
|
|
Get-NetFirewallRule -Name $old.ExceptionName -ErrorAction SilentlyContinue | Remove-NetFirewallRule
|
|
}
|
|
foreach($name in @('ndc-ai-polygon-signal','ndc-ai-polygon-media')) {
|
|
Get-NetFirewallRule -Name $name -ErrorAction SilentlyContinue | Remove-NetFirewallRule
|
|
}
|
|
throw
|
|
}
|
|
if (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue) {
|
|
Stop-ScheduledTask -TaskName $taskName
|
|
}
|
|
$arguments="-u `"$worker`" --core http://127.0.0.1:18080 --token-file `"$root\private\worker.token`" --state `"$root\state`" --isaac `"$isaac`" --stream-address $address"
|
|
$action=New-ScheduledTaskAction -Execute $python -Argument $arguments -WorkingDirectory $root
|
|
$user=[Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
$principal=New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive -RunLevel Limited
|
|
$trigger=New-ScheduledTaskTrigger -AtLogOn -User $user
|
|
$settings=New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -RestartCount 3 `
|
|
-RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) `
|
|
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
|
|
Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal `
|
|
-Trigger $trigger -Settings $settings -Force | Out-Null
|
|
Start-ScheduledTask -TaskName $taskName
|
|
[ordered]@{schema_version='missioncore.ai-polygon-service/v1'; release=$Release;
|
|
verified_files=$verified;
|
|
installed_at=[DateTime]::UtcNow.ToString('o'); task=$taskName;
|
|
lifecycle='Windows task, at user logon; independent of SSH/viewer';
|
|
control='existing operator tunnel 18080'; video='private Tailscale only';
|
|
rollback='Install-RealtimeWorker.ps1 -Release <previous> or -Remove; requires no active episode'
|
|
} | ConvertTo-Json | Set-Content -Encoding UTF8 (Join-Path $root 'realtime-service-installation.json')
|
|
Get-Content (Join-Path $root 'realtime-service-installation.json')
|