Public/WebSocketClient.ps1
function Start-TMConsoleWebSocketClient { [CmdletBinding()] param ( [Parameter(Mandatory = $True)] [String]$WebSocketServer, [Parameter(Mandatory = $True)] [Int]$WebSocketPort, [Parameter()]$HostPID = -1, [Parameter(mandatory = $false)] [Bool]$OutputVerbose = $false, [Parameter()] [Bool]$AllowInsecureSSL = $False, [Parameter(Mandatory = $false)] [Version]$TMCVersion = '0.0.0' ) begin { $global:AllowInsecureSSL = $AllowInsecureSSL $global:OutputVerbose = $OutputVerbose ## Enable Verbose Output if requested if ($global:OutputVerbose) { $global:VerbosePreference = 'Continue' $VerbosePreference = 'Continue' Write-Output 'Starting PowerShell Web Socket Client' } #region Event Handlers ## Event handler to send data from the client to the server $EventHandler_WebSocket_SendData = [ScriptBlock] { $Message = $null if ( $global:Queues.WebSocketClientSend.TryDequeue([ref] $Message)) { $global:WebSocketClient.SendAsync($Message, [System.Net.WebSockets.WebSocketMessageType]::Text, $global:CancellationToken) } } ## Event handler to receive data from the server $EventHandler_WebSocket_ReceiveData = [scriptblock] { try { ## Create an Event to receive the next Character Array $Message = '' $Event.SourceEventArgs.Data | ForEach-Object { $Message = $Message + [char]$_ } ## Queue the Message for the SessionManager to handle $global:Queues.SessionManager.Enqueue($Message) } catch { $global:Queues.WebSocketClientSend.Enqueue( ([PSCustomObject]@{ Type = 'Debug THROWN ERROR' Error = $_ } | ConvertTo-Json -Depth 5) ) } } ## Event handler process messages that are added to the sessionmanager queue $EventHandler_SessionManager_Enqueued = [scriptblock] { $MessageString = '' if ($global:Queues.SessionManager.TryDequeue([ref]$MessageString)) { ## Convert the incoming String data to an Object $Message = $MessageString | ConvertFrom-Json ## Switch Activity based on the Type in the Message $InvokeSplat = @{} switch ($Message.Type) { 'ActionRequest' { ## Run the Action Request $InvokeSplat = @{ ScriptBlock = $global:ScriptBlock_TaskRunspace_InvokeActionRequest ArgumentList = $Message, $global:AllowInsecureSSL, $global:Queues, $global:WebSocketClient NoNewScope = $true } try { Invoke-Command @InvokeSplat } catch { $global:Queues.WebSocketClientSend.Enqueue( ([PSCustomObject]@{ Type = 'SystemError' From = 'SessionManager Invoking ActionRequest' ErrorMessage = $_.Exception.Message StackTrace = $_.Exception.StackTrace ScriptName = $_.InvocationInfo.ScriptName ErrorLine = $_.InvocationInfo.ScriptLineNumber } | ConvertTo-Json -Depth 3) ) } } 'RemoveRunspace' { ## Run the Action Request $InvokeSplat = @{ ScriptBlock = $global:ScriptBlock_TaskRunspace_Remove_Completed ArgumentList = $Message.TMTaskId } try { Invoke-Command @InvokeSplat } catch { $global:Queues.WebSocketClientSend.Enqueue( ([PSCustomObject]@{ Type = 'SystemError' From = 'SessionManager Invoking RemoveRunspace' ErrorMessage = $_.Exception.Message StackTrace = $_.Exception.StackTrace ScriptName = $_.InvocationInfo.ScriptName ErrorLine = $_.InvocationInfo.ScriptLineNumber } | ConvertTo-Json -Depth 3) ) } } } } } ## Event Hander Definitions: StreamOutput from Task Runspaces $global:EventHandler_TaskRunspace_Streams = [scriptblock] { ## Collect the TMTaskId from the MessageData $TMTaskId = $Event.MessageData.TMTaskId ## Assign the Stream ID based on a possible redirection $StreamId = $Global:TaskStreamRedirections.$TMTaskId ?? $TMTaskId ## Create a Messages Arrays to store the incoming messages in $MessagesToProcess = [System.Collections.ArrayList]::new() $MessagesToSend = [System.Collections.ArrayList]::new() ## ## Iterate to collect any SourceArgs items and move them to a Processing array ## This is done in this fashion to quickly collect the messages for later processing ## They are not processed and sent initally, because this function must also clear the stream ## If this is done too long after the messages come in, you risk clearing unprocessed items. ## ## Save each of the Messages delivered to a separate Array foreach ($NewData in $Event.SourceArgs[0]) { ## Move the NewData item into the Processing Queue Array [void]$MessagesToProcess.Add($NewData) } ## With the SourceArgs messages safely stored, ## Clear the Event Stream $Event.Sender.clear() ## ## Process each message, collecting Tokenized messages to send. ## Sending is not done one-at-a-time, but as an array so Angular ## Has the ability to process multiple messages before publishing an ## Observable update ## Process each Message waiting to be processed foreach ($NewData in $MessagesToProcess) { ## Switch based on the type of object in the stream switch ($NewData.GetType().ToString()) { ## Error Records 'System.Management.Automation.ErrorRecord' { ## Do nothing here because the Runspace Monitoring will pick up the error and supply ## it to TMConsole to be handled. break } ## Write Progress Messages 'System.Management.Automation.ProgressRecord' { ## Ignore ActivityID -1 (Used by Invoke-WebRequest and others for temporary Progress Bars) if (` ($NewData.Activity -ne 'Reading web response')` -and ($NewData.ActivityId -ge 0) ` -and ($NewData.PercentComplete -ge 0)` ) { # Setup a Progress Message to send [void]$MessagesToSend.Add( [PSCustomObject]@{ TMTaskId = $StreamId Type = 'Progress' Message = @{ Activity = $NewData.Activity ActivityId = $NewData.ActivityID ParentActivityId = $NewData.ParentActivityId CurrentOperation = $NewData.CurrentOperation StatusDescription = $NewData.StatusDescription SecondsRemaining = $NewData.SecondsRemaining PercentComplete = $NewData.PercentComplete RecordType = $NewData.RecordType } } ) } break } <## Write-Host, Out-Host Records $NewData.MessageData is like @{ Message = 'Hello, World!' ForegroundColor = 'White' BackgroundColor = 'Black' NoNewLine = $True|$False } for standard 'Write-Host' output. Within the TMConsole.Client UI command set, there are other types of output that are plucked from this stream #> 'System.Management.Automation.InformationRecord' { ## TMConsole.Client commands may prefix output with code "||TMC:" to perform an alternate action ## These Write-Host output objects are a token with a type that is handled specifically by the TMConsole UI ## to display a beautiful component or to invoke some functionality within TMConsole. if (($NewData.MessageData.Message.Length -gt 5) -and ($NewData.MessageData.Message.substring(0, 6) -eq '||TMC:')) { ## Trim the leading characters $Message = $NewData.MessageData.Message -replace '\|\|TMC:', '' ## Convert the reaminder of the first line from JSON $TmcObject = ($Message -split "`n")[0] | ConvertFrom-Json ## Handle Different Types of TMCObjects switch ($TmcObject.Type) { ## Banners are created by Write-Banner from TMConsole.Client ## They will be displayed with a CSS styled banner componenet 'Banner' { $global:Queues.WebSocketClientSend.Enqueue( ([PSCustomObject]@{ TMTaskId = $StreamId Type = 'Banner' Message = $TmcObject } | ConvertTo-Json -Compress) ) } <# BrokerUpdate messages are created when a Broker starts a Subject Task and when a Subject ends. BrokerUpdate = @{ Type = 'BrokerUpdate' TMTaskId = TMTaskId_{actionrequest.task.id} TargetStreamId = TMTaskId_{subjecttask.task.id} } The purpose of this message is 2 fold: 1 - to record a change to the target Task ID stream that should receive the console output When a broker first starts, the Stream ID is that of the Broker. This means console/progress output emitted by the Broker is displayed in the Progress/Console _for the broker_ task. When a BrokerUpdate provides an alternate StreamID, that stream then becomes the target Task ID. Any output received by the SessionManager will be streamed to the Subject Task ID. A BrokerUpdate is also received to return the Broker to 'normal', by supplying the TargetStreamId of the Broker Task. This restores the output stream to the Broker task, not the subject. 2 - When a BrokerUpdate has differing StreamIds (meaning it's output is redirected to a subject), this also indicates that the Subject Task should be displayed in TMConsole's Task List. When this occurs, the BrokerUpdate is forwarded to Angular, so it can caretake for ensuring that the Subject Task is then brought into the TaskList view. #> 'BrokerUpdate' { ## Calculate the TMTaskId_ for the Target Stream $SubjectId = 'TMTaskId_' + $TmcObject.SubjectTaskId ## If the BrokerStarting a Redirect if ($TmcObject.Change -eq 'StartRedirect') { $Global:TaskStreamRedirections.$TMTaskId = $SubjectId } ## The Broker is ending a Stream Redirection else { ## Get an existing Stream Redirection Record for the Broker task $TMTaskId if ($Global:TaskStreamRedirections.Keys -contains $TMTaskId) { [void]$Global:TaskStreamRedirections.Remove($TMTaskId) } } ## Create a message to TMC so the UI can add the Subject Task [void]$MessagesToSend.Add($TmcObject) } 'ActionRequest' { ## Create a message to TMC so the UI can add the Subject Task $Global:Queues.SessionManager.Enqueue($Message) } } } ## Plain Write-host InformationRecord objects Else { [void]$MessagesToSend.Add([PSCustomObject]@{ TMTaskId = $StreamId Type = 'Information' Message = $NewData.MessageData } ) } break } } } ## ## Send any messages collected as one array object. ## # If there were any messages collected to send if ($MessagesToSend.Count -gt 0) { ## Convert the data to JSON $MessagesJSON = ($MessagesToSend | ConvertTo-Json -Depth 10 -Compress ) ## Send the Update to TMConsole $global:Queues.WebSocketClientSend.Enqueue($MessagesJSON) } } #endregion Event Handlers #region ScriptBlocks $global:ScriptBlock_TaskRunspace_Remove_Completed = [scriptblock] { param($TMTaskId) Unregister-Event -SourceIdentifier ($TMTaskId + '_Runspace_StateChanged') Unregister-Event -SourceIdentifier ($TMTaskID + '_Stream_Information') Unregister-Event -SourceIdentifier ($TMTaskID + '_Stream_Progress') Unregister-Event -SourceIdentifier ($TMTaskID + '_Stream_Error') ## Remove the Runspace Job try { $RSJob = Get-RSJob | Where-Object { $_.Name -like $TMTaskId + '*' } Remove-RSJob -Job $RSJob -Force -ErrorAction SilentlyContinue } catch { $global:Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{ Type = 'SystemError' From = 'Removing Runspace' Detail = 'RSJob_RemoveCompleted FAILED removing the RSJob' Error = $_.Exception.Message } | ConvertTo-Json)) } ## Report current status of Runspace Jobs $RunningRSJobs = Get-RSJob | Where-Object { $_.State -eq 'Running' } $NewStatus = @{ Type = 'powershell-server-status' Message = @{ connectionStatus = 'Connected' serverName = $WebSocketServer serverStatus = "$($RunningRSJobs.Count) Actions Running" from = 'ScriptBlock_TaskRunspace_InvokeActionRequest' } } $global:Queues.WebSocketClientSend.Enqueue(($NewStatus | ConvertTo-Json -Compress)) } ## Event Hander Definitions: Invoke Action Requests $global:ScriptBlock_TaskRunspace_InvokeActionRequest = [scriptblock] { param ( [Parameter()] [PSObject]$ActionRequest ) ## Ensure all streams are enabled... $InformationPreference = 'Continue' $VerbosePreference = 'Continue' $ProgressPreference = 'Continue' $DebugPreference = 'Continue' $WarningPreference = 'Continue' ## but Errors stop $ErrorActionPreference = 'Stop' ## Rename the Variable to make the invocation logic more clear $TMTaskID = 'TMTaskId_' + [string]$ActionRequest.task.id ## Send a set of Startup Messages $StatusMessages = @( [PSCustomObject]@{ TMTaskId = $TMTaskId Type = 'TaskStarted' }, [PSCustomObject]@{ TMTaskId = $TMTaskID Type = 'Progress' Message = [PSCustomObject]@{ ActivityId = 0 ParentActivityId = -1 Activity = 'Queued Task: ' + $ActionRequest.task.taskNumber + ' - ' + $ActionRequest.task.title CurrentOperation = '' StatusDescription = '' PercentComplete = 0 SecondsRemaining = -1 RecordType = 0 } } ) ## Send the StatusMessages to TMConsole $global:Queues.WebSocketClientSend.Enqueue(($StatusMessages | ConvertTo-Json -Depth 10 -Compress)) ## Prepare the Runspace Job Options ## and start the Runspace Job try { ## Create an appropriate Runspace Name $TaskJobName = [String]([string]$TMTaskID + '_' + (Get-Date -Format 'FileDateTimeUniversal')) ## Before Starting the RSJob, Make sure the Provider Module is imported into ## this TMD session so it's loaded and available to supply to any future Provider Tasks ## Include TMD and TM, and add any provider modules $ModulesToImport = @('TMConsole.Client', 'TMD.Common', 'TransitionManager') $JobParams = @{ Name = $TaskJobName ArgumentList = @($ActionRequest, $global:AllowInsecureSSL) ModulesToImport = $ModulesToImport ScriptBlock = $global:ActionRequestInnerJobScriptBlock } ## Start the RS Job $RSJob = Start-RSJob @JobParams ## Send a set of Started Up Messages $StatusMessages = @( [PSCustomObject]@{ TMTaskId = $TMTaskID Type = 'Progress' Message = [PSCustomObject]@{ ActivityId = 0 ParentActivityId = -1 Activity = 'Starting Task: ' + $ActionRequest.task.taskNumber + ' - ' + $ActionRequest.task.title CurrentOperation = 'Starting' StatusDescription = '' PercentComplete = 0 SecondsRemaining = -1 RecordType = 0 } } ) ## Send the StatusMessages to TMConsole $global:Queues.WebSocketClientSend.Enqueue(($StatusMessages | ConvertTo-Json -Depth 10 -Compress)) ## ## RS Job Output Event Handlers ## ## Create a Common Streams Output Event Splat ## Used for Info, Debug, Progress, Error, Verbose and Warning $StreamsObjectEventSplat = @{ EventName = 'DataAdded' Action = $global:EventHandler_TaskRunspace_Streams MessageData = @{ TMTaskId = $TMTaskID Queues = $global:Queues } } ## Create an event for Information Stream Output $ActivitySplat = @{ InputObject = $RSJob.InnerJob.Streams.Information SourceIdentifier = ($TMTaskID + '_Stream_Information') } [void](Register-ObjectEvent @StreamsObjectEventSplat @ActivitySplat) ## Create an event for Progress Stream Output $ActivitySplat = @{ InputObject = $RSJob.InnerJob.Streams.Progress SourceIdentifier = ($TMTaskID + '_Stream_Progress') } [void](Register-ObjectEvent @StreamsObjectEventSplat @ActivitySplat) ## Create an event for Progress Error Output $ActivitySplat = @{ InputObject = $RSJob.InnerJob.Streams.Error SourceIdentifier = ($TMTaskID + '_Stream_Error') } [void](Register-ObjectEvent @StreamsObjectEventSplat @ActivitySplat) ## Register Startup and Output events $RunspaceStateChangedEventSplat = @{ SourceIdentifier = ($TMTaskID + '_Runspace_StateChanged') EventName = 'InvocationStateChanged' InputObject = $RSJob.InnerJob Action = $global:EventHandler_TaskRunspace_StateChanged MessageData = @{ TMTaskId = $TMTaskID Queues = $global:Queues ActionRequest = $ActionRequest } } [void](Register-ObjectEvent @RunspaceStateChangedEventSplat) ## ## Update the Action Counter in the UI ## ## Get the list of RS Jobs in progress to report Session Manager Status $RSJobs = Get-RSJob | Where-Object { $_.State -eq 'Running' } ## Update PowershellServerStatus $NewStatus = @{ Type = 'powershell-server-status' Message = @{ connectionStatus = 'Connected' serverName = $WebSocketServer serverStatus = "$($RSJobs.Count) Actions Running" from = 'ScriptBlock_TaskRunspace_InvokeActionRequest - Started New Action' } } $global:Queues.WebSocketClientSend.Enqueue(($NewStatus | ConvertTo-Json -Compress)) } catch { $NewStatus = @{ Type = 'SystemError' From = 'Starting ActionRequest RunspaceJob' Message = 'ScriptBlock_TaskRunspace_InvokeActionRequest - Invocation Error' Exception = $_.Exception.Message } $global:Queues.WebSocketClientSend.Enqueue(($NewStatus | ConvertTo-Json -Compress)) } } $global:EventHandler_TaskRunspace_StateChanged = [scriptblock] { ## Collect the TMTaskId from the MessageData $TMTaskId = $Event.MessageData.TMTaskId ## Assign the Stream ID based on a possible redirection $StreamId = $Global:TaskStreamRedirections.$TMTaskId ?? $TMTaskId ## Process each Event Item $Event.SourceArgs | ForEach-Object { ## Name the variable for convenience $NewData = $_ $NewDataType = $NewData.GetType().ToString() ## The type of Raised Event determines what to do switch ($NewDataType) { ## Handle a PowerShell (session) object 'System.Management.Automation.PowerShell' { ## Get the Job to determine if there's more data $RSJob = Get-RSJob | Where-Object { $_.Name -like $TMTaskId + '*' } ## Switch on the Invocation State switch ($NewData.InvocationStateInfo.State.ToString()) { 'Completed' { # Send a Progress Activity $StatusMessages = @( [PSCustomObject]@{ TMTaskId = $StreamId Type = 'Progress' Message = [PSCustomObject]@{ ActivityId = 0 ParentActivityId = -1 Activity = 'Task Completed' CurrentOperation = 'Complete' StatusDescription = '' PercentComplete = 100 SecondsRemaining = -1 RecordType = 1 } }, [PSCustomObject]@{ TMTaskId = $StreamId Type = 'TaskCompleted' RSJobName = $RSJob.Name } ) ## Send the StatusMessages to TMConsole $global:Queues.WebSocketClientSend.Enqueue(($StatusMessages | ConvertTo-Json -Compress)) ## SessionManager message $SessionManagerMessage = @{ TMTaskId = $TMTaskId Type = 'RemoveRunSpace' } | ConvertTo-Json $global:Queues.SessionManager.Enqueue($SessionManagerMessage) } 'Failed' { ## Capture the error message to send to TMConsole $InvocationError = $NewData.InvocationStateInfo.Reason.ErrorRecord.Exception.Message # Send a Progress Activity $StatusMessages = @( [PSCustomObject]@{ TMTaskId = $StreamId TMTaskNumber = $Event.MessageData.ActionRequest.task.taskNumber Type = 'Error' Message = $InvocationError }, [PSCustomObject]@{ TMTaskId = $StreamId Type = 'Progress' Message = [PSCustomObject]@{ ActivityId = 0 ParentActivityId = -1 Activity = 'Task Failed' CurrentOperation = 'Failed' StatusDescription = '' PercentComplete = 100 SecondsRemaining = -1 RecordType = 2 } }, [PSCustomObject]@{ TMTaskId = $StreamId TMTaskNumber = $Event.MessageData.ActionRequest.task.taskNumber Type = 'TaskFailed' Message = $InvocationError } ) ## Send the StatusMessages to TMConsole $global:Queues.WebSocketClientSend.Enqueue(($StatusMessages | ConvertTo-Json -Compress)) ## SessionManager message $SessionManagerMessage = @{ TMTaskId = $TMTaskId Type = 'RemoveRunSpace' } | ConvertTo-Json $global:Queues.SessionManager.Enqueue($SessionManagerMessage) # Report the task as failed to TM and add the error message as a task note Set-TMTaskOnHold -ActionRequest $Event.MessageData.ActionRequest -Message ('Action Error: ' + $InvocationError) } } break } # Allow for and expect PSInvocationStateChangedEventArgs 'System.Management.Automation.PSInvocationStateChangedEventArgs' { ## This State Changed Object is Redundent. The 'PowerShell' object that is also passed ## Contains all of the information needed and this Event Args can safely be ignored break } ## Handle anything that wasn't a known type Default { Write-Host "Received an unhandled Object!! $($NewData.GetType().ToString())" -ForegroundColor Red Write-Host "`t$($NewData | ConvertTo-Json -EnumsAsStrings -Depth 3)" -ForegroundColor Red } } } } ## Build up the proper runspace invocation that isn't otherwise working $global:ActionRequestInnerJobScriptBlock = [scriptblock] { param( $ActionRequest, $AllowInsecureSSL ) ## Ensure all streams are enabled $InformationPreference = 'Continue' $VerbosePreference = 'Continue' $ProgressPreference = 'Continue' $DebugPreference = 'Continue' $WarningPreference = 'Continue' $ErrorActionPreference = 'Continue' ## Sleep long enough to let the Event Handler attach Start-Sleep -Milliseconds 200 # ## Import the TMC Action Request, which also loads Provider Modules . Import-TMCActionRequest -PSObjectActionRequest $ActionRequest # ## Create a Parameters Variable from the Action Script New-Variable -Name Params -Scope Global -Value $ActionRequest.params -Force ## Add $Credential if there is one if ($ActionRequest.PSCredential) { New-Variable -Scope Global -Name Credential -Value $ActionRequest.PSCredential -Force } ## Enable Logging if ($ActionRequest.logPath) { ## Trim any quote characters from the Log Path $RootLogPath = $ActionRequest.logPath.trim('"').trim("'") Test-FolderPath -FolderPath $RootLogPath ## Create the Transcript Folder Path $ProjectFolder = Join-Path -Path $RootLogPath -ChildPath ($Global:TM.Server.Url -replace '.transitionmanager.net', '') -AdditionalChildPath $Global:TM.Project.Name, $Global:TM.Event.Name Test-FolderPath -FolderPath $ProjectFolder ## Create a unique file in the Transcript Folder $TranscriptFileName = ( (Get-Date -Format FileDateTimeUniversal) + '_TaskNumber-' + $Global:TM.Task.TaskNumber + '_TaskId-' + $Global:TM.Task.Id + '.txt' ) $TranscriptFilePath = Join-Path $ProjectFolder $TranscriptFileName ## Start a transcript for this session $TranscriptSplat = @{ Path = $TranscriptFilePath IncludeInvocationHeader = $True Confirm = $False Force = $True Append = $True } Start-Transcript @TranscriptSplat } ## Run the User Provided Script try { $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew() ## Invoke the User Script block $ActionScriptBlock = [scriptblock]::Create($ActionRequest.options.apiAction.script) Invoke-Command -ScriptBlock $ActionScriptBlock -ErrorAction 'Stop' -NoNewScope $Stopwatch.Stop() Write-Host -Message "Task completed in: " -NoNewline Write-Host -Message (Get-TimeSpanString -Timespan $Stopwatch.Elapsed) -ForegroundColor Green } catch { $Stopwatch.Stop() throw $_ } ## Create a Data Options parameter for the Complete-TMTask command $CompleteTaskParameters = @{} ## Check the Global Variable for any TMAssetUpdates to send to TransitionManager during the task completion if ($Global:TMAssetUpdates) { $CompleteTaskParameters = @{ Data = @{ assetUpdates = $Global:TMAssetUpdates } } } ## Add SSL Exception if necessary if ($global:AllowInsecureSSL) { $CompleteTaskParameters | Add-Member -NotePropertyName 'AllowInsecureSSL' -NotePropertyValue $True } ## Complete the TM Task, sending Updated Data values for the task Asset if ($ActionRequest.HostPID -ne -1) { Complete-TMTask -ActionRequest $ActionRequest @CompleteTaskParameters } ## End the log for this session if ($ActionRequest.LogPath) { Stop-Transcript } } #endregion ScriptBlocks } process { try { ## Create the WebSocket Send Queue as a Synchronized Queue so Event Handlers can access it ## Write-Verbose 'Creating Message Queues' $global:Queues = @{ WebSocketClientSend = New-Object 'TMCSessionQueue[String]' WebSocketClientReceive = New-Object 'TMCSessionQueue[String]' SessionManager = New-Object 'TMCSessionQueue[String]' } ## Write-Verbose 'Registering Queue Event Handlers' $EventHandlerSplat = @{ EventName = 'Enqueued' InputObject = $global:Queues.WebSocketClientSend SourceIdentifier = 'Websocket_Send' Action = $EventHandler_WebSocket_SendData } [void](Register-ObjectEvent @EventHandlerSplat) $EventHandlerSplat = @{ EventName = 'Enqueued' InputObject = $global:Queues.SessionManager SourceIdentifier = 'SessionManager_Enqueued' Action = $EventHandler_SessionManager_Enqueued } [void](Register-ObjectEvent @EventHandlerSplat) $global:CancellationToken = New-Object System.Threading.CancellationToken $global:TaskStreamRedirections = @{} ## Create Send and Receive queues for the Web Socket $global:WebSocketClient = [WatsonWebsocket.WatsonWsClient]::new($WebSocketServer, $WebSocketPort, $true) $EventHandlerSplat = @{ EventName = 'MessageReceived' InputObject = $global:WebSocketClient SourceIdentifier = 'Websocket_MessageReceived' Action = $EventHandler_WebSocket_ReceiveData } [void](Register-ObjectEvent @EventHandlerSplat) ## Establish the WebSocket connection try { $global:WebSocketClient.Start() } catch { throw "Connection Faulted: $($_.Exception.Message)" } if (-not $global:WebSocketClient.Connected) { throw "PowerShell was unable to connect to TMConsole WebSocketServer at $($WebSocketServer):$($WebSocketPort)" } ## Update PowershellServerStatus $global:Queues.WebSocketClientSend.Enqueue( ([PSCustomObject]@{ Type = 'powershell-server-status' Message = @{ connectionStatus = 'Connected' serverName = $WebSocketServer serverStatus = '0 Actions Running' from = 'WebSocket Client Connected' } } | ConvertTo-Json -Compress) ) ## Send a Keep Alive message $KeepAliveCounter = 0 do { if ($TMCVersion -ge '2.3.0') { if ($KeepAliveCounter -eq 100) { # Send a Keep alive $global:Queues.WebSocketClientSend.Enqueue( ([PSCustomObject]@{ Type = 'powershell-server-keepalive' Message = @{ datetime = Get-Date -Format FileDateTimeUniversal } } | ConvertTo-Json -Compress) ) $KeepAliveCounter = 0 } else { $KeepAliveCounter++ } } ## Sleep ~0.3 Seconds Start-Sleep -Milliseconds 300 } until (-Not $global:WebSocketClient.Connected) } catch { throw $_ } finally { ## Disconnect and close down if ($global:WebSocketClient) { $global:WebSocketClient.Stop() $global:WebSocketClient.Dispose() } } } } |