Unhandled Exceptions και Application Domains

Έχουν δημοσιευτεί 08 Φεβρουαρίου 06 11:57 μμ | KelMan 

Όλα ξεκίνησαν μια μέρα που ένας μαθητής μου, έθεσε την παρακάτω ερώτηση:

 

«Έχουμε μια managed εφαρμογή που έχει κάποιο πρόβλημα το οποίο προκαλεί unhandled exception με αποτέλεσμα να σκάει. Μπορούμε να την εκτελέσουμε μέσω μιας άλλης εφαρμογής που θα κατασκευάσουμε, η οποία θα εκτελεί την προβληματική και θα πιάνει το exception με κάποιο Try..Catch ώστε να κάνουμε κατόπιν τις κατάλληλες ενέργειες, πχ να την ξαναξεκινάμε;»

 

Αρχικά θεώρησα ότι είναι κάτι το απλό και θεωρητικά εφικτό. Ήξερα για το UnhandledExceptionEventHandler και είχα φτιάξει κάποια demos για τα Application Domains αλλά δεν είχα παίξει πραγματικά με αυτά. Επιχείρησα λοιπόν να κατασκευάσω το σενάριο αλλά δεν λειτούργησε, οπότε άρχισα να το ψάχνω το θέμα και ανακάλυψα κάποια πράγματα που έχουν μεγάλο ενδιαφέρον και σχετίζονται με τον μηχανισμό των exceptions, τα threads και τα Application Domains.

 

Μάλιστα, άσχετα με το αν σκοπεύετε να φτιάξετε μια εφαρμογή σαν αυτή του μαθητή μου και άσχετα με το αν έχετε σκοπό να χρησιμοποιήσετε Application Domains, το σωστό exception handling έχει μεγάλη σημασία για οποιαδήποτε εφαρμογή που σέβεται τον εαυτό της και σίγουρα τα παρακάτω θα σας φανούν χρήσιμα.

Unhandled Exceptions

Όλοι έχουμε μάθει πλέον τα βασικά περί Exception Handling και κύρια τη δομή Try..Catch. Μερικοί έχουν προχωρήσει και χρησιμοποιούν και το Catch..When για πιο σύνθετες δομές και κάποιοι άλλοι το Exception Management Application Block. Ωστόσο, εδώ αυτό που μας ενδιαφέρει είναι τα Unhandled Exceptions, δηλαδή τα exceptions που μας ξεφεύγουν και έχουν ως αποτέλεσμα να σκάσει η εφαρμογή.

 

Γενικά, όπως όλοι ξέρουμε, ο χειρισμός των exceptions είναι μια υπηρεσία που παρέχει το .NET Framework. Όμως ο χειρισμός αυτός δεν γίνεται πάντοτε με τον ίδιο τρόπο. Το πρώτο πράγμα που ανακάλυψα είναι ότι εξαρτάται από δύο παράγοντες. Ο πρώτος είναι το είδος της εφαρμογής (Console, Windows Forms ή ASP.NET) και ο δεύτερος τo thread στο οποίο εκδηλώνεται το exception. Τα threads στα οποία μπορεί να συμβεί το exception είναι:

·         Το κύριο thread στο οποίο εκτελείται η εφαρμογή

·         Κάποιο thread που έχουμε ξεκινήσει εμείς μέσα από την κλάση System.Threading.Thread

·         Τα pooled threads που δημιουργούνται μέσα από τις κλάσεις System.Threading.ThreadPool και System.Threading.Timer

·         Τα threads που τρέχει ο garbage collector

·         Δεν θέλω να σκέφτομαι την περίπτωση του unmanaged thread

 

To framework επίσης, μας παρέχει έναν μηχανισμό με τον οποίο μπορούμε να κάνουμε το «ύστατο» exception handling. Πρόκειται για ένα delegate που ονομάζεται System.UnhandledExceptionEventHandler το οποίο μπορούμε να συσχετίσουμε με το AppDomain.CurrentDomain.UnhandledException event. Με αυτόν τον τρόπο μπορούμε να ορίσουμε μια ρουτίνα που θα εκτελείται οποτεδήποτε συμβαίνει ένα unhandled exception. Προς το παρόν αυτό μας αρκεί για να κάνουμε μερικές δοκιμές και να βγάλουμε τα πρώτα συμπεράσματα.

Console Applications

