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

· Mis à jour le: · · Service Windows, Windows, .NET, C#, BackgroundService, Generic Host, Traitement résident, Exploitation, Tableau de décision, Conseil technique

« Le Planificateur de tâches toutes les 5 minutes ne suffit plus. » « On voudrait faire tourner en permanence un démon de surveillance qui attend les données d’un appareil. » « Quand un fichier arrive dans un dossier, il faudrait le traiter en quelques secondes. » En continuant les missions de conseil autour de l’exécution planifiée, les besoins finissent toujours, en grandissant, par mener à cette discussion.

Sur ce blog, nous avons poursuivi une série d’articles sur l’automatisation : Automatisation des processus métier avec Power Automate et Concevoir une exploitation fiable et sûre du Planificateur de tâches. Le chapitre 8 de l’article sur le Planificateur de tâches traçait une ligne : dès qu’un polling à la minute ou une surveillance permanente devient nécessaire, on entre dans le domaine du processus résident. Mais comment construire réellement un service Windows, et comment le faire fonctionner de façon fiable en production ? Laisser cette question dans le flou et se contenter de « le transformer en service pour l’instant » produit des services qu’on ne peut plus arrêter, des services qui meurent silencieusement sans que personne ne s’en aperçoive, et des services tournant sous LocalSystem, capables de tout faire.

Cet article organise, dans l’ordre où on les décide réellement en pratique : un tableau de décision pour savoir si un traitement résident doit devenir un service Windows, le minimum à connaître sur le fonctionnement des services (le SCM, les types de démarrage, la Session 0), une implémentation avec le modèle Worker Service de .NET 8, l’enregistrement et les options de récupération via sc.exe, le choix du compte d’exécution, et l’arrêt sécurisé.

1. L’essentiel d’abord

  • L’axe de décision est simple. Pour un traitement périodique à intervalles de quelques minutes ou plus, le Planificateur de tâches suffit. Dès que les besoins incluent une attente permanente (socket, tube nommé, FileSystemWatcher), une réaction à la seconde près, ou une reprise automatique après un plantage, il faut un service Windows.
  • Depuis Windows Vista, les services s’exécutent en Session 0 et ne peuvent pas interagir directement avec l’utilisateur (ils ne peuvent pas afficher d’UI). Si une UI est nécessaire, il faut séparer le service et l’application d’UI et les faire dialoguer par communication inter-processus.1
  • En .NET, la voie officielle est le modèle Worker Service associé à AddWindowsService du paquet Microsoft.Extensions.Hosting.WindowsServices (ou UseWindowsService pour la famille IHostBuilder). Il peut s’exécuter tel quel comme application console, ce qui rend le développement et le débogage nettement plus faciles que le développement de services traditionnel.2
  • Depuis .NET 6, une exception non gérée dans BackgroundService arrête l’hôte par défaut (BackgroundServiceExceptionBehavior.StopHost). Mais comme il s’agit d’un « arrêt normal », le redémarrage par les options de récupération du SCM ne se déclenche pas. Pour un échec qui doit provoquer un redémarrage, il faut terminer le processus avec un code de sortie non nul.32
  • Ne choisissez pas LocalSystem comme compte d’exécution par simple inertie. sc.exe create a LocalSystem comme valeur par défaut, donc sans y prêter attention, le service démarre avec les privilèges les plus élevés.4 Pour un traitement purement local, LocalService ou un compte virtuel est le bon choix ; pour toucher un partage ou une base de données dans un domaine, un gMSA s’impose.5
  • Les options de récupération (sc.exe failure) et le traitement d’arrêt (HostOptions.ShutdownTimeout, 30 secondes par défaut6) se conçoivent au moment de l’enregistrement. Un « service qui ne répond pas aux demandes d’arrêt » est ce que l’exploitation déteste le plus.

2. Quand faut-il en faire un service ? — tableau de décision entre Planificateur de tâches, service et application résidente

Quand un besoin qui ressemble à du « toujours actif » apparaît, il y a trois options : le Planificateur de tâches, un service Windows, ou une « application résidente enregistrée au démarrage » (le type qui vit dans la zone de notification). Voici comment elles se comparent.

