Design für Windows-Apps, das bei einem Absturz Protokolle und Dumps hinterlässt
· Go Komura · Windows-Entwicklung, Ausnahmebehandlung, Protokollierung, WER, Absturzdump, Fehleruntersuchung
Das Schmerzhafteste bei der Fehleruntersuchung von Windows-Apps ist der Zustand, in dem nur bekannt ist, dass die App abgestürzt ist, aber nichts darüber, warum.
Besonders bei folgenden Fällen wird dieses Problem gravierend:
- Der Absturz tritt nur in der Kundenumgebung auf
- Der Absturz tritt erst nach langem Dauerbetrieb auf
- WPF/WinForms/Windows-Dienst/residente App mit niedriger Reproduzierbarkeit
- COM, P/Invoke, native DLLs oder Vendor-SDKs sind beteiligt
- Nur die Ausnahmemeldung liegt vor, aber der Kontext davor fehlt
Vorab ehrlich gesagt: Allein im abstürzenden Prozess lässt sich ein Protokoll nicht „mit Sicherheit” hinterlassen. Bezieht man Stapelbeschädigung, Speicherbeschädigung, Fast Fail, Zwangsbeendigung und Stromausfall mit ein, ist das letzte In-Process-Protokoll grundsätzlich nur Best Effort.
Anzustreben ist in der Praxis ein Aufbau, der nicht allein auf das Innere des abstürzenden Prozesses setzt. Das heißt, man denkt in drei Schichten:
- Die normale, chronologische Protokollierung
- Der letzte Absturzmarker im Moment des Absturzes
- Die vom Betriebssystem oder einem separaten Prozess hinterlassenen Absturzbeweise
Dieser Artikel behandelt, ausgehend von Windows-Desktop-Apps, residenten Apps, Windows-Diensten und Tools zur Gerätekopplung, Best Practices, um auch beim Absturz durch eine von einem Programmierfehler verursachte Ausnahme die Untersuchbarkeit nicht zu verlieren.
1. Zuerst das Fazit
Vorab nur die Schlussfolgerungen.
- Am wichtigsten ist es, die „letzte Protokollzeile” nicht auf einen einzigen In-Process-Handler zu setzen.
- Am unauffälligsten in der Praxis ist die Kombination normales Protokoll + letzter Absturzmarker + WER LocalDumps.
- Bei Dauerbetrieb, Gerätekopplung, Plugins oder gemischten nativen SDKs wird es deutlich stabiler, wenn man einen Überwachungsprozess (Watchdog/Launcher/Dienst) hinzufügt.
- Im Absturz-Handler gilt die eiserne Regel: nichts Aufwändiges tun. Komprimierung, HTTP-Versand, DI-Auflösung, UI-Dialoge und komplexe JSON-Erstellung sind tabu.
- Beim Absturz nur kurz lokal aufzeichnen; Komprimierung, Upload und Benachrichtigung werden auf den nächsten Start oder einen separaten Prozess verschoben.
- WinForms’
ThreadExceptionoder WPFsDispatcherUnhandledExceptionzu nutzen, um die App scheinbar am Leben zu erhalten, ist bei einem Programmierfehler gefährlich. - Sowohl bei .NET als auch bei nativem Code gilt bei Ausnahmen, die einen beschädigten Zustand nahelegen: „aufzeichnen und beenden” ist die sicherere Grundhaltung, nicht „wiederherstellen”.
- Wer Dumps sammelt, muss PDBs und ausgelieferte Binärdateien gleichzeitig archivieren, sonst lassen sie sich später nicht mehr lesen.
Kurz gesagt lautet die Best Practice: „Nicht versuchen, im Moment des Absturzes alles zu erledigen. Die Aufgaben auf vor dem Absturz, im Moment des Absturzes und nach dem Absturz verteilen.”
1.1 In diesem Artikel verwendete Begriffe
Vorab die Begriffe, die im Folgenden ohne weitere Erklärung verwendet werden.
| Begriff | Ausschreibung/Aussprache | Bedeutung |
|---|---|---|
| WER | Windows Error Reporting, Windows-Fehlerberichterstattung | Der Windows-Mechanismus, der einen abnormalen Programmabbruch auf Betriebssystemebene erfasst und aufzeichnet. Die Einstellung, mit der lokal ein Dump hinterlassen wird, heißt LocalDumps |
| Dump/Minidump | crash dump | Eine Datei, die den Speicherinhalt des Prozesses im Moment des Absturzes sichert. Threads, Stack und Module lassen sich später einsehen |
| PDB | Program Database | Eine beim Build erzeugte Symboldatei. Ohne sie zeigt ein geöffneter Dump weder Funktionsnamen noch Zeilennummern |
| In-Process | innerhalb des Prozesses | Verarbeitung innerhalb des abstürzenden Prozesses selbst. Das Gegenteil ist ein separater Prozess |
| Best Effort | bestmögliche Anstrengung | Die Eigenschaft „wird im Idealfall hinterlassen, aber ohne Garantie”. Das In-Process-Protokoll im Moment des Absturzes fällt darunter |
Fast Fail/__fastfail |
schneller Fehlerabbruch | Ein Mechanismus, der bei erkanntem beschädigtem Zustand ohne Aufräumarbeiten mit minimalem Aufwand sofort beendet. Nativ entspricht das __fastfail, in .NET Environment.FailFast |
| Watchdog | Überwachungsprozess | Ein separater Prozess, der Start, Beendigung und Lebenszeichen des Hauptprozesses von außen beobachtet. Kann auch als Launcher oder übergeordneter Dienst realisiert werden |
| Heartbeat | Lebenszeichen | Ein regelmäßiges Signal an den Watchdog, das „ich laufe noch” bedeutet |
| UNC-Pfad | Universal Naming Convention | Ein Netzwerkfreigabepfad der Form \\server\share\.... Bei Verwendung im Absturzfall drohen Wartezeiten durch kurze Unterbrechungen oder Anmeldeinformationen |
| ACL | Access Control List, Zugriffssteuerungsliste | Die Festlegung, wer in einem Verzeichnis lesen und schreiben darf. Ein häufiger Grund, warum Dumps oder Protokolle „ins Leere laufen” |
| SEH | Structured Exception Handling, strukturierte Ausnahmebehandlung | Der native Ausnahme-Mechanismus von Windows. SetUnhandledExceptionFilter gehört hierher |
| CRT | C Runtime, C-Laufzeitbibliothek | Die Implementierung der C-/C++-Standardbibliothek. Sie besitzt unabhängig von SEH eigene Beendigungswege |
| Session | Sitzungs-ID | Ein Wert, der identifiziert, „von welcher Startinstanz die Rede ist”. Er ist der Schlüssel, um Protokoll-, Dump- und Watchdog-Aufzeichnungen abzugleichen |
2. Warum sich In-Process allein nicht „zuverlässig” machen lässt
Bleibt dieser Punkt vage, gerät das gesamte Design ins Wanken.
2.1 Der Kontext des abstürzenden Threads selbst kann bereits beschädigt sein
Hooks für unbehandelte Ausnahmen und Top-Level-Ausnahmefilter können im Kontext des beschädigten Threads laufen. Zu diesem Zeitpunkt kommt es ganz normal vor, dass
- der Stack bereits gefährdet ist,
- eine zusätzliche Speicherzuweisung wegen Heap-Beschädigung gefährlich ist,
- ein Warten wegen einer beim Auftreten der Ausnahme gehaltenen Sperre blockiert,
- Objekte, von denen der Logger selbst abhängt, bereits beschädigt sind.
Es ist daher sicherer, den letzten Handler nicht als „Ort, an dem alles möglich ist”, sondern als „Ort, an dem nur sehr wenig möglich ist” zu betrachten.
2.2 Fast Fail und Ausnahmen bei beschädigtem Zustand setzen „minimales In-Process-Verhalten” voraus
Bei Speicherbeschädigung oder fatalen Zuständen sollte man sich nicht auf die normale Ausnahmebehandlung verlassen.
Insbesondere native __fastfail-Mechanismen und Anomalien, die einen beschädigten Zustand nahelegen, sind darauf ausgelegt, „mit möglichst wenig Overhead sofort zu beenden”.
Das heißt, es ist naheliegend zu denken: Das letzte In-Process-Protokoll ist ein Glücksfall, falls es geschrieben wird; die Hauptbeweise liegen auf Seiten des Betriebssystems bzw. eines separaten Prozesses.
2.3 Auch das .NET-Ereignis für unbehandelte Ausnahmen ist kein Ort für „aufwändige Wiederherstellung”
AppDomain.UnhandledException in .NET ist praktisch, aber
hier sollte man sich auf eine kurze Aufzeichnung beschränken.
- Es kann von Sperren betroffen sein, die beim Auftreten der Ausnahme gehalten wurden
- Nicht alles lässt sich sicher abfangen, auch Ausnahmen bei beschädigtem Zustand nicht
- Wird hier zwanghaft eine Fortsetzungsstrategie gebaut, lebt der Prozess leicht in halb kaputtem Zustand weiter
Es ist realistisch, das Ereignis für unbehandelte Ausnahmen als „letzte Benachrichtigung” zu betrachten, nicht als „sicheren Wiederherstellungspunkt”.
3. Empfohlene Architektur - Absturzzeitpunkt und Zeit nach dem Neustart trennen
Am übersichtlichsten lässt sich das gestalten, indem man das, was zum Absturzzeitpunkt geschieht von dem, was nach dem Neustart geschieht trennt.
Zunächst auf einen Blick, in wessen Prozessverantwortung die drei Schichten jeweils liegen und wo sie landen.
flowchart TD
subgraph APP["App-Prozess"]
L1["Normales Protokoll<br/>Append-only-Zeitreihe"]
L2["Letzter Absturzmarker<br/>Nur eine Zeile schreiben und beenden"]
end
subgraph WIN["Windows-Seite"]
WER["WER LocalDumps<br/>Dump außerhalb des Prozesses speichern"]
end
subgraph WD["Watchdog-Prozess"]
EX["Exit Code und Endzeitpunkt erfassen<br/>Neustart-Entscheidung"]
end
subgraph NEXT["Nächster gesunder Prozessstart"]
POST["Komprimierung/Upload/Benachrichtigung<br/>Erkennung des vorherigen Absturzes"]
end
DISK[("Lokales festes Verzeichnis")]
L1 --> DISK
L2 --> DISK
WER --> DISK
EX --> DISK
DISK --> POST
L1 -. Ausnahme tritt auf .-> L2
L2 -. Prozess beendet .-> WER
L2 -. Prozess beendet .-> EX
Drei Punkte sind entscheidend:
- Der abstürzende Prozess schreibt selbst nur die beiden Elemente innerhalb des Rahmens „App-Prozess”. Und der letzte Absturzmarker endet damit, dass „eine Zeile geschrieben wird”.
- Die Hauptbeweise liegen außerhalb des Prozesses. Der WER-Dump und die Watchdog-Aufzeichnung bleiben erhalten, selbst wenn die App beschädigt ist.
- Der Schlüssel zum Abgleich ist eine gemeinsame Session-ID und PID. Fehlt diese Übereinstimmung, wirken die drei Beweisstücke wie drei getrennte Vorfälle.
| Phase | Ziel | Wo ausgeführt | Was zu tun ist |
|---|---|---|---|
| Normalbetrieb | Zeitreihe festhalten | In der App | Strukturiertes Protokoll, Heartbeat, Grenzereignisse |
| Beim Absturz | Minimale Beweise sichern | In der App + Betriebssystem | Letzter Absturzmarker, WER-Dump |
| Direkt nach der Beendigung | Unerwarteten Exit erkennen | Separater Prozess | Exit Code erfassen, Neustart entscheiden, benachrichtigen |
| Nach dem nächsten Start | Aufwändige Nachbearbeitung | Neuer, gesunder Prozess | Komprimierung, Upload, Benutzerbenachrichtigung, Aufräumen alter Protokolle |
Mit dieser Aufteilung wird das Design erheblich stabiler.
3.1 Minimalkonfiguration
Für kleinere Fachanwendungen oder interne WPF-/WinForms-Apps reicht zunächst oft schon Folgendes:
- Normales Protokoll: eine lokale Append-only-Datei
- Letzter Absturzmarker: eine dedizierte kurze Datei
- Dump: WER LocalDumps
- Beim nächsten Start: Anzeige „Die Anwendung wurde beim letzten Mal abnormal beendet. Diagnoseinformationen sind verfügbar.”
3.2 Stärkere Konfiguration
Eine Stufe stärker sollte man bei folgenden Anforderungen werden:
- 24/7-Betrieb
- Gerätesteuerung, Überwachung, residenter Betrieb
- Viel COM/P/Invoke/native SDKs
- Kindprozesse, Plugins oder Skriptausführung
- Ein „dauerhaftes Hängenbleiben” in der Kundenumgebung ist inakzeptabel
In diesem Fall wird die Aufteilung in
- Worker-Prozess: die eigentliche Verarbeitung
- Launcher/Watchdog/Dienst: Startüberwachung, Exit-Erfassung, Neustart
- WER LocalDumps: auf Worker-Seite
- Nächster Start oder Watchdog: Erfassung von Diagnoseinformationen
sehr praxistauglich.
4. Best Practices für das normale Protokoll
Wer versucht, mit nur der letzten Zeile im Moment des Absturzes zu kämpfen, verliert meistens. Was wirklich zählt, ist das normale Protokoll bis zum letzten Moment davor.
4.1 Protokolle sind eher „später korrelierbare Informationen” als „menschenlesbarer Text”
Diese Mindestangaben sollten im normalen Protokoll enthalten sein:
- UTC-Zeitstempel
- Verstrichene Zeit seit Prozessstart
- PID/TID
- App-Name, Version, Build-Nummer, Commit-Kennung
- Session-ID
- Vorgangs-ID/Job-ID/Korrelations-ID
- Modulname/Bildschirmname/Workername
- Die letzten externen Auswirkungen
- Dateischreibvorgänge
- DB-Aktualisierungen
- Gesendete Gerätebefehle
- Kommunikationsanfragen
- Ausnahmetyp, HRESULT/Win32-Fehler/Ausnahmecode
- Eine Zusammenfassung der wichtigsten Eingabeparameter
- Ziel-IDs, soweit sie keine vertraulichen Daten enthalten
Empfehlenswert ist ein Ereignis pro Zeile im JSON-Lines- oder key=value-Format.
Bei JSON Lines hat ein Ereignis etwa diese Granularität:
{"ts":"2026-03-18T01:15:33.412Z","up_ms":184213,"pid":1234,"tid":9,"app":"MyApp","ver":"3.2.1.884","commit":"9f1c2ab","session":"4f1c","level":"Warning","module":"DeviceWorker","op":"JOB-20260318-0042","event":"ExternalCommandSent","target":"COM3","cmd":"SEQ_START","timeout_ms":3000}
Das ist lang, aber damit erkennt man allein an dieser einen Zeile, wann welche Startinstanz, welcher Version, während welches Vorgangs, was getan hat. Zur Dump-Seite besteht die Verbindung über pid und session, zur Build-Seite über ver und commit.
Im key=value-Format sähe derselbe Inhalt so aus:
ts=2026-03-18T01:15:33.412Z up_ms=184213 pid=1234 tid=9 app=MyApp ver=3.2.1.884 commit=9f1c2ab session=4f1c level=Warning module=DeviceWorker op=JOB-20260318-0042 event=ExternalCommandSent target=COM3 cmd=SEQ_START timeout_ms=3000
Wichtiger als langer, für Menschen geschriebener Fließtext ist, dass sich später drei Dateien miteinander abgleichen lassen.
4.2 Kritische Ereignisse synchron aufzeichnen
Wird das gesamte normale Protokoll synchron geschrieben, wird es langsam. Verlässt man sich hingegen komplett auf einen asynchronen Puffer, verschwindet im Moment des Absturzes alles auf einmal.
In der Praxis ist es daher realistisch, die Behandlung je nach Stufe zu variieren.
- Feingranulare
Information-Ereignisse: Pufferung ist in Ordnung Warningund höher: frühzeitig flushen- Wichtige Grenzereignisse: synchron schreiben
- ProcessStart
- ConfigLoaded
- WorkerStarted
- ExternalCommandSent
- TransactionCommitted
- RecoveryStarted
- FatalPathEntered
Im Kern geht es darum, wenigstens die geschäftlichen Grenzen zuverlässig auf den Boden zu bringen.
4.3 „Das gerade geschriebene normale Protokoll” und „der letzte Absturzmarker” getrennt halten
Das ist ziemlich wichtig.
Versucht man, alles in ein einziges Rolling Log zu packen, passiert Folgendes:
- Es war gerade mitten in der Rotation
- Es lag noch in der asynchronen Warteschlange
- Der Logger selbst starb direkt nach der Ausnahme
- Die Protokollzeile wurde mitten im Schreibvorgang abgeschnitten
Daher empfiehlt sich eine Aufteilung in mindestens zwei Dateien:
app-<session>.jsonlDas normale chronologische Protokollfatal-last.logoderfatal-<session>.logAusschließlich für den letzten Absturzmarker
Allein die Tatsache, dass klar ist, wohin die letzte Zeile geschrieben wird, hilft in der Praxis erheblich.
4.4 Protokollspeicherort fest lokal, keine Netzwerkziele
Sich beim Absturz auf einen UNC-Pfad, ein NAS, HTTP oder eine Cloud-API zu verlassen, ist gefährlich, weil dabei
- kurzzeitige Netzwerkunterbrechungen,
- DNS-Verzögerungen,
- abgelaufene Anmeldeinformationen,
- Warten auf dem UI-Thread,
- unzureichende Berechtigungen des Dienstkontos
eine Rolle spielen.
Beim Absturz wird zunächst auf einen festen lokalen Pfad geschrieben. Der Versand erfolgt nach dem nächsten Start oder aus einem separaten Prozess.
4.5 Die Session in den Dateinamen aufnehmen
Ein Datum allein reicht nicht aus, da die App am selben Tag mehrfach neu gestartet wird.
Zum Beispiel so:
Logs\
MyApp_20260318_101530_pid1234_session-4f1c.jsonl
MyApp_fatal_20260318_101533_pid1234_session-4f1c.log
MyApp_watchdog_20260318.jsonl
Allein die Tatsache, dass klar ist, „von welcher Startinstanz die Rede ist”, verändert die Analysegeschwindigkeit erheblich.
5. Best Practices für den letzten Absturzmarker
Hier geht es nicht darum, einen vollwertigen Logger zu bauen. Hier wird nur einmal, kurz, mit Tendenz zur Zuverlässigkeit geschrieben.
5.1 Ziel ist „den Einstiegspunkt festzuhalten”, nicht „Ursachendetails”
Die Informationen im letzten Absturzmarker sind stärker, wenn man sie eng fasst.
- Auftrittszeitpunkt (UTC)
- PID/TID
- Session-ID
- Version/Build-Nummer
- Von welchem Hook aus
AppDomain.UnhandledExceptionApplication.ThreadExceptionDispatcherUnhandledExceptionSetUnhandledExceptionFilter_set_invalid_parameter_handlerset_terminate
- Ausnahmetyp oder Ausnahmecode
- Falls möglich eine kurze Meldung
- Die letzte Vorgangs-ID
- Der Dateiname des normalen Protokolls
- Das erwartete Dump-Verzeichnis
Das reicht aus.
5.2 Was im Absturz-Handler nicht getan werden darf
Jeder dieser Punkte wird mit hoher Wahrscheinlichkeit zur Falle.
- Den Logger über einen DI-Container auflösen
- async/await verwenden
- Tasks starten
- Auf Sperren warten
- Komplexes JSON zusammenbauen
- COM-Objekte anfassen
- UI-Dialoge anzeigen
- Komprimieren
- HTTP-/SMTP-/Slack-/Teams-Versand
- Den Dump analysieren und zusammenfassen
- Die Ausnahme abfangen und fortsetzen
Ein Absturz-Handler ist keine Fortsetzung des normalen Verarbeitungsablaufs. Die Devise lautet: „nur die minimale lokale Schreiboperation ausführen und beenden”.
5.3 Was im Absturz-Handler zu tun ist
Umgekehrt ist das, was zu tun ist, denkbar einfach.
- Mehrfacheintritt verhindern
- Nur eine Zeile schreiben
- Flushen
- Beenden
In dieser Reihenfolge.
Nach Möglichkeit werden verwendet:
- ein vorab angelegtes dediziertes Verzeichnis,
- ein vorab auf Existenz geprüfter Pfad,
- ein Zielort mit vorab geprüfter ACL.
Zu häufiges Flushen ist beim normalen Protokoll teuer, aber der Fatal-Marker hat eine extrem geringe Anzahl an Einträgen, weshalb hier allein ein starkes Flushen unbedenklich ist.
In .NET FileStream.Flush(true), nativ FlushFileBuffers – wird diese eine Zeile als „das muss jetzt sofort auf die Platte” behandelt, lässt sich das Design deutlich einfacher gestalten.
5.4 Nicht versuchen, die Ausführung fortzusetzen
Bei einer unerwarteten Ausnahme, die auf einen Programmierfehler zurückgeht, ist es sicherer, den letzten Handler als Aufzeichnungsgerät, nicht als Wiederherstellungsgerät zu betrachten.
Besonders bei folgenden Fällen sollte „nicht fortsetzen” die Grundhaltung sein:
- Selbst eine
NullReferenceExceptionoderInvalidOperationExceptionkann mitten in der Aktualisierung eines gemeinsamen Zustands aufgetreten sein - Eine unerwartete Ausnahme auf dem UI-Thread
- Eine unerwartete Ausnahme, die aus einer Überwachungs- oder übergeordneten Schleife entkommen ist
AccessViolationExceptionStackOverflowException- Anomalien an der nativen Grenze
- CRT invalid parameter/purecall/terminate
Der Wunsch, „die App nicht abstürzen zu lassen”, ist verständlich, aber ein halb kaputtes Überleben ist für Diagnose und Betrieb häufig schmerzhafter.
Beim Beenden sollte man sofort beendende APIs in Betracht ziehen – in .NET Environment.FailFast, nativ RaiseFailFastException oder __fastfail – und das Design nicht auf finally-Blöcke oder normale Aufräumarbeiten stützen.
5.5 Minimale Implementierung in C#
Setzt man die bisherigen Richtlinien direkt in C# ab .NET 6 um, ergibt sich in etwa dieser Umfang. Weder DI noch ein Logger werden dabei verwendet.
using System;
using System.IO;
using System.Text;
using System.Threading;
internal static class FatalMarker
{
// Ein vorab angelegter, fest lokaler Pfad, dessen Schreibbarkeit bereits getestet wurde
private static readonly string LogDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MyApp", "Logs");
// Der Schlüssel für den Abgleich mit normalem Protokoll, Dump und Watchdog-Aufzeichnung
private static readonly string SessionId = Guid.NewGuid().ToString("N").Substring(0, 8);
// Flag zur Verhinderung von Mehrfacheintritt. 0 bedeutet noch nicht geschrieben
private static int _written;
/// <summary>Direkt nach dem App-Start genau einmal aufrufen.</summary>
public static void Install()
{
// CreateDirectory soll beim Absturz nicht mehr aufgerufen werden müssen, daher hier schon anlegen
Directory.CreateDirectory(LogDirectory);
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
}
/// <summary>Von der Stelle aufrufen, an der entschieden wurde: „hier darf nicht weitergemacht werden".</summary>
public static void FailNow(string reason)
{
Write("FailNow", null, reason);
Environment.FailFast(reason);
}
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Write("AppDomain.UnhandledException", e.ExceptionObject as Exception, null);
// Hier nicht beenden. Da es eine unbehandelte Ausnahme ist, geht die CLR danach ohnehin
// in ihre Standard-Beendigungsroutine über.
// Würde man hier FailFast aufrufen, würde die im WER hinterlegte Ursache des Dumps
// von der ursprünglichen Ausnahme zu FailFast wechseln.
}
private static void Write(string hook, Exception ex, string note)
{
// Ab dem zweiten Aufruf nichts mehr tun
if (Interlocked.Exchange(ref _written, 1) != 0)
{
return;
}
try
{
string fileName = string.Format(
"MyApp_fatal_{0:yyyyMMdd_HHmmss}_pid{1}_session-{2}.log",
DateTime.UtcNow, Environment.ProcessId, SessionId);
string path = Path.Combine(LogDirectory, fileName);
// Keinen Serializer aufrufen, die Zeile nur durch Zeichenkettenverkettung bauen
string line = string.Join("\t",
"ts=" + DateTime.UtcNow.ToString("O"),
"pid=" + Environment.ProcessId,
"tid=" + Environment.CurrentManagedThreadId,
"session=" + SessionId,
"hook=" + hook,
"type=" + (ex != null ? ex.GetType().FullName : "none"),
"message=" + Flatten(ex != null ? ex.Message : note),
"log=" + LogDirectory);
using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{
byte[] bytes = new UTF8Encoding(false).GetBytes(line + Environment.NewLine);
stream.Write(bytes, 0, bytes.Length);
// Bei true wird nicht nur der OS-Cache, sondern tatsächlich die Festplatte beschrieben
stream.Flush(true);
}
}
catch
{
// Schlägt das hier fehl, gibt es nichts mehr zu tun. Abfangen und beenden
}
}
private static string Flatten(string s)
{
return string.IsNullOrEmpty(s) ? string.Empty : s.Replace("\r", " ").Replace("\n", " ");
}
}
Auf der Aufrufseite genügt es, direkt nach dem Start Install() aufzurufen. Wird das vergessen, wirkt der obige Code nicht ein einziges Byte.
internal static class Program
{
private static void Main(string[] args)
{
FatalMarker.Install();
// Ab hier der normale App-Startvorgang
RunApplication(args);
}
private static void RunApplication(string[] args)
{
// Beispiel: Wird ein beschädigter gemeinsamer Zustand festgestellt, ohne Fortsetzung beenden
// FatalMarker.FailNow("device state inconsistent");
}
}
Diese 60 Zeilen setzen nur die vier in 5.3 genannten Punkte um.
Interlocked.Exchangeverhindert Mehrfacheintritt- Reine Zeichenkettenverkettung schreibt eine Zeile
Flush(true)bringt es auf die Festplatte- Nach
UnhandledExceptionwird nicht fortgesetzt
Environment.FailFast schreibt die Meldung in das Windows-Anwendungsereignisprotokoll, bevor der Prozess sofort beendet wird, und nimmt den Inhalt auch in die Fehlerberichterstattung auf. Das heißt, auch FailFast selbst erzeugt einen weiteren Beweis. Wie oben erwähnt, ändert ein Aufruf über den Pfad einer unbehandelten Ausnahme jedoch das Erscheinungsbild des Dumps – wählen Sie den Aufrufort daher bewusst.
6. Hinweise nach Framework
6.1 .NET allgemein: AppDomain.CurrentDomain.UnhandledException
Das ist nützlich als letzte Benachrichtigung. Aufwändige Wiederherstellungsarbeit sollte hier jedoch vermieden werden.
Die grundlegende Verwendung ist einfach.
- Den letzten Absturzmarker schreiben
- Bei Bedarf eine minimale Meldung im Windows-Ereignisprotokoll hinterlassen
- Nicht fortsetzen
- Hier nicht warten oder erneut versuchen
UnhandledException ist praktisch, aber es ist sicherer, hier nicht davon auszugehen, dass die App in einen gesunden Zustand zurückgeführt werden kann.
6.2 WinForms: Application.ThreadException
Das Schwierige daran ist, dass es unbehandelte Ausnahmen auf dem UI-Thread abfängt und scheinbar eine Fortsetzung ermöglicht.
Für den Einsatz, um erwartete Eingabefehler aus dem Geschäftsbetrieb in Dialoge umzuwandeln, mag das noch angehen, für die Fortsetzung nach unerwarteten, auf einem Programmierfehler beruhenden Ausnahmen eignet es sich jedoch nicht.
Wer die Ursachenermittlung wirklich priorisieren will, fährt sicherer mit:
- In
ThreadExceptionnur eine minimale Aufzeichnung vornehmen - Oder eher zu
UnhandledExceptionMode.ThrowExceptiontendieren - Und den Prozess anschließend beenden, wobei Dump und Protokolle erhalten bleiben
6.3 WPF: Application.DispatcherUnhandledException
Bei WPF ist es ähnlich.
- Im Fokus stehen nur Ausnahmen auf dem UI-Thread
- Mit
Handled = truelässt sich scheinbar fortsetzen - Tut man das jedoch bei einem Programmierfehler, geraten Bildschirmzustand und interner Zustand leicht auseinander
Daher ist es auch bei WPF ratsam, es nicht als Lebenserhaltungsgerät für die Fortsetzung, sondern als Einstiegspunkt für die Aufzeichnung zu nutzen.
6.4 TaskScheduler.UnobservedTaskException nicht zum Hauptpfad machen
Das ist nicht „die letzte Bastion kurz vor dem Absturz”.
Es lässt sich als Hilfsmittel nutzen, um entgangene Task-Ausnahmen zu erkennen,
aber als zuverlässiger Aufzeichnungspfad beim Absturz ist es schwach.
Nutzen Sie es also für:
- das frühzeitige Erkennen unbeobachteter Ausnahmen,
- das Aufspüren von
Task-Designlücken während der Entwicklung,
aber machen Sie es nicht zur Hauptrolle Ihres letzten Absturz-Handlers.
6.5 Natives Win32/C++: SetUnhandledExceptionFilter nicht überschätzen
Auf der nativen Seite ist man versucht, sich auf SetUnhandledExceptionFilter zu verlassen.
Da es jedoch im Kontext des fehlerauslösenden Threads läuft, ist es beeinflusst von:
- einem ungültigen Stack,
- tiefer Rekursion,
- einem bereits beschädigten Heap,
- beim Auftreten der Ausnahme gehaltenen Sperren.
SetUnhandledExceptionFilter sollte daher als
Best-Effort-Einstiegspunkt für den Empfang der letzten Benachrichtigung betrachtet werden.
6.6 Natives C++ muss auch die CRT-Beendigungswege erfassen
Bei nativem C++ entstehen Lücken, wenn man nur die unbehandelte SEH im Blick hat.
Konkret sollte man Folgendes beachten:
_set_invalid_parameter_handler_set_purecall_handlerset_terminate
Diese Gruppe dient dazu, von der C- und C++-Laufzeit ausgehende „Beendigungswege” zu erfassen.
In der Praxis ist es ratsam,
- auch in diesen Handlern den letzten Absturzmarker zu schreiben,
- dabei aber keine aufwändige Wiederherstellung vorzunehmen,
- zuverlässig zu beenden,
- die Hauptbeweise WER/dem Dump zu überlassen.
7. WER LocalDumps als Fundament
Das ist in der Praxis ziemlich stark.
7.1 Erste Empfehlung: WER LocalDumps
Im Sinne von „nach dem Absturz mit Tendenz zur Zuverlässigkeit minimale Beweise zu hinterlassen” ist WER LocalDumps zunächst am einfachsten zu handhaben.
Die Gründe sind einfach.
- Der Dump kann auf Betriebssystemseite gesichert werden
- Leicht einzuführen, ohne zusätzliche Tools
- Pro Anwendung konfigurierbar
- Die Hauptbeweise beim Absturz lassen sich aus dem In-Process-Bereich herausbringen
Was das Protokoll allein nicht verrät:
- welcher Thread abgestürzt ist,
- an welchem Stack er abgestürzt ist,
- an welcher Modulgrenze es passierte,
- ob managed/native/COM/SDK verdächtig ist.
Das lässt sich nachträglich einsehen – das ist die Stärke.
7.2 Typische Konfiguration
Um beispielsweise für MyApp.exe Dumps unter C:\CrashDumps\MyApp zu hinterlassen, kann man Folgendes verwenden:
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpFolder /t REG_EXPAND_SZ /d "C:\CrashDumps\MyApp" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpCount /t REG_DWORD /d 10 /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpType /t REG_DWORD /d 2 /f
Anfangs genügt eine so grobe Einstellung.
| Wert | Erste Empfehlung |
|---|---|
DumpFolder |
Ein dediziertes Verzeichnis |
DumpCount |
5 bis 10 |
DumpType |
Auf Entwicklungsrechnern 2, im Feld je nach Speicherplatz und Vertraulichkeitsanforderungen 1 oder 2 |
7.3 Unbedingt die ACL des Dump-Speicherorts prüfen
Wie bei Protokollen gilt auch hier: Ein Verzeichnis zu konfigurieren, in das nicht geschrieben werden kann, ist sinnlos.
Besonders bei
- Windows-Diensten,
- rechteseparierten Kindprozessen,
- eingeschränkten Konten auf Feldrechnern,
- UAC-Beteiligung
ist die ACL des Zielverzeichnisses die Hauptursache für ins Leere laufende Sammlungen.
Beim Zielort sollte Folgendes geprüft werden:
- vorherige Anlage,
- ein Schreibtest,
- eine Begrenzung der Aufbewahrungsanzahl,
- ob der Betrieb dort überhaupt nachsehen kann.
7.4 Wenn das aktuelle Protokoll dem WER-Bericht beigefügt werden soll
Wer WER-Berichte an Microsoft oder eine eigene WER-Pipeline nutzt, kann mit WerRegisterFile auch die aktuelle Protokolldatei zur Aufnahme in den Fehlerbericht registrieren.
Das sollte jedoch als zusätzlicher Kanal, nicht als Ersatz für die lokale Speicherung betrachtet werden. Was beim Absturz wirklich gebraucht wird, ist zunächst eine mit Tendenz zur Zuverlässigkeit lokal auf dem Gerät hinterlassene Aufzeichnung.
Die praxistaugliche Reihenfolge lautet:
- Lokales normales Protokoll
- Lokaler Fatal-Marker
- Lokaler Dump
- Bei Bedarf zusätzlich verwandte Dateien im WER-Übermittlungspfad registrieren
7.5 Nicht nur Dumps, sondern auch die Versionsverwaltung archivieren
Selbst mit einem Dump in der Hand ist man schwach aufgestellt, wenn später
- die EXE-/DLL-Dateien dieses Builds fehlen,
- die PDBs fehlen,
- unklar ist, aus welchem Commit der Build stammt.
Mindestens sollte Folgendes archiviert werden:
- die ausgelieferten Binärdateien,
- die zugehörigen PDBs,
- die Version,
- der Build-Zeitpunkt,
- die Commit-Kennung,
- die Installer-Version.
Dump-Sammlung und PDB-Archivierung gehören zusammen.
7.6 Bis zum ersten Öffnen des gesammelten Dumps
„Dumps sammeln sich an, aber niemand hat je einen geöffnet” kommt wirklich häufig vor. Die Vertiefung überlassen wir einem spezialisierten Artikel, hier stehen nur die ersten zehn Minuten.
Zwei Dinge müssen vorbereitet sein:
- Den Dump sowie PDB und ausgelieferte Binärdatei desselben Builds in einem gemeinsamen Verzeichnis sammeln
- WinDbg aus den Debugging Tools for Windows des Windows SDK bereitstellen
Danach öffnet man mit WinDbg die .dmp-Datei und gibt der Reihe nach ein:
.sympath srv*C:\symbols*https://msdl.microsoft.com/download/symbols;C:\myapp\pdb
.reload /f
.ecxr
!analyze -v
~*k
lm v
Die jeweilige Rolle:
| Befehl | Was er tut |
|---|---|
.sympath |
Legt den Suchort für Symbole fest. Sowohl der Microsoft-Symbolserver als auch der eigene PDB-Ablageort werden angegeben |
.reload /f |
Lädt die Symbole zwangsweise neu |
.ecxr |
Springt zum Registerkontext im Moment der Ausnahme. Vergisst man das, sieht man nicht die Absturzstelle, sondern den Warte-Stack auf WER-Seite |
!analyze -v |
Analysiert die aktuelle Ausnahme automatisch und zeigt Details an. Hier zuerst lesen |
~*k |
Gibt die Aufrufstapel aller Threads aus. Man sieht, was die anderen Threads neben dem abgestürzten getan haben |
lm v |
Gibt geladene Module und Versionen aus. Dient dem Abgleich zwischen Auslieferung und Build-Nummer |
Kommt man an diesen Punkt und „der Funktionsname fehlt”, „die Zeilennummer fehlt”, liegt die Ursache meist darin, dass die PDB von einem anderen Build stammt. Kehren Sie dann zur Versionsverwaltung aus 7.5 zurück.
Tiefergehende Sammlung und Analyse werden im verwandten Artikel am Ende behandelt.
8. Überlegungen zu MiniDumpWriteDump und eigenen Absturzberichten
Es gibt Situationen, in denen eine eigene Implementierung nötig ist.
- Über die UI soll eine Schaltfläche „Diagnoseinformationen speichern” angeboten werden
- Protokolle und Konfigurationsdateien sollen mitgebündelt werden
- Eine Gruppe von Kindprozessen soll gemeinsam behandelt werden
- Vor dem automatischen Upload soll eine eigene Maskierung erfolgen
Am wichtigsten ist hier jedoch, auch das Erstellen des Dumps nicht zu stark der abstürzenden Seite aufzubürden.
8.1 Ein separater Prozess ist besser als ein Self-Dump
MiniDumpWriteDump ist mächtig, aber
es aus einem separaten Prozess heraus aufzurufen ist sicherer, als es aus dem abgestürzten Prozess selbst heraus aufzurufen.
Typisch sieht die Konstruktion so aus:
- Der Worker selbst erkennt die Anomalie
- Wenn möglich, benachrichtigt er einen Helper über ein Ereignis oder eine Named Pipe
- Der Helper erstellt einen Dump des Workers
- Der Helper bündelt das
tail-Ende der Protokolle und der Konfigurationsdateien - Der Helper legt nach der Beendigung alles in eine Upload-Warteschlange
So bleibt die Helper-Seite gesund, selbst wenn der Worker beschädigt ist.
8.2 Wenn es unbedingt In-Process sein muss, auf einen dedizierten Thread verlagern
Auch wenn ein separater Prozess nicht möglich ist, ist es besser, einen dedizierten Thread ausschließlich für das Dumpen zu reservieren.
Dennoch bleibt es im Kern Best Effort. „Wir haben eine eigene Dump-Implementierung eingebaut, also sind wir zu 100 % sicher” gilt nicht.
8.3 Aufwändiges auf den nächsten Start verschieben
Dinge, die man in einem eigenen Reporter gerne umsetzen möchte:
- ZIP-Komprimierung
- Abgleich mit Symbolinformationen
- Server-Upload
- Bildschirmaufnahme
- Zusätzliche Informationen aus der Datenbank abrufen
Diese verschiebt man auf nach dem Neustart oder auf die Helper-Seite, nicht auf den Absturzzeitpunkt.
9. Was sich durch einen Überwachungsprozess ändert
Bei Systemen mit Dauerbetrieb zahlt sich ein Überwachungsprozess erheblich aus.
9.1 Was der Überwachungsprozess festhält
Auf Watchdog-/Launcher-/übergeordneter Dienstseite lassen sich folgende Informationen festhalten:
- Startzeitpunkt des Kindprozesses
- Startargumente
- PID
- Version des überwachten Ziels
- Zeitpunkt des letzten empfangenen Heartbeats
- Endzeitpunkt
- Exit Code
- Anzahl der Neustarts
- Ob ein Dump vorliegt
- Ob ein Neustart erfolgte
Allein damit lässt sich weitgehend erkennen,
- ob wirklich ein Absturz vorlag,
- ob es ein Betriebssystem-Herunterfahren war,
- ob der Benutzer die App geschlossen hat,
- ob sie wegen eines Hängers gekillt wurde,
- wie oft eine Neustartschleife durchlaufen wurde.
9.2 Besonders geeignete Fälle
Eine Trennung sollte man aktiv erwägen bei Fällen wie:
- einem Worker mit angebundenem Vendor-SDK,
- Bildverarbeitung/Videoverarbeitung/Geräte-I/O,
- übergeordneten Überwachungs- oder Polling-Schleifen,
- Skript- oder Plugin-Ausführung,
- dem Hosten bestehender COM-/ActiveX-Assets,
- 64-Bit-/32-Bit-Brücken und Interop.
Gefährliche Verarbeitung in einem einzigen Worker einzuschließen erleichtert sowohl das Protokolldesign als auch das Wiederherstellungsdesign.
10. Häufige Fehler
Hier sind Fallstricke auf Designebene gesammelt. Die verfahrenstechnische Frage „was darf man im Absturz-Handler nicht tun” ist in 5.2 zusammengefasst – wer gerade implementiert, sollte dort nachsehen. Bei überschneidend wirkenden Punkten (etwa HTTP-Versand in 10.3) beschreibt 5.2, „warum es im Handler problematisch ist”, während hier steht, „was daraus im Betrieb folgt”.
10.1 Mit catch (Exception) nur protokollieren und fortsetzen
Das ist der häufigste und gefährlichste Fall.
- Teilweise Änderungen bleiben bestehen
- Der gemeinsame Zustand wird beschädigt
- Folgefehler nehmen zu
- Der wahre Ursprungsort verschwimmt
Man gewinnt eine zusätzliche Protokollzeile, aber häufig zieht sich der Vorfall dadurch in die Länge.
10.2 Sich nur auf die Warteschlange des asynchronen Loggers verlassen
Asynchrones Protokollieren an sich ist nicht schlecht. Das Problem ist, auch auf dem Fatal-Pfad in dieselbe Warteschlange zu schreiben und es dabei zu belassen.
Stoppt der Worker im Moment des Absturzes, geht die gesamte Warteschlange mit verloren.
Es ist sicherer, einen Fluchtweg zu haben, bei dem allein der Fatal-Pfad direkt schreibt.
10.3 Im Absturz-Handler HTTP-Anfragen senden
Man ist versucht, das zu implementieren, aber es ist ziemlich gefährlich.
- DNS
- TLS
- Proxy
- Authentifizierung
- Timeouts
- Wartezeiten bei erneutem Versand
All das läuft im beschädigten Kontext ab.
Der Versand erfolgt nach dem Neustart.
10.4 Ein Dump existiert, lässt sich aber nicht mit dem normalen Protokoll verknüpfen
Das kommt häufig vor.
- Keine Session im Dump-Dateinamen
- Keine PID/Session im Protokoll
- Keine PID auf Watchdog-Seite
- Build-Nummern stimmen nicht überein
Das Ergebnis: Die drei Beweisstücke wirken wie drei getrennte Geschichten.
10.5 Mit dem Ereignis für unbehandelte Ausnahmen bei WinForms/WPF künstlich am Leben halten
Scheinbar „stürzt die App nicht mehr ab”, was zunächst Freude auslöst. Tatsächlich entsteht aber häufig ein Zombie-Zustand:
- Nur der Bildschirm bleibt bestehen
- Der Worker ist tot
- Nur die Schaltfläche bleibt aktiv
- Unklar, ob gespeichert wurde
10.6 Die nativen Beendigungswege nicht im Blick haben
Wer sich allein auf SetUnhandledExceptionFilter verlässt, übersieht
- invalid parameter,
- purecall,
- terminate,
- fast fail.
Bei nativem C++ ist es besser, nicht nur SEH, sondern auch die Beendigungswege der CRT/C++-Laufzeit im Blick zu behalten.
11. Mindest-Checkliste für die Einführung
Erfüllt man Folgendes, ist man bereits ziemlich praxistauglich aufgestellt.
- Das normale Protokoll hält ein Ereignis pro Zeile fest
- Alle Protokolleinträge enthalten UTC, PID, TID, Version und Session
ProcessStartundProcessExitwerden festgehalten- Wichtige Grenzereignisse werden synchron geflusht
- Es gibt eine dedizierte Datei für den letzten Absturzmarker
- Der Fatal-Pfad läuft nicht über den asynchronen Logger
- WER LocalDumps ist pro Anwendung konfiguriert
- Die ACL des Dump-Speicherorts wurde geprüft
- PDBs und ausgelieferte Binärdateien werden archiviert
- Beim nächsten Start lässt sich der vorherige abnormale Abbruch erkennen
- Komprimierung/Upload/Benachrichtigung erfolgen nach dem Neustart oder in einem separaten Prozess
- Bei nativem C++ wurden auch invalid parameter/purecall/terminate berücksichtigt
- Auf einem Testrechner wurde absichtlich ein Absturz provoziert und geprüft, ob wirklich alles erhalten bleibt
Die letzte Zeile ist besonders wichtig. Nur zu designen reicht nicht – der „Test, ob wirklich alles gesammelt wird” muss unbedingt durchgeführt werden.
12. Wie weit testen
Die zu prüfenden Punkte in einer Tabelle.
| Test | Was zu prüfen ist |
|---|---|
| Unbehandelte Managed-Ausnahme | Sind normales Protokoll, Fatal-Marker und Dump vollständig vorhanden? |
| UI-Thread-Ausnahme | Verhält sich der WinForms-/WPF-Ereignispfad wie vorgesehen? |
| Worker-Thread-Ausnahme | Kommt es bis zu AppDomain.UnhandledException? Kann der Watchdog es erkennen? |
| Native Ausnahme | Wird der WER-Dump wirklich erstellt? |
| invalid parameter/terminate | Bleibt auch über den CRT-/C++-Laufzeitpfad eine minimale Aufzeichnung erhalten? |
| Erzwungenes Kill | Kann der Watchdog den unerwarteten Exit erfassen, auch wenn In-Process nichts mehr tun kann? |
| Neustart | Funktionieren Benachrichtigung, Sammlung und Upload nach dem nächsten Start? |
Wichtig ist nicht die Annahme „bei einer Ausnahme sollte ein Protokoll erscheinen”, sondern die Prüfung: „unter dieser Bedingung bleibt genau diese Datei erhalten”.
13. Zusammenfassung
Wenn eine Windows-App auch beim Absturz durch eine von einem Programmierfehler verursachte Ausnahme die für die Untersuchung nötigen Informationen hinterlassen soll, sind die Denkachsen ziemlich einfach.
- Nicht allein auf den abstürzenden Prozess setzen
- Aufteilen in normales Protokoll, letzten Absturzmarker und Beweise auf Betriebssystem-/separater Prozessseite
- Beim Absturz nur kurz lokal aufzeichnen
- Aufwändige Verarbeitung auf den Neustart oder einen separaten Prozess verschieben
- WER LocalDumps als Fundament nutzen
- Aufzeichnen und beenden statt fortsetzen als Grundhaltung
Letztlich gilt: „Einen Aufbau zu schaffen, der auch ohne die letzte Zeile nachvollziehbar bleibt” schlägt „sich bei der letzten Zeile abzumühen”.
Trotzdem wünscht man sich diese letzte Zeile, also: Den letzten Absturzmarker kurz in einer separaten Datei hinterlassen. Und die eigentlichen Hauptbeweise dem WER-Dump und dem normalen Protokoll bis zum letzten Moment überlassen. Das ist in der Praxis von Windows-Anwendungen eine ziemlich stabile Vorgehensweise.
Verwandte Artikel
- Einführung in das Sammeln von Windows-Absturzabbildern - WER/ProcDump/WinDbg
- Entscheidungstabelle: Nach einer unerwarteten Ausnahme beenden oder fortsetzen?
Referenzlinks
- Microsoft Learn: Collecting User-Mode Dumps https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
- Microsoft Learn: Using WER https://learn.microsoft.com/en-us/windows/win32/wer/using-wer
- Microsoft Learn: MiniDumpWriteDump function https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump
- Microsoft Learn: SetUnhandledExceptionFilter function https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setunhandledexceptionfilter
- Microsoft Learn: System.AppDomain.UnhandledException event https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-appdomain-unhandledexception
- Microsoft Learn: Application.ThreadException Event https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.application.threadexception
- Microsoft Learn: Application.DispatcherUnhandledException Event https://learn.microsoft.com/en-us/dotnet/api/system.windows.application.dispatcherunhandledexception
- Microsoft Learn: TaskScheduler.UnobservedTaskException Event https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskscheduler.unobservedtaskexception
- Microsoft Learn: Environment.FailFast https://learn.microsoft.com/en-us/dotnet/api/system.environment.failfast
- Microsoft Learn: Registering for Application Recovery https://learn.microsoft.com/en-us/windows/win32/recovery/registering-for-application-recovery
- Microsoft Learn: RegisterApplicationRecoveryCallback https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-registerapplicationrecoverycallback
- Microsoft Learn: WerRegisterFile https://learn.microsoft.com/en-us/windows/win32/api/werapi/nf-werapi-werregisterfile
- Microsoft Learn: _set_invalid_parameter_handler https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/set-invalid-parameter-handler-set-thread-local-invalid-parameter-handler
- Microsoft Learn: _set_purecall_handler https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/get-purecall-handler-set-purecall-handler
- Microsoft Learn: set_terminate https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/set-terminate-crt
- Microsoft Learn: __fastfail https://learn.microsoft.com/en-us/cpp/intrinsics/fastfail
- Microsoft Learn: FileStream.Flush(Boolean) https://learn.microsoft.com/en-us/dotnet/api/system.io.filestream.flush
- Microsoft Learn: !analyze (WinDbg) https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-analyze
- Microsoft Learn: .ecxr (Display Exception Context Record) https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-ecxr–display-exception-context-record-
- Microsoft Learn: Symbol path for Windows debuggers https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/symbol-path
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Einführung in das Sammeln von Windows-Absturzabbildern - WER/ProcDump/WinDbg
Um schwer reproduzierbare Abstürze von Windows-Anwendungen aufzuklären, ordnen wir ein, wann WER LocalDumps, ProcDump, MiniDumpWriteDump ...
Process Monitor (ProcMon) Praxisleitfaden — „Konfiguration wird nicht gelesen“ und ACCESS DENIED in 10 Minuten identifizieren
„Ich habe die Konfigurationsdatei korrigiert, aber die Änderung wird nicht übernommen“ – „Bis gestern lief es noch, aber heute startet es...
Mit PerfView und dotnet-trace die Ursache von „langsam“ finden — Praxis-Einstieg in die .NET-Performanceanalyse
Wenn eine Geschäftsanwendung langsam ist, die CPU auslastet oder gelegentlich einfriert: Welches Werkzeug zeigt was? Dieser Beitrag ordne...
Mindestanforderungen an einen selbstgeschriebenen Logger und eine Checkliste für Integrationstests
Damit die Diagnoseprotokolle einer selbstgeschriebenen Anwendung vertrauenswürdig sind, ordnen wir UTF-8 JSON Lines, die Pflichtfelder, F...
Entscheidungstabelle: Nach einer unerwarteten Ausnahme beenden oder fortsetzen?
Wenn eine unerwartete Ausnahme auftritt, ordnet dieser Artikel ein, ob die Anwendung beendet oder fortgesetzt werden sollte – aus der Per...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Fehleranalyse und Langzeitprobleme
Sporadische Fehler, Kommunikationsdiagnose, Langzeitabstürze und Tests von Fehlerpfaden.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Fehleruntersuchung und Ursachenanalyse
Abstürze, die nur in Kundenumgebungen auftreten, selten reproduzierbare Programmabbrüche und die Ursachenanalyse durch den Abgleich von Dumps und Protokollen passen gut zur Fehleruntersuchung und Ursachenanalyse.
Windows-App-Entwicklung
Wie normale Protokollierung, WER und ein Watchdog für WPF, WinForms, residente Apps und Windows-Dienste gestaltet werden, ist unmittelbar mit der Windows-App-Entwicklung selbst verknüpft.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Kann eine abstürzende App selbst zuverlässig ein Protokoll hinterlassen?
- Nein. Bezieht man Stapelbeschädigung, Speicherbeschädigung, Fast Fail, Zwangsbeendigung und Stromausfall mit ein, ist das letzte In-Process-Protokoll grundsätzlich nur Best Effort. In der Praxis teilt man das in drei Schichten auf: die normale Zeitreihen-Protokollierung, den letzten Absturzmarker im Moment des Absturzes und die vom Betriebssystem oder einem separaten Prozess hinterlassenen Absturzbeweise – so entsteht ein Aufbau, der nicht allein auf das Innere des abstürzenden Prozesses setzt. Am unauffälligsten ist die Kombination aus normalem Protokoll, letztem Absturzmarker und WER LocalDumps.
- Was darf man in einem Absturz-Handler nicht tun?
- Grundsätzlich nichts Aufwändiges. Das Auflösen des Loggers über einen DI-Container, async/await, Warten auf Sperren, das Erstellen komplexer JSON-Strukturen, der Umgang mit COM-Objekten, UI-Dialoge, Komprimierung sowie HTTP-/SMTP-/Slack-Versand werden mit hoher Wahrscheinlichkeit zur Falle. Erlaubt sind nur vier Dinge: Mehrfacheintritt verhindern, eine einzige Zeile schreiben, flushen und beenden. Aufwändige Nachbearbeitung wie Komprimierung, Upload oder Benachrichtigung wird auf den nächsten Start oder einen separaten Prozess verschoben.
- Wie sollte WER LocalDumps konfiguriert werden?
- Unter dem Registrierungspfad HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\App-Name.exe konfigurieren Sie DumpFolder (ein dediziertes Verzeichnis), DumpCount (etwa 5 bis 10) und DumpType (auf Entwicklungsrechnern 2, im Feld je nach Speicherplatz und Vertraulichkeitsanforderungen 1 oder 2). Entscheidend ist die Prüfung der ACL des Zielverzeichnisses – ein typischer Fehler ist, ein Verzeichnis anzugeben, in das ein Windows-Dienst oder ein eingeschränktes Konto gar nicht schreiben kann, sodass nichts gesammelt wird. Außerdem gehören Dump-Sammlung und die Aufbewahrung von PDBs und ausgelieferten Binärdateien zusammen – fehlt eines von beiden, lässt sich der Dump später nicht mehr lesen.
- Darf man in WPFs DispatcherUnhandledException die Ausnahme abfangen und fortsetzen?
- Bei unerwarteten Ausnahmen, die auf einen Programmierfehler zurückgehen, ist das riskant. Mit Handled=true lässt sich die App scheinbar fortsetzen, doch dabei entsteht leicht ein Zombie-Zustand: Der Bildschirm bleibt bestehen, der Worker ist aber bereits tot, oder eine Schaltfläche bleibt aktiv, ohne dass klar ist, ob gespeichert wurde. Sicherer ist es, das Ereignis für unbehandelte Ausnahmen nur als Einstiegspunkt für die Aufzeichnung zu nutzen, nach dem Schreiben des letzten Absturzmarkers nicht fortzusetzen, sondern zu beenden, und die Hauptbeweise der WER-Dump und dem normalen Protokoll bis zum letzten Moment zu überlassen. Für die Beendigung empfiehlt sich eine sofort beendende API wie Environment.FailFast.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.