Megosztás a következőn keresztül:


Minden, amit tudni akart a kivételekről

A hibakezelés csak része az életnek, amikor kódírásról van szó. Gyakran ellenőrizhetjük és érvényesíthetjük a várt viselkedés feltételeit. Amikor a váratlan esemény bekövetkezik, kivételkezelésre váltunk. Egyszerűen kezelheti a mások kódjai által létrehozott kivételeket, vagy létrehozhat saját kivételeket mások számára.

Feljegyzés

A cikk eredeti verziója @KevinMarquette által írt blogon jelent meg. A PowerShell csapata köszönjük Kevinnek, hogy megosztotta velünk ezt a tartalmat. Kérjük, nézze meg a blogját a PowerShellExplained.com.

Alapszintű terminológia

Néhány alapfogalmat meg kell fednünk, mielőtt belevágnánk ebbe.

Kivétel

A kivétel olyan esemény, mint egy olyan esemény, amely akkor jön létre, ha a normál hibakezelés nem tudja kezelni a problémát. A szám nullával való felosztása vagy a memória elfogyása olyan példák, amelyek kivételt okoznak. Előfordulhat, hogy a használt kód szerzője kivételeket hoz létre bizonyos problémák esetén.

Dobás és fogás

Amikor kivétel történik, azt mondjuk, hogy a rendszer kivételt ad ki. A kidobott kivétel kezeléséhez el kell kapnia. Ha a rendszer kivételt ad ki, és nem kap valamit, a szkript nem hajt végre.

A hívásverem

A hívásverem az egymásnak hívott függvények listája. Amikor meghív egy függvényt, a rendszer hozzáadja a veremhez vagy a lista elejéhez. Amikor a függvény kilép vagy visszatér, a függvény el lesz távolítva a veremből.

Kivétel esetén a hívásverem be van jelölve annak érdekében, hogy egy kivételkezelő elkapja azt.

Megszüntetési és nem megszüntetési hibák

A kivétel általában megszüntető hiba. A rendszer egy kidobott kivételt kap, vagy leállítja az aktuális végrehajtást. Alapértelmezés szerint a rendszer nem végződő hibát hoz létre Write-Error , és kivétel nélkül hozzáad egy hibát a kimeneti streamhez.

Rámutatok erre, mert Write-Error és más, nem végződő hibák nem aktiválják a catch.

Kivétel lenyelése

Ekkor kap egy hibát csak azért, hogy elnyomja azt. Ezt körültekintően végezze el, mert ez megnehezítheti a hibaelhárítási problémákat.

Egyszerű parancsszintaxis

Íme egy gyors áttekintés a PowerShellben használt alapvető kivételkezelési szintaxisról.

Vet

A saját kivételesemény létrehozásához kivételt adunk a throw kulcsszóval.

function Start-Something
{
    throw "Bad thing happened"
}

Ez létrehoz egy futásidejű kivételt, amely egy megszüntetési hiba. Egy hívó függvény kezeli catch , vagy kilép a szkriptből egy ilyen üzenettel.

PS> Start-Something

Bad thing happened
At line:1 char:1
+ throw "Bad thing happened"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (Bad thing happened:String) [], RuntimeException
    + FullyQualifiedErrorId : Bad thing happened

Írási hiba – ErrorAction leállítása

Megemlítettem, hogy Write-Error alapértelmezés szerint nem okoz megszüntetési hibát. Ha megadja -ErrorAction Stop, Write-Error egy olyan megszűnési hibát hoz létre, amely egy catch.

Write-Error -Message "Houston, we have a problem." -ErrorAction Stop

Köszönjük Lee Dailey-nek, hogy emlékeztetett az ilyen módszer használatára -ErrorAction Stop .

Parancsmag –ErrorAction Stop

Ha bármilyen speciális függvényt vagy parancsmagot ad meg -ErrorAction Stop , az összes Write-Error utasítást befejező hibákká alakítja, amelyek leállítják a végrehajtást, vagy amelyek egy catchadott parancsmaggal kezelhetők.

Start-Something -ErrorAction Stop

Az ErrorAction paraméterről további információt a about_CommonParameters című témakörben talál. A változóval kapcsolatos további információkért lásd: $ErrorActionPreference about_Preference_Variables.

Kipróbálás/fogás

A Kivételkezelés működése a PowerShellben (és sok más nyelven) az, hogy először try egy kódszakaszt ad meg, és ha hibát jelez, megteheti catch . Íme egy gyors minta.

try
{
    Start-Something
}
catch
{
    Write-Output "Something threw an exception"
    Write-Output $_
}

try
{
    Start-Something -ErrorAction Stop
}
catch
{
    Write-Output "Something threw an exception or used Write-Error"
    Write-Output $_
}

A catch szkript csak akkor fut, ha megszüntetési hiba történt. Ha a try végrehajtás megfelelően történik, akkor átugrik a catch. A blokk kivételadatait a catch változó használatával érheti $_ el.

Kipróbálás/végül

Néha nem kell kezelnie egy hibát, de szükség van valamilyen kód végrehajtására, ha kivétel történik vagy sem. Egy finally szkript pontosan ezt teszi.

Figyelje meg ezt a példát:

$command = [System.Data.SqlClient.SqlCommand]::New(queryString, connection)
$command.Connection.Open()
$command.ExecuteNonQuery()
$command.Connection.Close()

Amikor megnyit vagy csatlakozik egy erőforráshoz, zárja be. Ha a ExecuteNonQuery() rendszer kivételt jelez, a kapcsolat nincs lezárva. Itt ugyanaz a kód található egy try/finally blokkon belül.

$command = [System.Data.SqlClient.SqlCommand]::New(queryString, connection)
try
{
    $command.Connection.Open()
    $command.ExecuteNonQuery()
}
finally
{
    $command.Connection.Close()
}

Ebben a példában a kapcsolat megszakad, ha hiba történik. Akkor is bezárul, ha nincs hiba. A finally szkript minden alkalommal fut.

Mivel nem kapja meg a kivételt, a rendszer továbbra is propagálja a hívásvermet.

Kipróbálás/Fogás/Végül

Teljesen érvényes, hogy együtt és finally együtt használjukcatch. Legtöbbször az egyiket vagy a másikat fogja használni, de olyan forgatókönyveket is találhat, ahol mindkettőt használja.

$PSItem

Most, hogy az alapok kikerültek az útból, egy kicsit mélyebbre áshatunk.

catch A blokkon belül található egy automatikus változó ($PSItemvagy $_) típusErrorRecord, amely tartalmazza a kivétel részleteit. Az alábbiakban rövid áttekintést talál néhány kulcsfontosságú tulajdonságról.

Ezekben a példákban ReadAllText érvénytelen elérési utat használtam a kivétel létrehozásához.

[System.IO.File]::ReadAllText( '\\test\no\filefound.log')

PSItem.ToString()

Ez a legtisztább üzenetet adja a naplózáshoz és az általános kimenethez. ToString() a rendszer automatikusan meghívja, ha $PSItem egy sztringben van elhelyezve.

catch
{
    Write-Output "Ran into an issue: $($PSItem.ToString())"
}

catch
{
    Write-Output "Ran into an issue: $PSItem"
}

$PSItem.InvocationInfo

Ez a tulajdonság további információkat tartalmaz, amelyeket a PowerShell gyűjt a kivételt tartalmazó függvényről vagy szkriptről. Itt található a InvocationInfo létrehozott mintakivétel.

PS> $PSItem.InvocationInfo | Format-List *

MyCommand             : Get-Resource
BoundParameters       : {}
UnboundArguments      : {}
ScriptLineNumber      : 5
OffsetInLine          : 5
ScriptName            : C:\blog\throwerror.ps1
Line                  :     Get-Resource
PositionMessage       : At C:\blog\throwerror.ps1:5 char:5
                        +     Get-Resource
                        +     ~~~~~~~~~~~~
PSScriptRoot          : C:\blog
PSCommandPath         : C:\blog\throwerror.ps1
InvocationName        : Get-Resource

A fontos részletek itt a ScriptNamekód, a Line hívás indításának helyét ScriptLineNumber mutatják.

$PSItem.ScriptStackTrace

Ez a tulajdonság azokat a függvényhívásokat jeleníti meg, amelyek a kivételt létrehozó kódhoz adták.

PS> $PSItem.ScriptStackTrace
at Get-Resource, C:\blog\throwerror.ps1: line 13
at Start-Something, C:\blog\throwerror.ps1: line 5
at <ScriptBlock>, C:\blog\throwerror.ps1: line 18

Csak ugyanabban a szkriptben indítok hívásokat a függvényekre, de ez nyomon követné a hívásokat, ha több szkript is érintett volna.

$PSItem.Exception

Ez a tényleges kivétel, amely ki lett dobva.

$PSItem.Exception.Message

Ez az általános üzenet ismerteti a kivételt, és jó kiindulópont a hibaelhárításhoz. A legtöbb kivétel alapértelmezett üzenettel rendelkezik, de a kivétel eldobásakor egyénire is beállítható.

PS> $PSItem.Exception.Message

Exception calling "ReadAllText" with "1" argument(s): "The network path was not found."

Ez az üzenet akkor is visszaadva, ha a híváshoz $PSItem.ToString() nincs beállítva egy beállítás.ErrorRecord

$PSItem.Exception.InnerException

A kivételek belső kivételeket tartalmazhatnak. Ez gyakran akkor fordul elő, ha a hívott kód kivételt kap, és egy másik kivételt ad ki. Az eredeti kivétel az új kivételen belülre kerül.

PS> $PSItem.Exception.InnerExceptionMessage
The network path was not found.

Ezt később fogom újra áttekinteni, amikor a kivételek újravetéséről beszélek.

$PSItem.Exception.StackTrace

Ez a StackTrace kivétel. Megmutattam a ScriptStackTrace fenti, de ez az egyik a hívások felügyelt kód.

at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean
 useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs,
 String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32
 bufferSize, FileOptions options, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean
 checkHost)
at System.IO.StreamReader..ctor(String path, Encoding encoding, Boolean detectEncodingFromByteOrderMarks,
 Int32 bufferSize, Boolean checkHost)
at System.IO.File.InternalReadAllText(String path, Encoding encoding, Boolean checkHost)
at CallSite.Target(Closure , CallSite , Type , String )

Ezt a veremkövetést csak akkor kapja meg, ha az eseményt felügyelt kódból dobják ki. Egy .NET-keretrendszerfüggvényt hívok meg közvetlenül, hogy ez minden, amit láthatunk ebben a példában. Általában ha egy veremkövetést keres, akkor azt keresi, hogy hol áll le a kód, és hol kezdődnek a rendszerhívások.

Kivételek használata

Több kivétel létezik, mint az alapszintaxis és a kivétel tulajdonságai.

Beírt kivételek elfogása

A kifogott kivételekkel szelektív lehet. A kivételeknek van egy típusa, és megadhatja, hogy milyen típusú kivételt szeretne elkapni.

try
{
    Start-Something -Path $path
}
catch [System.IO.FileNotFoundException]
{
    Write-Output "Could not find $path"
}
catch [System.IO.IOException]
{
        Write-Output "IO error with the file: $path"
}

Az egyes blokkok kivételtípusa mindaddig catch be van jelölve, amíg a kivételnek megfelelőt nem talál. Fontos tisztában lennie azzal, hogy a kivételek öröklődhetnek más kivételektől. A fenti FileNotFoundException példában a következőtől IOExceptionöröklődik: . Tehát ha az IOException első lenne, akkor inkább meghívnák. A rendszer csak egy fogási blokkot hív meg, még akkor is, ha több egyezés van.

