Marrt
Marrt

Reputation: 105

Leaking Handles when using Microsoft Remote Desktop Client

I am utilizing the AxMSTSCLib.dll to embed the MSTSC Client into a WPF-Control.

Everything is working as expected, but: Each successful "Connect-Disconnect" cycle, the Process.HandleCount increases and leaks memory: Leak in Action
They are never garbage collected in runtime. They leak only on successful Connections, so I suspect there must be some leftover File- or TCP-Stream but I still have a hard time finding, accessing and manipulating those unmanaged resources

Reproduction Code

To set up the stripped core of the project:

  1. Open Visual Studio and create a "WPF App (.Net Framework)" project named "RemoteClientHandleLeak"

  2. Add these 4 References:

    • Assemblies: System.Windows.Forms and WindowsFormsIntegration
    • Browse: AxMSTSCLib.dll and MSTSCLib.dll located at:
      C:\Program Files (x86)\Common Files\Microsoft Shared\Windows Simulator\14.0\AxMSTSCLib.dll
      ...and set their 'Embed Interop Types' property to False (this should set 'Copy Local' to True)
  3. Paste this into MainWindow.xaml:

<Window x:Class="RemoteClientHandleLeak.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Width="830" Height="535" >
    <Grid x:Name="LayoutRoot" Background="#cccdd1">
        <Button x:Name="Start" Content="Start" Click="Click_StartSequence" Margin="10,3,685,466" />
        <Button x:Name="Stop" Content="Stop" Click="Click_StopSequence" Margin="274,3,421,466" />
        <Button x:Name="Probe" Content="Probe Handles" Click="Click_Probe" Margin="496,3,199,466" />
        <Button x:Name="ForceGC" Content="Force GC" Click="Click_ForceGC" Margin="628,3,67,466" />
        <Button x:Name="StartAlt" Content="Start (no connect)" Click="Click_StartSequenceAlt" Margin="142,3,553,466" />
        <Grid x:Name="parentGrid" Width="800" Height="450" Margin="10,42,10,10"/>
    </Grid>
</Window>
  1. Paste this into MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Text;

using RdpClient = AxMSTSCLib.AxMsRdpClient7NotSafeForScripting;
using FormHost = System.Windows.Forms.Integration.WindowsFormsHost;

namespace RemoteClientHandleLeak
{
    public partial class MainWindow : Window
    {
        private string memLogPath = @"C:\Marrt\logWpf_RemoteDesktopClient.txt";
        private const string srv = "192.168.1.15", usr = "remote", pwd = "remote";
        private int connectionDurationMs = 200;

        private bool trackDisposalState = true;
        List<WeakReference> hostHistory = new List<WeakReference>();
        List<WeakReference> clientHistory = new List<WeakReference>();


        public async Task MySequence()
        {
            for (int i = 0; i < 10; i++)
            {
                var task1 = LeakDemonstration();
                await Task.WhenAll(task1);
                ProbeWeakRef();
                MemoryStamp();
                if (stop) { return; }
            }
        }

        public async Task LeakDemonstration()
        {
            using (var winFormHost = new FormHost())
            using (var axMsRdpClient = new RdpClient())
            {
                if(trackDisposalState){
                    hostHistory.Add(new WeakReference(winFormHost, false));
                    clientHistory.Add(new WeakReference(axMsRdpClient, false));
                }

                winFormHost.Child = axMsRdpClient;
                parentGrid.Children.Add(winFormHost);
                ApplyClientSetting(axMsRdpClient, (int)parentGrid.ActualWidth, (int)parentGrid.ActualHeight);

                if (!skipConnect){
                    //LEAKING HANDLES!
                    axMsRdpClient.Connect();
                    await Task.Delay(connectionDurationMs);
                    axMsRdpClient.Disconnect();
                } else {
                    //no leak, perfectly clean disposal
                    await Task.Delay(connectionDurationMs);
                }
                await Task.Delay(120);

                parentGrid.Children.Clear();// same as parentGrid.Children.Remove(winFormHost);
            }
        }


