lib/SessionManager.ps1




<#
    This SessionManager Script handles creating and receiving output from a Session-Based Runspace Environment
    Output received from actions that are being run in this script are sent to TMConsole via WebSocket a websocket
 
    Messages sent to the WebSocket must be:
        - An JSON string
        - The Json Object must have the following properties:
            - TMTaskId: <int> This is the task.taskNumber (not the taskId)
            - Type:
                One of [
                    'Started' - Expressly creates a 'TaskCache' object which stores the output of a given Task's Action
                        ** Note - The receiving handler does not expressly require a 'Started' token, if any other types are received first
                                  And one doesn't already exist, one is created to store the other Type-d data.
 
                    'Info' - A 'Write-Host' message from the 'info' stream
                    'Progress' - An 'Activity Progress' message from the 'Progress' stream
                    'Failed' - An Error bearing token with a 'Message' property which is displayed in the console
                    'Completed' - A token with a simply indicating that the task is complete and all progress should reflect this
 
                ]
 
    Among others. There is "Info", "Started", "Failed", "Progress" and "Completed"
    Each of the JSON representations are of the associated PowerShell Primative Classes that are emitted by the the script using:
 
    - Started:
        Psuedo message, not actually created by PowerShell, it's written to the StdOut as early as possible (when this script gets the ID for the first time
 
    - Info: Write-Host or any pipeline output (Get-Date, for example)
 
    - Progress: Write-Progress
        ActivityID (Id)
        ParentActivityId
        Activity
        CurrentOpperation
        Status
        PercentComplete
        SecondsRemaining
        RecordType (1 = running, 0=Completed)
     
    - Errors Writtn as "Failed"
        - Errors Thrown from Infrastructure problems (Session issues, network, etc)
        - TM Action Scripts are run by the user are in a try catch and reported this way
 
    - Completed:
        Occurs when the Action Script provided by TM is completed, with no errors
 
    --- TODO
    - Verbose
        Write-Verbose output should be displayed in TMD as well (with a UI switch)
 
 #>