Ha lenne egy System.IO.PathTooLongException, a IOException megfelelne, de ha lenne, InsufficientMemoryException akkor semmi sem fogná fel, és propagálja fel a vermet.

Több típus egyidejű elfogása

Több kivételtípust is elkaphat ugyanazzal catch az utasítással.

try
{
    Start-Something -Path $path -ErrorAction Stop
}
catch [System.IO.DirectoryNotFoundException],[System.IO.FileNotFoundException]
{
    Write-Output "The path or file was not found: [$path]"
}
catch [System.IO.IOException]
{
    Write-Output "IO error with the file: [$path]"
}

Köszönjük Redditornak u/Sheppard_Ra , hogy javasolta ezt a kiegészítést.

Beírt kivételek dobálása

Gépelt kivételeket a PowerShellben is megadhat. throw Sztring helyett:

throw "Could not find: $path"

Használjon a következőhöz hasonló kivételgyorsítót:

throw [System.IO.FileNotFoundException] "Could not find: $path"

De ha így tesz, meg kell adnia egy üzenetet.

Létrehozhat egy új kivételpéldányt is, amely ki lesz dobva. Ez az üzenet nem kötelező, mert a rendszer minden beépített kivételhez alapértelmezett üzenetekkel rendelkezik.

throw [System.IO.FileNotFoundException]::new()
throw [System.IO.FileNotFoundException]::new("Could not find path: $path")

Ha nem a PowerShell 5.0-s vagy újabb verzióját használja, a régebbi New-Object megközelítést kell használnia.

throw (New-Object -TypeName System.IO.FileNotFoundException )
throw (New-Object -TypeName System.IO.FileNotFoundException -ArgumentList "Could not find path: $path")

Egy beírt kivétel használatával Ön (vagy mások) az előző szakaszban említett típus alapján is elkaphatja a kivételt.

Írási hiba – Kivétel

Hozzáadhatjuk ezeket a beírt kivételeket, Write-Error és kivételtípusonként továbbra is catch meg tudjuk adni a hibákat. Használja Write-Error az alábbi példákhoz hasonlóan:

# with normal message
Write-Error -Message "Could not find path: $path" -Exception ([System.IO.FileNotFoundException]::new()) -ErrorAction Stop

# With message inside new exception
Write-Error -Exception ([System.IO.FileNotFoundException]::new("Could not find path: $path")) -ErrorAction Stop

# Pre PS 5.0
Write-Error -Exception ([System.IO.FileNotFoundException]"Could not find path: $path") -ErrorAction Stop

Write-Error -Message "Could not find path: $path" -Exception (New-Object -TypeName System.IO.FileNotFoundException) -ErrorAction Stop

Ezután a következő módon foghatjuk el:

catch [System.IO.FileNotFoundException]
{
    Write-Log $PSItem.ToString()
}

A .NET-kivételek nagy listája

Összeállítottam egy mesterlistát a Reddit r/PowerShell közösség segítségével, amely több száz .NET-kivételt tartalmaz, hogy kiegészítse ezt a bejegyzést.

Először is keres, hogy a listát a kivételek, amelyek úgy érzik, hogy lenne egy jó illeszkedik az én helyzetem. Próbálja meg kivételeket használni az alapnévtérben System .

A kivételek objektumok

Ha sok beírt kivételt kezd használni, ne feledje, hogy ezek objektumok. A különböző kivételek különböző konstruktorokkal és tulajdonságokkal rendelkeznek. Ha megnézzük a FileNotFoundException dokumentációját System.IO.FileNotFoundException, láthatjuk, hogy át tudunk adni egy üzenetet és egy fájl elérési útját.

[System.IO.FileNotFoundException]::new("Could not find file", $path)

És van egy FileName tulajdonsága, amely elérhetővé teszi a fájl elérési útját.

catch [System.IO.FileNotFoundException]
{
    Write-Output $PSItem.Exception.FileName
}

