Jak leczyć nieobsługiwane wyjątki? (Zakończ aplikację a utrzymaj ją przy życiu)

30

Jaka jest najlepsza praktyka, gdy w aplikacji komputerowej występują nieobsługiwane wyjątki?

Myślałem o wyświetleniu użytkownikowi wiadomości, aby mógł skontaktować się z pomocą techniczną. Poleciłbym użytkownikowi ponowne uruchomienie aplikacji, ale nie wymuszanie jej. Podobne do omawianych tutaj: ux.stackexchange.com - Jaki jest najlepszy sposób radzenia sobie z nieoczekiwanymi błędami aplikacji?

Projekt jest aplikacją WPF platformy .NET, więc opisana propozycja może wyglądać następująco (zwróć uwagę, że jest to uproszczony przykład. Prawdopodobnie sensowne byłoby ukrycie szczegółów wyjątku, dopóki użytkownik nie kliknie opcji „Pokaż szczegóły” i zapewni funkcjonalność łatwo zgłosić błąd):

public partial class App : Application
{
    public App()
    {
        DispatcherUnhandledException += OnDispatcherUnhandledException;
    }

    private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
    {
        LogError(e.Exception);
        MessageBoxResult result = MessageBox.Show(
             $"Please help us fix it and contact [email protected]. Exception details: {e.Exception}" +
                        "We recommend to restart the application. " +
                        "Do you want to stop the application now? (Warning: Unsaved data gets lost).", 
            "Unexpected error occured.", MessageBoxButton.YesNo);

        // Setting 'Handled' to 'true' will prevent the application from terminating.
        e.Handled = result == MessageBoxResult.No;
    }

    private void LogError(Exception ex)
    {
        // Log to a log file...
    }
}

We wdrożeniu (Komendy ViewModels lub moduł obsługi zdarzeń zewnętrznych) wychwyciłbym tylko konkretny wyjątek egzogeniczny i pozwoliłbym, aby wszystkie inne wyjątki (bez kości i nieznane wyjątki) wygasały aż do opisanego powyżej „modułu ostatniej instancji”. Definicję wyjątków egzogenicznych i egzogennych można znaleźć w: Eric Lippert - wyjątki Vexing

Czy ma sens pozwolić użytkownikowi zdecydować, czy aplikacja powinna zostać zakończona? Gdy aplikacja zostanie zakończona, na pewno nie będziesz mieć niespójnego stanu ... Z drugiej strony użytkownik może utracić niezapisane dane lub nie jest w stanie zatrzymać żadnego rozpoczętego procesu zewnętrznego, dopóki aplikacja nie zostanie ponownie uruchomiona.

A może to decyzja, czy zakończyć aplikację z nieobsługiwanymi wyjątkami w zależności od rodzaju pisanej aplikacji? Czy to tylko kompromis między „solidnością” a „poprawnością”, jak opisano w Code Complete, wydanie drugie

Aby dać ci kontekst, o jakim rodzaju aplikacji mówimy: Aplikacja służy głównie do kontroli laboratoryjnych instrumentów chemicznych i pokazywania użytkownikowi zmierzonych wyników. W tym celu aplikacje WPF komunikują się z niektórymi usługami (usługami lokalnymi i zdalnymi). Aplikacja WPF nie komunikuje się bezpośrednio z instrumentami.

Jonas Benz
źródło
27
Jeśli nie spodziewałeś się wyjątku, skąd możesz mieć pewność, że aplikacja będzie mogła bezpiecznie kontynuować pracę?
Deduplicator
2
@Deduplicator: Oczywiście nie możesz być pewien. Jak już napisano jako komentarz do odpowiedzi Matthew : „Tak, oczywiście aplikacja może być w nieprawidłowym stanie. Może niektóre ViewModel zostały zaktualizowane tylko częściowo. Ale czy to może zaszkodzić? Użytkownik może ponownie załadować dane i jeśli coś będzie nieważne wysłać do usługi, usługa i tak go nie zaakceptuje. Czy to nie jest lepsze dla użytkownika, jeśli będzie mógł zapisać przed ponownym uruchomieniem aplikacji? ”
Jonas Benz
2
@ Voo Więc masz pewność, że aplikacja będzie mogła bezpiecznie kontynuować pracę, zawsze oczekując wyjątku? Wygląda na to, że zaprzeczasz przesłance otrzymania nieoczekiwanego wyjątku.
Deduplicator
2
W każdym razie prosimy o skopiowanie komunikatu o błędzie. Możesz też powiedzieć, w którym pliku dziennika został zapisany.
ComFreek
2
Obsługa niekoniecznie oznacza wyraźne działanie. Jeśli masz pewność, że aplikacja będzie mogła być bezpiecznie kontynuowana, załatwiłeś wyjątek.
chepner

Odpowiedzi:

47

Musisz się spodziewać, że twój program się zakończy z wielu powodów niż nieobsługiwany wyjątek, taki jak awaria zasilania lub inny proces w tle, który powoduje awarię całego systemu. Dlatego zaleciłbym zakończenie i ponowne uruchomienie aplikacji, ale z pewnymi środkami, aby złagodzić konsekwencje takiego ponownego uruchomienia i zminimalizować możliwą utratę danych .

Zacznij od analizy następujących punktów:

  • Ile danych faktycznie można utracić w przypadku zakończenia programu?

  • Jak poważna jest taka strata dla użytkownika? Czy utracone dane mogą zostać zrekonstruowane w mniej niż 5 minut, czy mówimy o utracie dni pracy?

  • Ile wysiłku wymaga wdrożenie strategii „pośredniego tworzenia kopii zapasowych”? Nie wykluczaj tego, ponieważ „użytkownik musiałby wprowadzić powód zmiany” podczas regularnej operacji składowania, jak napisałeś w komentarzu. Lepiej wymyśl coś w rodzaju pliku tymczasowego lub stanu, który może zostać ponownie załadowany po awarii programu automatycznie. Robi to wiele rodzajów oprogramowania produkcyjnego (na przykład MS Office i LibreOffice mają funkcję „automatycznego zapisywania” i odzyskiwania po awarii).

  • Czy w przypadku, gdy dane były niepoprawne lub uszkodzone, użytkownik może to łatwo dostrzec (może po ponownym uruchomieniu programu)? Jeśli tak, możesz zaoferować opcję zapisania danych przez użytkownika (z niewielką szansą, że są one uszkodzone), a następnie wymuś restart, załaduj ponownie i pozwól użytkownikowi sprawdzić, czy dane wyglądają dobrze. Pamiętaj, aby nie zastępować ostatniej wersji, która była regularnie zapisywana (zamiast tego zapisz w tymczasowej lokalizacji / pliku), aby uniknąć uszkodzenia starej wersji.

To, czy taka strategia „pośredniego tworzenia kopii zapasowych” jest sensowna, zależy ostatecznie od aplikacji i jej architektury oraz od charakteru i struktury danych. Ale jeśli użytkownik straci mniej niż 10 minut pracy, a taka awaria zdarza się raz w tygodniu lub jeszcze rzadziej, prawdopodobnie nie zainwestowałbym w to zbyt wiele.

Doktor Brown
źródło
10
en.wikipedia.org/wiki/Crash-only_software , i tak właśnie działają aplikacje na Androida z konieczności.
Mooing Duck
3
Doskonała odpowiedź - i dobry przykład na rozważanie rzeczy w szerszym kontekście (w tym przypadku „w jaki sposób możemy zapobiec utracie danych w przypadku awarii?”) Prowadzi do lepszego rozwiązania.
śleske
1
Dokonałem niewielkiej zmiany, aby zauważyć, że nie powinieneś nadpisywać starych danych - mam nadzieję, że nie masz nic przeciwko.
śleske
1
@MooingDuck Wiele aplikacji na Androida (takich jak gry) traci stan po awarii.
user253751
1
@immibis: Tak, Android ma naprawdę wiele naprawdę niskiej jakości aplikacji.
Mooing Duck
30

Zależy to w pewnym stopniu od rozwijanej aplikacji, ale ogólnie powiedziałbym, że jeśli aplikacja napotka nieobsługiwany wyjątek, musisz go zakończyć.

Czemu?

Ponieważ nie możesz już mieć zaufania do stanu aplikacji.

Zdecydowanie przekaż użytkownikowi pomocną wiadomość, ale ostatecznie należy zakończyć aplikację.

Biorąc pod uwagę twój kontekst, zdecydowanie chciałbym, aby aplikacja została zakończona. Nie chcesz, aby oprogramowanie działające w laboratorium generowało uszkodzone dane wyjściowe, a ponieważ nie pomyślałeś, aby poradzić sobie z wyjątkiem, nie masz pojęcia, dlaczego został zgłoszony i co się dzieje.

