Invoking arbitrary PowerShell commands with retries in PowerShell

Yesterday I blogged about a function I created to execute arbitrary shell (cmd) commands in PowerShell, with retries in face of failures. But what if the command I want to execute is actually another PowerShell script (or function, or cmdlet)? The rules change a bit:

  • We need to use splatting when making the call (otherwise named parameters won’t work, see the references below).
  • In addition to splatting the call itself, we also need to format the argument hash table for printing the command (otherwise it would just print something like Executing: Foo.ps1 System.Collections.Hashtable).
  • $LASTEXITCODE is no longer (necessarily) relevant, we need to work with PowerShell’s $? automatic variable.
  • Parsing the error stream makes less sense, but we need to handle exceptions.

Here’s how it looks:

<#

.SYNOPSIS
Invokes a command with retries.

.DESCRIPTION
The Call-PSCommandWithRetries function invokes PowerShell scripts, functions, or Cmdlets using the provided parameters, with optional retries in configurable intervals upon failures.

.PARAMETER Command 
The command to invoke. Can be any PowerShell script, function, or Cmdlet.

.PARAMETER Arguments
Arguments to pass when invoking the comand (using splatting). 

.PARAMETER RetrySleepSeconds
Time in seconds to sleep between retry attempts in case of command failure.

.PARAMETER MaxAttempts
Maximum number of retry attempts in case of command failure.

.PARAMETER PrintCommand
Determines whether or not to print the full command to the host before execution.

.INPUTS 
None. You cannot pipe objects to Call-PSCommandWithRetries.

.OUTPUTS
The output of the last command execution.

