为什么 -ErrorAction Stop / $ErrorActionPreference = 'Stop' 有时无效,例如使用隐式远程处理/Windows PowerShell 兼容性?

mkl*_*nt0 5 error-handling powershell powershell-remoting powershell-core

这个自我回答的问题是从 PowerShell (Core) v7.3.x 开始编写的,旨在解决以下症状:

有时,使用或似乎都无法有效地通过/语句捕获和处理错误。-ErrorAction -Stop$ErrorActionPreference = 'Stop'trycatch

这是两个例子:

  • Get-NetAdapter$ErrorActionPreference = 'Stop' 从脚本或函数中忽略(从全局范围以外的范围):

    # Run on Windows. Affects both PowerShell editions.
    # The `catch` block isn't triggered, and the (still non-terminating) error prints.
    & { # Simulate running in a child scope.
      $ErrorActionPreference = 'Stop'
      try {     
        Get-NetAdapter nosuch 
      }
      catch {
        "Should get here, but don't."
      }
    }
    
    Run Code Online (Sandbox Code Playgroud)
    • 然而,使用-ErrorAction Stop( Get-NetAdapter nosuch -ErrorAction Stop)确实可以按预期工作。
  • [这仅是PowerShell (Core) v7.4 预览版中的一个错误]Get-AppLockerFileInformation忽略非全局和;的使用。这里仅显示后者:$ErrorActionPreference = 'Stop' ErrorAction Stop

    # Run on Windows. Affects PowerShell (Core) v7+ only
    # The `catch` block isn't triggered, and the (still non-terminating) error prints.
    try {
      Get-AppLockerFileInformation NoSuch.exe -ErrorAction Stop
    }
    catch {
      "Should get here, but don't in PowerShell (Core)."
    }
    
    Run Code Online (Sandbox Code Playgroud)

所以问题是:

  • 什么决定了何时-ErrorAction和/或$ErrorActionPreference无效?

  • 当它们不可用时,有哪些解决方法可用?

mkl*_*nt0 4

两个相关因素决定调用给定命令时是否有效-ErrorAction和/或无效:$ErrorActionPreference

  • 从根本上来说,由于 PowerShell 作用域的工作方式,托管在脚本模块中的命令(即在 PowerShell 中实现的命令,而不是通过编译的 .NET 程序集)不会看到调用者的首选项变量,包括,但从全局$ErrorActionPreference作用域调用时除外,当您从(非点源)脚本或函数调用时,情况并非如此。

    • 这个非常不幸的设计限制是GitHub 问题 #4568的主题。上述问题还提到了该PreferenceVariables模块,脚本模块作者可以使用该模块来解决该限制(有关详细信息,请参阅此博客文章)。

    • 对于调用者,在每次调用的基础上使用(-ErrorAction Stop即相应的公共参数)是受到尊重的,但并不完全等效,因为它也不适用于语句终止错误,因此另外明确要求将后者转换为脚本- 使用/ with或等效语句终止(致​​命) - 请参阅下面的详细信息以及通过(临时)全局设置的替代解决方法。trycatchthrowtrap$ErrorActionPreference

  • 使用隐式远程处理的模块(根据定义也是脚本模块)可能会引入额外的复杂性:

    • 重要提示:以下内容不再适用于 Windows PowerShell 5.1 / PowerShell (Core) 7+ 或可能仅影响选定的模块- 我不知道是哪个;我有旧的注释表明隐式远程处理 Exchange cmdlet 在 Windows PowerShell 5中受到影响;如果您有更多信息,请告诉我们。

    • 对于来自此类模块的命令,even-ErrorAction Stop绝对无效,因为此参数被传播到远程执行命令,并且根据设计,远程发生的终止错误会在本地转换为非终止错误。

      • 我相信这背后的设计原理如下,与一般的 PowerShell远程处理相关:
        • 当针对多个远程机器时,例如viaInvoke-Command的-ComputerName参数,远程发生的终止错误不应该终止整个多机操作,因此这种远程终止错误被转换为本地非终止错误。

        • [一般情况下不再正确]这种逻辑也适用于隐式远程处理的场景- 尽管根据定义,只有一台远程计算机成为目标。

    • 虽然使用隐式远程处理通常需要显式操作(例如使用 cmdlet Import-PSSession),但在一种情况下,默认情况下会自动使用隐式远程处理:

      • 在PowerShell (Core) 7+中,如果您尝试使用仅与 Windows PowerShell 兼容的模块中的命令,则默认情况下会通过Windows PowerShell 兼容性功能自动导入关联的模块,这涉及通过隐藏的powershell.exe子进程进行通信。

      • 虽然这在技术上不是远程处理,但考虑到一切都发生在本地计算机上,底层跨进程通信机制与真正的远程处理相同,顺便说一句,这也会降低类型保真度- 请参阅此答案。

      • 将这种远程发生的、总是非终止的错误转变为可以用/捕获的终止错误的唯一方法是(暂时)将变量的全局化身设置为,如下所示。trycatch$ErrorActionPreference'Stop'


