From 5aa7c30a6aef2e22e8064dfdd71f0d6e4bfe2009 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Tue, 2 Jun 2026 15:25:39 -0500 Subject: [PATCH] Finalize DI: drop transitional VM ctor; harden disposal Remove the transitional MainWindowViewModel(AppSettings, IStartupTracer) constructor and the _ownsServices flag now that every caller resolves the view model from the container; the container owns and disposes the hardware monitor (driver unload), so the view model no longer disposes it. Make HardwareMonitorService.Dispose idempotent with a _disposed guard, and run provider disposal in a finally block in MainWindow_Closed so the ring0 driver is always unloaded on real exit even if an earlier teardown step throws. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../MainWindow.cs | 26 ++++++++++------- .../Services/HardwareMonitorService.cs | 6 ++++ .../ViewModels/MainWindowViewModel.cs | 29 ------------------- 3 files changed, 22 insertions(+), 39 deletions(-) diff --git a/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs b/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs index 26f2d47..0d61987 100644 --- a/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs +++ b/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs @@ -456,16 +456,22 @@ public sealed partial class MainWindow : Window } _isShuttingDown = true; - _timer.Stop(); - _columnMeasurer.StopSettleTimer(); - _trayIconService.Dispose(); - _secondaryWindows.CloseAll(); - _placementService.Save(); - - // The container owns the view model and domain services. Disposing the provider runs - // MainWindowViewModel.Dispose first (reverse construction order) to persist settings while the - // hardware monitor and web server are still alive, then disposes those services and the tracer. - (_serviceProvider as IDisposable)?.Dispose(); + try + { + _timer.Stop(); + _columnMeasurer.StopSettleTimer(); + _trayIconService.Dispose(); + _secondaryWindows.CloseAll(); + _placementService.Save(); + } + finally + { + // The container owns the view model and domain services. Disposing the provider runs + // MainWindowViewModel.Dispose first (reverse construction order) to persist settings while the + // hardware monitor and web server are still alive, then disposes those services and the tracer. + // This MUST run even if a teardown step above throws, so the ring0 driver is always unloaded. + (_serviceProvider as IDisposable)?.Dispose(); + } } private void AppWindow_Changed(AppWindow sender, AppWindowChangedEventArgs args) diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs b/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs index 95df3cb..739d4c3 100644 --- a/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs +++ b/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs @@ -28,6 +28,7 @@ public sealed class HardwareMonitorService : IHardwareMonitorService private readonly UpdateVisitor _updateVisitor = new(); private readonly TreeRebuildCoalescer _treeRebuildCoalescer; private bool _isOpen; + private bool _disposed; public HardwareMonitorService(AppSettings settings) { @@ -199,6 +200,11 @@ public sealed class HardwareMonitorService : IHardwareMonitorService public void Dispose() { + if (_disposed) + return; + + _disposed = true; + // Clear before closing so any tree-rebuild task still pending its delay bails instead of rebuilding from a // half-closed Computer. _isOpen = false; diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs index 3f6d081..8d7f2f4 100644 --- a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs @@ -90,7 +90,6 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable private readonly IRemoteWebServer _remoteWebServer; private readonly StartupService _startupService; private readonly IStartupTracer _startupTrace; - private readonly bool _ownsServices; private AppThemeMode _themeMode; private int _loggingIntervalIndex; private PlotLocation _plotLocation; @@ -141,32 +140,6 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable InitializeFromSettings(settings); } - // Transitional constructor that builds the domain graph the legacy way for callers that do not yet - // resolve the view model from the container. Removed once MainWindow is DI-resolved (di-app-invert). - internal MainWindowViewModel(AppSettings settings, IStartupTracer startupTrace) - { - Settings = settings; - _startupTrace = startupTrace; - _startupTrace.Mark("MainWindowViewModel.Constructor.Begin"); - _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); - HardwareMonitorService hardwareMonitor = new(settings); - _hardwareMonitor = hardwareMonitor; - _logger = new Logger(hardwareMonitor.Computer); - _sensorSelection = new SensorSelectionService(settings); - _remoteWebServer = new RemoteWebServer( - hardwareMonitor.Computer, - hardwareMonitor.SensorReadLock, - settings.GetValue("listenerIp", "?"), - settings.GetValue("listenerPort", 8085), - settings.GetValue("authenticationEnabled", false), - settings.GetValue("authenticationUserName", ""), - settings.GetValue("authenticationPassword", "")); - _plotTracking = new PlotTrackingService(); - _startupService = new StartupService(); - _ownsServices = true; - InitializeFromSettings(settings); - } - private void InitializeFromSettings(AppSettings settings) { _remoteWebServer.SetRootProvider(() => RootItems.FirstOrDefault()); @@ -789,8 +762,6 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable Settings.SetValue("authenticationUserName", AuthWebServerUserName); Settings.SetValue("authenticationPassword", _remoteWebServer.PasswordHash); _remoteWebServer.Quit(); - if (_ownsServices) - _hardwareMonitor.Dispose(); Save(); }