Monday, October 24, 2005

.NET memory leaks with event handlers

One of the biggest pains (and biggest benefits) of working with C++ is memory management. It is quite easy to create a memory leak and C++ and often difficult to later discover where the leak originated.

One of the biggest benefits of C# is that memory management is handled for the developer (through garbage collection). Well…. not so fast. I’ve discovered a serious memory leak within the .NET MenuItem that is quite easy to duplicate. Most applications do not create and destroy too many windows so a small leak here is not a big deal. However, the application I have been working on uses hundreds of windows! Here are the details…

When we create forms in .NET and double-click on buttons on textboxes, Visual Studio will automatically stub out the event handlers within the ‘InitializeComponent’ method. If you take a look at this method you’ll see several event handlers ‘wired’ up through the ‘+=’ syntax. However, you don’t see any corresponding ‘-=’ syntax to disconnect the event. For the most part, that’s ok because the CLR takes care of cleaning up and destroying the form and its event handlers. There is 1 exception…. MenuItems.

The problem:
I’ve discovered that any event handlers that .NET automatically generates for MenuItems result in memory leaks.

The solution:
To fix this memory leak you must implement Dispose and explicitly release these MenuItem event handlers through the ‘-=’ syntax.

Also, any event handlers you setup on your own (outside of InitializeComponent) must also be explicitly released.

However, keep in mind that there is a specific way to implement a destructor in .NET as you don’t want to the garbage collector to call ‘Dispose’ twice. An example follows (our search MSDN):

public class Foo: IDisposable
{
public Foo()
{
}

~Foo()
{
this.Dispose();
}

public void Dispose()
{
// TODO : Release all EventHandlers

GC.SuppressFinalize(this);
}
}

NOTE: I used ‘.NET Memory Profiler’ to help aid in finding the cause of these memory leaks. It is a great tool for quickly isolating these kinds of problems.

Friday, October 21, 2005

Detecting Proxy Servers using the .NET Framework

I think Microsoft really missed the mark with their web service implementation in the 1.1 Framework. Using the framework it is impossible to develop a web service application which can work reliably. This is due to the fact that the .NET Framework is unable to detect dynamic web proxy settings! The use of proxy servers is a common practice among many companies. In order to work around this limitation, I have few strategies to share.



Manual Proxy Server

If this is the scenario you are trying to support, you're in luck. Below is a screen shot of how this setting looks in IE.







Thankfully this one simply requires a modification of your application configuration file. Just add a 'proxy usessystemdefault="true"' element to the 'defaultproxy' section within the 'system.net' section of your app.config file.

With this modification, your web service calls will automatically detect these proxy server settings and utilize them.


Auto Detect and Configuration Scripts

This is where the pain begins. Basically, you must make use of some unmanaged functions to detect the proxy settings as the modification to the app.config file shown above does not work. The calls you should study include:


WinHttpGetIEProxyConfigForCurrentUser: as the name implies it will retrieve the current settings as set in the IE 'LAN Settings' dialog shown above.


WinHttpGetProxyForUrl: given a Url, this call will make use of the 'Auto Detect' proxy server settings and return the correct proxy server for the given Url. A WINHTTP_AUTOPROXY_OPTIONS

structure is passed into this call and should be configured based on the current user's IE settings


Here is a simple class that will return the valid proxy servers given a Url (NOTE: Some of the code below was obtained from here):


using System;
using System.Net;
using System.Runtime.InteropServices;
using System.Collections;

