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
podpowiada, że:
- 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ątekSystem.ObjectDisposedExceptionprzechwytywany przez odśmiecacz pamięci jakoTaskScheduler.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
- Proponowane rozwiązanie:
Rozwiązanie A (Zabezpieczenie lokalizatora Splat): W pliku adapteraAutofacDependencyResolver.cs(okolice linii 58), opakować wywołanieResolveComponentw bezpieczny bloktry-catchobsługującyObjectDisposedExceptioni zwracającynull. Pozwoli to potokom ReactiveUI łagodnie zignorować brak usługi podczas zamykania programu.
- Rozwiązanie B (Wprowadzenie CancellationToken): Aktualna sygnatura metody
GetMessagesAsyncwMessageService.csnie przyjmuje tokenu anulowania. Należy dopisać i respektowaćCancellationTokenpowiązany z cyklem życia aplikacji, aby pętlaQueueProcessorcicho i bezpiecznie wygaszała zadania przed zniszczeniem kontenera DI. - Rozwiązanie C (Obsługa wyjątków w ReactiveCommand): W pliku
MessagesViewModel.csupewnić 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 doUnobservedTaskException.
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ć.