Mateusz
źródło
W ostatniej części próbowałem dodać informacje kontekstowe dotyczące aplikacji.
Jonas Benz
10
@JonasBenz Czy to nie jest lepsze dla użytkownika, jeśli jest w stanie zapisać przed ponownym uruchomieniem aplikacji? Tak, ale skąd wiesz, czy dane, które użytkownik zapisuje, są prawidłowe i nie są uszkodzone? W tym momencie masz nieoczekiwany wyjątek i naprawdę nie wiesz, dlaczego. Najbezpieczniejszą ścieżką, choć irytującą dla użytkownika, jest zakończenie aplikacji. Jeśli obawiasz się pracy oszczędzającej użytkownika, zastosuj strategię ciągłego oszczędzania. Znowu wszystko zależy od pisanej aplikacji.
Matthew
4
Tak, mogę się tutaj kłócić w ten sam sposób: nie zgadzam się z obecnością przycisku Kontynuuj. Problem polega po prostu na tym, że jeśli Ty, twórca aplikacji, nie wiesz, czy możesz kontynuować bezpiecznie, skąd użytkownik może wiedzieć? Jeśli otrzymasz nieobsługiwany wyjątek, oznacza to, że masz błąd, którego się nie spodziewałeś, i nie możesz z całą pewnością powiedzieć, co się dzieje w tym momencie. Rozumiem, że użytkownik będzie chciał kontynuować, ponieważ nie chce stracić pracy, ale czy chcesz pozwolić mu kontynuować, nawet jeśli aplikacja może dawać złe wyniki z powodu tego błędu?
Matthew
3
@Matthew „jeśli Ty, twórca aplikacji, nie wiesz, czy możesz kontynuować bezpiecznie, skąd użytkownik może wiedzieć” , programista nie wiedział, kiedy napisał kod. Gdy użytkownik napotka taki błąd, może być znany. A użytkownik może dowiedzieć się z dowolnego fora użytkownika, kanałów pomocy itp., Lub po prostu testując i widząc, co dzieje się z ich danymi ... Zgadzam się, że nadal jest to zbyt niejasne i niebezpieczne jako funkcja użytkownika, po prostu wskazując, że czas to pozwala możliwe jest, aby użytkownik wiedział, czy „kontynuacja” jest sensowna czy nie.
hyde
1
@JonasBenz, w systemie Windows 3.1, okno dialogowe, które pojawiło się, gdy program wykonał nielegalny dostęp do pamięci, miało przycisk „ignoruj”, który pozwala programowi kontynuować działanie. Zauważysz, że każda kolejna wersja systemu Windows nie ma tego przycisku.
Mark
12

Biorąc pod uwagę, że jest to przeznaczone dla laboratorium chemicznego i że twoja aplikacja nie kontroluje instrumentów bezpośrednio, ale raczej za pośrednictwem innych usług:

Wymuś zakończenie po wyświetleniu komunikatu. Po nieobsługiwanym wyjątku aplikacja jest w nieznanym stanie. Może wysyłać błędne polecenia. Może nawet wywoływać demony nosowe . Błędna komenda może potencjalnie tracić drogich odczynników lub przynieść niebezpieczeństwo dla sprzętu lub ludzi .

Ale możesz zrobić coś innego: z wdziękiem wyzdrowieć po ponownym uruchomieniu . Zakładam, że twoja aplikacja nie wyłącza tych usług w tle, gdy ulega awarii. W takim przypadku możesz łatwo przywrócić im stan. Lub, jeśli masz więcej stanu, rozważ zapisanie go. W magazynie, który ma warunki dotyczące atomowości i integralności danych (może SQLite?).

Edytować:

Jak stwierdzono w komentarzach, proces, który kontrolujesz, może wymagać zmian na tyle szybko, aby użytkownik nie miał czasu na reakcję. W takim przypadku należy rozważyć dyskretne ponowne uruchomienie aplikacji oprócz płynnego odzyskiwania stanu.

Jan Dorniak
źródło
Zakończenie w stanie, który wymaga dalszych poleceń PRAWO TERAZ, może być równie niebezpieczne w laboratorium chemicznym.
Oleg V. Volkov
@ OlegV.Volkov, więc może zrestartuj się po zakończeniu? Na przyzwoitym komputerze uruchomienie GUI powinno zająć setki milisekund. Jeśli proces wymaga trudniejszych czasów, kontrola nie byłaby zaimplementowana w systemie operacyjnym nie działającym w czasie rzeczywistym. Chociaż to OP powinien przeprowadzić ostateczną ocenę ryzyka.
Jan Dorniak,
@ OlegV.Volkov to jednak dobra uwaga, więc w odpowiedzi dodałem swoją opinię.
Jan Dorniak,
8

