Private/CuiTests/CdpClient.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Connect-CdpSession { [CmdletBinding()] param( [Parameter()][int]$Port = 9222, [Parameter()][string]$HostName = 'localhost', [Parameter()][int]$TimeoutSec = 10 ) Set-StrictMode -Version Latest $targets = Invoke-RestMethod -Uri "http://${HostName}:${Port}/json/list" -TimeoutSec $TimeoutSec $target = @($targets | Where-Object { $_.type -eq 'page' }) | Select-Object -First 1 if (-not $target) { throw (New-ActionableError -Goal 'Connect to Electron app via CDP' ` -Problem "No page target found on port $Port" ` -Location 'Connect-CdpSession' ` -NextSteps @("Ensure the Electron app is running with --remote-debugging-port=$Port", 'Run: npx electron . --remote-debugging-port=9222')) } $wsUrl = $target.webSocketDebuggerUrl $ws = [System.Net.WebSockets.ClientWebSocket]::new() $cts = [System.Threading.CancellationTokenSource]::new([TimeSpan]::FromSeconds($TimeoutSec)) try { $ws.ConnectAsync([Uri]$wsUrl, $cts.Token).GetAwaiter().GetResult() } catch { throw (New-ActionableError -Goal 'Connect CDP WebSocket' ` -Problem "WebSocket connect failed: $($_.Exception.Message)" ` -Location 'Connect-CdpSession' ` -NextSteps @("Verify the app is still running on port $Port", "Check that $wsUrl is accessible")) } [PSCustomObject]@{ PSTypeName = 'CdpSession' WebSocket = $ws NextId = 1 Buffer = [byte[]]::new(4194304) Port = $Port } } function Invoke-CdpCommand { [CmdletBinding()] param( [Parameter(Mandatory)][PSCustomObject]$Session, [Parameter(Mandatory)][string]$Method, [Parameter()][hashtable]$Params = @{}, [Parameter()][int]$TimeoutSec = 10 ) Set-StrictMode -Version Latest $id = $Session.NextId $Session.NextId = $Session.NextId + 1 $msg = @{ id = $id; method = $Method; params = $Params } | ConvertTo-Json -Depth 10 -Compress $bytes = [System.Text.Encoding]::UTF8.GetBytes($msg) $segment = [System.ArraySegment[byte]]::new($bytes) $cts = [System.Threading.CancellationTokenSource]::new([TimeSpan]::FromSeconds($TimeoutSec)) $Session.WebSocket.SendAsync( $segment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, $cts.Token ).GetAwaiter().GetResult() | Out-Null while ($true) { $response = Read-CdpMessage -Session $Session -TimeoutSec $TimeoutSec if ($response.PSObject.Properties['id'] -and $response.id -eq $id) { if ($response.PSObject.Properties['error']) { throw "CDP error ($Method): $($response.error.message)" } return $response.result } } } function Read-CdpMessage { [CmdletBinding()] param( [Parameter(Mandatory)][PSCustomObject]$Session, [Parameter()][int]$TimeoutSec = 10 ) $cts = [System.Threading.CancellationTokenSource]::new([TimeSpan]::FromSeconds($TimeoutSec)) $sb = [System.Text.StringBuilder]::new() do { $segment = [System.ArraySegment[byte]]::new($Session.Buffer) $result = $Session.WebSocket.ReceiveAsync($segment, $cts.Token).GetAwaiter().GetResult() $sb.Append([System.Text.Encoding]::UTF8.GetString($Session.Buffer, 0, $result.Count)) | Out-Null } while (-not $result.EndOfMessage) $sb.ToString() | ConvertFrom-Json } function Disconnect-CdpSession { [CmdletBinding()] param( [Parameter(Mandatory)][PSCustomObject]$Session ) if ($Session.WebSocket.State -eq [System.Net.WebSockets.WebSocketState]::Open) { $cts = [System.Threading.CancellationTokenSource]::new([TimeSpan]::FromSeconds(5)) try { $Session.WebSocket.CloseAsync( [System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure, '', $cts.Token ).GetAwaiter().GetResult() | Out-Null } catch { Write-Verbose "CDP close warning: $($_.Exception.Message)" } } $Session.WebSocket.Dispose() } |