Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Error handling of command prompt commands in Powershell

Tags:

powershell

My goal is to check, disable and remove Scheduled Tasks on numerous Windows servers using Powershell. Some of the servers are Windows 2008R2, so Get-ScheduledTask is out of question. I have to use schtasks

Here is what I have thus far

$servers = (Get-ADComputer -Server DomainController -Filter 'OperatingSystem -like "*Server*"').DNSHostname

$servers |
    ForEach-Object {
        if (Test-Connection -Count 1 -Quiet -ComputerName  $_) {
            Write-Output "$($_) exists, checking for Scheduled Task"
            Invoke-Command -ComputerName $_ {
                    If((schtasks /query /TN 'SOMETASK')) {
                        Write-Output "Processing removal of scheduled task`n"
                        schtasks /change /TN 'SOMETASK' /DISABLE
                        schtasks /delete /TN 'SOMETASK' /F
                    }
                    else {
                        Write-Output "Scheduled Task does not exist`n"
                    }
            }
        }
    }

This works fine for when SOMETASK exists but when it doesn't, Powershell spits an error, like this:

ERROR: The system cannot find the file specified.
    + CategoryInfo          : NotSpecified: (ERROR: The syst...file specified.:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError
    + PSComputerName        : SERVER1

NotSpecified: (:) [], RemoteException
Scheduled Task does not exist

I can circumvent this behavior by setting $ErrorActionPreference to "SilentlyContinue" but this suppresses other errors I may be interested in. I also tried Try, Catch but that still generates the error. I don't think I can add -ErrorHandling argument to an IF statement. Can anyone please lend a helping hand?

Thank you,

like image 939
Norskyi Avatar asked Mar 03 '23 20:03

Norskyi


1 Answers

tl;dr:

Use 2>$null to suppress the stderr output from a call to an external program (such as schtasksk.exe)

  • To work around a bug present up to at least PowerShell [Core] 7.0 (see below), make sure that $ErrorActionPreferece is not set to 'Stop'.
# Execute with stderr silenced.
# Rely on the presence of stdout output in the success case only
# to make the conditional true.
if (schtasks /query /TN 'SOMETASK' 2>$null) { # success, task exists
  "Processing removal of scheduled task`n"
  # ...
}

For background information and more general use cases, read on.


Given how the line from the external program's stderr stream manifests as shown in your question, it sounds like you're running your code in the PowerShell ISE, which I suggest moving away from: The PowerShell ISE is obsolescent and should be avoided going forward (bottom section of the linked answer).

That the ISE surfaces stderr lines surface via PowerShell's error stream by default is especially problematic - see this GitHub issue.

The regular console doesn't do that, fortunately - it passes stderr lines through to the host (console), and prints them normally (not in red), which is the right thing to do, given that you cannot generally assume that all stderr output represents errors (the stream's name notwithstanding).

With well-behaved external programs, you should only ever derive success vs. failure from their process exit code (as reflected in the automatic $LASTEXITCODE variable[1]), not from the presence of stderr output.: exit code 0 indicates success, any nonzero exit code (typically) indicates failure.


As for your specific case:

In the regular console, the value of the $ErrorActionPreference preference variable does not apply to external programs such as schtasks.exe, except in the form of a bug [fixed in PowerShell 7.2+] when you also use a 2> redirection - see GitHub issue #4002; as of PowerShell 7.1.0-preview.6; the corrected behavior is a available as experimental feature PSNotApplyErrorActionToStderr.

Since your schtasks /query /TN 'SOMETASK' command functions as a test, you can do the following:

# Execute with all streams silenced (both stdout and stderr, in this case).
# schtask.exe will indicate the non-existence of the specified task
# with exit code 1
schtasks /query /TN 'SOMETASK' *>$null
 
if ($LASTEXITCODE -eq 0) { # success, task exists
  "Processing removal of scheduled task`n"
  # ...
}

# You can also squeeze it into a single conditional, using
# $(...), the subexpression operator.
if (0 -eq $(schtasks /query /TN 'SOMETASK' *>$null; $LASTEXITCODE)) { # success, task exists
  "Processing removal of scheduled task`n"
  # ...
}

In your specific case, a more concise solution is possible, which relies on your schtasks command (a) producing stdout output in the case of success (if the task exists) and (b) only doings so in the success case:

# Execute with stderr silenced.
# Rely on the presence of stdout output in the success case only
# to make the conditional true.
if (schtasks /query /TN 'SOMETASK' 2>$null) { # success, task exists
  "Processing removal of scheduled task`n"
  # ...
}

If schtasks.exe produces stdout output (which maps to PowerShell's success output stream, 1), PowerShell's implicit to-Boolean conversion will consider the conditional $true (see the bottom section of this answer for an overview of PowerShell's to-Boolean conversion rules).

Note that a conditional only ever acts on the success output stream's output (1), other streams are passed through, such as the stderr output (2) would be in this case (as you've experienced).

2>$null silences stderr output, by redirecting it to the null device.

1 and 2 are the numbers of PowerShell's success output / error streams, respectively; in the case of external programs, they refers to their stdout (standard output) and stderr (standard error) streams, respectively - see about_Redirection.

You can also capture stderr output with a 2> redirection, if you want to report it later (or need to examine it specifically for an ill-behaved program that doesn't use exit codes properly).

  • 2> stderr.txt sends the stderr lines to file sdterr.txt; unfortunately, there is currently no way to capture stderr in a variable - see GitHub issue #4332, which proposes syntax 2>&variableName for that.

    • As implied by the aforementioned bug, you must ensure that $ErrorActionPreference isn't set to 'Stop', because the 2> will then mistakenly trigger a script-terminating error.
  • Aside from the aforementioned bug, using 2> currently has another unexpected side effect [fixed in PowerShell 7.2+]: The stderr lines are unexpectedly also added to the automatic $Error collection, as if they're errors (which they cannot assumed to be).

    • The root cause of both issues is that stderr lines are unexpectedly routed via PowerShell's error stream, even though there is no good reason to do so - see GitHub issue #11133.

[1] Note that the automatic $? variable that indicates success vs. failure as a Boolean ($true / $false) is also set, but not reliably so: since stderr output is currently (v7.0) unexpectedly routed via PowerShell's error stream if redirected with 2>&, the presence of any stderr output invariably sets $? to $false, even if the external program reports overall success, via $LASTEXITCODE reporting 0. Therefore, the only reliable way to test for success is $LASTEXITCODE -eq 0, not $?.

like image 60
mklement0 Avatar answered Mar 11 '23 08:03

mklement0