简体   繁体   中英

Error handling of command prompt commands in 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,

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 $? .

Personally I prefer to use the Scheduler ComObject to manage scheduled tasks. You can connect to other servers with it, and search them simply enough to manage their tasks.

$Scheduler = New-Object -ComObject Schedule.Service

$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"
            $Scheduler.Connect($_)
            $RootFolder = $Scheduler.GetFolder("\")
            $TargetTask = $RootFolder.GetTask('SOMETASK')
            # If the task wasn't found continue to the next server
            If(!$TargetTask){
                Write-Output "Scheduled Task does not exist`n"
                Continue
            }
            Write-Output "Processing removal of scheduled task`n"
            $TargetTask.Enabled = $false
            $RootFolder.DeleteTask('SOMETASK')
        }
    }

This appears like you've way over-complicated execution of this effort.

Why disable and remove vs just remove, as that seems a bit redundant?

All scheduled tasks are nothing but xml files and reg entries, that you can just delete if you don't want the task any longer. Thus, you can use sue Get-ChildItem.

# File system:
(Get-ChildItem -Path "$env:windir\System32\Tasks").FullName

# Results
<#
...
C:\Windows\System32\Tasks\Microsoft
...
C:\Windows\System32\Tasks\MicrosoftEdgeUpdateTaskMachineCore
...
#>

# Registry:
Get-ChildItem -Path 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Schedule\Taskcache\Tasks'
# Results
<#
Name                           Property                                                                                                                             
----                           --------                                                                                                                             
{01C5B377-A7EB-4FF3-9C6C-86852 Path               : \Microsoft\Windows\Management\Provisioning\Logon                                                                
...                                                                                                       
#>

Get-ChildItem -Path 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Schedule\Taskcache\Tree'
# Results
<#
Name                           Property                                                                                                                             
----                           --------                                                                                                                             
Adobe Acrobat Update Task      SD    : {1...
#>

Just select your task by name and delete the file and the regkeys using the normal filesystem cmdlets.

So you just want to hide the error message from schtasks? One way is to redirect standard error or "2" to $null. This is an example anyone can run as admin. The if statement only works because there's no output to standard out when there's an error. It looks like invoke-command generates a remote exception when something comes over standard error, but it doesn't stop the commands that follow. I don't see a way to try/catch it.

invoke-command localhost { if (schtasks /query /tn 'foo' 2>$null) {
  'yes' } ; 'hi'}

hi

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM