O obsłudze sytuacji wyjątkowych słów kilka

Podczas jednej z niedawnych rozmów rekrutacyjnych (w sumie to chyba lubię na nie chodzić – często są dość inspirujące 🙂 ) spotkałem się z ciekawym pytaniem – “Jakie są negatywne konsekwencje stosowania konstrukcji try-catch?” Oczywiście zbyt wiele klawiatur zjadłem, by w ciemno walnąć głupotą, że są wyłącznie same zalety. Pominę co wtedy dokładnie odpowiedziałem, ale moja własna odpowiedź skłoniła mnie by jeszcze trochę pod tym kątem pogrzebać…

Trochę postów, trochę artykułów, sprawdzenie treści kilku książek. Wygląda na to, że ten teren nie jest jeszcze zbyt dokładnie spenetrowany… Mam swoją tezę do udowaodnienia – że jeśli już się decydujemy na przechwytywanie wyjątków to czeka nas ogromna ilość planowania i pracy. Tym artykułem postaram się ją udowodnić oraz podać kilka wskazówek jak można by zabrać się do realizacji tego zadania, o czym może warto pomyśleć z wyprzedzeniem.

Jednak na początek trochę rozważań teoretycznych i to zaczynając od poziomu bardziej abstrakcyjnego. Autor (Patrick Cauldwell) książki „Code Leader” (Amazon.com) dość jasno formułuje zasadę prezentowania sytuacji wyjątkowych użytkownikowi:

Errors should only be handed off to the user if there is something that the user can do to correct the problem, and that needs to be clearly communicated. (…) It is vitally important to make your error messages to the user actionable. If an error message doesn’t tell the user how to correct the problem, then there is no point in showing the user an error message.

Oczywiście po takim oświadczeniu natychmiast powinna nastąpić gorąca debata wśród architektów i programistów:

  • które błędy prezentować?
  • co robić z błędami których nie prezentujemy?
  • co zrobić z samą aplikacją po prezentacji błędu? Zwłaszcza z przetwarzanymi danymi?
  • które sytuacje to faktyczne wyjątki, a które to łatwe do przewidzenia sytuacje nieprawidłowości?
  • które błędy – ostatecznie – należy uznać za krytyczne i zagrażające porządkowi publicznemu?

Moim zdaniem nie powinno się nawet ruszać kodowania, dopóki stosowna polityka obsługi błędów nie zostanie ustalona oraz utrwalona w zespole. Groźne będą zarówno podejście takie, gdzie nikt się tym nie przejmuje i tylko swoje koduje jak i takie, gdy każdy programista coś tam po swojemu od czasu do czasu przechwyci (i to – zazwyczaj – dopiero wtedy, gdy mu w twarz wybuchnie podczas testów…). Nie żebym sam zawsze był w takim zachowaniu bez winy…
Szczególna niefrasobliwość co do istoty problemu wydaje się występować wśród programistów aplikacji webowych (Wybuchają często i sypią błędami na ekran bez żadnych zahamowań. No ale przecież wystarczy odświeżyć stronę lub wrócić na poprzednią by wznowić pracę, wiec co za problem?) czy konsolowych (Najczęściej to tylko narzędzia a ich odbiorca zapewne też jest inżynierem; i zapewne podał zły parametr, więc w zasadzie po co się tym przejmować, prawda?). Trochę lepiej jest w aplikacjach desktopowych. (Ale zapewne spory udział mają tutaj zdarzenia typu CatchUnhandledExceptions. Możliwe jednak, że i trochę większa świadomość tego co się robi.) Totalna masakra następuje w przypadku aplikacji mobilnych – często pisanych w pośpiechu i na wiele odmiennych platform jednocześnie – te zazwyczaj bez ostrzeżenia po prostu wychodzą „na pulpit” i tyle – i to niezależnie czy to iOS, Windows czy Android. Często także zawieszają całe urządzenie.
Być może i problem nie jest duży – przecież w końcu użytkownik widzi, że coś jest nie tak i sobie apkę uruchomi jeszcze raz, więc po co tracić czas i pieniądze na porządne tego zaplanowanie, wykonanie i przetestowanie? A że człowiek straci ileś-tam-dziesiąt minut pracy to przecież problem kogoś innego… (Ale temat „Czy opłaca się pisać bezbłędne oprogramowanie?” zamierzam poruszyć przy innej okazji.)
Co jednak w przypadku usług działających w tle, serwerów przetwarzających dane, serwisów usług, workflow czy systemów bankowych. Niby problemu nie ma, bo prędzej czy później ktoś zauważy, że serwer przestał działać… I zapewne na tym się skończy, no bo niby w jaki sposób ma powiadomić stosownego administratora? Zwłaszcza jak serwer należy do zewnętrznej firmy i jest weekend? Takie sytuacje są niedopuszczalne i uważam, że serwer powinien starać się utrzymać „przy życiu” za wszelką cenę. Ale o tym dalej.
Najpierw porozmawiajmy o prostszych sprawach.