Στο solution ConsoleExceptions υλοποιούνται τα τέσσερα διαφορετικά είδη από exceptions με τη βασική δομή να έχει κάπως έτσι:

 

    Sub SimpleEx()

        AddHandler AppDomain.CurrentDomain.UnhandledException, AddressOf SimpleHandler

 

        Dim o As Object

        Console.WriteLine(o.ToString)

    End Sub

 

    Sub SimpleHandler(ByVal Sender As Object, ByVal Args As UnhandledExceptionEventArgs)

        Dim e As Exception = DirectCast(Args.ExceptionObject, Exception)

 

        Console.WriteLine("Caught exception : {0}", e.Message)

    End Sub

 

Με την εντολή AddHandler ορίζουμε ότι η ρουτίνα SimpleHandler θα εκτελείται οποτεδήποτε συμβεί ένα unhandled exception. Εδώ το μόνο που κάνουμε είναι να εμφανίσουμε το μήνυμα λάθους και φυσικά, το exception συμβαίνει στο κύριο thread. Σε αυτήν την περίπτωση, αφού εμφανιστεί το μήνυμα λάθους, η εφαρμογή θα κλείσει και δεν μπορούμε να κάνουμε τίποτα γι αυτό. Απλά, δεν γινόμαστε ρεζίλι, εμφανίζουμε ένα κομψό μήνυμα για τον χρήστη και καταγράφουμε σε κάποιο log όλες τις πληροφορίες που μας ενδιαφέρουν.

 

Τα ενδιαφέροντα αρχίζουν από την στιγμή που το exception συμβεί σε κάποιο άλλο thread (περιπτώσεις 2, 3 και 4). O κώδικας (όπως θα δείτε στο solution) δεν αλλάζει δραματικά, όμως: Όταν το unhandled exception συμβεί σε κάποιο thread πέραν του κύριου thread τότε αυτό το thread θα τερματιστεί, ωστόσο το κύριο θα συνεχίσει να τρέχει κανονικά! Και αν αυτό για κάποιους λύνει προβλήματα, για κάποιους άλλους ενδεχομένως να αποτελεί πρόβλημα, ειδικά όταν χρειάζονται να τερματίσουν ολόκληρο το Application εξαιτίας ενός τέτοιου exception που στο δικό τους business context θεωρείται fatal. Γι αυτήν την περίπτωση μπορείτε να καλέσετε τη μέθοδο Environment.Exit μέσα από τον handler.

 

Τα event args του unhandled exception handler περιέχουν επίσης ένα ενδιαφέρον property. Ονομάζεται IsTerminating, είναι boolean και έχει τιμή false που πρακτικά σημαίνει ότι υπάρχει κάποιο thread που κλείνει αλλά δεν είναι το κύριο. Τέλος, όπως θα παρατηρήσετε στον κώδικα, η σωστή υλοποίηση του handler γίνεται στην sub ExceptHandler. Εκεί γίνεται ο έλεγχος If Not e Is Nothing Then... για τον απλό λόγο του ότι ο handler πιάνει οποιοδήποτε exception, ακόμη και τα μη-CLR, όπερ σημαίνει δεν είναι απαραίτητο ότι υπάρχει πραγματικά το exception object.

 

Μπορείτε να παίξετε με το ConsoleExceptions solution και να δείτε πως συμπεριφέρεται ανά περίπτωση. Απλά μην το τρέξετε μέσα από Studio, ούτε με διπλό-κλικ μέσα από το bin γιατί δεν θα προλάβετε να δείτε τίποτα. Ανοίχτε ένα command line παράθυρο και τρέξτε το από εκεί ενώ παράλληλα μπορείτε να βλέπετε τον κώδικα στο Studio.

WinForms Applications

Ένα αντίστοιχο παράδειγμα βλέπετε στο solution WinFormsExceptions, όμως εδώ έχουμε κάποιες αλλαγές.

 

Αρχικά, ας δούμε τα πράγματα ως ανυποψίαστοι νέοι προγραμματιστές. Ως τέτοιοι λοιπόν, θα μπορούσαμε να γράψουμε τον παρακάτω κώδικα:

 

Module Module1

    Sub main()

        Try

            Application.Run(New Form1)

        Catch ex As Exception

            MsgBox(ex.Message)

        End Try

    End Sub

End Module

 

