From 949ec02177a125b21c27fa0ed3992767cb13c2c0 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Sat, 30 May 2026 01:35:02 +0200 Subject: [PATCH 1/3] Fix: Possible Dispatcher.ShutdownStarted handler memory leak in Dragablz Tabs --- .../NETworkManager/Controls/PowerShellControl.xaml.cs | 4 ++++ Source/NETworkManager/Controls/PuTTYControl.xaml.cs | 4 ++++ .../Controls/RemoteDesktopControl.xaml.cs | 4 ++++ .../NETworkManager/Controls/TigerVNCControl.xaml.cs | 4 ++++ .../NETworkManager/Controls/WebConsoleControl.xaml.cs | 11 +++++++++++ Source/NETworkManager/Views/DNSLookupView.xaml.cs | 4 ++++ Source/NETworkManager/Views/IPGeolocationView.xaml.cs | 4 ++++ Source/NETworkManager/Views/IPScannerView.xaml.cs | 4 ++++ Source/NETworkManager/Views/PortScannerView.xaml.cs | 4 ++++ Source/NETworkManager/Views/SNMPView.xaml.cs | 4 ++++ Source/NETworkManager/Views/SNTPLookupView.xaml.cs | 4 ++++ Source/NETworkManager/Views/TracerouteView.xaml.cs | 4 ++++ Source/NETworkManager/Views/WhoisView.xaml.cs | 4 ++++ 13 files changed, 59 insertions(+) diff --git a/Source/NETworkManager/Controls/PowerShellControl.xaml.cs b/Source/NETworkManager/Controls/PowerShellControl.xaml.cs index 099dffd42f..7c30d75e7a 100644 --- a/Source/NETworkManager/Controls/PowerShellControl.xaml.cs +++ b/Source/NETworkManager/Controls/PowerShellControl.xaml.cs @@ -259,6 +259,10 @@ public void CloseTab() _closed = true; + // Detach the app-lifetime handler so this transient tab control can be collected + // after the tab is closed (the process itself is released by Disconnect()). + Dispatcher.ShutdownStarted -= Dispatcher_ShutdownStarted; + // Disconnect the session Disconnect(); diff --git a/Source/NETworkManager/Controls/PuTTYControl.xaml.cs b/Source/NETworkManager/Controls/PuTTYControl.xaml.cs index 922f77dcaf..71e3910cd6 100644 --- a/Source/NETworkManager/Controls/PuTTYControl.xaml.cs +++ b/Source/NETworkManager/Controls/PuTTYControl.xaml.cs @@ -280,6 +280,10 @@ public void CloseTab() _closed = true; + // Detach the app-lifetime handler so this transient tab control can be collected + // after the tab is closed (the process itself is released by Disconnect()). + Dispatcher.ShutdownStarted -= Dispatcher_ShutdownStarted; + // Disconnect the session Disconnect(); diff --git a/Source/NETworkManager/Controls/RemoteDesktopControl.xaml.cs b/Source/NETworkManager/Controls/RemoteDesktopControl.xaml.cs index 065fa15ae6..3bc4548a93 100644 --- a/Source/NETworkManager/Controls/RemoteDesktopControl.xaml.cs +++ b/Source/NETworkManager/Controls/RemoteDesktopControl.xaml.cs @@ -517,6 +517,10 @@ public void CloseTab() _closed = true; + // Detach the app-lifetime handler so this transient tab control can be collected + // after the tab is closed (the RDP session is released by Disconnect()). + Dispatcher.ShutdownStarted -= Dispatcher_ShutdownStarted; + // Disconnect the session Disconnect(); diff --git a/Source/NETworkManager/Controls/TigerVNCControl.xaml.cs b/Source/NETworkManager/Controls/TigerVNCControl.xaml.cs index 863ab057a3..885eacfa88 100644 --- a/Source/NETworkManager/Controls/TigerVNCControl.xaml.cs +++ b/Source/NETworkManager/Controls/TigerVNCControl.xaml.cs @@ -244,6 +244,10 @@ public void CloseTab() _closed = true; + // Detach the app-lifetime handler so this transient tab control can be collected + // after the tab is closed (the process itself is released by Disconnect()). + Dispatcher.ShutdownStarted -= Dispatcher_ShutdownStarted; + // Disconnect the session Disconnect(); diff --git a/Source/NETworkManager/Controls/WebConsoleControl.xaml.cs b/Source/NETworkManager/Controls/WebConsoleControl.xaml.cs index 11fe885b3d..90e528f3a5 100644 --- a/Source/NETworkManager/Controls/WebConsoleControl.xaml.cs +++ b/Source/NETworkManager/Controls/WebConsoleControl.xaml.cs @@ -199,6 +199,17 @@ public void CloseTab() _closed = true; + // Release the subscriptions that would otherwise keep this transient per-tab + // control (and its heavyweight WebView2 instance) alive for the lifetime of the + // application, then dispose the browser to free the native resources. + SettingsManager.Current.PropertyChanged -= Current_PropertyChanged; + Dispatcher.ShutdownStarted -= Dispatcher_ShutdownStarted; + + Browser.NavigationStarting -= Browser2_NavigationStarting; + Browser.NavigationCompleted -= Browser2_NavigationCompleted; + Browser.SourceChanged -= Browser2_SourceChanged; + Browser.Dispose(); + ConfigurationManager.Current.WebConsoleTabCount--; } diff --git a/Source/NETworkManager/Views/DNSLookupView.xaml.cs b/Source/NETworkManager/Views/DNSLookupView.xaml.cs index 310f8dae15..b00f030485 100644 --- a/Source/NETworkManager/Views/DNSLookupView.xaml.cs +++ b/Source/NETworkManager/Views/DNSLookupView.xaml.cs @@ -23,6 +23,10 @@ public DNSLookupView(Guid tabId, string host = null) public void CloseTab() { + // Detach the app-lifetime handler so this transient tab view (and its view model) + // can be collected after the tab is closed. + Dispatcher.ShutdownStarted -= Dispatcher_ShutdownStarted; + _viewModel.OnClose(); } diff --git a/Source/NETworkManager/Views/IPGeolocationView.xaml.cs b/Source/NETworkManager/Views/IPGeolocationView.xaml.cs index 65a27a24ae..daf31b4822 100644 --- a/Source/NETworkManager/Views/IPGeolocationView.xaml.cs +++ b/Source/NETworkManager/Views/IPGeolocationView.xaml.cs @@ -22,6 +22,10 @@ public IPGeolocationView(Guid tabId, string domain = null) public void CloseTab() { + // Detach the app-lifetime handler so this transient tab view (and its view model) + // can be collected after the tab is closed. + Dispatcher.ShutdownStarted -= Dispatcher_ShutdownStarted; + _viewModel.OnClose(); } diff --git a/Source/NETworkManager/Views/IPScannerView.xaml.cs b/Source/NETworkManager/Views/IPScannerView.xaml.cs index 82991e2132..322727dc1a 100644 --- a/Source/NETworkManager/Views/IPScannerView.xaml.cs +++ b/Source/NETworkManager/Views/IPScannerView.xaml.cs @@ -30,6 +30,10 @@ public IPScannerView(Guid tabId, string hostOrIPRange = null) public void CloseTab() { + // Detach the app-lifetime handler so this transient tab view (and its view model) + // can be collected after the tab is closed. + Dispatcher.ShutdownStarted -= Dispatcher_ShutdownStarted; + _viewModel.OnClose(); } diff --git a/Source/NETworkManager/Views/PortScannerView.xaml.cs b/Source/NETworkManager/Views/PortScannerView.xaml.cs index 1ce0d1ce6f..4daa36e746 100644 --- a/Source/NETworkManager/Views/PortScannerView.xaml.cs +++ b/Source/NETworkManager/Views/PortScannerView.xaml.cs @@ -23,6 +23,10 @@ public PortScannerView(Guid tabId, string host = null, string ports = null) public void CloseTab() { + // Detach the app-lifetime handler so this transient tab view (and its view model) + // can be collected after the tab is closed. + Dispatcher.ShutdownStarted -= Dispatcher_ShutdownStarted; + _viewModel.OnClose(); } diff --git a/Source/NETworkManager/Views/SNMPView.xaml.cs b/Source/NETworkManager/Views/SNMPView.xaml.cs index 53030bd96c..78095d421c 100644 --- a/Source/NETworkManager/Views/SNMPView.xaml.cs +++ b/Source/NETworkManager/Views/SNMPView.xaml.cs @@ -28,6 +28,10 @@ public SNMPView(Guid tabId, SNMPSessionInfo sessionInfo) public void CloseTab() { + // Detach the app-lifetime handler so this transient tab view (and its view model) + // can be collected after the tab is closed. + Dispatcher.ShutdownStarted -= Dispatcher_ShutdownStarted; + _viewModel.OnClose(); } diff --git a/Source/NETworkManager/Views/SNTPLookupView.xaml.cs b/Source/NETworkManager/Views/SNTPLookupView.xaml.cs index 6d723e7b07..d5eba290b6 100644 --- a/Source/NETworkManager/Views/SNTPLookupView.xaml.cs +++ b/Source/NETworkManager/Views/SNTPLookupView.xaml.cs @@ -23,6 +23,10 @@ public SNTPLookupView(Guid tabId) public void CloseTab() { + // Detach the app-lifetime handler so this transient tab view (and its view model) + // can be collected after the tab is closed. + Dispatcher.ShutdownStarted -= Dispatcher_ShutdownStarted; + _viewModel.OnClose(); } diff --git a/Source/NETworkManager/Views/TracerouteView.xaml.cs b/Source/NETworkManager/Views/TracerouteView.xaml.cs index fcfe099b73..e842e3ed9e 100644 --- a/Source/NETworkManager/Views/TracerouteView.xaml.cs +++ b/Source/NETworkManager/Views/TracerouteView.xaml.cs @@ -23,6 +23,10 @@ public TracerouteView(Guid tabId, string host = null) public void CloseTab() { + // Detach the app-lifetime handler so this transient tab view (and its view model) + // can be collected after the tab is closed. + Dispatcher.ShutdownStarted -= Dispatcher_ShutdownStarted; + _viewModel.OnClose(); } diff --git a/Source/NETworkManager/Views/WhoisView.xaml.cs b/Source/NETworkManager/Views/WhoisView.xaml.cs index 0235fddfb7..de23752f5a 100644 --- a/Source/NETworkManager/Views/WhoisView.xaml.cs +++ b/Source/NETworkManager/Views/WhoisView.xaml.cs @@ -22,6 +22,10 @@ public WhoisView(Guid tabId, string domain = null) public void CloseTab() { + // Detach the app-lifetime handler so this transient tab view (and its view model) + // can be collected after the tab is closed. + Dispatcher.ShutdownStarted -= Dispatcher_ShutdownStarted; + _viewModel.OnClose(); } From a3c4a4aefe3ff825d0fa9739cf2261cd742e9c91 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Sat, 30 May 2026 01:39:40 +0200 Subject: [PATCH 2/3] Docs: #3454 --- Website/docs/changelog/next-release.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index d384d45df3..56d72f43c2 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -118,6 +118,7 @@ Release date: **xx.xx.2025** - Migrated from `LiveCharts` to `LiveCharts2` (`LiveChartsCore.SkiaSharpView.WPF`) for chart rendering. [#3449](https://github.com/BornToBeRoot/NETworkManager/pull/3449) - Fixed `CancellationTokenSource` leak in `IPScanner`, `PortScanner`, `Traceroute`, `PingMonitor`, `PingMonitorHost` and `SNMP` ViewModels. The previous instance was never disposed before being overwritten on each run, leaking the underlying `WaitHandle`. [#3448](https://github.com/BornToBeRoot/NETworkManager/pull/3448) +- Fixed a `Dispatcher.ShutdownStarted` handler leak in the Dragablz tab items (PowerShell, PuTTY, TigerVNC, Remote Desktop and Web Console controls, plus the IP Scanner, Port Scanner, Traceroute, DNS Lookup, IP Geolocation, SNMP, SNTP Lookup and Whois views). The handler was subscribed in the constructor but never removed, keeping each closed tab (view and view model) alive until the application exited. It is now unsubscribed in `CloseTab()`; the Web Console additionally disposes its WebView2 instance. [#3454](https://github.com/BornToBeRoot/NETworkManager/pull/3454) - Replace fire-and-forget `.ConfigureAwait(false)` calls with explicit discard assignments (`_ = SomeAsyncOperation()`) across command handlers, startup/load paths and profile callbacks. [#3441](https://github.com/BornToBeRoot/NETworkManager/pull/3441) - Code cleanup & refactoring - Language files updated via [#transifex](https://github.com/BornToBeRoot/NETworkManager/pulls?q=author%3Aapp%2Ftransifex-integration) From 778b736de186d9d19a0401395ea9d86957855c27 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 23:43:16 +0000 Subject: [PATCH 3/3] Fix WebConsoleControl load/close disposal race --- .../Controls/WebConsoleControl.xaml.cs | 47 ++++++++++++++----- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/Source/NETworkManager/Controls/WebConsoleControl.xaml.cs b/Source/NETworkManager/Controls/WebConsoleControl.xaml.cs index 90e528f3a5..5b993a8b1f 100644 --- a/Source/NETworkManager/Controls/WebConsoleControl.xaml.cs +++ b/Source/NETworkManager/Controls/WebConsoleControl.xaml.cs @@ -4,6 +4,7 @@ using NETworkManager.Settings; using NETworkManager.Utilities; using System; +using System.Threading; using System.Windows; using System.Windows.Input; @@ -17,6 +18,7 @@ public partial class WebConsoleControl : UserControlBase, IDragablzTabItem private bool _initialized; private bool _closed; + private readonly CancellationTokenSource _loadCancellationTokenSource = new(); private readonly Guid _tabId; private readonly WebConsoleSessionInfo _sessionInfo; @@ -92,25 +94,44 @@ private void Browser2_SourceChanged(object sender, CoreWebView2SourceChangedEven private async void UserControl_Loaded(object sender, RoutedEventArgs e) { // Connect after the control is drawn and only on the first init - if (_initialized) + if (_initialized || _closed || _loadCancellationTokenSource.IsCancellationRequested) return; - // Set user data folder - Fix #382 - var webView2Environment = - await CoreWebView2Environment.CreateAsync(null, GlobalStaticConfiguration.WebConsole_Cache); + try + { + // Set user data folder - Fix #382 + var webView2Environment = + await CoreWebView2Environment.CreateAsync(null, GlobalStaticConfiguration.WebConsole_Cache); + + if (_closed || _loadCancellationTokenSource.IsCancellationRequested) + return; - await Browser.EnsureCoreWebView2Async(webView2Environment); + await Browser.EnsureCoreWebView2Async(webView2Environment); + + if (_closed || _loadCancellationTokenSource.IsCancellationRequested || Browser.CoreWebView2 == null) + return; - Log.Debug($"UserControl_Loaded - WebView2 profile path: {Browser.CoreWebView2.Profile.ProfilePath}"); + Log.Debug($"UserControl_Loaded - WebView2 profile path: {Browser.CoreWebView2.Profile.ProfilePath}"); - // Set the default settings - Browser.CoreWebView2.Settings.IsStatusBarEnabled = SettingsManager.Current.WebConsole_IsStatusBarEnabled; - Browser.CoreWebView2.Settings.IsPasswordAutosaveEnabled = - SettingsManager.Current.WebConsole_IsPasswordSaveEnabled; + // Set the default settings + Browser.CoreWebView2.Settings.IsStatusBarEnabled = SettingsManager.Current.WebConsole_IsStatusBarEnabled; + Browser.CoreWebView2.Settings.IsPasswordAutosaveEnabled = + SettingsManager.Current.WebConsole_IsPasswordSaveEnabled; - Navigate(_sessionInfo.Url); + Navigate(_sessionInfo.Url); - _initialized = true; + _initialized = true; + } + catch (ObjectDisposedException) + { + if (!_closed && !_loadCancellationTokenSource.IsCancellationRequested) + throw; + } + catch (InvalidOperationException) + { + if (!_closed && !_loadCancellationTokenSource.IsCancellationRequested) + throw; + } } #endregion @@ -198,6 +219,8 @@ public void CloseTab() return; _closed = true; + _loadCancellationTokenSource.Cancel(); + _loadCancellationTokenSource.Dispose(); // Release the subscriptions that would otherwise keep this transient per-tab // control (and its heavyweight WebView2 instance) alive for the lifetime of the