Zwracanie wartości z polecenia wywołania powłoki PowerShell do agenta SQL Server

Tworząc własną metodologię zarządzania kopiami zapasowymi na wielu serwerach MS-SQL, spędziłem dużo czasu na studiowaniu mechanizmu przekazywania wartości w Powershell podczas zdalnych połączeń, więc piszę sobie przypomnienie na wypadek, gdyby było to przydatne komuś innemu.

Zacznijmy więc od prostego skryptu i uruchommy go lokalnie:

$exitcode = $args[0]
Write-Host 'Out to host.'
Write-Output 'Out to output.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)

Do uruchomienia skryptów użyję następującego pliku CMD, nie będę go za każdym razem dołączał:

@Echo OFF
PowerShell .TestOutput1.ps1 1
ECHO ERRORLEVEL=%ERRORLEVEL%

Na ekranie zobaczymy:

Out to host.
Out to output.
ExitCode: 1
1
ERRORLEVEL=1


Teraz uruchommy ten sam skrypt poprzez WSMAN (zdalnie):

Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]

A oto wynik dla Ciebie:

Out to host.
Out to output.
ExitCode: 2
2
ERRORLEVEL=0

Świetnie, poziom błędu gdzieś zniknął, ale musimy uzyskać wartość ze skryptu! Spróbujmy następującej konstrukcji:

$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]

To jest jeszcze bardziej interesujące. Wiadomość w Output gdzieś zniknęła:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Teraz, w ramach lirycznej dygresji, zauważę, że jeśli wewnątrz funkcji Powershell napiszesz Write-Output lub po prostu wyrażenie bez przypisania go do żadnej zmiennej (a to domyślnie implikuje wyjście do kanału wyjściowego), to nawet jeśli działasz lokalnie, nic nie będzie wyświetlane na ekranie! Jest to konsekwencja architektury potokowej PowerShell - każda funkcja ma swój potok wyjściowy, tworzona jest dla niej tablica i wszystko, co do niej trafia, jest uznawane za wynik wykonania funkcji, operator Return dodaje do tego samego zwracaną wartość potok jako ostatni element i przekazuje kontrolę do funkcji wywołującej. Aby to zilustrować, uruchommy lokalnie następujący skrypt:

Function Write-Log {
  Param( [Parameter(Mandatory=$false, ValueFromPipeline=$true)] [String[]] $OutString = "`r`n" )
  Write-Output ("Function: "+$OutString)
  Return "ReturnValue"
}
Write-Output ("Main: "+"ParameterValue")
$res = Write-Log "ParameterValue"
$res.GetType()
$res.Length
$res | Foreach-Object { Write-Host ("Main: "+$_) }

A oto wynik:

Main: ParameterValue

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
2
Main: Function: ParameterValue
Main: ReturnValue

Główna funkcja (treść skryptu) również ma swój własny potok wyjściowy i jeśli uruchomimy pierwszy skrypt z CMD, przekierowując wyjście do pliku,

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

wtedy zobaczymy na ekranie

ERRORLEVEL=1

i w pliku

Out to host.
Out to output.
ExitCode: 1
1

jeśli wykonamy podobne wywołanie z PowerShell

PS D:sqlagent> .TestOutput1.ps1 1 > TestOutput1.txt

wtedy będzie na ekranie

Out to host.
ExitCode: 1

i w pliku

Out to output.
1

Dzieje się tak, ponieważ CMD uruchamia PowerShell, który w przypadku braku innych instrukcji miksuje dwa wątki (Host i Output) i przekazuje je CMD, która wysyła wszystko, co otrzyma do pliku, a w przypadku uruchomienia z PowerShell, te dwa wątki istnieją osobno, a przekierowania symboli wpływają tylko na dane wyjściowe.

Wracając do tematu głównego pamiętajmy, że model obiektowy .NET wewnątrz PowerShell w całości istnieje w obrębie jednego komputera (jednego systemu operacyjnego), podczas zdalnego uruchamiania kodu poprzez WSMAN obiekty przesyłane są poprzez serializację XML, co wnosi wiele dodatkowego zainteresowania do naszego badania. Kontynuujmy nasze eksperymenty, uruchamiając następujący kod:

$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$res.GetType()
$host.SetShouldExit($res)

A oto co mamy na ekranie:

Out to host.

ExitCode: 3

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
Не удается преобразовать аргумент "exitCode", со значением: "System.Object[]", для "SetShouldExit" в тип "System.Int32": "Не удается преобразовать значение "System.Object[]" типа "System.Object[]" в тип "System
.Int32"."
D:sqlagentTestOutput3.ps1:3 знак:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

ERRORLEVEL=0

Świetny wynik! Oznacza to, że przy wywołaniu Invoke-Command zostaje zachowany podział potoków na dwa wątki (Host i Output), co daje nam nadzieję na sukces. Spróbujmy pozostawić w strumieniu wyjściowym tylko jedną wartość, dla której zmienimy już pierwszy skrypt, który uruchomimy zdalnie:

$exitcode = $args[0]
Write-Host 'Out to host.'
#Write-Output 'Out to output.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)

Uruchommy to w ten sposób:

$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$host.SetShouldExit($res)

i... TAK, to wygląda na zwycięstwo!

Out to host.
ExitCode: 4

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Int32                                    System.ValueType


ERRORLEVEL=4

Spróbujmy dowiedzieć się, co się stało. Wywołaliśmy lokalnie PowerShell, który z kolei wywołał PowerShell na zdalnym komputerze i tam wykonał nasz skrypt. Dwa strumienie (Host i Output) ze zdalnej maszyny zostały zserializowane i przekazane z powrotem, natomiast strumień Output, zawierający pojedynczą wartość cyfrową, został przekonwertowany na typ Int32 i jako taki przekazany do strony odbierającej, a strona odbierająca go wykorzystała jako kod wyjścia powłoki programu wywołującego.

Na koniec utwórzmy jednoetapowe zadanie na serwerze SQL o typie „System operacyjny (cmdexec)” z następującym tekstem:

PowerShell -NonInteractive -NoProfile "$res=Invoke-Command -ComputerName BACKUPSERVER -ConfigurationName SQLAgent -ScriptBlock {&'D:sqlagentTestOutput1.ps1' 6}; $host.SetShouldExit($res)"

hurra! Zadanie zakończone błędem, w logu tekst:

Выполняется от имени пользователя: DOMAINagentuser. Out to host. ExitCode: 6.  Код завершения процесса 6.  Шаг завершился с ошибкой.

Wnioski:

  • Unikaj używania funkcji Write-Output i określania wyrażeń bez przypisania. Należy pamiętać, że przeniesienie tego kodu w inne miejsce skryptu może dać nieoczekiwane rezultaty.
  • W skryptach przeznaczonych nie do ręcznego uruchamiania, ale do wykorzystania w mechanizmach automatyzacji, szczególnie w przypadku zdalnych wywołań za pośrednictwem WINRM, wykonaj ręczną obsługę błędów za pomocą Try/Catch i upewnij się, że podczas każdego rozwoju zdarzeń ten skrypt wysyła dokładnie jedną wartość typu pierwotnego . Jeśli chcesz uzyskać klasyczny poziom błędu, ta wartość musi być liczbowa.

Źródło: www.habr.com

Dodaj komentarz