Table of Contents

ZebraPuma.System.ServiceProcess

Gestion Avancée des Services Windows avec Support Plugins


📋 Table des Matières


Vue d'ensemble

ZebraPuma.System.ServiceProcess est une bibliothèque avancée pour créer et gérer des services Windows extensibles en .NET. Elle combine le framework de services Windows natif avec le système de plugins ZebraPuma pour créer des services modulaires et maintenables.

Caractéristiques Principales

  • Services Windows étendus avec interface IServiceExtended
  • Classe de base ServiceBaseExtended thread-safe
  • ServiceManager pour gérer plusieurs services simultanément
  • Interface WinForms de contrôle (ServiceControllerForm)
  • Intégration complète avec ZebraPuma.Plugins
  • Support multi-frameworks avec adaptation automatique
  • Mode console pour développement et debugging
  • Mode service pour production Windows
  • Rechargement dynamique des services

Cibles Supportées

  • .NET Framework 4.8 (Windows)
  • .NET 10.0-windows

Dépendances

  • ZebraPuma.Plugins (projet référencé)
  • Newtonsoft.Json 13.0.4
  • NLog 6.0.7
  • System.ServiceProcess (pour .NET Framework 4.8)
  • System.ServiceProcess.ServiceController 9.0.0 (pour .NET 10.0)

Architecture

Diagramme de Classes

┌─────────────────────────────────────────────────────────┐
│                  Windows Service Host                    │
│                                                           │
│  ┌────────────────────────────────────────────────────┐  │
│  │           ServiceManager                           │  │
│  │  • Services: List<IServiceExtended>               │  │
│  │  • StartService(name)                             │  │
│  │  • StopService(name)                              │  │
│  │  • StartAll() / StopAll()                         │  │
│  │  • ReloadFromPlugins()                            │  │
│  │  • Events: ServiceStateChanged, ServicesReloaded  │  │
│  └────────────────────────────────────────────────────┘  │
│                          │                               │
│          ┌───────────────┴────────────┐                  │
│          ▼                            ▼                  │
│  ┌──────────────┐            ┌──────────────┐            │
│  │ IServiceExt. │            │ServiceFactory│            │
│  │  + IPlugin   │            │ LoadServices │            │
│  └──────────────┘            └──────────────┘            │
│          │                                               │
└──────────┼───────────────────────────────────────────────┘
           │
           ▼
┌────────────────────────────────────────┐
│     ServiceBaseExtended                │
│     : ServiceBase, IServiceExtended    │
│                                        │
│  • OnStart(args) → OnStartCore(args)  │
│  • OnStop() → OnStopCore()            │
│  • StartService() / StopService()     │
│  • IsRunning (thread-safe)            │
│  • Lock-based synchronization         │
└────────────────────────────────────────┘
           │
           ▼
┌────────────────────────────────────────┐
│      Votre Service Concret             │
│                                        │
│  protected override OnStartCore()     │
│  protected override OnStopCore()      │
└────────────────────────────────────────┘

Flux d'Exécution

Mode Service Windows

1. Windows SCM lance le service
        ↓
2. OnStart() appelé automatiquement
        ↓
3. ServiceBaseExtended.OnStart() → StartInternal()
        ↓
4. Lock acquisition
        ↓
5. OnStartCore() de votre service
        ↓
6. IsRunning = true
        ↓
7. Service en cours d'exécution
        ↓
8. Windows SCM demande l'arrêt
        ↓
9. OnStop() → StopInternal()
        ↓
10. OnStopCore() de votre service
        ↓
11. IsRunning = false

Mode Console (Debug)

1. Application console démarre
        ↓
2. ServiceManager.CreateFromPlugins()
        ↓
3. Chargement des services via PluginLoader
        ↓
4. ServiceControllerForm.Show() (optionnel)
        ↓
5. manager.StartAll() ou StartService(name)
        ↓
6. Services s'exécutent
        ↓
7. manager.StopAll() avant exit

Interfaces et Classes

IServiceExtended

Interface étendant IPlugin avec les capacités de service Windows.

public interface IServiceExtended : IPlugin
{
    /// <summary>
    /// Démarre le service Windows.
    /// </summary>
    void StartService();
    
    /// <summary>
    /// Arrête le service Windows.
    /// </summary>
    void StopService();
    
    /// <summary>
    /// Indique si le service est actuellement en cours d'exécution.
    /// </summary>
    bool IsRunning { get; }
}

