$ErrorActionPreference = 'Stop' $project = Split-Path -Parent $PSScriptRoot $assets = @{ '/api' = Join-Path $project 'build/linux-arm64/tera-clone-demo-api' '/sessiond' = Join-Path $project 'build/linux-arm64/tera-sessiond' '/web' = Join-Path $project 'build/linux-arm64/tera-clone-demo-web' '/directstream.test' = Join-Path $project 'build/linux-arm64/directstream.test' '/portservice.test' = Join-Path $project 'build/linux-arm64/portservice.test' } $hashes = @{} foreach ($key in $assets.Keys) { $hashes[$key] = (Get-FileHash -LiteralPath $assets[$key] -Algorithm SHA256).Hash.ToLowerInvariant() } $manifest = (($assets.Keys | Sort-Object | ForEach-Object { $hashes[$_] + ' ' + $_.Substring(1) }) -join "`n") + "`n" $script = [IO.File]::ReadAllText((Join-Path $PSScriptRoot 'deploy-shared-sessions.sh')).Replace("`r`n", "`n") $rule = 'TERA-shared-sessions-' + [guid]::NewGuid().ToString('N') $listener = New-Object System.Net.HttpListener $listener.Prefixes.Add('http://192.168.0.100:9001/') $serial = New-Object System.IO.Ports.SerialPort 'COM4',115200,'None',8,'One' $serial.Encoding = [Text.Encoding]::UTF8 Start-Transcript -Path (Join-Path $project 'runtime/shared-sessions-transfer.log') -Force | Out-Null try { New-NetFirewallRule -DisplayName $rule -Direction Inbound -Action Allow -Protocol TCP -LocalPort 9001 -RemoteAddress 192.168.0.223 | Out-Null $listener.Start() $serial.Open() $serial.DiscardInBuffer() $serial.WriteLine('wget -q -T 15 -O /tmp/deploy-shared-sessions.sh http://192.168.0.100:9001/deploy.sh && sh /tmp/deploy-shared-sessions.sh') $deadline = [DateTime]::UtcNow.AddSeconds(240) $output = '' $pending = $listener.GetContextAsync() while ([DateTime]::UtcNow -lt $deadline) { if ($pending.IsCompleted) { $context = $pending.GetAwaiter().GetResult() $bytes = $null $path = $context.Request.Url.AbsolutePath if ($context.Request.RemoteEndPoint.Address.ToString() -ne '192.168.0.223') { $context.Response.StatusCode = 403 } elseif ($assets.ContainsKey($path)) { $bytes = [IO.File]::ReadAllBytes($assets[$path]) } elseif ($path -eq '/deploy.sh') { $bytes = [Text.Encoding]::UTF8.GetBytes($script) } elseif ($path -eq '/SHA256SUMS') { $bytes = [Text.Encoding]::UTF8.GetBytes($manifest) } else { $context.Response.StatusCode = 404 } if ($null -ne $bytes) { $context.Response.ContentLength64 = $bytes.Length $context.Response.OutputStream.Write($bytes, 0, $bytes.Length) } $context.Response.Close() $pending = $listener.GetContextAsync() } $chunk = $serial.ReadExisting() if ($chunk) { $output += $chunk; Write-Host -NoNewline $chunk } if ($output.Contains('SHARED_SESSIONS_ROLLED_BACK')) { throw 'Deployment rolled back.' } if ($output.Contains('SHARED_SESSIONS_DEPLOY_OK')) { break } Start-Sleep -Milliseconds 100 } if (-not $output.Contains('SHARED_SESSIONS_DEPLOY_OK')) { throw 'Deployment verification incomplete; inspect console.' } foreach ($key in @('/api','/sessiond','/web')) { if (-not $output.Contains($hashes[$key])) { throw "Missing installed hash for $key" } } $response = Invoke-WebRequest -UseBasicParsing -TimeoutSec 10 'http://192.168.0.223/static/js/integration.js?v=20260916-shared-port-sessions-1' if ($response.Content -cne [IO.File]::ReadAllText((Join-Path $project 'frontend/web/static/js/integration.js'))) { throw 'Served integration.js differs from local file.' } Write-Output 'VERIFIED API, sessiond, web hashes and exact browser script' } finally { if ($serial.IsOpen) { $serial.Close() } $serial.Dispose() $listener.Close() Remove-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue Stop-Transcript | Out-Null }