Public/Utils/Start-ExternalProcess.ps1
function Start-ExternalProcess { <# .SYNOPSIS Runs external process. .DESCRIPTION Runs an external process with proper logging and error handling. It fails if anything is present in stderr stream or if exitcode is non-zero. .EXAMPLE Start-ExternalProcess -Command "git" -ArgumentList "--version" #> [CmdletBinding()] [OutputType([int])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidDefaultValueSwitchParameter', '')] param( # Command to run. [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Command, # ArgumentList for Command. [Parameter(Mandatory = $false)] [string] $ArgumentList, # Working directory. Leave empty for default. [Parameter(Mandatory = $false)] [string] $WorkingDirectory, # If true, exit code will be validated (if zero, an error will be thrown). # If false, it will not be validated but returned as a result of the function. [Parameter(Mandatory = $false)] [switch] $CheckLastExitCode = $true, # If true, the cmdlet will return exit code of the invoked command. # If false, the cmdlet will return nothing. [Parameter(Mandatory = $false)] [switch] $ReturnLastExitCode = $true, # If true and any output is present in stderr, an error will be thrown. [Parameter(Mandatory = $false)] [switch] $CheckStdErr = $true, # If not null and given string will be present in stdout, an error will be thrown. [Parameter(Mandatory = $false)] [string] $FailOnStringPresence, # If set, then $Command will be executed under $Credential account. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] $Credential, # Reference parameter that will get STDOUT text. [Parameter(Mandatory = $false)] [ref] $Output, # Reference parameter that will get STDERR text. [Parameter(Mandatory = $false)] [ref] $OutputStdErr, # Timeout to wait for external process to be finished. [Parameter(Mandatory = $false)] [int] $TimeoutInSeconds, # If true, no output from the command will be passed to the console. [Parameter(Mandatory = $false)] [switch] $Quiet = $false, # If true, STDOUT/STDERR will be displayed if error occurs (even if -Quiet is specified). [Parameter(Mandatory = $false)] [switch] $ReportOutputOnError = $true, # Each stdout/stderr line that match this regex will be ignored (not written to console/$output). [Parameter(Mandatory = $false)] [string] $IgnoreOutputRegex ) $commandPath = $Command if (!(Test-Path -LiteralPath $commandPath)) { $exists = $false if (![System.IO.Path]::IsPathRooted($commandPath)) { # check if $Command exist in PATH $exists = $env:PATH.Split(";") | Where-Object { $_ -and (Test-Path (Join-Path -Path $_ -ChildPath $commandPath)) } if (!$exists -and $WorkingDirectory) { $commandPath = Join-Path -Path $WorkingDirectory -ChildPath $commandPath $exists = Test-Path -LiteralPath $commandPath $commandPath = (Resolve-Path -LiteralPath $commandPath).ProviderPath } } if (!$exists) { throw "'$commandPath' cannot be found." } } else { $commandPath = (Resolve-Path -LiteralPath $commandPath).ProviderPath } if (!$Quiet) { $timeoutLog = " (timeout $TimeoutInSeconds s)" Write-Log -Info "Running external process${timeoutLog}: $Command $ArgumentList" } $process = New-Object -TypeName System.Diagnostics.Process $process.StartInfo.CreateNoWindow = $true $process.StartInfo.FileName = $commandPath $process.StartInfo.UseShellExecute = $false $process.StartInfo.RedirectStandardOutput = $true $process.StartInfo.RedirectStandardError = $true $process.StartInfo.RedirectStandardInput = $true if ($WorkingDirectory) { $process.StartInfo.WorkingDirectory = $WorkingDirectory } if ($Credential) { $networkCred = $Credential.GetNetworkCredential() $process.StartInfo.Domain = $networkCred.Domain $process.StartInfo.UserName = $networkCred.UserName $process.StartInfo.Password = $networkCred.SecurePassword } $outputDataSourceIdentifier = "ExternalProcessOutput" $errorDataSourceIdentifier = "ExternalProcessError" Register-ObjectEvent -InputObject $process -EventName OutputDataReceived -SourceIdentifier $outputDataSourceIdentifier Register-ObjectEvent -InputObject $process -EventName ErrorDataReceived -SourceIdentifier $errorDataSourceIdentifier try { $stdOut = '' $stdErr = '' $isStandardError = $false $isStringPresenceError = $false $process.StartInfo.Arguments = $ArgumentList [void]$process.Start() $process.BeginOutputReadLine() $process.BeginErrorReadLine() $getEventLogParams = @{ OutputDataSourceIdentifier = $outputDataSourceIdentifier; ErrorDataSourceIdentifier = $errorDataSourceIdentifier; Quiet = $Quiet; IgnoreOutputRegex = $IgnoreOutputRegex } if ($Output -or ($Quiet -and $ReportOutputOnError)) { $getEventLogParams["Output"] = ([ref]$stdOut) } if ($OutputStdErr -or ($Quiet -and $ReportOutputOnError)) { $getEventLogParams["OutputStdErr"] = ([ref]$stdErr) } if ($FailOnStringPresence) { $getEventLogParams["FailOnStringPresence"] = $FailOnStringPresence } $validateErrorScript = { switch ($_) { 'StandardError' { $isStandardError = $true } 'StringPresenceError' { $isStringPresenceError = $true } Default {} } } $secondsPassed = 0 while (!$process.WaitForExit(1000)) { Write-EventsToLog @getEventLogParams | Where-Object -FilterScript $validateErrorScript if ($TimeoutInSeconds -gt 0 -and $secondsPassed -gt $TimeoutInSeconds) { Write-Log -Info "Killing external process due to timeout $TimeoutInSeconds s." Stop-ProcessForcefully -Process $process -KillTimeoutInSeconds 10 break } $secondsPassed += 1 } Write-EventsToLog @getEventLogParams | Where-Object -FilterScript $validateErrorScript } finally { Unregister-Event -SourceIdentifier ExternalProcessOutput Unregister-Event -SourceIdentifier ExternalProcessError } if ($Output) { [void]($Output.Value = $stdOut) } if ($OutputStdErr) { [void]($OutputStdErr.Value = $stdErr) } $errMsg = '' if ($CheckLastExitCode -and $process.ExitCode -ne 0) { $errMsg = "External command failed with exit code '$($process.ExitCode)'." } elseif ($CheckStdErr -and $isStandardError) { $errMsg = "External command failed - stderr Output present" } elseif ($isStringPresenceError) { $errMsg = "External command failed - stdout contains string '$FailOnStringPresence'" } if ($errMsg) { if ($Quiet -and $ReportOutputOnError) { Write-Log -Error "Command line failed: `"$Command`" $($ArgumentList -join ' ')`r`nSTDOUT: $stdOut`r`nSTDERR: $stdErr" } throw $errMsg } if ($ReturnLastExitCode) { return $process.ExitCode } } |