L'arrêt de Windows vu depuis l'application — survivre correctement aux notifications de fermeture, aux redémarrages et aux coupures d'alimentation
· Go Komura · Windows, Arrêt, Développement Windows, Services Windows, PC d'équipement, Intégrité des données, Fonctionnement de longue durée, UPS
« Après un redémarrage nocturne de Windows Update, l’application de mesure sur le PC d’équipement s’est arrêtée en milieu d’écriture, et le matin le fichier de mesure était corrompu. » « Quelqu’un s’est déconnecté d’un PC partagé et s’est plaint que des modifications non enregistrées avaient disparu. » — Pour les applications Windows de longue durée, ces deux consultations sont des classiques.
Ce que les deux sites ont en commun, c’est de traiter l’arrêt comme « un événement anormal qui ne devrait pas se produire ». En réalité, toutefois, des redémarrages automatiques de Windows Update, de la déconnexion utilisateur et d’un arrêt initié par UPS jusqu’à une coupure d’alimentation non annoncée, les événements qui coupent l’exécution depuis l’extérieur de l’application viendront, tôt ou tard. Vous ne pouvez pas empêcher qu’ils viennent. Ce que vous pouvez empêcher, c’est « de perdre des données lorsqu’ils viennent ».
Heureusement, Windows a un mécanisme qui notifie l’application avant l’arrêt, pour les applications graphiques, les applications console et les services. Destiné au personnel informatique des PME et aux développeurs d’applications Windows (surtout de PC d’équipement et d’applications de longue durée), cet article organise comment recevoir ces notifications, comment concevoir un nettoyage qui « se termine en quelques secondes », la récupération automatique après un redémarrage, et comment se préparer à une coupure d’alimentation qui n’apporte aucune notification — le tout ancré dans les sources primaires Microsoft Learn à août 2026.
1. La conclusion, d’abord
- Concevez l’arrêt comme « un événement normal qui viendra, tôt ou tard ». Le temps dont vous disposez après avoir reçu la notification n’est, en principe, que d’environ 5 secondes, donc une conception qui enregistre frénétiquement tout sur place s’effondrera. Le prérequis est une sauvegarde automatique fréquente pour que « le delta qui doit être enregistré à l’arrêt » reste petit.1
- Sur les OS clients à partir de Windows 8, lorsque le démarrage rapide est activé (le défaut sur la plupart des PC qui prennent en charge la mise en veille prolongée), « Arrêter » est un arrêt hybride, et le noyau ne fait que se mettre en veille prolongée. La seule chose qui est pleinement réinitialisée est « Redémarrer ». C’est la vraie raison de « je l’ai arrêté et ça n’a pas été mieux, puis j’ai redémarré et ça l’a été ».2
- Une application graphique doit renvoyer TRUE immédiatement à WM_QUERYENDSESSION, et faire le nettoyage dans WM_ENDSESSION. En principe vous ne devez pas renvoyer FALSE (refuser).1
- Seulement lorsque vous avez vraiment une opération qui ne peut pas être interrompue devez-vous afficher un motif avec ShutdownBlockReasonCreate. Même alors l’utilisateur et l’OS peuvent forcer la continuation, donc une conception qui suppose « nous pouvons bloquer » ne tient pas.34
- Une application console reçoit la notification avec SetConsoleCtrlHandler. Le délai de grâce est encore plus court — un défaut de 5 secondes pour la fermeture de console. Il y a aussi un piège : dans un processus qui a chargé gdi32.dll ou user32.dll, certains de ces événements n’arrivent pas.56
- Le nettoyage qui s’appuie sur AppDomain.ProcessExit de .NET ne s’exécute pas, à partir de .NET 10, sur les chemins où le processus est « terminé de l’extérieur ». Sur une sortie normale telle que le retour de Main il s’exécute encore comme avant, mais parce que le runtime ne fournit plus de gestion par défaut des signaux de terminaison tels que la fermeture de console et l’arrêt, le nettoyage sur ces chemins doit passer à la notification qui correspond au modèle d’application.7
- Un service Windows peut recevoir SERVICE_ACCEPT_PRESHUTDOWN plus tôt, et avec un délai de grâce configurable, que SERVICE_ACCEPT_SHUTDOWN (environ 20 secondes de grâce). Le délai d’expiration PRESHUTDOWN par défaut, toutefois, a été raccourci à 10 secondes à partir de Windows 10 Creators Update, donc dans les deux cas vous avez besoin d’une conception qui ne s’appuie pas trop sur le délai de grâce.89
- La récupération automatique après un redémarrage peut s’obtenir en combinant RegisterApplicationRestart avec ARSO (ouverture de session automatique). Des chemins de récupération sont fournis pour le plantage, « Ne répond pas », et le redémarrage déclenché par une mise à jour.1011
- Une coupure d’alimentation n’apporte aucune notification du tout. Le motif standard est d’écrire complètement dans un fichier temporaire, vider, et échanger avec ReplaceFile, mais parce que ReplaceFile ne garantit pas non plus l’atomicité à travers une coupure d’alimentation, un chemin de récupération de sauvegarde (.bak) plus validation au chargement fait partie de l’ensemble. L’isolement après coup peut se faire depuis le journal des événements (1074/41/6008).121314
En une phrase, la conclusion de cet article est : « gardez toujours un état depuis lequel vous pouvez fermer boutique en quelques secondes lorsque la notification arrive, et écrivez d’une façon qui ne casse pas même sur une coupure d’alimentation qui n’apporte aucune notification ».
2. Ce qui se passe à l’arrêt — Quatre façons de se terminer
2.1. Déconnexion, arrêt, redémarrage et coupure d’alimentation
Du point de vue de l’application, ce qui compte sont deux axes : « comment la session utilisateur se termine » et « que devient le noyau ».
| Opération | Session utilisateur | Noyau et pilotes | Notification à l’application |
|---|---|---|---|
| Déconnexion | Se termine | Continue de tourner | WM_QUERYENDSESSION (ENDSESSION_LOGOFF) → WM_ENDSESSION |
| Arrêt (démarrage rapide activé) | Se termine | Se met en veille prolongée (enregistré dans hiberfil.sys) | WM_QUERYENDSESSION → WM_ENDSESSION, (PRE)SHUTDOWN aux services |
| Redémarrage | Se termine | Se termine complètement ; le prochain démarrage est un démarrage complet | Comme ci-dessus |
| Coupure d’alimentation | Disparaît immédiatement | Disparaît immédiatement | Aucune |
La déconnexion et l’arrêt sont, du point de vue de l’application, presque le même événement. Si le bit ENDSESSION_LOGOFF est positionné dans le lParam de WM_QUERYENDSESSION c’est une déconnexion ; s’il est 0 c’est un arrêt ou un redémarrage (vous ne pouvez pas distinguer les deux).1 Autrement dit, la complaisance de « ce n’est qu’une déconnexion, ça ira » ne tient pas, et la conception correcte est que le même code de nettoyage soit appelé.
flowchart TB
accTitle: Quatre façons de se terminer, et la notification à l'application
accDescr: La déconnexion, l'arrêt et le redémarrage délivrent la notification WM_QUERYENDSESSION vers WM_ENDSESSION, et le nettoyage se termine en quelques secondes. Seule une coupure d'alimentation n'a aucune notification, donc vous préparez avec la conception d'écriture du chapitre 8 et un UPS
signout["Déconnexion"] --> notified["QUERY → ENDSESSION"]
shutdown["Arrêt"] --> notified
reboot["Redémarrage"] --> notified
poweroff["Coupure d'alimentation"] --> none["Aucune notif. : écriture + UPS"]
notified --> cleanup["Nettoyage en secondes"]
Figure 1 : La déconnexion, l’arrêt et le redémarrage délivrent la notification WM_QUERYENDSESSION vers WM_ENDSESSION, et le nettoyage se termine en quelques secondes. Seule une coupure d’alimentation n’a aucune notification, donc vous préparez avec la conception d’écriture du chapitre 8 et un UPS.
2.2. La vraie raison de « je l’ai arrêté et ça n’a pas été mieux » — l’arrêt hybride
La ligne facile à manquer est la deuxième du tableau. Sur les OS clients à partir de Windows 8, le démarrage rapide (arrêt hybride) est activé par défaut sur les PC qui prennent en charge la mise en veille prolongée, et le comportement de « Arrêter » a changé. La déconnexion de la session utilisateur se produit encore comme d’habitude, mais la session noyau n’est pas fermée ; elle est enregistrée, pilotes de périphériques et tout, dans le fichier de mise en veille prolongée (hiberfil.sys) et restaurée telle quelle au prochain démarrage. Cela rend le démarrage plus rapide, mais l’état du noyau et des pilotes survit même après que vous avez coupé l’alimentation.2 C’est toutefois un comportement conditionnel. Dans un environnement où la mise en veille prolongée elle-même est désactivée (powercfg /hibernate off), où une stratégie ou Options d’alimentation a désactivé le démarrage rapide, et sur Windows Server, l’arrêt est un arrêt complet conventionnel. Vous pouvez dire de quel côté un PC donné tourne depuis la case « Activer le démarrage rapide » dans Options d’alimentation, ou depuis le fait que powercfg /a (états de veille disponibles) liste « Fast Startup ».
flowchart TB
accTitle: Ce qui arrive au noyau sur une opération Arrêter
accDescr: Une opération Arrêter se scinde en un arrêt complet ou une mise en veille prolongée du noyau selon que le démarrage rapide est activé, et Redémarrer fait toujours un démarrage complet
op["Arrêter"]
restart["Redémarrer"]
op -->|"Démarrage rapide activé"| hybrid["Session se termine + hibernate noyau"]
op -->|"Hibernate désactivé / Server"| full["Arrêt complet"]
hybrid --> resume["Ensuite : restaurer le noyau"]
full --> boot["Ensuite : démarrage complet"]
restart --> boot
Figure 2 : Une opération Arrêter se scinde en un arrêt complet ou une mise en veille prolongée du noyau selon que le démarrage rapide est activé, et Redémarrer fait toujours un démarrage complet.
« Redémarrer », en revanche, exécute toujours un cycle de démarrage complet. Après une mise à jour de pilote, par exemple, vous avez besoin d’un état complètement nouveau.2 À partir de là, plusieurs phénomènes que vous entendez sur le terrain s’expliquent.
- « Je l’ai arrêté et rallumé, mais l’ennui de périphérique n’est pas parti » — le noyau et les pilotes n’ont été que restaurés depuis la mise en veille prolongée ; ils n’ont pas été réinitialisés
- « Ça a été mieux après que j’ai redémarré » — parce qu’un démarrage complet les a initialisés
- Les procédures d’incident de PC d’équipement devraient dire « Redémarrer », pas « l’éteindre et le rallumer »
Si vous voulez rendre un arrêt complet explicite depuis la ligne de commande, shutdown /s (le défaut de Shutdown.exe est un arrêt complet) ; si vous voulez le comportement hybride par défaut, shutdown /s /hybrid.2 Désactiver le démarrage rapide n’est pas recommandé. Le côté application devrait supposer « à l’arrêt le noyau peut seulement être en veille prolongée » — par exemple, ne pas estimer le « temps de fonctionnement cumulé » à partir de l’heure de démarrage de l’OS — et concevoir pour ne casser ni d’un côté ni de l’autre (que le démarrage rapide soit activé ou non diffère selon l’environnement).
3. Comment une application graphique doit se comporter — WM_QUERYENDSESSION et WM_ENDSESSION
3.1. Comment les deux messages scindent le travail
Une application qui a une fenêtre et une file de messages est notifiée de la fin de session en deux étapes.1
- WM_QUERYENDSESSION — une requête : « est-il acceptable de terminer ? » L’application doit renvoyer TRUE immédiatement ; la réponse par défaut de DefWindowProc est aussi TRUE. Ne commencez pas le nettoyage ici.
- WM_ENDSESSION (wParam=TRUE) — une notification engagée : « la session se termine vraiment ». Le nettoyage se produit ici.
Renvoyer FALSE à WM_QUERYENDSESSION peut interrompre l’arrêt, mais la documentation est explicite que « vous devez renvoyer TRUE et respecter l’intention de l’utilisateur », et une application qui a renvoyé FALSE est encore exposée dans l’interface plein écran comme « une application qui empêche l’arrêt ». Les applications console et les applications sans fenêtre visible ne peuvent pas interrompre l’arrêt en premier lieu, et si elles ne répondent pas dans les 5 secondes elles sont terminées automatiquement.14
flowchart TB
accTitle: Flux de la notification de fin de session en deux étapes
accDescr: Renvoyer TRUE à la requête WM_QUERYENDSESSION engage avec WM_ENDSESSION et le nettoyage s'exécute. Refuser avec FALSE affiche l'application comme une qui empêche l'arrêt, et environ 5 secondes sans réponse peuvent forcer la continuation
q["WM_QUERYENDSESSION"]
q -->|"TRUE(règle)"| e["WM_ENDSESSION(engagé)"]
q -->|"FALSE(refuser)"| blocked["Affiché comme bloquant l'arrêt"]
q -->|"Pas de réponse ~5 s"| hung["Traité comme bloqué"]
e --> cleanup["Nettoyage ici"]
cleanup --> term["Sortie du processus"]
hung --> term
blocked -->|"Forcer la continuation"| term
blocked -->|"Annuler"| cont["Arrêt interrompu"]
Figure 3 : Renvoyer TRUE à la requête WM_QUERYENDSESSION engage avec WM_ENDSESSION et le nettoyage s’exécute. Refuser avec FALSE affiche l’application comme une qui empêche l’arrêt, et environ 5 secondes sans réponse peuvent forcer la continuation.
3.2. Ce qui se passe si vous ne répondez pas — le mur des 5 secondes
Sur WM_QUERYENDSESSION comme sur WM_ENDSESSION, vous pouvez retarder la réponse d’environ 5 secondes. Au-delà, le système affiche l’écran « Cette application empêche l’arrêt », et l’utilisateur peut choisir de forcer la continuation (= forcer la terminaison de l’application).4 Un processus forcé n’a pas une autre chance de terminer son enregistrement.
Les points de conception sont donc ces deux-là.
- Gardez le nettoyage à une quantité qui se termine dans les 5 secondes. Microsoft lui-même recommande d’enregistrer les données fréquemment en fonctionnement ordinaire pour que moins doive être enregistré à l’arrêt, et d’enregistrer les données non enregistrées dans un emplacement temporaire pour restaurer au prochain lancement.1
- Ne mettez pas de boîte de confirmation pendant l’arrêt. Pendant que vous attendez sur « Voulez-vous enregistrer ? », les 5 secondes passent. Tombez silencieusement du côté sûr (sauvegarde automatique).
3.3. Implémentation dans WinForms et WPF
Dans une application de bureau .NET, ces messages sont traduits en événements du framework. Dans WinForms, FormClosing est levé, et CloseReason vous dit si l’arrêt en est la cause.
// WinForms: FormClosing is also raised on shutdown / sign-out
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.WindowsShutDown)
{
// Do only an idempotent snapshot save. Do not show a dialog.
// Do not set e.Cancel = true (refuse) either.
SaveWorkingStateToTempFile();
return;
}
// For ordinary closes such as the user clicking the × button, you may confirm here
}
Dans WPF, l’événement Application.SessionEnding (l’attribut XAML SessionEnding, ou une surcharge OnSessionEnding) correspond.
flowchart TB
accTitle: Comment les événements WinForms/WPF correspondent aux messages
accDescr: La phase de requête de WM_QUERYENDSESSION correspond à WinForms FormClosing et WPF SessionEnding, et ce que vous y faites est au plus un enregistrement de snapshot idempotent. Il n'y a pas d'événement correspondant pour le WM_ENDSESSION engagé, donc recevez-le dans WndProc ou un hook et faites le nettoyage qui ne peut s'exécuter qu'après l'engagement
q["WM_QUERYENDSESSION"] --> fc["WinForms : FormClosing"]
q --> se["WPF : SessionEnding"]
fc -.-> idem["Snapshot idempotent seulement"]
se -.-> idem
e["WM_ENDSESSION"] --> hook["Pas d'événement : hook WndProc"]
hook -.-> final["Nettoyage après engagement"]
Figure 4 : La phase de requête de WM_QUERYENDSESSION correspond à WinForms FormClosing et WPF SessionEnding, et ce que vous y faites est au plus un enregistrement de snapshot idempotent. Il n’y a pas d’événement correspondant pour le WM_ENDSESSION engagé, donc recevez-le dans WndProc ou un hook et faites le nettoyage qui ne peut s’exécuter qu’après l’engagement.
// WPF: App.xaml.cs
protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
{
base.OnSessionEnding(e);
// You can distinguish ReasonSessionEnding.Logoff / Shutdown,
// but the baseline is to run the same snapshot save in either case
SaveWorkingStateToTempFile();
// Do not set e.Cancel = true unless you have an exceptional reason
}
Il y a une mise en garde ici. FormClosing (CloseReason.WindowsShutDown) comme SessionEnding de WPF correspondent à la phase de requête (WM_QUERYENDSESSION). Si une autre application refuse, l’arrêt est interrompu et votre application continue de tourner. Donc ce que vous pouvez faire dans ces événements est un enregistrement de snapshot idempotent qui ne fait pas de mal si l’arrêt est interrompu et produit le même résultat peu importe combien de fois il s’exécute. Si vous avez besoin d’un « nettoyage qui ne doit être fait que lorsque nous terminons vraiment » (déconnexion, restitution de ressources, et ainsi de suite), accrochez le WM_ENDSESSION engagé (wParam=TRUE) directement dans WndProc et faites-le là.
Sur l’un ou l’autre chemin, pliez le corps dans une fonction commune « enregistrement de snapshot » et écrivez les données de restauration pour une sortie normale, un arrêt, et (si possible) un plantage dans le même format, afin que la logique de restauration au prochain lancement soit un seul chemin. Concevoir pour laisser des informations même lors d’un plantage est couvert dans « Concevoir la conservation des journaux et des dumps lors du crash d’une application Windows ».
4. Si vous devez vraiment bloquer — ShutdownBlockReasonCreate
Les opérations qui cassent physiquement si elles sont coupées en cours de route, telles qu’écrire un CD ou un micrologiciel, sont l’exception. La pratique correcte ici est d’enregistrer une chaîne de motif avec ShutdownBlockReasonCreate lorsque l’opération ininterruptible commence, et d’appeler ShutdownBlockReasonDestroy immédiatement lorsqu’elle se termine. Lorsque l’arrêt est demandé, ce motif est affiché sur l’écran « Cette application empêche l’arrêt », et l’utilisateur peut décider de continuer ou d’annuler.3
flowchart TB
accTitle: Flux de la protection avec ShutdownBlockReasonCreate
accDescr: Enregistrez un motif lorsque l'opération ininterruptible commence ; si une demande d'arrêt arrive pendant la protection, le motif est affiché en plein écran et WM_QUERYENDSESSION est refusé avec FALSE. L'utilisateur peut annuler ou forcer la continuation, et le motif est effacé lorsque l'opération se termine
begin["Démarrer le travail ininterruptible"] --> reg["ShutdownBlockReasonCreate"]
reg --> work["Exécuter sur un thread worker"]
work --> done["Terminer : Destroy"]
req["Arrêt pendant ceci"] --> show["Afficher le motif + FALSE"]
show -->|"Annuler"| work
show -->|"Forcer la continuation"| kill["Sortie du processus"]
Figure 5 : Enregistrez un motif lorsque l’opération ininterruptible commence ; si une demande d’arrêt arrive pendant la protection, le motif est affiché en plein écran et WM_QUERYENDSESSION est refusé avec FALSE. L’utilisateur peut annuler ou forcer la continuation, et le motif est effacé lorsque l’opération se termine.
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool ShutdownBlockReasonCreate(IntPtr hWnd, string pwszReason);
[DllImport("user32.dll", SetLastError = true)]
static extern bool ShutdownBlockReasonDestroy(IntPtr hWnd);
// Call from the thread that created the main window (it fails from other threads)
_criticalOperationInProgress = true;
ShutdownBlockReasonCreate(this.Handle, "Writing measurement data to a file");
try
{
// Run the uninterruptible operation on a worker thread. If you run it
// synchronously on the UI thread the message pump stops, and the process
// is force-continued as "Not Responding" before the WM_QUERYENDSESSION
// refusal code below can run
await Task.Run(() => WriteMeasurementData());
}
finally
{
ShutdownBlockReasonDestroy(this.Handle);
_criticalOperationInProgress = false;
}
// Also, refuse WM_QUERYENDSESSION with FALSE only while protected
protected override void WndProc(ref Message m)
{
const int WM_QUERYENDSESSION = 0x0011;
if (m.Msg == WM_QUERYENDSESSION && _criticalOperationInProgress)
{
m.Result = IntPtr.Zero; // Refuse. The registered reason string is shown in the full-screen UI
return;
}
base.WndProc(ref m);
}
Le malentendu facile ici est la scission des rôles. Tout ce que ShutdownBlockReasonCreate fait, c’est enregistrer une chaîne de motif ; il n’arrête pas lui-même l’arrêt. Ce qui retient réellement l’arrêt est votre propre gestion qui renvoie FALSE à WM_QUERYENDSESSION pendant qu’un drapeau de protection est positionné, comme ci-dessus. Utilisez les deux comme un ensemble, et effacez les deux immédiatement lorsque l’opération se termine. Aussi, exécutez l’opération protégée elle-même sur un thread worker et gardez le thread UI capable de traiter les messages — le mécanisme de refus ne fonctionne qu’une fois le message arrivé (et même alors l’utilisateur et l’OS peuvent forcer la continuation, donc une conception d’écriture qui ne casse pas « s’il ne s’est pas arrêté » — chapitre 8 — est encore requise).
Il y a trois mises en garde opérationnelles.
- Gardez la chaîne de motif courte et spécifique. L’utilisateur est pressé et ne lira que quelques secondes. La documentation elle-même donne « Burning a CD » comme exemple approprié.3
- Ne le laissez pas enregistré pendant toute la vie de l’application. « Seulement pendant qu’une opération ininterruptible est en cours » est ce que l’API suppose.
- Ne concevez pas sur l’hypothèse que vous pouvez bloquer. L’utilisateur peut choisir de forcer la continuation, et un arrêt forcé (ENDSESSION_CRITICAL) n’attendra pas en premier lieu. La documentation est explicite : « Applications should not depend on being able to block shutdown ».4
5. Comment les applications console et les processus d’arrière-plan doivent se comporter
5.1. SetConsoleCtrlHandler et un délai de grâce court
Une application console ne peut pas recevoir de messages de fenêtre, donc les signaux de contrôle arrivent à une fonction gestionnaire enregistrée avec SetConsoleCtrlHandler. Le délai de grâce par défaut par signal est le suivant.5
| Signal | Quand il se produit | Délai de grâce par défaut |
|---|---|---|
| CTRL_C_EVENT / CTRL_BREAK_EVENT | Ctrl+C / Ctrl+Break | Pas de délai d’expiration |
| CTRL_CLOSE_EVENT | Fermeture de la console, « Fin de tâche » du Gestionnaire des tâches (un kill forcé de processus depuis l’onglet « Détails » est une sortie immédiate sans notification, et est hors de ce tableau) | Environ 5 secondes |
| CTRL_SHUTDOWN_EVENT | Arrêt du système (processus de service) | Environ 20 secondes |
Il y a deux points à surveiller. Premier, essentiellement seul un processus qui tourne comme service peut recevoir CTRL_LOGOFF_EVENT et CTRL_SHUTDOWN_EVENT. Une application dans une session interactive est terminée à la déconnexion, donc une conception qui attend ces signaux ne tient pas.5 Deuxième, un processus qui a chargé gdi32.dll ou user32.dll est traité comme une application Windows même si vous le pensez comme une application console, et les gestionnaires LOGOFF/SHUTDOWN ne sont pas appelés. La solution de contournement officielle est de créer une fenêtre cachée et de recevoir WM_QUERYENDSESSION/WM_ENDSESSION.6
flowchart TB
accTitle: Délai de grâce par signal console
accDescr: Ctrl+C et Ctrl+Break n'ont pas de délai d'expiration explicite ; la fermeture de console a environ 5 secondes et un signal d'arrêt vers un processus de service a environ 20 secondes ; le dépasser force la terminaison du processus
ctrlc["CTRL_C / BREAK"] -->|"Pas de délai"| handler["Nettoyage HandlerRoutine"]
closeev["CTRL_CLOSE"] -->|"Environ 5 s"| handler
shutev["CTRL_SHUTDOWN"] -->|"Environ 20 s"| handler
handler --> timeout["Kill forcé après grâce"]
Figure 6 : Ctrl+C et Ctrl+Break n’ont pas de délai d’expiration explicite ; la fermeture de console a environ 5 secondes et un signal d’arrêt vers un processus de service a environ 20 secondes ; le dépasser force la terminaison du processus.
// Console app: clean up on Ctrl+C and console close
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool SetConsoleCtrlHandler(HandlerRoutine handler, bool add);
delegate bool HandlerRoutine(int ctrlType); // 2 = CTRL_CLOSE_EVENT
static readonly HandlerRoutine s_handler = OnCtrlEvent; // Keep a reference so GC does not collect it
static bool OnCtrlEvent(int ctrlType)
{
// Do only cleanup that finishes within 5 seconds
FlushAndCloseDataFile();
return false; // Proceed to the default handler; the process exits
}
static void Main()
{
SetConsoleCtrlHandler(s_handler, add: true);
// ...
}
5.2. Le piège .NET — ne vous fiez pas à ProcessExit
En .NET il y a longtemps eu un motif stock de « nettoyer simplement dans AppDomain.ProcessExit », mais à partir de .NET 10 le runtime ne fournit plus de gestionnaires de signal de terminaison par défaut, et ni ProcessExit ni AssemblyLoadContext.Unloading ne se déclenchent sur CTRL_CLOSE_EVENT/CTRL_SHUTDOWN_EVENT. Le gestionnaire par défaut de l’OS termine simplement le processus immédiatement.7
flowchart TB
accTitle: Comment ProcessExit a changé dans .NET 10
accDescr: Jusqu'à .NET 9 le gestionnaire de signal par défaut du runtime recevait le signal de terminaison, levait ProcessExit, puis sortait. À partir de .NET 10 le runtime ne fournit pas de gestionnaire par défaut, la gestion par défaut de l'OS termine le processus immédiatement, et vous enregistrez un gestionnaire vous-même
sig["CTRL_CLOSE / SHUTDOWN"] --> old9["Jusqu'à .NET 9 : ProcessExit"]
sig --> new10["À partir de .NET 10 : sortie immédiate"]
new10 -.-> alt["Enregistrer un gestionnaire vous-même"]
Figure 7 : Jusqu’à .NET 9 le gestionnaire de signal par défaut du runtime recevait le signal de terminaison, levait ProcessExit, puis sortait. À partir de .NET 10 le runtime ne fournit pas de gestionnaire par défaut, la gestion par défaut de l’OS termine le processus immédiatement, et vous enregistrez un gestionnaire vous-même.
À la place, passez au chemin canonique pour chaque modèle d’application.
- Application graphique : FormClosing / SessionEnding du chapitre précédent
- Generic Host (y compris Worker Service) : IHostApplicationLifetime et BackgroundService.StopAsync. Rendez le délai de grâce d’arrêt explicite avec HostOptions.ShutdownTimeout
- Application console nue : SetConsoleCtrlHandler (ou abonnez-vous aux équivalents SIGINT/SIGTERM avec PosixSignalRegistration)
flowchart TB
accTitle: Où chaque modèle d'application reçoit la notification de fermeture
accDescr: Une application graphique utilise FormClosing et SessionEnding plus un hook WM_ENDSESSION pour le travail engagé ; Generic Host utilise IHostApplicationLifetime et StopAsync ; une application console nue utilise SetConsoleCtrlHandler ou PosixSignalRegistration. Se fier à ProcessExit ne se déclenche pas sur les chemins de signal externe
model{"Quel modèle d'application ?"}
model -->|"Graphique"| gui["FormClosing / SessionEnding"]
model -->|"Pas graphique"| other{"Host ou console ?"}
gui -.-> guihook["Hook ENDSESSION"]
other -->|"Host"| host["Lifetime + StopAsync"]
other -->|"Console"| con["SetConsoleCtrlHandler"]
host -.-> hostto["Définir ShutdownTimeout"]
con -.-> ngx["Ne pas se fier à ProcessExit"]
Figure 8 : Une application graphique utilise FormClosing et SessionEnding plus un hook WM_ENDSESSION pour le travail engagé ; Generic Host utilise IHostApplicationLifetime et StopAsync ; une application console nue utilise SetConsoleCtrlHandler ou PosixSignalRegistration. Se fier à ProcessExit ne se déclenche pas sur les chemins de signal externe.
Le délai de grâce diffère par chemin — environ 5 secondes pour la fermeture graphique et console, le délai de grâce SCM du chapitre 6 pour un service (environ 20 secondes, ou la valeur configurée pour PRESHUTDOWN), et pas de délai d’expiration explicite pour Ctrl+C. Sur chaque chemin, toutefois, le délai de grâce est limité et on ne peut pas compter dessus, donc l’axe de conception est que le cas normal est « déjà enregistré à chaque point de contrôle de traitement », pas « travailler dur dans l’événement de sortie ».
6. Comment un service Windows doit se comporter — SHUTDOWN et PRESHUTDOWN
6.1. Deux sortes de notification d’arrêt
Un service n’est pas affecté par la déconnexion, mais il est arrêté à l’arrêt et au redémarrage. La notification arrive comme un code de contrôle du Service Control Manager (SCM), et la recevoir exige de déclarer un drapeau d’acceptation.8
| Déclaration | Notification qui arrive | Moment et délai de grâce |
|---|---|---|
| SERVICE_ACCEPT_SHUTDOWN | SERVICE_CONTROL_SHUTDOWN | Notifié pendant le traitement d’arrêt. Défaut environ 20 secondes, plafond WaitToKillServiceTimeout |
| SERVICE_ACCEPT_PRESHUTDOWN | SERVICE_CONTROL_PRESHUTDOWN | Notifié avant SHUTDOWN. Le SCM attend jusqu’à ce que le service s’arrête ou que le délai expire |
flowchart TB
accTitle: Ordre des notifications d'arrêt vers un service
accDescr: Lorsque l'arrêt commence, les services qui ont déclaré PRESHUTDOWN sont notifiés d'abord avec le délai de grâce configuré, puis la notification SHUTDOWN est envoyée avec un défaut d'environ 20 secondes, et le processus est terminé lorsque le délai de grâce expire
start["L'arrêt commence"] --> pre["PRESHUTDOWN(si déclaré)"]
pre --> shut["SHUTDOWN(environ 20 s)"]
shut --> kill["La grâce expire → sortie"]
Figure 9 : Lorsque l’arrêt commence, les services qui ont déclaré PRESHUTDOWN sont notifiés d’abord avec le délai de grâce configuré, puis la notification SHUTDOWN est envoyée avec un défaut d’environ 20 secondes, et le processus est terminé lorsque le délai de grâce expire.
Le délai d’expiration PRESHUTDOWN se configure avec ChangeServiceConfig2 (SERVICE_CONFIG_PRESHUTDOWN_INFO) ; le défaut est 10 secondes à partir de Windows 10 Creators Update (build 15063), et 3 minutes avant.9 Si vous travaillez encore à partir de l’ancienne connaissance que « PRESHUTDOWN vous donne 3 minutes », sur un OS actuel vous n’avez que 1/18 du délai de grâce que vous attendiez. Aussi, PRESHUTDOWN retient l’arrêt de tout le système pendant cet intervalle, donc la documentation aussi dit qu’il « should be used only in special circumstances ».8
La pratique côté gestionnaire compte aussi. Le gestionnaire de contrôle doit renvoyer dans les 30 secondes ; laissez le travail d’arrêt long à un autre thread, signalez SERVICE_STOP_PENDING, et renvoyez immédiatement.8
// Win32 service: accept PRESHUTDOWN and leave stop work to a worker
g_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PRESHUTDOWN;
DWORD WINAPI ServiceHandlerEx(DWORD control, DWORD, LPVOID, LPVOID)
{
switch (control)
{
case SERVICE_CONTROL_PRESHUTDOWN:
case SERVICE_CONTROL_STOP:
ReportStatus(SERVICE_STOP_PENDING, /*waitHintMs*/ 3000);
SetEvent(g_stopEvent); // Tell the worker to stop and return immediately
return NO_ERROR;
}
return ERROR_CALL_NOT_IMPLEMENTED;
}
// Worker side: if cleanup runs longer than waitHint, keep reporting
// SERVICE_STOP_PENDING periodically while incrementing dwCheckPoint.
// The SCM judges "still alive and making progress" from waitHint and
// checkpoint advance. If reporting stops it can be treated as hung and
// shutdown can proceed. Always report SERVICE_STOPPED when finished
6.2. Une conception qui ne s’appuie pas sur le délai de grâce
Prolonger le plafond du délai de grâce, WaitToKillServiceTimeout, en le réécrivant depuis le côté service n’est explicitement pas recommandé. La documentation demande l’inverse — un service doit terminer le nettoyage aussi vite que possible pour qu’une machine alimentée par UPS puisse terminer l’arrêt avant que la batterie meure. Le guidage est d’enregistrer fréquemment en fonctionnement ordinaire pour que les données non enregistrées soient minimisées, de ne pas passer de temps à libérer de la mémoire à l’arrêt, et de ne pas trop attendre une réponse lors de la notification d’un pair réseau. Aussi, le SCM à l’arrêt ne considère pas, par défaut, les dépendances, donc le traitement d’arrêt doit encore fonctionner « même si un service dont vous dépendez est déjà tombé ».8
flowchart TB
accTitle: Concevoir un traitement d'arrêt qui ne s'appuie pas sur le délai de grâce
accDescr: Si vous enregistrez à chaque point de contrôle de traitement pour que les données non enregistrées soient toujours minimales, le nettoyage lorsque la notification d'arrêt arrive se termine en quelques secondes. Une conception qui enregistre tout à la sortie ne tiendra pas dans le délai de grâce, et une terminaison forcée perd les données
good["Enregistrer à chaque point de contrôle"] --> gstop["Arrêt → enregistrer le reliquat → fini"]
bad["Tout enregistrer à la sortie"] --> bstop["Arrêt → l'enregistrement rate la grâce"]
bstop --> killed["Kill forcé → perte de données"]
Figure 10 : Si vous enregistrez à chaque point de contrôle de traitement pour que les données non enregistrées soient toujours minimales, le nettoyage lorsque la notification d’arrêt arrive se termine en quelques secondes. Une conception qui enregistre tout à la sortie ne tiendra pas dans le délai de grâce, et une terminaison forcée perd les données.
Dans un Worker Service .NET (UseWindowsService), SERVICE_CONTROL_STOP et SHUTDOWN sont traduits en arrêt de l’hôte, et BackgroundService.StopAsync est appelé. L’implémentation stock à ce jour accepte la famille STOP/SHUTDOWN ; si vous avez aussi besoin de PRESHUTDOWN vous aurez besoin d’un gestionnaire étendu. Dans les deux cas, rendez HostOptions.ShutdownTimeout explicite et terminez StopAsync en quelques secondes. Pour construire un service en général, voir « Comment créer et exploiter un service Windows ».
7. Récupérer automatiquement après un redémarrage
Sur un PC d’équipement ou un PC sans surveillance, la portée de conception n’est pas seulement « survivre à l’arrêt » mais « revenir tout seul après un redémarrage ».
7.1. RegisterApplicationRestart et le callback de récupération
Si vous avez appelé RegisterApplicationRestart, l’application est enregistrée comme candidate au redémarrage pour un plantage (exception non gérée), « Ne répond pas », un redémarrage d’application déclenché par une mise à jour, et un redémarrage d’OS déclenché par une mise à jour. Vous pouvez enregistrer des arguments de ligne de commande pour le redémarrage, donc si vous incluez « quel fichier était ouvert » et « quel point de restauration », vous pouvez reprendre là où vous vous étiez arrêté après le redémarrage.10
Les spécifications à intégrer sont les suivantes.10
- L’enregistrement doit être terminé avant que le problème se produise (pendant le traitement de WM_QUERYENDSESSION est la dernière chance dans un scénario de mise à jour)
- Pour empêcher une boucle de redémarrage, un processus qui tourne depuis moins de 60 secondes n’est pas redémarré
- Un processus qui tourne en élévation n’est pas candidat au redémarrage automatique (le processus ne peut pas être recréé sans consentement d’élévation). La récupération automatique d’une application qui a besoin d’élévation se conçoit en gardant l’interface au privilège standard et en isolant le travail privilégié dans un service, ou par un chemin de lancement explicite tel qu’une tâche du Planificateur de tâches « Exécuter avec les privilèges les plus élevés »
- Le redémarrage après un plantage ou un blocage passe par le consentement de l’utilisateur ; le redémarrage après une mise à jour est automatique
- Pour récupérer à travers un redémarrage d’OS, le côté qui demande le redémarrage (un installeur et analogues) doit appeler l’API d’arrêt avec les drapeaux EWX_RESTARTAPPS / SHUTDOWN_RESTARTAPPS
Si vous enregistrez aussi RegisterApplicationRecoveryCallback, WER (Windows Error Reporting) appelle le callback lors d’un plantage et vous donne un délai de grâce pour enregistrer les données en cours. Si l’enregistrement prend du temps, toutefois, vous devez continuer d’appeler ApplicationRecoveryInProgress dans l’intervalle de ping spécifié à l’enregistrement ou le travail de récupération est coupé en cours de route. Lorsque l’enregistrement est terminé, notifiez l’achèvement avec ApplicationRecoveryFinished. « Remplacer un fichier en cours d’utilisation et redémarrer » au moment d’une mise à jour d’application est le territoire de Restart Manager, couvert en détail dans « Comment remplacer un exe/DLL en cours d’utilisation ».
7.2. ARSO — ouverture de session automatique après un redémarrage de mise à jour
Après un redémarrage de Windows Update, si personne ne s’ouvre une session, les applications de session utilisateur ne reviennent pas. Ce qui comble cet écart est ARSO (Winlogon Automatic Restart Sign-On). Lorsque Windows Update démarre un redémarrage, il enregistre de façon sécurisée les informations d’identification du dernier utilisateur interactif, configure Autologon, et après le redémarrage ouvre automatiquement la session de cet utilisateur puis verrouille l’écran.11 Il y a aussi une commande telle que shutdown /g qui demande un redémarrage plus la reprise des applications enregistrées. Certains environnements désactivent cela avec une stratégie d’organisation (DisableAutomaticRestartSignOn et analogues), donc lorsque vous concevez une récupération sans surveillance, vérifiez ce paramètre comme un ensemble. Et si vous vous fiez au lancement automatique dans une session utilisateur pour un travail d’arrière-plan dont vous avez toujours besoin, le bon geste est d’en faire un service Windows dès le départ.
flowchart TB
accTitle: Chemin par lequel une application récupère automatiquement après un redémarrage
accDescr: Si vous enregistrez avec RegisterApplicationRestart avant qu'un problème se produise, l'application est redémarrée après consentement de l'utilisateur lors d'un plantage ou « Ne répond pas », et après ouverture de session automatique ARSO et verrouillage d'écran lors d'un redémarrage déclenché par une mise à jour. Les processus de moins de 60 secondes de durée et les processus élevés sont hors de portée
reg["RegisterApplicationRestart"]
reg --> crash["Plantage ou blocage"]
reg --> update["Redémarrage de mise à jour"]
crash -->|"Consentement"| restart["Redémarrage de l'application"]
update --> arso["Ouverture de session ARSO + verrouillage"]
arso --> restart
restart -.-> limits["Pas : moins de 60 s / élevé"]
Figure 11 : Si vous enregistrez avec RegisterApplicationRestart avant qu’un problème se produise, l’application est redémarrée après consentement de l’utilisateur lors d’un plantage ou « Ne répond pas », et après ouverture de session automatique ARSO et verrouillage d’écran lors d’un redémarrage déclenché par une mise à jour. Les processus de moins de 60 secondes de durée et les processus élevés sont hors de portée.
8. Survivre à une coupure d’alimentation qui n’apporte aucune notification — Conception d’écriture et UPS
8.1. Une écriture qui « ne casse pas peu importe quand elle est coupée » — fichier temporaire + ReplaceFile
Un disjoncteur déclenché, une alimentation défaillante, ou une prise arrachée n’apporte ni WM_ENDSESSION ni PRESHUTDOWN. Tant que vous « écrasez le fichier original sur place » pour des paramètres ou des résultats de mesure, une coupure d’alimentation en milieu d’écriture peut laisser un fichier cassé qui mélange ancien et nouveau.
Le motif standard est d’écrire complètement dans un fichier temporaire sur le même volume puis d’échanger. ReplaceFile empaquette la séquence « enregistrer dans le nouveau fichier → mettre l’original de côté → renommer → supprimer » en une seule API, et reporte aussi les attributs du fichier original tels que l’heure de création, l’ACL et les flux alternatifs (les trois fichiers doivent être sur le même volume).12 File.Replace de .NET appelle cela tel quel.
flowchart TB
accTitle: Flux d'enregistrement et de récupération avec un fichier temporaire et ReplaceFile
accDescr: À l'enregistrement, écrivez complètement dans un fichier temporaire, videz, et échangez avec ReplaceFile, en laissant l'ancien contenu dans .bak. Au prochain lancement, validez le fichier principal et retombez sur .bak s'il est cassé
subgraph save["À l'enregistrement"]
w["Écrire un fichier temp. complet"] --> f["Vider vers le disque"]
f --> r["ReplaceFile → .bak"]
end
subgraph startup["Au prochain lancement"]
v["Valider le principal"]
v -->|"Intact"| use["Utiliser tel quel"]
v -->|"Cassé"| bak["Retomber sur .bak"]
end
r -.->|"Coupure à n'importe quelle étape"| v
Figure 12 : À l’enregistrement, écrivez complètement dans un fichier temporaire, videz, et échangez avec ReplaceFile, en laissant l’ancien contenu dans .bak. Au prochain lancement, validez le fichier principal et retombez sur .bak s’il est cassé.
// The standard pattern for settings and data: write completely to a temporary file, then swap, and keep the old contents
public static void SaveAtomically(string path, string content)
{
string dir = Path.GetDirectoryName(path)!;
string tmp = Path.Combine(dir, Path.GetRandomFileName()); // Create it on the same volume
try
{
using (var fs = new FileStream(tmp, FileMode.CreateNew, FileAccess.Write))
using (var writer = new StreamWriter(fs))
{
writer.Write(content);
writer.Flush();
fs.Flush(flushToDisk: true); // FlushFileBuffers equivalent. Write the OS buffer
// out to disk (device-side cache limits are in 8.2)
}
if (File.Exists(path))
File.Replace(tmp, path, path + ".bak"); // Calls ReplaceFile. Keep the old contents as .bak
else
File.Move(tmp, path);
}
catch
{
// If we fail mid-way, do not leave the temporary file. Repeated failures
// of a periodic save would fill the volume with complete copies
try { File.Delete(tmp); } catch { /* Prefer the original exception if delete fails */ }
throw;
}
}
Avec cela, le fonctionnement ordinaire vous laisse toujours capable de lire soit « un ancien fichier complet » soit « un nouveau fichier complet ». ReplaceFile est toutefois une opération d’espace de noms en plusieurs étapes, et l’atomicité à travers une coupure d’alimentation n’est pas garantie par spécification. C’est pourquoi l’exemple ci-dessus conserve une sauvegarde (.bak) — le côté lecture valide le fichier principal au lancement et retombe sur la sauvegarde s’il est cassé, comme un ensemble. Vous ne pouvez pas utiliser cela pour des journaux ou CSV en ajout seulement, donc ceux-là utilisent un format qui intègre la casse, tel que « une ligne = un enregistrement, et jeter une dernière ligne cassée à la lecture ».
8.2. Le succès de WriteFile n’est pas l’arrivée au disque
L’autre prémisse est que même si WriteFile renvoie le succès, les données peuvent n’être encore que dans le cache de l’OS. Windows met les lectures et écritures de fichiers sur le tampon système et les reflète sur le disque périodiquement avec une écriture paresseuse. Pour que les données aillent au disque pour de bon, soit videz explicitement avec FlushFileBuffers, soit spécifiez FILE_FLAG_WRITE_THROUGH à CreateFile pour que chaque écriture traverse le cache. Les métadonnées du système de fichiers sont toujours mises en cache, donc confirmer les métadonnées a aussi besoin d’un vidage ou d’un write-through.13
Appeler FlushFileBuffers à chaque fois est toutefois inefficace, et la documentation aussi encourage à envisager FILE_FLAG_NO_BUFFERING+WRITE_THROUGH au lieu d’appels fréquents.13 En pratique, « vider seulement à un point de contrôle de transaction ou juste avant de fermer le fichier » est un compromis réaliste. Les mécaniques de cette couche — le gestionnaire de cache, l’écriture paresseuse, et le fait que « j’ai vidé et ça n’a peut-être toujours pas atteint le disque » à cause du cache matériel — sont couvertes en profondeur dans « Le gestionnaire de cache : quand votre WriteFile atteint-il vraiment le disque ? ».
8.3. UPS et surveillance de batterie — transformer une coupure d’alimentation en un arrêt
La vraie contre-mesure contre une coupure d’alimentation sur un PC d’équipement est un UPS. Pensez le rôle de l’UPS non pas comme « arrêter une panne » mais comme transformer « une coupure d’alimentation sans notification » en « un arrêt planifié avec une notification ». La conception est un montage en deux étapes.
- Concevoir le délai de grâce : temps de maintien de la batterie UPS > la somme de « détecter le basculement sur batterie → nettoyage de l’application et du service → arrêt de l’OS terminé ». Si le traitement d’arrêt du service est lent, cette équation ne tient plus (section 6.2)
- Détection : le basculement de secteur à batterie, et une baisse de capacité restante, sont notifiés avec l’événement PBT_APMPOWERSTATUSCHANGE. Une application avec une fenêtre le reçoit comme WM_POWERBROADCAST ; un service sans fenêtre déclare SERVICE_ACCEPT_POWEREVENT et le reçoit comme SERVICE_CONTROL_POWEREVENT dans HandlerEx (WM_POWERBROADCAST n’arrive pas à un gestionnaire de contrôle de service). À la réception, appelez GetSystemPowerStatus, vérifiez ACLineStatus (si sur secteur) et BatteryLifePercent, et conduisez vers l’interruption de la mesure, l’enregistrement, et la demande d’arrêt15
flowchart TB
accTitle: Flux de transformer une coupure d'alimentation en un arrêt planifié avec un UPS
accDescr: Lorsqu'une panne bascule l'UPS sur batterie, PBT_APMPOWERSTATUSCHANGE est notifié, l'état d'alimentation est vérifié, et l'enregistrement plus une demande d'arrêt transforment une coupure sans notification en un arrêt planifié avec une notification
outage["Panne"] --> ups["UPS sur batterie"]
ups --> pbt["PBT_APMPOWERSTATUSCHANGE"]
pbt --> check["GetSystemPowerStatus"]
check --> saveop["Interrompre et enregistrer"]
saveop --> req["Demander l'arrêt"]
req --> normal["Flux de notif. habituel(3–6)"]
Figure 13 : Lorsqu’une panne bascule l’UPS sur batterie, PBT_APMPOWERSTATUSCHANGE est notifié, l’état d’alimentation est vérifié, et l’enregistrement plus une demande d’arrêt transforment une coupure sans notification en un arrêt planifié avec une notification.
Un UPS typique connecté en USB apparaît à Windows comme une batterie, donc vous pouvez le détecter avec cette API standard. Si le logiciel de gestion du fournisseur a une fonction « arrêter l’OS à N % restants », vérifiez aussi que le seuil s’aligne avec le temps de nettoyage de votre application. La reprise depuis la veille ou la mise en veille prolongée, et les problèmes de longue durée, sont un axe séparé, couvert dans « Veille, mise en veille prolongée, Modern Standby et applications longue durée ».
9. Comment vérifier — Essayer l’arrêt en sécurité
Le traitement de l’arrêt tend à devenir « nous l’avons écrit mais ne l’avons jamais essayé dans des conditions équivalentes à la production ». Gardez une procédure pour le vérifier en sécurité.
- Essayez-le sur une machine de test ou une machine virtuelle : Ne l’essayez pas d’abord sur un PC d’équipement de production. Dans un environnement de test avec un point de contrôle Hyper-V (instantané), répétez l’arrêt, le redémarrage, et une coupure d’alimentation forcée (éteindre la machine virtuelle). Un « power off » de machine virtuelle, toutefois, ne reproduit que « l’OS invité qui s’arrête sans préavis » ; il ne reproduit pas la disparition du cache volatile d’un disque physique ou une casse dépendante du contrôleur. Si vous l’expédiez comme PC d’équipement, le contrôle final est un vrai test de coupure sur du matériel équivalent à la production
- Un contrôle rapide avec la déconnexion : Le chemin WM_QUERYENDSESSION → WM_ENDSESSION s’exécute aussi à la déconnexion (la seule différence est que le bit ENDSESSION_LOGOFF est positionné dans lParam), donc vous pouvez commodément confirmer le comportement du code de nettoyage sur une machine de développement1
- Essayez un arrêt complet et hybride séparément : Essayez
shutdown /s /t 0(complet),shutdown /s /hybrid /t 0(comportement par défaut), etshutdown /r /t 0(redémarrage) chacun2 - Mesurez combien de temps le nettoyage prend : Écrivez un horodatage dans le journal au début et à la fin de la fonction de nettoyage, et mesurez s’il tient dans 5 secondes (ou le délai de grâce configuré pour un service)
flowchart TB
accTitle: Opérations à vérifier et ce que chacune peut confirmer
accDescr: La déconnexion est un contrôle commode du chemin de notification ; l'arrêt-commande complet, hybride et redémarrage confirment le chemin de notification de production et le délai de grâce ; l'extinction de machine virtuelle teste la résilience à l'arrêt soudain ; un test de coupure physique est le contrôle final y compris le stockage physique
signtest["Déconnexion"] -.-> path1["Chemin QUERY → ENDSESSION"]
signtest --> shuttest["shutdown /s /hybrid /r"]
shuttest -.-> path2["Chemin de production + grâce"]
shuttest --> vmtest["Extinction de machine virtuelle"]
vmtest -.-> path3["Arrêt soudain de l'invité"]
vmtest --> hwtest["Coupure physique"]
hwtest -.-> path4["Stockage inclus(final)"]
Figure 14 : La déconnexion est un contrôle commode du chemin de notification ; l’arrêt-commande complet, hybride et redémarrage confirment le chemin de notification de production et le délai de grâce ; l’extinction de machine virtuelle teste la résilience à l’arrêt soudain ; un test de coupure physique est le contrôle final y compris le stockage physique.
Pour l’isolement après coup, le journal des événements (Système) est utile. Sur un arrêt ou un redémarrage normal, l’ID d’événement 1074 (quel processus a démarré l’arrêt, pour qui, et pour quel motif) est enregistré. Sur une coupure d’alimentation soudaine ou un plantage il n’y a pas de 1074, et au prochain démarrage l’ID d’événement 41 (Kernel-Power) et 6008 (L’arrêt précédent du système était inattendu) sont enregistrés.14 « Que s’est-il passé pendant la nuit » commence ici.
# Check the recent history of shutdown-related events
Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 1074, 6008, 41 } -MaxEvents 20 |
Select-Object TimeCreated, Id, ProviderName, Message |
Format-List
Si 1074 montre « un redémarrage par Windows Update » et que les données de l’application étaient cassées, le problème est le code de nettoyage. 6008/41 ne montrent que « un arrêt inattendu » ; ils sont aussi enregistrés pour un écran bleu (plantage) ou une réinitialisation forcée, pas seulement une coupure d’alimentation. Si le BugcheckCode de 41 est non nul c’est un plantage ; s’il est 0 et qu’il n’y a pas non plus de vidage mémoire, une coupure d’alimentation est probable — isolez la cause à partir des informations environnantes, et une fois que vous savez que c’était une coupure d’alimentation, la conception d’écriture du chapitre 8 et un UPS sont l’étape suivante.
10. Résumé
- L’arrêt est « un événement normal qui viendra, tôt ou tard ». Le délai de grâce après la notification n’est, en principe, que d’environ 5 secondes, donc le prérequis est une sauvegarde automatique fréquente pour que « ce que vous faites à la sortie » soit minimisé.
- Sur les OS clients à partir de Windows 8, si le démarrage rapide est activé, « Arrêter » est un arrêt hybride et le noyau ne fait que se mettre en veille prolongée. La seule réinitialisation complète est « Redémarrer » — écrivez « Redémarrer » dans la procédure d’incident.
- Une application graphique renvoie TRUE immédiatement à WM_QUERYENDSESSION, et fait le nettoyage engagé dans WM_ENDSESSION. FormClosing et SessionEnding de WinForms/WPF correspondent à la phase de requête, donc ce que vous y faites est au plus un enregistrement de snapshot idempotent. Ne mettez pas de boîte de dialogue pendant l’arrêt.
- Une opération qui vraiment ne peut pas être interrompue est protégée en affichant un motif avec ShutdownBlockReasonCreate. Il n’y a toutefois nulle part de garantie que vous puissiez bloquer.
- Une application console reçoit la notification avec SetConsoleCtrlHandler ; un service avec SERVICE_ACCEPT_PRESHUTDOWN/SHUTDOWN. Le délai de grâce PRESHUTDOWN par défaut est de 10 secondes sur un OS actuel. En .NET, arrêtez de vous fier à ProcessExit et passez au chemin canonique pour le modèle d’application.
- La récupération après un redémarrage peut être sans surveillance avec RegisterApplicationRestart (+ un callback de récupération) et ARSO.
- Une coupure d’alimentation n’apporte aucune notification. Préparez avec un échange fichier temporaire + ReplaceFile (comme un ensemble avec sauvegarde + validation au chargement), un vidage aux points de contrôle, et un UPS qui « transforme une coupure d’alimentation en un arrêt planifié ».
flowchart TB
accTitle: Le tableau d'ensemble du traitement de l'arrêt
accDescr: Pour une fin avec une notification, répondez avec un nettoyage qui peut fermer boutique en quelques secondes et conduisez vers une récupération automatique après un redémarrage ; pour une coupure d'alimentation sans notification, préparez avec une écriture qui ne casse pas peu importe quand elle est coupée et un UPS, et vérifiez y compris sur du matériel physique. Ces deux piliers sont la conclusion de l'article
ending{"Comment ça se termine"}
ending -->|"Avec notification"| pillar1["Nettoyage en quelques secondes(3–6)"]
ending -->|"Sans notification"| pillar2["Écriture sûre + UPS(8)"]
pillar1 --> recover["Récupération auto après redémarrage(7)"]
pillar2 --> verifytest["Vérifier sur le matériel(9)"]
Figure 15 : Pour une fin avec une notification, répondez avec un nettoyage qui peut fermer boutique en quelques secondes et conduisez vers une récupération automatique après un redémarrage ; pour une coupure d’alimentation sans notification, préparez avec une écriture qui ne casse pas peu importe quand elle est coupée et un UPS, et vérifiez y compris sur du matériel physique. Ces deux piliers sont la conclusion de l’article.
- Vérifiez en sécurité sur une machine virtuelle et avec la déconnexion, et isolez après coup avec les ID d’événement 1074/41/6008.
La prochaine fois que vous ajoutez une fonctionnalité à l’application, demandez-vous ceci une fois : si WM_ENDSESSION arrive au milieu de ce travail, ou si l’alimentation est arrachée, que reste-t-il au prochain lancement ? Écrire cette réponse dans la conception est le chemin le plus court pour ne plus jamais passer une matinée debout devant un PC d’équipement, la tête dans les mains.
Articles connexes
- Comment remplacer un exe/DLL en cours d’utilisation — Restart Manager et le problème du « fichier en cours d’utilisation » dans les mises à jour automatiques
- Comment créer et exploiter un service Windows ── du choix entre Planificateur de tâches et service à la transformation d’un BackgroundService en service Windows
- Veille, mise en veille prolongée, Modern Standby et applications longue durée — concevoir pour éviter « ça s’était arrêté pendant la nuit »
- Les profondeurs de l’E/S Windows (épisode 4) — Le gestionnaire de cache : quand votre WriteFile atteint-il vraiment le disque ?
- Concevoir la conservation des journaux et des dumps lors du crash d’une application Windows
- Checklist pour gérer les processus enfants en toute sécurité dans une application Windows
Domaines de conseil associés
KomuraSoft LLC prend en charge la conception et l’implémentation de contre-mesures d’arrêt et de coupure d’alimentation pour les applications de PC d’équipement et de longue durée, l’investigation des causes profondes de corruption de données et d’incidents « ça s’était arrêté le matin » qui partent d’un redémarrage Windows Update ou d’une déconnexion, et la revue de conception du traitement d’arrêt des services Windows et de la récupération automatique. Il est possible de commencer dès le stade de « quelque chose semble casser à chaque fois que nous arrêtons, et je ne sais pas par où commencer ».
- Développement d’applications Windows
- Investigation de bugs et analyse des causes
- Conseil technique et revue de conception
- Nous contacter
Références
-
Microsoft Learn, WM_QUERYENDSESSION message. Que WM_QUERYENDSESSION est envoyé à la fin de session et que l’application doit renvoyer TRUE et respecter l’intention de l’utilisateur (le défaut de DefWindowProc est aussi TRUE) ; que le nettoyage doit être différé jusqu’à WM_ENDSESSION ; qu’après 5 secondes le système affiche une interface pour les applications qui empêchent l’arrêt et que l’utilisateur peut forcer la terminaison ; la signification des bits ENDSESSION_LOGOFF/CLOSEAPP/CRITICAL dans lParam ; que l’arrêt et le redémarrage ne peuvent pas être distingués ; et que les données doivent être enregistrées fréquemment pour que moins doive être enregistré à la sortie. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Fast startup causes hibernation or shutdown to fail in Windows 10 or Windows 8.1. Qu’avec le démarrage rapide la session noyau n’est pas fermée et est traitée comme une mise en veille prolongée, et que l’état du noyau et des pilotes de périphériques est enregistré dans hiberfil.sys ; que « Redémarrer » fait toujours un démarrage complet parce qu’un état Windows complètement nouveau est nécessaire ; que le démarrage rapide est activé par défaut et que le désactiver n’est pas recommandé ; et que le défaut de Shutdown.exe est un arrêt complet, l’option /hybrid produisant le comportement hybride. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, ShutdownBlockReasonCreate function (winuser.h). Que vous l’appelez au début d’une opération ininterruptible pour enregistrer une chaîne de motif et appelez ShutdownBlockReasonDestroy lorsque c’est terminé ; qu’elle ne peut être appelée que depuis le thread qui a créé la fenêtre ; et que l’utilisateur ne lira le motif que quelques secondes, donc la chaîne doit être courte et claire. ↩ ↩2 ↩3
-
Microsoft Learn, Shutdown Changes for Windows Vista. Que la réponse à WM_QUERYENDSESSION/WM_ENDSESSION peut être retardée de 5 secondes chacune et que l’utilisateur peut alors choisir de continuer ou d’annuler ; qu’une application console ou une application sans fenêtre visible ne peut pas interrompre l’arrêt et est terminée automatiquement après 5 secondes sans réponse ou une réponse FALSE ; que si un blocage est nécessaire un motif doit être enregistré avec ShutdownBlockReasonCreate ; et qu’une application ne doit pas dépendre de pouvoir bloquer l’arrêt. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, HandlerRoutine callback function. Les événements CTRL_C/BREAK/CLOSE/LOGOFF/SHUTDOWN reçus par un gestionnaire enregistré avec SetConsoleCtrlHandler ; que le délai d’expiration par défaut pour CTRL_CLOSE_EVENT est d’environ 5000 millisecondes et pour CTRL_SHUTDOWN_EVENT sur un processus de service d’environ 20000 millisecondes ; que CTRL_LOGOFF/SHUTDOWN_EVENT sont reçus essentiellement seulement par les services parce qu’une application interactive est terminée à la déconnexion ; et que le gestionnaire s’exécute sur un thread séparé. ↩ ↩2 ↩3
-
Microsoft Learn, SetConsoleCtrlHandler function. Qu’un processus qui a chargé gdi32.dll ou user32.dll est traité comme une application Windows et que les gestionnaires CTRL_LOGOFF_EVENT/CTRL_SHUTDOWN_EVENT ne sont pas appelés ; que la solution de contournement est de créer une fenêtre cachée et de gérer WM_QUERYENDSESSION/WM_ENDSESSION ; et que les fonctions console peuvent ne pas fonctionner correctement pendant le traitement des signaux. ↩ ↩2
-
Microsoft Learn, .NET runtime no longer provides default termination signal handlers. Qu’à partir de .NET 10 le runtime ne fournit plus de gestionnaire par défaut pour Windows CTRL_SHUTDOWN_EVENT/CTRL_CLOSE_EVENT (les équivalents Unix SIGTERM/SIGHUP) ; que la gestion par défaut de l’OS termine l’application immédiatement et que AppDomain.ProcessExit et AssemblyLoadContext.Unloading ne se déclenchent plus ; et qu’une gestion des signaux appropriée au modèle d’application doit être enregistrée dans une bibliothèque de plus haut niveau ou dans le code de l’application. ↩ ↩2
-
Microsoft Learn, Service Control Handler Function. Qu’un service qui a déclaré SERVICE_ACCEPT_PRESHUTDOWN reçoit SERVICE_CONTROL_PRESHUTDOWN d’abord, puis un service SERVICE_ACCEPT_SHUTDOWN reçoit SERVICE_CONTROL_SHUTDOWN ; que le délai de grâce par défaut à l’arrêt est d’environ 20 secondes et que le plafond au redémarrage de l’OS est WaitToKillServiceTimeout ; que cette valeur ne doit pas être prolongée ; que le gestionnaire de contrôle doit renvoyer dans les 30 secondes, signaler STOP_PENDING et un wait hint, et laisser le travail long à un autre thread ; que le nettoyage doit se terminer aussi vite que possible en ayant à l’esprit le fonctionnement UPS ; et que le SCM à l’arrêt ne considère pas, par défaut, les dépendances. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, SERVICE_PRESHUTDOWN_INFO structure (winsvc.h). Qu’après la notification PRESHUTDOWN le SCM attend jusqu’à ce que le service s’arrête ou que le délai expire ; que le délai d’expiration par défaut est de 10 secondes à partir de Windows 10 Creators Update (build 15063) et de 3 minutes avant ; qu’il se configure avec ChangeServiceConfig2 ; et que l’état peut continuer d’être mis à jour pendant SERVICE_STOP_PENDING. ↩ ↩2
-
Microsoft Learn, RegisterApplicationRestart function (winbase.h). Que le redémarrage peut être enregistré pour un plantage, « Ne répond pas », une mise à jour, et un redémarrage de l’ordinateur accompagnant une mise à jour ; que des arguments de ligne de commande pour le redémarrage peuvent être spécifiés ; que l’enregistrement doit être fait avant que le problème se produise et que pendant le traitement de WM_QUERYENDSESSION est la dernière chance dans un scénario de mise à jour ; qu’un processus de moins de 60 secondes de durée n’est pas redémarré ; que le redémarrage après un plantage ou un blocage passe par le consentement de l’utilisateur ; et que traverser un redémarrage d’OS exige un arrêt avec EWX_RESTARTAPPS/SHUTDOWN_RESTARTAPPS. ↩ ↩2 ↩3
-
Microsoft Learn, Winlogon automatic restart sign-on (ARSO). Que lorsque Windows Update démarre un redémarrage automatique il enregistre les informations d’identification du dernier utilisateur interactif et configure Autologon ; qu’après le redémarrage il ouvre automatiquement la session de l’utilisateur et verrouille la session ; que les informations d’identification enregistrées sont supprimées après une ouverture de session réussie ; et qu’il se configure avec une stratégie de groupe (DisableAutomaticRestartSignOn et analogues). ↩ ↩2
-
Microsoft Learn, ReplaceFileW function (winbase.h). Que ReplaceFile empaquette en une seule fonction les plusieurs étapes équivalentes à « enregistrer dans le nouveau fichier, renommer temporairement l’original, renommer le nouveau fichier, supprimer l’original » ; qu’il préserve les attributs du fichier original tels que l’heure de création, le DACL, le chiffrement, la compression et les flux nommés ; et que la sauvegarde, le fichier en cours de remplacement et le fichier de remplacement doivent être sur le même volume. ↩ ↩2
-
Microsoft Learn, File Caching. Que les écritures vont sur le cache système par défaut et sont reflétées sur le disque par une écriture paresseuse ; que FILE_FLAG_WRITE_THROUGH écrit immédiatement sur le disque ; que FlushFileBuffers peut vider explicitement ; et que les métadonnées du système de fichiers sont toujours mises en cache, donc confirmer les métadonnées exige un vidage ou un write-through. ↩ ↩2 ↩3
-
Microsoft Learn, Troubleshoot unexpected reboots using system event logs. Qu’un redémarrage normal enregistre l’ID d’événement 1074 (quel processus a démarré l’arrêt, pour qui, et pour quel motif) ; qu’un redémarrage inattendu enregistre l’ID d’événement 41 (Kernel-Power) et 6008 (l’arrêt précédent était inattendu) ; et que ces ID peuvent isoler le type de redémarrage. ↩ ↩2
-
Microsoft Learn, PBT_APMPOWERSTATUSCHANGE event. Que cet événement est notifié via WM_POWERBROADCAST lors d’un basculement entre batterie et secteur ou d’une baisse de capacité restante ; et qu’à la réception vous devez appeler GetSystemPowerStatus et vérifier les champs SYSTEM_POWER_STATUS tels que ACLineStatus, BatteryFlag et BatteryLifePercent. ↩
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
L'API du pool de threads Win32 — de la concurrence sans créer de threads, via CreateThreadpoolWork
Vous semez des appels CreateThread partout dans votre code natif ? Cet article explique l'API du pool de threads Win32 refondue sous Vist...
Tubes nommés en pratique — l'IPC standard de Windows, de la conception à la sécurité
Guide pratique des tubes nommés, mécanisme standard de communication inter-processus sous Windows. Cet article organise, à partir des sou...
Les applications qui cassent à la reprise de veille — comment fonctionnent les événements d'alimentation Windows et comment construire des applications métier qui y survivent
Vous avez ouvert le portable et les connexions de l'application métier étaient mortes — la cause est une conception qui n'a jamais tenu c...
DllMain et le verrou du chargeur — la vraie raison pour laquelle on vous dit de « ne rien faire dans l'initialisation d'une DLL »
Pourquoi il ne faut pas appeler LoadLibrary ni synchroniser avec d'autres threads depuis DllMain. En s'appuyant sur les sources primaires...
Ce qu'est vraiment « Ne répond pas » — comment Windows décide qu'une application est bloquée, et comment concevoir des applications qui ne le sont pas
Le « Ne répond pas » de Windows est un mécanisme dans lequel l'OS juge qu'une fenêtre n'a pas récupéré de message pendant 5 secondes et l...
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.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Développement d'applications Windows
Applications métier, intégration d'équipements et outils de communication, des besoins au développement.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Un problème qui n'est pas parti après « Arrêter » est parti après un « Redémarrer ». Pourquoi ?
- Sur les OS clients à partir de Windows 8, lorsque le démarrage rapide est activé (le défaut sur la plupart des PC qui prennent en charge la mise en veille prolongée), « Arrêter » utilise un mécanisme appelé arrêt hybride. L'utilisateur est déconnecté, mais l'état du noyau et des pilotes est enregistré dans le fichier de mise en veille prolongée et restauré tel quel au prochain démarrage. Autrement dit, le cœur de l'OS n'a pas été réinitialisé. « Redémarrer », en revanche, effectue toujours un démarrage complet, donc les ennuis de pilotes et de services sont réinitialisés. Écrivez « Redémarrer » dans votre procédure d'isolement, pas « Arrêter et rallumer ». Si vous voulez un arrêt complet depuis la ligne de commande, vous pouvez utiliser shutdown /s.
- Puis-je arrêter l'arrêt jusqu'à ce que l'application finisse d'enregistrer ?
- Vous pouvez demander d'attendre temporairement, mais vous ne pouvez pas l'arrêter de façon fiable. Si vous enregistrez une chaîne de motif avec ShutdownBlockReasonCreate uniquement pendant qu'une opération ininterruptible est en cours, ce motif apparaît sur l'écran « Cette application empêche l'arrêt » et l'utilisateur peut décider de continuer ou d'annuler. L'utilisateur peut toutefois encore choisir de forcer la continuation, et un arrêt forcé ou un redémarrage déclenché par une mise à jour peut ne pas attendre du tout. L'approche correcte n'est donc pas « bloquer », mais une sauvegarde automatique fréquente pour que moins de données soient en risque, plus un nettoyage conçu pour se terminer en quelques secondes à partir de la notification de fermeture.
- L'arrêt de mon service Windows prend longtemps. Puis-je prolonger le délai de grâce d'arrêt ?
- Dans la configuration par défaut qui reçoit SERVICE_CONTROL_SHUTDOWN, le délai de grâce est d'environ 20 secondes et dépend de la valeur de registre WaitToKillServiceTimeout. Réécrire cette valeur depuis le côté application pour la prolonger n'est pas recommandé. Si vous avez besoin d'un délai de grâce plus long, vous pouvez déclarer SERVICE_ACCEPT_PRESHUTDOWN et recevoir SERVICE_CONTROL_PRESHUTDOWN ; vous êtes notifié plus tôt que les autres, et le délai d'expiration se configure avec ChangeServiceConfig2 (le défaut est 10 secondes à partir de Windows 10 Creators Update, et 3 minutes avant). PRESHUTDOWN, toutefois, retient tout l'arrêt pendant cet intervalle, donc limitez-le aux cas dont vous avez vraiment besoin, et concevez fondamentalement le travail d'arrêt lui-même pour qu'il se termine en quelques secondes.
- Est-il sûr de faire le nettoyage d'arrêt dans AppDomain.ProcessExit de .NET ?
- Je recommande de ne pas s'y fier. Historiquement le runtime enregistrait un gestionnaire de signal par défaut, et ProcessExit se déclenchait sur CTRL_CLOSE_EVENT et CTRL_SHUTDOWN_EVENT, mais à partir de .NET 10 le runtime ne fournit plus de gestionnaires de signal de terminaison par défaut, et ProcessExit ne se déclenche plus dans ces cas. Implémentez le nettoyage sur le chemin de notification qui correspond au modèle d'application : les applications graphiques utilisent FormClosing ou SessionEnding (ce sont des notifications de phase de requête, donc limitez-les à des enregistrements idempotents ; le nettoyage qui ne peut s'exécuter qu'après que la session est engagée appartient à un hook WM_ENDSESSION) ; Generic Host / Worker Service utilisent IHostApplicationLifetime et StopAsync ; les applications console utilisent SetConsoleCtrlHandler ou PosixSignalRegistration.
- Comment empêcher que les fichiers soient corrompus par une coupure d'alimentation soudaine ?
- Une coupure d'alimentation n'apporte aucune notification du tout, donc la seule option est d'écrire d'une façon qui ne casse pas peu importe quand l'alimentation est coupée. La base est de ne pas écraser le fichier original sur place : écrire complètement dans un fichier temporaire sur le même volume, vider, et échanger avec ReplaceFile (File.Replace en .NET). En fonctionnement ordinaire cela vous laisse capable de lire soit un ancien fichier complet soit un nouveau fichier complet, mais l'atomicité de ReplaceFile à travers une coupure d'alimentation n'est pas garantie par spécification, donc conservez une sauvegarde (le troisième argument) et implémentez une récupération au chargement qui valide le fichier principal et retombe sur la sauvegarde s'il est cassé. Aussi, le succès de WriteFile ne signifie pas que les données ont atteint le disque, donc aux points de contrôle importants confirmez l'écriture avec FlushFileBuffers ou FILE_FLAG_WRITE_THROUGH. Sur les PC d'équipement le montage standard est de combiner cela avec un UPS, détecter le basculement sur batterie, et conduire vers un arrêt sûr.
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.