【问题标题】:Error handling of command prompt commands in PowershellPowershell中命令提示符命令的错误处理
【发布时间】:2020-02-06 22:57:18
【问题描述】:

我的目标是使用 Powershell 在众多 Windows 服务器上检查、禁用和删除计划任务。 有些服务器是 Windows 2008R2,所以 Get-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 语句中。有人可以帮忙吗?

谢谢,

【问题讨论】:

    标签: powershell


    【解决方案1】:

    tl;dr

    使用2>$null 抑制调用外部程序时的标准错误输出(例如schtasksk.exe

    • 要解决至少在 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 是 obsolescentshould be avoided going forward(底部链接的答案)。

    ISE 通过 PowerShell 的错误流默认显示 stderr 线特别成问题 - 请参阅 this GitHub issue

    幸运的是,常规控制台不会这样做 - 它会将 stderr 行传递到主机(控制台),然后正常打印它们(不是红色),这是正确的做法,因为您通常不能假设所有 stderr 输出都代表 错误(尽管有流的名称)。

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


    至于你的具体情况:

    在常规控制台中,$ErrorActionPreference 首选项变量的值适用于外部程序,例如schtasks.exe,除了 错误 当您还使用 2> 重定向时(从 PowerShell [Core] 7.0 开始) - 请参阅 this GitHub issue;从 PowerShell 7.1.0-preview.6 开始;更正后的行为可作为 experimental feature PSNotApplyErrorActionToStderr 使用。

    由于您的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(请参阅this answer 的底部以了解PowerShell 的 to-Boolean 转换规则)。

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

    2>$null silences stderr 输出,将其重定向到空设备。

    12分别是PowerShell的成功输出/错误流的数量;对于外部程序,它们分别引用其 stdout(标准输出)和 stderr(标准错误)流 - 请参阅 about_Redirection

    您也可以捕获带有2> 重定向的stderr 输出,如果您想稍后报告它(或需要专门检查它是否有行为不端的程序没有正确使用退出代码)。

    • 2> stderr.txt 将标准错误行发送到文件sdterr.txt;不幸的是,目前没有办法在 变量 中捕获 stderr - 请参阅 this suggestion on GitHub,它为此建议使用语法 2>&variableName

      • 正如上述错误所暗示的,您必须确保$ErrorActionPreference 设置为'Stop',因为2> 将错误地触发脚本终止错误。李>
    • 除了上述错误之外,当前使用 2>(从 v7.0 开始)还有另一个意想不到的副作用:stderr 行也意外地添加到自动 $Error 集合中,就好像它们是错误一样(他们不能假设是)。

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

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

    【讨论】:

      【解决方案2】:

      我个人更喜欢使用 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')
              }
          }
      

      【讨论】:

      • 非常感谢!我肯定会尝试这个而不是依赖 schtasks.exe。这是否仅限于特定的 Powershell 版本?
      • 据我所知,我已经在 Server 2008R2 及更高版本上使用过它,没有任何问题。
      【解决方案3】:

      这看起来好像您执行这项工作过于复杂。

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

      所有计划任务都只是 xml 文件和 reg 条目,如果您不再需要该任务,可以将其删除。因此,您可以使用 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...
      #>
      

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

      【讨论】:

      • 你是对的,我无缘无故地禁用了任务。我认为原因是,当您使用 GPO 时,有时,取决于操作系统,任务不会删除,除非它被禁用(如在给定时间未运行)。但这是 GPO,很可能与这种特殊情况无关。
      【解决方案4】:

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

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

      【讨论】: