Archiv für den Monat: November 2005

Mit .NET den Fuss wegschiessen und es nicht bemerken

Mit C war es einfach, sich selber in den Fuss zu schiessen. Durch C++ wurde es erschwert, aber nicht verhindert, und es hatte schwerwiegendere Konsequenzen.

Mit C# / .NET ist es noch schwieriger, sich selbst in den Fuss zu schiessen; und falls man es tut, merkt man es noch nicht einmal.

So habe ich heute ein Problem behoben, bei welchem der Speicherverbrauch einer Anwendung (eines Services, um genauer zu sein) stetig zunahm. Natürlich habe ich als erstes überprüft, ob irgend ein Array immer mehr Objekte beinhaltet. Aber erst, als ich mit einer Testversion des ANTS Profiler von Red Gate Software den Speicherverbrauch überprüft hatte, entdeckte ich das Problem:

Ich erzeugte ein Objekt, welches einen Timer besitzt. Die Referenz auf dieses Objekt speichere ich in einem Array. Wenn das Objekt nicht mehr benötigt wird, lösche ich das Objekt aus dem Array. Es existiert keine Referenz mehr auf dieses Objekt, somit kann der Garbage Collector das Objekt aus dem Speicher entfernen.

Oder auch nicht …

Da das Objekt einen Timer enthält und sich beim Timer mit einem Eventhandler angemeldet hat, referenzieren sich das Objekt und der Timer gegenseitig. Der Garbage Collector kann somit weder Timer noch Objekt löschen und sie schwirren bis in alle Ewigkeit (bzw. bis zum Programmende), losgelöst von allen anderen Objekten, im Speicher umher.

Solche oder ähnliche Probleme entdeckte man unter Visual C++ um einiges schneller, da die Entwicklungsumgebung nicht freigegebene Objekte reklamierte, wenn man die Anwendung im Debug Modus testete. Mit dem Garbage Collector ist diese Kontrollinstanz entfernt worden, was mich in diesem Fall einiges an Zeit gekostet hat.

Ich könnte mir vorstellen, dass noch in einigen .NET-Anwendungen solche Speicherfresser vorhanden sind. Wenn es normale Desktop-Anwendungen sind, wird dies auch nicht so bald auffallen, da diese Anwendungen normalerweise nicht über mehrere Tage laufen.

Es wäre schön, wenn man, falls man dies möchte, den Garbage Collector ausschalten könnte und den Speicher wieder selber kontrollieren könnte, da man nur so sicher sein kann, dass auch alle Objekte wieder gelöscht werden. Ob mein Wunsch in einer der nächsten .NET-Framework-Versionen wohl erfüllt wird? Wahrscheinlich eher nicht.

Somit werden wir wohl mit ungeladenen Waffen umherlaufen müssen und immer wieder kontrollieren, ob wir nicht ein Loch in unserem Fuss haben…

Workaround bei RAS via .NET unter Windows Server 2003

Für eine .NET-Anwendung musste ich via Modem eine Internet-Verbindung aufbauen. Dabei wird das Password Authentication Protocol (PAP) verwendet. Da in der Version 1.1 des .NET-Frameworks keine RAS-Unterstützung vorhenden ist, schrieb ich eine .NET-Wrapper-Klasse, die den Zugriff auf die benötigten Funktionen des RASAPI (rasapi32.dll) ermöglicht.
Die Anwendung funktionierte auf meinem Entwicklungssystem (Windows 2000 Server) ohne Probleme. Als die Anwendung dann auf dem Einsatzsystem unter Windows 2003 Server getestet wurde, klappte die Einwahl nicht. Untersuchungen mit dem PortMon von sysinternals ergaben, dass alles funktionierte, aber bei der Authentifizierung zwar der Benutzername, nicht aber das Passwort übermittelt wurde.
Verschiedene Versuche mit .NET und C++-Programmen zeigten dann, dass die Einwahl mittels RASAPI mit einem C++-Programm funktioniert, mit einem .NET-Programm das Passwort aber jedesmal fehlte.
Recherchen im Internet ergaben keine Lösung, nicht einmal ein vorheriges Auftreten dieses Problems konnte ich finden.
Um dieses Problem in den Griff zu bekommen, schrieb ich eine kleine DLL, welche die RASAPI-Aufrufe kapselt. Die Funktionen dieser DLL sehen wie folgt aus:


#define DLLEXPORT extern "C" __declspec(dllexport)
DLLEXPORT DWORD Dial(
    LPCSTR connectionName,
    LPCSTR number,
    LPCSTR username,
    LPCSTR password,
    LPVOID callback,
    LPHRASCONN hRas)
 {
    RASDIALEXTENSIONS rasDialExtensions;
    rasDialExtensions.dwSize = sizeof(RASDIALEXTENSIONS);
    rasDialExtensions.hwndParent = NULL;
    rasDialExtensions.reserved = 0;
    rasDialExtensions.dwfOptions = 0;
    RASDIALPARAMS rasDialParams;
    rasDialParams.dwSize = sizeof(RASDIALPARAMS);
    strcpy(rasDialParams.szPhoneNumber, number);
    strcpy(rasDialParams.szCallbackNumber, "");
    strcpy(rasDialParams.szDomain, "");
    strcpy(rasDialParams.szEntryName, connectionName);
    strcpy(rasDialParams.szUserName, username);
    strcpy(rasDialParams.szPassword, password);
    DWORD res = RasDial(&rasDialExtensions, 0, &rasDialParams, 1, callback, hRas);
    return res;
}
DLLEXPORT DWORD HangUp(HRASCONN hRas)
{
    return RasHangUp(hRas);
}
DLLEXPORT DWORD GetConnectStatus(
    HRASCONN hrasconn,
    LPRASCONNSTATUS lprasconnstatus)
{
    return RasGetConnectStatus(hrasconn, lprasconnstatus);
}

Die .NET-Anwendung ruft nun die Funktionen dieser DLL auf. Dadurch funktioniert die Einwahl nun auch unter Windows 2003 Server.