ServiceBaseExtended

Classe de base abstraite pour créer des services Windows.

public abstract class ServiceBaseExtended : ServiceBase, IServiceExtended
{
    // Propriétés héritées de IPlugin
    public abstract string Name { get; }
    public virtual string Version { get; }
    public virtual string Description { get; }
    
    // Propriété de IServiceExtended
    public bool IsRunning { get; }
    
    // Méthodes publiques
    public void StartService();
    public void StopService();
    public virtual void Initialize(IPluginContext context);
    
    // Méthodes abstraites à implémenter
    protected abstract void OnStartCore(string[] args);
    protected abstract void OnStopCore();
    
    // Pattern Dispose
    protected virtual void DisposeManagedResources();
    protected virtual void DisposeUnmanagedResources();
}

Caractéristiques :

  • Thread-safe : Utilise un lock pour synchroniser Start/Stop
  • Gestion d'état : IsRunning volatile pour lecture thread-safe
  • Sealed overrides : OnStart() et OnStop() sont sealed, vous devez override OnStartCore() et OnStopCore()
  • Exception safety : Si OnStartCore() lève une exception, IsRunning reste false

ServiceManager

Gestionnaire centralisé pour administrer plusieurs services.

public class ServiceManager : IDisposable
{
    // Constructeurs
    public ServiceManager(IServiceExtended service);
    public ServiceManager(List<IServiceExtended> services);
    
    // Factory methods
    public static ServiceManager CreateFromPlugins(PluginLoadingOptions options = null);
    
    // Propriétés
    public IList<IServiceExtended> Services { get; }
    
    // Méthodes de contrôle
    public void StartService(string name);
    public void StopService(string name);
    public void RestartService(string name);
    public void StartAll();
    public void StopAll();
    public void ReloadFromPlugins(PluginLoadingOptions options = null);
    
    // Événements
    public event EventHandler<ServiceStateChangedEventArgs> ServiceStateChanged;
    public event EventHandler ServicesReloaded;
}

ServiceFactory

Factory pour créer des services depuis une configuration JSON.

public class ServiceFactory
{
    public List<IServiceExtended> LoadServices(string configPath);
}

ServiceDefinition

Classe de configuration pour définir un service.

public class ServiceDefinition
{
    public string Name { get; set; }
    public string Type { get; set; }
    public string Description { get; set; }
}

ServiceManager

Le ServiceManager est le composant central pour gérer plusieurs services.

Création

Depuis des plugins :

var manager = ServiceManager.CreateFromPlugins(new PluginLoadingOptions
{
    PluginsDirectoryName = "Services",
    ConfigFileName = "services.json"
});

Avec une liste de services :

var services = new List<IServiceExtended>
{
    new MonService1(),
    new MonService2()
};
var manager = new ServiceManager(services);

Contrôle des Services

// Démarrer un service spécifique
manager.StartService("MonService1");

// Arrêter un service
manager.StopService("MonService1");

// Redémarrer
manager.RestartService("MonService1");

// Démarrer tous les services
manager.StartAll();

// Arrêter tous les services
manager.StopAll();

Rechargement Dynamique

// Recharger les services depuis les plugins
manager.ReloadFromPlugins();

// Événement déclenché après rechargement
manager.ServicesReloaded += (sender, args) =>
{
    Console.WriteLine("Services rechargés");
};

Événements

manager.ServiceStateChanged += (sender, args) =>
{
    Console.WriteLine($"Service {args.ServiceName}: {(args.IsRunning ? "Démarré" : "Arrêté")}");
};

ServiceControllerForm

Interface graphique WinForms pour contrôler les services.

Utilisation

var manager = ServiceManager.CreateFromPlugins();
var form = new ServiceControllerForm(manager);
form.ShowDialog();

Fonctionnalités

  • ✅ Liste tous les services avec leur état
  • ✅ Boutons Start/Stop/Restart par service
  • ✅ Bouton Start All / Stop All
  • ✅ Bouton Reload pour recharger les plugins
  • ✅ Refresh automatique de l'interface
  • ✅ Gestion d'erreurs avec MessageBox

Configuration

services.json

Structure pour ServiceFactory :

[
  {
    "Name": "MonService1",
    "Type": "MonApp.Services.MonService1, MonApp.Services",
    "Description": "Premier service"
  },
  {
    "Name": "MonService2",
    "Type": "MonApp.Services.MonService2, MonApp.Services",
    "Description": "Deuxième service"
  }
]

plugins.json

Pour utiliser avec PluginLoader :

{
  "AutoDiscover": true,
  "Plugins": [
    {
      "Folder": "MonService",
      "Assembly": "MonService.dll",
      "Type": "MonApp.Services.MonService"
    }
  ]
}

NLog.config

Logging structuré pour les services :

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <targets>
    <target name="file" xsi:type="File"
            fileName="${basedir}/logs/${shortdate}.log"
            layout="${longdate} ${uppercase:${level}} ${logger} ${message} ${exception:format=tostring}" />
    <target name="eventlog" xsi:type="EventLog"
            source="MonService"
            log="Application"
            layout="${message} ${exception:format=tostring}" />
  </targets>
  <rules>
    <logger name="*" minlevel="Info" writeTo="file" />
    <logger name="*" minlevel="Error" writeTo="eventlog" />
  </rules>
</nlog>

Utilisation

Scénario 1 : Service Simple

Étape 1 : Créer le Service

using System;
using System.Threading;
using NLog;
using ZebraPuma.Plugins;
using ZebraPuma.System.ServiceProcess;

namespace MonApp.Services
{
    public class HeartbeatService : ServiceBaseExtended
    {
        private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
        private Timer _timer;

        public override string Name => "HeartbeatService";
        public override string Description => "Service envoyant un heartbeat toutes les 10 secondes";

        protected override void OnStartCore(string[] args)
        {
            Logger.Info("Démarrage de {Service}", Name);
            
            _timer = new Timer(OnTick, null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
        }

        protected override void OnStopCore()
        {
            Logger.Info("Arrêt de {Service}", Name);
            
            _timer?.Change(Timeout.Infinite, Timeout.Infinite);
            _timer?.Dispose();
            _timer = null;
        }

        private void OnTick(object state)
        {
            Logger.Debug("Heartbeat à {Time}", DateTime.Now);
        }

        protected override void DisposeManagedResources()
        {
            _timer?.Dispose();
        }
    }
}

Étape 2 : Mode Console (Debug)

using System;
using ZebraPuma.System.ServiceProcess;

namespace MonApp
{
    class Program
    {
        static void Main(string[] args)
        {
            if (Environment.UserInteractive)
            {
                // Mode console pour développement
                var service = new HeartbeatService();
                service.StartService();

                Console.WriteLine("Service démarré. Appuyez sur Enter pour arrêter...");
                Console.ReadLine();

                service.StopService();
                service.Dispose();
            }
            else
            {
                // Mode service Windows
                var servicesToRun = new ServiceBase[]
                {
                    new HeartbeatService()
                };
                ServiceBase.Run(servicesToRun);
            }
        }
    }
}

Étape 3 : Installation Windows

# En tant qu'administrateur

# Créer le service
sc create HeartbeatService binPath="C:\MonApp\MonApp.exe" DisplayName="Heartbeat Service"

# Configurer le démarrage automatique
sc config HeartbeatService start=auto

# Démarrer le service
sc start HeartbeatService

# Vérifier l'état
sc query HeartbeatService

Scénario 2 : Multi-Services avec ServiceManager

Programme Principal

using System;
using System.ServiceProcess;
using ZebraPuma.Plugins;
using ZebraPuma.System.ServiceProcess;

namespace MonApp
{
    class Program
    {
        static void Main(string[] args)
        {
            if (Environment.UserInteractive)
            {
                RunInConsoleMode();
            }
            else
            {
                RunAsWindowsService();
            }
        }

        static void RunInConsoleMode()
        {
            var manager = ServiceManager.CreateFromPlugins(new PluginLoadingOptions
            {
                PluginsDirectoryName = "Services",
                ConfigFileName = "plugins.json"
            });

            // Afficher l'interface de contrôle
            var form = new ServiceControllerForm(manager);
            form.ShowDialog();

            // Cleanup
            manager.StopAll();
            manager.Dispose();
        }