Sytuacja na froncie

Nie ma sensu szczegółowo informować użytkownika o błędzie, którego:

  • nie jest w stanie naprawić
  • którego nie spowodował

Kropka. Może się także zdarzyć błąd systemu, który nie jest spowodowany przez użytkownika, ale który jednak uniemożliwi mu dalszą pracę z aplikacją. Taką sytuację podciągnąłbym zasadniczo pod punkt pierwszy – użytkownik może sytuację poprawić, po prostu próbując skorzystać z aplikacji w innym terminie, gdy usterka zostanie naprawiona lub wyeliminowana 😛 Ale niech przynajmniej wie co się dzieje… (A w zasadzie: co się NIE dzieje.)
Ale nigdy… Hmm, lepiej chyba: NIGDY… A w zasadzie NIGDY!!! Aplikacja nie może rzucać wyjątkami spowodowanymi:

  • danymi wprowadzonymi przez użytkownika
  • plikami wczytanymi przez użytkownika
  • konfiguracją aplikacji, jeśli siedzi w pliku tekstowym i jest podatna na swobodne modyfikacje – z założenia przez użytkownika właśnie).

Wszelkie dane tego typu powinny ZAWSZE być traktowane jako w 99% błędne i przetrzepywane na wszystkie strony przez wszelkiej maści walidatory, kontrolery czy co tam jeszcze. Że o weryfikacji i zabezpieczeniach pod kątem celowego wprowadzania systemu w błędny stan nie wspomnę. Zanim zaczniemy z nimi cokolwiek dalej robić.
Błędne dane wejściowe to nie jest żadna wyjątkowa sytuacja. Nawet gdy się urwą w połowie podczas ściągania ze zdalnego serwera nie powinniśmy być tym faktem zaskoczeni. Elegancko informujemy zleceniodawcę o błędzie transmisji i niech sam zdecyduje czy ponawiać od razu, później czy dopiero jak się modem zwolni…
Nie znaczy to bynajmniej by kod prezentujący dane użytkownikowi miał nie rzucać wyjątkami – aż tak niefrasobliwy co do solidności pracy kolegów programistów to ja nie jestem – ale wszystkie powinny zostać odpowiednio zaadresowane na etapie testów i nigdy więcej nie powinny być wyrzucone przez kod produkcyjny! Powinny jedynie służyć pomocą podczas dewelopingu.

Aplikacje tła

Utrzymać się przy życiu…. Hmmm… Najbardziej kuszącym rozwiązaniem, i pierwszym, które przychodzi do głowy jest zastosowanie wyklętej przez mądre głowy konstrukcji Catch’em’All:

public string GetFileContents(string fileName)
{
    try
    {
        StreamReader sr = new StreamReader(ReadFileFromNetwork(fileName));
        return sr.ReadToEnd();
    }
    catch (Exception e)
    {
        return null;
    }
}

Myślę, że większość programistów wie i rozumie dlaczego. Jeśli nie, to przyczyny dość zgrabnie wyłożył Victor Hurdugaci w swojej odpowiedzi (jednej z wielu podobnych w sieci) do podstawowego pytania „Is catching general exceptions really a bad thing?”:

Yes, catching general exceptions is a bad thing. An exception usually means that the program cannot do what you asked it to do.
There are a few types of exceptions that you could handle:

  • Fatal exceptions: out of memory, stack overflow, etc. Some supernatural force just messed up your universe and the process is already dying. You cannot make the situation better so just give up
  • Exception thrown because bugs in the code that you are using: Don’t try to handle them but rather fix the source of the problem. Don’t use exceptions for flow control
  • Exceptional situations: Only handle exception in these cases. Here we can include: network cable unplugged, internet connection stopped working, missing permissions, etc.

Oh, and as a general rule: if you don’t know what to do with an exception if you catch it, it is better to just fail fast (pass the exception to caller and let it handle it)

