0

my powershell script calls a third party console application which uses custom commands. I want powershell to try to run that console applications command but if an error is returned (not by the powershell script but the external console app) which contains a specific string then run another command instead. If not just move onto the next instruction in the script.

What would be the best way of handling that, so basically:

if command1 returns "error1" then run command2. if command 1 does not return error1 skip command2 and move down the script.

1 Answers1

0

You can call and catch errors of native applications in many ways. Some examples:

1. Most easy with no process handling, no distinguishing between success and error.

$nativeAppFilePath = 'ping.exe'

# parameters as collection. Parameter-Value pairs with a space in between must be splitted into two.
$nativeAppParam= @(
    'google.com'
    '-n'
    '5'
)

# using powershell's call operator '&' 
$response = & $nativeAppFilePath $nativeAppParam
$response

2. Easy, same as 1., but distinguishing between success and error possible.

$nativeAppFilePath = 'ping.exe'

 # parameters as collection. Parameter-Value pairs with a space in between must be splitted into two.
$nativeAppParam= @(
    'google2.com'
    '-n'
    '5'
)
 
# using powershell's call operator '&' and redirect the error stream to success stream
$nativeCmdResult = & $nativeAppFilePath $nativeAppParam 2>&1

if ($LASTEXITCODE -eq 0) {
    # success handling
    $nativeCmdResult

} else {
    # error handling
    #   even with redirecting the error stream to the success stream (above)..
    #   $LASTEXITCODE determines what happend if returned as not "0" (depends on application)
    Write-Error -Message "$LASTEXITCODE - $nativeCmdResult"
}

! Now two more complex snippets, which doesn't work with "ping.exe" (but most other applications), because "ping" doesn't raise error events.

3. More complex with process handling, but still process blocking until the application has been finished.

$nativeAppProcessStartInfo = @{
    FileName               = 'ping.exe' # either OS well-known as short name or full path
    Arguments              = @(
        'google.com'
        '-n 5'
    )
    RedirectStandardOutput = $true  # required to catch stdOut stream 
    RedirectStandardError  = $true  # required to catch stdErr stream 
    UseShellExecute        = $false # required to redirect streams
    CreateNoWindow         = $true  # does what is says (background work only)
}

try {
    $nativeApp= [System.Diagnostics.Process]@{
        EnableRaisingEvents = $true
        StartInfo           = $nativeAppProcessStartInfo
    }
    
    [void]$nativeApp.Start()

    # Warning: As soon as the buffer gets full, the application could stuck in a deadlock. Then you require async reading
    #   see: https://stackoverflow.com/a/7608823/13699968
    $stdOut = $nativeApp.StandardOutput.ReadToEnd()
    $stdErr = $nativeApp.StandardError.ReadToEnd()
    # To avoid deadlocks with synchronous read, always read the output stream first and then wait. 
    #   see: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.standardoutput?redirectedfrom=MSDN&view=net-5.0#remarks
    $nativeApp.WaitForExit()

    if ($stdOut.Result) {
        # success handling
        $stdOut.Result
    }

    if ($stdErr.Result) {
        # error handling
        $stdErr.Result
    }

} finally {
    $nativeApp.Dispose()
}

4. The most complex with realtime output & reaction, capturing, and so on...

This time with wmic.exe and a nonsense parameter as example.

$appFilePath  = 'wmic.exe'
$appArguments = @(
    'someNonExistentArgument'
)
$appWorkingDirPath = ''

# handler for events of process
$eventScriptBlock = {
    # $Event is an automatic variable. Only existent in scriptblocks used with Register-ObjectEvent
    # received app output
    $receivedAppData = $Event.SourceEventArgs.Data

    # Write output as stream to console in real-time (without -stream parameter output will produce blank lines!)
    #   (without "Out-String" output with multiple lines at once would be displayed as tab delimited line!)
    Write-Host ($receivedAppData | Out-String -Stream)

    <#
        Insert additional real-time processing steps here.
        Since it is in a different scope, variables changed in this scope will not get changed in parent scope 
        and scope "$script:" will not work as well. (scope "$global:" would work but should be avoided!)
        Modify/Enhance variables "*MessageData" (see below) before registering the event to modify such variables.
    #>

    # add received data to stringbuilder definded in $stdOutEventMessageData and $stdErrEventMessageData
    $Event.MessageData.Data.AppendLine($receivedAppData)
}


