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)]
        [string]$WebSocketEncryptedAccessKey,

        [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'
        }

        ## Define the Script Block that is used to wrap and invoke the ActionRequest
        $global:ScriptBlock_ActionRequestInnerJobScriptBlock = [scriptblock] {
            param(
                $ActionRequest,
                $AllowInsecureSSL,
                [EventQueue[psobject]]$TMConsoleActionRequestQueue
            )
            
            ## Ensure all streams are disabled
            $InformationPreference = 'SilentlyContinue'
            $VerbosePreference = 'SilentlyContinue'
            $ProgressPreference = 'SilentlyContinue'
            $DebugPreference = 'SilentlyContinue'
            $WarningPreference = 'SilentlyContinue'
            $Global:InformationPreference = 'SilentlyContinue'
            $Global:VerbosePreference = 'SilentlyContinue'
            $Global:ProgressPreference = 'SilentlyContinue'
            $Global:DebugPreference = 'SilentlyContinue'
            $Global:WarningPreference = 'SilentlyContinue'
            $ErrorActionPreference = 'SilentlyContinue'
            
            ## Import the TMC Action Request
            . Import-TMCActionRequest -PSObjectActionRequest $ActionRequest
            
            # ## Create a Parameters Variable from the Action Script
            New-Variable -Name Params -Scope Global -Value $ActionRequest.params -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 ($Global:TM.Server.Url -replace '.transitionmanager.net', '') $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
            }

            ## Create a Data Options parameter for the Complete-TMTask command
            $CompleteTaskParameters = @{}
            
            ## Add SSL Exception if necessary
            if ($global:AllowInsecureSSL) {
                $CompleteTaskParameters | Add-Member -NotePropertyName 'AllowInsecureSSL' -NotePropertyValue $True
            }

            $InvocationError = $Null
            $InvocationErrorText = $Null
            ## Run the User Provided Script
            try {
                ## Invoke the User Script block
                $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
                $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()
                $InvocationError = $_
                $InvocationErrorText = $_?.Exception?.Message ?? $_ ?? "Error"
            }
            
            ## Check the Global Variable for any TMAssetUpdates to send to TransitionManager during the task completion
            if ($Global:TMAssetUpdates) {
                $CompleteTaskParameters['Data'] = @{
                    assetUpdates = $Global:TMAssetUpdates
                }
            }
           
            ## End the log for this session
            if ($ActionRequest.LogPath) {
                Stop-Transcript
            }
            
            if ($InvocationErrorText) {
                Write-Host "Action Error: $InvocationErrorText" -ForegroundColor Red 
                Set-TMTaskOnHold -ActionRequest $ActionRequest -Message ('Action Error: ' + $InvocationErrorText) @CompleteTaskParameters
                throw $InvocationError
            }
            else {
                Complete-TMTask -ActionRequest $ActionRequest @CompleteTaskParameters
            }
        }
    }

    process {
        try {
            ## Define the global TMConsole dataset
            $Global:TMConsole = @{
                
                ## TMC PS Operation Queues
                ActionRequests       = New-Object 'EventQueue[PSObject]'
                TerminateTaskActions = New-Object 'EventQueue[PSObject]'
                RemoveRunspaces      = New-Object 'TMCSessionQueue[PSObject]'
                
                ## UI Message Queues - Major events
                TaskChanges          = New-Object 'TMCSessionQueue[PSObject]'
                SystemErrors         = New-Object 'TMCSessionQueue[PSObject]'
                
                ## UI Message Queues - Aggregate events
                TaskOutput           = New-Object 'TMCSessionQueue[PSObject]'
                TaskProgress         = New-Object 'TMCSessionQueue[PSObject]'

                ## Runspaces manifest
                Runspaces            = [pscustomobject]@{}
            }
            $ActionRequestEventHandler = @{
                EventName        = 'ItemEnqueued'
                InputObject      = $Global:TMConsole.ActionRequests
                SourceIdentifier = 'TMC_ActionRequest_Enqueued'
                Action           = [scriptblock] {
                    param (
                        [Parameter()]
                        [PSObject]$ActionRequest
                    )
                    ## Rename the Variable to make the invocation logic more clear
                    $TMTaskID = 'TMTaskId_' + [string]$ActionRequest.task.id
                    $TMTaskNumber = $ActionRequest.task.taskNumber.ToString()
                    
                    ## Send the StatusMessages to TMConsole
                    $Global:TMConsole.TaskChanges.Enqueue(
                        [PSCustomObject]@{
                            TMTaskId = $TMTaskId
                            Type     = 'TaskStarted'
                        }
                    )

                    $Global:TMConsole.TaskProgress.Enqueue(
                        [PSCustomObject]@{
                            TMTaskId = $TMTaskID
                            Type     = 'Progress'
                            Message  = [PSCustomObject]@{
                                ActivityId        = 0
                                ParentActivityId  = -1
                                Activity          = 'Starting Task: ' + $TMTaskNumber + ' - ' + $ActionRequest.task.title
                                CurrentOperation  = 'Starting'
                                StatusDescription = ''
                                PercentComplete   = 0
                                SecondsRemaining  = -1
                                RecordType        = 0
                            }
                        }    
                    )

                    ##
                    ## Create the Runspace
                    ##
                    $Powershell = [powershell]::Create()
                    $Powershell.RunspacePool = $Global:RunspacePool
                    
                    ## Add a $TMTaskId variable so the Pipeline knows who the output belongs to
                    [void]($Powershell.AddScript(('$TMTaskId = "{0}"' -f $TMTaskID)))
                    [void]($PowerShell.AddScript({ [TMCPSHost]::CurrentTMTaskId.Value = $ExecutionContext.SessionState.PSVariable.GetValue("TMTaskId") }))

                    [void]($Powershell.AddScript($global:ScriptBlock_ActionRequestInnerJobScriptBlock))
                    [void]($Powershell.AddArgument($ActionRequest))
                    [void]($Powershell.AddArgument($global:AllowInsecureSSL))
                    [void]($Powershell.AddArgument($global:TMConsole.ActionRequests))
                    
                    ## Start the Runspace and record the Async handle
                    try {
                        $AddMemberSplat = @{
                            InputObject      = $Global:TMConsole.Runspaces 
                            NotePropertyName = $TMTaskID 
                            Force            = $True
                        }
                        [void](Add-Member @AddMemberSplat -NotePropertyValue @{
                                TMTaskId     = $TMTaskId
                                TMTaskNumber = $TMTaskNumber
                                ErrorText    = ''
                                Runspace     = $Powershell 
                                AsyncHandle  = $Powershell.BeginInvoke()
                            }
                        )
                        [TMCRunspaceDirectory]::ActiveRunspaces[$TMTaskID] = 'Running'
                    }
                    catch {
                        Write-Host -ForegroundColor "Red" "ScriptBlock_TaskRunspace_InvokeActionRequest - Invocation Error $_"
                        $Global:TMConsole.SystemErrors.Enqueue(
                            [PSCustomObject]@{
                                Type      = 'SystemError'
                                From      = 'Starting ActionRequest RunspaceJob'
                                Message   = 'ScriptBlock_TaskRunspace_InvokeActionRequest - Invocation Error'
                                Exception = $_.Exception.Message
                            }
                        )
                    }

                }
            }
            [void](Register-ObjectEvent @ActionRequestEventHandler)

            $TerminateTaskActionEventHandler = @{
                EventName        = 'ItemEnqueued'
                InputObject      = $Global:TMConsole.TerminateTaskActions
                SourceIdentifier = 'TMC_TerminateTaskAction_Enqueued'
                Action           = [scriptblock] {
                    param($TMTask)
                    $TMTaskId = "TMTaskId_$($TMTask.task.id)"
                    try {

                        ## Send Terminated task output to the console in TMC
                        $Global:TMConsole.TaskProgress.Enqueue([PSCustomObject]@{
                                TMTaskId = $TMTaskId
                                Type     = 'Progress'
                                Message  = [PSCustomObject]@{
                                    ActivityId        = 0
                                    ParentActivityId  = -1
                                    Activity          = 'Task Terminated'
                                    CurrentOperation  = 'Terminated!'
                                    StatusDescription = ''
                                    PercentComplete   = -1
                                    SecondsRemaining  = -1
                                    RecordType        = 1
                                }
                            }
                        )
                        $Global:TMConsole.TaskOutput.Enqueue(
                            ## Force a line break
                            [PSCustomObject]@{
                                TMTaskId = $TMTaskId
                                Type     = 'Information'
                                Message  = @{
                                    Message         = ""
                                    NoNewLine       = $False
                                    ForegroundColor = 'White'
                                    BackgroundColor = 'Black'
                                }
                            }
                        )
                        $Global:TMConsole.TaskOutput.Enqueue(
                            ## Print a message that the task was terminated
                            [PSCustomObject]@{
                                TMTaskId = $TMTaskId
                                Type     = 'Information'
                                Message  = @{
                                    Message         = "Task has been terminated!"
                                    NoNewLine       = $False
                                    ForegroundColor = "Red"
                                    BackgroundColor = "Black"
                                }
                            }
                        )
                        try {
                            $Global:TMConsole.Runspaces.$TMTaskId.Runspace.BeginStop(
                                $Null, 
                                $Global:TMConsole.Runspaces.$TMTaskId.Runspace.AsyncHandle
                            )
                        }
                        catch { }
                        $Global:TMConsole.RemoveRunspaces.Enqueue($TMTaskId)
                    }
                    catch {
                        Write-Host -ForegroundColor "Red" "RSJob_TerminateTaskAction FAILED removing the RSJob $_"
                        $Global:TMConsole.SystemErrors.Enqueue(
                            [PSCustomObject]@{
                                Type   = 'SystemError'
                                From   = 'Terminating Runspace'
                                Detail = 'RSJob_TerminateTaskAction FAILED removing the RSJob'
                                Error  = $_.Exception.Message
                            }
                        )
                    }
                }
            }
            [void](Register-ObjectEvent @TerminateTaskActionEventHandler)
           
            ##
            ## Prepare Runspaces for execution
            ##
            $InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
            $InitialSessionState.ImportPSModule('TMD.Common')
            $InitialSessionState.ImportPSModule('TransitionManager')
            $TMCPsHost = New-Object -TypeName TMCPsHost
            $MaxRunspaces = [Math]::Min([Environment]::ProcessorCount * 8, 64)
            $global:RunspacePool = [runspacefactory]::CreateRunspacePool(
                1, 
                $MaxRunspaces,
                $InitialSessionState,
                $TMCPsHost
            )
            $global:RunspacePool.ApartmentState = "MTA"
            $global:RunspacePool.Open()

            ##
            ## Build the Web Socket Client and connect
            ##
            $global:CancellationToken = New-Object System.Threading.CancellationToken
            $global:WebSocketClient = [WatsonWebsocket.WatsonWsClient]::new($WebSocketServer, $WebSocketPort, $true)
            $WebSocketReceiveMessageEventHandler = @{
                InputObject      = $global:WebSocketClient
                EventName        = 'MessageReceived'
                SourceIdentifier = 'TMC_Websocket_MessageReceived'
                Action           = [scriptblock] {
                    try {

                        ## Create an Event to receive the next Character Array
                        $MessageString = ''
                        $Event.SourceEventArgs.Data | ForEach-Object {
                            $MessageString += [char]$_
                        }
                        $Message = $MessageString | ConvertFrom-Json

                        ## Queue the Message in the appropriate Queue
                        switch ($Message.Type) {
                            'ActionRequest' {
                                ## Run the Action Request
                                $Global:TMConsole.ActionRequests.Enqueue($Message)
                                break
                            }
                            'TerminateTaskAction' {
                                ## Run the Action Request
                                $Global:TMConsole.TerminateTaskActions.Enqueue($Message)
                                break
                            }
                        }
                    }
                    catch {
                        Write-Host -ForegroundColor "Red" "EventHandler_WebSocket_ReceiveData $_"
                        $Global:TMConsole.SystemErrors.Enqueue(
                            [PSCustomObject]@{
                                Type  = 'Debug THROWN ERROR'
                                Error = $_
                            }
                        )
                    }
                }
            }
            [void](Register-ObjectEvent @WebSocketReceiveMessageEventHandler)

            ##
            ## Establish the WebSocket connection
            ##
            try {
                Write-Verbose $WebSocketEncryptedAccessKey
                $Cookie = New-Object System.Net.Cookie
                $Cookie.Name = 'tm-access-key'
                $Cookie.Value = $WebSocketEncryptedAccessKey
                $Cookie.Domain = $WebSocketServer
                $global:WebSocketClient.AddCookie($Cookie)
                $global:WebSocketClient.Start()
            }
            catch {
                Write-Host -ForegroundColor "Red" "Connection Faulted: $($_.Exception.Message)"
                throw "Connection Faulted: $($_.Exception.Message)"
            }
            
            if (-not $global:WebSocketClient.Connected) {
                Write-Host -ForegroundColor "Red" "PowerShell was unable to connect to TMConsole WebSocketServer at $($WebSocketServer):$($WebSocketPort)"
                throw "PowerShell was unable to connect to TMConsole WebSocketServer at $($WebSocketServer):$($WebSocketPort)"
            }
            
            ## Warm up each runspace so the modules load
            1..$MaxRunspaces | ForEach-Object {
                $PrepShell = [powershell]::Create()
                $PrepShell.RunspacePool = $Global:RunspacePool
                $Null = $PrepShell.AddScript('').Invoke()
            }
            
            ##
            ## Perform primary loop,
            ## Receives ActionRequets and TerminateTask items from Angular
            ## Sends UI update messages to Angular
            ## Manages RunspacePool
            ##
            $LastBatchSend = [datetime]::UtcNow
            $LastRunningActionCount = -1
            $MinBatchSize = 5
            $MaxBatchSize = 50
            $MaxBatchLatency = 50
            $SleepInterval = 10
            $MessageBatch = [System.Collections.Generic.List[object]]@()

            do {
                
                ## Check for any completed runspaces
                foreach ($TMTaskID in $Global:TMConsole.Runspaces.PSObject.Properties.Name) {
                    $TMRunspace = $Global:TMConsole.Runspaces.$TMTaskID
                    if ($TMRunspace.AsyncHandle.IsCompleted) {
                        if ($TMRunspace.Runspace.HadErrors) {
                            ## Capture the error message to send to TMConsole
                            # $InvocationError = $TMRunspace.Runspace.Streams.Error[0].Exception.Message
                            $InvocationError = $TMRunspace.Runspace.Streams.Information[-1].MessageData.Message

                            ## Send the StatusMessages to TMConsole
                            $Global:TMConsole.TaskProgress.Enqueue([PSCustomObject]@{
                                    TMTaskId = $TMTaskId
                                    Type     = 'Progress'
                                    Message  = [PSCustomObject]@{
                                        ActivityId        = 0
                                        ParentActivityId  = -1
                                        Activity          = 'Task Failed'
                                        CurrentOperation  = 'Failed'
                                        StatusDescription = ''
                                        PercentComplete   = 100
                                        SecondsRemaining  = -1
                                        RecordType        = 2
                                    }
                                }
                            )
                            $Global:TMConsole.TaskChanges.Enqueue(
                                [PSCustomObject]@{
                                    TMTaskId     = $TMTaskId
                                    TMTaskNumber = $TMRunspace.TMTaskNumber
                                    Type         = 'Error'
                                    Message      = $InvocationError
                                }
                            )
                            $Global:TMConsole.TaskChanges.Enqueue(
                                [PSCustomObject]@{
                                    TMTaskId     = $TMTaskId
                                    TMTaskNumber = $TMRunspace.TMTaskNumber
                                    Type         = 'TaskFailed'
                                    Message      = $InvocationError
                                }
                            )
                        }
                        else {
                            $Global:TMConsole.TaskProgress.Enqueue( 
                                [PSCustomObject]@{
                                    TMTaskId = $TMTaskID
                                    Type     = 'Progress'
                                    Message  = [PSCustomObject]@{
                                        ActivityId        = 0
                                        ParentActivityId  = -1
                                        Activity          = 'Task Completed'
                                        CurrentOperation  = 'Complete'
                                        StatusDescription = ''
                                        PercentComplete   = 100
                                        SecondsRemaining  = -1
                                        RecordType        = 1
                                    }
                                }
                            )
                            $Global:TMConsole.TaskChanges.Enqueue( 
                                [PSCustomObject]@{
                                    TMTaskId = $TMTaskId
                                    Type     = 'TaskCompleted'
                                }
                            )
                        }
                        $Global:TMConsole.RemoveRunspaces.Enqueue($TMTaskId)
                    }
                }

                ## Collect all of the Task State Change tokens
                while ($Global:TMConsole.RemoveRunspaces.Count) {
                    $TMTaskId = $null
                    if ($Global:TMConsole.RemoveRunspaces.TryDequeue([ref]$TMTaskId)) {

                        try {
                            $TMRunspace = $Global:TMConsole.Runspaces.$TMTaskID
                            [void]($TMRunspace.Runspace.Streams.Information.Clear())
                            [void]($TMRunspace.Runspace.Streams.Error.Clear())
                            [void]($TMRunspace.Runspace.Streams.Warning.Clear())
                            [void]($TMRunspace.Runspace.Streams.Verbose.Clear())
                            [void]($TMRunspace.Runspace.Streams.Debug.Clear())
                            [void]($TMRunspace.Runspace.Streams.Progress.Clear())
                        }
                        catch {
                            ## Errors are present in this output but are not ours to handle here
                        }
                        try {
                            [void]($TMRunspace.Runspace.BeginStop($Null, $TMRunspace.AsyncHandle))
                        }
                        catch {
                            ## Errors are present in this output but are not ours to handle here
                        }
                        try {
                            [void]($TMRunspace.Runspace.Dispose())
                            $TMRunspace.Runspace = $null
                            $TMRunspace.AsyncHandle = $null
                        }
                        catch {
                            ## Errors are present in this output but are not ours to handle here
                        }
                        if ($Global:TMConsole.Runspaces.PSObject.Properties.Name -contains $TMTaskId) {
                            $Global:TMConsole.Runspaces.PSObject.Properties.Remove($TMTaskId)
                        }
                        if ([TMCRunspaceDirectory]::ActiveRunspaces.Keys -contains $TMTaskId) {
                            [void]([TMCRunspaceDirectory]::ActiveRunspaces.TryRemove($TMTaskId, [ref]$null))
                        }
                    }
                }

                ## Check if the runspace count has changed
                if ($LastRunningActionCount -ne $Global:TMConsole.Runspaces.PSObject.Properties.Name.count) {
                    $LastRunningActionCount = $Global:TMConsole.Runspaces.PSObject.Properties.Name.count
                    $ActionCountLabel = switch ($LastRunningActionCount) {
                        0 { '0 Actions Running' }
                        1 { '1 Action Running' }
                        Default { "$([int]$LastRunningActionCount) Actions Running" }
                    }   
                    [void]($MessageBatch.Add([PSCustomObject]@{
                                Type    = 'powershell-server-status'
                                Message = @{
                                    connectionStatus = 'Connected'
                                    serverName       = $WebSocketServer
                                    serverStatus     = $ActionCountLabel
                                }
                            }
                        )
                    )
                }
                
                ##
                ## Priority Messages, always get sent when available
                ##
              
                ## Collect all of the Task State Change tokens
                while ($Global:TMConsole.TaskChanges.Count) {
                    $message = $null
                    if ($Global:TMConsole.TaskChanges.TryDequeue([ref]$message)) {
                        [void]($MessageBatch.Add($message))
                    }
                }
                
                ## Collect all of the System Error tokens
                while ($Global:TMConsole.SystemErrors.Count) {
                    $message = $null
                    if ($Global:TMConsole.SystemErrors.TryDequeue([ref]$message)) {
                        [void]($MessageBatch.Add($message))
                        Write-Warning $Message
                    }
                }

                if ($MessageBatch.Count -gt 0) {
                    [void]($global:WebSocketClient.SendAsync(
                            ($MessageBatch | ConvertTo-Json -EnumsAsStrings -Compress), 
                            [System.Net.WebSockets.WebSocketMessageType]::Text, 
                            $global:CancellationToken
                        )
                    )
                    [void]($MessageBatch.Clear())
                }

                ##
                ## Aggregate Console Messages and load in blocks
                ##
                $Now = [datetime]::UtcNow
                $Elapsed = ($Now - $LastBatchSend).TotalMilliseconds

                ## Collect ConsoleOutput (Fabricated ones from Pipeline, needs updating)
                if ($Global:TMConsole.TaskOutput.count -gt $MinBatchSize -or $Elapsed -gt $MaxBatchLatency) {
                    for ($i = 0; $i -lt $MinBatchSize; $i++) {
                        $message = $null
                        if ($Global:TMConsole.TaskOutput.TryDequeue([ref]$message)) {
                            # Write-Host ($message | ConvertTo-Json -EnumsAsStrings -Compress) -Foreground Yellow
                            [void]($MessageBatch.Add($message))
                        }
                    }
                }
                
                ## Collect Progress Output
                if (
                    ($MessageBatch.Count -gt 0) -or    
                    ($Global:TMConsole.TaskProgress.count -gt $MinBatchSize) -or 
                    ($Elapsed -gt $MaxBatchLatency)
                ) {
                    while ($Global:TMConsole.TaskProgress.Count) {
                        $message = $null
                        if ($Global:TMConsole.TaskProgress.TryDequeue([ref]$message)) {
                            [void]($MessageBatch.Add($message))
                        }
                    }
                }

                if ($MessageBatch.Count -gt 0) {
                    [void]($global:WebSocketClient.SendAsync(
                            ($MessageBatch | ConvertTo-Json -EnumsAsStrings -Compress), 
                            [System.Net.WebSockets.WebSocketMessageType]::Text, 
                            $global:CancellationToken
                        )
                    )
                    [void]($MessageBatch.Clear())
                    $LastBatchSend = $Now
                }

                ## TMC TMCPSHost Collection
                $message = $null
                while ([TMCPSHost]::OutputQueue.TryDequeue([ref]$message)) {
                    [void]($MessageBatch.Add($message))
                    if ($MessageBatch.Count -gt $MaxBatchSize) { break }
                }
                    
                if ($MessageBatch.Count -gt 0) {
                    [void]($global:WebSocketClient.SendAsync(
                            ($MessageBatch | ConvertTo-Json -EnumsAsStrings -Compress), 
                            [System.Net.WebSockets.WebSocketMessageType]::Text, 
                            $global:CancellationToken
                        )
                    )
                    [void]($MessageBatch.Clear())
                    $LastBatchSend = $Now
                }

                Start-Sleep -Milliseconds $SleepInterval

            } until (-Not $global:WebSocketClient.Connected)
        }
        catch {
            Write-Host -ForegroundColor "Red" "FAILURE IN WSS Client"
            throw $_
        }
        finally {
            
            $EventSubscribers = Get-EventSubscriber -SourceIdentifier "TM*" | Foreach-Object
            foreach ($TMEventSubscriber in $EventSubscribers) {
                Unregister-Event -Force -SubscriptionId $TMEventSubscriber.SubscriptionId
                $TMEventSubscriber.Action.Dispose()
                $TMEventSubscriber = $null
            }
            
            ## Disconnect and close down
            if ($global:WebSocketClient) {
                if ($global:WebSocketClient.Connected) {
                    $global:WebSocketClient.Stop()
                }
                $global:WebSocketClient.Dispose()
            }
        }
    }
}