Windows 7, PlaySound und die vermisste Prüfung auf Speicherlecks

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.

VS-Tipps & Tricks: Debugger Pseudo Variablen

Die folgenden Variablen helfen mir immer wieder beim Debuggen und zeigen einem während des Debuggens in Watch-Window manch nützliche Information:

  • $TID
    Die Thread ID des aktuellen Threads. Nützlich wenn man mehrere Threads debuggt und bestimmte Threads bei einem Breakpoint identifizieren muss.
  • $HANDLES
    Die Anzahl der aktuell benutzen System-Handles des Prozesses. Handle Leaks lassen sich damit im Debugger leicht überwachen.
  • $ERR
    GetLastError
    leicht gemacht. Schlägt ein WinApi Befehl fehl, dann ist es schön das man sofort beim Debuggen im Watch-Window sehen kann was die Ursache war. Oft genug ist ja im Code selbst kein Aufruf von GetLastError vorhanden oder nötig.

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.

VS-Tipps & Tricks: Wie man gezielt einen Breakpoint für einen Thread setzen kann

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.

  1. Zuallererst ermitteln wir die Thread-Id.
    Das geht elementar einfach über die Pseudo-Debugger-Variable $TID, die wir uns im Watch-Fenster, oder im Quick-View anzeigen lassen:
  2. Als nächstes modifizieren wir den Breakpoint so, dass er nur noch dann stoppt, wenn auch unser Thread diesen Breakpoint erreicht.
    Das erreichen wir über die Breakpoint-Eigenschaft Condition (rechter Mausklick, über das Kontextmenü):

    Hier geben wir einfach als Bedingung an, das $TID (also die Thread-ID) identisch sein muss, zu der ID des Threads, den wir beobachten wollen.
  3. Den Rest macht der Debugger für uns, wenn wir wieder den Go-Befehl (F5) geben:

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

Umstellung eines Projektes auf VS-2010 schaltet DEP (/NXCOMPAT) ein und ASLR (/DYNAMICBASE) ein

Bei der Umstellung eines Projektes von VS-2005 auf VS-2010 lief unsere Software im Testfeld ohne Probleme, bis auf einem Rechner, auf dem gezielt immer wieder der selbe Fehler auftrat.

Der Stackdump zeigte aber immer wieder eine unterschiedliche Absturzadresse. Glücklicherweise konnten wir durch Crash-Dumps einigermaßen lokalisieren was passierte. Eigentümlicherweise zeigte sich der Crash immer beimAufruf einer bestimmten Windows-Prozedur:

Eigentümlicherweise lief das selbe Programm mit VS-2005 kompiliert auf allen Maschinen. Also vermutete ich einen Compilerfehler! Aber wie kann ein Compilerfehler nur auf einer Maschine zu einem Problem werden?

Aber ich lag komplett daneben.
Nach der Analyse des Codes stellte sich heraus das DEP die Ursache war, und das der Crash nur auf der einizgen Maschine auftrat, auf der DEP unterstützt wurde und eingeschaltet war.

Die anderen Rechner im Testfeld unterstützen DEP nicht:

Und auf diesen trat der Fehler nicht auf.

Weitere Analyse zeigte, dass ein spezieller Code aus einer Fensterklasse, die ich von einer Libary übernommen hatte ein spezielles Windows Subclassing mit einem Thunking ähnlich wie ATL machte, jedoch wurde der Speicher nicht korrekt als ausführbar markierte.
Folgerichtig krachte es. D.h. die paar Codezeilen, die gerade mal einen JMP und das laden eines Registers durchführten wurden von DEP als illegal betrachtet und es kam zum Crash.

Und das eigentümliche, dass der Code mit VS-2005 kompiliert lief, war auch schnell erklärt.
Als das Projekt von VS-2005 in VS-2010 übernommen wurde, wurden auch die Optionen für DEP (/NXCOMPAT) und auch ASLR (/DYNAMICBASE) eingeschaltet ❗

Das ist eigentlich nicht nett, denn es hat schon einige weitreichende Konsequenzen für die Software.
Also aufgepast bei der Konvertierung von Programmen und genau darüber nachgedacht ob man DEP und ASLR wirklich für seine Software will ❗

Und auf einmal ging der Debugger in VS-2010 nicht mehr…

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!

Minidumps ganz einfach

Manchmal, wenn man ein kleines Programm entwickelt mag es als Overkill erscheinen extra Code für Minidumps einzubauen.
Was aber, wenn man doch einen Fehler aufspüren möchte und ein Minidump ad hoc ganz praktisch wäre?

Unter Vista und Windows 7 ist es ganz einfach in den WER Einstellungen Einträge vorzunehmen, mit denen man mit nur ein paar Registry Einträgen sofort zu Minidumps kommt.

Nachfolgend die Registry Einträge, die einen Fulldump im Verzeichnis %LOCALAPPDATA%\CrashDumps erzeugen.

Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps]
"DumpType"=dword:00000001
"DumpCount"=dword:00000010
"DumpFolder"=hex(2):25,00,4c,00,4f,00,43,00,41,00,4c,00,41,00,50,00,50,00,44,\
00,41,00,54,00,41,00,25,00,5c,00,43,00,72,00,61,00,73,00,68,00,44,00,75,00,\
6d,00,70,00,73,00,00,00

Eine vollständige Liste der Einstellungen findet sich in der MSDN Doku:
http://msdn.microsoft.com/en-us/library/bb787181(VS.85).aspx

PS:
Ich benutze diese Einstellungen aktuell auch einem Bug in VisualStudio 2010 auf die Spur zu kommen, damit ich Crashdumps regelmässig an http://connect.microsoft.com übertragen kann.