namespace MyProxyHelper
{
public class ProxyHelper
{
// Constant declarations
const int WINHTTP_ACCESS_TYPE_DEFAULT_PROXY = 0;
const int WINHTTP_ACCESS_TYPE_NO_PROXY = 1;
const int WINHTTP_ACCESS_TYPE_NAMED_PROXY = 3;

const int WINHTTP_AUTOPROXY_AUTO_DETECT = 0x00000001;
const int WINHTTP_AUTOPROXY_CONFIG_URL = 0x00000002;
const int WINHTTP_AUTOPROXY_RUN_INPROCESS = 0x00010000;
const int WINHTTP_AUTOPROXY_RUN_OUTPROCESS_ONLY = 0x00020000;

const int WINHTTP_AUTO_DETECT_TYPE_DHCP = 0x00000001;
const int WINHTTP_AUTO_DETECT_TYPE_DNS_A = 0x00000002;

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
struct WINHTTP_AUTOPROXY_OPTIONS
{
[MarshalAs(UnmanagedType.U4)]
public int dwFlags;
[MarshalAs(UnmanagedType.U4)]
public int dwAutoDetectFlags;
public string lpszAutoConfigUrl;
public IntPtr lpvReserved;
[MarshalAs(UnmanagedType.U4)]
public int dwReserved;
public bool fAutoLoginIfChallenged;
}

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
struct WINHTTP_PROXY_INFO
{
[MarshalAs(UnmanagedType.U4)]
public int dwAccessType;
public IntPtr pwszProxy;
public IntPtr pwszProxyBypass;
}

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
struct WINHTTP_CURRENT_USER_IE_PROXY_CONFIG
{
public bool fAutoDetect;
public IntPtr lpszAutoConfigUrl;
public IntPtr lpszProxy;
public IntPtr lpszProxyBypass;
}

[DllImport("winhttp.dll", SetLastError=true, CharSet=CharSet.Unicode)]
static extern IntPtr WinHttpOpen(
string pwszUserAgent,
int dwAccessType,
IntPtr pwszProxyName,
IntPtr pwszProxyBypass,
int dwFlags
);

[DllImport("winhttp.dll", SetLastError=true, CharSet=CharSet.Unicode)]
static extern bool WinHttpCloseHandle(IntPtr hInternet);

[DllImport("winhttp.dll", SetLastError=true, CharSet=CharSet.Unicode)]
static extern bool WinHttpGetProxyForUrl(
IntPtr hSession,
string lpcwszUrl,
ref WINHTTP_AUTOPROXY_OPTIONS pAutoProxyOptions,
ref WINHTTP_PROXY_INFO pProxyInfo
);

[DllImport("winhttp.dll", SetLastError=true, CharSet=CharSet.Unicode)]
static extern bool WinHttpGetIEProxyConfigForCurrentUser(
ref WINHTTP_CURRENT_USER_IE_PROXY_CONFIG pProxyConfig
);

public ProxyHelper()
{
}

public IWebProxy[] DetermineProxysForUrl(string targetUrl)
{
// Open a sesssion
IntPtr hSession = WinHttpOpen("ProxyHelper", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, IntPtr.Zero, IntPtr.Zero, 0);

WebProxy[] webProxys = null;

try
{
int errorCode = 0;

// First detect the current IE settings
bool autoDetect = false;
string configURL = null;
WINHTTP_CURRENT_USER_IE_PROXY_CONFIG ieProxyConfig = new WINHTTP_CURRENT_USER_IE_PROXY_CONFIG();
if(WinHttpGetIEProxyConfigForCurrentUser(ref ieProxyConfig))
{
autoDetect = ieProxyConfig.fAutoDetect;

if(ieProxyConfig.lpszAutoConfigUrl != IntPtr.Zero)
{
configURL = Marshal.PtrToStringUni(ieProxyConfig.lpszAutoConfigUrl);
Marshal.FreeHGlobal(ieProxyConfig.lpszAutoConfigUrl);
}

if(ieProxyConfig.lpszProxy != IntPtr.Zero)
Marshal.FreeHGlobal(ieProxyConfig.lpszProxy);
if(ieProxyConfig.lpszProxyBypass != IntPtr.Zero)
Marshal.FreeHGlobal(ieProxyConfig.lpszProxyBypass);
}

// Initialize the structs
WINHTTP_AUTOPROXY_OPTIONS autoProxyOptions = new WINHTTP_AUTOPROXY_OPTIONS();
WINHTTP_PROXY_INFO proxyInfo = new WINHTTP_PROXY_INFO();
proxyInfo.pwszProxy = proxyInfo.pwszProxyBypass = IntPtr.Zero;

bool result = false;
if(autoDetect)
{
// IE settings are to autodetect
autoProxyOptions.dwFlags = WINHTTP_AUTOPROXY_AUTO_DETECT;
autoProxyOptions.dwAutoDetectFlags = (WINHTTP_AUTO_DETECT_TYPE_DHCPWINHTTP_AUTO_DETECT_TYPE_DNS_A);
autoProxyOptions.fAutoLoginIfChallenged = true;

//Get proxy
result = WinHttpGetProxyForUrl(hSession, targetUrl, ref autoProxyOptions, ref proxyInfo);
if(!result)
errorCode = Marshal.GetLastWin32Error();
}

if(autoDetect == false (errorCode != 0 && configURL != null))
{
// Using a configured PAC file so setup structs
autoProxyOptions.dwFlags = WINHTTP_AUTOPROXY_CONFIG_URL;
autoProxyOptions.lpszAutoConfigUrl = configURL;
autoProxyOptions.dwAutoDetectFlags = 0;

//Get proxy
result = WinHttpGetProxyForUrl(hSession, targetUrl, ref autoProxyOptions, ref proxyInfo);
if(!result)
errorCode = Marshal.GetLastWin32Error();
else
errorCode = 0;
}

// If we have success, get the proxy info
if(result)
{
if(proxyInfo.pwszProxy != IntPtr.Zero)
{
string proxyUrl = Marshal.PtrToStringUni(proxyInfo.pwszProxy);
Marshal.FreeHGlobal(proxyInfo.pwszProxy);

// split returned proxies into array
string[] theUrls = proxyUrl.Split(';');

// use only first one, get rid of "PROXY " prefix and whitespace
webProxys = new WebProxy[theUrls.Length];
for(int i = 0; i < theUrls.Length; i++)
{
string curProxy = theUrls[i].Replace("PROXY ", "").Trim();
webProxys.SetValue(new WebProxy(curProxy), i);
}
}
if(proxyInfo.pwszProxyBypass != IntPtr.Zero)
Marshal.FreeHGlobal(proxyInfo.pwszProxyBypass);
}
}
finally
{
if(hSession != IntPtr.Zero)
WinHttpCloseHandle(hSession);
}

return webProxys;
}
}
}