lib/SubjectTasks.ps1

function Invoke-SubjectTaskActionParallel {
    <#
    .SYNOPSIS
    Provides the Actionrequest to the TMConsole PowerShell Session Manager for invocation.
 
    .DESCRIPTION
    This function will collect the ActionRequest object from TransitionManager, and will send it to SessionManager for invocation.
 
    .PARAMETER TMSession
    The name of the TMSession that the Broker and Subject belong to
 
    .PARAMETER Subject
    The TMBrokerSubject object representing a Broker's Subject Task
 
    .PARAMETER Cache
    The Broker's cache if available
 
    .EXAMPLE
    Invoke-SubjectTaskActionParallel -TMSession 'Broker' -Subject $Broker.Subjects[0][1] -Cache $Broker.Cache
 
    .OUTPUTS
    None
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [String]$TMSession = 'Default',

        [Parameter(Mandatory = $true)]
        [TMBrokerSubject]$Subject,

        [Parameter(Mandatory = $false)]
        [Object]$Cache
    )

    ##
    ## Connect to TransitionManager to collect the task to run
    ##

    # Get the session configuration
    Write-Verbose 'Checking for cached TMSession'
    $TMSessionConfig = $global:TMSessions[$TMSession]
    Write-Debug 'TMSessionConfig:'
    Write-Debug ($TMSessionConfig | ConvertTo-Json -Depth 5)
    if (-not $TMSessionConfig) {
        throw "TMSession '$TMSession' not found. Use New-TMSession command before using features."
    }

    # Tell TM that the Task's Action is starting and receive an ActionRequest with Params
    $Subject.ActionRequest = Start-TMTaskAction -TMSession $TMSession -TaskId $Subject.Task.Id -Force -ErrorAction 'SilentlyContinue'

    ## An Error invoking the task may have been handled, and no SubjectActionRequest was returned.
    if (-not $Subject.ActionRequest) {
        $Subject.Action.ExecutionStatus = 'Started'
        return
    }

    ## Add the TM User Session to the Subject Action Request so that the TM session and $TM variable are available during script execution
    if ($ActionRequest) {
        $Subject.ActionRequest | Add-Member -NotePropertyName 'TMUserSession' -NotePropertyValue $ActionRequest.TMUserSession -Force
    }

    ## Add the Broker ID to the ActionRequest so its output can be interpered by TMConsole
    $Subject.ActionRequest | Add-Member -NotePropertyName 'BrokerId' -NotePropertyValue "TMTaskID_$($Broker.Task.Id)" -Force

    # Check the localsettings file to see if this action needs to be logged
    try {
        $LocalSettingsFilePath = Join-Path $Env:APPDATA 'tmconsole' 'localsettings'
        $LocalSettings = Get-Content -LiteralPath $LocalSettingsFilePath -Raw | ConvertFrom-Json
        if ($LocalSettings.logging.enabled) {
            $Subject.ActionRequest | Add-Member -NotePropertyName 'logPath' -NotePropertyValue $LocalSettings.logging.path
        }
    } catch {
        Write-Host "Could not read local settings: $($_.Exception.Message)" -ForegroundColor DarkYellow
    }

    ## Notify invocation of the next task
    Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] Queueing Parallel Task: " -NoNewline
    Write-Host "#$($Subject.Task.TaskNumber)" -NoNewline -ForegroundColor Cyan
    Write-Host ', Title: ' -NoNewline
    Write-Host $Subject.Task.Title -ForegroundColor Yellow

    ## Create a TMC message to be forwarded to SessionManager by the Write-Host handler
    $Subject.ActionRequest | Add-Member -NotePropertyName 'Type' -NotePropertyValue 'ActionRequest' -Force
    $QueueActionRequestString = "||TMC:$($Subject.ActionRequest | ConvertTo-Json -Depth 10 -Compress)"

    # Record the time that this Action was invoked
    $Subject.Action.InvokedAt = Get-Date

    ## The following command will execute 'Normally' in debug mode, but when run in a TMC runspace, it has a special handling,
    ## and will result in the ActionRequest object being sent for invocation
    Write-Host $QueueActionRequestString
}