# MessageData parameters for events of success stream
$stdOutEventMessageData = @{
    # useful for further usage after application has been exited
    Data = [System.Text.StringBuilder]::new()

    # add additional properties necessary in event handler scriptblock above
}


# MessageData parameters for events of error stream
$stdErrEventMessageData = @{
    # useful for further usage after application has been exited
    Data = [System.Text.StringBuilder]::new()

    # add additional properties necessary in event handler scriptblock above
}


#######################################################
#region Process-Definition, -Start and Event-Subscriptions
#------------------------------------------------------
try {
    $appProcessStartInfo = @{
        FileName               = $appFilePath
        Arguments              = $appArguments
        WorkingDirectory       = $appWorkingDirPath
        RedirectStandardOutput = $true  # required to catch stdOut stream 
        RedirectStandardError  = $true  # required to catch stdErr stream 
        # RedirectStandardInput  = $true # only useful in some circumstances. Didn't find any use yet, but mentioned in: https://stackoverflow.com/questions/8808663/get-live-output-from-process
        UseShellExecute        = $false # required to redirect streams
        CreateNoWindow         = $true  # does what is says (background work only)
    }

    $appProcess = [System.Diagnostics.Process]@{
        EnableRaisingEvents = $true
        StartInfo           = $appProcessStartInfo
    }

    # to obtain available events of an object / type, read the event members of it:     "Get-Member -InputObject $appProcess -MemberType Event"
    $stdOutEvent = Register-ObjectEvent -InputObject $appProcess -Action $eventScriptBlock -EventName 'OutputDataReceived' -MessageData $stdOutEventMessageData
    $stdErrEvent = Register-ObjectEvent -InputObject $appProcess -Action $eventScriptBlock -EventName 'ErrorDataReceived' -MessageData $stdErrEventMessageData

    [void]$appProcess.Start()
    # async reading
    $appProcess.BeginOutputReadLine()
    $appProcess.BeginErrorReadLine()

    while (!$appProcess.HasExited) {
        # Don't use method "WaitForExit()"! This will not show the output in real-time as it blocks the output stream!
        #   using "Sleep" from System.Threading.Thread for short sleep times below 1/1.5 seconds is better than 
        #   "Start-Sleep" in terms of PS overhead/performance (Test it yourself)
        [System.Threading.Thread]::Sleep(250)

        # maybe timeout ...
    }

} finally {
    if (!$appProcess.HasExited) {
        $appProcess.Kill() # WARNING: Entire process gets killed!
    }
    $appProcess.Dispose()

    if ($stdOutEvent -is [System.Management.Automation.PSEventJob]) {
        Unregister-Event -SourceIdentifier $stdOutEvent.Name
    }
    if ($stdErrEvent -is [System.Management.Automation.PSEventJob]) {
        Unregister-Event -SourceIdentifier $stdErrEvent.Name
    }
}
#------------------------------------------------------
#endregion
#######################################################


$stdOutText = $stdOutEventMessageData.Data.ToString() # final output for further usage
$stdErrText  = $stdErrEventMessageData.Data.ToString() # final errors for further usage
swbbl
  • 814
  • 1
  • 4
  • 10
  • Do the above options assume the error is actually from powershell? I think that is the case For me powershell will be running a command I usually run in command line (for a custom 3rd party application). When that command (lets call it command1) runs it will either return a 'success' message or 'fail' string. I want powershell to interpret that returning string and: if 'success' run 'command2' if 'fail' run 'command3' Would i need to read the string into a variable and search that to determine whether to run command2 or command3? – Garyb1976 Jan 18 '21 at 13:58
  • That depends on the 3rd party application. If they are well programmed for command line operations, then they should return exit codes (`$LASTEXITCODE`) apart from their returned message. E.g. Exit code `0` indicates success, everything from `1 upwards` indicates very often an error, but could also mean something special. For exit codes you should use sample 2, for reading the success and error streams use 3 or 4. The error stream differs sometimes from exit codes, like "ping.exe". Ping returns exit code 1 if ping fails, but still writes everything to the success stream. – swbbl Jan 19 '21 at 05:46
  • Best approach for sample 2 would be to combine exit codes with parsing the message returned by the 3rd party application. – swbbl Jan 19 '21 at 05:47