2

I'm using the below code to display the results of PowerShell Jobs with a timeout of 120 seconds. I would like to enhance this code by incorporating Write-Progress (based on number of jobs completed). I tried using this example as a reference, however, when I try to incorporate that code, the progress bar displays briefly after all the jobs are all done already.

    $Jobs = @()
    $ForceStoppedIds = @{}
    
    $Jobs += Get-Job
    $Jobs | Wait-Job -Timeout 120 | Out-Null
    $Jobs | ?{$_.State -eq 'Running'} | Stop-Job -PassThru | %{$ForceStoppedIds[$_.Id] = $true}
    
    foreach ($Job in $Jobs) {
    
        $Name = $Job.Name
        $Output = (Get-Job -Name $Name | Receive-Job)
    
        if ($ForceStoppedIds.Contains($Job.Id)) {
    
            Write-Output "$($Name) - Device unable to process request within 2 minutes"
    
        } else {
    
            Write-Output $Output
    
        }
    
    }
MKANET
  • 573
  • 6
  • 27
  • 51

1 Answers1

6

Wait-Job -Timeout 120 will block the thread until the specified timeout or all jobs have completed, hence, is not possible to display progress and wait for them at the same time.

There are 2 alternatives that I can think of, the first one would be to create a proxy command / proxy function around this cmdlet to extend it's functionality.

These blogs demonstrate how to do it:

You can also follow the indications from this helpful answer.


The other alternative is to define your own function that does a similar work as Wait-Job but, instead of blocking the thread, you can add a loop that will run based on 2 conditions:

  • That the elapsed time is lower than or equal to the Timeout we passed as argument to the function (we can use Diagnostics.Stopwatch for this).
  • And, that the jobs are still Running (the $jobs List<T> is still populated).

Note, the function below should work in most cases however is purely for demonstration purposes only and should not be relied upon.

First we define a new function that can be used to display progress as well as wait for our jobs based on a timeout:

using namespace System.Collections.Generic
using namespace System.Diagnostics
using namespace System.Threading
using namespace System.Management.Automation

function Wait-JobWithProgress {
    [cmdletbinding()]
    param(
        [parameter(Mandatory, ValueFromPipeline)]
        [object[]] $InputObject,

        [parameter()]
        [ValidateRange(1, [double]::MaxValue)]
        [double] $TimeOut # In seconds!
    )

    begin {
        $jobs = [List[object]]::new()

        if ($withTimeOut = $PSBoundParameters.ContainsKey('TimeOut')) {
            $span = [timespan]::FromSeconds($TimeOut)
        }
    }

    process {
        $jobs.AddRange($InputObject)
    }

    end {
        $timer = [Stopwatch]::StartNew()
        $total = $jobs.Count
        $completed = 0.1

        while ($jobs.Count) {
            if ($withTimeOut -and $timer.Elapsed -gt $span) {
                break
            }

            $remaining = $total - $completed
            $average = $timer.Elapsed.TotalSeconds / $completed
            $estimate = [math]::Round($remaining * $average)
            $status = 'Completed Jobs: {0:0} of {1}' -f $completed, $total
            $progress = @{
                Activity         = 'Waiting for Jobs'
                PercentComplete  = $completed / $total * 100
                Status           = $status
                SecondsRemaining = $estimate
            }
            Write-Progress @progress

            $id = [WaitHandle]::WaitAny($jobs.Finished, 200)
            if ($id -eq [WaitHandle]::WaitTimeout) {
                continue
            }

            # output this job
            $jobs[$id]
            # remove this job
            $jobs.RemoveAt($id)
            $completed++
        }

        # Stop the jobs not yet Completed and remove them
        $jobs | Stop-Job -PassThru | ForEach-Object {
            Remove-Job -Job $_
            'Job [#{0} - {1}] did not complete on time and was removed.' -f $_.Id, $_.Name
        } | Write-Warning
        Write-Progress @progress -Completed
    }
}

Then for testing it, we can create a few jobs with a random timer:

0..10 | ForEach-Object {
    Start-Job {
        Start-Sleep (Get-Random -Minimum 5 -Maximum 15)
        [pscustomobject]@{
            Job    = $using:_
            Result = 'Hello from [Job #{0:D2}]' -f $using:_
        }
    }
} | Wait-JobWithProgress -TimeOut 10 |
Receive-Job -AutoRemoveJob -Wait | Format-Table -AutoSize
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • Why not get-job|` Wait-JobWithProgress -Verbose | Receive-Job -AutoRemoveJob -Wait | ` Format-Table -AutoSize – Patrick Burwell Mar 16 '23 at 23:19
  • Does this work with PowerShell 5.1? Since this answer is from 2022, which made me just want to confirm on that. Thanks. – Fajela Tajkiya Jun 03 '23 at 07:12
  • 1
    @FajelaTajkiya all my answers are for 5.1 unless specified – Santiago Squarzon Jun 03 '23 at 14:12
  • 1
    Nice solution, will certainly be able to reuse. Just wanted to ask if the $expression variable is used correctly. It seems it's only checked once before the while loop, and after that only to update the progress. The only condition used to break the loop is the $jobs collection. Or am I missing something? – Terry Jul 11 '23 at 10:07
  • @Terry it was being used correctly in the `while` condition itself: `(& $expression)` but I realized that was too complicated and changed the logic to make it more simple to understand – Santiago Squarzon Jul 11 '23 at 12:40
  • 1
    @SantiagoSquarzon I wasn't familiar with how the expression was evaluated (with the &), just realized it through debugging. Another day, something learned. A timer is a nice touch to the function, I see a benefit of using it. In any case, I got a working variant of your example, so cheers for that. – Terry Jul 11 '23 at 12:50