function Invoke-SubjectTaskAction {
    <#
    .SYNOPSIS
    Invokes the Action associated with a Broker's Subject Task
 
    .DESCRIPTION
    This function will invoke the PowerShell Action associated with a Broker's Subject Task
 
    .PARAMETER TMSession
    The name of the TMSession that the Broker and Subject belong to
 
    .PARAMETER Subject
    The TMBrokerSubject object representing a Broker's Subject Task
 
    .PARAMETER Cache
    The Broker's cache if available
 
    .EXAMPLE
    Invoke-SubjectTaskAction -TMSession 'Broker' -Subject $Broker.Subjects[0][1] -Cache $Broker.Cache
 
    .OUTPUTS
    None
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [String]$TMSession = 'Default',

        [Parameter(Mandatory = $true)]
        [TMBrokerSubject]$Subject,

        [Parameter(Mandatory = $false)]
        [Object]$Cache
    )

    # Get the session configuration
    Write-Verbose 'Checking for cached TMSession'
    $TMSessionConfig = $global:TMSessions[$TMSession]
    Write-Debug 'TMSessionConfig:'
    Write-Debug ($TMSessionConfig | ConvertTo-Json -Depth 5)
    if (-not $TMSessionConfig) {
        throw "TMSession '$TMSession' not found. Use New-TMSession command before using features."
    }

    ##
    ## Connect to TransitionManager to collect the task to run
    ##

    # Get the session configuration
    Write-Verbose 'Checking for cached TMSession'
    $TMSessionConfig = $global:TMSessions[$TMSession]
    Write-Debug 'TMSessionConfig:'
    Write-Debug ($TMSessionConfig | ConvertTo-Json -Depth 5)
    if (-not $TMSessionConfig) {
        throw "TMSession '$TMSession' not found. Use New-TMSession command before using features."
    }

    # Tell TM that the Task's Action is starting and receive an ActionRequest with Params
    $Subject.ActionRequest = Start-TMTaskAction -TMSession $TMSession -TaskId $Subject.Task.Id -Force -ErrorAction 'SilentlyContinue'

    ## An Error ivoking the task may have been handled, and no SubjectActionRequest was returned.
    if (-not $Subject.ActionRequest) {
        $Subject.Action.ExecutionStatus = 'Started'
        return
    }

    ## Add the Broker ID to the ActionRequest so it's output can be interpered by TMConsole
    $Subject.ActionRequest | Add-Member -NotePropertyName 'BrokerId' -NotePropertyValue "TMTaskID_$($Broker.Task.Id)" -Force

    ##
    ## Invoke the Action in this runspace
    ##
    $InvocationRunspaceScriptBlock = [scriptblock] {
        param($ActionRequest, $AllowInsecureSSL)
        try {
            ## Complete the root activity to provide a consistent experience for all tasks
            $StartingProgressActivity = @{
                Id               = 0
                ParentId         = -1
                Activity         = 'Broker is Running Task: ' + $ActionRequest.task.taskNumber + ' - ' + $ActionRequest.task.title
                PercentComplete  = 5
                SecondsRemaining = -1
                Completed        = $False
            }
            Write-Progress @StartingProgressActivity

            ## Preserve the Broker's Parameters
            $BrokerParams = $Params
            $BrokerTM = $TM

            ## Import the ActionRequest to create necessary objects in the pipeline
            . Import-TMCSubjectActionRequest -ActionRequest $ActionRequest -BrokerTaskId 1

            ## Invoke the Action's Script block
            $ActionScriptBlock = [scriptblock]::Create($ActionRequest.options.apiAction.script)
            Invoke-Command -ScriptBlock $ActionScriptBlock -ErrorAction 'Stop' -NoNewScope

            ## 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
                    }
                }
                ## Clear the Global variable now that it's assigned into the respsonse belonging
                ## To the subject task. Otherwise, the next subject gets the same data
                Remove-Variable -Name 'TMAssetUpdates' -Scope Global
            }

            ## Add SSL Exception if necessary
            if ($AllowInsecureSSL) {
                $CompleteTaskParameters | Add-Member -NotePropertyName 'AllowInsecureSSL' -NotePropertyValue $True -Force
            }

            ## Complete the TM Task, sending Updated Data values for the task Asset
            if ($ActionRequest.HostPID -ne 0) {
                Complete-TMTask -ActionRequest $ActionRequest @CompleteTaskParameters
            }

            ## Complete the root activity to provide a consistent experience for all tasks
            $CompleteProgressActivity = @{
                Id               = 0
                ParentId         = -1
                Activity         = 'Task Complete: ' + $ActionRequest.task.taskNumber + ' - ' + $ActionRequest.task.title
                PercentComplete  = 100
                SecondsRemaining = -1
                Completed        = $True
            }
            Write-Progress @CompleteProgressActivity

            $Subject.Action.ExecutionStatus = 'Successful'
        } catch {

            ## Get the Exception message
            $ExceptionMessage = $_.Exception.Message

            ## Send the Error Message (Only send if TMD started the process)
            if ($ActionRequest.HostPID -ne 0) {
                Set-TMTaskOnHold -ActionRequest $ActionRequest -Message ('Action Error: ' + $ExceptionMessage)
            }

            ## Throw the full error message
            Write-Host $_.Exception.Message -ForegroundColor Red

            $Subject.Action.ExecutionStatus = 'Failed'
            $Subject.Action.Errors = @($ExceptionMessage)
        }

        ## Return the Broker Params and TM scope
        New-Variable -Name Params -Scope Global -Value $BrokerParams -Force
        New-Variable -Name TM -Scope Global -Value $BrokerTM -Force
    }

    ## Notify invocation of the next task
    Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] Invoking Task: " -NoNewline
    Write-Host "#$($Subject.Task.TaskNumber)" -NoNewline -ForegroundColor Cyan
    Write-Host ', Title: ' -NoNewline
    Write-Host $Subject.Task.Title -ForegroundColor Yellow

    ## Run the Script Block
    $RunningTime = Measure-Command {
        $InvokeSplat = @{
            ScriptBlock  = $InvocationRunspaceScriptBlock
            ArgumentList = @($Subject.ActionRequest, $TMSessionConfig.AllowInsecureSSL)
            NoNewScope   = $true
        }
        $Subject.Action.InvokedAt = Get-Date
        Invoke-Command @InvokeSplat
    }
    $RuntimeSeconds = [Math]::Ceiling($RunningTime.TotalSeconds)

    ## Produce Broker output to finish the line of output
    if ($Subject.Action.ExecutionStatus -eq 'Failed') {

        Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] Action completed with errors after running for " -NoNewline -ForegroundColor Red
        Write-Host $RuntimeSeconds -NoNewline -ForegroundColor Red
        Write-Host " seconds. Error: $($Subject.Action.Errors -join ', ')" -ForegroundColor Red
    } else {
        Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] Action completed successfully in: " -NoNewline
        Write-Host $RuntimeSeconds -NoNewline -ForegroundColor Cyan
        Write-Host ' seconds'
    }
}