Niby wszystko jasne, ale z drugiej strony nawet najczęściej dydaktycznie wykorzystywane OutOfMemoryException nie do końca musi oznaczać coś nieodwracalnie złego. Ot np. w sytuacji gdy próbujemy równolegle przetwarzać wiele plików różnej wielkości a pamięć jest już i tak poszatkowana wcześniejszymi alokacjami, którymi GC się jeszcze nie zajął. W momencie gdy spróbujemy zarezerwować miejsce na kolejny duży plik dostaniemy właśnie ten wyjątek. (No dobra, nie dotyczy aplikacji 64bit gdy w systemie działa poprawnie pamięć wirtualna a na dysku jest jeszcze dość miejsca. Swoją drogą to chyba najprostszy sposób uniknięcia problemów z alokacją pamięci w podanym scenariuszu.)

Teoretycznie od razu powinniśmy zakończyć działanie aplikacji – bo błąd krytyczny. Ale przecież naszej aplikacji wcale nie zabrakło pamięci – jedynie nie była w stanie w DANEJ CHWILI przeprowadzić tylko jednej z wielu operacji. Mało tego – ponieważ alokacja się nie powiodła to do dyspozycji możemy jeszcze nadal mieć całkiem sporo wolnej pamięci – np. by dokończyć inne zadania a potem jeszcze raz spróbować zmierzyć się z większym problemem. Może nawet GC w międzyczasie trochę posprząta? Jeszcze za chwilę do tego wrócę, ale przyjrzyjmy się temu trochę przekornemu kodowi:

public static class Helpers
{
    public static bool CanWriteToFile(string path)
    {
        if (path == null) throw new ArgumentNullException("path");

        try
        {
            Uri uri = new Uri(path);
            if (!uri.IsFile)
                return false;
            if (!File.Exists(uri.LocalPath))
                return false;
            using (var sw = new StreamWriter(uri.LocalPath, true))
            {
                sw.Close();
            }
            // it seems we can write...
            return true;
        }
        catch (Exception)
        {
            return false;
        }
    }
}

OK? Założenie jest proste – niezależnie jaka jest przyczyna (zła ścieżka, błędne znaki, plik tylko do odczytu, brak dostępu itd.) – do pliku pisać nie możemy. TRUE w linii 14 jest bardzo istotne – nie chcemy przypadkiem skasować zawartości pliku. (Oczywiście funkcja w tej postaci nie jest w stanie zweryfikować czy możemy rzeczywiście chociaż jeden bajt do pliku zapisać z powodu np. braku miejsca na dysku – to zapewne można by sprawdzić bardziej bezpośrednio za pomocą WinAPI albo zdecydować się na łapanie wyjątku już podczas samej operacji zapisu 🙂 )

Kilka szybkich testów dość dobrze ilustruje zachowanie tej funkcji:

[TestFixture]
public class HelpersTests
{
    [Test]
    public void local_file_without_read_only_flag_shall_be_writable()
    {
        Assert.IsTrue(Helpers.CanWriteToFile(@"file://F:\test\Test.rtf"));
        // jest TRUE
    }

    [Test]
    public void local_file_with_read_only_flag_shall_not_be_writable()
    {
        Assert.IsFalse(Helpers.CanWriteToFile(@"file://F:\test\Ro_Test.rtf"));
        // Przechwycony System.UnauthorizedAccessException {"Odmowa dostępu do ścieżki „F:\\test\\Ro_Test.rtf”."}
    }

    [Test]
    [ExpectedException(typeof(ArgumentNullException))]
    public void null_file_path_shall_throw_exception()
    {
        Assert.IsFalse(Helpers.CanWriteToFile(null));
        // akurat to jest błąd programisty
    }

    [Test]
    public void empty_file_path_shall_not_be_writable()
    {
        Assert.IsFalse(Helpers.CanWriteToFile(""));
        // Teoretycznie też powinien być ArgumentException, ale darujmy sobie - URI samo zwraca:
        // System.UriFormatException {"Nieprawidłowy identyfikator URI: identyfikator URI jest pusty."}, który skrzętnie przechwytujemy
    }

    [Test]
    public void web_path_shall_not_be_writable()
    {
        Assert.IsFalse(Helpers.CanWriteToFile(@"http://www.codeproject.com/index.html"));
        // W oczywisty sposób nie jest to ścieżka do pliku, tym bardziej lokalnego
    }
}

Gdybyśmy chcieli nawet tak prostą funkcję dokładnie rozpisać i obwarować testami i wyłapywaniem poszczególnych wyjątków moglibyśmy zmarnotrawić całkiem sporo godzin. Zwłaszcza, że lista potencjalnie możliwych do wyrzucenia wyjątków jest dość spora (o tym jak ją wyprodukowałem planuję napisać kiedy indziej. Te z gwiazdką nie są wyrzucane przez kod, ale dodałem je w celu uzupełnienia hierarchii.):

---System.Exception(*)
   ---System.SystemException(*)
      ---System.ArgumentException
         ---System.ArgumentNullException
         ---System.ArgumentOutOfRangeException
         ---System.Globalization.CultureNotFoundException
      ---System.TypeLoadException
      ---System.InvalidOperationException
         ---System.ObjectDisposedException
      ---System.IndexOutOfRangeException
      ---System.OutOfMemoryException
      ---System.FormatException
         ---System.UriFormatException
      ---System.Threading.AbandonedMutexException
      ---System.NotSupportedException
      ---System.NullReferenceException
      ---System.IO.IOException
         ---System.IO.PathTooLongException
         ---System.IO.FileNotFoundException
         ---System.IO.DirectoryNotFoundException
         ---System.IO.DriveNotFoundException
      ---System.UnauthorizedAccessException
      ---System.OperationCanceledException
      ---System.Security.SecurityException
      ---System.MemberAccessException
         ---System.MissingMemberException(*)
            ---System.MissingMethodException
      ---System.NotImplementedException
      ---System.Resources.MissingManifestResourceException
      ---System.Configuration.ConfigurationException(*)
         ---System.Configuration.ConfigurationErrorsException
      ---System.Security.HostProtectionException
      ---System.Xml.XmlException
      ---System.Collections.Generic.KeyNotFoundException
      ---System.TypeInitializationException
      ---System.Reflection.AmbiguousMatchException
      ---System.MulticastNotSupportedException
      ---System.RankException
      ---System.Xml.Schema.XmlSchemaException
         ---System.Xml.Schema.XmlSchemaValidationException
      ---System.Xml.XPath.XPathException
      ---System.ArithmeticException(*)
         ---System.OverflowException
      ---System.InvalidCastException
   ---System.Diagnostics.Tracing.EventSourceException
   ---System.ApplicationException
      ---System.Reflection.TargetInvocationException
      ---System.Reflection.TargetException
      ---System.Reflection.TargetParameterCountException
   ---System.Xml.Schema.UpaException

Oczywiście większość z nich raczej nie wystąpi, gdyż pochodzą z głębin frameworka (zwłaszcza te z Xml czy dotyczące refleksji) tym niemniej potencjalnie są możliwe… Nie jest dobrze… Pojawia się nawet stary znajomy wspomniany kilka akapitów wyżej…

Opracowanie dokładnej polityki na różne okazje wydaje się w takiej sytuacji zadaniem nie do ogarnięcia. Pewną pomocą pozostaje oczywiście dokumentacja (zwłaszcza MSDN), która dość dokładnie opisuje w jakich sytuacjach dana funkcja wyrzuca jakie wyjątki – oczywiście, jeśli jej autorzy niczego nie pominęli lub zapomnieli zaktualizować.

Pozostaje chyba faktycznie stosować się do reguły ogólnej proponowanej przez Victora – nie połykać wyjątków, których znaczenia ani źródła nie ogarniamy.

No ale co w sytuacji, kiedy naprawdę chcemy zachować przy życiu system a nie mamy możliwości ciągłego nadzoru? Życie przyniosło już wiele możliwych podejść i prób rozwiązania problemu. Spróbuję poniżej opisać kilka z nich oraz poprzeć pomysłami – przynajmniej fragmentarycznych – rozwiązań.

Kody błędów

Nie używamy wyjątków. Jeśli się pojawią – zastępujemy je kodami niepowodzenia a na każdym etapie logiki biznesowej dokładnie je analizujemy. Dalsza lektura „Code leader’a”, przypomina zwyczaj jeszcze z czasów COM – gdy zasadniczo każda funkcja zwracała oprócz wyniku także kod statusu wykonania operacji, np. tak:

public class ReturnCodes
{
    public const int S_OK = 0;
    public const int E_FAIL = -1;
    public const int E_NULL_REFERENCE = -2;
    public const int E_NETWORK_FAILURE = -3;

    public int ReadFileFromNetwork(string fileName, out Stream file)
    {
        //attempt to read file fileName

        //success case
        return S_OK;

        //else

        //network failed
        return E_NETWORK_FAILURE;

        //etc...
    }
}

Z czym często możemy się nadal spotkać jeśli tylko potrzebujemy skomunikować się z Windows API. Jednakże korzystanie z takich funkcji po pierwsze rodzi bardzo złożone konstrukcje warunkowe, często solidnie spiętrzone, których zrozumienie, utrzymanie i tworzenie z czasem staje się coraz trudniejsze. Po drugie – zwracanie właściwych wartości wynikowych wymusza korzystanie z parametrów typu out. Nieporęczne to i niezbyt ładne.

Swego czasu próbowałem pewnego zakresu poradzenia sobie z tym zagadnieniem wprowadzając w projekcie zestaw klas generycznych opakowujących zarówno wyjątki, wartości zwracane, komunikaty o błędach i kody wynikowe w sprytny sposób dokonując także operacji konwersji explicite/implicite, które mogły spowodować, że w tej samej metodzie mogły koegzystować zupełnie odmienne instrukcje return:

 
return client;
// …
return BadBusinessOperation.UnathorizedAccess;
// …
catch (DataInconsistentExcpetion die)
{
    return die;
}

Zespołowi się to nawet spodobało i nawet dość ciekawie się z tego korzystało – ale z czasem sprawy się i tak znacznie skomplikowały. Opracowane handlery i sposoby rozpoznawania poziomu zagrożenia traktowane w sposób tak generyczny okazywały się czasami niewystarczające… I skończyło się na kilku mniejszych lub większych drzewach analizujących kody / flagi wynikowe…

Error Handler

Na początek lektura: Try Catch handler for C#. Co prawda zaproponowany tam Handler dotyczy trochę specyficznego aspektu pracy z instrukcją try-catch (pomijając drobny fakt, że jednak pewne rzeczy bardziej komplikuje niż upraszcza, zwłaszcza składnię, co czyni go mało przydatnym) ale podsuwa pomysł na możliwe odmienne podejście do uproszczenia i (przynajmniej częściowego) ustandaryzowania pracy z wyjątkami. Pokazuje też, że ludzie próbują ogarnąć jakoś temat. Na razie to tylko luźny pomysł – nie przetestowany w praktyce ale postaram się go pokrótce omówić:

  • Przechwytujemy wszystko jak leci – wyjątki, które wymagają specjalnej obsługi na danym etapie oddzielnie, a całą resztę – do wora spod bandery catch-all.
  • W tejże sekcji przechwycony wyjątek przekazujemy do klasy ErrorHandlera
  • Tamże wyjątek jest najpierw logowany a następnie analizowany.
  • Handler we wnętrzu aplikuje stosowną strategię, która decyduje czy dany rodzaj wyjątku jest bieżącym kontekście akceptowalny czy nie.
  • To oczywiście oznacza potrzebę istnienia fabryki / słownika / kontenera IoC, z którego będziemy pobierać / otrzymywać odpowiedni handler dla zaistniałej sytuacji biznesowej.
  • Sama wspomagana funkcja powinna dostać od handlera wskazówkę czy nie powinna przypadkiem wyrzucić błędu dalej i się zastosować

To najbardziej skomplikowana część zagadnienia, ale z drugiej strony może znacznie uzupełnić i usystematyzować sposób obrabiania wyjątków oraz uniknąć wielokrotnego powtarzania tych samych reguł przetwarzania wyjątków dla sytuacji zbliżonych do siebie biznesowo.
Wydaje się dość obiecujące, zwłaszcza, gdyby połączyć to z innymi pomysłami poniżej… Co się będzie działo z błędem dalej – gdzie handler go przekaże i jaki to może mieć wpływ na resztę systemu – trochę pomysłów podrzucam w jednej z następnych sekcji.

Wyjątki i Wątki

