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>
This commit is contained in:
2026-06-02 15:25:39 -05:00
co-authored by Copilot
parent dcb6a5b803
commit 5aa7c30a6a
3 changed files with 22 additions and 39 deletions
@@ -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)
@@ -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;
@@ -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();
}