Aspect Planificateur de tâches Service Windows Application résidente (enregistrée au démarrage)
Moment du démarrage Démarré à chaque fois, sur une heure ou un événement Résident depuis le démarrage de l’OS (avant l’ouverture de session) Depuis l’ouverture de session de l’utilisateur
Session ouverte requise ? Peut fonctionner sans session ouverte (exécution non interactive) Non requise Requise (disparaît à la fermeture de session)
UI Impossible à afficher (en configuration non interactive) Impossible à afficher (Session 0) Possible (zone de notification, boîtes de dialogue)
Permanent ou périodique Périodique (adapté à des intervalles horaires à quotidiens) Permanent Permanent (mais dans la session utilisateur)
Privilèges Compte d’exécution défini par tâche Compte de service (chapitre 6) Privilèges de l’utilisateur connecté
Surveillance et récupération Historique + notifications maison Options de récupération du SCM (redémarrage automatique) Aucune (à construire soi-même)
Effort de déploiement Enregistrement via XML / PowerShell sc.exe / installeur Enregistrement dans la clé Run, etc.

Voici l’axe de décision.

  • Pour un traitement périodique, de quelques fois par jour à toutes les heures, sans état conservé entre deux exécutions, utilisez le Planificateur de tâches. En faire un service pour gérer soi-même un minuteur est excessif dans ce cas ; l’exploitation décrite dans l’article sur le Planificateur de tâches revient bien moins cher.
  • Si l’essence même du besoin est une attente permanente — attendre des requêtes en TCP ou par tube, surveiller un dossier avec FileSystemWatcher, traiter une file séquentiellement, réagir à la seconde près — alors c’est un service Windows qu’il faut. Si vous commencez à découper le travail en « tâche toutes les 5 minutes » pour faire du polling en tranches, c’est le signe que vous réimplémentez, en moins bien, un processus résident.
  • Si l’UI est l’essentiel (pilotage depuis la zone de notification, pop-up vers l’utilisateur), c’est une application résidente qu’il faut. Mais elle ne fonctionne que si une session est ouverte, donc elle ne convient pas à un usage de type serveur. Si « le traitement en arrière-plan doit tourner en permanence, mais une UI est aussi nécessaire », séparez-le en deux processus, un service et une application d’UI, comme décrit au chapitre suivant.

Un autre critère de décision, souvent négligé, est la reprise. Un job du Planificateur de tâches qui échoue reste simplement en l’état jusqu’à la prochaine exécution planifiée, alors que pour un service, le SCM se charge du redémarrage automatique (chapitre 5). Un besoin du type « s’il tombe pendant la nuit, il doit s’être rétabli tout seul avant le matin » constitue, en soi, une raison d’en faire un service.

3. Le minimum à savoir sur le fonctionnement des services — SCM, type de démarrage, Session 0

3.1 Le SCM et les types de démarrage

L’organisateur des services Windows est le gestionnaire de contrôle des services (SCM). Il gère le registre des services, sert d’intermédiaire pour les demandes de démarrage et d’arrêt, et exécute les actions de récupération en cas d’échec. Un processus de service doit être construit pour dialoguer avec le SCM selon son protocole, mais les bibliothèques .NET s’en chargent à notre place (chapitre 4), si bien qu’il ne nous reste, en tant que concepteurs, que quatre choses à décider : le type de démarrage, le compte d’exécution, la récupération et l’arrêt.

Il y a quatre choix pour le type de démarrage.4

Type de démarrage Comportement Où l’utiliser
Automatique (auto) Démarre au démarrage de l’OS La base pour un service permanent
Automatique, démarrage différé (delayed-auto) Démarre un peu après les autres services automatiques Le premier choix pour un service métier ; évite l’encombrement juste après le démarrage et les dépendances pas encore prêtes
Manuel (demand) Démarre seulement sur demande Service auxiliaire lancé par une autre application
Désactivé (disabled) Ne peut pas démarrer Mise à l’arrêt / verrouillage

Pour un service métier, faites du démarrage différé la valeur par défaut. Juste après le démarrage de l’OS, ni le réseau, ni la base de données, ni les autres services ne sont encore tous prêts, donc démarrer le plus vite possible avec « Automatique » échoue souvent à la première tentative de connexion. Le montage stable consiste à gagner du temps avec le démarrage différé, puis à absorber les échecs de connexion restants par des nouvelles tentatives dans ExecuteAsync, décrit plus loin.

Il faut aussi connaître le délai d’expiration du démarrage. Le SCM n’attend pas indéfiniment le rapport de fin de démarrage d’un service. Au-delà du délai par défaut (ServicesPipeTimeout, 30 secondes), les événements 7000 / 7011 sont consignés et le démarrage est considéré comme échoué.7 Autrement dit, n’effectuez pas dans le « traitement de démarrage » une initialisation lourde comme des tentatives répétées de connexion à une base de données ou la construction d’un grand cache. La règle d’or est de terminer le démarrage immédiatement et de faire le travail lourd dans la boucle principale (ExecuteAsync).

3.2 Isolation de la Session 0 — un service ne peut pas afficher d’UI

Depuis Windows Vista, les services s’exécutent dans une session isolée appelée Session 0, et ne peuvent pas interagir directement avec l’utilisateur.1 Appeler MessageBox.Show ou afficher un formulaire depuis un service n’affiche rien sur l’écran de l’utilisateur connecté. Pire, c’est la cause d’un blocage classique : l’ensemble du traitement s’arrête en attendant indéfiniment un bouton OK que personne ne peut cliquer. Lors du portage d’un ancien code vers un service, vérifiez toujours qu’aucune boîte de message destinée à afficher une erreur n’a été laissée en place.

Quand une UI est nécessaire, la bonne solution est de séparer le service (Session 0) et l’application d’UI (session utilisateur) en deux processus, qui dialoguent par communication inter-processus (IPC). C’est la conception que Microsoft lui-même recommande : on utilise un mécanisme IPC tel qu’un tube nommé, le côté UI renvoyant les résultats au service.1 Le choix du mécanisme IPC est détaillé dans l’article compagnon publié le même jour, « Un tableau de décision pour la communication inter-processus ». Mettre également l’application d’UI sur Generic Host permet d’aligner l’injection de dépendances, la journalisation et la configuration entre les deux processus (voir « Utiliser Generic Host et BackgroundService dans une application de bureau »).

4. Comment le construire en .NET — le modèle Worker Service et AddWindowsService

4.1 Le modèle et Program.cs

Le développement d’un service en .NET part du modèle Worker Service. Il est fourni avec le SDK, donc dotnet new worker génère le squelette du projet ; il suffit ensuite d’ajouter le paquet Microsoft.Extensions.Hosting.WindowsServices et d’appeler AddWindowsService pour être prêt à s’exécuter comme un service Windows.2 Le mécanisme du Generic Host qui sert de socle est expliqué dans « Qu’est-ce que le Generic Host .NET ? ».

using App.MonitorService;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// Intègre le cycle de vie qui dialogue avec le SCM lorsque l'application est démarrée comme service Windows.
// Démarrée comme application console, elle continue de fonctionner avec le ConsoleLifetime habituel
builder.Services.AddWindowsService(options =>
{
    options.ServiceName = "KsMonitor";
});

builder.Services.AddSingleton<MeasurementQueue>();
builder.Services.AddHostedService<MonitorWorker>();

IHost host = builder.Build();
host.Run();

Pour un projet de style classique utilisant Host.CreateDefaultBuilder, appelez à la place l’extension UseWindowsService() d’IHostBuilder. Son rôle est identique.

Il existe un piège de la même nature que le problème du « Répertoire de démarrage (facultatif) » de l’article sur le Planificateur de tâches. Quand le service démarre, le répertoire courant est C:\Windows\System32. Si vous chargez appsettings.json ou un fichier similaire en supposant un chemin relatif, vous obtenez « ça marche depuis la console, mais le service n’arrive pas à lire sa configuration ». Résolvez les fichiers de configuration par rapport à l’exécutable, ou bien, comme le recommande le tutoriel officiel, passez --contentRoot dans le binpath lors de l’enregistrement pour expliciter la racine de contenu.2

4.2 ExecuteAsync — concevoir autour de stoppingToken et des exceptions

Le corps du service s’écrit dans ExecuteAsync, en héritant de BackgroundService. Ce qu’il faut décider ici se résume à deux points : comment répondre à une demande d’arrêt, et comment traiter les exceptions.

namespace App.MonitorService;

public sealed class MonitorWorker(
    MeasurementQueue queue,
    ILogger<MonitorWorker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    // Retire un élément de la file et le traite. Passez toujours le token, même à l'attente
                    var item = await queue.DequeueAsync(stoppingToken);
                    await ProcessAsync(item, stoppingToken);
                }
                catch (Exception ex) when (ex is IOException or TimeoutException)
                {
                    // Échec avec lequel on peut continuer : on consigne, puis on retente après un délai
                    logger.LogError(ex, "Le traitement a échoué. Nouvelle tentative dans 30 secondes.");
                    await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
                }
            }
        }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
        {
            // Demande d'arrêt venant du SCM. C'est le déroulement normal, donc on ne fait rien.
            // La clause `when` restreint ce cas à une annulation causée par stoppingToken,
            // afin qu'une annulation levée par un délai d'expiration interne ne soit pas prise
            // à tort pour un « arrêt normal », ce qui laisserait le worker silencieusement
            // arrêté pendant que l'hôte continuerait de vivre
        }
        catch (Exception ex)
        {
            // Échec inattendu : avec le comportement par défaut StopHost, l'hôte s'arrête
            // « normalement », donc l'option de récupération du SCM (redémarrage automatique)
            // ne se déclenche jamais.
            // On termine avec un code de sortie non nul, pour signaler au SCM qu'il s'agit d'un échec
            logger.LogCritical(ex, "Erreur irrécupérable : arrêt du service.");
            Environment.Exit(1);
        }
    }

    private Task ProcessAsync(Measurement item, CancellationToken token)
        => throw new NotImplementedException();
}
  • Passez stoppingToken à toutes les attentes. Task.Delay, les E/S, l’attente sur la file. Il suffit d’une seule attente longue qui ignore le token pour devenir le terreau d’un « service qui ne répond pas aux demandes d’arrêt » (chapitre 7).
  • Séparez les échecs récupérables des échecs irrécupérables. Interceptez dans la boucle les coupures réseau et les délais d’expiration temporaires, consignez-les et retentez. Un catch (Exception) qui se contente d’avaler l’erreur et de faire continue ne produit qu’un service qui tourne silencieusement à vide. La façon de décider dans quelle catégorie classer un échec est détaillée dans « Un tableau de décision pour choisir entre se terminer ou continuer face à une exception inattendue ».
  • Connaissez le comportement par défaut pour une exception non gérée. Avant .NET 6, une exception qui s’échappait d’ExecuteAsync disparaissait sans laisser de trace, et le service devenait un « zombie » : il semblait fonctionner mais ne faisait plus rien. Depuis .NET 6, le comportement par défaut est BackgroundServiceExceptionBehavior.StopHost : l’exception est consignée puis l’hôte s’arrête.3 Mais cet arrêt est un « arrêt normal », donc même avec des options de récupération configurées, il n’y a pas de redémarrage. Pour un échec que vous voulez voir relevé par les options de récupération, terminez explicitement de façon anormale avec Environment.Exit(1), comme dans le code ci-dessus — c’est la méthode indiquée par le tutoriel officiel.2

4.3 Toujours exécutable comme application console — la principale raison pour laquelle le développement est devenu plus simple

AddWindowsService ne bascule vers le cycle de vie propre aux services que lorsque l’application est réellement démarrée comme un service Windows. Autrement dit, le même exe s’exécute comme une application console ordinaire sous F5 dans Visual Studio ou avec dotnet run. Les points d’arrêt fonctionnent, et appuyer sur Ctrl+C permet de vérifier localement tout le déroulement, de la demande d’arrêt jusqu’à StopAsync.

Cela dit, fonctionner depuis la console ne garantit pas de fonctionner comme service. Trois points doivent vraiment être vérifiés en conditions réelles, en tant que service : la différence de compte d’exécution (chapitre 6), la Session 0, et le répertoire courant. La cause de « ça marche depuis la console mais pas en tant que service » se ramène presque toujours à l’un de ces trois points.

5. Enregistrement et exploitation — sc.exe, options de récupération et journal des événements

5.1 Enregistrement avec sc.exe create

Une fois l’exe publié (dotnet publish — une publication en fichier unique est plus simple à manier), enregistrez-le auprès du SCM avec sc.exe create. Exécutez cette commande depuis un PowerShell administrateur.

sc.exe create "KsMonitor" binpath= "C:\Services\KsMonitor\KsMonitor.exe" start= delayed-auto obj= "NT AUTHORITY\LocalService" displayname= "Service KS d'ingestion des données de mesure"
sc.exe description "KsMonitor" "Service résident qui ingère les données de mesure et les enregistre dans la base de données (responsable : service informatique)"

D’abord un piège classique. Un espace est obligatoire après le signe égal de binpath= et de start=. C’est une particularité de syntaxe propre à sc.exe : tout ce qui précède le signe égal est le nom de l’option, et un espace entre celui-ci et la valeur est obligatoire (l’omettre fait échouer la commande).4 Par ailleurs, si vous omettez obj=, la valeur par défaut est LocalSystem.4 Un enregistrement fait sans y réfléchir démarre donc avec les privilèges les plus élevés ; faites le choix du chapitre 6 au moment de l’enregistrement.

Définir description est discrètement important. Laisser une information qui permettra à quelqu’un, dans quelques années, en ouvrant services.msc, de juger « à quoi sert ce service, si on peut l’arrêter, et à qui demander » fait disparaître beaucoup de coût d’investigation ultérieur. Pour le supprimer, arrêtez-le d’abord puis exécutez sc.exe delete "KsMonitor".2

Si le déploiement concerne plusieurs machines, ou si des mises à jour (remplacement de fichiers) ont lieu régulièrement, orientez-vous rapidement vers un installeur (MSI, ServiceInstall de WiX, etc.) qui prend en charge ensemble l’enregistrement, la mise à jour et la suppression, plutôt que de faire cela à la main avec sc.exe. Une procédure d’enregistrement manuelle finit toujours par produire un poste où une étape a été sautée.

5.2 Options de récupération — laissez le SCM gérer le « redémarrage en cas de panne »

Un des grands avantages de transformer un traitement en service est de bénéficier des options de récupération standard du SCM. On peut configurer de façon déclarative le comportement à adopter quand le processus se termine anormalement.2

sc.exe failure "KsMonitor" reset= 86400 actions= restart/60000/restart/60000/restart/300000

Cet exemple signifie : « redémarrer après 60 secondes lors du 1er et du 2e échec, redémarrer après 5 minutes à partir du 3e échec, et remettre le compteur d’échecs à zéro après 24 heures ». Le même réglage peut se faire via l’interface graphique (propriétés de services.msc → onglet « Récupération »). Utilisez d’abord cette fonctionnalité standard avant de construire votre propre « tâche de surveillance » ou script de supervision.

Deux précautions seulement. Premièrement, comme au chapitre précédent, cela ne se déclenche que si le processus se termine avec un code non nul ; un arrêt normal via StopHost n’est pas couvert par la récupération. Deuxièmement, si le service est certain de tomber juste après le démarrage (erreur de configuration, base de données injoignable), on obtient une boucle de redémarrage. C’est justement pour cela qu’il faut allonger l’intervalle à partir du 3e échec, et s’assurer au préalable que « pourquoi il est tombé » reste consigné dans le journal des événements (section suivante).

5.3 Combiner journal des événements et journal de fichiers

Sous Windows, Host.CreateApplicationBuilder ajoute automatiquement le fournisseur de journalisation EventLog. Et par défaut, seuls les niveaux Warning et au-dessus atteignent le journal des événements.2 Il est facile de se laisser troubler en pensant « en en faisant un service, tous mes journaux Information ont disparu » — ils n’ont pas disparu, ils sont simplement bloqués par le filtre par défaut du journal des événements. Si vous voulez aussi consigner des événements d’exploitation comme le démarrage et l’arrêt, précisez explicitement le niveau du fournisseur EventLog dans appsettings.json.

{
  "Logging": {
    "EventLog": {
      "SourceName": "KsMonitor",
      "LogLevel": {
        "Default": "Warning",
        "Microsoft.Hosting.Lifetime": "Information"
      }
    }
  }
}

Une précaution : une source d’événement ne peut pas être écrite si elle n’a pas été enregistrée au préalable, et cet enregistrement exige les privilèges administrateur. Omettre SourceName n’y échappe pas non plus — par défaut, AddWindowsService utilise le nom de l’application comme nom de source2, donc dans tous les cas on part d’une « source non enregistrée ». Un compte à privilèges minimaux comme LocalService ne peut pas créer de source à l’exécution, et si la création échoue, l’exploitation démarre sans rien du tout écrit dans le journal des événements. Faites cet enregistrement du côté de l’installeur (ou d’une étape de configuration exécutée avec les privilèges administrateur). C’est le même sujet que celui traité au chapitre 6 de l’article sur le Planificateur de tâches : « séparer CreateEventSource vers le côté configuration ».

Le repère pour la répartition des rôles : le journal des événements ne doit porter que ce que l’exploitant doit voir (démarrage, arrêt, échec, récupération), et le détail de la trace des traitements va dans votre propre journal de fichiers. Les exigences minimales pour tenir son propre journal de fichiers (rotation, comportement en cas d’échec d’écriture) sont détaillées dans « Les exigences minimales d’un enregistreur maison », et la conception qui laisse des preuves quand tout le processus s’effondre est traitée dans « Concevoir des applications Windows qui laissent des journaux et des dumps en cas de plantage ». Dans une configuration où les options de récupération redémarrent automatiquement le service, ce qui a été consigné à l’instant précis du plantage est le seul indice dont vous disposerez.

6. Le compte d’exécution — ne choisissez pas LocalSystem par inertie