Próba ogólnej odpowiedzi na to pytanie na najwyższym poziomie programu nie jest mądrą zabawą.

Jeśli coś burzyło się przez całą drogę i w żadnym momencie architektury aplikacji nikt nie rozważał tego przypadku, nie możesz uogólnić, jakie działania są lub nie są bezpieczne do podjęcia.

Tak, nie, zdecydowanie nie jest ogólnie dopuszczalnym projektem pozwalanie użytkownikowi na wybór, czy aplikacja będzie próbowała odzyskać, ponieważ aplikacja i programiści demonstracyjnie nie dołożyli należytej staranności, aby dowiedzieć się, czy jest to możliwe, czy nawet mądre .

Jeśli jednak aplikacja ma wartościowe części swojej logiki lub zachowania, które zostały opracowane z myślą o tym rodzaju odzyskiwania po awarii i możliwe jest wykorzystanie ich w tym przypadku, to zrób to za wszelką cenę - w takim przypadku , może być dopuszczalne poproszenie użytkownika o sprawdzenie, czy chce podjąć próbę odzyskania danych, czy też chciałby po prostu wywołać zamknięcie i zacząć od nowa.

Ten rodzaj odzyskiwania nie jest generalnie konieczny ani zalecany dla wszystkich (a nawet większości) programów, ale jeśli pracujesz nad programem, dla którego wymagany jest taki stopień integralności operacyjnej, może to być okoliczność, w której przedstawienie tego rodzaju monit dla użytkownika byłby rozsądnym posunięciem.

Poza specjalną logiką odzyskiwania po awarii - Nie, nie rób tego. Dosłownie nie masz pojęcia, co się stanie, gdybyś to zrobił, złapałbyś wyjątek i zajął się nim.

Żelazny Gremlin
źródło
Niestety, wiele metod takich jak „Zbuduj obiekt z danymi otrzymanymi z określonej lokalizacji” nie czyni rozróżnienia między wyjątkami wskazującymi, że akcja nie mogła zostać zakończona, ale próba nie miała skutków ubocznych, w porównaniu z tymi, które wskazują, że coś poważniejszego poszło nie tak. Fakt, że próba załadowania zasobu nie powiodła się z jakiegoś powodu, którego się nie spodziewano, nie powinien wymusić krytycznego błędu, jeśli ogólnie jest się przygotowanym na niemożność skonstruowania obiektu. Liczy się efekt uboczny, który niestety jest czymś wyjątkowym, który zignorują ramy.
supercat
@ superupat - Jeśli możesz zidentyfikować błąd, możesz go obsłużyć. Jeśli nie możesz go zidentyfikować, nie możesz sobie z tym poradzić, chyba że napiszesz procedurę sprawdzania integralności stanu aplikacji, aby spróbować wykryć, co mogło pójść nie tak. Nie ma znaczenia, jaki mógł być błąd, „wyraźnie stwierdziliśmy, że nie wiemy tego, ponieważ staramy się ogólnie radzić sobie z nie wyłapanymi wyjątkami.
Żelazny Gremlin,
3

Problem z „wyjątkowymi wyjątkami”, tj. Wyjątkami, których nie przewidziałeś, polega na tym, że nie wiesz, w jakim stanie jest program. Na przykład próba zapisania danych użytkownika może w rzeczywistości zniszczyć jeszcze więcej danych .

Z tego powodu powinieneś zakończyć aplikację.

Istnieje bardzo interesujący pomysł, nazwany przez George Candea i Armando Foxa Crash-only Software . Chodzi o to, że jeśli zaprojektujesz swoje oprogramowanie w taki sposób, że jedynym sposobem na jego zamknięcie jest jego awaria, a jedynym sposobem na jego uruchomienie jest odzyskanie po awarii, wtedy twoje oprogramowanie będzie bardziej odporne, a odzyskiwanie po błędzie ścieżki kodu zostaną znacznie dokładniej przetestowane i wykonane.

Wpadli na ten pomysł po zauważeniu, że niektóre systemy zaczęły działać szybciej po awarii niż po uporządkowanym wyłączeniu.