PPS: (14.01.2011 nach Hinweis von André)
Wie man auch der Doku ennehmen kann ist fpr dieses Funktion Vista SP1, Windows 2008 Server oder Windows 7 notwendig. Vista RTM hat diese Funktion nicht.

Nette Falle im SQL-Server: Der Kompatibilitätsgrad

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 ❗

VS-Tipps & Tricks: MFC/ATL Tracing selektiv ein und ausschalten

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

Wie man den Namen einer RegisterWindowMessage bekommt

Manchmal muss man Software verstehen. D.h. auch andere Software, die man selbst nicht geschrieben hat 😉

In meinem Fall war es hier ein Client, den ich geschrieben habe, der eine andere Software startet. Diese Software verwendete interne Nachrichten zur Kommunikation, die mit RegisterWindowMessage registriert wurden. Ich wollte nun hier einen Eingriff machen, der ein Fehlverhalten unter Windows 7 und Vista vermeiden soll.

Hilfreich wäre für mich nun gewesen an den Namen der registrierten Nachrichten zu kommen. Spy++ kann es auch und der importiert auch keine mystischen Funktionen. Also muss es einfach gehen.

Und ein wenig Recherche und ein Verweis eines Community Eintrags brachte mich auf diesen Thread:
http://groups.google.it/group/microsoft.public.vc.mfc/browse_thread/thread/f83f7c12c80e4ada/460bc4c43a844a37

Siehe da GetClipboardFormatName löst das Problem. Der nachfolgende Code lieferte mir nun im Detail, was das so hin und her läuft und der Name der Nachrichten war zum Glück sprechend. Ich konnte das Problem lösen.

if (uiMsg>=0xC000)
{
  TCHAR szName[MAX_PATH];
  ::GetClipboardFormatName(uiMsg,szName,MfxCountOf(szName));
  TCHAR szOut[MAX_PATH*2];
  _stprintf(szOut,_T(__FUNCTION__) _T(" %s, wp=0x%08x, lp=0x%08x\n"),
            szName, wParam, lParam);
  OutputDebugString(szOut);
}

ExitInstance gibt für dialogbasierende MFC Anwendungen Unfug zurück

Wer eine dialogbasierende Anwendung mal mit etwas mehr Aufmerksamkeit debuggt oder analysiert wird feststellen, dass der Returncode der Anwendung irgendwie ziemlich zufällig ist. Beobachtet man dies genauer dann stellt man folgendes fest:

  • Beendet man die Anwendung mit der Maus (Klick auf X) oder OK/Cancel so ist der Returncode 0
  • Hält man die Strg-Taste beim Klick fest ist der Returncode 8
  • Beendet man die Anwendung mit Alt+F4 bekommen wir 2.
  • Und jedermann kann jetzt schon mal raten was passiert, wenn wir die Umschalttaste festhalten. Genau dann bekommen wir 4 als Returncode.

Die Mystik hinter dem Ganzen ist die Behandlung von (Afx)PostQuitMessage. Eigentlich sollte mit dieser Nachricht auch der Exitcode gesetzt werden, der mit WM_QUIT versendet wird. Und wenn eben bei einer MDI/SDI Anwendung alles normal läuft, dann ist diese Nachricht die letzte, die aus der Messsagequeue gezogen wird. Und was passiert in CWinApp::ExitInstance? Genau… aus dem statischen Thread Puffer für die Windowsnachrichten wird mit  AfxGetCurrentMessage die letzte Windowsnachricht (normalerweise WM_QUIT) geholt und der wParam Wert bestimmt. Dieser wird dann zurückgegeben.

Leider ist aber WM_QUIT in manchen Fällen aber nicht die letzte Nachricht, die zum Beenden eines Programms führt. Ganz besonders eben nicht bei einer dialogbasierenden MFC-Anwendung. Da ist die letzte Nachricht ist dann eben ein WM_COMMAND oder ein WM_LBUTTONUP der das Schließen der Anwendung auslöst ❗ Und der wParam Wert ist eben entsprechend dieser Nachricht belegt!

Gleiches passiert natürlich, wenn man nach dem Beenden der Messageloop noch andere interne Fenster zerstört. Auch in diesem Fall kann noch mal die interne AfxWndProc durchlaufen werden und dann wird der Returncode auch wieder verändert.

Wer also wirklich Wert auf den Returncode legt (im wahrsten Sinne des Wortes), der sollte sich nie auf den Wert verlassen, der durch CWinApp::ExitInstance zurückgegeben wird oder den Wert, den man selbst mit AfxPostQuitMessage evtl. versucht zu setzen. Eine Variable in CWinApp tut hier einen besseren Dienst. Ebenfalls sollte man ExitInstance überschreiben und immer 0 zurückgeben, wenn man sowieso keine Verwendung für den Returncode des Prozesses hat oder haben möchte.

BTW:
Die Geschichte, wie ich darauf gekommen bin ist schon eigentümlich genug.
Ich habe komplexere Batch-Dateien, die die gesamte Erstellung einer produktiv-Version regelt. Darin kommen im Problemfall auch ein paar Userinteraktionen vor. Diese werden durch Windows Anwendungen ausgelöst, die evtl. einen Dialog anzeigen. Jedem ist klar, dass ohne Dialog und ohne Fenster keine Nachricht abgearbeitet wird. Der Returncode ist also 0! Der Batch verwendet 4NT Syntax und dort kann man ON ERROR definieren und somit sofort eine Fehlerbehandlung auslösen, wenn der Returncode eines Programms nicht 0 ist.
Nun kann sich jeder schon denken was passiert ist. Die Userinteraktion wurde ausgelöst. Der Benutzer machte eine Angabe und… der Batch terminierte erstaunlicherweise mit einem Fehler… (s.o.)