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 = "TMTaskId_$($Global:TM.Task.Id)_$((Get-Date -Format FileDateTimeUniversal).ToString()).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'
                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()
            }
        }
    }
}