A PowerShell script doesn't always progress one task at a time without delay. There are sometimes instances when in order to one task to start another one must complete. In this post, I'll show you how to intelligently build code to wait for that take instead of simply inserting a Start-Sleep reference.

There have been many times when I'm writing a script that, during execution, needs something else to be in a particular state before proceeding. It could be a particular server to be online, some other process doing something that your script depends on, etc.

When I first started scripting, I'd gauge the typical time it took for that process to do it's thing and put in a Start-Sleep reference for however many seconds I thought the process might take. If I assumed it would take 10 seconds, I'd put Start-Sleep -Seconds 15 as a fudge factor.

This approach "worked" but there was those occasional times when the process actually took longer than expected, my script would continue and then subsequently blow up because that dependency wasn't there. The problem here was that I had to assume how long that process would take. This is never a good practice.

You must account for as many variables as possible and handle them appropriately.

Instead, I should have been periodically checking that state over and over until the state was reached. This way, it didn't matter how long the external process lasted. My script could accommodate without modification.

Let's go over an example. Let's say that your script depends on some other server being up. There's some external process at stake that sometimes reboots the server while your script is running but you want to ensure that doesn't stop your script. Your script must ensure that this server is online. If it's not, you want to wait for up to 5 minutes.

If you were me a few years ago, your code might look something like this:

## Do some code
$remoteServer = 'MYSERVER'

if (-not (Test-Connection -ComputerName $remoteServer -Quiet -Count 1))
{
� � Start-Sleep -Seconds 300
}

## Connect to the remote server
$session = New-PSSession -ComputerName $remoteServer

## Do some more stuff in the session

You can see I'm doing some stuff, checking to see if $remoteServer is online, if not, sleeping for 5 minutes then immediately trying to establish a new PowerShell remoting session to $remoteServer afterwards.

This would work great if the server was for sure going to be up after 5 minutes. But what if it came back sooner? Your script is wasting time just waiting on something that's already available. If it came back later (or at all), your script would bomb. There's no win scenario here! The solution? Create a small function called Wait-Ping that pings the server over and over until it comes back up which will hold the script until that happens.

To do this will require a while loop and a timer. I always include a timer because if not, your Wait-Ping function might just wait forever if MYSERVER never comes back up!

A good PowerShell function function consists of three things:

  • A true/false condition to indicate whether the task is done or not
  • A loop to continually check for that condition
  • A pause so that PowerShell doesn't test the condition ever microsecond.
  • A timer to ensure the loop doesn't continually run

Build the Condition

In our example above, the condition is whether or not a server responds to a ping request. This condition can be defined with a single line of code. The condition below returns a boolean True or False result which is exactly what you want.

Test-Connection -ComputerName $ComputerName -Quiet -Count 1

Build the While Loop

Next, you need to check for your condition over and over until the condition returns True or False. The best way to do this is to use a PowerShell while loop. The while loop continually runs while a condition is met. You can see below where you can add in the condition for the while loop.

This while loop below would ping the SRV1 computer as fast as it possibly could until Test-Connection returned True.

$ComputerName = 'SRV1'
while (-not (Test-Connection -ComputerName $ComputerName -Quiet -Count 1)) {
    ## Other code in here
}

Pausing for Every Iteration

To prevent PowerShell from ping our server every waking microsecond, it's also important to introduce a delay in the loop for every iteration. This delay time is up to you but I typically ensure it tests the condition once per second using Start-Sleep.

Now you can see below that Test-Connection will only run once per second while it is returning False.

$ComputerName = 'SRV1'
while (-not (Test-Connection -ComputerName $ComputerName -Quiet -Count 1)) {
    Start-Sleep -Seconds 1
}

Adding a Timer

Finally, there will always be that one time your condition never is met. Instead of risking your PowerShell script to run forever, you need to add a timeout. One of the best way to add a timer to your PowerShell wait code is to use the Diagnostics.Stopwatch object. This object allows you to start and stop timers at will.

To demonstrate the stopwatch, start it using the StartNew() method and then check the Elapsed.TotalSeconds object property over and over. You'll notice that it increments every time it's read. Then, to stop it, use the Stop() method which will force the stopwatch to cease incrementing as shown below.

$timer = [Diagnostics.Stopwatch]::StartNew()
$timer.Elapsed.TotalSeconds
$timer.Stop()

This timer can be integrated into your PowerShell wait function as shown below. Notice that I'm using the Elapsed.TotalSeconds object property consistently checking to see if the script has been running over a specific number of seconds.

I've also defined all of the variables at the top of the PowerShell wait code which allows me to tweak things as needed.

The resulting code would like like the below example.

$Timeout = 5 ## seconds
$ComputerName = 'MYCOMPUTER'
$CheckEvery = 1 ## second

## Start the timer
$timer = [Diagnostics.Stopwatch]::StartNew()

## Keep in the loop while the computer is not pingable
while (-not (Test-Connection -ComputerName $ComputerName -Quiet -Count 1))
{
� � Write-Verbose -Message "Waiting for [$($ComputerName)] to become pingable..."
� � ## If the timer has waited greater than or equal to the timeout, throw an exception exiting the loop
� � if ($timer.Elapsed.TotalSeconds -ge $Timeout)
� � {
� �� � throw "Timeout exceeded. Giving up on ping availability to [$ComputerName]"
� � }
� � ## Stop the loop every $CheckEvery seconds
� � Start-Sleep -Seconds $CheckEvery
}

## When finished, stop the timer
$timer.Stop()
Waiting for a ping response with timeout

Building the PowerShell Wait Function

Finally, wrapping up all of the knowledge you've accumulated throughout this post, you can create a generic function called Wait-Condition that allows you to wait for any condition; not just a ping response.

An example of what that might look like is below. If you're not familiar with functions, check out my PowerShell functions introduction blog post. I also have a short blog post with a different way to implement a PowerShell timer as well.

function Wait-Condition {
    [OutputType('void')]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [scriptblock]$Condition,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [int]$CheckEvery = 1, ## seconds

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [int]$Timeout = 10 ## seconds
    )

    $ErrorActionPreference = 'Stop'

    try {
        ## Start the timer
        $timer = [Diagnostics.Stopwatch]::StartNew()

        ## Keep in the loop while the computer is not pingable
        Write-Verbose -Message "Waiting for condition..."
        while (-not (& $Condition)) {
            Write-Verbose -Message "Waiting for condition..."
            ## If the timer has waited greater than or equal to the timeout, throw an exception exiting the loop
            if ($timer.Elapsed.TotalSeconds -ge $Timeout) {
                throw "Timeout exceeded. Giving up..."
            }
            ## Stop the loop every $CheckEvery seconds
            Start-Sleep -Seconds $CheckEvery
        }
    } catch {
        $PSCmdlet.ThrowTerminatingError($_)
    } finally {
        ## When finished, stop the timer
        $timer.Stop()
    }
}
Waiting for a condition in PowerShell

Join the Jar Tippers on Patreon

It takes a lot of time to write detailed blog posts like this one. In a single-income family, this blog is one way I depend on to keep the lights on. I'd be eternally grateful if you could become a Patreon patron today!

Become a Patron!