Vraćanje vrijednosti iz powershell invoke-command agentu SQL Server

Kada sam razvijao svoju metodu upravljanja sigurnosnim kopijama na više MS-SQL servera, potrošio sam mnogo vremena proučavajući mehanizme prijenosa vrijednosti u PowerShellu prilikom udaljenih poziva, stoga pišem sebi bilješku, možda će nekome drugome biti od pomoći.

Dakle, započnimo s najjednostavnijim skriptom i pokrenimo ga lokalno:

$exitcode = $args[0]
Write-Host 'Izlaz za domaćina.'
Write-Output 'Izlaz za izlaz.'
Write-Host ('StatusIzlaza: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)

Za pokretanje skripti koristit ću sljedeći CMD-dokument, svaki put ga neću ponovno prikazivati:

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

Na ekranu ćemo vidjeti sljedeće:

Izlaz za domaćina.
Izlaz za izlaz.
StatusIzlaza: 1
1
ERRORLEVEL=1


Sada ćemo pokrenuti istu skriptu preko WSMAN-a (udaljeno):

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

I evo rezultata:

Izlaz za domaćina.
Izlaz za izlaz.
StatusIzlaza: 2
2
ERRORLEVEL=0

Odlično, Errorlevel je negdje nestao, ali potreban nam je povratni vrijednost iz skripte! Pokušajmo sljedeću konstrukciju:

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

Također, ovdje postaje zanimljivo. Cijeli izlaz u Output je negdje nestao:

Izlaz za domaćina.
StatusIzlaza: 2
ERRORLEVEL=0

Sada kao lirski odmak primijetit ću da ako unutar PowerShell funkcije napišete Write-Output ili jednostavno izraz bez dodjeljivanja nekoj varijabli (što neizbježno implicira izlaz u Output kanal), tada čak ni pri lokalnom pokretanju ništa neće biti ispisano na ekran! To je posljedica cijevne arhitekture PowerShella — svaka funkcija ima vlastiti Output kanal, za njega se stvara niz, i sve što u njega ulazi smatra se rezultatom izvršenja funkcije, operator Return dodaje vraćenu vrijednost kao posljednji element u taj isti kanal i predaje kontrolu pozivu funkcije. Za ilustraciju, lokalno ćemo izvršiti sljedeću skriptu:

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

I evo rezultata:

Glavno: VrijednostParametra

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
2
Glavno: Funkcija: VrijednostParametra
Glavno: PovratnaVrijednost

Glavna funkcija (tijelo skripte) također ima svoj Output kanal, i ako pokrenemo prvu skriptu iz CMD, preusmjeravajući izlaz u datoteku,

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

Tako ćemo vidjeti na ekranu

ERRORLEVEL=1

a u datoteci

Izlaz na domaćina.
Izlaz na izlaz.
ExitCode: 1
1

ako napravimo sličan poziv iz powershell-a

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

tako će na ekranu biti

Izlaz na domaćina.
ExitCode: 1

a u datoteci

Izlaz na izlaz.
1

To se događa jer CMD pokreće powershell, koji u odsutnosti drugih uputa miješa dva tokova (Host i Output) i vraća ih CMD-u, koji šalje u datoteku sve što je primio, dok u slučaju pokretanja iz powershell-a ti se tokovi postoje odvojeno, a simbol preusmjeravanja utječe samo na Output.

Vraćajući se na glavnu temu, prisjetimo se da objektni model .NET unutar powershell-a postoji u okviru jednog računala (jedan OS), pri udaljenom pokretanju koda putem WSMAN, prijenos objekata se odvija putem XML serijalizacije, što donosi mnogo dodatnog interesa u naše istraživanje. Nastavit ćemo eksperimente pokretanjem sljedećeg koda:

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

I evo što imamo na ekranu:

Izlaz na domaćina.

ExitCode: 3

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
Ne može se pretvoriti argument "exitCode", sa vrijednošću: "System.Object[]", za "SetShouldExit" u tip "System.Int32": "Ne može se pretvoriti vrijednost "System.Object[]" tipa "System.Object[]" u tip "System
.Int32"."
D:sqlagentTestOutput3.ps1:3 znak:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

ERRORLEVEL=0

Predivan rezultat! To znači da se prilikom poziva Invoke-Command očuvava podjela cjevovoda na dva toka (Host i Output), što nam daje nadu za uspjeh. Pokušajmo ostaviti u toku Output samo jednu vrijednost, u što ćemo promijeniti prvi skript koji pokrećemo udaljeno:

$exitcode = $args[0]
Write-Host 'Izlaz na domaćina.'
#Write-Output 'Izlaz na izlaz.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)

Pokrenut ćemo ga ovako:

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

i… DA, izgleda, to je pobjeda!

Izlaz na domaćina.
ExitCode: 4

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


ERRORLEVEL=4

Pokušajmo shvatiti što se dogodilo. Lokalno smo pozvali PowerShell, koji je zatim pokrenuo PowerShell na udaljenom računalu i tamo izvršio naš skript. Dva toka (Host i Output) iz udaljenog računala su serijalizirana i vraćena, pri čemu je stream Output, ukoliko je sadržavao jednu brojčanu vrijednost, pretvoren u tip Int32 i u tom obliku poslan prijemnoj strani, koja ga je koristila kao kod završetka pozvanog PowerShell-a.

I kao posljednju provjeru kreirat ćemo na serveru SQL zadatak od jednog koraka tipa „Operacijski sustav (cmdexec)“ s tekstom:

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

URA! Zadatak je završio s greškom, tekst u dnevniku:

Izvodi se u ime korisnika: DOMAINagentuser. Izlaz za host. ExitCode: 6. Kod završetka procesa 6. Korak je završio s greškom.

Zaključci:

  • Izbjegavajte korištenje Write-Output i navođenje izraza bez dodjeljivanja. 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 ručnom pokretanju, već za korištenje u vašim mehanizmima automatizacije, osobito za udaljene pozive putem WINRM, osigurajte ručno rukovanje pogreškama putem Try/Catch, i postignite da ovaj skript pošalje u stream Output točno jednu vrijednost jednostavnog tipa. Ako želite dobiti klasičnu Errorlevel — ta vrijednost mora biti brojčana.

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster