Prilikom kreiranja vlastite metodologije za upravljanje sigurnosnom kopijom na više MS-SQL servera, proveo sam dosta vremena proučavajući mehanizam za prosljeđivanje vrijednosti u Powershell-u tokom udaljenih poziva, pa pišem sebi podsjetnik u slučaju da bude koristan nekom drugom.
Dakle, počnimo s jednostavnom skriptom i pokrenimo je lokalno:
$exitcode = $args[0]
Write-Host 'Out to host.'
Write-Output 'Out to output.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)
Za pokretanje skripte koristit ću sljedeću CMD datoteku, neću je uključivati svaki put:
@Echo OFF
PowerShell .TestOutput1.ps1 1
ECHO ERRORLEVEL=%ERRORLEVEL%
Na ekranu ćemo vidjeti sljedeće:
Out to host.
Out to output.
ExitCode: 1
1
ERRORLEVEL=1
Sada pokrenimo istu skriptu preko WSMAN-a (daljinski):
Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
A evo i rezultata:
Out to host.
Out to output.
ExitCode: 2
2
ERRORLEVEL=0
Odlično, nivo greške je negdje nestao, ali moramo dobiti vrijednost iz skripte! Pokušajmo sa sljedećim dizajnom:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
Ovo je još zanimljivije. Poruka u Outputu je negdje nestala:
Out to host.
ExitCode: 2
ERRORLEVEL=0
Sada, kao lirsku digresiju, primijetit ću da ako unutar funkcije Powershell napišete Write-Output ili samo izraz bez dodjeljivanja bilo kojoj varijabli (a to implicitno implicira izlaz na izlazni kanal), onda čak i kada se izvodi lokalno, ništa se neće prikazati na ekranu! Ovo je posljedica arhitekture powershell cjevovoda - svaka funkcija ima svoj izlazni cjevovod, za nju se kreira niz, a sve što ulazi u nju smatra se rezultatom izvršavanja funkcije, operator Return dodaje povratnu vrijednost istoj pipeline kao posljednji element i prenosi kontrolu na funkciju koja poziva. Za ilustraciju, pokrenimo sljedeću skriptu lokalno:
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 evo i rezultata:
Main: ParameterValue
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
2
Main: Function: ParameterValue
Main: ReturnValue
Glavna funkcija (tijelo skripte) također ima svoj izlazni cjevovod, i ako pokrenemo prvu skriptu iz CMD-a, preusmjeravajući izlaz u datoteku,
PowerShell .TestOutput1.ps1 1 > TestOutput1.txt
onda ćemo vidjeti na ekranu
ERRORLEVEL=1
i u fajlu
Out to host.
Out to output.
ExitCode: 1
1
ako uputimo sličan poziv iz powershell-a
PS D:sqlagent> .TestOutput1.ps1 1 > TestOutput1.txt
onda će biti na ekranu
Out to host.
ExitCode: 1
i u fajlu
Out to output.
1
To se dešava zato što CMD pokreće powershell, koji, u nedostatku drugih instrukcija, miješa dvije niti (Host i Output) i daje ih CMD-u, koji šalje sve što je primio u datoteku, a u slučaju pokretanja iz powershell-a, ove dvije niti postoje odvojeno, a simbol preusmjeravanja utječe samo na izlaz.
Vraćajući se na glavnu temu, sjetimo se da .NET objektni model unutar powershell-a u potpunosti postoji unutar jednog računala (jedan OS), kada se kod daljinski izvodi preko WSMAN-a, prijenos objekata se odvija kroz XML serijalizaciju, što donosi mnogo dodatnog interesa našem istraživanju. Nastavimo naše eksperimente pokretanjem sljedećeg koda:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$res.GetType()
$host.SetShouldExit($res)
A evo šta imamo na ekranu:
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
Odličan rezultat! To znači da se prilikom pozivanja Invoke-Command-a održava podjela cjevovoda na dvije niti (Host i Output), što nam daje nadu u uspjeh. Pokušajmo ostaviti samo jednu vrijednost u izlaznom toku, za koji ćemo promijeniti prvu skriptu koju pokrećemo na daljinu:
$exitcode = $args[0]
Write-Host 'Out to host.'
#Write-Output 'Out to output.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)
Pokrenimo to ovako:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$host.SetShouldExit($res)
i... DA, izgleda kao pobeda!
Out to host.
ExitCode: 4
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
ERRORLEVEL=4
Hajde da pokušamo da shvatimo šta se dogodilo. Lokalno smo pozvali powershell, koji je zauzvrat pozvao powershell na udaljenom računaru i tamo izvršio našu skriptu. Dva toka (Host i Output) sa udaljene mašine su serijalizovana i prosleđena nazad, dok je izlazni tok, koji ima jednu digitalnu vrednost u sebi, konvertovan u tip Int32 i kao takav prosleđen na stranu koja prima, a strana koja ga je koristila kao izlazni kod powershell-a pozivaoca.
I kao konačnu provjeru, napravimo posao u jednom koraku na SQL serveru sa tipom “Operativni sistem (cmdexec)” sa sljedećim tekstom:
PowerShell -NonInteractive -NoProfile "$res=Invoke-Command -ComputerName BACKUPSERVER -ConfigurationName SQLAgent -ScriptBlock {&'D:sqlagentTestOutput1.ps1' 6}; $host.SetShouldExit($res)"
URA! Zadatak je završen sa greškom, tekst u dnevniku:
Выполняется от имени пользователя: DOMAINagentuser. Out to host. ExitCode: 6. Код завершения процесса 6. Шаг завершился с ошибкой.
Zaključci:
- Izbjegavajte korištenje Write-Outputa i specificiranje izraza bez dodjele. Imajte na umu da premještanje ovog koda na drugo mjesto u skripti može dovesti do neočekivanih rezultata.
- U skriptama koje nisu namijenjene za ručno pokretanje, već za korištenje u vašim mehanizmima automatizacije, posebno za udaljene pozive putem WINRM-a, izvršite ručno rukovanje greškama putem Try/Catch i osigurajte da, u bilo kojem razvoju događaja, ova skripta šalje tačno jednu vrijednost primitivnog tipa . Ako želite da dobijete klasični nivo greške, ova vrednost mora biti numerička.
izvor: www.habr.com