PSChromeDevToolsServer.psm1
|
# $global:DebugPreference = 'Continue' $script:Powershell = $null function Initialize { $script:Powershell = [powershell]::Create() $script:Powershell.AddScript( { function New-UnboundClassInstance ([Type] $type, [object[]] $arguments) { [activator]::CreateInstance($type, $arguments) } }.Ast.GetScriptBlock() ).Invoke() $script:Powershell.Commands.Clear() } function New-UnboundClassInstance ([Type] $type, [object[]] $arguments = $null) { if ($null -eq $script:Powershell) { Initialize } try { if ($null -eq $arguments) { $arguments = @() } $result = $script:Powershell.AddCommand('New-UnboundClassInstance'). AddParameter('type', $type). AddParameter('arguments', $arguments). Invoke() return $result } finally { $script:Powershell.Commands.Clear() } } function ConvertTo-Delegate { [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline, Position = 0)] [System.Management.Automation.PSMethod[]]$Method, [Parameter(Mandatory)] [object]$Target ) process { $reflectionMethod = if ($Target.GetType().Name -eq 'PSCustomObject') { $Target.psobject.GetType().GetMethod($Method.Name) } else { $Target.GetType().GetMethod($Method.Name) } $parameterTypes = [System.Linq.Enumerable]::Select($reflectionMethod.GetParameters(), [func[object, object]] { $args[0].parametertype }) $concatMethodTypes = $parameterTypes + $reflectionMethod.ReturnType $delegateType = [System.Linq.Expressions.Expression]::GetDelegateType($concatMethodTypes) $delegate = [delegate]::CreateDelegate($delegateType, $Target, $reflectionMethod.Name) $delegate } } class CdpPage { # it's more dictionary now than property # did not want to use monitor.enter/exit [string]$TargetId [string]$Url [string]$Title [string]$BrowserContextId [int]$ProcessId CdpPage($TargetId, $Url, $Title, $BrowserContextId) { $this.TargetId = $TargetId $this.Url = $Url $this.Title = $Title $this.BrowserContextId = $BrowserContextId $this.TargetInfo.SessionId = $null $this.LoadingEvents.IsLoading = $false $this.LoadingEvents.DomContentEventFired = 0 $this.LoadingEvents.LoadEventFired = 0 $this.LoadingEvents.FrameStoppedLoading = 0 $this.LoadingEvents.FrameStartedLoading = 0 $this.PageInfo.RuntimeUniqueId = $null $this.PageInfo.ObjectId = $null $this.PageInfo.Node = $null $this.PageInfo.BoxModel = $null } [System.Collections.Concurrent.ConcurrentDictionary[string, object]]$TargetInfo = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new() [System.Collections.Concurrent.ConcurrentDictionary[string, object]]$LoadingEvents = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new() [System.Collections.Concurrent.ConcurrentDictionary[string, object]]$Frames = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new() [System.Collections.Concurrent.ConcurrentDictionary[string, object]]$PageInfo = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new() } class CdpFrame { $FrameId $ParentFrameId $SessionId $RuntimeUniqueId CdpFrame ($FrameId, $SessionId, $ParentFrameId) { $this.LoadingEvents.FrameStartedLoading = 0 $this.LoadingEvents.FrameStoppedLoading = 0 $this.LoadingEvents.IsLoading = $false $this.FrameId = $FrameId $this.ParentFrameId = $ParentFrameId $this.SessionId = $SessionId $this.RuntimeUniqueId = $null } CdpFrame ($FrameId, $SessionId) { $this.LoadingEvents.FrameStartedLoading = 0 $this.LoadingEvents.FrameStoppedLoading = 0 $this.LoadingEvents.IsLoading = $false $this.FrameId = $FrameId $this.ParentFrameId = $null $this.SessionId = $SessionId $this.RuntimeUniqueId = $null } # so far not needed. # $FrameInfo = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new() $LoadingEvents = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new() } # [NoRunspaceAffinity()] class CdpEventHandler { [System.Collections.Generic.Dictionary[string, object]]$SharedState [hashtable]$EventHandlers CdpEventHandler([System.Collections.Concurrent.ConcurrentDictionary[string, object]]$SharedState) { $this.SharedState = $SharedState $this.InitializeHandlers() } hidden [void]InitializeHandlers() { $this.EventHandlers = @{ 'Page.domContentEventFired' = $this.DomContentEventFired 'Page.frameAttached' = $this.FrameAttached 'Page.frameDetached' = $this.FrameDetached 'Page.frameNavigated' = $this.FrameNavigated 'Page.loadEventFired' = $this.LoadEventFired 'Page.frameRequestedNavigation' = $this.FrameRequestedNavigation 'Page.frameStartedLoading' = $this.FrameStartedLoading 'Page.frameStartedNavigating' = $this.FrameStartedNavigating 'Page.frameStoppedLoading' = $this.FrameStoppedLoading 'Page.navigatedWithinDocument' = $this.NavigatedWithinDocument 'Target.targetCreated' = $this.TargetCreated 'Target.targetDestroyed' = $this.TargetDestroyed 'Target.targetInfoChanged' = $this.TargetInfoChanged 'Target.attachedToTarget' = $this.AttachedToTarget 'Target.detachedFromTarget' = $this.DetachedFromTarget 'Runtime.bindingCalled' = $this.BindingCalled 'Runtime.executionContextsCleared' = $this.ExecutionContextsCleared 'Runtime.executionContextCreated' = $this.ExecutionContextCreated } } [void]ProcessEvent($Response) { if ($null -eq $Response.method) { return } $handler = $this.EventHandlers[$Response.method] if ($handler) { $handler.Invoke($Response) } # else { # Write-Debug ('Unprocessed Event: ({0})' -f $Response.method) # } } hidden [void]DomContentEventFired($Response) { $CdpPage = $this.GetPageBySessionId($Response.sessionId) $CdpPage.LoadingEvents.AddOrUpdate('DomContentEventFired', 1, { param($Key, $OldValue) $OldValue + 1 }) if ($CdpPage.LoadingEvents.LoadEventFired -eq $CdpPage.LoadingEvents.DomContentEventFired) { $CdpPage.LoadingEvents.AddOrUpdate('IsLoading', $false, { param($Key, $OldValue) $false }) } $Callback = $this.SharedState.Callbacks['OnDomContentEventFired'] if ($Callback) { $Callback.Invoke($Response) } } hidden [void]FrameAttached($Response) { $CdpPage = $this.GetPageBySessionId($Response.sessionId) $Frame = $CdpPage.Frames.GetOrAdd($Response.params.frameId, [CdpFrame]::new($Response.params.frameId, $Response.sessionId, $Response.params.parentFrameId)) $Frame.ParentFrameId = $Response.params.parentFrameId $Callback = $this.SharedState.Callbacks['OnFrameAttached'] if ($Callback) { $Callback.Invoke($Response) } } hidden [void]FrameDetached($Response) { $CdpPage = $this.GetPageBySessionId($Response.sessionId) if ($CdpPage -and $Response.params.reason -eq 'remove') { $null = $CdpPage.Frames.TryRemove($Response.params.frameId, [ref]$null) } $Callback = $this.SharedState.Callbacks['OnFrameDetached'] if ($Callback) { $Callback.Invoke($Response) } } hidden [void]FrameNavigated($Response) { # Write-Debug ('Frame Navigated: ({0})' -f ($Response | ConvertTo-Json -Depth 10)) $Callback = $this.SharedState.Callbacks['OnFrameNavigated'] if ($Callback) { $Callback.Invoke($Response) } } hidden [void]LoadEventFired($Response) { $CdpPage = $this.GetPageBySessionId($Response.sessionId) $CdpPage.LoadingEvents.AddOrUpdate('LoadEventFired', 1, { param($Key, $OldValue) $OldValue + 1 }) # if ($CdpPage.LoadingEvents.LoadEventFired -eq $CdpPage.LoadingEvents.DomContentEventFired) { $CdpPage.LoadingEvents.AddOrUpdate('IsLoading', $false, { param($Key, $OldValue) $false }) # } $Callback = $this.SharedState.Callbacks['OnLoadEventFired'] if ($Callback) { $Callback.Invoke($Response) } } hidden [void]FrameRequestedNavigation($Response) { # Write-Debug ('Frame Requested Navigation: ({0})' -f ($Response | ConvertTo-Json -Depth 10)) $Callback = $this.SharedState.Callbacks['OnFrameRequestedNavigation'] if ($Callback) { $Callback.Invoke($Response) } } hidden [void]FrameStartedLoading($Response) { $CdpPage = $this.GetPageBySessionId($Response.sessionId) if ($CdpPage.TargetId -eq $Response.params.frameId) { $CdpPage.LoadingEvents.AddOrUpdate('FrameStartedLoading', 1, { param($Key, $OldValue) $OldValue + 1 }) $CdpPage.LoadingEvents.AddOrUpdate('IsLoading', $true, { param($Key, $OldValue) $true }) } else { # this event can be emitted before a Page.frameAttached or Runtime.executionContextCreated...? $Frame = $CdpPage.Frames.GetOrAdd($Response.params.frameId, [CdpFrame]::new($Response.params.frameId, $Response.sessionId)) $Frame.LoadingEvents.AddOrUpdate('FrameStartedLoading', 1, { param($Key, $OldValue) $OldValue + 1 }) $Frame.LoadingEvents.AddOrUpdate('IsLoading', $true, { param($Key, $OldValue) $true }) } $Callback = $this.SharedState.Callbacks['OnFrameStartedLoading'] if ($Callback) { $Callback.Invoke($Response) } } hidden [void]FrameStartedNavigating($Response) { # $CdpPage = $this.GetPageBySessionId($Response.sessionId) # $CdpPage.LoadingEvents.AddOrUpdate('IsLoading', $true, { param($Key, $OldValue) $true }) # Write-Debug ('Frame Started Navigating: ({0})' -f ($Response | ConvertTo-Json -Depth 10)) $Callback = $this.SharedState.Callbacks['OnFrameStartedNavigating'] if ($Callback) { $Callback.Invoke($Response) } } hidden [void]FrameStoppedLoading($Response) { $CdpPage = $this.GetPageBySessionId($Response.sessionId) if ($CdpPage.TargetId -eq $Response.params.frameId) { $CdpPage.LoadingEvents.AddOrUpdate('FrameStoppedLoading', 1, { param($Key, $OldValue) $OldValue + 1 }) $CdpPage.LoadingEvents.AddOrUpdate('IsLoading', $false, { param($Key, $OldValue) $false }) } else { # this event can be emitted before a Page.frameAttached or Runtime.executionContextCreated...? $Frame = $CdpPage.Frames.GetOrAdd($Response.params.frameId, [CdpFrame]::new($Response.params.frameId, $Response.sessionId)) $Frame.LoadingEvents.AddOrUpdate('FrameStoppedLoading', 1, { param($Key, $OldValue) $OldValue + 1 }) $Frame.LoadingEvents.AddOrUpdate('IsLoading', $false, { param($Key, $OldValue) $false }) } $Callback = $this.SharedState.Callbacks['OnFrameStoppedLoading'] if ($Callback) { $Callback.Invoke($Response) } } hidden [void]NavigatedWithinDocument($Response) { # Write-Debug ('Navigated Within Document: ({0})' -f ($Response | ConvertTo-Json -Depth 10)) $Callback = $this.SharedState.Callbacks['OnNavigatedWithinDocument'] if ($Callback) { $Callback.Invoke($Response) } } hidden [void]TargetCreated($Response) { $Target = $Response.params.targetInfo $CdpPage = [CdpPage]::new($Target.targetId, $Target.Url, $Target.Title, $Target.browserContextId) $null = $this.SharedState.Targets.TryAdd($Target.targetId, $CdpPage) $Callback = $this.SharedState.Callbacks['OnTargetCreated'] if ($Callback) { $Callback.Invoke($Response) } } hidden [void]TargetDestroyed($Response) { $CdpPage = $this.GetPageByTargetId($Response.params.targetId) if ($CdpPage) { $null = $this.SharedState.Targets.TryRemove($CdpPage.TargetId, [ref]$null) } $Callback = $this.SharedState.Callbacks['OnTargetDestroyed'] if ($Callback) { $Callback.Invoke($Response) } } hidden [void]TargetInfoChanged($Response) { $Target = $Response.params.targetInfo $CdpPage = $this.GetPageByTargetId($Target.targetId) if ($CdpPage) { $CdpPage.Url = $Target.Url $CdpPage.Title = $Target.Title $CdpPage.ProcessId = $Target.pid # $CdpPage.Frames.Clear() } $Callback = $this.SharedState.Callbacks['OnTargetInfoChanged'] if ($Callback) { $Callback.Invoke($Response) } } hidden [void]AttachedToTarget($Response) { $Target = $Response.params.targetInfo $CdpPage = $this.GetPageByTargetId($Target.targetId) $CdpPage.TargetInfo.AddOrUpdate('SessionId', $Response.params.sessionId, { param($Key, $OldValue) $Response.params.sessionId }) $null = $this.SharedState.Sessions.TryAdd($Response.params.sessionId, $CdpPage) $Callback = $this.SharedState.Callbacks['OnAttachedToTarget'] if ($Callback) { $Callback.Invoke($Response) } } hidden [void]DetachedFromTarget($Response) { $CdpPage = $this.GetPageBySessionId($Response.params.sessionId) $CdpPage.TargetInfo.AddOrUpdate('SessionId', $null, { param($Key, $OldValue) $null }) $null = $this.SharedState.Sessions.TryRemove($Response.params.sessionId, [ref]$null) $Callback = $this.SharedState.Callbacks['OnDetachedFromTarget'] if ($Callback) { $Callback.Invoke($Response) } } hidden [void]BindingCalled($Response) { $Callback = $this.SharedState.Callbacks['OnBindingCalled'] if ($Callback) { $Callback.Invoke($Response) } } hidden [void]ExecutionContextsCleared($Response) { $CdpPage = $this.GetPageBySessionId($Response.sessionId) $CdpPage.PageInfo.AddOrUpdate('RuntimeUniqueId', $null, { param($Key, $OldValue) $null } ) $Callback = $this.SharedState.Callbacks['OnExecutionContextsCleared'] if ($Callback) { $Callback.Invoke($Response) } } hidden [void]ExecutionContextCreated($Response) { $CdpPage = $this.GetPageBySessionId($Response.sessionId) $FrameId = $Response.params.context.auxData.frameId if ($CdpPage.TargetId -eq $FrameId) { $CdpPage.PageInfo.AddOrUpdate('RuntimeUniqueId', $Response.params.context.uniqueId, { param($Key, $OldValue) $Response.params.context.uniqueId } ) } else { $Frame = $CdpPage.Frames.GetOrAdd($FrameId, [CdpFrame]::new($FrameId, $Response.sessionId)) $Frame.RuntimeUniqueId = $Response.params.context.uniqueId } $Callback = $this.SharedState.Callbacks['OnExecutionContextCreated'] if ($Callback) { $Callback.Invoke($Response) } } [CdpPage]GetPageBySessionId([string]$SessionId) { $Page = $null while ($null -eq $Page) { if (!$this.SharedState.Sessions.TryGetValue($SessionId, [ref]$Page)) { Start-Sleep -Milliseconds 1 } } return $Page } [CdpPage]GetPageByTargetId([string]$TargetId) { $Page = $null while ($null -eq $Page) { if (!$this.SharedState.Targets.TryGetValue($TargetId, [ref]$Page)) { Start-Sleep -Milliseconds 1 } } return $Page } } # [NoRunspaceAffinity()] class CdpServer { [System.Collections.Concurrent.ConcurrentDictionary[string, object]]$SharedState = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new() [System.Management.Automation.Runspaces.RunspacePool]$RunspacePool [System.Diagnostics.Process]$ChromeProcess [pscustomobject]$Threads = @{ MessageReader = $null MessageReaderHandle = $null MessageProcessor = $null MessageProcessorHandle = $null MessageWriter = $null MessageWriterHandle = $null } [System.Collections.Concurrent.ConcurrentDictionary[string, string]]$CommandHistory = [System.Collections.Concurrent.ConcurrentDictionary[string, string]]::new() CdpServer($StartPage, $UserDataDir, $BrowserPath, $StreamOutput) { $this.Init($StartPage, $UserDataDir, $BrowserPath, $StreamOutput, 0, $null) } CdpServer($StartPage, $UserDataDir, $BrowserPath, $StreamOutput, $AdditionalThreads) { $this.Init($StartPage, $UserDataDir, $BrowserPath, $StreamOutput, 0, $null) } CdpServer($StartPage, $UserDataDir, $BrowserPath, $StreamOutput, $AdditionalThreads, $Callbacks) { $this.Init($StartPage, $UserDataDir, $BrowserPath, $StreamOutput, $AdditionalThreads, $Callbacks) } hidden [void]Init($StartPage, $UserDataDir, $BrowserPath, $StreamOutput, $AdditionalThreads, $Callbacks) { $this.SharedState = [System.Collections.Generic.Dictionary[string, object]]::new() $this.SharedState.IO = @{ PipeWriter = [System.IO.Pipes.AnonymousPipeServerStream]::new([System.IO.Pipes.PipeDirection]::Out, [System.IO.HandleInheritability]::Inheritable) PipeReader = [System.IO.Pipes.AnonymousPipeServerStream]::new([System.IO.Pipes.PipeDirection]::In, [System.IO.HandleInheritability]::Inheritable) UnprocessedResponses = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() CommandQueue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() } $this.SharedState.MessageHistory = [System.Collections.Concurrent.ConcurrentDictionary[version, object]]::new() $this.SharedState.CommandId = 0 $this.SharedState.Targets = [System.Collections.Concurrent.ConcurrentDictionary[string, CdpPage]]::new() $this.SharedState.Sessions = [System.Collections.Concurrent.ConcurrentDictionary[string, CdpPage]]::new() $this.SharedState.Callbacks = [System.Collections.Generic.Dictionary[string, scriptblock]]::new() $this.SharedState.BrowserContexts = [System.Collections.Generic.List[string]]::new() foreach ($Key in $Callbacks.Keys) { $this.SharedState.Callbacks[$Key] = $Callbacks[$Key] } $this.SharedState.Commands = @{ SendRuntimeEvaluate = $this.CreateDelegate($this.SendRuntimeEvaluate) GetPageBySessionId = $this.CreateDelegate($this.GetPageBySessionId) GetPageByTargetId = $this.CreateDelegate($this.GetPageByTargetId) } $this.SharedState.EventHandler = New-UnboundClassInstance -type ([CdpEventHandler]) -arguments @($this.SharedState) #[CdpEventHandler]::new($this.SharedState) $State = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() $State.ImportPSModule("$PSScriptRoot\PSChromeDevToolsServer") $RunspaceSharedState = [System.Management.Automation.Runspaces.SessionStateVariableEntry]::new('SharedState', $this.SharedState, $null) $State.Variables.Add($RunspaceSharedState) $State.ThrowOnRunspaceOpenError = $true $this.RunspacePool = [RunspaceFactory]::CreateRunspacePool(3, 3 + $AdditionalThreads, $State, $StreamOutput) $this.RunspacePool.Open() $BrowserArgs = @( ('--user-data-dir="{0}"' -f $UserDataDir) '--no-first-run' '--remote-debugging-pipe' ('--remote-debugging-io-pipes={0},{1}' -f $this.SharedState.IO.PipeWriter.GetClientHandleAsString(), $this.SharedState.IO.PipeReader.GetClientHandleAsString()) $StartPage ) | Where-Object { $_ -ne '' -and $_ -ne $null } $StartInfo = [System.Diagnostics.ProcessStartInfo]::new() $StartInfo.FileName = $BrowserPath $StartInfo.Arguments = $BrowserArgs $StartInfo.UseShellExecute = $false $this.ChromeProcess = [System.Diagnostics.Process]::Start($StartInfo) while (!$this.SharedState.IO.PipeWriter.IsConnected -and !$this.SharedState.IO.PipeReader.IsConnected) { Start-Sleep -Milliseconds 1 } $this.SharedState.IO.PipeWriter.DisposeLocalCopyOfClientHandle() $this.SharedState.IO.PipeReader.DisposeLocalCopyOfClientHandle() } [void]StartMessageReader() { $this.Threads.MessageReader = [powershell]::Create() $this.Threads.MessageReader.RunspacePool = $this.RunspacePool $null = $this.Threads.MessageReader.AddScript({ if ($SharedState.DebugPreference) { $DebugPreference = $SharedState.DebugPreference } if ($SharedState.VerbosePreference) { $VerbosePreference = $SharedState.VerbosePreference } $Buffer = [byte[]]::new(1024) $StringBuilder = [System.Text.StringBuilder]::new() $NullTerminatedString = "`0" while ($SharedState.IO.PipeReader.IsConnected) { # Will hang here until something comes through the pipe. $BytesRead = $SharedState.IO.PipeReader.Read($Buffer, 0, $Buffer.Length) $null = $StringBuilder.Append([System.Text.Encoding]::UTF8.GetString($Buffer, 0, $BytesRead)) $HasCompletedMessages = if ($StringBuilder.Length) { $StringBuilder.ToString($StringBuilder.Length - 1, 1) -eq $NullTerminatedString } else { $false } if ($HasCompletedMessages) { $RawResponse = $StringBuilder.ToString() $SplitResponse = @(($RawResponse -split $NullTerminatedString).Where({ "`0" -ne $_ }) | ConvertFrom-Json) $SplitResponse.ForEach({ $SharedState.IO.UnprocessedResponses.Enqueue($_) } ) $StringBuilder.Clear() } } } ) $this.Threads.MessageReaderHandle = $this.Threads.MessageReader.BeginInvoke() } [void]StartMessageProcessor() { $this.Threads.MessageProcessor = [powershell]::Create() $this.Threads.MessageProcessor.RunspacePool = $this.RunspacePool $null = $this.Threads.MessageProcessor.AddScript({ if ($SharedState.DebugPreference) { $DebugPreference = $SharedState.DebugPreference } if ($SharedState.VerbosePreference) { $VerbosePreference = $SharedState.VerbosePreference } $Response = $null $IdleTime = 1 $ResponseIndex = 1 while ($SharedState.IO.PipeReader.IsConnected -and $SharedState.IO.PipeWriter.IsConnected) { while ($SharedState.IO.UnprocessedResponses.TryDequeue([ref]$Response)) { $LastCommandId = $null if ($Response.id) { $LastCommandId = $Response.id } else { while (!$SharedState.TryGetValue('CommandId', [ref]$LastCommandId)) { Start-Sleep -Milliseconds 1 } } do { $SucessfullyAdded = if ($Response.id) { $SharedState.MessageHistory.TryAdd([version]::new($LastCommandId, 0), $Response) } else { $SharedState.MessageHistory.TryAdd([version]::new($LastCommandId, $ResponseIndex++), $Response) } if (!$SucessfullyAdded) { Start-Sleep -Milliseconds 1 } } while (!$SucessfullyAdded) # $Start = Get-Date $SharedState.EventHandler.ProcessEvent($Response) # $End = Get-Date # Write-Debug ('{0} {1} Processing Time: {2} ms' -f $Response.id, $Response.method, ($End - $Start).TotalMilliseconds) } Start-Sleep -Milliseconds $IdleTime } } ) $this.Threads.MessageProcessorHandle = $this.Threads.MessageProcessor.BeginInvoke() } [void]StartMessageWriter() { $this.Threads.MessageWriter = [powershell]::Create() $this.Threads.MessageWriter.RunspacePool = $this.RunspacePool $null = $this.Threads.MessageWriter.AddScript({ if ($SharedState.DebugPreference) { $DebugPreference = $SharedState.DebugPreference } if ($SharedState.VerbosePreference) { $VerbosePreference = $SharedState.VerbosePreference } $CommandBytes = $null $IdleTime = 1 while ($SharedState.IO.PipeReader.IsConnected -and $SharedState.IO.PipeWriter.IsConnected) { while ($SharedState.IO.CommandQueue.TryDequeue([ref]$CommandBytes)) { $SharedState.IO.PipeWriter.Write($CommandBytes, 0, $CommandBytes.Length) } Start-Sleep -Milliseconds $IdleTime } } ) $this.Threads.MessageWriterHandle = $this.Threads.MessageWriter.BeginInvoke() } [void]Stop() { $this.SharedState.IO.PipeReader.Dispose() $this.SharedState.IO.PipeWriter.Dispose() while ($this.SharedState.IO.PipeReader.IsConnected -or $this.SharedState.IO.PipeWriter.IsConnected) { Start-Sleep -Milliseconds 1 } if ($this.Threads.MessageReaderHandle) { $this.Threads.MessageReader.EndInvoke($this.Threads.MessageReaderHandle) $this.Threads.MessageReader.Dispose() } if ($this.Threads.MessageProcessorHandle) { $this.Threads.MessageProcessor.EndInvoke($this.Threads.MessageProcessorHandle) $this.Threads.MessageProcessor.Dispose() } if ($this.Threads.MessageWriterHandle) { $this.Threads.MessageWriter.EndInvoke($this.Threads.MessageWriterHandle) $this.Threads.MessageWriter.Dispose() } $this.ChromeProcess.Dispose() $this.RunspacePool.Dispose() } [void]SendCommand([hashtable]$Command) { $this.SendCommand($Command, $false) } [object]SendCommand([hashtable]$Command, [bool]$WaitForResponse) { # This should be the only place where $this.SharedState.CommandId is incremented. $CommandId = $this.SharedState.AddOrUpdate('CommandId', 1, { param($Key, $OldValue) $OldValue + 1 }) $null = $this.CommandHistory.TryAdd($CommandId, $Command.method) $Command.id = $CommandId $JsonCommand = $Command | ConvertTo-Json -Depth 10 -Compress $CommandBytes = [System.Text.Encoding]::UTF8.GetBytes($JsonCommand) + 0 $this.SharedState.IO.CommandQueue.Enqueue($CommandBytes) if ($WaitForResponse) { $AwaitedMessage = $null while (!$this.SharedState.MessageHistory.TryGetValue([version]::new($CommandId, 0), [ref]$AwaitedMessage)) { Start-Sleep -Milliseconds 1 } return $AwaitedMessage } return $null } [CdpPage]GetPageBySessionId([string]$SessionId) { $Page = $null while ($null -eq $Page) { if (!$this.SharedState.Sessions.TryGetValue($SessionId, [ref]$Page)) { Start-Sleep -Milliseconds 1 } } return $Page } [CdpPage]GetPageByTargetId([string]$TargetId) { $Page = $null while ($null -eq $Page) { if (!$this.SharedState.Targets.TryGetValue($TargetId, [ref]$Page)) { Start-Sleep -Milliseconds 1 } } return $Page } [void]SendRuntimeEvaluate([string]$SessionId, [string]$Expression) { $JsonCommand = @{ method = 'Runtime.evaluate' sessionId = $SessionId params = @{ expression = $Expression } } $this.SendCommand($JsonCommand) } [void]EnableDefaultEvents() { $JsonCommand = Get-Target.setDiscoverTargets $this.SendCommand($JsonCommand) $JsonCommand = Get-Target.setAutoAttach $this.SendCommand($JsonCommand) while ($this.SharedState.Targets.Count -eq 0) { Start-Sleep -Milliseconds 1 } $TargetCreatedEvents = $this.SharedState.MessageHistory.GetEnumerator() | Sort-Object -Property Key | Where-Object { $_.Value.method -eq 'Target.targetCreated' } $AvailableTargets = $this.SharedState.Targets.GetEnumerator() | Where-Object { $_.Value.TargetId -in $TargetCreatedEvents.Value.params.targetInfo.targetId } $JsonCommand = Get-Page.enable $AvailableTargets[0].Value.TargetInfo.SessionId $this.SendCommand($JsonCommand) $JsonCommand = Get-Runtime.enable $AvailableTargets[0].Value.TargetInfo.SessionId $this.SendCommand($JsonCommand, $true) } [object]ShowMessageHistory() { $CommandSnapshot = @{} $Commands = $this.CommandHistory.GetEnumerator() foreach ($Message in $Commands) { $CommandSnapshot[[int]$Message.Key] = $Message.Value } $Events = $this.SharedState.MessageHistory.GetEnumerator() | Sort-Object -Property Key | Select-Object -Property @( @{Name = 'id'; Expression = { $_.Value.id } }, @{Name = 'method'; Expression = { if ($_.Value.method) { $_.Value.method } else { $CommandSnapshot[[int]$_.Value.id] } } }, @{Name = 'error'; Expression = { $_.Value.error } }, @{Name = 'sessionId'; Expression = { $_.Value.sessionId } }, @{Name = 'result'; Expression = { $_.Value.result } }, @{Name = 'params'; Expression = { $_.Value.params } } ) return $Events } hidden [Delegate]CreateDelegate([System.Management.Automation.PSMethod]$Method) { return $this.CreateDelegate($Method, $this) } hidden [Delegate]CreateDelegate([System.Management.Automation.PSMethod]$Method, $Target) { $reflectionMethod = if ($Target.GetType().Name -eq 'PSCustomObject') { $Target.psobject.GetType().GetMethod($Method.Name) } else { $Target.GetType().GetMethod($Method.Name) } $parameterTypes = [System.Linq.Enumerable]::Select($reflectionMethod.GetParameters(), [func[object, object]] { $args[0].parametertype }) $concatMethodTypes = $parameterTypes + $reflectionMethod.ReturnType $delegateType = [System.Linq.Expressions.Expression]::GetDelegateType($concatMethodTypes) $delegate = [delegate]::CreateDelegate($delegateType, $Target, $reflectionMethod.Name) return $delegate } } function Get-DOM.describeNode { param($SessionId, $ObjectId) @{ method = 'DOM.describeNode' sessionId = $SessionId params = @{ objectId = "$ObjectId" } } } function Get-DOM.getBoxModel { param($SessionId, $ObjectId) @{ method = 'DOM.getBoxModel' sessionId = $SessionId params = @{ objectId = "$ObjectId" } } } function Get-Input.dispatchKeyEvent { param($SessionId, $Text) @{ method = 'Input.dispatchKeyEvent' sessionId = $SessionId params = @{ type = 'char' text = $Text } } } function Get-Input.dispatchMouseEvent { param($SessionId, $Type, $X, $Y, $Button) @{ method = 'Input.dispatchMouseEvent' sessionId = $SessionId params = @{ type = $Type button = $Button clickCount = 0 x = $X y = $Y } } } function Get-Page.bringToFront { param($SessionId) @{ method = 'Page.bringToFront' sessionId = $SessionId } } function Get-Page.enable { param($SessionId) @{ method = 'Page.enable' sessionId = $SessionId } } function Get-Page.navigate { param($SessionId, $Url) @{ method = 'Page.navigate' sessionId = $SessionId params = @{ url = $Url } } } function Get-Page.getFrameTree { param($SessionId) @{ method = 'Page.getFrameTree' sessionId = $SessionId } } function Get-Runtime.addBinding { param($SessionId, $Name) @{ method = 'Runtime.addBinding' sessionId = $SessionId params = @{ name = $Name } } } function Get-Runtime.enable { param($SessionId) @{ method = 'Runtime.enable' sessionId = $SessionId } } function Get-Runtime.evaluate { param($SessionId, $Expression) @{ method = 'Runtime.evaluate' sessionId = $SessionId params = @{ expression = $Expression } } } function Get-Target.createTarget { param($Url) @{ method = 'Target.createTarget' params = @{ url = $Url } } } function Get-Target.createBrowserContext { param() @{ method = 'Target.createBrowserContext' params = @{ disposeOnDetach = $true } } } function Get-Target.setAutoAttach { param() @{ method = 'Target.setAutoAttach' params = @{ autoAttach = $true waitForDebuggerOnStart = $false filter = @( @{ type = 'service_worker' exclude = $true }, @{ type = 'worker' exclude = $true }, @{ type = 'browser' exclude = $true }, @{ type = 'tab' exclude = $true }, # @{ # type = 'other' # exclude = $true # }, @{ type = 'background_page' exclude = $true }, @{} ) flatten = $true } } } function Get-Target.setDiscoverTargets { param($Url) @{ method = 'Target.setDiscoverTargets' params = @{ discover = $true filter = @( @{ type = 'service_worker' exclude = $true }, @{ type = 'worker' exclude = $true }, @{ type = 'browser' exclude = $true }, @{ type = 'tab' exclude = $true }, # @{ # type = 'other' # exclude = $true # }, @{ type = 'background_page' exclude = $true }, @{} ) } } } function Get-CdpFrames { param($Tree) if ($Tree.frame) { $Tree.frame } if ($Tree.childFrames) { foreach ($Child in $Tree.childFrames) { Get-CdpFrames $Child } } } function Start-CdpServer { <# .SYNOPSIS Starts the CdpServer by launching the browser process, initializing the event handlers, and starting the message reader, processor, and writer threads .PARAMETER StartPage The URL of the page to load when the browser starts .PARAMETER UserDataDir The directory to use for the browser's user data profile. This should be a unique directory for each instance of the server to avoid conflicts .PARAMETER BrowserPath The path to the browser executable to launch .PARAMETER AdditionalThreads Sets the max runspaces the pool can use + 3. Default runspacepool uses 3min and 3max threads for MessageReader, MessageProcessor, MessageWriter A number higher than 0 increases the maximum runspaces for the pool. More MessageProcessor can be started with $CdpServer.MessageProcessor() These will be queued forever if the max number of runspaces are exhausted in the pool. .PARAMETER Callbacks A hashtable of scriptblocks to be invoked for specific events. The keys should be the event names without the domain prefix and preceeded by 'On'. For example: @{ OnLoadEventFired = { param($Response) $Response.params } } .PARAMETER DisableDefaultEvents This stops targets from being auto attached and auto discovered. .PARAMETER StreamOutput This is the $Host/Console which runspace streams will output to. #> [CmdletBinding()] param ( [Parameter(Mandatory)] [string]$StartPage, [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Container -IsValid })] [string]$UserDataDir, [Parameter(Mandatory)] [string]$BrowserPath, [ValidateScript({ $_ -ge 0 })] [int]$AdditionalThreads = 0, [hashtable]$Callbacks, [switch]$DisableDefaultEvents, [object]$StreamOutput ) $LockFile = Join-Path -Path $UserDataDir -ChildPath 'lockfile' if (Test-Path -Path $LockFile -PathType Leaf) { throw 'Browser is already open. Please close it and run Start-CdpServer again.' } # $Server = [CdpServer]::new($StartPage, $UserDataDir, $BrowserPath, $AdditionalThreads, $Callbacks) $ConsoleHost = if ($StreamOutput) { $StreamOutput } else { (Get-Host) } $Server = New-UnboundClassInstance CdpServer -arguments $StartPage, $UserDataDir, $BrowserPath, $ConsoleHost, $AdditionalThreads, $Callbacks if ($PSBoundParameters.ContainsKey('Debug')) { $Server.SharedState.DebugPreference = 'Continue' } if ($PSBoundParameters.ContainsKey('Verbose')) { $Server.SharedState.VerbosePreference = 'Continue' } $Server.StartMessageReader() $Server.StartMessageProcessor() $Server.StartMessageWriter() if (!$DisableDefaultEvents) { $Server.EnableDefaultEvents() } $Server.SharedState.BrowserContexts.Add($Server.SharedState.Targets.Values[0].BrowserContextId) $Server } function Stop-CdpServer { <# .SYNOPSIS Disposes the Server Pipes, Threads, ChromeProcess, and RunspacePool #> [CmdletBinding()] param ( [Parameter(Mandatory)] [CdpServer]$Server ) $Server.Stop() } function New-CdpPage { <# .SYNOPSIS Creates a new target and returns the corresponding CdpPage object from the server's SharedState.Targets list #> [CmdletBinding()] param ( [Parameter(Mandatory)] [CdpServer]$Server, [string]$Url = 'about:blank', [Parameter(ParameterSetName = 'Tab')] [string]$BrowserContextId, [Parameter(ParameterSetName = 'NewWindow')] [switch]$NewWindow ) if ($NewWindow) { $Command = Get-Target.createBrowserContext $Response = $Server.SendCommand($Command, $true) } $Command = Get-Target.createTarget $Url if ($NewWindow) { $Command.params.newWindow = $true $Command.params.browserContextId = $Response.result.browserContextId } else { $Command.params.browserContextId = $BrowserContextId #$Server.SharedState.BrowserContexts[$BrowserContextIndex] } $Response = $Server.SendCommand($Command, $true) $CdpPage = $Server.GetPageByTargetId($Response.result.targetId) $SessionId = $null while ($null -eq $SessionId) { $null = $CdpPage.TargetInfo.TryGetValue('SessionId', [ref]$SessionId) Start-Sleep -Milliseconds 1 } $Command = Get-Page.enable $SessionId $null = $Server.SendCommand($Command, $true) $Command = Get-Runtime.enable $SessionId $null = $Server.SendCommand($Command, $true) $RuntimeUniqueId = $null while ($null -eq $RuntimeUniqueId) { $null = $CdpPage.PageInfo.TryGetValue('RuntimeUniqueId', [ref]$RuntimeUniqueId) Start-Sleep -Milliseconds 1 } $IsLoading = $null $null = $CdpPage.LoadingEvents.TryGetValue('IsLoading', [ref]$IsLoading) while ($IsLoading) { Start-Sleep -Milliseconds 1 $null = $CdpPage.LoadingEvents.TryGetValue('IsLoading', [ref]$IsLoading) } $CdpPage if ($CdpPage.Frames.Count -eq 0) { return } while ([System.Linq.Enumerable]::Sum([int[]]@($CdpPage.Frames.Values.LoadingEvents.IsLoading)) -gt 0) { Start-Sleep -Milliseconds 1 } $Command = Get-Page.getFrameTree $SessionId do { $Response = $Server.SendCommand($Command, $true) $Tree = Get-CdpFrames $Response.result.frameTree $HasAllFrames = $CdpPage.Frames.ToArray().Key | Where-Object { $_ -in $Tree.id } } while ($HasAllFrames.Count -ne $CdpPage.Frames.Count) } function Invoke-CdpPageNavigate { <# .SYNOPSIS Navigates and automatically waits for the page to load with LoadEventFired and FrameStoppedLoading Also waits for frames to load if they are present #> [CmdletBinding()] param ( [Parameter(Mandatory)] [CdpServer]$Server, [Parameter(Mandatory)] [string]$SessionId, [Parameter(Mandatory)] [string]$Url ) $CdpPage = $Server.GetPageBySessionId($SessionId) $OldRuntimeUniqueId = $CdpPage.PageInfo.RuntimeUniqueId $Command = Get-Page.navigate $SessionId $Url $Server.SendCommand($Command) $NewRuntimeUniqueId = $null $null = $CdpPage.PageInfo.TryGetValue('RuntimeUniqueId', [ref]$NewRuntimeUniqueId) if ($null -ne $OldRuntimeUniqueId) { while ($NewRuntimeUniqueId -eq $OldRuntimeUniqueId) { Start-Sleep -Milliseconds 1 $null = $CdpPage.PageInfo.TryGetValue('RuntimeUniqueId', [ref]$NewRuntimeUniqueId) } } $IsLoading = $null $null = $CdpPage.LoadingEvents.TryGetValue('IsLoading', [ref]$IsLoading) while ($IsLoading) { Start-Sleep -Milliseconds 1 $null = $CdpPage.LoadingEvents.TryGetValue('IsLoading', [ref]$IsLoading) } if ($CdpPage.Frames.Count -eq 0) { return } while ([System.Linq.Enumerable]::Sum([int[]]@($CdpPage.Frames.Values.LoadingEvents.IsLoading)) -gt 0) { Start-Sleep -Milliseconds 1 } $Command = Get-Page.getFrameTree $SessionId do { $Response = $Server.SendCommand($Command, $true) $Tree = Get-CdpFrames $Response.result.frameTree $HasAllFrames = $CdpPage.Frames.ToArray().Key | Where-Object { $_ -in $Tree.id } } while ($HasAllFrames.Count -ne $CdpPage.Frames.Count) } function Invoke-CdpInputClickElement { <# .SYNOPSIS Finds and clicks with element in the center of the box. Clicks from the top left of the element when $TopLeft is switched on. .PARAMETER Selector Javascript that returns ONE node object For example: document.querySelectorAll('[name=q]')[0] .PARAMETER Click Number of times to left click the mouse .PARAMETER OffsetX Number of pixels to offset from the center of the element on the X axis .PARAMETER OffsetY Number of pixels to offset from the center of the element on the Y axis .PARAMETER TopLeft Clicks from the top left of the element instead of center. Offset x and y will be relative to this position instead. #> [CmdletBinding()] param ( [Parameter(Mandatory)] [CdpServer]$Server, [Parameter(Mandatory)] [string]$SessionId, [Parameter(Mandatory)] [string]$Selector, [Parameter(ParameterSetName = 'Click')] [int]$Click = 0, [Parameter(ParameterSetName = 'Click')] [int]$OffsetX = 0, [Parameter(ParameterSetName = 'Click')] [int]$OffsetY = 0, [Parameter(ParameterSetName = 'Click')] [switch]$TopLeft ) $CdpPage = $Server.GetPageBySessionId($SessionId) $Command = Get-Runtime.evaluate $SessionId $Selector $Command.params.uniqueContextId = "$($CdpPage.PageInfo.RuntimeUniqueId)" $Response = $Server.SendCommand($Command, $true) $CdpPage.PageInfo.ObjectId = $Response.result.result.objectId if ($Click -le 0) { return } $Command = Get-DOM.describeNode $SessionId $CdpPage.PageInfo.ObjectId $Command.params.objectId = $CdpPage.PageInfo.ObjectId $Response = $Server.SendCommand($Command, $true) if ($Response.error) { throw ('Error describing node: {0}' -f $Response.error.message) } $CdpPage.PageInfo.Node = $Response.result.node $Command = Get-DOM.getBoxModel $SessionId $CdpPage.PageInfo.ObjectId $Command.params.objectId = $CdpPage.PageInfo.ObjectId $Response = $Server.SendCommand($Command, $true) $CdpPage.PageInfo.BoxModel = $Response.result.model if ($TopLeft) { $PixelX = $CdpPage.PageInfo.BoxModel.content[0] + $OffsetX $PixelY = $CdpPage.PageInfo.BoxModel.content[1] + $OffsetY } else { $PixelX = $CdpPage.PageInfo.BoxModel.content[0] + ($CdpPage.PageInfo.BoxModel.width / 2) + $OffsetX $PixelY = $CdpPage.PageInfo.BoxModel.content[1] + ($CdpPage.PageInfo.BoxModel.height / 2) + $OffsetY } $Command = Get-Input.dispatchMouseEvent $SessionId 'mousePressed' $PixelX $PixelY 'left' $Command.params.clickCount = $Click $CommandFront = Get-Page.bringToFront $SessionId $null = $Server.SendCommand($CommandFront, $true) $Server.SendCommand($Command) $Command.params.type = 'mouseReleased' $Server.SendCommand($Command) } function Invoke-CdpInputSendKeys { <# .SYNOPSIS Sends keys to a session .PARAMETER Keys String to send .EXAMPLE Invoke-CdpInputSendKeys -Server $Server -SessionId $SessionId -Keys 'Hello World' #> [CmdletBinding()] param ( [Parameter(Mandatory)] [CdpServer]$Server, [Parameter(Mandatory)] [string]$SessionId, [Parameter(Mandatory)] [string]$Keys ) $Command = Get-Input.DispatchKeyEvent $SessionId $null $CommandFront = Get-Page.bringToFront $SessionId $null = $Server.SendCommand($CommandFront, $true) $Keys.ToCharArray().ForEach({ $Command.params.text = $_ # $null = $Server.SendCommand($CommandFront, $true) $Server.SendCommand($Command) } ) } function Invoke-CdpRuntimeEvaluate { <# .SYNOPSIS Run javascript on the browser and return the raw response. .PARAMETER Expression The javascript expression to run. .PARAMETER AwaitPromise Use if the Expression includes a promise that needs to be awaited. .EXAMPLE This returns after ~3-4 seconds rather than 2+2+2=6 seconds If AwaitPromise was not used, Invoke-CdpRuntimeEvaluate will return immediately with $Result.result.result = javascript promise object. $Expression = @' function timedPromise(name, delay) { return new Promise(resolve => { setTimeout(() => { resolve(`${name} resolved`); }, delay); }); } async function awaitMultiplePromises() { const promise1 = timedPromise("Promise 1", 2000); const promise2 = timedPromise("Promise 2", 2000); const promise3 = timedPromise("Promise 3", 2000); const results = await Promise.all([promise1, promise2, promise3]); const displayBox = document.querySelector("[id=textInput]"); displayBox.value = results; return 'Promise was awaited.' } awaitMultiplePromises(); '@ $StartTime = Get-Date $Result = Invoke-CdpRuntimeEvaluate -Server $Server -SessionId $SessionId -Expression $Expression -AwaitPromise $EndTime = Get-Date ($EndTime - $StartTime).TotalSeconds $Result.result.result #> [CmdletBinding()] param ( [Parameter(Mandatory)] [CdpServer]$Server, [Parameter(Mandatory)] [string]$SessionId, [Parameter(Mandatory)] [string]$Expression, [switch]$AwaitPromise ) $CdpPage = $Server.GetPageBySessionId($SessionId) $Command = Get-Runtime.evaluate $SessionId $Expression $Command.params.uniqueContextId = "$($CdpPage.PageInfo.RuntimeUniqueId)" if ($AwaitPromise) { $Command.params.awaitPromise = $true } $Response = $Server.SendCommand($Command, $true) $Response } function Invoke-CdpRuntimeAddBinding { <# .SYNOPSIS Adds a binding object to the browser .PARAMETER Name Name of the object to use in javascript - window.Name(json); #> [CmdletBinding()] param ( [Parameter(Mandatory)] [CdpServer]$Server, [Parameter(Mandatory)] [string]$SessionId, [Parameter(Mandatory)] [string]$Name ) $Command = Get-Runtime.addBinding $SessionId $Name $Server.SendCommand($Command) } |