Files
rdp-proxy/scripts/smoke/drive-visibility-smoke.ps1
T
2026-04-28 22:29:50 +03:00

261 lines
11 KiB
PowerShell

param(
[string]$BackendApiBase = "http://192.168.200.61:8080/api/v1",
[string]$BackendWsBase = "ws://192.168.200.61:8080/api/v1/gateway/ws",
[string]$DockerSshAlias = "docker-test",
[string]$WorkerContainerName = "rap_worker_smoke",
[string]$WorkerImage = "rap-rdp-worker:stage5-drive-visible",
[string]$ResourceName = "Windows Smoke Default Resource",
[string]$Email = "windows-smoke@example.local",
[string]$Password = "SmokePass!123",
[string]$OutputFrame = "artifacts/stage5-drive-visibility-frame.bmp"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Write-SmokeLog([string]$Message) {
Write-Host "[$(Get-Date -Format o)] $Message"
}
function Invoke-RemoteSqlScalar([string]$Sql) {
$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Sql))
$output = ssh $DockerSshAlias "printf '%s' '$encoded' | base64 -d | docker exec -i rap_postgres psql -U rap_user -d remote_access_platform -t -A -v ON_ERROR_STOP=1"
return (($output | Select-Object -Last 1) -as [string]).Trim()
}
function Invoke-Api([string]$Method, [string]$Path, [object]$Body = $null) {
$uri = "$BackendApiBase/$Path"
if ($null -eq $Body) {
return Invoke-RestMethod -Method $Method -Uri $uri
}
return Invoke-RestMethod -Method $Method -Uri $uri -ContentType "application/json" -Body ($Body | ConvertTo-Json -Depth 16)
}
function Restart-SmokeWorker {
$cmd = "docker rm -f $WorkerContainerName 2>/dev/null || true; docker run -d --name $WorkerContainerName --network host -e RDP_WORKER_ID=rdp-worker-1 -e RDP_WORKER_REDIS_HOST=127.0.0.1 -e RDP_WORKER_REDIS_PORT=6379 -e RDP_WORKER_TRANSFER_ROOT=/tmp/rap-rdp-worker-transfers -e RDP_WORKER_CAPABILITIES=adaptive-quality,dirty-rects,clipboard,file-transfer $WorkerImage"
$output = ssh $DockerSshAlias $cmd
Write-SmokeLog "worker restart output=$($output -join ' ')"
Start-Sleep -Seconds 5
}
function New-GatewayWebSocket([string]$AttachToken) {
$ws = [Net.WebSockets.ClientWebSocket]::new()
$uri = [Uri]::new("${BackendWsBase}?attach_token=$([Uri]::EscapeDataString($AttachToken))")
$null = $ws.ConnectAsync($uri, [Threading.CancellationToken]::None).GetAwaiter().GetResult()
return $ws
}
function Receive-Envelope([Net.WebSockets.ClientWebSocket]$WebSocket, [int]$TimeoutMilliseconds = 30000) {
$buffer = [byte[]]::new(1024 * 1024)
$segment = [ArraySegment[byte]]::new($buffer)
$stream = [IO.MemoryStream]::new()
while ($true) {
$cts = [Threading.CancellationTokenSource]::new($TimeoutMilliseconds)
try {
$result = $WebSocket.ReceiveAsync($segment, $cts.Token).GetAwaiter().GetResult()
}
finally {
$cts.Dispose()
}
if ($result.MessageType -eq [Net.WebSockets.WebSocketMessageType]::Close) {
throw "websocket closed"
}
$stream.Write($buffer, 0, $result.Count)
if ($result.EndOfMessage) {
break
}
}
return ([Text.Encoding]::UTF8.GetString($stream.ToArray()) | ConvertFrom-Json)
}
function Receive-EnvelopeOfType([Net.WebSockets.ClientWebSocket]$WebSocket, [string[]]$Types, [int]$TimeoutSeconds = 30) {
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
$envelope = Receive-Envelope -WebSocket $WebSocket -TimeoutMilliseconds 10000
if ($Types -contains [string]$envelope.type) {
return $envelope
}
}
throw "timed out waiting for envelope type: $($Types -join ',')"
}
function Send-Envelope([Net.WebSockets.ClientWebSocket]$WebSocket, [object]$Envelope) {
$json = $Envelope | ConvertTo-Json -Depth 16 -Compress
$bytes = [Text.Encoding]::UTF8.GetBytes($json)
$null = $WebSocket.SendAsync([ArraySegment[byte]]::new($bytes), [Net.WebSockets.WebSocketMessageType]::Text, $true, [Threading.CancellationToken]::None).GetAwaiter().GetResult()
}
function Wait-SessionActive([Net.WebSockets.ClientWebSocket]$WebSocket) {
$deadline = (Get-Date).AddSeconds(90)
while ((Get-Date) -lt $deadline) {
$envelope = Receive-Envelope -WebSocket $WebSocket -TimeoutMilliseconds 30000
Write-SmokeLog "ws event type=$($envelope.type) state=$($envelope.payload.state)"
if ($envelope.type -eq "session.state" -and $envelope.payload.state -eq "active") {
return
}
}
throw "session did not become active"
}
function Send-Key([Net.WebSockets.ClientWebSocket]$WebSocket, [int]$ScanCode, [bool]$Extended = $false) {
Send-Envelope $WebSocket @{ type = "input"; payload = @{ kind = "keyboard"; action = "key_down"; scan_code = $ScanCode; is_extended = $Extended } }
Start-Sleep -Milliseconds 60
Send-Envelope $WebSocket @{ type = "input"; payload = @{ kind = "keyboard"; action = "key_up"; scan_code = $ScanCode; is_extended = $Extended } }
}
function Send-Chord([Net.WebSockets.ClientWebSocket]$WebSocket, [int]$ModifierScanCode, [bool]$ModifierExtended, [int]$KeyScanCode) {
Send-Envelope $WebSocket @{ type = "input"; payload = @{ kind = "keyboard"; action = "key_down"; scan_code = $ModifierScanCode; is_extended = $ModifierExtended } }
Start-Sleep -Milliseconds 80
Send-Key $WebSocket $KeyScanCode $false
Start-Sleep -Milliseconds 80
Send-Envelope $WebSocket @{ type = "input"; payload = @{ kind = "keyboard"; action = "key_up"; scan_code = $ModifierScanCode; is_extended = $ModifierExtended } }
}
function Upload-TextFile([Net.WebSockets.ClientWebSocket]$WebSocket) {
$bytes = [Text.Encoding]::UTF8.GetBytes("Stage 5.1.1 RAP_Transfers visible file`r`nрусский текст`r`n")
$transferId = [guid]::NewGuid().ToString("D")
Send-Envelope $WebSocket @{
type = "file_upload.start"
payload = @{
direction = "client_to_server"
transfer_id = $transferId
file_name = "stage5-upload-text.txt"
file_size = [int64]$bytes.Length
total_chunks = 1
content_hash = ""
}
}
$started = Receive-EnvelopeOfType $WebSocket @("file_upload.progress", "file_upload.blocked") 20
if ($started.type -ne "file_upload.progress") {
throw "upload start was blocked"
}
Send-Envelope $WebSocket @{
type = "file_upload.chunk"
payload = @{
direction = "client_to_server"
transfer_id = $transferId
chunk_index = 0
offset = 0
chunk_size = $bytes.Length
chunk_bytes = [Convert]::ToBase64String($bytes)
}
}
$done = Receive-EnvelopeOfType $WebSocket @("file_upload.progress", "file_upload.blocked") 20
Write-SmokeLog "upload result type=$($done.type) status=$($done.payload.status)"
if ($done.type -ne "file_upload.progress" -or $done.payload.status -ne "completed") {
throw "upload did not complete"
}
}
function Write-BgraFrameToBmp([string]$SessionId, [string]$Path) {
$state = $null
$data = $null
$width = 0
$height = 0
$imageSize = 0
$deadline = (Get-Date).AddSeconds(20)
do {
$json = ssh $DockerSshAlias "docker exec rap_redis redis-cli --raw GET live:session:$SessionId"
if ($json) {
$state = ($json -join "`n") | ConvertFrom-Json
$width = [int]$state.render_width
$height = [int]$state.render_height
$stride = $width * 4
$imageSize = $stride * $height
if ($width -gt 0 -and $height -gt 0 -and -not [string]::IsNullOrWhiteSpace([string]$state.render_frame_data)) {
$data = [Convert]::FromBase64String([string]$state.render_frame_data)
if ($data.Length -ge $imageSize) {
break
}
Write-SmokeLog "waiting for full framebuffer in live state bytes=$($data.Length) expected=$imageSize sequence=$($state.render_frame_sequence)"
}
}
Start-Sleep -Milliseconds 500
} while ((Get-Date) -lt $deadline)
if ($null -eq $data -or $data.Length -lt $imageSize) {
$actualBytes = if ($null -eq $data) { 0 } else { $data.Length }
throw "frame data too small or unavailable bytes=$actualBytes expected=$imageSize"
}
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Path) | Out-Null
$fs = [IO.File]::Open($Path, [IO.FileMode]::Create, [IO.FileAccess]::Write)
try {
$writer = [IO.BinaryWriter]::new($fs)
$fileSize = 14 + 40 + $imageSize
$writer.Write([byte][char]'B')
$writer.Write([byte][char]'M')
$writer.Write([uint32]$fileSize)
$writer.Write([uint16]0)
$writer.Write([uint16]0)
$writer.Write([uint32]54)
$writer.Write([uint32]40)
$writer.Write([int32]$width)
$writer.Write([int32](-$height))
$writer.Write([uint16]1)
$writer.Write([uint16]32)
$writer.Write([uint32]0)
$writer.Write([uint32]$imageSize)
$writer.Write([int32]2835)
$writer.Write([int32]2835)
$writer.Write([uint32]0)
$writer.Write([uint32]0)
$writer.Write($data, 0, $imageSize)
}
finally {
$fs.Dispose()
}
Write-SmokeLog "wrote frame $Path"
}
Restart-SmokeWorker
$login = Invoke-Api Post "auth/login" @{
email = $Email
password = $Password
trust_device = $true
device_fingerprint = "stage5-drive-visibility"
device_label = "stage5-drive-visibility"
}
$userId = [string]$login.user.id
if ([string]::IsNullOrWhiteSpace($userId)) { $userId = [string]$login.user.ID }
$deviceId = [string]$login.device.id
if ([string]::IsNullOrWhiteSpace($deviceId)) { $deviceId = [string]$login.device.ID }
$escapedResourceName = $ResourceName.Replace("'", "''")
$resourceId = Invoke-RemoteSqlScalar "SELECT id::text FROM resources WHERE name = '$escapedResourceName' LIMIT 1;"
$null = Invoke-RemoteSqlScalar "UPDATE resource_policies SET file_transfer_mode = 'client_to_server', file_transfer_enabled = TRUE, clipboard_mode = 'client_to_server', clipboard_enabled = TRUE, updated_at = NOW() WHERE resource_id = '$resourceId'; SELECT 'ok';"
$session = Invoke-Api Post "sessions" @{
resource_id = $resourceId
user_id = $userId
device_id = $deviceId
}
$sessionId = [string]$session.session.id
if ([string]::IsNullOrWhiteSpace($sessionId)) { $sessionId = [string]$session.session.ID }
$attachToken = [string]$session.attach_token.token
Write-SmokeLog "visibility session=$sessionId"
$ws = New-GatewayWebSocket $attachToken
try {
Wait-SessionActive $ws
Upload-TextFile $ws
Start-Sleep -Seconds 2
Send-Envelope $ws @{ type = "clipboard"; payload = @{ direction = "client_to_server"; text = "\\tsclient\RAP_Transfers\stage5-upload-text.txt"; sequence_id = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds(); origin = "visibility-smoke"; content_hash = "" } }
Start-Sleep -Seconds 1
Send-Chord $ws 91 $true 19
Start-Sleep -Seconds 1
Send-Chord $ws 29 $false 47
Start-Sleep -Milliseconds 300
Send-Key $ws 28 $false
Start-Sleep -Seconds 5
Write-BgraFrameToBmp $sessionId $OutputFrame
}
finally {
$ws.Dispose()
try {
Invoke-Api Post "sessions/$sessionId/terminate" @{ user_id = $userId; reason = "stage5_drive_visibility_cleanup" } | Out-Null
}
catch {
Write-SmokeLog "terminate skipped: $($_.Exception.Message)"
}
}