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

时间:2016-02-04 11:27:39

标签: powershell invoke-command

我正在编写一个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

但由于它不在错误流上,因此我的构建工具无法正确选取它。如果我尝试写入错误,我有一个类似的问题,重新抛出异常,堆栈跟踪指向脚本的错误部分。

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

3 个答案:

答案 0 :(得分:5)

当您运行某些代码但失败时,您会收到ErrorRecord,其中会反映执行(本地计算机)的代码。因此,当您使用throw "error"时,您可以访问该代码的invocationinfo和异常。

当您使用Invoke-Command时,您不再执行throw "error",远程计算机正在执行。您(本地计算机)正在执行Invoke-Command ....,这就是您获得的ErrorRecord反映的原因(而不是您想要的真正异常)。这是必须的,因为异常可能来自远程comptuer执行的scriptblock,但它也可能是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,因此,如果您要检查异常类型,则需要从{{{{1}中提取它1}}。

psobject.TypeNames

答案 1 :(得分:3)

我相信有经验的人可以提供帮助,但我想在平均时间给你一些东西来咀嚼。听起来您希望使用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

所以现在我们有一个适当的终止错误,包括相对于scriptblock的信息以及调用代码中生成错误的位置。你会看到一些重复,但也许这将是你的开始。

如果您需要具体提供其他信息,我会使用$exception深入研究Get-Member对象,看看您是否能找到您正在寻找的特定内容。这里有一些值得注意的属性是

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

答案 2 :(得分:0)

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

我发现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}