Un service s’exécute dans le contexte de sécurité du compte indiqué ; au démarrage, le SCM ouvre une session sous ce compte et attache son jeton au processus.8 C’est la version « service » de la discussion sur « en tant que qui le traitement s’exécute » menée au chapitre 3 de l’article sur le Planificateur de tâches. Voici les options.

Compte Privilèges Identité réseau Gestion du mot de passe Où l’utiliser
LocalSystem Extrêmement élevés (équivalents à l’OS) Compte d’ordinateur Non nécessaire À éviter en principe. Attention, c’est la valeur par défaut de sc.exe create
LocalService Minimaux Anonyme (pas d’accès aux partages) Non nécessaire Premier choix pour un traitement purement local
NetworkService Minimaux Compte d’ordinateur Non nécessaire Traitement léger touchant des ressources du domaine
Compte virtuel (NT SERVICE\nom-du-service) Seulement ce qui est accordé Compte d’ordinateur Non nécessaire Peut recevoir une ACL par service. Privilège minimal pour des ressources locales
gMSA Seulement ce qui est accordé Le gMSA lui-même Géré automatiquement par le contrôleur de domaine Le choix de référence pour un service touchant un partage ou une base de données dans un domaine

Il y a trois points de décision.

  • Ne choisissez pas LocalSystem parce que « ça marche ». Une vulnérabilité du service se traduit directement par la prise de contrôle de toute la machine. Pour savoir si des privilèges équivalents à l’administrateur sont réellement nécessaires, l’analyse de « Quand les privilèges administrateur sont-ils vraiment nécessaires ? » s’applique telle quelle. Dans la plupart des cas, ce qui est nécessaire tient à quelque chose comme « écrire dans un dossier précis », et il suffit alors d’accorder l’ACL de ce dossier à un compte virtuel (NT SERVICE\KsMonitor). Un compte virtuel ne nécessite ni création ni gestion de mot de passe.5
  • Le piège des partages réseau. LocalService est anonyme sur le réseau, donc l’accès à \\serveur\partage échoue. NetworkService, les comptes virtuels et LocalSystem sortent sur le réseau en tant que compte d’ordinateur (DOMAINE\nom-machine$)5, donc côté partage, il faut accorder les autorisations de partage et NTFS au compte d’ordinateur. « J’ai donné l’autorisation à l’utilisateur, mais seul le service n’arrive pas à lire » vient presque toujours de là. Décidez d’abord sous quelle identité vous sortez sur le réseau, puis configurez le partage en conséquence.
  • Si vous faites tourner le service sous un compte utilisateur ordinaire, prévoyez aussi la gestion du mot de passe. Utiliser un compte dédié exige le droit « Ouvrir une session en tant que service », et une fois le mot de passe expiré, l’ouverture de session échoue et le service ne peut plus démarrer.8 C’est la version « service » du problème « la tâche meurt silencieusement après un changement de mot de passe ». En environnement de domaine, la bonne réponse consiste à faire disparaître ce problème avec un gMSA dont le mot de passe est géré automatiquement par le domaine (la même conclusion qu’à la section 3.2 de l’article sur le Planificateur de tâches).

7. Arrêt sécurisé — ShutdownTimeout et traitement en cours

Voyons le déroulement de l’arrêt. Quand services.msc, sc.exe stop ou l’arrêt de l’OS fait émettre une demande d’arrêt par le SCM, le cycle de vie du service .NET la traduit en arrêt de l’hôte (StopApplication) : stoppingToken est annulé, ExecuteAsync se termine, et le StopAsync de chaque service est appelé. Le temps que l’hôte attend l’ensemble de cette séquence d’arrêt est HostOptions.ShutdownTimeout, dont la valeur par défaut est 30 secondes.69 Si ce délai est dépassé, l’arrêt est forcé sans attendre la fin du nettoyage.

Si 30 secondes ne suffisent pas pour terminer d’écrire un élément en cours de traitement, prolongez le délai en partant du temps réellement nécessaire.

builder.Services.Configure<HostOptions>(options =>
{
    // Partez du temps maximal nécessaire pour terminer d'écrire le traitement en cours
    options.ShutdownTimeout = TimeSpan.FromSeconds(90);
});

