Debugging und ASSERTs in Services

Ich habe in der letzten Zeit einige COM-PlugIns  und Service Komponenten entwickelt. Alles Teile von anderen Diensten und TSPs (Tapi Service Provider). D.h. alles ohne UI. Die ganze Maschinerie, die ich hierzu verwendete befand sich auf einem Windows 2003 R2 Server. Aufgrund bestimmter Hardware war ein virtueller Server zum Testen nicht drin.
Macht ja nix. Man kann ja auch mit Remote Desktop auf dem Server vom eigenen Platz aus arbeiten, ohne deshalb im klimatisierten und immer zu kaltem und außerdem viel zu lautem Serverraum zu arbeiten…

Ziemlich schnell nervte mich gleich ein bestimmtes Problem. Ein Service mit einer meiner Komponenten stand auf einmal. Ich habe mich mit dem Debugger remote attached und merkte mehr oder weniger schnell, dass ein bestimmter Thread (von 67) auf einen ASSERT gelaufen war. Dämlicher Weise hatte der nun kein DebugBreak ausgelöst. Genaugenommen stand der Thread in einer MessageBox mit dem ASSERT Fenster, dass jeder kennt.
Da ich aber per Remote Session mit dem Server verbunden war sah ich diese nicht. Wäre ich am primären Monitor angemeldet gewesen, hätte mich die MessageBox erreicht, dafür trifft die CRT Vorsorge.
Dämlich! Mir wäre sogar ein Crash (mit Minidump natürlich) lieber gewesen. So stand der Service blockierte noch drei andere Sachen und es dauerte doch einige Zeit bis ich diesen stehenden Service als Ursache ausmachen konnte. Wäre der Service gecrasht hätte ich es in Sekunden mitbekommen.

OK! Wie gestalte ich das System nun um, dass ein ASSERT immer einen DebugBreak auslöst und keine MessageBox, die sowieso keiner zu sehen bekommt?
Das würde einen Minidump schreiben und wenn ich mit dem Debugger verbunden wäre, würde es sofort das System an der entsprechenden Stelle stoppen. Die MessageBox mit dem ASSERT brauche ich nicht.

Ein wenig Lesen in der CRT Doku schadet nicht. Also hier die Lösung:

Schritt 1: Wir verhindern, dass die entsprechende MessageBox erscheint und stellen entsprechend ein, dass der ASSERT in der Debug Ausgabe mit protokolliert wird. Und wenn man es hat auch noch will, zusätzlich in einer Protokolldatei.

_CrtSetReportMode(_CRT_ASSERT,_CRTDBG_MODE_DEBUG/*|_CRTDBG_MODE_FILE*/);

Schritt 2: Nun brauchen wir noch einen DebugBreak, der immer ausgelöst wird. Auch das ist kein Problem. Wir benutzen den Debug Report Hook:

_CrtSetReportHook2(_CRT_RPTHOOK_INSTALL, MyDebugHook);

MyDebugHook ist nun nichts weiter als eine kleine Funktion die nur eins enthält: den Aufruf der Funktion DebugBreak();.

So ausgestattet lassen sich Services im Debugmode weitaus besser entwickeln. Jetzt sorgen Sie wenigstens für einen anständigen Crash (natürlich mit Dump), wenn es ASSERTet… :mrgreen:

Late Binding und schwache Performance durch GetIDsOfNames

Immer wieder sehe ich Entwickler, die über Late Binding COM Komponenten ansprechen. Das ist an sich nur zu unterstützen, denn DISPIDs können sich schnell mal ändern, wenn sich ein Interface ändert. Gerade bei den rasant an Verbreitung zunehmenden .NET Komponenten, die man mal schnell in die eigene Anwendung per COM einbindet, kann das Binding über die Interfaces und DISPIDs schnell zum Frust werden. Gleiches gilt für Office Komponenten, bei denen man sich nicht zwingend an ein Interface binden will. Zudem ist es der von Microsoft empfohlene Weg die Office-Automation zu benutzen (siehe auch hier).

OK! Also man nimmt Late Binding und verwendet z.B. die aktuellen Wrapper, die netterweise von der ATL zur Verfügung gestellt werden,  z.B. die netten Funktionen aus der CComDispatchDriver Klasse. GetPropertyByName, PutPropertyByName und auch die Funktionen Invoke0, Invoke1 und InvokeN haben entsprechende Überladungen, die die Verwendung von Funktions-/Eigenschaftsnamen direkt erlauben.

Der Nachteil liegt nicht gleich auf der Hand. Immer wenn solch eine Funktion aufgerufen wird, wird nicht durch der IDispatch::Involke ausgeführt, sondern auch ein Aufruf von IDispatch::GetIDsOfNames. Bei einem Out-Of-Process-Server kann dieser Roundtrip einiges an Performance kosten. Dabei ist es so einfach es besser zu machen.

Die Doku von IDispatch::GetIDsOfNames sagt folgendes:  

The member and parameter DISPIDs must remain constant for the lifetime of the object. This allows a client to obtain the DISPIDs once, and cache them for later use.

Und wer den oben genannten KB-Artikel aufmerksam gelesen hat, der hat auch was von DISPID-Caching mitbekommen.

Ich benutze gerne die die Wrapper, die in der MFC automatisch erzeugt werden, wenn man einen Type-Library über den Class View importiert. Hier werden die Funktionen auch in eine nette kleine COleDispatchDriver-Klasse verpackt und man bekommt netterweise ein gutes Exception Handling den Fehlerfall geliefert. Leider werden hier aber auch wieder nur DISPIDs verwendet.
Aber mit einem kleinen Trick, kann man diese Klassen genial einfach für Late Binding nutzen. Ich gehe wie folgt vor:

  • Ich importiere die Type-Library (tlb) mit dem Visual Studio Class View.
  • D.h. ich habe jetzt alle Wrapper mit DISPIDs, Was ich eigentlich für Late Binding vermeiden will.
  • Jetzt passe ich einfach die einzelnen Wrapper Funktionen in der folgenden Art und Weise an, ich ersetze die DISPID durch eine statische Variable:
void CMyWrapper::DoSomething(){
  static CDispIdHolder dispid(m_lpDispatch,L"DoSomething");
  InvokeHelper(dispid, DISPATCH_METHOD, VT_EMPTY, NULL, NULL);
}
  • Die kleine Klasse die ich hier verwende macht nun den Rest und besorgt die DISPID und cached sie damit weil die Variable statisch definiert ist:
class CDispIdHolder
{
public:
 CDispIdHolder(IDispatch *pDispatch,LPCOLESTR pName)
  : m_dispid(DISPID_UNKNOWN)
  {
   HRESULT hr = pDispatch->GetIDsOfNames(
         IID_NULL,
         &const_cast<LPOLESTR>(pName),
         1,
         LOCALE_SYSTEM_DEFAULT,
         &m_dispid);
   if (FAILED(hr))
    AfxThrowOleException(hr);
  }
  operator DISPID() { return m_dispid; }
private:
 DISPID m_dispid;
};

Die Exception, die man verwendet ist natürlich Implementierungsfrage.

❗ Der Performance Gain ist zum Teil beträchtlich, besonders wnen bestimmte Funktionen sehr oft aufgerufen werden müssen!

Anmerkung (für alle die es ganz genau nehmen):
Wenn man die Doku genau liest, heißt es natürlich hier, dass die Implementierung nur für die Lebenszeit des Objektes konstante DISPIDs garantiert. Wenn man allerdings bei Early Binding schon DISPIDs als konstant annimmt, ist meine Methode für Late Binding sicherlich vertretbar.

Alle reden von Sichertheit in der IT-Branche… Alle?

Bei einem Kunden muss ich aktuell einem Problem auf den Grund gehen, das irgendwie mit Locks in den tiefsten Tiefen des SQL-Server 2005 zu tun hat. Aus diesem Grund habe ich Einblick in den originalen – doch etwas größeren – Datenbestand erhalten. Besagter Kunde ist in seiner Branche ziemlich populär. Es werden in dieser Datenbank 750.000 Kunden verwaltet. Alles aktive Kunden eines bestimmten Produkts einer kleinen geographischen Zone in Deutschland. Qualitativ wirklich aktuelles Material, denn alle diese Datensätze sind wirklich aktive Kunden, d.h. bekommen mindestens einmal im Jahr eine Rechnung.

Um sich nun mit unserer Anwendung als Benutzer Administrator anzumelden benötigte ich noch ein Kennwort. Das hatte mir keiner mitgeteilt (bis jetzt). Aber was soll’s, probieren wir doch mal 😉

  1. Versuch: Der Firmenname
  2. Versuch: Der Produktname
  3. Versuch: admin… Bingo 😮

Ich will nicht davon reden, wieviele Aktivität betrieben wird,  die Datenbanken nach außen abzuschirmen. Auch will ich nicht über die interne Panik klagen, die uns gegenüber immer geschoben wird, dass alles hoch sicher und geheim zu behandeln ist.

Jeder Angestellte, mit etwas Spielwitz kann in etwa 20 Sekunden als Admin an alle Daten…

Nur als Anmerkung: Dieses Sicherheits-Problem ist natürlich mittlerweile behoben… :mrgreen:

Verhindern des Flackerns von Controls wenn ein Fenster-Resize erfolgt

Immer wieder taucht in Foren die Frage aus, wie man das Flackern von Controls verhindern kann. Die Allgemeine Antwort heißt Doublebuffering, d.h. die Ausgabe wird zuerst auf einem nicht sichtbaren Memory DC durchgeführt und anschließend in einem Schlag auf den eigentlichen DC kopiert. Wichtig ist hier, dass auch der Hintergrund im eigentlichen WM_PAINT Handler mit gezeichnet wird und WM_ERASEBKGND gar nichts mehr macht. Der Code-Klassiker hierzu findet sich in Code-Project http://www.codeproject.com/KB/GDI/flickerfree.aspx

Anders liegt die Sache wenn es beim Resize eines Fensters flackert. Hier ist selten Doublebuffering eine Lösung. Meistens liegt hier das Problem darin, dass das Parent Fenster seinen Hintergrund neu zeichnet und anschließend alle Child-Windows auch neu gezeichnet werden müssen.

Aber auch hier ist Abhilfe einfach. Im Parent-Fenster wird einfach der Stil WS_CLIPCHILDREN gesetzt. Das sorgt dafür, dass das Parent Fenster einen DC bekommt bei dem die einzelnen Child-Windows ausgeclippt sind, und somit weder durch WM_ERASEBKGND noch durch den WM_PAINT Handler des Parents überschrieben werden.

Sollten sich die Child-Fenster überlappen müsste man zusätzlich an WS_CLIPSIBLINGS bei allen Kindfenstern als Stil denken (nicht beim Parent).

Wie man sich mit CComDispatchDriver bzw. CComPtr<IDispatch>::InvokeN hereinlegen kann

Eigentlich müsste dieser Artikel eine weitere Überschrift bekommen:
Wie fatal es ist, dass es keine vollständige ATL Dokumentation gibt!

Einige werden CComDispatchDriver kennen. Seit den VS-200x Versionen ist diese Klasse nichts anderes als sie Spezialisierung CComPtr<IDispatch>

Ich hatte vor, eine bestimmte .NET Komponente per late binding in ein C++ Programm zu integrieren.
Kein Problem. CreateInstance, hier ein Invoke0, dort ein Invoke2 und da noch ein InvokeN 😮 … und hier geht auf einmal nichts mehr. Dokumentation in der MSDN: Fehlanzeige!

Bestandsaufnahme: Ich habe einen CComVariant Array aufgebaut und die entsprechenden Daten übergeben. Sieht alles richtig aus. Aber ich erhalte immer nur Fehler (immer E_NOINTERFACE).

  1. OK, nächster Schritt. Das ganze mal direkt mit dem entsprechenden Interface versuchen. Also CComQIPtr eingebaut und direkter Aufruf über die existierende duale Schnittstelle. Komisch, nun geht es.
  2. Nun denn. Das ganze nun mal als Klasse mit dem Classview importiert und einen MFC Wrapper gebaut. Der verwendet ja auch IDispatch. Jetzt wird es mysteriös: Es geht auch!

Dann debuggen wir mal den MFC Wrapper. Schrittweise gehe ich durch den Code im COleDispatchDriver::InvokeHelperV bis Zeile 223 und dort lese ich den folgenden Code und Kommentar:

pArg += dispparams.cArgs - 1; // params go in opposite order

❗ Aha! Hier liegt der Hase im Pfeffer. Ich hatte den CComVariant Array natürlich so aufgebaut, dass das erste Argument auch das erste Element im Array war. InvokeN will aber die selbe Reihenfolge wie IDispatch::Invoke, d.h. in umgekehrter Folge. Also beginnt der Array nun mit dem letzten Argument und endet mit dem ersten und siehe da: Es geht ❗

Schade, dass hier die ATL-Doku so lückenhaft ist. Das hätte mir hier, 2 Stunden Arbeit gespart.
Ich hätte es auch schneller haben können, wenn ich mir aufmerksam die Implementierung von CComPtr<IDispatch>::Invoke2 angesehen hätte (Man achte auf den Array varArgs):