Tekintse meg a .NET dokumentációját más konstruktorok és objektumtulajdonságok esetében.

Kivétel ismételt kivetése

Ha a blokkban catchthrow csak ugyanazt a kivételt fogja elvégezni, akkor ne catch tegye. Csak olyan kivételt kell végrehajtania catch , amelyet akkor szeretne kezelni vagy végrehajtani, ha ez történik.

Vannak olyan esetek, amikor egy kivételen műveletet szeretne végrehajtani, de a kivételt újra szeretné dobni, hogy valami az alsóbb rétegben kezelni tudja azt. Írhatunk egy üzenetet, vagy naplózhatjuk a problémát a felfedezés helye közelében, de a problémát tovább kezelhetjük a veremen.

catch
{
    Write-Log $PSItem.ToString()
    throw $PSItem
}

Érdekes módon, hívhatunk throw belülről catch , és ez újra dobja a jelenlegi kivételt.

catch
{
    Write-Log $PSItem.ToString()
    throw
}

A kivételt újra szeretnénk dobni, hogy megőrizzük az eredeti végrehajtási adatokat, például a forrásszkriptet és a sorszámot. Ha ezen a ponton új kivételt adunk ki, az elrejti, hogy hol kezdődött a kivétel.

Új kivétel ismételt eldobása

Ha kivételt észlel, de egy másikat szeretne dobni, akkor az eredeti kivételt az újba kell ágyaznia. Ez lehetővé teszi, hogy valaki a veremen lefelé, mint a $PSItem.Exception.InnerException.

catch
{
    throw [System.MissingFieldException]::new('Could not access field',$PSItem.Exception)
}

$PSCmdlet.ThrowTerminatingError()

Az egyetlen dolog, amit nem szeretek használni throw a nyers kivételek, hogy a hibaüzenet pont az throw utasítás, és azt jelzi, hogy a sor, ahol a probléma van.

Unable to find the specified file.
At line:31 char:9
+         throw [System.IO.FileNotFoundException]::new()
+         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (:) [], FileNotFoundException
    + FullyQualifiedErrorId : Unable to find the specified file.

Miután a hibaüzenet azt mondja, hogy a szkriptem hibás, mert a 31. sorban hívtam throw , az rossz üzenet a szkript felhasználói számára. Nem mond nekik semmi hasznosat.

Dexter Dhami rámutatott, hogy ezt ki tudom javítani ThrowTerminatingError() .

$PSCmdlet.ThrowTerminatingError(
    [System.Management.Automation.ErrorRecord]::new(
        ([System.IO.FileNotFoundException]"Could not find $Path"),
        'My.ID',
        [System.Management.Automation.ErrorCategory]::OpenError,
        $MyObject
    )
)

Ha feltételezzük, hogy ezt ThrowTerminatingError() egy úgynevezett Get-Resourcefüggvényben hívták meg, akkor ez a hiba, amelyet látnánk.

Get-Resource : Could not find C:\Program Files (x86)\Reference
Assemblies\Microsoft\Framework\.NETPortable\v4.6\System.IO.xml
At line:6 char:5
+     Get-Resource -Path $Path
+     ~~~~~~~~~~~~
    + CategoryInfo          : OpenError: (:) [Get-Resource], FileNotFoundException
    + FullyQualifiedErrorId : My.ID,Get-Resource

Látja, hogyan utal a Get-Resource függvényre a probléma forrásaként? Ez valami hasznosat jelez a felhasználónak.

Mert $PSItem ez egy ErrorRecord, mi is használhatja ThrowTerminatingError ezt a módszert, hogy újra dobni.

catch
{
    $PSCmdlet.ThrowTerminatingError($PSItem)
}

Ez megváltoztatja a hiba forrását a parancsmagra, és elrejti a függvény belső elemeit a parancsmag felhasználói elől.

Megpróbálhat megszüntetési hibákat létrehozni

