Public/Psrunner/Invoke-RetriableCommand.ps1
function Invoke-RetriableCommand { # .SYNOPSIS # Runs Retriable Commands # .DESCRIPTION # Retries a script process for a number of times or until it completes without terminating errors. # All Unnamed arguments will be passed as arguments to the script # .NOTES # Information or caveats about the function e.g. 'This function is not supported in Linux' # .LINK # https://github.com/alainQtec/PsRunner/blob/main/Public/Invoke-RetriableCommand.ps1 # .EXAMPLE # Invoke-RetriableCommand -ScriptBlock $downloadScript -Verbose # Retries the download script 3 times (default) [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ScriptBlock')] [OutputType([PSCustomObject])][Alias('Invoke-rtCommand')] param ( [Parameter(Mandatory = $false, Position = 0, ParameterSetName = 'ScriptBlock')] [Alias('Script')] [ScriptBlock]$ScriptBlock, [Parameter(Mandatory = $false, Position = 0, ParameterSetName = 'Command')] [Alias('Command', 'CommandPath')] [string]$FilePath, [Parameter(Mandatory = $false, Position = 1, ParameterSetName = '__AllParameterSets')] [Object[]]$ArgumentList, [Parameter(Mandatory = $false, Position = 2, ParameterSetName = '__AllParameterSets')] [CancellationToken]$CancellationToken = [CancellationToken]::None, [Parameter(Mandatory = $false, Position = 3, ParameterSetName = '__AllParameterSets')] [Alias('Retries', 'MaxRetries')] [int]$MaxAttempts = 3, [Parameter(Mandatory = $false, Position = 4, ParameterSetName = '__AllParameterSets')] [int]$MillisecondsAfterAttempt = 500, [Parameter(Mandatory = $false, Position = 5, ParameterSetName = '__AllParameterSets')] [string]$Message = "Running $('[' + $MyInvocation.MyCommand.Name + ']')" ) DynamicParam { $DynamicParams = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new() $attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new() $attributes = [System.Management.Automation.ParameterAttribute]::new(); $attHash = @{ Position = 6 ParameterSetName = '__AllParameterSets' Mandatory = $False ValueFromPipeline = $true ValueFromPipelineByPropertyName = $true ValueFromRemainingArguments = $true HelpMessage = 'Allows splatting with arguments that do not apply. Do not use directly.' DontShow = $False }; $attHash.Keys | ForEach-Object { $attributes.$_ = $attHash.$_ } $attributeCollection.Add($attributes) # $attributeCollection.Add([System.Management.Automation.ValidateSetAttribute]::new([System.Object[]]$ValidateSetOption)) # $attributeCollection.Add([System.Management.Automation.ValidateRangeAttribute]::new([System.Int32[]]$ValidateRange)) # $attributeCollection.Add([System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new()) # $attributeCollection.Add([System.Management.Automation.AliasAttribute]::new([System.String[]]$Aliases)) $RuntimeParam = [System.Management.Automation.RuntimeDefinedParameter]::new("IgnoredArguments", [Object[]], $attributeCollection) $DynamicParams.Add("IgnoredArguments", $RuntimeParam) return $DynamicParams } begin { [System.Management.Automation.ActionPreference]$eap = $ErrorActionPreference; $ErrorActionPreference = "SilentlyContinue" $fxn = '[PsRunner]'; $PsBoundParameters.GetEnumerator() | ForEach-Object { New-Variable -Name $_.Key -Value $_.Value -ea 'SilentlyContinue' } $Output = [string]::Empty $Result = [PSCustomObject]@{ Output = $Output IsSuccess = [bool]$IsSuccess # $false in this case ErrorRecord = $null } } process { $Attempts = 1; $CommandStartTime = Get-Date while (($Attempts -le $MaxAttempts) -and !$Result.IsSuccess) { $Retries = $MaxAttempts - $Attempts if ($cancellationToken.IsCancellationRequested) { Write-Verbose "$fxn CancellationRequested when $Retries retries were left." throw } try { $downloadAttemptStartTime = Get-Date if ($PSCmdlet.ShouldProcess("$fxn Retry Attempt # $Attempts/$MaxAttempts ...", ' ', '')) { if ($PSCmdlet.ParameterSetName -eq 'Command') { try { $Output = & $FilePath $ArgumentList $IsSuccess = [bool]$? } catch { $IsSuccess = $false $ErrorRecord = $_.Exception.ErrorRecord # Write-Log $_.Exception.ErrorRecord Write-Verbose "$fxn Errored: $($_.CategoryInfo.Category) : $($_.CategoryInfo.Reason) : $($_.Exception.Message)" } } else { $Output = Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList $IsSuccess = [bool]$? } } } catch { $IsSuccess = $false $ErrorRecord = [System.Management.Automation.ErrorRecord]$_ # Write-Log $_.Exception.ErrorRecord Write-Verbose "$fxn Error encountered after $([math]::Round(($(Get-Date) - $downloadAttemptStartTime).TotalSeconds, 2)) seconds" } finally { $Result = [PSCustomObject]@{ Output = $Output IsSuccess = $IsSuccess ErrorRecord = $ErrorRecord } if ($Retries -eq 0 -or $Result.IsSuccess) { $ElapsedTime = [math]::Round(($(Get-Date) - $CommandStartTime).TotalSeconds, 2) $EndMsg = $(if ($Result.IsSuccess) { "Completed Successfully. Total time elapsed $ElapsedTime" } else { "Completed With Errors. Total time elapsed $ElapsedTime. Check the log file $LogPath" }) } elseif (!$cancellationToken.IsCancellationRequested -and $Retries -ne 0) { Write-Verbose "$fxn Waiting $MillisecondsAfterAttempt seconds before retrying. Retries left: $Retries" [System.Threading.Thread]::Sleep($MillisecondsAfterAttempt); } $Attempts++ } } } end { Write-Verbose "$fxn $EndMsg" $ErrorActionPreference = $eap; return $Result } } <# inspiration function Invoke-Program { # .SYNOPSIS # This is a helper function that will invoke the SQL installers via command-line. # .EXAMPLE # PS> Invoke-Program -FilePath C:\file.exe -ComputerName SRV1 [CmdletBinding()] [OutputType([System.Management.Automation.PSObject])] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$FilePath, [Parameter(Mandatory)] [string]$ComputerName, [Parameter()] [ValidateNotNullOrEmpty()] [string]$ArgumentList, [Parameter()] [bool]$ExpandStrings = $false, [Parameter()] [ValidateNotNullOrEmpty()] [string]$WorkingDirectory, [Parameter()] [ValidateNotNullOrEmpty()] [uint32[]]$SuccessReturnCodes = @(0, 3010) ) begin { $ErrorActionPreference = 'Stop' } process { try { Write-Verbose -Message "Acceptable success return codes are [$($SuccessReturnCodes -join ',')]" $scriptBlock = { $VerbosePreference = $using:VerbosePreference $processStartInfo = New-Object System.Diagnostics.ProcessStartInfo; $processStartInfo.FileName = $Using:FilePath; if ($Using:ArgumentList) { $processStartInfo.Arguments = $Using:ArgumentList; if ($Using:ExpandStrings) { $processStartInfo.Arguments = $ExecutionContext.InvokeCommandWithCred.ExpandString($Using:ArgumentList); } } if ($Using:WorkingDirectory) { $processStartInfo.WorkingDirectory = $Using:WorkingDirectory; if ($Using:ExpandStrings) { $processStartInfo.WorkingDirectory = $ExecutionContext.InvokeCommandWithCred.ExpandString($Using:WorkingDirectory); } } $processStartInfo.UseShellExecute = $false; # This is critical for installs to function on core servers $ps = New-Object System.Diagnostics.Process; $ps.StartInfo = $processStartInfo; Write-Verbose -Message "Starting process path [$($processStartInfo.FileName)] - Args: [$($processStartInfo.Arguments)] - Working dir: [$($Using:WorkingDirectory)]" $null = $ps.Start(); if (!$ps) { throw "Error running program: $($ps.ExitCode)" } else { $ps.WaitForExit() } # Check the exit code of the process to see if it succeeded. if ($ps.ExitCode -notin $Using:SuccessReturnCodes) { throw "Error running program: $($ps.ExitCode)" } } # Run program on specified computer. Write-Verbose -Message "Running command line [$FilePath $ArgumentList] on $ComputerName" Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptblock } catch { $PSCmdlet.ThrowTerminatingError($_) } } } #> |