解决方法:

  • 要么:在每次调用的基础上,使用-ErrorAction Stop而不是$ErrorActionPreference = 'Stop', 与try/catch或结合使用以获得完全的鲁棒性trap,以确保(相对罕见的)语句终止错误也转换为脚本终止(致命)错误:

      try {     
        Get-NetAdapter nosuch -ErrorAction Stop
      }
      catch {
        Write-Verbose -Verbose "OK, caught: $_"
        # If desired, re-throw to make script-terminating (fatal)
        throw 
      }
    
      # ------------------------
      # ALTERNATIVE, via `trap`:
    
      trap { 
        Write-Verbose -Verbose "OK, caught: $_"
        # If desired, use `break` to ensure that the error is script-terminating (fatal).
        # Use `continue` to quietly continue.
        break 
      }
    
      Get-NetAdapter nosuch -ErrorAction Stop
    
    Run Code Online (Sandbox Code Playgroud)
    • 笔记:

      • 不幸的是,$ErrorActionPreference = 'Stop'和-ErrorAction Stop并不是平等的:后者仅作用于非终止错误,而前者也作用于语句终止错误 - 请参阅此答案。try但是,在使用/catch和 的上下文中trap,这种区别并不重要,因为语句终止错误本身也会触发catch块/trap脚本块。

      • 如果使用隐式远程处理的模块仍然存在问题,您将需要下一个解决方法。

  • 或者:临时设置,即$global:ErrorActionPreference = 'Stop'全局设置偏好变量。

      & { # Simulate running in a child scope.
        $prevGlobalPref = $global:ErrorActionPreference
        $global:ErrorActionPreference = 'Stop'
        try {     
          Get-AppLockerFileInformation NoSuch.exe
        }
        catch {
          Write-Verbose -Verbose "OK, caught: $_"
          # If desired, re-throw to make script-terminating (fatal)
          throw 
        } finally {
          $global:ErrorActionPreference = $prevGlobalPref
        }
      }
    
    Run Code Online (Sandbox Code Playgroud)

至于如何判断给定的命令是否来自任一模块类型(常规与隐式远程处理):

  • 以下辅助函数Get-CommandModuleType告诉您给定命令来自什么类型的模块,输出指示'Script'常规(非隐式远程)脚本模块和'ImplicitRemoting'隐式远程脚本模块;有关详细信息,请参阅源代码中基于注释的帮助。

  • 示例调用,来自 Windows 上的 PowerShell(核心):

    # -> 'Script'  
    Get-CommandModuleType Get-NetAdapter 
    
    # -> 'ImplicitRemoting'
    Get-CommandModuleType Get-AppLockerFileInformation 
    
    Run Code Online (Sandbox Code Playgroud)
function Get-CommandModuleType {
  <#
  .SYNOPSIS
    Indicates a given command's module type.
  .DESCRIPTION
    Aliases are automatically resolved to their underlying commands.
  
    Note:
     * The output is a *string* that is one of the following:    
       * '(None)', If the command is a function *not from a module*.
       * 'ImplicitRemoting' for an implicitly remoting module.
       * 'Script' for regular script modules (that don't use implicit remoting)
       * 'Binary' if the command is a *binary cmdlet* - even if its part of
         a module that is a *script* module, due to *also* containing 
         PowerShell code. Strictly speaking, binary cmdlets are themselves
         modules.
     * Not all script modules report themselves as such via the underlying
       module's manifest; some of them report just 'Manifest'. Conversely,
       a script module can also contain *binary* cmdlets.
       To see the actual value from the module definition, use the -Raw switch.
    
  .EXAMPLE
    Get-CommandModuleType Get-NetAdapter
  
    Reports the *de facto* type of the Get-NetAdapter command.

  .EXAMPLE
    Get-CommandModuleType Get-NetAdapter -Raw 
  
    Reports the *formal* type of the module that hosts the Get-NetAdapter, as
    specified in its manifest.
  #>
  
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $CommandName,
        [switch] $Raw
    )
  
    $command = Get-Command $CommandName
    if ($command -and $command.ResolvedCommand) { 
        $command = $command.ResolvedCommand
        Write-Verbose "Resolved command is: $command"
    }
    if ($module = $command.Module) {
        Write-Verbose "Source module is: $module; command type is $($command.CommandType)"
        if (-not $Raw -and $command.CommandType -in 'Function', 'Filter') {
            # A function from a script module
            if ($module.PrivateData.ImplicitRemoting) {
                # an implicitly remoting module
                'ImplicitRemoting'
            }
            else {
                'Script'
            }
        }
        elseif (-not $Raw -and $command.CommandType -eq 'Cmdlet') {
            'Binary'
        }
        else {
            if ($Raw) { Write-Verbose 'Reporting raw module type, i.e as manifested.' }
            $module.ModuleType.ToString()
        }
    }
    else {
        '(None)'  
    }
}
Run Code Online (Sandbox Code Playgroud)