Είμαστε αρκετά έξυπνοι για να καταλάβουμε ότι μόνο ξεκινώντας την εφαρμογή μέσα από ένα module με sub Main μπορούμε να πιάσουμε ένα unhandled exception που φεύγει από τη φόρμα. Άρα, κάνουμε Application.Run μέσα σε ένα Try…Catch! Το τρέχουμε μέσα από το Studio και παίζει μια χαρά. Τρέχουμε το exe από το bin και παίρνουμε το γνωστό παράθυρο:

 


 

Ο λόγος που συμβαίνει αυτό είναι ότι τα Windows Forms έχουν τον δικό τους μηχανισμό για τα unhandled exceptions και η default συμπεριφορά αλλάζει ανάλογα με το αν κάνετε debugging ή όχι. Γι αυτόν τον λόγο, μπορείτε να πάτε στο application configuration αρχείο και προσθέσετε το παρακάτω:

 

<configuration>

      <system.windows.forms jitDebugging="true" />

</configuration>

 

Μπορεί να φαίνεται περίεργο, πώς θέτοντας τιμή true στο jitDebugging δεν εμφανίζεται το παράθυρο που λέγαμε αλλά περνάει ο έλεγχος στο try…catch block, ωστόσο είναι θέμα ερμηνείας. Όταν του λέμε jitDebugging="true", εννοούμε «μην ενεργοποιήσεις τον internal μηχανισμό γιατί έχω δικό μου debugger» (άσχετα αν δεν τον ενεργοποιούμε). Έτσι λοιπόν, η εφαρμογή θα συμπεριφέρεται το ίδιο, είτε την τρέχετε μέσα από το Studio, είτε τρέχοντας το exe.

 

Ψάχνοντας λίγο παραπάνω τον τρόπο που δουλεύουν τα unhandled exceptions σε Windows Forms, ανακαλύπτουμε ότι υπάρχει διαφορετικός handler (System.Threading.ThreadExceptionEventHandler) για τα unhandled exceptions του κύριου thread ενώ ο System.UnhandledExceptionEventHandler ισχύει για όλα τα υπόλοιπα threads. Έτσι λοιπόν επιστρέφουμε στην λογική του module, με χρήση του Application.Run, με τη διαφορά στο πως κάνουμε το exception handling, όπως φαίνεται παρακάτω:

 

Module Module1

    Sub main()

        AddHandler AppDomain.CurrentDomain.UnhandledException, AddressOf UnhandledExceptionHandler

        AddHandler Application.ThreadException, AddressOf ThreadExceptionHandler

        Application.Run(New Form1)

    End Sub

 

    Sub UnhandledExceptionHandler(ByVal sender As Object, ByVal args As UnhandledExceptionEventArgs)

        Dim e As Exception = DirectCast(args.ExceptionObject, Exception)

        MsgBox("UnhandledException handler caught : " & vbCrLf & e.Message)

    End Sub

 

    Private Sub ThreadExceptionHandler(ByVal sender As Object, ByVal e As Threading.ThreadExceptionEventArgs)

        MsgBox("ThreadException handler caught : " & vbCrLf & e.Exception.Message)

    End Sub

End Module

 

Για την ακρίβεια, θα πρέπει απαραιτήτως να ορίσετε και τους δύο handlers πριν το Application.Run.

 

Σε αυτό το σημείο τίθεται το ερώτημα: Για ποιόν λόγο να προτιμήσουμε την δεύτερη υλοποίηση σε σχέση με την πρώτη;

 

Την απάντηση μπορείτε να την καταλάβετε εύκολα δοκιμάζοντας τον κώδικα στο solution WinFormsExceptions. Η πρώτη υλοποίηση έχει πολύ περιορισμένο range συλλογής exceptions. Μόνο ό,τι συμβαίνει στο Application.Run scope! Δηλαδή σε ένα μεγάλο main block για παράδειγμα, οποιοδήποτε exception θα μπορούσε να ξεφύγει. Επίσης (αυτό είναι το καλύτερο), στα υπόλοιπα threads τα unhandled exceptions τα καταπίνει το σύστημα!

ASP.NET Applications

