Search code examples
powershelltfsjobs

Powershell Return-Job sometimes returns empty values


I'm using a Powershell script to check the status of multiple TFS builds in parallel, via the Start-Job APIs.

Everything seems to work fine, but if I cancel the builds right after they kick off the Receive-Job seems to return null, despite the tasks having completed.

This is the code acting as a gatekeeper:

While (@(Get-Job | Where { $_.State -eq "Running" }).Count -ne 0)
{
    ForEach ($Job in (Get-Job)) {
       $currentJobOutput = Receive-Job $job 6>&1  
    }
   Start-Sleep -Seconds $buildCheckingIntervalInSeconds
}

$jobsReturnData = @()
ForEach ($Job in (Get-Job)) {
Write-Host $job.State
    Wait-Job $job | out-null
   $hastBaleResult = Receive-Job $Job -Wait:$true;
   $jobsReturnData += New-Object PSObject -property $hastBaleResult
   Remove-Job $Job
}

Write-Host "`n`nBuilds recap`n"
Write-Host "Build Definition Name".PadRight(29,' ')"Build Status".PadRight(14,' ')"Build Execution URL"
$hasAnyBuildFailed=$false;

foreach($jobReturnData in $jobsReturnData)
{
Write-Host "JobReturnedData"$jobReturnData
    $currentForegroundColor="White";
    switch($jobReturnData.buildStatus){
        "succeeded" {$currentForegroundColor="Green"; break}
        "partiallySucceeded" {$currentForegroundColor="Yellow"; break}
        "failed" {$currentForegroundColor="Red"; break}
        "canceled" {$currentForegroundColor="Red"; break}
    }

    Write-Host -ForegroundColor $currentForegroundColor "$($jobReturnData.buildDefinitionName.PadRight(30,' '))$($jobReturnData.buildStatus.PadRight(15,' '))$($jobReturnData.buildUrl)"
    if(($jobReturnData.buildStatus -eq "failed") -or ($jobReturnData.buildStatus -eq "canceled")){
        $hasAnyBuildFailed = $true;
    }
}

if($hasAnyBuildFailed){
    throw "Not all builds completed successfully";
}

Most of the time it works fine, but as stated above, under some circumstances the output I get is

Completed
Completed
Pull Request - Full Build      - ExecID=247764 - Build Status: canceled        - You can access the build execution at myTfs/_build/index?buildId=247764&_a=summary
DeleteMe:Returning System.Collections.DictionaryEntry System.Collections.DictionaryEntry System.Collections.DictionaryEntry


Builds recap

Build Definition Name         Build Status   Build Execution URL
JobReturnedData 
You cannot call a method on a null-valued expression.
At myScript.ps1:377 char:60
+ ... undColor "$($jobReturnData.buildDefinitionName.PadRight(30,' '))$($jo ...
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

You cannot call a method on a null-valued expression.
At myScript.ps1:377 char:114
+ ... adRight(30,' '))$($jobReturnData.buildStatus.PadRight(15,' '))$($jobR ...
+                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull


JobReturnedData @{buildStatus=canceled; buildDefinitionName=Pull Request - Full Build; buildUrl=myTfs/_build/index?buildI
d=247764&_a=summary}
Pull Request - Full Build     canceled       myTfs/_build/index?buildId=247764&_a=summary
Not all builds completed successfully
At myScript.ps1:384 char:5
+     throw "Not all builds completed successfully";
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (Not all builds completed successfully:String) [], RuntimeException
    + FullyQualifiedErrorId : Not all builds completed successfully

What is this due to?


Solution

  • I ended up storing the queued jobs in an array and iterating it.

    $jobsReturnData = @()
    While (@($jobs | Where { $_.State -eq "Running" }).Count -ne 0) {
    
        #Redirect the transient output from each job to the main output window
        ForEach ($job in @($jobs | Where { $_.State -eq "Running" })) {
            $currentJobOutput = Receive-Job $job 6>&1
            if($job.HasMoreData){
                Wait-Job $Job -Timeout $buildCheckingIntervalInSeconds
            }
        }
        #Get return values from each completedjob without waiting for all jobs to finish
        ForEach ($job in @($jobs | Where { ($_.State -eq "Completed")})) {
            $jobReturnData = Receive-Job $Job -Wait:$true;
            $jobsReturnData += New-Object PSObject -property $jobReturnData
            Remove-Job $Job
            $jobs[[array]::IndexOf($jobs, $Job)] = $null
        }
    
    
        Start-Sleep -Seconds $buildCheckingIntervalInSeconds
    }
    
    #Get return values from each completedjob
    ForEach ($job in @($jobs | Where { ($_.State -eq "Completed")})) {
        $jobReturnData = Receive-Job $Job -Wait:$true;
        $jobsReturnData += New-Object PSObject -property $jobReturnData
        Remove-Job $Job
        $jobs[[array]::IndexOf($jobs, $Job)] = $null
    }
    

    I've noticed that sometimes a job has completed but stays running until a Wait-Job is done on it, which is quite puzzling.

    I've added ttwo checks for getting the return values from completed jobs to not have to wait for all of them to have completed before displaying the update on screen.