【问题标题】:How do I retain ScriptStackTrace in an exception thrown from within an Invoke-Command on a remote computer?如何在远程计算机上的 Invoke-Command 引发的异常中保留 ScriptStackTrace?
【发布时间】:2016-03-14 08:17:00
【问题描述】:

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

问题在于 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 报告来自远程机器的异常,就好像它是在本地引发的一样,具有相同的堆栈跟踪信息和错误流?

【问题讨论】:

  • 使用Invoke-Command -ErrorVariable remoteerror之类的东西,那么错误流的结果应该在$remoteerror中?
  • 我收回-ErrorVariable 不应该包含您需要的信息。我想我有一个想法,使用throw
  • this article 上的内容有帮助吗? “为了调试,在 Invoke-Command 上,使用 Enter-PSSession 可能会更好,这样你就可以在远程机器上有一个 shell 来尝试一下”
  • 你不能在脚本中使用Enter-PSSession

标签: powershell invoke-command


【解决方案1】:

当您运行某些代码但它失败时,您会收到一个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,其中包含来自远程会话中引发的异常的信息。使用这些,您可以收到“内部”异常。

示例:

#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是因为网络传输过程中的序列化/反序列化,所以如果你要检查异常类型,你需要从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

【讨论】:

  • 太棒了——这为我提供了我需要的异常信息。谢谢!
  • @FrodeF。很棒的答案。我可以获取 PositionMessage,但我似乎无法从序列化异常中获取 StackTrace 或 ScriptStackTrace。有什么想法吗?
  • scriptstacktrace 是错误记录的一部分,而不是异常。 $remoteexception.ScriptStackTrace 对我来说很好。至于异常堆栈跟踪,我在$remoteexception.Exception.SerializedRemoteException.InnerException.StackTrace 中得到与$localexception.Exception.InnerException.StackTrace 相同的输出。外部异常堆栈跟踪在我的测试中不匹配,但这只是因为 Invoke-Command 需要 -ErrorAction Stop 将异常生成为终止。
【解决方案2】:

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

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

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

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

您还可以引发 ErrorRecord 对象或 Microsoft .NET Framework 异常。

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

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";
+         ~~~~~~~~~~~~~~~~~~

【讨论】:

  • 感谢您对此进行更多挖掘 - 看来我必须使用这种方法(堆叠异常消息)结合来自 Frode F. 的答案的序列化信息来创建信息丰富的日志条目.
【解决方案3】:

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

我找到了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}

【讨论】:

  • 他想要访问在Invoke-Command 中使用的脚本块内引发的异常。使用具有相同 cmdlet 的会话与计算机名不会改变将异常传递到“管理计算机”的方式。
  • 碰巧的是,我正在使用的实际脚本确实使用了用 New-PSSession 创建的会话,但由于该部分非常复杂(跨进程断开连接和重新连接很多)并且似乎没有在这种情况下影响异常处理,我在示例代码中省略了它。还是谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-16
相关资源
最近更新 更多