.EXAMPLE
$output = Call-PSCommandWithRetries "dir" @{"Path"='C:\'}

#>
function Call-PSCommandWithRetries
{
 [CmdletBinding()]
 Param( 
    [Parameter(Mandatory=$True)]
    [string]$Command,
    [hashtable]$Arguments,
    [int]$RetrySleepSeconds = 10,
    [int]$MaxAttempts = 10,
    [bool]$PrintCommand = $True
 )

 Process
 {
  $attempt = 0
  while ($true)
  {
   $formattedArgs = $Arguments.Keys.ForEach({"-$($_):$($Arguments.$_)"}) -join ' '
   Write-Host $(if ($PrintCommand) {"Executing: $Command $formattedArgs"} else {"Executing PS command..."}) 
  
   $exceptionThrown = $false
   try
   {
    & $Command @Arguments 2>&1 | tee -Variable output | Write-Host
   }
   catch
   {
    Write-Host "PS command threw exception: $($_.Exception)" -ForegroundColor Yellow
    $exceptionThrown = $true
   }
  
   if ($? -and !($exceptionThrown))
   {
    Write-Host "PS Command executed successfully"
    return $output
   }
  
   Write-Host "PS Command failed" -ForegroundColor Yellow
   if ($attempt -eq $MaxAttempts)
   {
    $ex = new-object System.Management.Automation.CmdletInvocationException "All retry attempts exhausted"
    $category = [System.Management.Automation.ErrorCategory]::LimitsExceeded
    $errRecord = new-object System.Management.Automation.ErrorRecord $ex, "CommandFailed", $category, $Path
    $psCmdlet.WriteError($errRecord)
    return $output
   }
    
   $attempt++;
   Write-Host "Retrying test execution [#$attempt/$MaxAttempts] in $RetrySleepSeconds seconds..."
   Start-Sleep -s $RetrySleepSeconds
  }
 }
}

References:

Invoking arbitrary shell (cmd) commands with retries in PowerShell

EDIT– I initially tried to support execution of PowerShell scripts, functions, and cmdlets in the code below. However, it turns out that they behave too differently so I wrote a separate post on it: https://www.ohadsoft.com/2016/04/invoking-arbitrary-powershell-commands-with-retries-in-powershell/.

It may sound ironic, but executing shell (cmd) commands from PowerShell is not always trivial. There are various possibilities including Start-Process, [Diagnostics.Process]::Start, the call operator (&), and others. Each has its own quirks regarding exit codes, output redirection, argument escaping, and more. For a taste of the issues you might run into, see this blog post by edgylogic.

Having played around with most (all?) of the options, I finally settled down on what I percieve to be the most robust, versatile, and simple way of doing it. The (advanced) function below will allow the following:

  • Print the command before execution.
  • Inspect the exit code and error stream to determine failure.
  • Retry the command in configurable intervals upon failure.
  • Pass multiple parameters.
  • Emit the command’s output to the pipe for further processing.
  • Set the execution status ($?) to $False in case of failre on all retries.
<#

.SYNOPSIS
Calls a shell (cmd) command with retries

.DESCRIPTION
The Call-CommandWithRetries function calls shell (cmd) commands using the provided parameters, with optional retries in configurable intervals upon failures.

.PARAMETER Command 
The command to call.

.PARAMETER Arguments
Arguments to pass when invoking the comand. 

.PARAMETER TrustExitCode
Trust the command's exit code for the purpose of determining whether it was successful or not. 
If this parameter is $False, a non-empty stderr will also be considered a failure.

.PARAMETER RetrySleepSeconds
Time in seconds to sleep between retry attempts in case of command failure.

.PARAMETER MaxAttempts
Maximum number of retry attempts in case of command failure.

.PARAMETER PrintCommand
Determines whether or not to print the full command to the host before execution.

.INPUTS 
None. You cannot pipe objects to Call-CommandWithRetries.

.OUTPUTS
The output of the last command execution.

.EXAMPLE
Use cURL for Windows to download the latest NuGet command-line client
C:\PS> Call-CommandWithRetries "curl.exe" @("--fail", "-O", "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe")

#>
function Call-CommandWithRetries
{
 [CmdletBinding()]
 param( 
     [Parameter(Mandatory=$True)]
     [string]$Command,
     [Array]$Arguments,
     [bool]$TrustExitCode = $True,
     [int]$RetrySleepSeconds = 10,
     [int]$MaxAttempts = 10,
     [bool]$PrintCommand = $True
 )

 Process
 {
  $attempt = 0
  while ($true)
  {   
   Write-Host $(if ($PrintCommand) {"Executing: $Command $Arguments"} else {"Executing command..."}) 
   & $Command $Arguments 2>&1 | tee -Variable output | Write-Host
        
   $stderr = $output | where { $_ -is [System.Management.Automation.ErrorRecord] }
   if ( ($LASTEXITCODE -eq 0) -and ($TrustExitCode -or !($stderr)) )
   {
    Write-Host "Command executed successfully"
    return $output
   }

   Write-Host "Command failed with exit code ($LASTEXITCODE) and stderr: $stderr" -ForegroundColor Yellow
   if ($attempt -eq $MaxAttempts)
   {
    $ex = new-object System.Management.Automation.CmdletInvocationException "All retry attempts exhausted"
    $category = [System.Management.Automation.ErrorCategory]::LimitsExceeded
    $errRecord = new-object System.Management.Automation.ErrorRecord $ex, "CommandFailed", $category, $Command
    $psCmdlet.WriteError($errRecord)
    return $output
   }
            
   $attempt++;
   Write-Host "Retrying test execution [#$attempt/$MaxAttempts] in $RetrySleepSeconds seconds..."
   Start-Sleep -s $RetrySleepSeconds
  }
 }
}

References:

Creating beautiful WordPress header images for free – legally

Personally, I like using the official WordPress.org themes as they always seem to look professional and clean. Moreover, they are very unlikely to contain bugs or compatibility issues like I’ve seen in the past with various other themes I tried. Consequently, I recently updated my blog’s theme to Twenty Sixteen. To my surprise though, no header image was included (it is included in other WordPress themes) and the top section seemed a little barren.

A quick web search for free WordPress header images turned up a few sites that did have some nice photos, but required you to link back to them which is a bit cumbersome. Fortunately, I realized there was a better way.

Enter Freeimages. This neat site contains almost 400,000 free images – many of them free even for commercial use (if you have ads on your site, it is considered commercial). You can also usually modify the image (so cropping it to fit your theme should be OK), and for the most part not even attribution is necessary! Of course, you should always check the license on each image to be sure.

So in a nutshell, here’s what you need to do:

  1. Search for some pictures you might like for your header
  2. Pick the one you like the most (or several, if you’d like to rotate them)
  3. Download a high-res copy so that WordPress only has to crop it and/or scale it down (rather than scale it up / stretch it)
  4. Make sure you abide by the license
  5. Upload and crop the image (via the WordPress theme customizer)

If you don’t find an image you fancy in that particular site, there are many others you can browse. See if you can spot the image you’re currently seeing at the top of this page 😉

Happy hunting!

Redirecting all URLs to their HTTPS WWW equivalents

Whether you’re a no-www or a yes-www person (you can guess which one I am based on the URL currently present on your address bar 🙂), one thing is certain – you need to be consistent. This means that if you like www, then yourdomain.com should redirect (301 permanent) to www.yourdomain.com, and if you don’t like it then the redirection needs to go the other way around. Either way, you can’t have people browsing the same page from different domain (with www and without it), it’s just confusing.

In addition, you really should be using SSL everywhere, all the time. Certificates are free, and even if your hosting plan doesn’t support them (as is the case with Azure’s free tier), you can have CloudFlare take care of that for you (and enjoy improved performance and security as a bonus). Once you have SSL up and running, you’d want to redirect all HTTP traffic to HTTPS and enable HTS for increased security.

To recap – once you’ve decided whether you like www or not, and finished setting up SSL, you want your redirections to look like this (assuming yes-www):

  • HTTP://yourdomain.com -> HTTPS://www.yourdomain.com
  • HTTP://www.yourdomain.com -> HTTPS://www.yourdomain.com
  • HTTPS://yourdomain.com -> HTTPS://www.yourdomain.com

In addition, you want HTS headers (Strict_Transport_Security) in place. Fortunately, using the IIS Rewrite module (installed by default on Azure deployments), accomplishing all of the above is a breeze:

<configuration>
<system.webServer>
 <rewrite>
  <rules>
   <rule name="Redirect to www" stopProcessing="true">
    <match url="(.*)" />
    <conditions logicalGrouping="MatchAny">
     <add input="{HTTP_HOST}" pattern="^yourdomain\.com"/>
     <add input="{HTTPS}" pattern="off" ignoreCase="true"/>
    </conditions>
    <action type="Redirect" 
            url="https://www.yourdomain.com/{R:1}" 
            redirectType="Permanent"/>
   </rule>
  </rules>
  <outboundRules>
   <rule name="HSTS" enabled="true">
    <match 
       serverVariable="RESPONSE_Strict_Transport_Security" 
       pattern=".*" />
    <conditions>
     <add input="{HTTPS}" pattern="on" ignoreCase="true" />
    </conditions>
    <action type="Rewrite" value="max-age=31536000" />
   </rule>
  </outboundRules>
 </rewrite>
</system.webServer>
</configuration>

Happy rewriting 😉