PowerShell How-To

Building a Wait-Action Function in PowerShell

The next time you need to wait on a step in your script, don't just add a delay. Instead, use a Wait-Action function to wait just the right amount of time.

A script is all about defining a set of actions and running them together. Most of the time, when a command is executed in PowerShell, PowerShell waits for the command to finish. When it's finished, the script continues doing its thing. Each step is dependent on the last step.

This works when each commands natively waits, but sometimes there's a process that immediately releases control and PowerShell continues to run the rest of the script.

But what if other tasks depend on the previous one being done? In that case, you've got two options. You can either add a Start-Sleep line in your script to simply wait a set amount of time and hope the task is done, or you can periodically check to see if the task is done and only then release control to the rest of the script. Periodically checking the task is always recommended because you'll never accidentally continue too soon or too late. It will be just in time.

Creating a Wait-Action function in PowerShell consists of the code to check the status of the task, a while loop and a timer. Using the while loop, we'll continually run the code to check the status of the task; and to be sure we don't get stuck in an infinite loop, we'll set a timer ahead of time and ensure the process times out after a fixed amount of time.

First, we need to build the code that will return True or False depending on whether the task is complete. I'll make it return False if it's not done yet or True to indicate it's complete. For this example, I'll just use something simple like a file exists in a specific path. Perhaps we've got another process that drops a file in a specific spot, and we have another script that performs some action on that file.

The code to check for a file's existence is Test-Path -Path <FilePath> -PathType Leaf. Now that we have the code to confirm the file's existence, let's now build the while loop which will continually execute the code until the condition is False.

while (-not (Test-Path -Path  -PathType Leaf) {
    Write-Verbose -Message "Still waiting for action to complete after [$totalSecs] seconds..."
}

This will continually check for the condition, but we have a problem: It's going to check as fast as PowerShell can run! Also, what if the file never appears? Our script is just going to hang. To fix these things, we have to do two things: Add a small delay to the check and introduce a timer.

$Timeout = 60
$timer = [Diagnostics.Stopwatch]::StartNew()
while (($timer.Elapsed.TotalSeconds -lt $Timeout) -and (-not (Test-Path -Path  -PathType Leaf)) {
    Start-Sleep -Seconds 1
    Write-Verbose -Message "Still waiting for action to complete after [$totalSecs] seconds..."
}
$timer.Stop()

Above, you can see that I've defined a timeout or how long this will wait. I'm then checking that timer every time the loop runs again to ensure it's always less than the timeout. I've also introduced a delay using Start-Sleep to perform the check every second.

That's about all there is to waiting on a task. We can, however, create an elegant PowerShell function to do this for us that's generic. By using a scriptblock to encapsulate the code we'd like to perform the check and ensuring that it always returns True or False, we can build a useful Wait-Action function. The function below allows you to run any code, specify a timeout and the delay between checks.

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

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [int]$Timeout,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [object[]]$ArgumentList,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [int]$RetryInterval = 5
    )
    try {
        $timer = [Diagnostics.Stopwatch]::StartNew()
        while (($timer.Elapsed.TotalSeconds -lt $Timeout) -and (-not (& $Condition $ArgumentList)) {
            Start-Sleep -Seconds $RetryInterval
            $totalSecs = [math]::Round($timer.Elapsed.TotalSeconds, 0)
            Write-Verbose -Message "Still waiting for action to complete after [$totalSecs] seconds..."
        }
        $timer.Stop()
        if ($timer.Elapsed.TotalSeconds -gt $Timeout) {
            throw 'Action did not complete before timeout period.'
        } else {
            Write-Verbose -Message 'Action completed before timeout period.'
        }
    } catch {
        Write-Error -Message $_.Exception.Message
    }
}

The next time you need to wait on a step in your script, don't just add a delay. Instead, use a Wait-Action function to periodically check the status to wait the exact right amount of time.

About the Author

Adam Bertram is a 20-year veteran of IT. He's an automation engineer, blogger, consultant, freelance writer, Pluralsight course author and content marketing advisor to multiple technology companies. Adam also founded the popular TechSnips e-learning platform. He mainly focuses on DevOps, system management and automation technologies, as well as various cloud platforms mostly in the Microsoft space. He is a Microsoft Cloud and Datacenter Management MVP who absorbs knowledge from the IT field and explains it in an easy-to-understand fashion. Catch up on Adam's articles at adamtheautomator.com, connect on LinkedIn or follow him on Twitter at @adbertram or the TechSnips Twitter account @techsnips_io.


comments powered by Disqus
Most   Popular