Cela posé, la conception du traitement d’arrêt tient en trois points.

  • Décidez à l’avance, sous forme de séquence, ce qu’il faut faire après une demande d’arrêt. Arrêter d’accepter de nouveaux travaux → terminer le traitement en cours (ou l’interrompre à un point de rupture sûr et le consigner sous une forme reprenable) → nettoyer les connexions et les fichiers temporaires. Sans décider cet ordre à l’avance, on tombe dans l’un des deux extrêmes : tout abandonner dès qu’on voit le token, ou essayer de tout terminer et manquer de temps.
  • Ne construisez pas un « service qui ne répond pas aux demandes d’arrêt ». Il suffit d’une seule longue boucle ou attente qui ignore le token pour obtenir un service figé indéfiniment sur « en cours d’arrêt » côté SCM. C’est le défaut le plus détesté en exploitation : il empêche les redémarrages de Windows Update et impose un kill manuel à chaque arrêt planifié du serveur. Comme au chapitre 4, vérifiez le token à chaque point de rupture du traitement, et passez un CancellationToken à toute E/S longue. Comme Ctrl+C déclenche exactement ce flux d’arrêt en exécution console, nous recommandons d’ajouter « combien de secondes après Ctrl+C pour se terminer » à votre liste de tests avant mise en production.
  • Priorisez une conception qui survit à un kill plutôt que le soin apporté au traitement d’arrêt. Une coupure de courant ou une terminaison forcée se produit quoi qu’il arrive, peu importe le soin apporté au traitement d’arrêt. Les transactions, les écritures de fichiers atomiques, des unités de traitement rejouables — une conception où « même en mourant en cours de route, la cohérence est rétablie au prochain démarrage » constitue le fond du problème ; un traitement d’arrêt soigné n’est, en définitive, qu’une question de bonnes manières.

8. Résumé

La décision est simple. Le traitement périodique va au Planificateur de tâches ; s’il faut une attente permanente, une réaction à la seconde près ou une récupération automatique, c’est un service Windows ; si l’UI est l’essentiel, c’est une application résidente. Une fois la décision prise d’en faire un service, la voie standard actuelle consiste à utiliser le Worker Service de .NET avec AddWindowsService, sous la forme « développer comme une application console, livrer comme un service ».

Il y a quatre points à décider comme conception au moment de l’enregistrement : le type de démarrage (démarrage différé pour un service métier), le compte d’exécution (éviter LocalSystem, préférer un compte virtuel ou un gMSA), les options de récupération (sc.exe failure associé à Environment.Exit(1)), et l’arrêt (ShutdownTimeout et le nettoyage du traitement en cours). En maîtrisant ces quatre points, ainsi qu’une écriture d’ExecuteAsync qui respecte stoppingToken, un service Windows cesse d’être « quelque chose de particulièrement difficile à construire ». À l’inverse, un service qui a commencé à tourner sans que ces quatre points aient été décidés revient, des années plus tard, avec une triple peine : impossible à arrêter, personne ne remarque quand il tombe, et des privilèges trop élevés pour qu’on ose y toucher. Redresser un service existant part aussi, le plus efficacement, d’un inventaire de ces quatre points.

Articles connexes

Domaines de conseil associés

KomuraSoft LLC prend en charge les consultations sur la revue de conception des traitements résidents (y compris la décision de migrer depuis le Planificateur de tâches), ainsi que l’investigation et le redressement de services Windows qui « ne répondent pas aux demandes d’arrêt » ou « tombent sans que personne ne s’en aperçoive ».

