From f5a7dd80d60d0f0b0fa1ffa97d0e167f0bdd2e63 Mon Sep 17 00:00:00 2001 From: laurentiu021 Date: Mon, 18 May 2026 11:27:50 +0300 Subject: [PATCH] fix: replace fire-and-forget async with safe InitializeAsync helper (CQ-M3) --- CHANGELOG.md | 14 ++++++++++++ .../SysManager/ViewModels/AboutViewModel.cs | 2 +- .../ViewModels/BatteryHealthViewModel.cs | 2 +- .../SysManager/ViewModels/CleanupViewModel.cs | 2 +- .../ViewModels/DashboardViewModel.cs | 2 +- .../ViewModels/DeepCleanupViewModel.cs | 2 +- .../ViewModels/PerformanceViewModel.cs | 2 +- .../ViewModels/ProcessManagerViewModel.cs | 2 +- .../ViewModels/ServicesViewModel.cs | 2 +- .../ViewModels/SpeedTestViewModel.cs | 2 +- .../SysManager/ViewModels/StartupViewModel.cs | 2 +- .../ViewModels/SystemHealthViewModel.cs | 2 +- .../SysManager/ViewModels/ViewModelBase.cs | 22 +++++++++++++++++++ .../ViewModels/WindowsUpdateViewModel.cs | 2 +- 14 files changed, 48 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcaf6612..1bb089bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.48.30] - 2026-05-18 + +### Fixed +- **ViewModelBase** — added `InitializeAsync` helper method that wraps + fire-and-forget async calls with structured error handling. Exceptions + from async initialization are now caught and logged via Serilog instead + of becoming unobserved task exceptions (CQ-M3). +- **12 ViewModels** — replaced `_ = InitAsync()` fire-and-forget pattern + with `InitializeAsync(InitAsync)` in: AboutViewModel, BatteryHealthViewModel, + CleanupViewModel, DashboardViewModel, DeepCleanupViewModel, + PerformanceViewModel, ProcessManagerViewModel, ServicesViewModel, + SpeedTestViewModel, StartupViewModel, SystemHealthViewModel, + WindowsUpdateViewModel. + ## [0.48.29] - 2026-05-18 ### Changed diff --git a/SysManager/SysManager/ViewModels/AboutViewModel.cs b/SysManager/SysManager/ViewModels/AboutViewModel.cs index 4e2c96d4..eba8a7de 100644 --- a/SysManager/SysManager/ViewModels/AboutViewModel.cs +++ b/SysManager/SysManager/ViewModels/AboutViewModel.cs @@ -47,7 +47,7 @@ public AboutViewModel() : this(new UpdateService()) { } public AboutViewModel(UpdateService updates) { _updates = updates; - _ = InitAsync(); + InitializeAsync(InitAsync); } private async Task InitAsync() diff --git a/SysManager/SysManager/ViewModels/BatteryHealthViewModel.cs b/SysManager/SysManager/ViewModels/BatteryHealthViewModel.cs index 1b5a9c8a..2cce9a41 100644 --- a/SysManager/SysManager/ViewModels/BatteryHealthViewModel.cs +++ b/SysManager/SysManager/ViewModels/BatteryHealthViewModel.cs @@ -23,7 +23,7 @@ public partial class BatteryHealthViewModel : ViewModelBase public BatteryHealthViewModel() { - _ = InitAsync(); + InitializeAsync(InitAsync); } private async Task InitAsync() diff --git a/SysManager/SysManager/ViewModels/CleanupViewModel.cs b/SysManager/SysManager/ViewModels/CleanupViewModel.cs index 420ac253..81971481 100644 --- a/SysManager/SysManager/ViewModels/CleanupViewModel.cs +++ b/SysManager/SysManager/ViewModels/CleanupViewModel.cs @@ -53,7 +53,7 @@ public CleanupViewModel(PowerShellRunner runner) _runner.ProgressChanged += OnRunnerProgressChanged; IsElevated = AdminHelper.IsElevated(); - _ = InitAsync(); + InitializeAsync(InitAsync); } private void OnRunnerLineReceived(PowerShellLine l) => Console.Append(l); diff --git a/SysManager/SysManager/ViewModels/DashboardViewModel.cs b/SysManager/SysManager/ViewModels/DashboardViewModel.cs index bf4c2e48..bfba474d 100644 --- a/SysManager/SysManager/ViewModels/DashboardViewModel.cs +++ b/SysManager/SysManager/ViewModels/DashboardViewModel.cs @@ -55,7 +55,7 @@ public DashboardViewModel(SystemInfoService sys, TuneUpService tuneUp, HealthSco _tuneUp = tuneUp; _healthScore = healthScore; IsElevated = AdminHelper.IsElevated(); - _ = LoadHealthScoreAsync(); + InitializeAsync(LoadHealthScoreAsync); } // ── Health Score ─────────────────────────────────────────────────── diff --git a/SysManager/SysManager/ViewModels/DeepCleanupViewModel.cs b/SysManager/SysManager/ViewModels/DeepCleanupViewModel.cs index dd40aab9..de1cd05a 100644 --- a/SysManager/SysManager/ViewModels/DeepCleanupViewModel.cs +++ b/SysManager/SysManager/ViewModels/DeepCleanupViewModel.cs @@ -60,7 +60,7 @@ public partial class DeepCleanupViewModel : ViewModelBase public DeepCleanupViewModel() { - _ = InitAsync(); + InitializeAsync(InitAsync); } private async Task InitAsync() diff --git a/SysManager/SysManager/ViewModels/PerformanceViewModel.cs b/SysManager/SysManager/ViewModels/PerformanceViewModel.cs index 6d402fc6..fb990f6d 100644 --- a/SysManager/SysManager/ViewModels/PerformanceViewModel.cs +++ b/SysManager/SysManager/ViewModels/PerformanceViewModel.cs @@ -54,7 +54,7 @@ public partial class PerformanceViewModel : ViewModelBase public PerformanceViewModel(PowerShellRunner ps) { _service = new PerformanceService(ps); - _ = InitAsync(); + InitializeAsync(InitAsync); } private async Task InitAsync() diff --git a/SysManager/SysManager/ViewModels/ProcessManagerViewModel.cs b/SysManager/SysManager/ViewModels/ProcessManagerViewModel.cs index abde73bd..b8aaa310 100644 --- a/SysManager/SysManager/ViewModels/ProcessManagerViewModel.cs +++ b/SysManager/SysManager/ViewModels/ProcessManagerViewModel.cs @@ -33,7 +33,7 @@ public partial class ProcessManagerViewModel : ViewModelBase public ProcessManagerViewModel() { - _ = InitAsync(); + InitializeAsync(InitAsync); } private async Task InitAsync() diff --git a/SysManager/SysManager/ViewModels/ServicesViewModel.cs b/SysManager/SysManager/ViewModels/ServicesViewModel.cs index 05d5c61f..554639ef 100644 --- a/SysManager/SysManager/ViewModels/ServicesViewModel.cs +++ b/SysManager/SysManager/ViewModels/ServicesViewModel.cs @@ -36,7 +36,7 @@ public partial class ServicesViewModel : ViewModelBase public ServicesViewModel() { - _ = InitAsync(); + InitializeAsync(InitAsync); } private async Task InitAsync() diff --git a/SysManager/SysManager/ViewModels/SpeedTestViewModel.cs b/SysManager/SysManager/ViewModels/SpeedTestViewModel.cs index dd1021b3..321b4d06 100644 --- a/SysManager/SysManager/ViewModels/SpeedTestViewModel.cs +++ b/SysManager/SysManager/ViewModels/SpeedTestViewModel.cs @@ -40,7 +40,7 @@ public partial class SpeedTestViewModel : ViewModelBase public SpeedTestViewModel(NetworkSharedState shared) { Shared = shared; - _ = LoadHistoryAsync(); + InitializeAsync(LoadHistoryAsync); } private async Task LoadHistoryAsync() diff --git a/SysManager/SysManager/ViewModels/StartupViewModel.cs b/SysManager/SysManager/ViewModels/StartupViewModel.cs index 2273c43a..fe9f1197 100644 --- a/SysManager/SysManager/ViewModels/StartupViewModel.cs +++ b/SysManager/SysManager/ViewModels/StartupViewModel.cs @@ -30,7 +30,7 @@ public partial class StartupViewModel : ViewModelBase public StartupViewModel() { - _ = InitAsync(); + InitializeAsync(InitAsync); } partial void OnHideWindowsEntriesChanged(bool value) => ApplyFilter(); diff --git a/SysManager/SysManager/ViewModels/SystemHealthViewModel.cs b/SysManager/SysManager/ViewModels/SystemHealthViewModel.cs index 009b99c2..a9b7ad44 100644 --- a/SysManager/SysManager/ViewModels/SystemHealthViewModel.cs +++ b/SysManager/SysManager/ViewModels/SystemHealthViewModel.cs @@ -50,7 +50,7 @@ public SystemHealthViewModel(SystemInfoService sys) IsElevated = AdminHelper.IsElevated(); _runner.LineReceived += OnRunnerLineReceived; - _ = InitAsync(); + InitializeAsync(InitAsync); } private void OnRunnerLineReceived(PowerShellLine l) => Console.Append(l); diff --git a/SysManager/SysManager/ViewModels/ViewModelBase.cs b/SysManager/SysManager/ViewModels/ViewModelBase.cs index ed011ab9..c6f356e8 100644 --- a/SysManager/SysManager/ViewModels/ViewModelBase.cs +++ b/SysManager/SysManager/ViewModels/ViewModelBase.cs @@ -3,6 +3,7 @@ // License: MIT using CommunityToolkit.Mvvm.ComponentModel; +using Serilog; namespace SysManager.ViewModels; @@ -15,6 +16,27 @@ public abstract partial class ViewModelBase : ObservableObject, IDisposable private bool _disposed; + /// + /// Safely launches an async task from a constructor or non-async context. + /// Exceptions are caught and logged instead of becoming unobserved task + /// exceptions that could crash the application (CQ-M3). + /// + protected static async void InitializeAsync(Func asyncAction, [System.Runtime.CompilerServices.CallerMemberName] string callerName = "") + { + try + { + await asyncAction().ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Expected during shutdown — no action needed. + } + catch (Exception ex) + { + Log.Error(ex, "Unhandled exception in async initialization of {Caller}", callerName); + } + } + /// /// Override in derived classes to release managed resources /// (CancellationTokenSources, event handlers, timers, etc.). diff --git a/SysManager/SysManager/ViewModels/WindowsUpdateViewModel.cs b/SysManager/SysManager/ViewModels/WindowsUpdateViewModel.cs index 09ae1f08..2ad0c0e9 100644 --- a/SysManager/SysManager/ViewModels/WindowsUpdateViewModel.cs +++ b/SysManager/SysManager/ViewModels/WindowsUpdateViewModel.cs @@ -35,7 +35,7 @@ public WindowsUpdateViewModel(PowerShellRunner runner) _runner.LineReceived += OnRunnerLineReceived; _runner.ProgressChanged += OnRunnerProgressChanged; IsElevated = AdminHelper.IsElevated(); - _ = InitAsync(); + InitializeAsync(InitAsync); } private void OnRunnerLineReceived(PowerShellLine l) => Console.Append(l);