Initial project snapshot
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
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)"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user