L'arrêt de Windows vu depuis l'application — survivre correctement aux notifications de fermeture, aux redémarrages et aux coupures d'alimentation

· · 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é.

Quatre façons de se terminer, et la notification à l'applicationLa 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 UPSDéconnexionQUERY → ENDSESSIONArrêtRedémarrageCoupure d'alimentationAucune notif. : écriture + UPSNettoyage 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 ».

Ce qui arrive au noyau sur une opération ArrêterUne 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 completDémarrage rapide activéHibernate désactivé / ServerArrêterRedémarrerSession se termine + hibernate noyauArrêt completEnsuite : restaurer le noyauEnsuite : démarrage complet

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

  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.
  2. 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

Flux de la notification de fin de session en deux étapesRenvoyer 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 continuationTRUE(règle)FALSE(refuser)Pas de réponse ~5 sForcer la continuationAnnulerWM_QUERYENDSESSIONWM_ENDSESSION(engagé)Affiché comme bloquant l'arrêtTraité comme bloquéNettoyage iciSortie du processusArrê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.

Comment les événements WinForms/WPF correspondent aux messagesLa 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'engagementWM_QUERYENDSESSIONWinForms : FormClosingWPF : SessionEndingSnapshot idempotent seulementWM_ENDSESSIONPas d'événement : hook WndProcNettoyage 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

Flux de la protection avec ShutdownBlockReasonCreateEnregistrez 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 termineAnnulerForcer la continuationDémarrer le travail ininterruptibleShutdownBlockReasonCreateExécuter sur un thread workerTerminer : DestroyArrêt pendant ceciAfficher le motif + FALSESortie 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

Délai de grâce par signal consoleCtrl+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 processusPas de délaiEnviron 5 sEnviron 20 sCTRL_C / BREAKNettoyage HandlerRoutineCTRL_CLOSECTRL_SHUTDOWNKill 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

Comment ProcessExit a changé dans .NET 10Jusqu'à .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êmeCTRL_CLOSE / SHUTDOWNJusqu'à .NET 9 : ProcessExitÀ partir de .NET 10 : sortie immédiateEnregistrer 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)
Où chaque modèle d'application reçoit la notification de fermetureUne 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 externeGraphiquePas graphiqueHostConsoleQuel modèle d'application ?FormClosing / SessionEndingHost ou console ?Hook ENDSESSIONLifetime + StopAsyncSetConsoleCtrlHandlerDéfinir ShutdownTimeoutNe 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
Ordre des notifications d'arrêt vers un serviceLorsque 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 expireL'arrêt commencePRESHUTDOWN(si déclaré)SHUTDOWN(environ 20 s)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

Concevoir un traitement d'arrêt qui ne s'appuie pas sur le délai de grâceSi 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éesEnregistrer à chaque point de contrôleArrêt → enregistrer le reliquat → finiTout enregistrer à la sortieArrêt → l'enregistrement rate la grâceKill 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.

Chemin par lequel une application récupère automatiquement après un redémarrageSi 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éeConsentementRegisterApplicationRestartPlantage ou blocageRedémarrage de mise à jourRedémarrage de l'applicationOuverture de session ARSO + verrouillagePas : 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.

Flux d'enregistrement et de récupération avec un fichier temporaire et ReplaceFileÀ 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éAu prochain lancementÀ l'enregistrementIntactCasséCoupure à n'importe quelle étapeValider le principalUtiliser tel quelRetomber sur .bakVider vers le disqueÉcrire un fichier temp. completReplaceFile → .bak

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.

  1. 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)
  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
Flux de transformer une coupure d'alimentation en un arrêt planifié avec un UPSLorsqu'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 notificationPanneUPS sur batteriePBT_APMPOWERSTATUSCHANGEGetSystemPowerStatusInterrompre et enregistrerDemander l'arrêtFlux 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), et shutdown /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)
Opérations à vérifier et ce que chacune peut confirmerLa 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 physiqueDéconnexionChemin QUERY → ENDSESSIONshutdown /s /hybrid /rChemin de production + grâceExtinction de machine virtuelleArrêt soudain de l'invitéCoupure physiqueStockage 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é ».
Le tableau d'ensemble du traitement de l'arrêtPour 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'articleAvec notificationSans notificationComment ça se termineNettoyage en quelques secondes(3–6)Écriture sûre + UPS(8)Récupération auto après redémarrage(7)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

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 ».

Références

  1. 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

  2. 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

  3. 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

  4. 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

  5. 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

  6. 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

  7. 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

  8. 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

  9. 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

  10. 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

  11. 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

  12. 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

  13. 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

  14. 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

  15. 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 récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

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.

Retour au blog