Références

  1. Microsoft Learn, Interactive Services. Sur le fait que, depuis Windows Vista, les services ne peuvent pas interagir directement avec l’utilisateur, qu’ils s’exécutent en Session 0, et que, lorsqu’une UI est nécessaire, la conception recommandée consiste à collaborer avec une application GUI dans un processus séparé via IPC.  2 3

  2. Microsoft Learn, Create Windows Service using BackgroundService. Tutoriel officiel pour transformer un Worker Service en service Windows : AddWindowsService, le fait que le fournisseur EventLog a Warning comme niveau par défaut, l’enregistrement / la récupération / la suppression via sc.exe, et la nécessité d’un code de sortie non nul pour les options de récupération.  2 3 4 5 6 7 8 9

  3. Microsoft Learn, .NET 6 breaking change: Exception handling in hosting. Sur le fait que, depuis .NET 6, une exception non gérée dans BackgroundService.ExecuteAsync est consignée puis arrête l’hôte par défaut (BackgroundServiceExceptionBehavior.StopHost).  2

  4. Microsoft Learn, sc.exe create. Sur les spécifications de start= (auto / demand / delayed-auto, etc.) et obj= (LocalSystem par défaut), ainsi que sur l’obligation d’un espace après le signe égal des options (son omission fait échouer la commande).  2 3 4

  5. Microsoft Learn, Service Accounts in Windows Server. Sur le fait qu’un compte virtuel (NT SERVICE\nom-du-service) accède au réseau avec les identifiants du compte d’ordinateur sans nécessiter de gestion de mot de passe, et que le mot de passe d’un gMSA est géré automatiquement côté domaine.  2 3

  6. Microsoft Learn, .NET Generic Host. Sur le déroulement de l’arrêt de l’hôte et sur le délai d’expiration d’arrêt configurable (30 secondes par défaut) qu’il attend.  2

  7. Microsoft Learn, A service does not start, and events 7000 and 7011 are logged. Sur le fait que le SCM n’attend le démarrage d’un service que pendant la durée spécifiée par ServicesPipeTimeout, et consigne les événements 7000 / 7011 en cas de dépassement, ainsi que sur la procédure pour relever le délai par défaut. 

  8. Microsoft Learn, Service User Accounts. Sur le fait qu’un service s’exécute dans le contexte de sécurité du compte indiqué, que le SCM effectue une ouverture de session au démarrage, qu’un mot de passe expiré empêche le service de démarrer, et sur les comptes spéciaux LocalService / NetworkService / LocalSystem.  2

  9. Microsoft Learn, HostOptions.ShutdownTimeout Property. Sur la définition de la propriété qui contrôle le délai d’expiration par défaut de StopAsync

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.

Comment choisir entre le Planificateur de tâches et un service Windows ?
Pour un traitement périodique à intervalles de quelques minutes ou plus, sans état à conserver entre deux exécutions, le Planificateur de tâches suffit. Dès que les besoins incluent une attente permanente en TCP ou par tube, une surveillance de dossier avec FileSystemWatcher, une réaction à la seconde près, ou une reprise automatique après un plantage, il faut un service Windows. Si vous commencez à découper le traitement en « tâche toutes les 5 minutes » pour faire du polling en tranches, c'est le signe que vous réimplémentez, en moins bien, un processus résident. Si l'UI est l'essentiel (pilotage depuis la zone de notification, par exemple), une troisième option existe : une application résidente qui ne fonctionne que pendant la session ouverte.
Un service Windows peut-il afficher une UI (un écran) ?
Non. Depuis Windows Vista, les services s'exécutent dans une session isolée appelée Session 0 et ne peuvent pas interagir directement avec l'utilisateur. Appeler MessageBox.Show depuis un service n'affiche rien sur l'écran de l'utilisateur, et provoque un blocage classique : le traitement s'arrête en attendant indéfiniment un bouton OK que personne ne peut cliquer. Quand une UI est nécessaire, la bonne solution consiste à séparer le service et l'application d'UI en deux processus distincts, qui dialoguent via une communication inter-processus telle qu'un tube nommé. Lors du portage d'un ancien code vers un service, vérifiez toujours qu'aucune boîte de message destinée à afficher une erreur n'a été laissée en place.
Comment créer un service Windows en .NET ?
La voie officielle consiste à partir du modèle Worker Service (dotnet new worker), à ajouter le paquet Microsoft.Extensions.Hosting.WindowsServices, puis à appeler AddWindowsService. Le même exe s'exécute comme une application console ordinaire avec F5 dans Visual Studio ou avec dotnet run, ce qui rend le développement et le débogage nettement plus faciles. L'enregistrement se fait avec sc.exe create, mais attention à sa syntaxe particulière : un espace est obligatoire après le signe égal de binpath= ou de start=, et si vous omettez obj=, le compte par défaut est LocalSystem (les privilèges les plus élevés). Au démarrage du service, le répertoire courant est C:\Windows\System32, donc les fichiers de configuration doivent être résolus par rapport à l'exécutable.
Que se passe-t-il pour le service en cas d'exception dans BackgroundService ?
Depuis .NET 6, une exception non gérée qui s'échappe d'ExecuteAsync est, par défaut (BackgroundServiceExceptionBehavior.StopHost), consignée dans le journal puis provoque l'arrêt de l'hôte. Mais comme cela compte comme un « arrêt normal », les options de récupération du SCM (redémarrage automatique) ne se déclenchent pas. Pour qu'un échec provoque un redémarrage via les options de récupération, il faut faire se terminer le processus explicitement de façon anormale, avec un code de sortie non nul, comme dans Environment.Exit(1). Les échecs qui permettent de continuer, comme une coupure réseau, doivent être interceptés dans la boucle, consignés puis retentés, en les distinguant dès la conception des échecs irrécupérables.

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