Kirk Munro rámutat, hogy egyes kivételek csak akkor okoznak hibákat, ha egy try/catch blokkon belül hajtják végre. Itt van az a példa, amit adott nekem, hogy létrehoz egy osztás nulla futásidejű kivétel.

function Start-Something { 1/(1-1) }

Ezután hívja meg így, hogy lássa a hibát, és továbbra is ki tudja adni az üzenetet.

&{ Start-Something; Write-Output "We did it. Send Email" }

De azzal, hogy ugyanazt a kódot egy try/catchadott fájlba helyezi, valami más történik.

try
{
    &{ Start-Something; Write-Output "We did it. Send Email" }
}
catch
{
    Write-Output "Notify Admin to fix error and send email"
}

Azt látjuk, hogy a hiba megszűnő hibává válik, és nem az első üzenetet adja ki. Amit nem szeretek ebben az egy, hogy lehet ez a kód egy függvényben, és másképp működik, ha valaki használ egy try/catch.

Én nem futott a problémákat ezzel magam, de ez a sarok esetben, hogy tisztában kell lennie.

$PSCmdlet.ThrowTerminatingError() belül try/catch

Ennek egyik árnyalata $PSCmdlet.ThrowTerminatingError() , hogy megszüntetési hibát hoz létre a parancsmagon belül, de a parancsmag elhagyása után nem végződő hibává válik. Ez a függvény hívójára hárul, hogy eldöntse, hogyan kezelje a hibát. Visszafordíthatják egy megszüntető hibává, ha egy adott fájlon belülről try{...}catch{...}hívják vagy használják-ErrorAction Stop.

Nyilvános függvénysablonok

Az egyik utolsó út volt a beszélgetésem Kirk Munro volt, hogy ő helyez egy try{...}catch{...} körül minden begin, process és end blokk az összes fejlett funkcióit. Ezekben az általános fogási blokkokban egyetlen vonallal $PSCmdlet.ThrowTerminatingError($PSItem) kezeli a függvényeit elhagyó kivételeket.

function Start-Something
{
    [CmdletBinding()]
    param()

    process
    {
        try
        {
            ...
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($PSItem)
        }
    }
}

Mivel minden egy try utasításban van a funkcióiban, minden következetesen működik. Ez tiszta hibákat is ad a végfelhasználónak, amely elrejti a belső kódot a generált hiba elől.

Csapda

A kivételekre koncentráltam try/catch . De van egy régi funkció, amit meg kell említenem, mielőtt ezt becsomagolnánk.

A trap parancsprogramba vagy függvénybe kerül, hogy elkapja az adott hatókörben előforduló összes kivételt. Kivétel esetén a program végrehajtja a trap benne lévő kódot, majd a normál kód folytatódik. Ha több kivétel is előfordul, a csapda újra és újra le lesz hívva.

trap
{
    Write-Log $PSItem.ToString()
}

throw [System.Exception]::new('first')
throw [System.Exception]::new('second')
throw [System.Exception]::new('third')

Én személy szerint soha nem fogadta el ezt a megközelítést, de látom az értéket a rendszergazdai vagy vezérlő szkriptek, hogy naplózza az esetleges és minden kivételt, majd továbbra is végrehajtani.

Záró megjegyzések

A megfelelő kivételkezelés hozzáadása a szkriptekhez nemcsak stabilabbá teszi őket, hanem megkönnyíti a kivételek elhárítását is.

Sok időt töltöttem a beszélgetéssel throw , mert ez egy alapvető fogalom, amikor a kivételkezelésről beszélek. A PowerShell azt is megadta nekünk Write-Error , hogy kezelje az összes olyan helyzetet, ahol ön használná throw. Ezért ne gondolja, hogy ezt elolvasása után kell használnia throw .

Most, hogy időt vettem, hogy ebben a részletben írjak a kivételkezelésről, át fogok váltani a kód hibáinak generálására szolgáló használatára Write-Error -Stop . Én is fogadom Kirk tanácsát, és minden funkcióhoz intézem ThrowTerminatingError a goto kivételkezelőmet.