Function Start-TMCSessionManager {
    <#
    .SYNOPSIS
        Executes a new TMConsole PowerShell Server instance to handle
      
    .NOTES
        Name: Start-TMCSessionManager
        Author: TransitionManager
        Version: 1.0
        DateCreated: 2021-08-21
      
      
    .EXAMPLE
        Start-TMCSessionManager -Hostname '127.0.0.1' -Port 8620
      
    .LINK
        https://support.transitionmanager.net
    #>

    param(
        [Parameter(mandatory = $false)]
        # [bool]$OutputVerbose = $false, ## PROD SETTING
        [bool]$OutputVerbose = $true, ## DEV SETTING
    
        [Parameter()]$HostPID = -1,
        
        [Parameter()][Bool]$AllowInsecureSSL = $False,
        [Parameter()][String]$CertificateThumbprint,
        
        ## Use if debugging. This bypasses running the ActionRequest in an RS Job, and runs it locally instead.
        [Parameter()][Bool]$AllowDirectExecution = $False,
        
        [Parameter()][String]$WebSocketServer,
        [Parameter()][Int32]$WebSocketPort

    )

    Begin {
        
        ## Enable Verbose Output if OutputVerbose was true
        if ($OutputVerbose) { 
            $Global:VerbosePreference = 'Continue'    
            $VerbosePreference = 'Continue'    
        }

        ## Create the powershell WebSocketServer connection settings
        $PowerShellServerSettings = @{
            Hostname = $WebSocketServer.ToLower()
            Port     = $WebSocketPort
        }

        Write-Verbose -Message "Starting TMConsole WebSocketClient Session {$($PowershellServerSettings.Hostname):$($PowershellServerSettings.Port)}"
        Write-Verbose -Message ($MyInvocation.BoundParameters | ConvertTo-Json)

        ##
        ## Perform Configuration Validation/Setup
        ##
        
        ## Standardize access to the important Paths
        if ($OutputVerbose) { 
            $Global:VerbosePreference = 'Continue'    
            Write-Host 'VERBOSE: Checking TMD PsSession Configuration' 
        }
        
        ## Skip creating the Remote Session if it's a local task
        # if (-not $AllowDirectExecution) {
        # # Test the PowerShell Configuration Settings
        # Test-TMDPSSessionConfiguration
        # }

        ##
        ## Begin Running the Session Manager
        ##
      
        ## Allow the TMD Watcher script to continue and move past any transient errors
        $ErrorActionPreference = 'Continue' ## PRODUCTION = 'Continue'. Setting this to 'Stop' is useful in debugging this script
    
        ## Enable Verbose (using bool, not switch)
        if ($OutputVerbose) { 
        
            Write-Host "VERBOSE: PSSessionManager is running in Verbose Mode."
            $VerbosePreference = 'Continue' 
        
            ## If Host PID is present, notify that SessionManager is bound to that pid
            if ($HostPID -ne -1) {
                Write-Host "VERBOSE: Bound to TMD PID: $HostPID"
            }
        }
        else { 
            $VerbosePreference = 'SilentlyContinue' 
        }
        
        ## Create empty cache object
        $Global:LocalSessionCache = @{
            SessionManagerState = 'Connecting'
            LastJobEndTime      = Get-Date
        }
      
        ## Skip creating the Remote Session if it's a local task
        if (-not $AllowDirectExecution) {
            ### REAL VALUE, this change is to confirm something
            $TmdUserSession = New-TMDPSSession
    
            #Check that we have a TMD Session for the user
            if (-not $TmdUserSession) {
                throw "Can't get a PSSession"
            }
        }
    }

    Process {
        
        ## Write a Verbose Message
        if ($OutputVerbose) { Write-Host 'VERBOSE: Starting Remote Session WebSocketClient' }
    
        ## Error Action
        $ErrorActionPreference = 'Continue'
    
        ## Skip creating the Remote Session if it's a local task
        if (-not $AllowDirectExecution) {
            
            ## Connect to the Session when it's available
            Connect-PSSessionWhenReady $TmdUserSession | Out-Null
            
            ## Check on the Session Status
            if (-Not $TmdUserSession) {
                Write-Error 'PowerShell Session has errored!'
                $Global:LocalSessionCache.SessionManagerState = 'NotRunning'
            }
        }    
        
        ## Define the settings for the web socket client
        $StartWebSocketClientSplat = @{
            Hostname             = $($PowerShellServerSettings.Hostname.toString())
            Port                 = $($PowerShellServerSettings.Port.toString())
            Verbose              = $Verbose 
            OutputVerbose        = $OutputVerbose 
            AllowDirectExecution = $AllowDirectExecution 
            AllowInsecureSSL     = $AllowInsecureSSL
        }

        ## Allow Debugging
        if ($AllowDirectExecution) {
            
            ## Start the WebSocketClient directly
            Start-TMConsoleWebSocketClient @StartWebSocketClientSplat
        }
        else {
            
            ## Create a Script Block to run in the remote session
            $RemoteSessionScriptBlock = [scriptblock] {
                param($StartWebSocketClientSplat)
                Start-TMConsoleWebSocketClient @StartWebSocketClientSplat
            }
            
            ## Then Run the Remote Scriptblock in the TMDUserSession
            Write-Verbose "Starting WebSocketClient Connection in the in the TMDUserSession PSSession"

            ## Invoke the Session Manager in the remote session
            $RemotePSSessionManagerJob = Invoke-Command -Session $TmdUserSession -ScriptBlock $RemoteSessionScriptBlock -ArgumentList $StartWebSocketClientSplat -AsJob
            
            Write-Verbose "WebSocketClient has been started in the TMDUserSession PSSession, Monitoring status until it's no longer running"
        
            ## Keep this script session alive while the TMDUserSession is still open
            while ($RemotePSSessionManagerJob.JobStateInfo.State -eq 'Running') {
                Start-Sleep -Seconds 1
            }
            Write-Verbose "WebSocketClient has ended in the TMDUserSession PSSession"
        }
        
    }    
    End {
        Write-Verbose "TMConsole.SessionManager is ending"
        
        ## Skip creating the Remote Session if it's a local task
        if (-not $AllowDirectExecution) {
            Write-Verbose "Cleaning up PSSessions"
            
            ## Disconnect and remove from the session
            if ($IsWindows) {
                Disconnect-PSSession -Session $TmdUserSession | Out-Null
                Remove-PSSession -Session $TmdUserSession | Out-Null
            }
        }
        Write-Verbose "TMConsole.SessionManager has ended"
    }
}