如何在从远程计算机上的 Invoke-Command 中抛出的异常中保留 ScriptStackTrace?

How do I retain ScriptStackTrace in an exception thrown from within an Invoke-Command on a remote computer?

我正在编写一个 Powershell 脚本来执行我的 build/deploy 进程中的一个步骤,它需要 运行 在远程机器上执行一些操作。该脚本相对复杂,因此如果在该远程期间发生错误 activity 我想要一个详细的堆栈跟踪,以了解脚本中发生错误的位置(除了已经生成的日志记录之外)。

出现问题的原因是 Invoke-Command 在从远程计算机中继终止异常时丢失堆栈跟踪信息。如果在本地机器上调用脚本块:

Invoke-Command -ScriptBlock {
    throw "Test Error";
}

返回所需的异常详细信息:

Test Error
At C:\ScriptTest\Test2.ps1:4 char:2
+     throw "Test Error";
+     ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (Test Error:String) [], RuntimeException
    + FullyQualifiedErrorId : Test Error

但是如果运行远程:

Invoke-Command -ComputerName $remoteComputerName -ScriptBlock {
    throw "Test Error";
}

异常堆栈跟踪指向整个 Invoke-Command 块:

Test Error
At C:\ScriptTest\Test2.ps1:3 char:1
+ Invoke-Command -ComputerName $remoteComputerName -ScriptBlock {
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (Test Error:String) [], RuntimeException
    + FullyQualifiedErrorId : Test Error

我可以手动将异常传输回本地机器:

$exception = Invoke-Command -ComputerName $remoteComputerName -ScriptBlock {
    try
    {
        throw "Test Error";
    }
    catch
    {
        return $_;
    }
}

throw $exception;

但是重新抛出它会丢失堆栈跟踪:

Test Error
At C:\ScriptTest\Test2.ps1:14 char:1
+ throw $exception;
+ ~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (Test Error:PSObject) [], RuntimeException
    + FullyQualifiedErrorId : Test Error

如果我将异常写入输出:

$exception = Invoke-Command -ComputerName $remoteComputerName -ScriptBlock {
    try
    {
        throw "Test Error";
    }
    catch
    {
        return $_;
    }
}

Write-Output $exception;

我得到了正确的堆栈跟踪信息:

Test Error
At line:4 char:3
+         throw "Test Error";
+         ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (Test Error:String) [], RuntimeException
    + FullyQualifiedErrorId : Test Error

但由于它不在错误流中,因此我的构建工具无法正确拾取它。如果我尝试 Write-Error,我会遇到类似的问题来重新抛出异常并且堆栈跟踪指向脚本的错误部分。

所以我的问题是 - 我如何让 Powershell 报告来自远程机器的异常,就好像它是在本地引发的一样,具有相同的堆栈跟踪信息和错误流?

我相信有更多经验的人可以提供帮助,但我想同时给你一些可以咀嚼的东西。听起来您想使用 throw 因为您正在寻找终止异常。 Write-Error 确实会写入错误流,但不会终止。不管你的选择是什么,我的建议都是一样的。

将异常捕获到变量中是一个好的开始,因此我会推荐您示例中的这个块:

$exception = Invoke-Command -ComputerName $remoteComputerName -ScriptBlock {
    try
    {
        throw "Test Error";
    }
    catch
    {
        return $_;
    }
}

$exception 在这种情况下应该是 Deserialized.System.Management.Automation.ErrorRecord。您可以将自定义对象发送到 throw...

You can also throw an ErrorRecord object or a Microsoft .NET Framework exception.

但在这种情况下它不起作用,这可能是由于反序列化。有一次我尝试创建自己的错误对象,但一些所需的属性是只读的,所以我跳过了它。

PS M:\Scripts> throw $exception
Test Error
At line:1 char:1
+ throw $return
+ ~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (Test Error:PSObject) [], RuntimeException
    + FullyQualifiedErrorId : Test Error

然而,如您所见,只需写入输出流即可为您提供正确的信息。

PS M:\Scripts> $exception
Test Error
At line:4 char:9
+         throw "Test Error";
+         ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (Test Error:String) [], RuntimeException
    + FullyQualifiedErrorId : Test Error

$exception 是一个包含所有这些信息的属性对象,因此获得所需信息的一种可能方法是从所需属性创建自定义错误字符串。然而,事实证明这是值得的更多工作。一个简单的折衷方案是使用 Out-String 转换有用的输出,以便它可以作为错误返回。

PS M:\Scripts> throw ($return | out-string)
Test Error
At line:4 char:9
+         throw "Test Error";
+         ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (Test Error:String) [], RuntimeException
    + FullyQualifiedErrorId : Test Error

At line:1 char:1
+ throw ($return | out-string)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (Test Error
At ...Test Error

:String) [], RuntimeException
    + FullyQualifiedErrorId : Test Error
At line:4 char:9
+         throw "Test Error";
+         ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (Test Error:String) [], RuntimeException
    + FullyQualifiedErrorId : Test Error

所以现在我们有了一个正确的终止错误,其中包含与脚本块相关的信息以及与调用代码中生成错误的位置相关的信息。您显然会看到一些重复,但也许这对您来说是一个开始。

如果您还需要其他信息,我会用 Get-Member 深入研究 $exception 对象,看看您是否可以找到您正在寻找的特定信息。这里的一些值得注意的属性是

$exception.ErrorCategory_Reason
$exception.PSComputerName
$exception.InvocationInfo.Line
$exception.InvocationInfo.CommandOrigin

最后一个会读这样的东西。

PS M:\Scripts> $exception.InvocationInfo.PositionMessage
At line:4 char:9
+         throw "Test Error";
+         ~~~~~~~~~~~~~~~~~~

当您 运行 一些代码失败时,您会收到一个 ErrorRecord ,它反映了 (本地计算机)执行的代码。因此,当您使用 throw "error" 时,您可以访问该代码的调用信息和异常。

当您使用 Invoke-Command 时,您不再执行 throw "error",而是远程计算机。您(本地计算机)正在执行 Invoke-Command ....,这就是为什么您得到的 ErrorRecord 反映了这一点(而不是您想要的真正异常)。这是必须的方式,因为异常可能来自远程计算机执行的脚本块,但它也可能是 Invoke-Command 本身的异常,因为它无法连接到远程计算机或其他东西相似。

当异常最初是在远程计算机上抛出时,Invoke-Command/PowerShell 在本地计算机上抛出 RemoteException

#Generate errors
try { Invoke-Command -ComputerName localhost -ScriptBlock { throw "error" } }
catch { $remoteexception = $_ }

try { throw "error" }
catch { $localexception = $_ }

#Get exeception-types
$localexception.Exception.GetType().Name
RuntimeException

$remoteexception.Exception.GetType().Name
RemoteException

此异常类型有一些额外的属性,包括 SerializedRemoteExceptionSerializedRemoteInvocationInfo,其中包含远程会话中抛出的异常的信息。使用这些,您可以收到 "internal" 异常。

样本:

#Show command that threw error 
$localexception.InvocationInfo.PositionMessage

At line:4 char:7
+ try { throw "error" }
+       ~~~~~~~~~~~~~

$remoteexception.Exception.SerializedRemoteInvocationInfo.PositionMessage    

At line:1 char:2
+  throw "error"
+  ~~~~~~~~~~~~~

然后您可以编写一个简单的函数来动态提取信息,例如:

function getExceptionInvocationInfo ($ex) {
    if($ex.Exception -is [System.Management.Automation.RemoteException]) {
        $ex.Exception.SerializedRemoteInvocationInfo.PositionMessage
    } else {
        $ex.InvocationInfo.PositionMessage
    }
}

function getException ($ex) {
    if($ex.Exception -is [System.Management.Automation.RemoteException]) {
        $ex.Exception.SerializedRemoteException
    } else {
        $ex.Exception
    }
}

getExceptionInvocationInfo $localexception

At line:4 char:7
+ try { throw "error" }
+       ~~~~~~~~~~~~~

getExceptionInvocationInfo $remoteexception
At line:1 char:2
+  throw "error"
+  ~~~~~~~~~~~~~

请注意,SerializedRemoteExpcetion 显示为 PSObject,因为在网络传输期间 serialization/deserialization,因此如果您要检查异常类型,则需要提取它来自 psobject.TypeNames.

$localexception.Exception.GetType().FullName
System.Management.Automation.ItemNotFoundException

$remoteexception.Exception.SerializedRemoteException.GetType().FullName
System.Management.Automation.PSObject

#Get TypeName from psobject
$remoteexception.Exception.SerializedRemoteException.psobject.TypeNames[0]
Deserialized.System.Management.Automation.ItemNotFoundException

希望有更多经验的人对此发表评论,我的评论似乎被忽略了。

我发现 this article 它提供了一些使用 Enter-PSSession

的见解

或者更好的是,创建一个持久会话

$session = New-PSSession localhost
Invoke-Command $session {ping example.com}
Invoke-Command $session {$LASTEXITCODE}

要使用跟踪进行调试,请记住您需要在远程会话中设置 VerbosePreference、DebugPreference 等

Invoke-Command $session {$VerbosePreference = ‘continue’}
Invoke-Command $session {Write-Verbose hi}