lib/WebSocketClient.ps1
function Start-TMConsoleWebSocketClient { [CmdletBinding()] param ( [Parameter(Mandatory = $True)] [String]$WebSocketServer, [Parameter(Mandatory = $True)] [Int]$WebSocketPort = 8620, [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] { Write-Verbose 'Sending Data on WebSocket Connection' $Message = $null if ( $global:Queues.WebSocketClientSend.TryDequeue([ref] $Message)) { ## Write to Output if ($global:OutputVerbose) { Write-Output "EventHandler_WebSocket_SendData Sending: $Message" } Write-Verbose $Message # $global:WebSocketClient.SendAndWaitAsync($Message, $global:WebsocketOperationTimeout, $global:CancellationToken) $global:WebSocketClient.SendAsync($Message, [System.Net.WebSockets.WebSocketMessageType]::Text, $global:CancellationToken) } } ## Event handler to receive data from the server $EventHandler_WebSocket_ReceiveData = [scriptblock] { Write-Verbose 'Receiving Data on WebSocket Connection' try { ## Write to Output if ($global:OutputVerbose) { Write-Verbose 'Starting EventHandler_WebSocket_ReceiveData' } ## Create an Event to receive the next Character Array $Message = '' $Event.SourceEventArgs.Data | ForEach-Object { $Message = $Message + [char]$_ } Write-Verbose $Message ## 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) ) Write-Host $_ } } ## Event handler process messages that are added to the sessionmanager queue $EventHandler_SessionManager_Enqueued = [scriptblock] { Write-Verbose 'Session Manager Received an Message' $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) ) } } default {} } } } ## Event Hander Definitions: StreamOutput from Task Runspaces $global:EventHandler_TaskRunspace_Streams = [scriptblock] { ## Write to Output if ($global:OutputVerbose) { Write-Output 'Starting EventHandler_TaskRunspace_Streams' } ## Collect the TMTaskId from the MessageData $TMTaskId = $Event.MessageData.TMTaskId Write-Verbose "Runspace Produced Output $TMTaskID" # $global:Queues = $Event.MessageData.Queues ## 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-Verbose Messages 'System.Management.Automation.VerboseRecord' { # [void]$MessagesToSend.Add( # [PSCustomObject]@{ # TMTaskId = $StreamId # Type = 'Verbose' # Message = $NewData.Message # } # ) break } ## Write-Warning Records 'System.Management.Automation.WarningRecord' { [void]$MessagesToSend.Add( [PSCustomObject]@{ TMTaskId = $StreamId Type = 'Warning' Message = $NewData.Message } ) break } ## Write-Debug Records 'System.Management.Automation.DebugRecord' { # [void]$MessagesToSend.Add( # [PSCustomObject]@{ # TMTaskId = $StreamId # Type = 'Debug' # Message = $NewData.Message # } # ) 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 } ## Error Records Default { if ($global:OutputVerbose) { Write-Output ('Unknown Data: ' + ($NewData | ConvertTo-Json)) } } } } ## ## 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) } ## Write to Output if ($global:OutputVerbose) { Write-Output 'Ending EventHandler_TaskRunspace_Streams' } } #endregion Event Handlers #region ScriptBlocks $global:ScriptBlock_TaskRunspace_Remove_Completed = [scriptblock] { param($TMTaskId) Write-Verbose "Removing Runspace $TMTaskID" ## Write to Output if ($global:OutputVerbose) { Write-Output "Running ScriptBlock_TaskRunspace_Remove_Completed for $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') Unregister-Event -SourceIdentifier ($TMTaskID + '_Stream_Verbose') Unregister-Event -SourceIdentifier ($TMTaskID + '_Stream_Debug') Unregister-Event -SourceIdentifier ($TMTaskID + '_Stream_Warning') ## 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)) ## Write to Output if ($global:OutputVerbose) { Write-Output "Ending ScriptBlock_TaskRunspace_Remove_Completed for $TMTaskId" } } ## Event Hander Definitions: Invoke Action Requests $global:ScriptBlock_TaskRunspace_InvokeActionRequest = [scriptblock] { param ( [Parameter()] [PSObject]$ActionRequest ) ## Write to Output if ($global:OutputVerbose) { Write-Output 'Starting ScriptBlock_TaskRunspace_InvokeActionRequest' } ## Ensure all streams are enabled, but Errors stop $InformationPreference = 'Continue' $VerbosePreference = 'Continue' $ProgressPreference = 'Continue' $DebugPreference = 'Continue' $WarningPreference = 'Continue' $ErrorActionPreference = 'Continue' ## 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)) ## Else (Not needed because of the return above) ## Prepare the Runspace Job Options ## and start the Runspace Job try { ## Write to Output if ($global:OutputVerbose) { Write-Output 'Sending WebSocket Message and Starting Runspace Job' } ## 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) ## Create an event for Progress Verbose Output $ActivitySplat = @{ InputObject = $RSJob.InnerJob.Streams.Verbose SourceIdentifier = ($TMTaskID + '_Stream_Verbose') } [void](Register-ObjectEvent @StreamsObjectEventSplat @ActivitySplat) ## Create an event for Progress Debug Output $ActivitySplat = @{ InputObject = $RSJob.InnerJob.Streams.Debug SourceIdentifier = ($TMTaskID + '_Stream_Debug') } [void](Register-ObjectEvent @StreamsObjectEventSplat @ActivitySplat) ## Create an event for Progress Warning Output $ActivitySplat = @{ InputObject = $RSJob.InnerJob.Streams.Warning SourceIdentifier = ($TMTaskID + '_Stream_Warning') } [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)) } ## Write to Output if ($global:OutputVerbose) { Write-Output 'Ending ScriptBlock_TaskRunspace_InvokeActionRequest' } } $global:EventHandler_TaskRunspace_StateChanged = [scriptblock] { ## Write to Output if ($global:OutputVerbose) { Write-Output 'Starting EventHandler_TaskRunspace_StateChanged' } ## Collect the TMTaskId from the MessageData $TMTaskId = $Event.MessageData.TMTaskId Write-Verbose "Runspace State Changed $TMTaskID" # $global:Queues = $Event.MessageData.Queues ## 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 + '*' } ## Write to Output if ($global:OutputVerbose) { Write-Output "EventHandler_TaskRunspace_StateChanged -- State Change: $RSJob" } ## Switch on the Invocation State switch ($NewData.InvocationStateInfo.State.ToString()) { 'Completed' { # if ($RSJob.HasMoreData) { # $Output = $RSJob | Receive-RSJob # $Output | ConvertTo-Json -Depth 5 -Compress | Write-Host -ForegroundColor green # } # 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) } Default { if ($global:OutputVerbose) { Write-Output "TMTask: $TMTaskId has some other state: $($NewData.InvocationState.State.ToString())" } } } break } # Handle a 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 } } } ## Write to Output if ($global:OutputVerbose) { Write-Output 'Ending EventHandler_TaskRunspace_StateChanged' } } ## 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' ## Write to Output if ($global:OutputVerbose) { Write-Output 'Starting ActionRequestInnerJobScriptBlock' } ## Sleep long enough to let the Event Handler attach Start-Sleep -Milliseconds 200 ## Write a Verbose Message starting the task Write-Verbose ('Starting Task TMTaskId_' + [string]$ActionRequest.task.taskNumber + ': ' + $ActionRequest.task.title) # ## 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("'") Write-Verbose "Creating Logging Folder at: $RootLogPath" Test-FolderPath -FolderPath $RootLogPath ## Create the Transcript Folder Path $ProjectFolder = Join-Path $RootLogPath ($Global:TM.Server.Url -replace '.transitionmanager.net', '') $Global:TM.Project.Name $Global:TM.Event.Name Test-FolderPath -FolderPath $ProjectFolder Write-Verbose "Transcript Folder: $($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' ) Write-Verbose "File Name: $TranscriptFileName" $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 ## Write a verbose message for transcript Write-Verbose "Invoking TransitionManager ActionRequest at: $(Get-Date)" Write-Verbose ($Global:TM | ConvertTo-Json -Depth 100) ## Write a verbose message for transcript Write-Verbose 'Action Request Parameters' Write-Verbose ($Params | ConvertTo-Json -Depth 100) } ## Invoke the User Script block $ActionScriptBlock = [scriptblock]::Create($ActionRequest.options.apiAction.script) ## Run the User Provided Script try { Invoke-Command -ScriptBlock $ActionScriptBlock -ErrorAction 'Stop' -NoNewScope } catch { 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 } ## Write to Output if ($global:OutputVerbose) { Write-Output 'Ending ActionRequestInnerJobScriptBlock' } ## 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 $WssEndpoint = "wss://$($WebSocketServer):$($WebSocketPort)" $global:WebSocketClient = [WatsonWebsocket.WatsonWsClient]::new($WebSocketServer, $WebSocketPort, $true) Write-Verbose 'Registering WebSocket Event Handlers' $EventHandlerSplat = @{ EventName = 'MessageReceived' InputObject = $global:WebSocketClient SourceIdentifier = 'Websocket_MessageReceived' Action = $EventHandler_WebSocket_ReceiveData } [void](Register-ObjectEvent @EventHandlerSplat) ## Establish the WebSocket connection try { Write-Verbose "Connecting to WSS Endpoint: $WssEndpoint" $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)" } else { Write-Verbose 'PowerShell SessionManager is connected to TMConsole WebSocketServer' } ## 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() } } } } |