Tuesday, December 27, 2016

A function for robust command execution in PowerShell

I have always been a great fan of a tool named RoboCopy, which I bet many of you have used countless times. Now these days I have been in need for this very same type of robustness for one of my functions since I am running it in an unreliable environment.

What I wanted to achieve with PowerShell in particular, was to fetch a great deal of data from public web servers and reuse the information in my scripts. Unfortunately when you use a cmdlet, be it Invoke-RestMethod, or Test-Connection, you can get failures which are not due to your cmdlets but to the underlying infrastructure (such as a Wi-Fi network, a distorted topology because of a flapping router or a way too busy web server).

Sure Invoke-RestMethod has a TimeoutSec parameter, but what if it fails and I really need the information coming from that website? Well, this reasoning brought me to write an advanced function that takes a command and its parameters and tries to run it a given number of times (three by default) with intervals of three seconds.

This function, which I called Start-RoboCommand (Start is an approved verb, so that my PSScriptAnalyzer is happy, and I borrowed the idea of the RoboCommand noun from RoboCopy itself), also supports an improved functioning where the command is run indefinitely, through the addition of a -Wait parameter, such as the one you can find in recent versions of Get-Content.

To finish with, I added a LogFile parameter to log errors (which is particularly important here because we are exactly dealing with commands not being successfull) and Verbose, which tells you exactly what's going wrong.


Now without further ado, here's my function:

function Start-RoboCommand {

<#
.Synopsis
   Function that tries to run a command until it succeeds or forever
.DESCRIPTION
   Function that tries to run a command until it succeeds or forever. By default this function tries to run a command three times with three seconds intervals.
.PARAMETER Command
    Command to execute
.PARAMETER Args
    Arguments to pass to the command
.PARAMETER Count
    Number of tries before throwing an error
.PARAMETER Wait
    Run the command forever even if it succeeds
.PARAMETER Delay
    Time in seconds between two tries
.PARAMETER LogFile
    The path to the error log
.EXAMPLE
   Start-RoboCommand -Command 'Invoke-RestMethod' -Args @{ URI = "http://guid.it/json"; TimeoutSec = 1 } -Count 2 -Verbose
.EXAMPLE
   Start-RoboCommand -Command 'Invoke-RestMethod' -Args @{ URI = "http://notexisting.it/json"; TimeoutSec = 1 } -Count 2 -Verbose
.EXAMPLE
   Start-RoboCommand -Command 'Invoke-RestMethod' -Args @{ URI = "http://guid.it/json"; TimeoutSec = 1 } -Wait -Verbose
.EXAMPLE
   Start-RoboCommand -Command 'Invoke-RestMethod' -Args @{ URI = "http://notexisting.it/json"; TimeoutSec = 1 } -Wait -Verbose
.EXAMPLE
   Start-RoboCommand -Command 'Test-Connection' -Args @{ ComputerName = "bing.it" } -Wait -Verbose
.EXAMPLE
   Start-RoboCommand -Command 'Test-Connection' -Args @{ ComputerName = "nocomputer" } -Wait -LogFile $Env:temp\error.log -Verbose
.EXAMPLE
   Start-RoboCommand -Command Get-Content -Args @{path='d:\inputfile.txt'} -Wait -DelaySec 2 -LogFile $Env:temp\error.log -Verbose
.NOTES
   happysysadm.com
   @sysadm2010
#>

    [CmdletBinding(SupportsShouldProcess,DefaultParameterSetName='Limited')]
    Param (
    
    [Parameter(Mandatory=$true)]
    [Alias("Cmd")]
    [string]$Command, 

    [Parameter(Mandatory=$true)]
    [hashtable]$Args, 

    [Parameter(Mandatory=$false,ParameterSetName = 'Limited')]
    [int32]$Count = 3, 

    [Parameter(Mandatory=$false,ParameterSetName = 'Forever')]
    [switch]$Wait,

    [Parameter(Mandatory=$false)]
    [int32]$DelaySec = 3,

    [Parameter(Mandatory=$false)]
    $LogFile
    )
    
    $Args.ErrorAction = "Stop"
        
    $RetryCount = 0

    $Success = $false
    
    do {

        try {

            & $command @args

            Write-Verbose "$(Get-Date) - Command $Command with arguments `"$($Args.values[0])`" succeeded."

            if(!$Wait) {
                
                $Success = $true

                }
            
            }
        
        catch {

            if($LogFile) {

                "$(Get-Date) - Error: $($_.Exception.Message) - Command: $Command - Arguments: $($Args.values[0])" | Out-File $LogFile -Append

                }
            
            if ($retrycount -ge $Count) {

                Write-Verbose "$(Get-Date) - Command $Command with arguments `"$($Args.values[0])`" failed $RetryCount times. Exiting."

                $PSCmdlet.ThrowTerminatingError($_)
                
                }

            else {

                Write-Verbose "$(Get-Date) - Command $Command with arguments `"$($Args.values[0])`" failed. Retrying in $DelaySec seconds."

                Start-Sleep -Seconds $DelaySec

                if(!$Wait) {
                
                    $RetryCount++

                    }

                }

            }

        }

    while (!$Success)

 }

Let me know how it works for you and if you have any suggestion on the logic I'll be more than happy to improve it over time. Fo sure you can find it also on my github.

No comments:

Post a Comment

Related Posts Plugin for WordPress, Blogger...