Concevoir la conservation des journaux et des dumps lors du crash d'une application Windows
· Mis à jour le: · Go Komura · Développement Windows, Gestion des exceptions, Journalisation, WER, Dump de crash, Investigation de bug
Ce qu’il y a de plus pénible dans l’investigation des dysfonctionnements d’une application Windows, c’est la situation où l’on sait seulement qu’elle a planté, sans qu’aucune trace n’explique pourquoi.
Ce problème devient particulièrement lourd dans des projets comme les suivants.
- Elle ne plante que dans l’environnement du client
- Elle ne plante qu’après un fonctionnement de longue durée
- WPF / WinForms / services Windows / applications résidentes, avec un faible taux de reproduction
- COM, P/Invoke, DLL natives ou SDK tiers sont impliqués
- On dispose seulement du « message d’exception », sans contexte sur ce qui précédait
Cela dit, pour être honnête d’emblée : il est impossible de garantir « à coup sûr » qu’un journal sera écrit par le seul processus qui plante. Dès lors que l’on inclut la corruption de pile, la corruption mémoire, le fast fail, l’arrêt forcé et la coupure d’alimentation, le dernier journal in-process reste fondamentalement du best effort.
Ce qu’il faut viser en pratique, c’est une configuration qui ne repose pas uniquement sur l’intérieur du processus qui plante. Autrement dit, on raisonne en trois couches :
- Le journal chronologique habituel
- Le marqueur de crash final au moment du plantage
- Les preuves de crash laissées par l’OS ou un processus séparé
Dans cet article, en nous plaçant du point de vue des applications de bureau Windows, des applications résidentes, des services Windows et des outils d’intégration avec des équipements, nous présentons les bonnes pratiques permettant de ne pas perdre la capacité d’investigation même quand l’application plante à cause d’une exception due à une erreur de programmation.
1. Les conclusions d’abord
Voici d’abord uniquement les conclusions.
- Le plus important est de ne pas miser « la dernière ligne de journal » sur un seul gestionnaire in-process.
- La combinaison la plus sûre en pratique est journal habituel + marqueur de crash final + WER LocalDumps.
- Pour un fonctionnement de longue durée, une intégration avec des équipements, des plugins ou un mélange de SDK natifs, ajouter un processus de surveillance (watchdog / launcher / service) renforce considérablement le dispositif.
- Dans un gestionnaire de crash, la règle d’or est de ne rien faire de lourd : la compression, l’envoi HTTP, la résolution DI, les boîtes de dialogue UI et la génération de JSON complexe sont à exclure.
- Au moment du crash, ne laissez qu’un enregistrement local bref ; reportez la compression, l’envoi et la notification au prochain démarrage ou à un processus séparé.
- Concevoir un système qui, via
ThreadExceptionde WinForms ouDispatcherUnhandledExceptionde WPF, maintient l’application en vie en apparence est dangereux face à une erreur de programmation. - Que ce soit en .NET ou en natif, pour les exceptions faisant suspecter un état corrompu, il est plus sûr de faire de « l’enregistrement suivi de l’arrêt » la base, plutôt que la « récupération ».
- Si vous collectez des dumps, vous devez archiver en même temps les PDB et les binaires distribués, sinon vous ne pourrez plus les lire par la suite.
En résumé, la bonne pratique consiste à : « Ne pas essayer de tout faire au moment du crash. Répartir les rôles entre avant le crash, au moment du crash et après le crash. »
2. Pourquoi le in-process seul ne peut pas être « fiable »
Laisser ce point flou fait vaciller la conception.
2.1 Le contexte du thread qui plante peut lui-même être corrompu
Les hooks d’exception non gérée et les filtres d’exception de premier niveau peuvent s’exécuter dans le contexte du thread corrompu. À ce stade, il est tout à fait normal que :
- la pile soit déjà dangereuse
- une allocation supplémentaire soit dangereuse à cause d’une corruption du tas
- attendre bloque à cause des verrous détenus au moment de l’exception
- les objets dont dépend le logger lui-même soient déjà corrompus
Il est donc plus sûr de considérer le gestionnaire final non pas comme « un endroit où tout est possible », mais comme « un endroit où très peu de choses sont possibles ».
2.2 Le fast fail et les exceptions d’état corrompu supposent une « activité in-process minimale »
En cas de corruption mémoire ou d’état fatal, mieux vaut ne pas compter sur le traitement d’exception habituel.
En particulier, la famille __fastfail côté natif et les anomalies faisant suspecter un état corrompu sont conçues pour « se terminer immédiatement avec le moins de surcoût possible ».
Autrement dit, il est naturel de considérer que le dernier journal in-process est un bonus s’il parvient à s’écrire, et que la preuve principale se trouve du côté de l’OS ou d’un processus séparé.
2.3 L’événement d’exception non gérée de .NET n’est pas non plus un lieu pour une « récupération lourde »
AppDomain.UnhandledException de .NET est pratique, mais il vaut mieux considérer que ce que l’on peut y faire se limite à un enregistrement bref.
- Il peut être affecté par les verrous détenus au moment de l’exception
- Il ne peut pas capturer en toute sécurité absolument tout, y compris les exceptions d’état corrompu
- Forcer une politique de continuation ici rend facile le maintien en vie d’un processus à moitié brisé
Il est réaliste de considérer « l’événement d’exception non gérée = la dernière notification », et non « un point de récupération sûr ».
3. Architecture recommandée - séparer le moment du crash et l’après-redémarrage
La façon la plus simple de s’organiser consiste à séparer ce qui se passe au moment du crash de ce qui se passe après le redémarrage.
| Phase | Objectif | Où cela s’exécute | Ce qu’on fait |
|---|---|---|---|
| Fonctionnement normal | Conserver la chronologie | Dans l’application | Journalisation structurée, heartbeat, événements de frontière |
| Au moment du crash | Déposer le minimum de preuves | Dans l’application + OS | Marqueur de crash final, dump WER |
| Juste après la sortie | Détecter une sortie inattendue | Processus séparé | Enregistrement du code de sortie, décision de redémarrage, notification |
| Après le prochain démarrage | Effectuer les traitements lourds | Un nouveau processus sain | Compression, envoi, notification à l’utilisateur, nettoyage des anciens journaux |
Cette répartition rend la conception nettement plus stable.
3.1 Configuration minimale
Pour un petit outil métier ou une application WPF / WinForms à usage interne, cela suffit souvent pour commencer.
- Journal habituel : un fichier local en ajout seul (append-only)
- Marqueur de crash final : un fichier court dédié
- Dump : WER LocalDumps
- Au prochain démarrage : afficher « L’application s’est terminée anormalement la dernière fois. Des informations de diagnostic sont disponibles. »
3.2 Configuration renforcée
Il vaut mieux passer à un niveau supérieur dans des cas comme ceux-ci.
- Fonctionnement 24 h/24, 7 j/7
- Contrôle d’équipement, surveillance, fonctionnement résident
- Beaucoup de COM / P/Invoke / SDK natifs
- Présence de processus enfants, de plugins ou d’exécution de scripts
- Un « blocage prolongé » est inacceptable dans l’environnement du client
Dans ce cas, répartir en :
- Processus worker : le traitement principal
- Launcher / watchdog / service : supervision du démarrage, enregistrement de la sortie, redémarrage
- WER LocalDumps : côté worker
- Prochain démarrage ou watchdog : collecte des informations de diagnostic
rend le dispositif nettement plus adapté à la pratique.
4. Bonnes pratiques pour le journal habituel
Si vous essayez de vous battre avec seulement la dernière ligne au moment du crash, vous perdez généralement. Ce qui fonctionne vraiment, c’est le journal habituel jusqu’à l’instant précédent.
4.1 Un journal est fait d’« informations corrélables plus tard », pas de « prose pour humains »
Voici les éléments minimaux à inclure dans le journal habituel.
- Horodatage UTC
- Temps écoulé depuis le démarrage du processus
- PID / TID
- Nom de l’application, version, numéro de build, identifiant de commit
- ID de session
- ID d’opération / ID de job / ID de corrélation
- Nom de module / nom d’écran / nom de worker
- Les derniers effets externes
- Écritures de fichiers
- Mises à jour de base de données
- Envoi de commandes à l’équipement
- Requêtes de communication
- Type d’exception, HRESULT / erreur Win32 / code d’exception
- Résumé des principaux paramètres d’entrée
- Les identifiants de la cible, dans la mesure où ils ne contiennent aucune donnée sensible
Nous recommandons le format une ligne = un événement, en JSON Lines ou en key=value.
Plutôt que de laisser de longues phrases pour des humains, ce qui compte davantage, c’est de « pouvoir recouper trois fichiers plus tard ».
4.2 Écrire les événements critiques de façon synchrone
Rendre toutes les écritures du journal habituel synchrones alourdit le système. Mais confier tout à un buffer asynchrone fait tout disparaître d’un coup au moment du crash.
Il est donc réaliste, en pratique, de faire varier le traitement selon le niveau.
- Les événements fins de niveau
Information: la mise en buffer est acceptable Warninget au-dessus : flusher tôt- Les événements de frontière importants : les écrire de façon synchrone
- ProcessStart
- ConfigLoaded
- WorkerStarted
- ExternalCommandSent
- TransactionCommitted
- RecoveryStarted
- FatalPathEntered
L’essentiel est que les frontières métier, elles au moins, touchent bien le sol.
4.3 Séparer « le journal habituel en cours d’écriture » du « marqueur de crash final »
Ceci compte énormément.
Si vous essayez de tout faire tenir dans un seul rolling log :
- il était en cours de rotation
- il restait dans la file asynchrone
- le logger lui-même est mort juste après l’exception
- la ligne de journal a été coupée en plein milieu
voilà ce qui arrive.
Nous recommandons donc de séparer au moins en deux fichiers :
app-<session>.jsonlLe journal chronologique habituelfatal-last.logoufatal-<session>.logDédié au marqueur de crash final
Le simple fait de savoir clairement « où va la dernière ligne » aide énormément sur le terrain.
4.4 Destination des journaux : chemin local fixe, jamais un emplacement réseau
Compter au moment du crash sur un chemin UNC, un NAS, HTTP ou une API cloud est dangereux, à cause :
- des coupures réseau momentanées
- des délais DNS
- de l’expiration des identifiants
- de l’attente sur le thread UI
- du manque de droits du compte de service
Au moment du crash, écrivez d’abord sur un chemin local fixe. L’envoi se fait après le prochain démarrage ou depuis un processus séparé.
4.5 Mettre la session dans le nom de fichier
La date seule ne suffit pas, car l’application redémarre plusieurs fois le même jour.
Par exemple, sous cette forme :
Logs\
MyApp_20260318_101530_pid1234_session-4f1c.jsonl
MyApp_fatal_20260318_101533_pid1234_session-4f1c.log
MyApp_watchdog_20260318.jsonl
Le simple fait de rendre évident « de quelle instance de lancement il s’agit » change considérablement la vitesse d’analyse.
5. Bonnes pratiques pour le marqueur de crash final
Ce n’est pas l’endroit où construire un logger complet. C’est l’endroit où écrire une seule fois, brièvement, en visant la fiabilité.
5.1 L’objectif est de « fixer le point d’entrée », pas les « détails de la cause »
Les informations à inclure dans le marqueur de crash final sont plus efficaces si on les limite.
- UTC de l’occurrence
- PID / TID
- ID de session
- Version / numéro de build
- De quel hook cela provient
AppDomain.UnhandledExceptionApplication.ThreadExceptionDispatcherUnhandledExceptionSetUnhandledExceptionFilter_set_invalid_parameter_handlerset_terminate
- Type d’exception ou code d’exception
- Un message court si possible
- L’ID de la dernière opération
- Le nom de fichier du journal habituel
- Le dossier de dump attendu
Cela suffit amplement.
5.2 Ce qu’il ne faut pas faire dans un gestionnaire de crash
Chacun de ces points a une forte probabilité de devenir une mine.
- Résoudre le logger depuis un conteneur DI
- Utiliser async/await
- Lancer des Tasks
- Attendre sur des verrous
- Construire du JSON complexe
- Manipuler des objets COM
- Afficher des boîtes de dialogue UI
- Compresser
- Envoyer via HTTP / SMTP / Slack / Teams
- Analyser et résumer le dump
- Avaler l’exception et continuer
Un gestionnaire de crash n’est pas la continuation du flux de traitement normal. Orientez-le vers « se contenter d’une écriture locale minimale, puis se terminer ».
5.3 Ce qu’il faut faire dans un gestionnaire de crash
À l’inverse, ce qu’il faut faire est très simple.
- Empêcher les entrées multiples
- Écrire une seule ligne
- Flusher
- Se terminer
Dans cet ordre.
Si possible, utilisez :
- un dossier dédié créé à l’avance
- un chemin dont l’existence a été vérifiée à l’avance
- une destination dont les ACL ont été vérifiées à l’avance
Flusher trop souvent le journal habituel est lourd, mais le marqueur fatal est extrêmement peu volumineux : c’est le seul endroit où l’on peut flusher fortement.
En .NET, FileStream.Flush(true) ; en natif, FlushFileBuffers : traiter cette ligne unique comme devant « toucher le sol immédiatement » facilite la conception.
5.4 Ne pas essayer de maintenir l’application en vie
Face à une exception inattendue provenant d’une erreur de programmation, il est plus sûr de considérer le gestionnaire final comme un dispositif d’enregistrement, pas de récupération.
Voici en particulier les cas où « ne pas continuer » doit être la base :
- Même pour un
NullReferenceExceptionou unInvalidOperationException, si c’était en pleine mise à jour d’un état partagé - Une exception inattendue sur le thread UI
- Une exception inattendue qui a fui d’une boucle de surveillance ou d’une boucle parente
AccessViolationExceptionStackOverflowException- Une anomalie à la frontière native
- invalid parameter / purecall / terminate du CRT
Le désir de « ne pas faire planter » est compréhensible, mais survivre à moitié brisé est souvent plus pénible, tant pour le diagnostic que pour l’exploitation.
Pour terminer le processus, envisagez des API de terminaison immédiate — Environment.FailFast en .NET, RaiseFailFastException ou __fastfail en natif — et concevez le système sans compter sur les blocs finally ni sur un nettoyage normal.
6. Points d’attention par framework
6.1 .NET en général : AppDomain.CurrentDomain.UnhandledException
C’est utile comme la dernière notification. Mais évitez toute récupération lourde à cet endroit.
L’usage de base est simple.
- Écrire le marqueur de crash final
- Si nécessaire, laisser un message minimal dans le journal d’événements Windows
- Ne pas continuer
- Ne pas attendre ni réessayer ici
UnhandledException est pratique, mais il est plus sûr de ne pas partir du principe qu’on peut, à cet endroit, ramener l’application à un état sain.
6.2 WinForms : Application.ThreadException
Ce qui est délicat ici, c’est qu’il capture les exceptions non gérées du thread UI et permet à l’application de continuer, du moins en apparence.
L’utiliser pour transformer en boîte de dialogue des erreurs métier attendues, passe encore ; mais il ne convient pas pour continuer après une exception inattendue provenant d’une erreur de programmation.
Si l’investigation de la cause est vraiment la priorité :
- Ne faire qu’un enregistrement minimal dans
ThreadException - ou basculer vers
UnhandledExceptionMode.ThrowException - puis terminer le processus, en laissant le dump et les journaux
c’est plus sûr.
6.3 WPF : Application.DispatcherUnhandledException
C’est similaire pour WPF.
- Seules les exceptions sur le thread UI sont principalement visées
- Mettre
Handled = truepermet de continuer en apparence - mais faire cela face à une erreur de programmation fait facilement diverger l’état de l’écran et l’état interne
C’est pourquoi, en WPF aussi, il est préférable de l’utiliser comme point d’entrée pour l’enregistrement, et non comme dispositif de maintien en vie pour la continuation.
6.4 Ne pas faire de TaskScheduler.UnobservedTaskException le chemin principal
Ce n’est pas « le dernier rempart juste avant le crash ».
Il peut aider à détecter les exceptions de Task passées entre les mailles, mais il est faible comme voie d’enregistrement fiable au moment du crash.
Utilisez-le donc pour :
- Détecter tôt les exceptions non observées
- Faire ressortir, pendant le développement, les lacunes de conception des
Task
mais ne lui confiez pas le rôle principal de votre gestionnaire de crash final.
6.5 Win32 natif / C++ : ne pas trop faire confiance à SetUnhandledExceptionFilter
Côté natif, on est tenté de compter sur SetUnhandledExceptionFilter.
Cependant, il s’exécute dans le contexte du thread fautif, donc il est affecté par :
- une pile invalide
- une récursion profonde
- un tas déjà corrompu
- les verrous détenus au moment de l’exception
Il convient donc de considérer SetUnhandledExceptionFilter comme un point d’entrée best effort pour recevoir la dernière notification.
6.6 En C++ natif, couvrir aussi les chemins de terminaison du CRT
En C++ natif, se contenter de surveiller le SEH non géré laisse passer des cas.
Concrètement, il faut surveiller aussi :
_set_invalid_parameter_handler_set_purecall_handlerset_terminate
Cette famille sert à capturer les « chemins de terminaison » issus du runtime C et du runtime C++.
En pratique, l’approche saine est :
- Écrire le marqueur de crash final également dans ces gestionnaires
- mais sans faire de récupération lourde
- Terminer de façon fiable
- Confier la preuve principale à WER / au dump
7. Construire sur WER LocalDumps
Ce point est particulièrement puissant en pratique.
7.1 La première recommandation : WER LocalDumps
Dans le sens de « laisser après le crash le minimum de preuves, en visant la fiabilité », WER LocalDumps est, pour commencer, le plus simple à mettre en œuvre.
Les raisons sont simples.
- L’OS peut conserver le dump lui-même
- Facile à mettre en place sans outil supplémentaire
- Configurable par application
- Permet de faire sortir la preuve principale du crash hors du in-process
Ce que le journal seul ne peut pas dire :
- Quel thread a planté
- Sur quelle pile il a planté
- Quelle était la frontière de module concernée
- Où se situe le suspect : managed / natif / COM / SDK
Pouvoir observer cela après coup est un atout majeur.
7.2 Configuration typique
Par exemple, pour conserver les dumps de MyApp.exe dans C:\CrashDumps\MyApp, on peut procéder ainsi :
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
Ce niveau de simplification suffit pour commencer.
| Valeur | Recommandation initiale |
|---|---|
DumpFolder |
Un dossier dédié |
DumpCount |
5 à 10 |
DumpType |
2 sur les machines de développement ; 1 ou 2 sur le terrain, selon l’espace disque et les exigences de confidentialité |
7.3 Toujours vérifier les ACL de la destination des dumps
Comme pour les journaux, configurer un dossier où l’on ne peut pas écrire ne sert à rien.
En particulier, avec :
- Les services Windows
- Les processus enfants à privilèges séparés
- Les comptes restreints des machines sur le terrain
- Les cas impliquant l’UAC
les ACL de la destination sont la principale cause d’échecs silencieux.
Pour la destination, vérifiez jusqu’au bout :
- la création préalable
- un test d’écriture
- la limite du nombre de fichiers conservés
- si le personnel d’exploitation peut effectivement s’y rendre
7.4 Quand on veut joindre le journal actuel à un rapport WER
Si vous utilisez les rapports WER envoyés à Microsoft ou votre propre circuit WER, il existe aussi la méthode consistant à appeler WerRegisterFile pour enregistrer le fichier de journal actuel afin de l’inclure dans le rapport d’erreur.
Cependant, il est plus sûr de considérer cela comme un canal additionnel, pas un substitut au stockage local. Ce que l’on veut vraiment au moment du crash, c’est d’abord un enregistrement fiable sur la machine locale.
L’ordre pratique est le suivant :
- Journal habituel local
- Marqueur fatal local
- Dump local
- Si nécessaire, enregistrer aussi les fichiers associés dans le circuit d’envoi WER
c’est le plus adapté à la pratique.
7.5 Conserver aussi le suivi de version, pas seulement le dump
Même en disposant d’un dump, si plus tard :
- les EXE / DLL de cette époque n’existent plus
- les PDB manquent
- on ne sait pas de quel commit provient la build
vous vous retrouvez dans une position fragile.
Conservez au minimum :
- Les binaires distribués
- Les PDB correspondants
- La version
- La date et l’heure de build
- L’identifiant de commit
- La version de l’installeur
La collecte de dumps et l’archivage des PDB vont de pair.
8. Comment penser l’usage de MiniDumpWriteDump ou d’un rapporteur de crash maison
Il existe des situations où une implémentation maison devient nécessaire.
- Vous voulez un bouton « Enregistrer les informations de diagnostic » dans l’UI
- Vous voulez regrouper aussi les journaux et les fichiers de configuration
- Vous voulez traiter ensemble un groupe de processus enfants
- Vous voulez appliquer un masquage personnalisé avant l’envoi automatique
Mais le plus important ici est de ne pas trop charger le côté qui plante, y compris avec la prise de dump elle-même.
8.1 Un processus séparé plutôt que l’auto-dump
MiniDumpWriteDump est puissant, mais il est plus sûr de l’appeler depuis un processus séparé que depuis l’intérieur du processus qui vient de planter.
Une configuration typique ressemble à ceci.
- Le worker principal détecte l’anomalie
- Si possible, il notifie un helper via un événement ou un named pipe
- Le helper prend le dump du worker
- Le helper regroupe la fin (
tail) des journaux et les fichiers de configuration - Le helper place le tout dans une file d’envoi après la fin
Ainsi, même si le worker est brisé, le helper reste sain.
8.2 Si le in-process est incontournable, dédier un thread
Même quand la séparation en processus distinct n’est pas possible, réserver un thread dédié au dump est déjà mieux.
Cela reste néanmoins fondamentalement du best effort. « On a ajouté une implémentation de dump maison, donc c’est fiable à 100 % » n’est pas vrai.
8.3 Reporter les tâches lourdes au prochain démarrage
Voici des choses que l’on est souvent tenté d’inclure dans un rapporteur maison.
- Compression zip
- Recoupement avec les informations de symboles
- Envoi au serveur
- Capture d’écran
- Récupération d’informations supplémentaires depuis la base de données
Tout cela doit être reporté après le redémarrage ou au niveau du helper, pas au moment du crash.
9. Ce que change l’ajout d’un processus de surveillance
Pour les systèmes en fonctionnement de longue durée, un processus de surveillance apporte beaucoup.
9.1 Ce que le processus de surveillance enregistre
Du côté du watchdog / launcher / service parent, on peut conserver des informations comme :
- Heure de démarrage du processus enfant
- Arguments de lancement
- PID
- Version de la cible surveillée
- Heure de réception du dernier heartbeat
- Heure de sortie
- Code de sortie
- Nombre de redémarrages
- Présence ou non d’un dump
- Si un redémarrage a eu lieu
Cela seul permet de voir assez clairement :
- S’il s’agit réellement d’un crash
- S’il s’agissait d’un arrêt de l’OS
- Si l’utilisateur l’a fermé
- S’il a été tué après s’être figé (hang)
- Combien de fois il a bouclé sur des redémarrages
9.2 Les cas particulièrement adaptés
Il est pertinent d’envisager activement la séparation dans des cas comme :
- Un worker embarquant un SDK tiers
- Le traitement d’image / vidéo / les E/S d’équipement
- Une boucle parente de surveillance ou de polling
- L’exécution de scripts ou de plugins
- L’hébergement d’actifs COM / ActiveX existants
- Le pontage ou l’interopérabilité 64 bits / 32 bits
Confiner les traitements dangereux dans un seul worker facilite à la fois la conception des journaux et celle de la récupération.
10. Anti-patterns courants
10.1 catch (Exception) qui se contente de logger et de continuer
C’est le plus fréquent, et le plus dangereux.
- Des modifications partielles subsistent
- L’état partagé est corrompu
- Les défaillances en cascade se multiplient
- Le véritable point d’origine devient flou
En échange d’une ligne de journal supplémentaire, l’incident traîne souvent en longueur.
10.2 Faire confiance uniquement à la file du logger asynchrone
La journalisation asynchrone en elle-même n’est pas mauvaise. Le problème est de se contenter d’empiler dans la même file, même sur le chemin fatal.
Si le worker s’arrête au moment du crash, toute la file disparaît avec lui.
Il est plus sûr de conserver une échappatoire où seul le chemin fatal écrit directement.
10.3 Envoyer des requêtes HTTP depuis le gestionnaire de crash
On est tenté de l’implémenter, mais c’est très dangereux.
- DNS
- TLS
- Proxy
- Authentification
- Délais d’attente
- Attente de retransmission
Tout cela repose sur le contexte du crash.
Envoyez après le redémarrage.
10.4 Le dump existe, mais ne se relie pas au journal habituel
C’est fréquent.
- Pas de session dans le nom du fichier de dump
- Pas de PID / session côté journal
- Pas de PID côté watchdog
- Les numéros de build ne correspondent pas
Résultat : les trois preuves ont l’air de raconter trois histoires distinctes.
10.5 Maintenir l’application en vie via les événements d’exception non gérée de WinForms / WPF
Comme l’application « ne plante plus » en apparence, cela plaît d’abord. Mais en réalité, cela tend à créer un état zombie où :
- Seul l’écran survit
- Le worker est mort
- Seule l’activation du bouton subsiste
- On ne sait pas si l’enregistrement a réussi
10.6 Ne pas surveiller les chemins de terminaison côté natif
Si l’on se rassure avec SetUnhandledExceptionFilter seul, on laisse échapper le côté :
- invalid parameter
- purecall
- terminate
- fast fail
En C++ natif, il vaut mieux rester attentif non seulement au SEH, mais aussi aux chemins de terminaison côté CRT / runtime C++.
11. Liste de contrôle minimale pour la mise en place
Si vous satisfaites les points suivants, vous êtes dans une configuration très opérationnelle.
- Le journal habituel enregistre un événement par ligne
- Chaque ligne de journal contient UTC, PID, TID, version et session
ProcessStartetProcessExitsont enregistrés- Les événements de frontière importants sont flushés de façon synchrone
- Il existe un fichier dédié au marqueur de crash final
- Le chemin fatal ne passe pas par le logger asynchrone
- WER LocalDumps est configuré par application
- Les ACL de la destination des dumps ont été vérifiées
- Les PDB et les binaires distribués sont archivés
- Le prochain démarrage peut détecter la sortie anormale précédente
- La compression / l’envoi / la notification se font après redémarrage ou dans un processus séparé
- En C++ natif, invalid parameter / purecall / terminate ont aussi été pris en compte
- Vous avez délibérément fait planter l’application sur une machine de test et vérifié que les preuves sont réellement conservées
La dernière ligne est particulièrement importante. La concevoir ne suffit pas : il faut absolument effectuer le test de « capture complète ».
12. Jusqu’où tester
Voici, sous forme de tableau, les points à vérifier.
| Test | Ce qu’il faut vérifier |
|---|---|
| Exception non gérée managée | Le journal habituel, le marqueur fatal et le dump sont-ils tous présents ? |
| Exception sur le thread UI | Le chemin d’événement WinForms / WPF se comporte-t-il comme prévu ? |
| Exception sur le thread worker | Atteint-elle AppDomain.UnhandledException ? Le watchdog peut-il la détecter ? |
| Exception native | Le dump WER est-il vraiment capturé ? |
| invalid parameter / terminate | Un enregistrement minimal est-il conservé même sur les chemins CRT / runtime C++ ? |
| Kill forcé | Même si le in-process ne peut rien faire, le watchdog peut-il enregistrer la sortie inattendue ? |
| Redémarrage | La notification, la collecte et l’envoi fonctionnent-ils après le prochain démarrage ? |
Ce qui compte, ce n’est pas de se dire « un journal devrait apparaître si une exception survient », mais de vérifier que « dans telle condition, tel fichier est bien conservé ».
13. Conclusion
Si vous voulez qu’une application Windows conserve les informations nécessaires à l’investigation même quand elle plante à cause d’une exception due à une erreur de programmation, les axes de réflexion sont assez simples.
- Ne pas compter uniquement sur le processus qui plante
- Répartir entre journal habituel, marqueur de crash final et preuves du côté OS / processus séparé
- Au moment du crash, ne laisser qu’un enregistrement local bref
- Reporter les traitements lourds après le redémarrage ou vers un processus séparé
- Construire sur WER LocalDumps
- Faire de l’enregistrement suivi de l’arrêt la base, plutôt que la continuation
Au final, « construire une configuration traçable même sans la dernière ligne » l’emporte sur « s’acharner sur la dernière ligne ».
On souhaite tout de même cette dernière ligne, donc écrivez le marqueur de crash final brièvement, dans un fichier séparé. Et confiez la véritable preuve principale au dump WER et au journal habituel jusqu’à l’instant précédent. C’est là une façon de travailler très stable dans la pratique des applications Windows.
Articles connexes
- Introduction à la collecte des dumps de crash des applications Windows - Comment choisir entre WER / ProcDump / WinDbg
- Lorsqu’une exception inattendue survient, faut-il arrêter ou continuer l’application ? - Le tableau de décision à consulter en premier
Références
- 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
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Introduction à la collecte des dumps de crash Windows - WER/ProcDump/WinDbg
Pour traquer les crashs d'applications Windows difficiles à reproduire, nous passons en revue l'utilisation de WER LocalDumps, ProcDump, ...
Où catch et la journalisation doivent-ils se situer dans la gestion des exceptions ?
Pour éviter les catch trop larges dans les helpers profonds, les logs dupliqués à chaque couche et la transformation en résultat qui masq...
Exigences minimales pour un logger maison, avec une checklist de tests d'intégration
Pour rendre fiables les journaux de diagnostic d'une application maison, nous détaillons le format UTF-8 JSON Lines, les champs obligatoi...
Un tableau de décision pour choisir entre arrêt et poursuite après une exception inattendue
Lorsqu'une exception inattendue survient, faut-il arrêter l'application ou la laisser continuer ? Cet article organise la décision sous l...
Construire un socle de test des cas anormaux Windows avec Application Verifier
Ce qu'est Application Verifier, expliqué avec la construction d'un socle de test des cas anormaux Windows à l'aide de Handles, Heaps, Low...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Analyse de bugs et incidents de longue durée
Pannes intermittentes, diagnostic des communications, crashs après longue exécution et tests des chemins d'échec.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Analyse des bugs et des causes
Les crashs qui ne surviennent que dans l'environnement du client, les arrêts anormaux à faible taux de reproduction et l'analyse de cause racine par recoupement des dumps et des journaux sont des thèmes qui s'accordent bien avec l'investigation de bug et l'analyse de cause racine.
Développement d'applications Windows
La façon de concevoir la journalisation habituelle, WER et le watchdog pour WPF, WinForms, les applications résidentes et les services Windows est directement liée au développement d'applications Windows lui-même.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Comment puis-je m'assurer que mon application Windows laisse des preuves lorsqu'elle plante ?
- Non, ne misez pas tout sur le seul processus qui plante : un dernier journal in-process reste fondamentalement du best effort dès lors que la corruption de pile, la corruption mémoire, le fast fail ou l'arrêt forcé entrent en jeu. La configuration la plus sûre en pratique combine trois couches : un journal chronologique habituel écrit en fonctionnement normal, un marqueur de crash final bref écrit au moment du plantage, et des preuves de crash laissées par l'OS via WER LocalDumps. Pour les applications de longue durée ou intégrées à des équipements, ajouter un processus watchdog ou launcher qui enregistre les codes de sortie et le nombre de redémarrages rend la configuration nettement plus robuste.
- Que doit faire, et ne pas faire, un gestionnaire de crash ?
- La règle d'or est de ne rien faire de lourd. Évitez la résolution de logger via un conteneur DI, async/await, le lancement de Tasks, l'attente sur des verrous, la construction de JSON complexe, la manipulation d'objets COM, les boîtes de dialogue UI, la compression et l'envoi HTTP/SMTP : tout cela repose sur un contexte potentiellement corrompu. Ce que doit faire un gestionnaire de crash est simple : empêcher les entrées multiples, écrire une seule ligne courte dans un fichier local dédié, la flusher fermement (FileStream.Flush(true) en .NET, FlushFileBuffers en natif), puis se terminer. La compression, l'envoi et la notification sont reportés au prochain démarrage ou à un processus séparé et sain.
- Faut-il utiliser DispatcherUnhandledException ou ThreadException pour maintenir l'application en vie après un crash ?
- Non, pas face à des erreurs de programmation. DispatcherUnhandledException de WPF avec Handled=true et ThreadException de WinForms permettent à l'application de continuer en apparence, mais faire cela face à une exception inattendue crée facilement un état zombie où seul l'écran survit, le worker est mort, et personne ne sait si l'enregistrement a réussi. Utilisez ces événements comme point d'entrée pour l'enregistrement, puis terminez le processus en laissant le dump et les journaux. Pour les exceptions suggérant un état corrompu, enregistrer puis quitter via Environment.FailFast est une base plus sûre qu'une tentative de récupération.
- Pourquoi ai-je besoin des PDB et des informations de version en plus des dumps de crash ?
- Un dump seul est une preuve faible si les binaires livrés de cette build ont disparu, si les PDB manquent, ou si personne ne sait de quel commit provient la build. Archivez ensemble les binaires livrés, les PDB correspondants, la version, l'horodatage de build et l'identifiant de commit : la collecte de dumps et l'archivage des PDB vont de pair. Rendez également les preuves recoupables : placez l'ID de session et le PID dans les noms des fichiers de journal et de dump, afin que le journal habituel, le marqueur fatal et le dump racontent une seule histoire cohérente plutôt que trois histoires indépendantes.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.
Liens publics