Εδώ τα πράγματα αλλάζουν κατά πολύ, δεν θέλω όμως να επεκταθώ καθώς δεν έχω expertise σε αυτή την τεχνολογία. Όποιος όμως ενδιαφέρεται και θέλει να το ψάξει, θα πρέπει να ασχοληθεί με τα παρακάτω:

  • Σε επίπεδο σελίδας, το event που μας ενδιαφέρει είναι το System.Web.UI.Page.Error και το Page.ErrorPage property που περιέχει την σελίδα που προσπαθεί να δει ο χρήστης αλλά δεν τα κατάφερε λόγω του unhandled exception
  • Σε επίπεδο application, η κλάση System.Web.HttpApplication παρέχει το event System.Web.HttpApplication.Error. Μέσα από τον handler αυτού του event μπορείτε να χρησιμοποιήσετε την κλάση System.Web.HttpApplication.HttpServerUtility η οποία παρέχει το τελευταίο λάθος μέσα από τη μέθοδο GetLastError. Αν δεν θέλετε να τερματίσει το application, τότε μπορείτε να χρησιμοποιήσετε τη μέθοδο ClearLastError της ίδιας κλάσης.

Application Domains

Τα application domains είναι ένας μηχανισμός στο CLR του .ΝΕΤ Framework. Ουσιαστικά κάθε application domain καθορίζει αυστηρά μία περιοχή μέσα στην οποία τρέχει ένα application, παρέχουν δηλαδή isolation. Τα βασικά χαρακτηριστικά αυτού του isolation των application domains είναι ότι:

  • Ένα application σε ένα domain δεν μπορεί να έχει απευθείας πρόσβαση στα resources των άλλων application domains
  • Ένα σφάλμα σε ένα application domain δεν επηρεάζει τα υπόλοιπα application domains
  • Κάθε application domain μπορεί να σταματήσει ανεξάρτητα από τα υπόλοιπα.

 

Μέχρι σήμερα, στον κόσμο του COM ως isolation μηχανισμό είχαμε τo process ενώ στο .ΝΕΤ κάθε process μπορεί να περιέχει πολλαπλά application domains. Από την άλλη μεριά έχουμε επίσης τα threads, τα οποία δεν έχουν καμία ιδιαίτερη σχέση με τα application domains. Πολλαπλά threads μπορούν να τρέχουν σε ένα application domain, ενώ παράλληλα δεν περιορίζονται στα όρια τους και μπορούν να περνούν από domain σε domain. Ωστόσο πάντοτε, σε κάθε συγκεκριμένη χρονική στιγμή, κάθε thread τρέχει σε ένα συγκεκριμένο application domain.

 

Δεν έχει αξία να γράψω περισσότερα εισαγωγικά περί των application domains. Μπορείτε να βρείτε στο MSDN αρκετές πληροφορίες, ικανές για να καταστρέψετε ένα υπέροχο απόγευμα που δεν έχετε τίποτα καλύτερο να κάνετε, ξεκινώντας ίσως από εδώ. Υπάρχει επίσης ένα πολύ καλό FAQ στο GotDotNet.

 

Αυτό που μας ενδιαφέρει εδώ είναι ότι αποτελούν ένα μηχανισμό μέσω του οποίου θα επιχειρήσουμε να λύσουμε το πρόβλημα που περιέγραψα στην αρχή του άρθρου. Για τον σκοπό της δοκιμής έχουμε δύο απλές εφαρμογές που το μόνο που κάνουν είναι να παράγουν ένα unhandled exception, μία σε Windows Forms και μία console. Ο κώδικας μέσω του οποίου θα καλέσουμε αυτές εφαρμογές βρίσκεται στο solution ShellApplication.

 

Αρχικά επιχειρούμε να δημιουργήσουμε ένα νέο AppDomain με την εντολή

 

Dim someDomain As AppDomain = AppDomain.CreateDomain("someDomain")

 

Και κατόπιν μέσα σε αυτό το νέο AppDomain να τρέξουμε το console application

 

        Try

            someDomain.ExecuteAssembly("..\..\SimpleConsoleApp\bin\SimpleConsoleApp.exe")

        Catch ex As Exception

            MsgBox("Local Try..Catch caught : " & ex.Message)

        End Try

        AppDomain.Unload(someDomain)

 

Πραγματικά, το Try…Catch δουλεύει μια χαρά και πιάνει το exception που ξεφεύγει από το console application! Θα μπορούσαμε επίσης να μην χρησιμοποιήσουμε το Try…Catch και να αφήσουμε τα Application.ThreadException και AppDomain.CurrentDomain.UnhandledException να αναλάβουν δράση.

 