Dobrym, choć już nieistotnym przykładem, są niektóre starsze wersje Firefoksa, które nie tylko uruchamiają się szybciej po przywróceniu po awarii, ale także zapewniają lepsze uruchamianie w ten sposób ! W tych wersjach normalne zamknięcie przeglądarki Firefox spowoduje zamknięcie wszystkich otwartych kart i uruchomienie pojedynczej pustej karty. Podczas odzyskiwania po awarii przywróciłoby otwarte zakładki w chwili awarii. (I to był jedyny sposób na zamknięcie Firefoksa bez utraty aktualnego kontekstu przeglądania.) Co zrobili ludzie? Po prostu nigdy nie zamykali Firefoksa i zawsze pkill -KILL firefoxgo edytowali.

W Linux Weekly News znajduje się ładny artykuł o oprogramowaniu tylko do awarii autorstwa Valerie Aurora . Warto też przeczytać komentarze. Na przykład ktoś w komentarzach słusznie zauważa, że ​​te pomysły nie są nowe i w rzeczywistości są mniej więcej równoważne z zasadami projektowania aplikacji opartych na Erlang / OTP. I oczywiście, patrząc na to dzisiaj, kolejne 10 lat po Valerie i 15 lat po oryginalnym artykule, możemy zauważyć, że obecny szum w zakresie mikrousług jeszcze raz wymyśla te same pomysły. Nowoczesne projektowanie centrum danych w chmurze jest również przykładem grubszej ziarnistości. (Dowolny komputer może ulec awarii w dowolnym momencie bez wpływu na system.)

Nie wystarczy jednak pozwolić na awarię oprogramowania. Musi być do tego zaprojektowany. Idealnie byłoby, gdyby twoje oprogramowanie zostało podzielone na małe, niezależne komponenty, z których każdy może ulec awarii niezależnie. Ponadto „mechanizm awarii” powinien znajdować się poza komponentem, który ulega awarii.

Jörg W Mittag
źródło
1

Właściwym sposobem obsługi większości wyjątków powinno być unieważnienie dowolnego obiektu, który może być w konsekwencji w stanie uszkodzonym, i kontynuowanie wykonywania, jeśli unieważnione obiekty nie zapobiegną temu. Na przykład bezpieczny paradygmat aktualizacji zasobu to:

acquire lock
try
  update guarded resource
if exception
  invalidate lock
else
  release lock
end try

Jeśli podczas aktualizowania strzeżonego zasobu wystąpi nieoczekiwany wyjątek, zasób należy założyć w stanie uszkodzonym, a blokada unieważniona, niezależnie od tego, czy wyjątek jest typu, który w innym przypadku byłby łagodny.

Niestety strażnicy zasobów zaimplementowani przez IDisposable/ usingzostaną zwolnieni za każdym razem, gdy strzeżony blok wyjdzie, bez żadnej wiedzy, czy blok opuścił normalnie, czy nienormalnie. Tak więc, chociaż powinny istnieć dobrze zdefiniowane kryteria, kiedy należy kontynuować po wyjątku, nie ma sposobu, aby powiedzieć, kiedy mają zastosowanie.

supercat
źródło
+1 po prostu za wyrażenie tej stosunkowo nieoczywistej i wciąż nieczęstej perspektywy na to, co jest właściwe. Nie wiem jeszcze, czy się z tym zgadzam, ponieważ jest to dla mnie nowatorska heurystyka / zasada, więc muszę to przemyśleć przez jakiś czas, ale wydaje się to rozsądne.
mtraceur
0

Możesz zastosować podejście, które stosuje każda aplikacja na iOS i MacOS: Nieprzechwycony wyjątek natychmiast usuwa aplikację. Plus wiele błędów, takich jak przekroczenie granicy tablicy lub po prostu przepełnienie arytmetyczne w nowszych aplikacjach, robi to samo. Bez ostrzeżenia.

Z mojego doświadczenia wynika, że ​​wielu użytkowników nie zwraca na to uwagi, tylko ponownie stukając ikonę aplikacji.

Oczywiście musisz upewnić się, że taka awaria nie prowadzi do znacznej utraty danych i zdecydowanie nie prowadzi do kosztownych błędów. Ale ostrzeżenie „Twoja aplikacja ulegnie teraz awarii. Zadzwoń do wsparcia, jeśli Ci to przeszkadza ”, nikomu nie pomaga.

gnasher729
źródło