        private void ApplyClientSetting(RdpClient client, int w, int h)
        {
            //client.Domain = "";
            client.Server = srv;
            client.UserName = usr;
            client.AdvancedSettings2.ClearTextPassword = pwd;
            client.AdvancedSettings7.EnableCredSspSupport = true;

            //client.ColorDepth = 16;
            client.DesktopWidth = w;
            client.DesktopHeight = h;

            client.ConnectingText = "Connecting...";
            client.ConnectedStatusText = "Connected";
            client.DisconnectedText = "Offline";
        }

        #region  Helper & UI
        private void ProbeWeakRef()
        {
            int hCount = hostHistory.Count, cCount = clientHistory.Count, hAlive = 0, cAlive = 0;
            for (int i = 0; i < hostHistory.Count; i++)
            {
                hAlive += hostHistory[i].IsAlive ? 1 : 0;
                cAlive += hostHistory[i].IsAlive ? 1 : 0;
            }

            using (var process = System.Diagnostics.Process.GetCurrentProcess())
            {
                Console.WriteLine($"WeakRefs: host.IsAlive:{hAlive}/{hCount} client.IsAlive:{cAlive}/{cCount}; HandleCount:{process.HandleCount}");
            }
        }

        public void MemoryStamp()
        {
            using (var process = System.Diagnostics.Process.GetCurrentProcess())
            {
                StringBuilder sb = new StringBuilder();
                sb.Append($"{ DateTime.Now.ToString("yyyy-MM-dd\tHH:mm:ss.ffff")}\tMemory\t");
                sb.Append($"{process.PeakWorkingSet64}\t{process.PrivateMemorySize64}\t{process.WorkingSet64}\t{GC.GetTotalMemory(true).ToString("#.,###")}\t{process.HandleCount}\n");
                System.IO.File.AppendAllText( memLogPath, sb.ToString());   //Console.WriteLine(sb.ToString());
                sb.Clear();
            }
        }

        private void ForceGarbageCollection()
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            ProbeWeakRef();
        }

        public MainWindow() { InitializeComponent(); }
        private bool stop;
        private bool skipConnect;

        private void Click_StartSequence(object sender, RoutedEventArgs e) { stop = false; skipConnect = false; Task seq = MySequence(); }
        private void Click_StartSequenceAlt(object sender, RoutedEventArgs e) { stop = false; skipConnect = true; Task seq = MySequence(); }
        private void Click_StopSequence(object sender, RoutedEventArgs e) { stop = true; }
        private void Click_Probe(object sender, RoutedEventArgs e) { ProbeWeakRef(); }
        private void Click_ForceGC(object sender, RoutedEventArgs e) { ForceGarbageCollection(); }

        #endregion  Helper & UI
    }
}
  1. Modify the connection settings (host must be reachable and login valid for leaks to occur) Connection Settings
  2. Finally, Press Start (=10 Connect-Disconnect Cycles) and observe them Handles leaking
    Button
    Handles


I have tried countless things in the last weeks, all have those leaking handles:

Upvotes: 2

Views: 503

Answers (1)

Marrt
Marrt

Reputation: 105

As outlined in the comments below the original question:
This is demonstrably a Bug in Microsoft's MSTSC Client.

I have set up a Win8.1x64 system with and without the hotfix (hinted at in this Microsoft article) ...and these are the graphs for the connection cycles:
Graph That means in Win8.1+Hotfix there is no leak!

So how can i get this Hotfix into Win10?
-> I tried using the fixed mtscax.dll which extracted from the Win8.1 Hotfix within Win10, but that seems fishy. Additionally, while my tests in Win10 have no Handle Leaks, they are leaking GDI-Handles now (which is worse). So there must be some incompatibility.

Upvotes: 0

Related Questions