Mam jedną regułę (Pewnie nie zawsze udaje się jej przestrzegać w 100% ale staram się :P) – każdą metodę wywoływaną asynchronicznie na najwyższym poziomie (nie ważne czy to TPL, Thread, Invoke czy inne Action) mam otoczoną (w ten czy inny sposób) za pomocą try-catch-all. Co robię ze złapanym wyjątkiem to już inna sprawa, ale jeden zdechły wątek nie ma prawa wysypywać mi całej aplikacji!
Co robię?
1. W AppDomain / Application podpinam się pod zdarzenie CatchUnhandledExceptions (czy jak tam się ono nazywa) I łapię wszystko. Następnie loguję skrupulatnie błąd do oddzielnego loga i ubijam aplikację. Ten log jest dla mnie. I na potrzeby ewentualnego blame 😉
2. Na etapie dewelopmentu skrzętnie wyłapuje takie sierotki i naprawiam kod tak długo aż zostaną prawidłowo zaadresowane – tak by nigdy wyjątki z innych wątków niż główny tam nie trafiały.
3. W momencie przechwycenia wyjątku w wątku rozpoczynam jego analizę – to wymarzony moment na strategiczne zastosowanie specjalistycznych Error Handlerów.
a. Jeśli to był zwykły ThreadAbort – anuluję co robiłem i robię zwykłe return.
b. Jeśli był to błąd związany z OptimisticLocking, Timeout itp. – wycofuję całą transakcję (co jest proste przy podejściu typu Item of Work) i próbuję jeszcze raz.
c. Jeśli błąd był związany z kwestiami typu Serwis / Server/ System nie jest dostępny – powiadamiam ogólnego zarządcę wątków, ewentualnie odczekuję jakiś czas i próbuję wykonać pracę ponownie, jak wyżej – ale nie więcej niż n razy. Jeśli się nie uda – anuluję całe zadanie, czasami próbuję zapisać błąd i opuszczam wątek.
d. W tym czasie zarządca zbiera i zlicza powiadomienia o niedostępności usługi. Jeśli zbierze ich wystarczająco wiele – pauzuje wszystkie podległe zadania i uruchamia moduł oczekujący na przywrócenie niedostępnego systemu. Po sygnale od niego – kasuje licznik powiadomień i wznawia pracę podległych zadań / wątków.
e. Dlaczego tak? Cóż, podczas pracy z bazą MarkLogic poprzez Rest, podczas bardzo intensywnego przetwarzania danych spotkałem się z sytuacją, gdy niektóre wywołania komend kończyły się błędami o niedostępności bazy podczas gdy inne działały w najlepsze. Zdechła baza była nie do odróżnienia od takiej, która odrzuciła jedno z poleceń. Podejście takie może niekoniecznie się sprawdzać w niektórych scenariuszach, ale wydaje mi się bardzo elastyczne i dość generyczne dla większości potrzeb. Poza tym – od czego odpowiednia strategia podczas analizy konkretnego wątku?
4. Błędy systemowe ogólnie poza logowaniem powodują anulowanie pracy, zapisanie jej ze statusem błędu itp.
a. Zarządca może zliczać poszczególne kategorie błędów i też się do nich ustosunkować.
b. W ostateczności zarządca może zarządzić restart lub ubicie całej aplikacji – np. gdy próby przetwarzania jakiejś partii danych mogłyby skończyć się poważnymi konsekwencjami – np. opróżnienie kolejki, skasowanie plików z danymi źródłowymi itp.
c. Oczywiście do zadań zarządcy należy także wysyłanie odpowiednich powiadomień do administratora – fajnie jak jest czas by również w tym zakresie mieć jakieś strategie i politykę eskalacji oraz grupowania informacji o błędach – by byłe pierdołą głowy nie zawracać. Zwłaszcza 1000 razy pod rząd…

Stabilizacja w skali Makro

No dobrze. Chcemy ubić aplikację, albo ją zrestartować gdy stwierdzamy stan krytyczny. Może się też zdarzyć, że pojawił się błąd poziomu tak niskiego, że nie możemy go przechwycić i proces zostaje zabity przez system? Co robić?
Jeśli nasz serwis jest zwykłą usługą Windows – to jest tam trochę opcji dostępnych dla administratora – pozwalają one na obsługę całkiem sporej ilości scenariuszy i strategii.

Odzyskiwanie Usługi

Co jeśli to zbyt mało? Bo np. chcielibyśmy by restart był warunkowy? Albo odroczony? Nie odkryję koła na nowo, bo rozwiązanie jest już sprawdzone i zostało wielokrotnie zaimplementowane.
Co robimy? Dopisujemy drugą usługę – Strażnika – której jedynym zadaniem jest obserwowanie stanu głównej usługi i ewentualne jej restartowanie. Pewne dane co do sposobu jej działania wystarczy przekazać w rejestrze podczas zamykania głównej usługi. By wskazać np. parametry wznowienia, przyczynę zamknięcia i np. uniknąć restartu podczas zamykania całego systemu Windows 😛

Hmmm, to chyba na tyle. Na dzisiaj wystarczy :D. Powodzenia.
Aha, jeśli macie jakieś uwagi, pytania, własne pomysły lub przemyślenia – chętnie o nich usłyszę. Może uda się z tego skompilować drugą część – trochę materiału już mam :-P. A jeśli uważacie, że napisałem kompletne bzdury – też nie bójcie się mi tego udowodnić.