        static void RunAsWindowsService()
        {
            // Charger les services
            var services = PluginLoader.LoadPlugins<IServiceExtended>(new PluginLoadingOptions
            {
                PluginsDirectoryName = "Services"
            });

            // Convertir en ServiceBase[]
            var serviceBases = services
                .OfType<ServiceBase>()
                .ToArray();

            if (serviceBases.Any())
            {
                ServiceBase.Run(serviceBases);
            }
        }
    }
}

Scénario 3 : Service avec Configuration

Configuration (heartbeatservice.json)

{
  "IntervalSeconds": 30,
  "LogToFile": true,
  "LogToEventLog": false,
  "Endpoints": [
    "https://api1.example.com/ping",
    "https://api2.example.com/ping"
  ]
}

Classe de Configuration

public class HeartbeatServiceConfig
{
    public int IntervalSeconds { get; set; } = 10;
    public bool LogToFile { get; set; } = true;
    public bool LogToEventLog { get; set; } = false;
    public List<string> Endpoints { get; set; } = new List<string>();
}

Service avec Configuration

using ZebraPuma.Plugins;
using ZebraPuma.System.ServiceProcess;

public class HeartbeatService : ServiceBaseExtended
{
    private HeartbeatServiceConfig _config;
    private Timer _timer;

    public override string Name => "HeartbeatService";

    public override void Initialize(IPluginContext context)
    {
        base.Initialize(context);
        
        // Charger la configuration
        _config = PluginConfigLoader.LoadAssemblyConfig<HeartbeatServiceConfig>();
        Logger.Info("Configuration chargée: Intervalle={Seconds}s, Endpoints={Count}",
            _config.IntervalSeconds, _config.Endpoints.Count);
    }

    protected override void OnStartCore(string[] args)
    {
        var interval = TimeSpan.FromSeconds(_config.IntervalSeconds);
        _timer = new Timer(OnTick, null, TimeSpan.Zero, interval);
    }

    private void OnTick(object state)
    {
        foreach (var endpoint in _config.Endpoints)
        {
            try
            {
                // Ping l'endpoint
                using (var client = new HttpClient())
                {
                    var response = client.GetAsync(endpoint).Result;
                    Logger.Debug("Ping {Endpoint}: {Status}", endpoint, response.StatusCode);
                }
            }
            catch (Exception ex)
            {
                Logger.Error(ex, "Erreur ping {Endpoint}", endpoint);
            }
        }
    }

    protected override void OnStopCore()
    {
        _timer?.Dispose();
    }
}

Exemples

Exemple 1 : Service de Traitement de Queue

using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using ZebraPuma.System.ServiceProcess;

public class QueueProcessorService : ServiceBaseExtended
{
    private readonly ConcurrentQueue<string> _queue = new ConcurrentQueue<string>();
    private CancellationTokenSource _cts;
    private Task _processingTask;

    public override string Name => "QueueProcessor";

    protected override void OnStartCore(string[] args)
    {
        _cts = new CancellationTokenSource();
        _processingTask = Task.Run(() => ProcessQueue(_cts.Token));
        Logger.Info("Queue processor démarré");
    }

    protected override void OnStopCore()
    {
        _cts?.Cancel();
        _processingTask?.Wait(TimeSpan.FromSeconds(30));
        Logger.Info("Queue processor arrêté");
    }

    private async Task ProcessQueue(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            if (_queue.TryDequeue(out string item))
            {
                await ProcessItem(item);
            }
            else
            {
                await Task.Delay(100, ct);
            }
        }
    }

    private async Task ProcessItem(string item)
    {
        Logger.Debug("Traitement de {Item}", item);
        await Task.Delay(1000); // Simule le traitement
    }

    public void Enqueue(string item)
    {
        _queue.Enqueue(item);
    }

    protected override void DisposeManagedResources()
    {
        _cts?.Dispose();
        _processingTask?.Dispose();
    }
}

Exemple 2 : Service de Monitoring avec Alertes

using System;
using System.Diagnostics;
using System.Threading;
using ZebraPuma.System.ServiceProcess;

public class SystemMonitorService : ServiceBaseExtended
{
    private Timer _timer;
    private PerformanceCounter _cpuCounter;
    private PerformanceCounter _ramCounter;

    public override string Name => "SystemMonitor";
    public override string Description => "Surveillance du CPU et de la RAM";

