Handle Unhandled, czyli .NET'owa obsługa krytycznych błędów w kilku krokach
23.03.2012 16:17
W programowaniu, jak w codziennym życiu, pojawiają się sytuacje całkiem nieprzewidziane. Sytuacje, czy też zdarzenia, na które nie mieliśmy gotowych odpowiedzi na etapie projektowania programu. Lecz człowiek to nie maszyna. O ile w prawdziwym świecie na każde nieprzewidziane zdarzenie mamy szansę choćby "jakoś", nawet prowizorycznie, zareagować (nie dotyczy tragicznych zdarzeń losowych skutkujących definitywnym wyjściem z nieskończonej pętli życia), to już prawie każde nieprzewidziane działanie algorytmów komputerowych zwykle kończy się dla użytkownika drastycznie.
Cóż wtedy czynić? Gdyby taka sytuacja miała miejsce na etapie przygotowywania aplikacji, programista z pewnością rozwiązałby problem. Jeśli natomiast błąd w oprogramowaniu uwidacznia się po stronie użytkownika końcowego - tu sprawa wygląda nieciekawie. Użytkownik po prostu coś wymamrocze pod nosem (zapewne przeklnie dzień, w którym zdecydował się na zakup Twojego programu :) i uruchomi aplikację ponownie. Do czasu następnego błędu, czyli powielenia schematu, który przyczynił się do powstania poprzedniego. I tak w kółko. Setki kilometrów dalej, Ty - dumny programista, spijasz kawę, nieświadomy całej sytuacji. Czasem dostaniesz nieprzyjaznego mejla, czasem ktoś do Ciebie zadzwoni i powie, że "to i to nie działa" i spyta "dlaczego". A Ty powiesz:
"Bo się zepsuło"
Wszyscy dobrze wiemy, że nawet w przypadku niewielkich projektów sprawny system raportowania i przekazywania informacji o błędach to podstawa i uzasadniona konieczność. Owszem, użytkownik może przedstawić zaistniałą sytuację dokładnie, opowiedzieć Tobie co, kiedy i ile razy kliknął. Niemniej będą to tylko wskazówki, samemu naprawdę trudno będzie zrekonstruować takie zdarzenie. A i tak większość użytkowników nie piśnie nawet słowem.
Płatne czy darmowe
Znasz to powiedzenie "jeśli coś jest do wszystkiego...". Naprawdę dobre systemy raportowania błędów sporo kosztują, a rozwiązania darmowe trącą w/w "wszystkim", więc nie tracąc więcej czasu powiem otwarcie - mechanizm raportowania błędów napisz sobie sam. Dzięki temu przygotujesz rozwiązanie dedykowane konkretnej, stworzonej przez Ciebie aplikacji. Rozwiązanie niezależne od zewnętrznych bibliotek, takie w sam raz. I nie jest to szczególnie trudne, jeśli poza znakomitą :) wiedzą z zakresu języka C# znasz choćby podstawy PHP.
Obsługa typowych błędów
Mechanizm obsługi wyjątków w środowisku .NET jest fundamentem całej platformy programistycznej oferowanej nam przez Microsoft. Temat ten jest obszernie i szczegółowo opisany zarówno w MSDN, jak również w wielu pozycjach książkowych dostępnych także w naszym rodzimym języku. Pokrótce sprowadza się do prostej zasady, która mówi, że każdy element kodu, który w Twoim programie może być przyczyną powstania błędu na skutek nieprawidłowych danych wejściowych, braku odpowiednich uprawnień do jego wykonania itp. powinieneś umieścić w bloku try/catch:
try { // Instrukcje, których wykonanie może skutkować // pojawieniem się błędu } catch (...Exception ex) // Typ wyjątku - znakomita większość z przyrostkiem Exception { // Obsługa błędu np. przekazanie użytkownikowi // informacji o nieprawidłowych danych wejściowych } finally { // Zwolnienie zasobów itp. }
Czytelników anglojęzycznych odsyłam do przystępnie napisanego artykułu autorstwa Daniela Turini prezentującego najlepsze praktyki radzenia sobie z wyjątkami w .NET (źródło: CodeProject): Exception Handling Best Practices in .NET
Obsługa wystąpienia wyjątku to bardzo kosztowne przedsięwzięcie z punktu widzenia programisty, któremu zależy na wydajności przygotowywanych aplikacji. Nie można zatem traktować mechanizmu obsługi błędów (w postaci przechwytywania wyjątków) jako prostego mechanizmu zarządzającego kolejnością wykonywania instrukcji w programie. Taką rolę powinny spełniać wyłącznie tradycyjne instrukcje przepływu sterowania (np. instrukcje warunkowe, pętle, funkcje itd.). Właściwa inicjalizacja zmiennych czy też analiza danych wejściowych przed wykonaniem kodu w ciele metody to prawidłowe sposoby radzenia sobie z błędami w większości przypadków. Nie zawsze jednak przewidzisz każdą z możliwych do wystąpienia sytuacji. Wtedy też sięgniesz po opisane powyżej mechanizmy obsługi wyjątków. Jeśli i to zawiedzie, staniesz przed poważnym problemem, którego efektem będzie niespodziewane zakończenie pracy oprogramowania. Pozostanie tylko usunąć błąd i przeprosić użytkowników za zaistniałe niedogodności.
Ostatnia deska ratunku
W sytuacjach, w których nie możesz już liczyć na sprawne (czyt. jakiekolwiek) działanie programu należy zarejestrować wystąpienie błędu wraz z możliwie jak największą ilością danych jednoznacznie wskazujących przyczynę jego wystąpienia. Pozwól więc, że w kolejnych krokach przedstawię Tobie prostą ścieżkę do utworzenia skromnego, ale w pełni funkcjonalnego systemu raportowania krytycznych błędów w środowisku .NET (choć zawsze znajdą się odstępstwa od reguły). Nie przedstawię tu jednak konkretnej implementacji - tę pozostawiam Tobie. Mam nadzieję, że dzięki poniższym wskazówkom poradzisz sobie bez problemu (odpowiednie słowa kluczowe umożliwią Tobie sprawne odnalezienie wszelkich niezbędnych informacji w dokumentacji Microsoftu).
[list] 1. Utwórz publiczną, statyczną klasę (np. Logger) zawierającą prywatne składniki klasy System.Diagnostics.TraceSource oraz klasy System.Diagnostics.TextWriterTraceListener i powiąż je między sobą np. w statycznym konstruktorze klasy Logger. Inicjalizacja obiektu klasy TextWriterTraceListener powinna nastąpić w oparciu o ścieżkę do pliku w specjalnych folderze Environment.SpecialFolder.ApplicationData, aby uniknąć przykrości z systemowym UAC w przypadku Visty i wyżej.
2. Ustaw pole System.Diagnostics.Trace.AutoFlush, ponieważ jest ono wykorzystywane przez obiekty klasy TraceSource. Następnie utwórz filtr EventTypeFilter(SourceLevels.Critical) dla obiektu TextWriterTraceListener.
3. Zaimplementuj statyczną metodę LogCritical przez wywołanie na obiekcie klasy TraceSource metody TraceEvent (metoda LogCritical jako argument przyjmować będzie obiekt klasy System.Exception).
4. Uzupełnij metodę LogCritical o obiekt klasy StringBuilder, przechowujący dane dotyczące wyjątku (przekazanego jako parametr wejściowy metody) oraz wszelkie dane dotyczące środowiska, w którym wystąpił nieobsługiwany wyjątek (odpowiednie pola klasy System.Environment oraz instancji klasy System.Diagnostics.Process.GetCurrentProcess()) a następnie zapisz tak utworzony raport błędu przez wywołanie metody TraceEvent na obiekcie klasy TraceSource.
5. Przypisz aplikację do zdarzeń AppDomain.UnhandledException oraz Application.ThreadException. W zdarzeniach tych wykorzystaj metodę LogCritical.
6. Utwórz osobną aplikację Windows Forms (przyjmującą jako parametr wywołania - ścieżkę do pliku na dysku), której zadaniem będzie powiadomienie użytkownika o utworzeniu raportu, wyświetlenie zawartości tegoż raportu wraz z prośbą zarówno o jego uzupełnienie (od strony "okoliczności" pojawienia się błędu) jak i prośbą o wyrażenie zgody na przesłanie pliku do dewelopera, czyli Ciebie.
7. Uzupełnij obsługę zdarzeń AppDomain.UnhandledException oraz Application.ThreadException o kod odpowiedzialny za uruchomienie - tuż po wywołaniu metody LogCritical - wcześniej utworzonej aplikacji raportujących błędy (System.Diagnostic.Process i metoda Start).
8. Utwórz skrypt PHP na stronie domowej projektu rejestrujący treść otrzymywanych zapytań wywołanych metodą POST. Przekaż treść tak przesłanego raportu w formie e‑maila na adres swojej skrzynki pocztowej.
9. Uzupełnij (utworzoną osobno) aplikację Windows Forms raportującą błędy o funkcję wykorzystującą na przykład obiekt klasy System.Net.WebClient i jedną z przeciążonych metod UploadString i prześlij za jej pomocą raport o napotkanym błędzie na serwer. [/list]
Na koniec, pozostaje tylko szybko przygotować aktualizację i powiadomić użytkowników o pojawieniu się poprawionej wersji oprogramowania. Powodzenia!