inline HRESULT CComPtr<IDispatch>::Invoke2(__in DISPID dispid, 
       __in VARIANT* pvarParam1, 
       __in VARIANT* pvarParam2, 
       __out_opt VARIANT* pvarRet) throw() 
{ 
 if(pvarParam1 == NULL || pvarParam2 == NULL) 
   return E_INVALIDARG; 
 CComVariant varArgs[2] = { *pvarParam2, *pvarParam1 }; 
 DISPPARAMS dispparams = { &varArgs[0], NULL, 2, 0}; 
 return p->Invoke(dispid, IID_NULL, LOCALE_USER_DEFAULT, 
     DISPATCH_METHOD, &dispparams, pvarRet, NULL, NULL); 
}

Produktvergleich der verschiedenen Visual Studio 2008 Editionen

In diesem Produktvergleich http://msdn2.microsoft.com/en-us/vstudio/products/cc149003.aspx kann man einfach herausfinden welche Komponenten mit den verschiedenen Visual Studio Editionen ausgeliefert werden.

Die häufigste Frage lautet immer wieder in den Foren:
Was benötige ich für eine Edition um ATL+MFC Programme zu schreiben?
Antwort: Die Visual Studio 2008 Standard Edition! Die Express-Edition enthält weder ATL noch MFC.

VS-Tipps & Tricks: Einfaches Navigieren mit Strg+-

Vielleicht ist einigen schon aufgefallen, dass es die Schalter Navigate Backward und Navigate Forward im Standard-Toolbar von Visual Studio gibt. Diese Funktionen werden über die Hotkeys Strg+- (Bindestrich) und Strg+Umschalt+- ausgelöst.

Bei dieser Funktion werden Go-Back-Markierungen in einer Liste vermerkt. Hierbei wird nicht jede Cursorbewegung aufgezeichnet, sondern Bewegungen, die über eine größere Distanz erfolgen.

D.h. man kann mit Strg+- sofort an die Position zurückspringen, die man soeben verlassen hat. Die Umkehrfunktion wird wie gewohnt mit der Umschalt-Taste ausgelöst, Umschalt+Strg+-.

Solche Go-Back-Marken werden in den folgenden Fällen gesetzt.

  • Öffnet man eine neue Datei, dann wird die Position in der aktuellen Datei, als Go-Back-Marke gespeichert. Das Wechseln in eine andere Datei (Strg+Tab) setzt keine Go-Back-Marke (was ich verwunderlich finde).
  • Jede Löschoperation nach einer Cursorbewegung setzt eine Go-Back-Marke.
  • Eine Textsuche (Strg+F) setzt eine Go-Back-Marke an der Fundstelle.
  • Inkrementelle Suche (ob vorwärts oder rückwärts ist egal), trägt eine Go-Back-Marke in die Liste ein und gleichfalls, wenn die inkrementelle Suche beendet wird.
  • Verwendet man GotoLine (Strg+G) oder bewegt den Cursor mehr als 10 Zeilen von der aktuellen Position weg, wird eine Go-Back-Marke an der neuen Position gesetzt. Dies gilt auch wenn dies durch eine Suche ausgelöst wird, die mehr als 10 Zeilen weiterspringt. In diesem Fall wird auch die Startposition als Go-Back-Marke gespeichert.
  • Jeder Klick mit der linken bzw. rechten Maustaste platziert eine Go-Back-Marke an der alten Cursor-Position. Weitere Mausklicks ohne Cursor-Operationen zwischen drin platziert keine neue Go-Back-Marke.
  • Jeder Step-Into beim Debuggen löst auch setzt auch eine Go-Back-Marke.

Sehr nett ist auch die Möglichkeit den gesamten Text von der aktuellen Position bis zurück zur letzten Go-Back-Marke zu selektieren. Dies erfolgt über den Hotkey Strg++ (Pluszeichen).

Ausgesprochen nützlich finde ich diese Funktion auch beim debuggen. Man kann sofort an die Stelle zurückspringen von der man soeben kam, ohne das Call-Stack-Fenster zu verwenden.

BTW: Diese Funktionen habe ich erst durch das versehentliche Auslösen der Kombination Strg++ vor längerer Zeit entdeckt 🙂

Installation von Team Foundation Server 2008

Eben erst TFS 2005 installiert und gleich ein neuer Server mit TFS 2008.

Das ganze in folgender Umgebung:
2 Prozessor 2,4 Ghz Xeon, 2GB Speicher, mit 90GB Raid5 SCSI
Windows Server 2003 R2 (deutsch)
SQL Server 2005 (deutsch)
Team Foundation Server 2008 (englisch)

Das ganze lief so glatt, dass ich nicht mal was dazu bloggen kann 😉
Im Ernst: Microsoft hat aus den ganzen anfänglichen Schwierigkeiten mit der Team Foundation Server 200x Installation scheinbar wirklich gelernt.

Und netter Weise arbeitet auch der Team Explorer 2005 problemlos mit TFS 2008.

