Tasks/Invoke-WhiskeyParallelTask.ps1


function Invoke-WhiskeyParallelTask
{
    [CmdletBinding()]
    [Whiskey.Task('Parallel')]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    
    $queues = $TaskParameter['Queues']
    if( -not $queues )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Property "Queues" is mandatory. It should be an array of queues to run. Each queue should contain a "Tasks" property that is an array of task to run, e.g.
  
    Build:
    - Parallel:
        Queues:
        - Tasks:
            - TaskOne
            - TaskTwo
        - Tasks:
            - TaskOne
  
'

    }
    
    try
    {
        $jobs = New-Object 'Collections.ArrayList'
        $queueIdx = -1

        foreach( $queue in $queues )
        {
            $queueIdx++
            $whiskeyModulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\Whiskey.psd1' -Resolve

            if( -not $queue.ContainsKey('Tasks') )
            {
                Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Queue[{0}]: Property "Tasks" is mandatory. Each queue should have a "Tasks" property that is an array of Whiskey task to run, e.g.
  
    Build:
    - Parallel:
        Queues:
        - Tasks:
            - TaskOne
            - TaskTwo
        - Tasks:
            - TaskOne
  
    '
 -f $queueIdx);
            }

            Write-WhiskeyVerbose -Context $TaskContext -Message ('[{0}] Starting background queue.' -f $queueIdx)

            $job = Start-Job -Name $queueIdx -ScriptBlock {

                    Set-StrictMode -Version 'Latest'

                    function Sync-ObjectProperty
                    {
                        param(
                            [Parameter(Mandatory=$true)]
                            [object]
                            $Source,

                            [Parameter(Mandatory=$true)]
                            [object]
                            $Destination,

                            [string[]]
                            $ExcludeProperty
                        )

                        $Destination.GetType().DeclaredProperties | 
                            Where-Object { $ExcludeProperty -notcontains $_.Name } |
                            Where-Object { $_.GetSetMethod($false) } |
                            Select-Object -ExpandProperty 'Name' |
                            ForEach-Object { Write-Debug ('{0} {1} -> {2}' -f $_,$Destination.$_,$Source.$_) ; $Destination.$_ = $Source.$_ }

                        Write-Debug ('Source -eq $null ? {0}' -f ($Source -eq $null))
                        if( $Source -ne $null )
                        {
                            Write-Debug -Message 'Source'
                            Get-Member -InputObject $Source | Out-String | Write-Debug
                        }

                        Write-Debug ('Destination -eq $null ? {0}' -f ($Destination -eq $null))
                        if( $Destination -ne $null )
                        {
                            Write-Debug -Message 'Destination'
                            Get-Member -InputObject $Destination | Out-String | Write-Debug
                        }

                        Get-Member -InputObject $Destination -MemberType Property |
                            Where-Object { $ExcludeProperty -notcontains $_.Name } |
                            Where-Object { 
                                $name = $_.Name
                                if( -not $name )
                                {
                                    return
                                }

                                $value = $Destination.$name
                                if( $value -eq $null )
                                {
                                    return
                                }

                                Write-Debug ('Destination.{0,-20} -eq $null ? {1}' -f $name,($value -eq $null))
                                Write-Debug (' .{0,-20} is {1}' -f $name,$value.GetType())
                                return (Get-Member -InputObject $value -Name 'Keys') -or ($value -is [Collections.IList])
                            } |
                            ForEach-Object {
                                $propertyName = $_.Name
                                Write-Debug -Message ('{0}.{1} -> {2}.{1}' -f $Source.GetType(),$propertyName,$Destination.GetType())
                                $destinationObject = $Destination.$propertyName
                                $sourceObject = $source.$propertyName
                                if( (Get-Member -InputObject $destinationObject -Name 'Keys') )
                                {
                                    $keys = $sourceObject.Keys
                                    foreach( $key in $keys )
                                    {
                                        $value = $sourceObject[$key]
                                        Write-Debug (' [{0,-20}] -> {1}' -f $key,$value)
                                        $destinationObject[$key] = $sourceObject[$key]
                                    }
                                }
                                elseif( $destinationObject -is [Collections.IList] )
                                {
                                    $idx = 0
                                    foreach( $item in $sourceObject )
                                    {
                                        Write-Debug(' [{0}] {1}' -f $idx++,$item)
                                        $destinationObject.Add($item)
                                    }
                                }
                            }
                    }

                    $VerbosePreference = $using:VerbosePreference
                    $DebugPreference = $using:DebugPreference
                    $whiskeyModulePath = $using:whiskeyModulePath 
                    $originalContext = $using:TaskContext

                    Import-Module -Name $whiskeyModulePath
                    $moduleRoot = $whiskeyModulePath | Split-Path

                    . (Join-Path -Path $moduleRoot -ChildPath 'Functions\Use-CallerPreference.ps1' -Resolve)
                    . (Join-Path -Path $moduleRoot -ChildPath 'Functions\New-WhiskeyContextObject.ps1' -Resolve)
                    . (Join-Path -Path $moduleRoot -ChildPath 'Functions\New-WhiskeyBuildMetadataObject.ps1' -Resolve)
                    . (Join-Path -Path $moduleRoot -ChildPath 'Functions\New-WhiskeyVersionObject.ps1' -Resolve)
                    . (Join-Path -Path $moduleRoot -ChildPath 'Functions\ConvertTo-WhiskeyTask.ps1' -Resolve)
                    . (Join-Path -Path $moduleRoot -ChildPath 'Functions\Import-WhiskeyYaml.ps1' -Resolve)

                    # The task context gets serialized/deserialized into this new job process. We need to
                    # correctly deserialize it back to an actual `Whiskey.Context` object.
                    $buildInfo = New-WhiskeyBuildMetadataObject
                    Sync-ObjectProperty -Source $originalContext.BuildMetadata -Destination $buildInfo -Exclude @( 'BuildServer' )
                    if( $originalContext.BuildMetadata.BuildServer )
                    {
                        $buildInfo.BuildServer = $originalContext.BuildMetadata.BuildServer
                    }
                
                    $buildVersion = New-WhiskeyVersionObject
                    Sync-ObjectProperty -Source $originalContext.Version -Destination $buildVersion -ExcludeProperty @( 'SemVer1', 'SemVer2', 'SemVer2NoBuildMetadata' )
                    $buildVersion.SemVer1 = $originalContext.Version.SemVer1.ToString()
                    $buildVersion.SemVer2 = $originalContext.Version.SemVer2.ToString()
                    $buildVersion.SemVer2NoBuildMetadata = $originalContext.Version.SemVer2NoBuildMetadata.ToString()

                    [Whiskey.Context]$context = New-WhiskeyContextObject
                    Sync-ObjectProperty -Source $originalContext -Destination $context -ExcludeProperty @( 'BuildMetadata', 'Configuration', 'Version' )
                    $context.Configuration = Import-WhiskeyYaml -Path $context.ConfigurationPath

                    $context.BuildMetadata = $buildInfo
                    $context.Version = $buildVersion

                    $context.Variables | ConvertTo-Json -Depth 50 | Write-Debug
                    $context.ApiKeys | ConvertTo-Json -Depth 50 | Write-Debug
                    $context.Credentials | ConvertTo-Json -Depth 50 | Write-Debug
                    $context.TaskDefaults | ConvertTo-Json -Depth 50 | Write-Debug

                    # Load third-party tasks.
                    foreach( $info in $context.TaskPaths )
                    {
                        Write-Verbose ('Loading tasks from "{0}".' -f $info.FullName)
                        . $info.FullName
                    }

                    $tasks = $using:queue['Tasks']
                    foreach( $task in $tasks )
                    {
                        $taskName,$taskParameter = ConvertTo-WhiskeyTask -InputObject $task -ErrorAction Stop
                        Invoke-WhiskeyTask -TaskContext $context -Name $taskName -Parameter $taskParameter
                    }
                }
                $job | 
                    Add-Member -MemberType NoteProperty -Name 'QueueIndex' -Value $queueIdx -PassThru |
                    Add-Member -MemberType NoteProperty -Name 'Completed' -Value $false
                [void]$jobs.Add($job)
        }

        $lastNotice = (Get-Date).AddSeconds(-61)
        while( $jobs | Where-Object { -not $_.Completed } )
        {
            foreach( $job in $jobs )
            {
                if( $job.Completed )
                {
                    continue
                }

                if( $lastNotice -lt (Get-Date).AddSeconds(-60) )
                {
                    Write-WhiskeyVerbose -Context $TaskContext -Message ('[{0}][{1}] Waiting for queue.' -f $job.QueueIndex,$job.Name)
                    $notified = $true
                }

                $completedJob = $job | Wait-Job -Timeout 1
                if( $completedJob )
                {
                    $job.Completed = $true
                    $completedJob | Receive-Job
                    $duration = $job.PSEndTime - $job.PSBeginTime
                    Write-WhiskeyVerbose -Context $TaskContext -Message ('[{0}][{1}] {2} in {3}' -f $job.QueueIndex,$job.Name,$job.State.ToString().ToUpperInvariant(),$duration)
                    if( $job.JobStateInfo.State -eq [Management.Automation.JobState]::Failed )
                    {
                        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Queue[{0}] failed. See previous output for error information.' -f $job.Name)
                    }
                }
            }

            if( $notified )
            {
                $notified = $false
                $lastNotice = Get-Date
            }
        }
    }
    finally
    {
        $jobs | Stop-Job 
        $jobs | Remove-Job
    }
}