Как сохранить ScriptStackTrace в исключении, вызванном 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

Если я пишу исключение в Output:

$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 сообщать об исключении с удаленной машины, как если бы оно было вызвано локально, с той же информацией о трассировке стека и в потоке ошибок?


person FacticiusVir    schedule 04.02.2016    source источник
comment
Используйте что-то вроде Invoke-Command -ErrorVariable remoteerror, тогда результаты потока ошибок должны быть в $remoteerror?   -  person Matt    schedule 04.02.2016
comment
Я беру свои слова обратно -ErrorVariable не должно содержать нужной вам информации. Я думаю, что у меня есть идея с использованием throw   -  person Matt    schedule 08.03.2016
comment
Есть ли что-нибудь в этой статье полезный? Для отладки вместо Invoke-Command может быть лучше использовать Enter-PSSession, чтобы вы могли иметь оболочку на удаленной машине, чтобы попробовать что-то.   -  person user4317867    schedule 12.03.2016
comment
Вы не можете использовать Enter-PSSession в скриптах   -  person Frode F.    schedule 14.03.2016


Ответы (3)


Когда вы запускаете некоторый код, и он завершается с ошибкой, вы получаете 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

Этот тип исключения имеет несколько дополнительных свойств, в том числе SerializedRemoteException и SerializedRemoteInvocationInfo, которые содержат информацию об исключении, сгенерированном в удаленном сеансе. Используя их, вы можете получить «внутреннее» исключение.

  • SerializedRemoteException: получает исходное исключение, созданное удаленным экземпляром Windows PowerShell.
  • SerializedRemoteInvocationInfo: получает информацию о вызове удаленного экземпляра Windows PowerShell.

Образец:

#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
person Frode F.    schedule 12.03.2016
comment
Отлично - это дает мне всю информацию из исключения, которая мне нужна. Спасибо! - person FacticiusVir; 15.03.2016
comment
@ФродеФ. Фантастический ответ. Я могу получить PositionMessage, но не могу получить StackTrace или ScriptStackTrace из сериализованного исключения. Любые идеи? - person Guillaume CR; 29.11.2018
comment
Трассировка стека сценариев является частью записи об ошибке, а не исключением. $remoteexception.ScriptStackTrace отлично работает для меня. Что касается трассировки стека исключений, я получаю тот же вывод в $remoteexception.Exception.SerializedRemoteException.InnerException.StackTrace, что и в $localexception.Exception.InnerException.StackTrace. Трассировка стека внешнего исключения не соответствует моему тесту, но это только потому, что Invoke-Command требовалось -ErrorAction Stop для создания исключения как завершающего. - person Frode F.; 17.12.2018

Я уверен, что кто-то с большим опытом может помочь, но я хотел бы дать вам кое-что, чтобы пожевать в то же время. Похоже, вы хотите использовать 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

Итак, теперь у нас есть правильная завершающая ошибка с информацией как о блоке скрипта, так и о том, где в вызывающем коде генерируется ошибка. Очевидно, вы увидите некоторое повторение, но, возможно, это станет для вас началом.

Если есть другая информация, которая вам нужна конкретно, я бы углубился в объект $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";
+         ~~~~~~~~~~~~~~~~~~
person Matt    schedule 08.03.2016
comment
Спасибо за дополнительные исследования - похоже, мне придется использовать этот подход (сложенные сообщения об исключениях) в сочетании с сериализованной информацией из ответа Фроде Ф., чтобы создать информативную запись в журнале. - person FacticiusVir; 15.03.2016

Надеясь, что кто-то с большим опытом может прокомментировать это, мой комментарий, похоже, остался незамеченным.

Я нашел эту статью, которая предлагает некоторое понимание использования 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}
person user4317867    schedule 14.03.2016
comment
Он хочет получить доступ к исключению, которое генерируется внутри блока сценария, используемого в Invoke-Command. Использование имени сеанса и имени компьютера с одним и тем же командлетом не меняет способ передачи исключений на управляющий компьютер. - person Frode F.; 15.03.2016
comment
Как оказалось, реальный сценарий, который я использую, использует сеансы, созданные с помощью New-PSSession, но поскольку эта часть ужасно сложна (множество отключений и повторных подключений между процессами) и, похоже, в этом случае не влияет на обработку исключений, я опустил его из примера кода. Спасибо, в любом случае! - person FacticiusVir; 15.03.2016