You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Microsoft远程桌面客户端时出现句柄泄漏问题求助

AxMSTSCLib.dll嵌入WPF时的句柄泄漏问题

我正在使用AxMSTSCLib.dll将MSTSC客户端嵌入WPF控件,功能运行正常,但存在句柄泄漏问题:每次成功执行Connect-Disconnect循环后,Process.HandleCount会持续增长,且这些句柄无法在运行时被垃圾回收。仅在成功建立连接时才会发生泄漏,因此推测存在残留的文件句柄或TCP流,但难以定位、访问和操作这些非托管资源。

复现代码

搭建核心项目步骤:

  1. 打开Visual Studio,创建名为“RemoteClientHandleLeak”的WPF App (.Net Framework)项目
  2. 添加4个引用:
    • 程序集:System.Windows.Forms和WindowsFormsIntegration
    • 浏览引用:位于C:\Program Files (x86)\Common Files\Microsoft Shared\Windows Simulator\14.0\AxMSTSCLib.dll的AxMSTSCLib.dll和MSTSCLib.dll,并将它们的“Embed Interop Types”属性设置为False(这会自动将“Copy Local”设为True)
  3. 将以下代码粘贴到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. 将以下代码粘贴到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. 修改连接设置(主机需可访问且登录信息有效才会出现泄漏)
  2. 点击Start按钮(执行10次Connect-Disconnect循环),观察句柄泄漏情况

已尝试的无效解决方案

  • 纯WinForm项目(无异步自动化函数)
  • 调整多种AdvancedSettings、AdvancedSettings2-7配置
  • 使用Visual Studio内存分析器对比快照(提示存在TCP流句柄)
  • 手动调用Dispose释放资源
  • 尝试关闭句柄
  • 对多个对象调用Marshal.ReleaseComObject(Object)
  • 尝试Object.Finalize()模式(不确定实现是否正确)
  • 在rdp.GetOcx()上调用ResourceManager.ReleaseAllResources()
  • 连接其他操作系统(Unix上的xrdp)
  • 查阅相关官方文档
  • 尚未尝试AppDomain重载

环境:Windows 10,.Net Framework 4.7.2

内容的提问来源于stack exchange,提问作者Marrt

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 00:12:06