    protected override void OnStartCore(string[] args)
    {
        _cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
        _ramCounter = new PerformanceCounter("Memory", "Available MBytes");

        _timer = new Timer(CheckSystem, null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
        Logger.Info("Monitoring système démarré");
    }

    private void CheckSystem(object state)
    {
        var cpu = _cpuCounter.NextValue();
        var ram = _ramCounter.NextValue();

        Logger.Info("CPU: {Cpu:F2}%, RAM disponible: {Ram:F0} MB", cpu, ram);

        // Alertes
        if (cpu > 90)
        {
            Logger.Warn("⚠️ CPU élevé: {Cpu:F2}%", cpu);
            SendAlert($"CPU critique: {cpu:F2}%");
        }

        if (ram < 500)
        {
            Logger.Warn("⚠️ RAM faible: {Ram:F0} MB", ram);
            SendAlert($"RAM critique: {ram:F0} MB");
        }
    }

    private void SendAlert(string message)
    {
        // Envoyer email, SMS, etc.
        Logger.Error("ALERTE: {Message}", message);
    }

    protected override void OnStopCore()
    {
        _timer?.Dispose();
        _cpuCounter?.Dispose();
        _ramCounter?.Dispose();
        Logger.Info("Monitoring système arrêté");
    }
}

API Référence

Namespace: ZebraPuma.System.ServiceProcess

Classes

Classe Description
ServiceBaseExtended Classe de base pour services Windows
ServiceManager Gestionnaire multi-services
ServiceFactory Factory pour créer des services
ServiceControllerForm Interface WinForms de contrôle
ServiceDefinition Définition de configuration de service

Interfaces

Interface Description
IServiceExtended Contrat pour services Windows étendus

Events

Event Args Description
ServiceStateChanged ServiceStateChangedEventArgs Service démarré/arrêté
ServicesReloaded EventArgs Services rechargés

Déploiement Windows

Installation d'un Service

# Méthode 1 : sc.exe (recommandé)
sc create "MonService" `
    binPath="C:\MonApp\MonService.exe" `
    DisplayName="Mon Service ZebraPuma" `
    start=auto `
    obj="LocalSystem"

# Méthode 2 : PowerShell
New-Service -Name "MonService" `
    -BinaryPathName "C:\MonApp\MonService.exe" `
    -DisplayName "Mon Service ZebraPuma" `
    -StartupType Automatic

# Méthode 3 : InstallUtil (legacy)
InstallUtil.exe C:\MonApp\MonService.exe

Configuration du Service

# Changer le compte de démarrage
sc config MonService obj="DOMAIN\ServiceAccount" password="P@ssw0rd"

# Définir la description
sc description MonService "Service de traitement ZebraPuma"

# Configurer la récupération sur échec
sc failure MonService reset=86400 actions=restart/5000/restart/10000/restart/30000

Contrôle du Service

# Démarrer
Start-Service MonService
# ou
sc start MonService

# Arrêter
Stop-Service MonService
# ou
sc stop MonService

# Redémarrer
Restart-Service MonService

# Vérifier l'état
Get-Service MonService
# ou
sc query MonService

Désinstallation

# Arrêter d'abord
Stop-Service MonService

# Supprimer
sc delete MonService
# ou
Remove-Service MonService

Bonnes Pratiques

1. Thread Safety

Bon - Utiliser des CancellationToken

private CancellationTokenSource _cts;

protected override void OnStartCore(string[] args)
{
    _cts = new CancellationTokenSource();
    Task.Run(() => DoWork(_cts.Token), _cts.Token);
}

protected override void OnStopCore()
{
    _cts?.Cancel();
}

2. Timeout sur OnStopCore

Bon - Toujours implémenter un timeout

protected override void OnStopCore()
{
    _cts?.Cancel();
    
    if (!_task.Wait(TimeSpan.FromSeconds(30)))
    {
        Logger.Warn("Timeout lors de l'arrêt");
    }
}

3. Logging Structuré

Bon

Logger.Info("Service {Name} démarré avec {Count} workers", Name, workerCount);
Logger.Error(ex, "Erreur lors du traitement de {Item}", item);

4. Gestion d'Erreurs

Bon

protected override void OnStartCore(string[] args)
{
    try
    {
        // Initialisation critique
        InitializeDatabase();
    }
    catch (Exception ex)
    {
        Logger.Fatal(ex, "Échec d'initialisation critique");
        throw; // Laisser remonter pour que IsRunning reste false
    }
}

5. Ressources

Bon

protected override void DisposeManagedResources()
{
    _timer?.Dispose();
    _connection?.Close();
    _connection?.Dispose();
}

⬅️ Retour à la documentation principale