Ein böser Bug.
Ich konnte ihn gerade noch in meiner Brotdose als Testfeld isolieren, bevor ich mit der Korrektur angefangen habe… 🙂
http://youtu.be/_qSPnulPVjM
Und so sieht das ganze nun nach dem Fix aus… :8):
Gesammeltes aus dem Leben eines "normalen" Programmierers… :-)
Rund ums Debuggen
Ein böser Bug.
Ich konnte ihn gerade noch in meiner Brotdose als Testfeld isolieren, bevor ich mit der Korrektur angefangen habe… 🙂
http://youtu.be/_qSPnulPVjM
Und so sieht das ganze nun nach dem Fix aus… :8):
Overlapped I/O ist eine Standardtechnik, die ja jeder kennt, der mit Datenquellen arbeitet, die „irgendwann“ mal Informationen liefert, man aber in der Zeit evtl. noch anderes zu tun hat 😉
In meinem Fall, den ich hier schildere, benutzte unser Programm einen Service, mit dem es über Named Pipes kommuniziert. Das entsprechende Modul, ist schon ziemlich alt und hat seit ca. 3 Jahren keine Änderung erfahren. Aber jetzt auf einmal häuften sich Meldungen, dass unser Programm beim Beenden einen UAE auslöst.
Entsprechende Minidumps wurden angefordert und deren Analyse zeigte eigentlich nicht viel wirklich erhellendes. Es sah alles danach aus, als ob ein Speicherblock freigegeben wird, der bereits freigegeben wurde.
Leider konnte das Szenario in unserem Testfeld nicht einfach nachgestellt werden. Und auch mehrfache Analyse des Sourcecodes brachte erst einmal nichts. Man ist manchmal einfach betriebsblind 🙁
Auffällig war aber, dass diese Meldungen erst vereinzelt auftraten nachdem unsere Software auf VS-2013 Update 3 umgestellt war und diese an Kunden ausgerollt wurde. Und weiterhin konnten wir feststellen, dass alle diejenigen, die diesen Fehler gemeldet haben, relativ leistungsfähige Rechner hatten.
Schließlich gelang es mir, das Szenario auf einem meiner Entwicklungsrechner nachzustellen.
Das passierte:
Die Ursache war ein fehlender CancelIo. Damit das Beenden möglichst schnell vonstatten geht, wurde darauf verzichtet, auf eine Antwort des Services zu warten, ob der Logoff Befehl verstanden wurde, denn kurz danach wird sowieso die Pipe geschlossen. Keine gute Idee in diesem Fall! Das hatte auch den Grund darin, dass
Was ist also passiert ❓
Es waren in meinem Fall nur 4 Bytes und in manchen Fällen würde eine Rücksprungadresse verändert und es kam zum Crash. In anderen Fällen wurde Speicher verändert, der nicht mehr benutzt wurde. Das ganze passierte eben auch nur auf den Systemen, die sehr schnell auf die Antwort des Dienstes reagierten. Kam die Antwort spät genug, war die Änderung des Speichers nicht mehr gefährlich.
Der Fehler blieb jahrelang unentdeckt, weil scheinbar das Stacklayout, das durch den VS-2010 Compiler erzeugt wurde, gegen diesen Fehler unempfindlich war, weil nur Speicher verändert wurde, der nicht zu einem Fehler führte.
Was lerne ich daraus:
Wenn ich einen ReadFile mit OVERLAPPED verwende, sollte ich immer darauf achten, dass im Bedarfsfall auch dieser Vorgang abgebrochen wird durch ein CancelIo ❗ Ein CancelIo mehr kann nicht schaden, wenn man den Block verlässt, in der eine OVERLAPPED Struktur definiert war.
… oder auch: Es gibt Tage, da könnte man sich in der Tischplatte verbeißen…
Wir haben vor ein paar Wochen einen sehr mystischen Fehler in unserer Software gefunden.
Das Problem war eine Funktion, die einen (const) Zeiger auf einen Datenblock bekam.
Die Funktion hat aber unter Umständen eine UI-Interaktion (Dialog) ausgelöst und die entsprechende offene Nachrichtenschleife hat es wiederum einen Hintergrundthread erlaubt (unter bestimmten Umständen) Nachrichten an den Mainthread zu senden, der wiederum die Anzeige aktualisierte und auch den Datenblock, der eben noch als Zeiger übergeben wurde, ungültig machte.
Die Funktion sah in etwa so aus:
bool CSomeClass::SomeAction(const S_FOO *pData)
{
...
pData war nun ungültig und wurde verändert, war aber immer noch gültiger Speicher in dem ein paar Ids aus einer Datenbank standen. Das Programm stürzte nicht ab. Tat aber aufgrund der falschen Ids auch nicht das, was es eigentlich sollte.
Der Code wurde dann wie folgt geändert und ein entsprechender Kommentar geschrieben:
bool CSomeClass::SomeAction(const S_FOO &data)
{
// We use a copy of the result object. There is a chance that the
// global result gets replaced while we are inside this routine and
// the storage where our pointer points gets deleted and replaced
// by a new result object.
...
Oha. Aber was passierte hier. Anstatt wirklich eine Kopie zu verwenden, wurde wieder nur ein Zeiger verwendet. Nur diesmal in Form einer Referenz.
Es wird nun niemanden wundern., dass mir die selbe Fehlebeschreibung wieder auf den Tisch flatterte. Im Debugger wurden die selben Probleme festgestellt und ich musste dreimal auf die Code-Zeile schauen und den Kommentar dreimal lesen, bevor ich verstand was hier geändert wurde.
So hätte es aussehen müssen:
bool CSomeClass::SomeAction(const S_FOO data)
{
...
PS: Ich oute mich mal. Der Trottel war ich…
Bei der Entwicklung von neuen Funktionen in einem Modul bekam ich irgendwann einen ASSERT. Diesen ASSERT hatte ich in einem Cache eingebaut. Der ASSERT sprang an, wenn bei Programmende der Cache nicht sauber aufgeräumt wurde. Also eigentlich in der Entwicklungsphase nichts ungewöhnliches. Irgendwo wurde also eine Funktion zum Aufräumen nicht aufgerufen.
Aber was mich in diesem Moment extrem stutzig machte, war, dass in der Debugausgabe meines VisualStudios keine Memory Leaks angezeigt wurden ❗ Das machte irritierte mich nun schon sehr. Erster Verdacht. Evtl. hat ja jemand die Leakprüfung für den Cache ausgeschaltet (siehe AfxEnableMemoryTracking). Aber eine kurze Prüfung ergab, dass dem nicht so ist.
Also den Test noch mal ausgeführt. Diesmal wieder der ASSERT und zusätzlich der Dump. „Oha“ dachte ich „Hier ist aber was richtig faul!“
Nachdem ich immer wieder andere Testszenarien verwendet habe, erschienen mal die Leaks und mal erschienen sie nicht. Und nach einiger Zeit kaum ich dahinter, dass immer wenn PlaySound in meiner Debug Version der Anwendung verwendet wurde, kein Leak-Check erfolgte. Wurde PlaySound nicht verwendet war alles gut und die Leaks wurden ausgegeben.
Jetzt ging es ans eingemachte. Schnell isolierte ich folgende DLLs die zusätzlich beim ersten PlaySound geladen wurden.
'xyz.exe': Loaded 'C:\Windows\SysWOW64\MMDevAPI.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\propsys.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\wdmaud.drv', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\ksuser.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\avrt.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\setupapi.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\cfgmgr32.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\devobj.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\AudioSes.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\msacm32.drv', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\msacm32.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\midimap.dll', Symbols loaded (source information stripped).
Und nach etwas weiter debuggen kam ich dahinter, dass der DllMain Code von setupapi.dll beim DETACH_PROCESS den Prozess sofort terminiert und nicht alle DLLs einen DETACH_PROCESS Aufruf erhalten. Aber der Code für die Leak-Detection der MFC liegt in der MFCx.DLL und wird durch eine statische Variable ausgelöst, die beim Entladen der MFCx.DLL dann zur Ausgabe der Speicherlecks führt. (Siehe PROCESS_LOCAL(_AFX_DEBUG_STATE, afxDebugState) und AfxDiagnosticInit).
Eine genauere Analyse des Stacktraces ergab folgendes Bild:
ntdll.dll!_ZwTerminateProcess@8() + 0x5 bytes
ntdll.dll!_RtlpWaitOnCriticalSection@8() + 0x1d38f bytes
ntdll.dll!_RtlEnterCriticalSection@4() + 0x16a38 bytes
setupapi.dll!_pSetupInitGlobalFlags@4() + 0x1fc bytes
setupapi.dll!_pSetupGetGlobalFlags@0() + 0xe2 bytes
setupapi.dll!_FlushWVTCache@0() + 0x2f bytes
setupapi.dll!_DestroyDeviceInfoSet@8() + 0x3f bytes
setupapi.dll!_SetupDiDestroyDeviceInfoList@4() + 0x44 bytes
MMDevAPI.dll!CDeviceEnumerator::FinalRelease() + 0x78 bytes
MMDevAPI.dll!ATL::CComObjectCached::~CComObjectCached() + 0x3c bytes
MMDevAPI.dll!ATL::CComObjectCached::`scalar deleting destructor'() + 0xd bytes
MMDevAPI.dll!ATL::CComObjectCached::Release() + 0xf0c6 bytes
MMDevAPI.dll!ATL::CComClassFactorySingleton::~CComClassFactorySingleton() + 0x18 bytes
MMDevAPI.dll!ATL::CComObjectNoLock<ATL::CComClassFactorySingleton >::`scalar deleting destructor'() + 0x1a bytes
MMDevAPI.dll!ATL::CComObjectNoLock<ATL::CComClassFactorySingleton >::Release() + 0xe0e3 bytes
MMDevAPI.dll!ATL::CAtlComModule::Term() + 0x27 bytes
MMDevAPI.dll!__CRT_INIT@12() + 0x26e6 bytes
MMDevAPI.dll!__CRT_INIT@12() + 0x2588 bytes
ntdll.dll!_LdrpCallInitRoutine@16() + 0x14 bytes
ntdll.dll!_LdrShutdownProcess@0() + 0x141 bytes
ntdll.dll!_RtlExitUserProcess@4() + 0x74 bytes
kernel32.dll!75eb7a0d()
msvcr100d.dll!__crtExitProcess(int status=0) Line 709 C
msvcr100d.dll!doexit(int code=0, int quick=0, int retcaller=0) Line 621 + 0x9 bytes C
msvcr100d.dll!exit(int code=0) Line 393 + 0xd bytes C
xyz.exe!__tmainCRTStartup() Line 568 C
Ursache war, dass MMDevAPI.DLL in seinem DllMain Code ausführt in der setupapi.dll, die aber bereits den DETACH_PROCESS abgearbeitet hat. Das grundsätzliche Problem wird hier in diesem Blog Artikel geschildert: http://blogs.msdn.com/b/oldnewthing/archive/2005/05/23/421024.aspx
Mit eigenen Worten: Die MMDevAPI.dll ruft Funktionen in einer DLL auf, die bereits alle Ihren Speicher und Ressourcen freigegeben hat. Und wie es hier im Code so aussieht, versucht die SetupAPI.DLL wieder Ressourcen zu akquirieren, die eben bereits schon freigegeben wurden, weil eine DLL sie erneut benutzt.
Die Folge ein „Crash“ in DllMain, die der Windows-Lader aber abfängt und sofort mit einer Terminierung des Prozesses ahndet. D.h. nun aber, dass nicht alle DLLs, die noch einen DllMain Aufruf mit DETACH_PROCESS erhalten müssten, auch an die Reihe kommen.
Etwas weitere Recherche ergab, dass dieses Problem auch in den MSDN Foren bereits diskutiert wurde inkl. einer möglichen Lösung.
http://social.msdn.microsoft.com/Forums/vstudio/en-US/8cb1847d-3218-4610-9cb8-6905bd255ff5/no-dllprocessdetach-after-calling-playsound-on-windows-7-64bit
Die Lösung ist erstaunlich einfach: Wenn vor der Benutzung von PlaySound explizit die SetupAPI.DLL geladen wird, dann verläuft der Rest der Deinitialisierung vollkommen normal. SetupAPI.DLL wird nicht entladen, weil die DLL durch den LoadLibrary Aufruf die DLL im Speicher sperrt. MMDevAPI.DLL kann erfolgreich selbst aufräumen und der Code läuft nicht mehr ins Nirvana.
Hier handelt sich offensichtlich um einen Bug in Windows 7 (Windows 8 konnte ich dies bzgl. nicht testen).
Dieser Bug kann natürlich auch empfindlichere Probleme mit sich bringen, wenn Ressourcen betroffen sind, die nicht automatisch mit Prozessende freigegeben werden und wenn diese Ressourcen ausschließlich in der DllMain bei einem DETACH_PROCESS behandelt werden.
Die folgenden Variablen helfen mir immer wieder beim Debuggen und zeigen einem während des Debuggens in Watch-Window manch nützliche Information:
Siehe auch MSDN:
http://msdn.microsoft.com/en-us/library/ms164891.aspx
BTW: WinDbg hat noch einige mehr nette Pseudovariablen:
http://blogs.msdn.com/b/debuggingtoolbox/archive/2008/06/16/special-command-using-variables-and-retrieving-information-through-pseudo-registers.aspx
Schmerzhaft vermisse ich immer wieder $PEB, $TEB, $TPID, $RA im Visual Studio, komisch, dass es die nur im WinDbg gibt.
Dann will ich mal das Problem lüften, dass sich mit diesem Code ergibt, dein ich meinem letzten Artikel vorgestellt habe:
template <class T>
void SecureClearString(T &strText)
{
::SecureZeroMemory(strText.GetBuffer(0),strText.GetAllocLength());
strText.Empty();
}
Zuerst einmal liegt es nicht daran, dass es hier Template verwendet wurde.
Ein Template wurde verwendet, weil in dem Code nicht nur CString, sondern implizit CStringA und CStringW verwendet wurde. Der Code sollte also mit beiden Typen funktionieren.
Und damit sind wir bei Problem 1, das auch gelöst wurde:
Wenn ein CStringW verwendet wird, dann wird nur die Hälfte des Strings gelöscht, und nicht alles.
Das Szenario, dass zu einem miesen Crash führen kann, will ich nun in den einzelnen Schritten schildern (es wurde ja vermutet, dass es mit GetBuffer zusammenhängt und die Vermutung ist richtig):
Hier der Code, mit dem man den Crash gezielt nachbauen kann:
void Crash()
{
CString str1 = _T("12345678901234567890");
str1 = _T("123");
CString str2 = str1;
SecureClearString(str1); // Crash
SecureClearString(str2);
}
Der Vollständigkeit halber will ich aber auch noch ein Stück Code zegen, der es richtig macht:
template <class T>
void SecureClearString(T &strText)
{
// We need this only if there is a private buffer
if (strText.GetAllocLength()!=0)
{
// Execute GetBuffer first. This might cause a fork and may change
// GetAllocLength.
T::XCHAR *pBuffer = strText.GetBuffer(0);
size_t iLen =strText.GetAllocLength();
::SecureZeroMemory(pBuffer,iLen*sizeof(T::XCHAR));
}
strText.Empty();
}
PS: Der Leser kann sich denken, dass mich dieser Bug und die entsprechende Reproduktion einige Nerven gekostet haben. Denn es war nicht einfach die Vorbedingung (erst langer String, dann kurzer String, dann Zuweisung) zu ermitteln. Und wie es oft so ist führen Heap-Fehler erst sehr verzögert zu einem Problem.
Wen es genau interessiert: Ich habe ca 7 Stunden an dem Fall geknobelt und hatte 3 verschiedene Crashdumps zur Verfügung. Selbst konnte ich diesen Fehler in unserem Testfeld zuvor nicht erzeugen, weil eben nie alle Bedingungen erfüllt waren. Erst als mir klar war wo das Problem lag, gelang es mir natürlich auch sofort Eingaben zu erzeugen, die den Crash reproduzierten.
Wenn man eine Anwendung oder einen Dienst hat, der mit vielen Threads arbeitet, dann kann das Debuggen abenteuerlich werden. Besonders wenn viele Threads ein und die selbe Threadfunktion verwenden.
Was macht man nun wenn man einen Thread isoliert hat und dessen Verhalten weiter prüfen möchte? Single-Stepping ist nicht drin, denn der Breakpoint würde auch jeden anderen Thread anhalten, der diese Code-Position erreicht.
Die nachfolgende Methode ist relativ einfach um gezielt einen Breakpoint für einen Thread zu setzen.
Gehen wir mal davon aus, dass wir im Debugger einen Breakpoint haben und uns im Kontext des Threads befinden, den wir nun weiter verfolgen wollen.
Auf diese Weise kann man auch mehrere Threads einfach beobachten.
Hinweis:
Dieses Verfahren ändert das Laufzeitverhalten des Programmes, denn der Breakpoint wird immer intern ausgeführt, egal welcher Thread diese Codestelle passiert. Nur ermittelt der Debugger dann die Bedingung returniert und lässt das Programm, dann weiterlaufen, wenn die Id nicht passt. Wird diese Codestelle sehr oft passiert, dann kann ist der Einfluss eines solchen Breakpoints nicht unerheblich.
Alternativ, kann man in solch einem Fall auch ein Stück Code einsetzen, der die aktuelle Thread-ID gegen eine statische Variable testet und einen DebugBreak ausführt. Die statische Variable setzt man dann während der Debugsession auf die gwünschte Thread-ID über das Watch-Window oder den Quick-View
Weiterführende Infos zu Debugger-Pseudo-Variablen von Visual-Studio findet man hier:
http://msdn.microsoft.com/en-us/library/ms164891.aspx
Ich wollte heute morgen einfach ein kleines Testprogramm debuggen. Der Build wurde normale durchgeführt, aber danach ging nichts mehr. VS meldete nurnoch lapidar:
Microsoft Visual Studio is Busy
Microsoft Visual Studio is waiting for an internal Operation to complete. If you regularly encounter this delay during normal usage. please report this problem to Microsoft.
D.h. Visual Studio meldete nur noch, dass es beschäftigt wäre. Nach ein paar überlangen Sekunden/Minuten hatte sich dann zumindest der Bildschirm aufgebaut, wie ich es vom Debugger her gewohnt war.
Im Debug Ausgabefenster konnte ich nur sehen, dass er die Symbole der EXE geladen hat. Mehr nicht.
Also DEVENV.EXE abgeschossen.
Anders Mini-Projekt mit nur einer Consolen Ausgabe, kompiliert, Debuggen… gleiches Problem.
Neustart.
Gleiches Problem. 😯
💡 Ich starte DEVENV.EXE erneut, gehe auf Tools -> Options -> Debugging und sehe den Übeltäter:
Meine Server ziehen um, und ich habe zentral für alle Entwickler einen Symbol-Cache. Nun und dieser Server ist eben nicht mehr da. Also wurde für jede DLL die der Debugger gesucht hat ein Fileshare bemüht der ins Nirwana zeigte.
Wie so oft: Kleine Ursache – Fatale Wirkung!
Nach einem der letzten Updates unserer Software meldete uns ein Kunde einen SQL Fehler, der bei einer bestimmten Operation auftrat. Er setzt den MS-SQL Server 2008 ein.
OK, meine Testumgebung hat drei Server von SQL 2000, über 2005 bis 2008 R2. Keine der Testumgebungen brachte bei der entsprechenden gleichen Operation einen Fehler 😕 Gut oder besser schlecht… Der Kunde bekommt nun eine Fehlermeldung und auch wenn Kunden meistens ja nicht recht haben wenn sie Fehler melden 😀 schaute ich mir dennoch alle SQL Befehle etwas genauer an, die meine Software da auslöste.
In dem entsprechenden Teil meiner wurde nach Benutzerangaben ein relativ komplexer Query durch einen Abfragegenerator zusammengebaut. Darunter fand sich auch der folgende Subquery, als Teil der gesamten Abfrage:
SELECT a.[Id] FROM [tblXYZ] AS a
WHERE
(((a..[IdParent] IS NULL
AND a..[Id] NOT IN
(SELECT [IdXYZ]
FROM [tblSomething]
WHERE [IdParent] IS NOT NULL))))
Unschwer zu sehen werden hier mit dem Alias a zusammen irgendwie zwei Punkte verwendet. Bleibt die Frage warum in meiner Umgebung nun kein Fehler passiert und beim Kunden ein nun Syntax Fehler ausgelöst wird.
Nach einigem Suchen fand ich die Ursache im Kompatibilitätsgrad, den man im Managementstudio unter Datenbank -> Datenbankname -> Eigenschaften -> Optionen je Datenbank separat einstellen kann. Dort sind folgende Einstellungen möglich.
SQL Server 2000 (80)
SQL Server 2005 (90)
SQL Server 2010 (100)
In meiner Testumgebung verwende ich eine Datenbank, die seit den ersten Anfängen unserer Software immer weiter als Testumgebung mit vielen Testdaten dient. Sie wurde erstmals auf einem SQL Server 2000 angelegt. Dann auf einen 2005er und schließlich auf einen SQL Server 2008 R2 umgezogen. Netterweise – oder besser dummerweise – hat sich der SQL Server bei jeder Umstellung die ehemalige Kompatibilität gemerkt. Und man staunt nicht schlecht: Auf einem SQL Server 2000 ist es kein Fehler zwischen Alias und Spaltennamen zwei Punkte zu schreiben. Bei einem SQL Server 2005 oder später ist das sehr wohl ein Syntaxfehler.
Der Fehler lag also doch bei uns – was ja wirklich selten vorkommt 😀 – und wurde trotz genauer Tests nicht entdeckt.
Man merke sich: SQL Server Syntax ist trotz gleicher SQL Server Version eben doch lange nicht das selbe.
Wer also Software auf einem SQL Server testet sollte tunlichst darauf achten welchen Kompatibilitätsgrad er benutzt ❗
In ATL und MFC steckt ein ziemlich ausgeklügelter Trace-Mechanismus. Wenn man sich das MFC – ATL Trace Tool ansieht kann man zu allen möglichen Kategorien Informationen in Debug Fenster ausgeben lassen.
Alleine die MFC hat 14 verschiedene Trace Kategorien. Darunter besonders interessante wie CommandRouting, AppMsg und WinMsg. Die ATL hat weitere 27 Kategorien.
Es lohnt sich mal einen Blick in dieses Tool und die entsprechenden Ausgaben zu machen. Es gehört zu den oft unbekannten netten Helferlein, die leider mangels Bekanntheit selten benutzt werden.
Um Fehler zu finden und einzugrenzen, sind mir jedoch oft eher zu viele Ausgaben vorhanden, als zu wenige. Zudem finde ich es manchmal unhandlich mit dem Trace-Tool die Nachrichten ab einem bestimmten Moment einzuschalten und wieder auszuschalten.
Ich habe eine kleine Hilfsklasse gebaut,mit der man in jedem Szenario, jederzeit zu einem bestimmten Moment das Tracing im Code ein- und automatisch wieder ausschalten kann.
class CDebugEnableTraceForCategory
{
public:
CDebugEnableTraceForCategory(ATL::CTraceCategory &category,
PCSTR pszPrompt=NULL,
UINT uiLevel=4,
ATL::ATLTRACESTATUS eStatus=ATL::ATLTRACESTATUS_ENABLED)
: m_category(category)
, m_uiSaveLevel(category.GetLevel())
, m_eSaveStatus(category.GetStatus())
, m_strPrompt(pszPrompt)
{
if (!m_strPrompt.IsEmpty())
TRACE("%s - Tracelevel %d\n", m_strPrompt.GetString(),uiLevel);
m_category.SetLevel(uiLevel);
m_category.SetStatus(eStatus);
}
~CDebugEnableTraceForCategory()
{
m_category.SetLevel(m_uiSaveLevel);
m_category.SetStatus(m_eSaveStatus);
if (!m_strPrompt.IsEmpty())
TRACE("%s - Tracelevel %d\n", m_strPrompt.GetString(), m_uiSaveLevel);
}
private:
// Data fields
ATL::CTraceCategory &m_category;
ATL::ATLTRACESTATUS m_eSaveStatus;
UINT m_uiSaveLevel;
CStringA m_strPrompt;
// no copy operator
CDebugEnableTraceForCategory(const CDebugEnableTraceForCategory &);
CDebugEnableTraceForCategory& operator=(const CDebugEnableTraceForCategory &);
};
Mit dieser Klasse kann ich zum Beispiel alle Windows-Nachrichten an MFC Fenster bei einer bestimmen Aktion ausgeben lassen. Und wenn die Aktion fertig ist stopp auch das Tracing wieder.
Hier als Beispiel um alle Fensternachrichten in der Aktion LoadFrame zu Tracen:
void CMyApp::InitInstance()
{
...
CDebugEnableTraceForCategory trace(traceWinMsg,"messages in LoadFrame");
pMainFrame->LoadFrame(IDR_MAINFRAME,WS_OVERLAPPEDWINDOW);
...
}
Weitere Infos in der MSDN findet man unter ATL::CTraceCategory und ATLTRACE2 http://msdn.microsoft.com/en-us/library/dhxsse89(VS.100).aspx