… und schließlich entdeckte Kolumbus SS_CENTERIMAGE

Wer wollte nicht  schon immer mal einen Text in einem Static-Control zentrieren. Nein nicht horizontal mit SS_CENTER, das weiß ja jeder ;-). Es geht um das vertikale zentrieren.

Und wieder lernt man etwas dazu, was man einfach die letzten Jahre überlesen hat.

SS_CENTERIMAGE Specifies that a bitmap is centered in the static control that contains it. The control is not resized, so that a bitmap too large for the control will be clipped. If the static control contains a single line of text, the text is centered vertically in the client area of the control.

Auch wenn es nicht für mehrzeilige Texte in einem Static-Controls geht, so funktioniert es zumindest für einzeilige Control, was mit Sicherheit die Mehrzahl ist.

Danke CTecS aus dem Deutschen C++ Forum für die Geist-erhellende Anmerkung.

Anmerkung zur Installation des Visual C++ Feature Pack für 2008 Beta (MFCNext)

Die Beta Version des Feature Pack für Visual C++ 2008 ist ja seit einigen Tagen verfügbar.

In der Beschreibung auf dem Download Link steht zu lesen:

This Feature Pack Beta release may fail to install if you do not have a complete installation of Visual Studio 2008 on your system. To workaround this issue, ensure your original Visual Studio 2008 installation source (network path or DVD) is available when installing the Feature Pack.

Diese Anmerkung sollte man wortwörtlich nehmen.
Es sollte wirklich das komplette Visual Studio 2008 installiert sein. Das schließt auch die von mir nie installierten Komponenten Visual Basic und Crystal Reports ein. Diese Teile habe ich noch nie gemocht und auch normalerweise nie installiert. Aber in diesem Fall schlägt die Installation des Feature Packs fehl.

Im Installations Log sind dann folgende Einträge am Ende zu finden:

[1/10/2008, 13:21:13] (HotIron::CMspExternalUiHandler::UiHandler) Returning 0. INSTALLMESSAGE_RESOLVESOURCE [1: 0 2: vs_setup.msi 3: {80C06CCD-7D07-3DB6-86CD-B57B3F0614D8} 4: {80C06CCD-7D07-3DB6-86CD-B57B3F0614D8}; 5: 0 6: 1 7: 1 8: 0 ]
[1/10/2008, 13:21:13] (HotIron::CMspExternalUiHandler::UiHandlerRecord) Returning IDOK. INSTALLMESSAGE_ERROR [Error [1].An installation package for the product [2] cannot be found. Try the installation again using a valid copy of the installation package ‚[3]‘.: 1706Microsoft Visual Studio Team System 2008 Team Suite – ENU]
[1/10/2008, 13:21:13] (HotIron::CMspExternalUiHandler::UiHandler) Returning IDOK. INSTALLMESSAGE_ACTIONSTART [Action 14:21:13: Rollback. Rolling back action:]

Im Eventlog liest man:

 Protokollname: Application
Quelle:        MsiInstaller
Datum:         10.01.2008 14:21:58
Ereignis-ID:   1023
Aufgabenkategorie:Keine
Ebene:         Fehler
Schlüsselwörter:Klassisch
Benutzer:      MyDomain\Martin
Computer:      LAP-DEV.xyz.loc
Beschreibung:
Produkt: Microsoft Visual Studio Team System 2008 Team Suite – ENU – Update „Visual C++ 2008 Beta Feature Pack“ konnte nicht installiert werden. Fehlercode 1603. Weitere Informationen sind in der Protokolldatei C:\Users\Martin\AppData\Local\Temp\Visual C++ 2008 Beta Feature Pack – KB945273_20080110_131516000-Msi0.txt enthalten.

Protokollname: Application
Quelle:        MsiInstaller
Datum:         10.01.2008 14:21:13
Ereignis-ID:   11706
Aufgabenkategorie:Keine
Ebene:         Fehler
Schlüsselwörter:Klassisch
Benutzer:      MyDomain\Martin
Computer:      LAP-DEV.xyz.loc
Beschreibung:
Product: Microsoft Visual Studio Team System 2008 Team Suite – ENU — Error 1706.An installation package for the product Microsoft Visual Studio Team System 2008 Team Suite – ENU cannot be found. Try the installation again using a valid copy of the installation package ‚vs_setup.msi‘.
Ereignis-XML:

Ach ja, ehe ich es vergesse:
Etwas Geduld sollte man auch mitbringen. Die Installation dauerte auf meinem sonst rechte flotten Firmenlaptop über 35 Minuten!