Στη συνέχεια, επιχειρούμε να κάνουμε το ίδιο με το Windows Form application. Εδώ όμως αντιμετωπίζουμε το ίδιο πρόβλημα με το γνωστό παράθυρο και το jitDebugging. Θα πρέπει λοιπόν να καταφέρουμε με το ExecuteAssembly να φορτώσουμε και το application configuration αρχείο που απ’ ό,τι φαίνεται αγνοήθηκε.

 

Η λύση είναι απλή και το μόνο που χρειάζεται είναι να υλοποιήσουμε διαφορετικά το AppDomain

 

        Dim info As AppDomainSetup = New AppDomainSetup

 

        info.ApplicationBase = {some path}

        info.ConfigurationFile = {some path with config file}

 

        Dim someotherDomain As AppDomain = AppDomain.CreateDomain("name", Nothing, info)

 

Και τώρα πλέον δουλεύει κανονικά το exception handling και αν πάλι αφαιρέσουμε το Try..Catch παρατηρούμε ότι ενεργοποιείται το Application.ThreadException. Έτσι λοιπόν με αυτόν τον τρόπο έχουμε λύσει το πρόβλημα μας.

 

Κάτι άλλο που σκέφτηκα κάνοντας τις δοκιμές μου ήταν να δοκιμάσω να προσθέσω άλλον έναν handler όπως ο παρακάτω:

 

AddHandler someDomain.UnhandledException, AddressOf AppDomainHandler

 

Αυτό που συνέβη ήταν να αγνοηθεί πανηγυρικά γιατί όπως θα διαβάσετε και στο MSDN, στο AppDomain.UnhandledException Event, «This event occurs only for the application domain that is created by the system when an application is started. If an application creates additional application domains, specifying a delegate for this event in those applications domains has no effect.»

 

Τέλος, πριν να κλείσω, μερικά ακόμα σχόλια σχετικά με τα AppDomains. Αν επιχειρήσουμε να μην δημιουργήσουμε ένα νέο AppDomain αλλά να φορτώσουμε το προβληματικό application μέσα στο current domain

 

AppDomain.CurrentDomain.ExecuteAssembly("someApp.exe")

 

τότε αυτό μπορεί να δουλέψει μόνο για την περίπτωση του console application γιατί στην περίπτωση του Windows Application, ενεργοποιείται το Application.ThreadException αλλά με το μήνυμα: “It is invalid to start a second message loop on a single thread. Use Application.RunDialog or Form.ShowDialog instead”! Το καταλάβατε το υπονοούμενο;

 

Χμμμ… «On single thread». Οκ, ας επιχειρήσουμε το CurrentDomain.ExecuteAssembly σε νέο thread. Αυτή τη φορά παίρνουμε το γνωστό μας παράθυρο και δεν υπάρχει τρόπος να πιάσουμε το exception. Ωστόσο, μπορούμε να συνδυάσουμε το νέο thread με το AppDomain.CreateDomain("name", Nothing, info) και τότε αυτό που συμβαίνει είναι ότι ενεργοποιείται το AppDomain.CurrentDomain.UnhandledException και όχι το Application.ThreadException.

Επίλογος

Το υλικό που στηρίχτηκα για να βρω τα παραπάνω είναι το “Applied Microsoft .NET Framework ProgrammingJeffrey Richter (MS-Press) και το MSDN. Φυσικά δεν διεκδικώ δάφνες πρωτοτυπίας και σίγουρα όλα αυτά μπορείτε να τα βρείτε κι αλλού, ωστόσο ψάχνωντας στο internet για το συγκεκριμένο θέμα πρόσεξα ότι υπάρχει ελάχιστο υλικό και γι αυτόν το λόγο, με αφορμή την ερώτηση του μαθητή μου, αποφάσισα να φτιάξω αυτό το άρθρο. Όλες οι υποδείξεις και τα σχόλια είναι ευπρόσδεκτα.

 

Ευχαριστώ τον Παναγιώτη Καναβό, τον Μιχάλη Νικήτα και τον Patrick Malone για το τεχνικό review του πρώτου draft.

 

KelMan

Σχόλια:

Χωρίς Σχόλια
Έχει απενεργοποιηθεί η προσθήκη σχολίων από ανώνυμα μέλη

Search

Go

Συνδρομές