function Import-TMCSubjectActionRequest {
    param(
        [Parameter(Mandatory = $true)][PSObject]$ActionRequest,
        [Parameter(Mandatory = $true)][Int64]$BrokerTaskId
    )

    ## Remove any leftover 'get_' parameter names
    $ActionRequest.params.PSObject.Properties.Name | Where-Object {
        $_ -like 'get_*'
    } | ForEach-Object {
        $ActionRequest.params.PSObject.Properties.Remove($_)
    }

    ## Allow Required Modules to be imported automatically
    $PSModuleAutoloadingPreference = 'All'
    $Global:PSModuleAutoloadingPreference = 'All'

    ## Import the remaining Variables
    New-Variable -Name 'ActionRequest' -Value $ActionRequest -Force -Scope Global
    New-Variable -Name 'Params' -Value $ActionRequest.params -Force -Scope Global

    ## Compute a Project root folder from the Server, Project and Event
    $TMServerUrl = ([uri]$ActionRequest.options.callback.siteUrl).Host
    $TMProjectName = $ActionRequest.task.project.name
    $TMEventName = $ActionRequest.task.event.name
    $ProjectRootFolder = Join-Path $Global:userPaths.root ($TMServerUrl -replace '.transitionmanager.net', '') $TMProjectName $TMEventName

    ## Create a Convenient TM Object with useful details
    $TM = [pscustomobject]@{
        Server   = @{
            Url = $TMServerUrl
            # Version = [Version]::parse($ActionRequest.tmUserSession.tmVersion)
        }
        Project  = @{
            Id   = $ActionRequest.task.project.id
            Name = $TMProjectName
        }
        Event    = [pscustomobject]@{
            Id      = $ActionRequest.task.event.id
            Name    = $TMEventName
            Bundles = $ActionRequest.task.event.bundles
        }
        Provider = @{
            Id   = $ActionRequest.options.apiAction.provider.id
            Name = $ActionRequest.options.apiAction.provider.name
        }
        Action   = @{
            Id   = $ActionRequest.options.apiAction.id
            Name = $ActionRequest.options.apiAction.name
        }
        Task     = [pscustomobject]@{
            Id         = $ActionRequest.task.id
            Title      = $ActionRequest.task.title
            TaskNumber = $ActionRequest.task.taskNumber
            Invoker    = $ActionRequest.task.assignedTo.name
            Team       = $ActionRequest.task.team
        }
        Asset    = $ActionRequest.task.asset

        # The tmUserSession object is not present, exclude for now
        # User = @{
        # Id = $ActionRequest.tmUserSession.userContext.user.id
        # Username = $ActionRequest.tmUserSession.userContext.user.username
        # Name = $ActionRequest.tmUserSession.userContext.person.fullName
        # }
        Paths    = @{
            debug            = Join-Path $ProjectRootFolder 'Debug'
            logs             = Join-Path $ProjectRootFolder 'Logs'
            queue            = Join-Path $ProjectRootFolder 'Queue'
            config           = Join-Path $ProjectRootFolder 'Config'
            input            = Join-Path $ProjectRootFolder 'Input'
            output           = Join-Path $ProjectRootFolder 'Output'
            credentials      = Join-Path $ProjectRootFolder 'Credentials'
            git              = Join-Path $ProjectRootFolder 'Git'
            referencedesigns = Join-Path $ProjectRootFolder 'Reference Designs'
        }
    }

    ## Scope the variable as global so the user will have access to it
    New-Variable -Name 'TM' -Value $TM -Scope Global -Force

}