Ko sem ustvarjal svojo metodo za upravljanje varnostnih kopij na več strežnikih MS-SQL, sem porabil ogromno časa za študij mehanizma prenosa vrednosti v PowerShell pri oddaljenih klicih, zato pišem tole za spomin, mogoče pa bo še komu koristilo.
Torej, začnimo s preprostim skriptom in ga zaženimo lokalno:
$exitcode = $args[0]
Write-Host 'Izpis na gostitelju.'
Write-Output 'Izpis na izhodu.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)Za zagon skriptov bom uporabil naslednjo CMD-datoteko, vsakokrat je ne bom ponovno prenašal:
@Echo OFF
PowerShell .TestOutput1.ps1 1
ECHO ERRORLEVEL=%ERRORLEVEL%Na zaslonu bomo videli naslednje:
Izpis na gostitelju.
Izpis na izhodu.
ExitCode: 1
1
ERRORLEVEL=1
Zdaj zaženimo isti skript preko WSMAN (od daleč):
Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]In tukaj je rezultat:
Izpis na gostitelju.
Izpis na izhodu.
ExitCode: 2
2
ERRORLEVEL=0Odlično, Errorlevel je nekam izginil, vendar potrebujemo vrednost iz skripta! Poskusimo naslednjo konstrukcijo:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]Tukaj je še bolj zanimivo. Celoten izhod v Output je nekam izginil:
Izpis na gostitelju.
ExitCode: 2
ERRORLEVEL=0Zdaj kot lirično odstopanje opazim, da če znotraj funkcije PowerShell zapišete Write-Output ali preprosto izraz brez dodelitve kateri koli spremenljivni (kar implicitno pomeni izhod v kanal Output), na zaslonu ob lokalnem zagonu ne bo nič prikazanega! To je posledica cevovne arhitekture PowerShell — vsaka funkcija ima svoj cevovod Output, za katerega se ustvari niz, in vse, kar vanj pade, velja za rezultat izvajanja funkcije, operator Return doda vrnjeno vrednost kot zadnji element temu cevovodu in prenese nadzor v poklicano funkcijo. Za ponazoritev izvedimo lokalno naslednji skript:
Function Write-Log {
Param( [Parameter(Mandatory=$false, ValueFromPipeline=$true)] [String[]] $OutString = "`r`n" )
Write-Output ("Funkcija: " + $OutString)
Return "ReturnValue"
}
Write-Output ("Glavno: " + "ParameterValue")
$res = Write-Log "ParameterValue"
$res.GetType()
$res.Length
$res | Foreach-Object { Write-Host ("Glavno: " + $_) }
In tukaj je njegov rezultat:
Glavno: ParameterValue
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
2
Glavno: Funkcija: ParameterValue
Glavno: ReturnValueGlavna funkcija (telo skripta) ima prav tako svoj cevovod Output, in če bomo prvi skript zagnali iz CMD in preusmerili izhod v datoteko,
PowerShell .TestOutput1.ps1 1 > TestOutput1.txt
na zaslonu ćemo videti
ERRORLEVEL=1a u datoteci
Izlaz na host.
Izlaz na output.
ExitCode: 1
1
ako napravimo sličan poziv iz PowerShell-a
PS D:sqlagent> .TestOutput1.ps1 1 > TestOutput1.txtna zaslonu će biti
Izlaz na host.
ExitCode: 1a u datoteci
Izlaz na output.
1To se dešava zato što CMD pokreće PowerShell, koji u odsustvu drugih uputstava meša dva toka (Host i Output) i šalje ih CMD-u, koji upisuje sve što je dobio u datoteku, dok su u slučaju pokretanja iz PowerShell-a ova dva toka odvojena, a simbol preusmeravanja utiče samo na Output.
Vraćajući se na glavnu temu, setimo se da objektni model .NET unutar PowerShell-a potpuno postoji u okviru jednog računara (jednog OS-a), a kada se kod pokreće na daljinu preko WSMAN-a, prenos objekata se vrši preko XML-serijalizacije, što unosi mnogo dodatnog interesa u naše istraživanje. Nastavljamo eksperimente pokretanjem sledećeg koda:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$res.GetType()
$host.SetShouldExit($res)
I evo šta imamo na ekranu:
Izlaz na host.
ExitCode: 3
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Ne može se konvertovati argument "exitCode", sa vrednošću: "System.Object[]", za "SetShouldExit" u tip "System.Int32": "Ne može se konvertovati vrednost "System.Object[]" tipa "System.Object[]" u tip "System.Int32"."
D:sqlagentTestOutput3.ps1:3 znak:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+ Kategorija Informacija : Nije određeno: (:) [], MethodException
+ Potpuni Identifikator Greške : MethodArgumentConversionInvalidCastArgument
ERRORLEVEL=0Divan rezultat! To znači da prilikom poziva Invoke-Command ostaje razdvajanje cevovoda na dva toka (Host i Output), što nam daje nadu za uspeh. Pokušaćemo da ostavimo u toku Output samo jednu vrednost, za što ćemo izmeniti najsitniji skript koji pokrećemo na daljinu:
$exitcode = $args[0]
Write-Host 'Izlaz na host.'
#Write-Output 'Izlaz na output.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)
Pokrenućemo ga ovako:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$host.SetShouldExit($res)
i… DA, izgleda da je ovo pobeda!
Izlaz na host.
ExitCode: 4
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
ERRORLEVEL=4Poskusimo ugotoviti, kaj se je zgodilo. Lokalne powershell ukaze smo izvedli, ki je nato poklical powershell na oddaljenem računalniku in izvedel naš skript. Dva toka (Host in Output) sta bila serializirana in vrnjena nazaj, pri čemer je bil tok Output, če je vseboval eno digitalno vrednost, pretvoren v tip Int32 in posredovan sprejemni strani, ki ga je uporabila kot kodo zaključka poklicanega powershell-a.
In kot zadnji korak bomo ustvarili na strežnikom SQL nalogo iz enega koraka tipa 'Operacijski sistem (cmdexec)' z naslednjim besedilom:
PowerShell -NonInteractive -NoProfile "$res=Invoke-Command -ComputerName BACKUPSERVER -ConfigurationName SQLAgent -ScriptBlock {&'D:sqlagentTestOutput1.ps1' 6}; $host.SetShouldExit($res)"URO! Naloga je bila uspešno zaključena, besedilo v dnevniku:
Izvaja se v imenu uporabnika: DOMAINagentuser. Out to host. ExitCode: 6. Koda zaključka procesa 6. Korak se je zaključil z napako.
Zaključki:
- Izogibajte se uporabi Write-Output in navajanju izrazov brez dodelitev. Upoštevajte, da prenos tega kode na drugo mesto v skriptu lahko privede do nepričakovanih rezultatov.
- V skriptih, namenjenih ne ročnemu zagonu, temveč za uporabo v vaših avtomatizacijskih mehanizmih, zlasti za oddaljene klice prek WINRM, izvedite ročno obdelavo napak z uporabo Try/Catch in poskrbite, da ta skript, ne glede na razvoj dogodkov, pošlje v tok Output natančno eno vrednost primitivnega tipa. Če želite pridobiti klasični Errorlevel — ta vrednost mora biti numerična.
Izvor: habr.com
