Powershell中命令提示符命令的错误处理



我的目标是使用Powershell在众多Windows服务器上检查、禁用和删除"计划任务"。有些服务器是Windows 2008R2,因此获取ScheduledTask是不可能的。我必须使用schtasks

以下是我迄今为止拥有的

$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"
}
}
}
}

SOMETASK存在时,这很好,但当它不存在时,Powershell会抛出一个错误,如下所示:

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

我可以通过将$ErrorActionPreference设置为"SilentlyContinue">来规避这种行为,但这会抑制我可能感兴趣的其他错误。我也尝试过Try,Catch,但仍然会生成错误。我认为我不能将-ErrorHandling参数添加到IF语句中。有人能伸出援手吗?

谢谢你,

tl;dr

使用2>$null抑制对外部程序(如schtasksk.exe)调用的stderr输出

  • 若要解决PowerShell[Core]7.0之前存在的错误(请参阅下文),请确保$ErrorActionPreferece设置为,而不是设置为'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"
# ...
}

有关背景信息和更通用的用例,请继续阅读。


给定外部程序的stderr流中的行如何显示,如您的问题所示,听起来您正在PowerShell ISE中运行代码,我建议您远离:PowerShell ISE已过时,应避免继续运行(链接答案的底部)。

ISE通过PowerShell的错误流默认情况下显示stderr行,这一点特别有问题-请参阅此GitHub问题。

幸运的是,常规控制台没有这样做——它将stderr行传递到主机(控制台),并正常打印(不是红色),这是正确的做法,因为您通常不能假设所有stderr输出都表示错误

对于表现良好的外部程序,您应该只从其进程退出代码(如自动$LASTEXITCODE变量[1]所示)中得出成功与失败,而不是从stderr输出中得出:退出代码0表示成功,任何非零的退出代码(通常)表示失败。


至于您的具体情况:

在常规控制台中,$ErrorActionPreference首选变量的值不会应用于外部程序,如schtasks.exe,除非是在PowerShell 7.2+]中修复的错误,当您也使用2>重定向时-请参阅GitHub问题#4002;截至PowerShell 7.1.0-preview.6;校正后的行为是可用的实验特征CCD_ 10。

由于schtasks /query /TN 'SOMETASK'命令的功能是测试,因此可以执行以下操作:

# 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"
# ...
}

在您的特定情况下,可能会有一个更简洁的解决方案,它依赖于您的schtasks命令(a)在成功的情况下(如果任务存在)生成stdout输出,(b)在成功情况下仅这样做:

# 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"
# ...
}

如果schtasks.exe产生stdout输出(映射到PowerShell的成功输出流1),PowerShell的隐式到布尔转换将考虑条件$true(有关PowerShell到布尔转换规则的概述,请参阅此答案的底部)。

请注意,条件只作用于success输出流的输出(1),其他流则通过,例如在这种情况下的stderr输出(2)(正如您所经历的)。

2>$null通过将stderr输出重定向到null设备,使其静音

12分别是PowerShell的成功输出/错误流的数目;在外部程序的情况下,它们分别引用它们的stdout(标准输出)和stderr(标准错误)流——请参见about_Redirection

如果您以后想要报告(或者需要专门针对未正确使用退出代码的不良程序进行检查),也可以使用2>重定向来捕获stderr输出

  • 2> stderr.txt将stderr行发送到文件sdterr.txt;不幸的是,目前没有办法在变量中捕获stderr——请参阅GitHub问题#4332,它为此提出了语法2>&variableName

    • 正如前面提到的错误所暗示的,您必须确保$ErrorActionPreference没有设置为'Stop',因为2>会错误地触发脚本终止错误
  • 除了上述错误之外,使用2>目前还有另一个意外的副作用[在PowerShell 7.2+]中修复:stderr行也意外地添加到自动$Error集合中,就好像它们是错误一样(不能假设它们是错误)。

    • 这两个问题的根本原因是stderr行意外地通过PowerShell的错误流路由,尽管没有充分的理由这样做-请参阅GitHub问题#111133

[1]注意,作为布尔值($true/$false)指示成功与失败的自动$?变量也被设置为,但并不可靠:由于stderr输出当前(v7.0)是通过PowerShell的错误流意外路由的,如果使用2>&重定向,任何stderr输出来的存在都会将$?设置为$false,即使外部程序通过$LASTEXITCODE报告0报告总体成功。因此,测试成功的唯一可靠方法是$LASTEXITCODE -eq 0,而不是$?

就我个人而言,我更喜欢使用Scheduler ComObject来管理计划任务。你可以用它连接到其他服务器,并简单地搜索它们来管理它们的任务。

$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')
}
}

这看起来像是您已经完成了这项工作的复杂执行。

为什么禁用和删除而只是删除,因为这似乎有点多余?

所有计划的任务都是xml文件和reg条目,如果您不想再执行该任务,可以将其删除。因此,您可以使用sue Get-ChildItem。

# File system:
(Get-ChildItem -Path "$env:windirSystem32Tasks").FullName
# Results
<#
...
C:WindowsSystem32TasksMicrosoft
...
C:WindowsSystem32TasksMicrosoftEdgeUpdateTaskMachineCore
...
#>
# Registry:
Get-ChildItem -Path 'HKLM:SoftwareMicrosoftWindows NTCurrentVersionScheduleTaskcacheTasks'
# Results
<#
Name                           Property                                                                                                                             
----                           --------                                                                                                                             
{01C5B377-A7EB-4FF3-9C6C-86852 Path               : MicrosoftWindowsManagementProvisioningLogon                                                                
...                                                                                                       
#>
Get-ChildItem -Path 'HKLM:SoftwareMicrosoftWindows NTCurrentVersionScheduleTaskcacheTree'
# Results
<#
Name                           Property                                                                                                                             
----                           --------                                                                                                                             
Adobe Acrobat Update Task      SD    : {1...
#>

只需按名称选择任务,然后使用普通的文件系统cmdlet删除文件和regkey。

所以您只想向schtasks隐藏错误消息?一种方法是将标准错误或"2"重定向到$null。这是一个任何人都可以以管理员身份运行的示例。if语句之所以有效,是因为当出现错误时,没有标准输出。当出现标准错误时,invoke命令似乎会生成远程异常,但它不会停止后面的命令。我看不出有什么办法可以抓住它。

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

最新更新