Crash Nask.Desk 2.0.2.71 po pewnym czasie od podpisania lub edycji pliku

Występuje u nas błąd objawiający się zawieszonym programem Nask.Desk . Błąd można wyświetlić klikając na ikonkę Nask.Desk w tray-u a następnie Interfejs->Pokaż interfejs użytkownika. Komunikt brzmi: “Wystapił problem z aplikacją zostanie zamknięta Instances cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it (or one of its parent scopes) has already been disposed.”
Użytkownik nie ma informacji o momencie zawieszenia programu. Użytkownik klika “Edytuj” a Word się nie uruchamia lub nie uruchamia się okno podpisywania pliku.

Po analizie logów w odniesieniu do plików pdb przy pomocy m.in. windb, AI :slight_smile: podpowiada, że:

  1. Główną przyczyną jest wyścig wątków (race condition) pomiędzy procedurą zamykania aplikacji (która niszczy kontener wstrzykiwania zależności Autofac – LifetimeScope), a asynchronicznymi zadaniami sprzątającymi w tle (SystemWatcherManager, ReactiveUI). Wątek poboczny próbuje odpytać martwy kontener o usługi, co generuje nieobsługiwany wyjątek System.ObjectDisposedException przechwytywany przez odśmiecacz pamięci jako TaskScheduler.UnobservedTaskException, co bezwzględnie ubija proces.

2.Na podstawie analizy binarnej plików NASK.Desk.Modules.MessageModule.dll oraz MessageService.cs ustalono, że kod nie posiada zabezpieczeń w miejscu wystąpienia awarii:

  • Moduł/Plik: NASK.Desk.Modules.MessageModule\Services\MessageService.cs
  • Metoda: GetMessagesAsync(Int64 page, Int64 take, String filter)
  • Linia kodu: 36
  • Wątek: ReactiveUI.Avalonia.AvaloniaScheduler / Wątek poboczny .NET

3.Proces:
Wyzwalane jest żądanie zamknięcia aplikacji (Cancellation requested). Usługa ServiceRunner wygasza serwisy i następuje formalna utylizacja (Dispose()) głównego kontenera Autofac (LifetimeScope).
Gwałtowne anulowanie zadań w tle. Mechanizm SystemWatcherManager.QueueProcessor wyzwalany zamknięciem sesji pliku rzuca nieobsługiwany, uciekający wyjątek System.Threading.Tasks.TaskCanceledException: A task was canceled w liniach 109 i 72.

Strzał w próżnię (Crash): Potok reaktywny próbuje zaktualizować listę wiadomości. W linii 36 pliku MessageService.cs wywoływana jest metoda Splat.DependencyResolverMixins.GetService<IMessageService>(). Ponieważ podpięty kontener Autofac jest już zniszczony, rzucany jest wyjątek ObjectDisposedException.

Wyjątek ten nie jest zalogowany tradycyjnie, ponieważ ucieka w asynchronicznym potoku System.Reactive i doprowadza do natychmiastowego zamknięcia procesu przez system Windows.

4.System.ObjectDisposedException: Instances cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it (or one of its parent scopes) has already been disposed. at Autofac.Core.Lifetime.LifetimeScope.ThrowDisposedException() at Autofac.Core.Lifetime.LifetimeScope.ResolveComponent(ResolveRequest& request) at Splat.Autofac.AutofacDependencyResolver.GetService(Type serviceType, String contract) in c:\temp\releaser\splat\src\Splat.Autofac\AutofacDependencyResolver.cs:line 58 at Splat.DependencyResolverMixins.GetService[T](IReadonlyDependencyResolver resolver, String contract) in c:\temp\releaser\splat\src\Splat.Core\ServiceLocation\DependencyResolverMixins.cs:line 28 at NASK.Desk.Modules.MessageModule.Services.MessageService.GetMessagesAsync(Int64 page, Int64 take, String filter) in C:\BuildAgentCS\work\5cb875900f2e924d\NASK.Desk.Modules.MessageModule\Services\MessageService.cs:line 36

  1. Proponowane rozwiązanie:
    Rozwiązanie A (Zabezpieczenie lokalizatora Splat): W pliku adaptera AutofacDependencyResolver.cs (okolice linii 58), opakować wywołanie ResolveComponent w bezpieczny blok try-catch obsługujący ObjectDisposedException i zwracający null. Pozwoli to potokom ReactiveUI łagodnie zignorować brak usługi podczas zamykania programu.
  • Rozwiązanie B (Wprowadzenie CancellationToken): Aktualna sygnatura metody GetMessagesAsync w MessageService.cs nie przyjmuje tokenu anulowania. Należy dopisać i respektować CancellationToken powiązany z cyklem życia aplikacji, aby pętla QueueProcessor cicho i bezpiecznie wygaszała zadania przed zniszczeniem kontenera DI.
  • Rozwiązanie C (Obsługa wyjątków w ReactiveCommand): W pliku MessagesViewModel.cs upewnić się, że wywołanie zarządzające odświeżaniem listy posiada jawną subskrypcję do obsługi błędów potoku (LoadMessagesCommand.ThrownExceptions.Subscribe(...)), zapobiegając propagacji błędu do UnobservedTaskException.

a konkretnie:
Modyfikacja 1: Wprowadzenie zabezpieczenia przed zniszczonym kontenerem (Najważniejsze)

  • Plik: NASK.Desk.Modules.MessageModule\Services\MessageService.cs

  • Metoda: GetMessagesAsync

  • Aktualna linia 36:

    csharp

    public async Task<IEnumerable<ClientMessageModel>> GetMessagesAsync(long page, long take, string filter)
    {
        // ... (początek metody) ...
    
        // LINIA 36: Bezpośrednie i niezabezpieczone odpytanie lokalizatora Splat
        // To wywołanie powoduje natychmiastowy crash, jeśli LifetimeScope Autofaca został zamknięty!
        var service = Splat.DependencyResolverMixins.GetService<IMessageService>(Splat.Locator.Current, null);
    
        // ... (reszta metody pobierającej dane) ...
    }
    
  • Zmiana kodu na bezpieczny: Należy opakować tę linię w blok try-catch, aby asynchroniczny potok nie wyłożył aplikacji:

    IMessageService service;
    try
    {
        service = Splat.DependencyResolverMixins.GetService<IMessageService>(Splat.Locator.Current, null);
        if (service == null) return Enumerable.Empty<ClientMessageModel>();
    }
    catch (ObjectDisposedException)
    {
        // Kontener Autofac został zniszczony podczas zamykania aplikacji - bezpiecznie przerywamy
        return Enumerable.Empty<ClientMessageModel>();
    }
    

Modyfikacja 2: Bezpieczne wygaszanie zadań w tle (Naprawa TaskCanceledException)

  • Plik: NASK.Desk.Modules.EzdRpModule\Managers\SystemWatcherManager.cs

  • Metoda: QueueProcessor

  • Aktualne linie 108–110:

    Pętla procesora kolejki rzuca uciekający wyjątek anulowania, gdy aplikacja jest wyłączana.

  • Zmiana kodu na bezpieczny: Należy jawnie przechwycić stan anulowania tokenu (CancellationToken):

    try
    {
        // Dotychczasowa logika przetwarzania kolejki (linia 109)
    }
    catch (OperationCanceledException) 
    {
        // Oczekiwane zachowanie podczas zamykania aplikacji - logujemy i kończymy czysto
        Serilog.Log.Information("QueueProcessor został pomyślnie anulowany.");
    }
    

Modyfikacja 3: Przechwycenie błędu w potoku ReactiveUI

  • Plik: NASK.Desk.Modules.MessageModule\ViewModels\MessagesViewModel.cs

  • Metoda: GetMessagesAsync (linia 253)

  • Zmiana kodu na bezpieczny: W konstruktorze ViewModelu lub w miejscu inicjalizacji komendy pobierania wiadomości (ReactiveCommand) brakuje subskrypcji błędów:

    // Należy dopisać obsługę ThrownExceptions dla komendy ładującej wiadomości
    this.LoadMessagesCommand.ThrownExceptions
        .Subscribe(ex => Serilog.Log.Warning(ex, "Ignorowanie błędu ładowania wiadomości podczas zamykania procesu."));
    
    

Nie wiem jak u innych firm. Obecnie poza monitorowaniem aplikacji i ręcznym restartem, jedyną opcją jest próba rozpoznawania problemu np. w logach i restart Nask.Desk przy pomocy skryptu co mam zamiar przetestować.

Inteligenta kontrola aplikacji Windows wystarczy to wyłączyć i po problemie. Tylko wtedy jest problem, że żeby włączyć to ponownie trzeba zrobić reinstall Windowsa. Niektóre rozwiązania w logach systemu potrafią wracać do pierwotnych ustawień po restarcie i problem wraca

Tak ja jak i inni w naszym Zakładzie, mamy tę opcję wyłączoną więc to nie pomaga.

U nas w firmie ten sam błąd. Tak jak pisze ArturH Inteligenta kontrola aplikacji wyłączona.

u mnie pomogło, u każdego u kogo nie działał głównie podpis

Jedyną potencjalna wskazówką jaką mam to odległość czasowa od momentu podpisu. Błędy zacięcia są wpisywane do logów w nocy (Nie wiem czy tak jest zawsze. U osób, które pytałem, tak było. Dopiero niedawno zauważyłem tę powtarzalność i zacząłem o to pytać). Być może z powodu upłynięcia okresu czasu. Zbiegiem okoliczności zacięcia następują u osób, które nie wyłączały komputera na noc.