JobHelper.ps1

$defaultScriptArgumentsValues = @{
    scriptPath = "";
    scriptArguments = "";
    inlineScript = "";
    inline = $true;
    workingDirectory = "";
    errorActionPreference = "continue";
    ignoreLASTEXITCODE = $false;
    failOnStdErr = $false;
    initializationScriptPath = "";
    sessionVariables = "";
}

function Run-RemoteScriptJobs {
    [CmdletBinding()]
    Param (
        [System.Management.Automation.Runspaces.PSSession[]] $sessions,
        [scriptblock] $script,
        [string] $sessionName,
        [hashtable] $scriptArgumentsByName,
        [hashtable[]] $targetMachines,
        [psobject] $sessionOption,
        [scriptblock] $outputHandler,
        [scriptblock] $errorHandler,
        [string] $logsFolder
    )
    Trace-VstsEnteringInvocation -InvocationInfo $MyInvocation -Parameter ""
    try {
        $scriptArguments = Get-ScriptArguments -scriptArgumentsByName $scriptArgumentsByName
        $jobName = Get-VstsTaskVariable -Name 'System.JobId'
        $parentJob = Invoke-Command -Session $sessions `
                                    -ScriptBlock $script `
                                    -ArgumentList $scriptArguments `
                                    -JobName $jobName `
                                    -AsJob `
                                    -ErrorAction 'Stop'
        $jobsInfo = $parentJob.ChildJobs | Select-Object Id, Location, @{ Name = 'JobRetrievelCount'; Expression = { 0 } }
        $jobResults = Get-JobResults -jobsInfo $jobsInfo `
                                     -targetMachines $targetMachines `
                                     -sessionName $sessionName `
                                     -sessionOption $sessionOption `
                                     -outputHandler $outputHandler `
                                     -errorHandler $errorHandler `
                                     -logsFolder $logsFolder
        
        return $jobResults
    } finally {
        Trace-VstsLeavingInvocation $MyInvocation
    }
}

function Get-ScriptArguments {
    Param (
        [hashtable] $scriptArgumentsByName
    )
    Trace-VstsEnteringInvocation -InvocationInfo $MyInvocation -Parameter ''
    try {
        foreach($key in $defaultScriptArgumentsValues.Keys) {
            if(!$scriptArgumentsByName.ContainsKey($key)) {
                $scriptArgumentsByName[$key] = $defaultScriptArgumentsValues[$key]
            }
        }
        # scriptArguments should be an array with each element being assigned in the
        # exact order of params that is accepted by RunPowerShellScriptJob
        $scriptArguments = @(
            $scriptArgumentsByName.scriptPath,
            $scriptArgumentsByName.scriptArguments,
            $scriptArgumentsByName.inlineScript,
            $scriptArgumentsByName.inline,
            $scriptArgumentsByName.workingDirectory,
            $scriptArgumentsByName.errorActionPreference,
            $scriptArgumentsByName.ignoreLASTEXITCODE,
            $scriptArgumentsByName.failOnStdErr,
            $scriptArgumentsByName.initializationScriptPath,
            $scriptArgumentsByName.sessionVariables
        );
        return $scriptArguments
    } finally {
        Trace-VstsLeavingInvocation $MyInvocation
    }
}

function Get-JobResults {
    Param (
        [psobject[]] $jobsInfo,
        [hashtable[]] $targetMachines,
        [string] $sessionName,
        [psobject] $sessionOption,
        [scriptblock] $outputHandler,
        [scriptblock] $errorHandler,
        [string] $logsFolder
    )
    Trace-VstsEnteringInvocation -InvocationInfo $MyInvocation -Parameter ''
    try {
        $jobResults = @()
        $remoteExecutionStatusByLocation = @{}
        $connectionAttemptsByLocation = @{}
        foreach($jobInfo in $jobsInfo) {
            $remoteExecutionStatusByLocation[$jobInfo.Location] = "Unknown"
            $connectionAttemptsByLocation[$jobInfo.Location] = 0
        }

        while($true) {
            foreach($jobInfo in $jobsInfo) {
                $jobId = $jobInfo.Id
                $computerName = $jobInfo.Location
                if($remoteExecutionStatusByLocation[$computerName] -eq "Unknown") {
                    try {
                        $job = Get-Job -Id $jobId -ErrorAction 'Stop'
                    } catch {
                        Write-Verbose "Unable to get job with id: '$jobId'. Error: '$($_.Exception.Message)'"
                        Write-Verbose $_.Exception.ToString()
                        $jobInfo.JobRetrievelCount++
                        if($jobInfo.JobRetrievelCount -ge 3) {
                            Write-Verbose "Maximum job retrievel count reached for jobid: '$jobId'. Dropping job."
                            $remoteExecutionStatusByLocation[$computerName] = "Finished"
                        }
                        continue;
                    }
                    $jobInfo.JobRetrievelCount = 0
                    $jobState = $job.State.ToString().ToLowerInvariant()
                    Write-Verbose "JobId: '$jobid', JobState: '$jobState', ComputerName: '$computerName'"
                    Write-Host "================================================ $computerName ================================================"
                    Receive-Job -Job $job |
                        ForEach-Object {
                            if($_.VstsRemoteDeployerJobResult -eq $true) { 
                                if($_ -is [hashtable]) {
                                    $_.Remove("VstsRemoteDeployerJobResult")
                                    $jobResults += $_ 
                                } else {
                                    Write-Verbose "jobResult is not a hashtable"
                                    Write-Verbose $_.ToString();
                                }
                            } else {
                                # Ensure that output and error handlers do not write anything to stream and
                                # task does not fail due to an exception from them
                                if($_ -is [System.Management.Automation.ErrorRecord]) {
                                    $errorRecord = $_
                                    $null = & { try { & $errorHandler $errorRecord $($job.Location) } catch { Write-Host "ErrorHandlerException: $($_.Exception.ToString())" } }
                                } else {
                                    $outputObject = $_
                                    $null = & { try { & $outputHandler $outputObject $($job.Location) } catch { Write-Host "OutputHandlerException: $($_.Exception.ToString())" } }
                                }
                                # write logs for each targetmachine
                                if(![string]::IsNullOrEmpty($logsFolder)) {
                                    if(!(($_ -is [string]) -and $_.StartsWith('##vso'))) {
                                        $fileName = "$logsFolder\$computerName.log"
                                        try {
                                            Add-Content -LiteralPath $fileName -Value $($_ | Out-String) -Encoding UTF8 -ErrorAction 'Stop'
                                        } catch {
                                            Write-Verbose "Unable to add content to file: $fileName. Error: $($_.Exception.Message)"
                                        }
                                    }
                                }
                            }
                        }
                        
                    if($jobState -eq "completed") {
                        $remoteExecutionStatusByLocation[$computerName] = "Finished"
                    } elseif($jobState -ne "running") {
                        Write-Verbose "Job (Id = $jobId) is in undesirable state (State = $jobState). Attempting reconnection"
                        if($connectionAttemptsByLocation[$computerName] -ge 15) {
                            Write-Verbose "Maximum connection retry limit reached for computerName: $computerName"
                            $remoteExecutionStatusByLocation[$computerName] = "Finished"
                        } else {
                            $newJobId = Retry-Connection -targetMachines $targetMachines -computerName $computerName -sessionName $sessionName -sessionOption $sessionOption
                            if($newJobId -ne $null) {
                                Write-Verbose "Connection re-established to computer: $computerName, JobId (New): $newJobId, JobId(Old): $($jobInfo.Id)"
                                Stop-Job -Id $jobInfo.Id
                                $jobInfo.Id = $newJobId
                                $jobInfo.JobRetrievelCount = 0
                                $connectionAttemptsByLocation[$computerName] = 0;
                            } else {
                                Write-Verbose "Unable to re-establish connection to computer: $computerName. Retry attempt #$($connectionAttemptsByLocation[$computerName])"
                                $connectionAttemptsByLocation[$computerName]++;
                            }
                        }
                    }
                }
            }
            if($($remoteExecutionStatusByLocation.Values | ? { $_ -eq "Unknown" }).Count -eq 0) {
                break;
            }
            Start-Sleep -Seconds 30
        }
        return $jobResults
    } finally {
        Trace-VstsLeavingInvocation $MyInvocation
    }
}