Erweiterte Programmierthemen: Dienste

Dienste ermöglichen es, die Anwendung, welche zumeist unsichtbar ist, als Hintergrundprozess arbeiten zu lassen. Sie sind praktisch nicht da und übernehmen trotzdem eine wichtige, teilweise sogar notwendige Funktionalität. Auch mit C# kann ein solcher Dienst (in Englisch auch Service genannt) programmiert werden. Für was könnte so etwas nützlich sein? Grundsätzlich werden für die meisten Anwendungs-Zwecke keine Dienste benötigt. Einige Hobby-Programmierer werden einen Dienst möglicherweise nie programmieren. Trotzdem lohnt es sich zu wissen, wie man einen Dienst programmiert. Nützlich ist ein Dienst immer dann, wenn etwas überwacht werden soll (z. B. Protokollierung von freiem Festplattenspeicher, Erreichbarkeit von Netzwerkgeräten usw.). Bei der Erstellung des Projekts müssen wir „Windows-Dienst“ (in der Gruppe „Windows-Desktop“) wählen. Hierdurch erhalten wir bereits ein vorgefertigtes Gerüst als Grundaufbau.
Die Datei Program.cs enthält (wie bereits bekannt) die Main()-Funktion, jedoch ohne Übergabeparameter. In der Funktion werden mehrere Dienste, welche von der Klasse ServiceBase abstammen, in einem Array deklariert und der statischen Funktion Run() der ServiceBase-Klasse übergeben. Hierdurch wäre es möglich, die Dienst-Anwendung in mehrere Dienste zu unterteilen. Zumeist findet sich hier jedoch nur ein Eintrag.
Jeder Dienst verfügt, ähnlich wie bei Windows Forms, über die Datei Service1.cs und Service1.Designer.cs. Natürlich kann auch hier der Name geändert werden (rechte Maustaste und Umbenennen). Die Service1.Desinger.cs-Datei enthält so gut wie keinen Programmcode und ist für uns eher uninteressant. In der Service1.Designer.cs-Datei gibt es nun die überschriebenen Methoden (Schlüsselwort override) OnStart() und OnStop(). OnStart() wird beim Starten des Dienstes aufgerufen und bekommt als Übergabe ein Array mit Parametern übergeben. OnStop() wird beim Stoppen des Dienstes ausgeführt. Übliche Praxis ist es, eine eigene Klasse zu erstellen, welche ebenfalls über eine Start- und Stopp-Funktion verfügen und diese über die OnStart() und OnStop() aufzurufen. Die im Dienst ausgeführten Statements sollten immer in einem eigenen Thread ausgeführt werden und dürfen niemals als synchroner (auch blockierend genannt) Code in der OnStart()-Funktion notiert werden.
Im Beispiel haben wir einen Dienst programmiert, welcher minütlich die CPU- und RAM-Last protokolliert. Das ganze wird über eine while-Schleife und eine Abfrage gelöst. Die while-Schleife prüft ein globales Bit (bool-Variable), welches über die Stopp-Funktion auf false gesetzt wird, um somit den Dienst anzuhalten. Das Protokoll wird in dem Verzeichnis der .exe-Datei (dies kann über AppDomain.CurrentDomain.BaseDirectory abgerufen werden) in der Datei log.txt gespeichert. In der while-Schleife notieren wir einen Aufruf der Sleep()-Funktion der Thread-Klasse. Dies ist zu empfehlen, da andernfalls die CPU-Last des Computers stark zunehmen würde, weil der Computer immer damit beschäftigt ist, die Zeitprüfung durchzuführen.
Die Installation eines Dienstes erfolgt mit dem Tool InstallUtil.exe, welches im Ordner des .NET-Frameworks enthalten ist (zumeist C:WindowsMicrosoft.NETFrameworkv4.0.30319). Bei 64bit-Systemen gibt es zudem den Ordner Framework64. Die Versionsnummer kann sich ebenfalls unterscheiden. Als Parameter müssen wir den Pfad zur .exe-Datei des Dienstes übergeben. Soll der Dienst deinstalliert werden, muss der Parameter /u vor dem Pfad angegeben werden. Der Aufruf erfolgt über die Windows-Eingabeaufforderung oder auch über die Developer-Eingabeaufforderung für Visual Studio.
Falls Sie sich fragen, woher der Dienst nun seine Einstellungen bekommt (z. B. Name oder Beschreibung), dann sollten Sie sich die Datei DienstInstallation.cs anschauen. Diese Datei wurde von uns hinzugefügt und empfiehlt sich, in jedem Dienst einzubauen, denn damit ist es möglich, dass sich der komplette Dienst mit allen Einstellungen über das InstallUtil-Tool installieren lässt. Die Klasse stammt von der Installer-Klasse (Namensraum System.Configuration.Install) ab. Im Konstruktor erfolgt nun die eigentliche Installation, bei welcher der Starttyp (Enumeration ServiceStartMode), der Name (ohne Sonderzeichen), der Anzeige-Name und die Beschreibung einem Objekt der ServiceInstaller-Klasse übergeben werden. Zusätzlich ist noch die Klasse ServiceProcessInstaller notwendig, um den Anmelde-Typ festzulegen (Enumeration ServiceAccount). Beide Objekte müssen der statischen Funktion Add() der Installers-Klasse übergeben werden.
Bitte bedenken Sie, dass die Vorlage für eine Dienst-Anwendung bei den Express-Editionen von Visual Studio 2013 und vorherigen Versionen fehlen. Ein Dienst kann jedoch auch mit Hilfe der Vorlage „leeres Projekt“ und dem Beispielcode dieses Projekts erstellt werden.

Program.cs

using System.ServiceProcess;

namespace CSV20.Dienste
{
    static class Program
    {
        /// <summary>
        /// Der Haupteinstiegspunkt für die Anwendung.
        /// </summary>
        static void Main()
        {
            ServiceBase[] ServicesToRun;
            ServicesToRun = new ServiceBase[] 
            { 
                new Service1() 
            };
            ServiceBase.Run(ServicesToRun);
        }
    }
}

Service1.cs
using System.ServiceProcess;
using System.Threading;

namespace CSV20.Dienste
{
    public partial class Service1 : ServiceBase
    {
        private PerformanceLogger oLogger = new PerformanceLogger();

        public Service1()
        {
            InitializeComponent();
        }

        protected override void OnStart(string[] args)
        {
            Thread oThread = new Thread(new ThreadStart(oLogger.StartLogging));
            oThread.Start();
        }

        protected override void OnStop()
        {
            oLogger.StoppeLogging();
        }
    }
}

Service1.Designer.cs
namespace CSV20.Dienste
{
    partial class Service1
    {
        /// <summary> 
        /// Erforderliche Designervariable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// Verwendete Ressourcen bereinigen.
        /// </summary>
        /// <param name="disposing">True, wenn verwaltete Ressourcen gelöscht werden sollen; andernfalls False.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Vom Komponenten-Designer generierter Code

        /// <summary> 
        /// Erforderliche Methode für die Designerunterstützung. 
        /// Der Inhalt der Methode darf nicht mit dem Code-Editor geändert werden.
        /// </summary>
        private void InitializeComponent()
        {
            components = new System.ComponentModel.Container();
            this.ServiceName = "Service1";
        }

        #endregion
    }
}

PerformanceLogger.cs
using System;
using System.Diagnostics;
using System.IO;
using System.ServiceProcess;
using System.Threading;

namespace CSV20.Dienste
{
    public class PerformanceLogger
    {
        private bool bLoggingRunning;
        
        public void StartLogging()
        {
            DateTime oLetzterZeitpunkt;
            PerformanceCounter oPerformanceCpu;
            PerformanceCounter oPerformanceRam;
            DateTime oAktuellerZeitpunkt;
            string sAusgabe;

            // globale Variable setzen
            bLoggingRunning = true;
            
            // interne Variablen setzen
            oLetzterZeitpunkt = DateTime.Now - TimeSpan.FromMinutes(1); // dadurch wird die 1. Protokollierung sofot ausgefürht
            oPerformanceCpu = new PerformanceCounter("Processor",    "% Processor Time",         "_Total");
            oPerformanceRam = new PerformanceCounter("Memory",       "% Committed Bytes In Use");

            // Task ausführen (solange bis StoppeLogging() aufgerufen wird)
            while (bLoggingRunning)
            {
                // aktuelle Zeit ermitteln
                oAktuellerZeitpunkt = DateTime.Now;
                // prüfen ob 1 Minuten seit letztem Aufruf vergangen sind
                if (oAktuellerZeitpunkt - oLetzterZeitpunkt > TimeSpan.FromMinutes(1))
                {
                    try
                    {
                        sAusgabe = oAktuellerZeitpunkt.ToString() + ": ";
                        sAusgabe += "CPU " + oPerformanceCpu.NextValue().ToString("F2") + "% ";
                        sAusgabe += " RAM " + oPerformanceRam.NextValue().ToString("F2") + "% ";
                        sAusgabe += Environment.NewLine;    // enthält standardmäßig \r\n
                        File.AppendAllText(AppDomain.CurrentDomain.BaseDirectory + "\\log.txt", sAusgabe);
                    }
                    catch (Exception ex)
                    {
                        // Fehlerbehandlung
                    }
                    // neuen "letzten" Zeitpunkt merken
                    oLetzterZeitpunkt = oAktuellerZeitpunkt;
                }
                // 1 Sekunde warten
                Thread.Sleep(1000);
            }
        }

        public void StoppeLogging()
        {
            bLoggingRunning = false;
        }
    }
}

DienstInstallation.cs
using System.ComponentModel;
using System.Configuration.Install;
using System.ServiceProcess;

namespace CSV20.Dienste
{
    [RunInstaller(true)]
    public class DienstInstallation : Installer
    {
        public DienstInstallation()
        {
            ServiceInstaller oInstaller = new ServiceInstaller();
            ServiceProcessInstaller oProcessInstaller = new ServiceProcessInstaller();

            oInstaller.StartType = ServiceStartMode.Manual; // für automatischen Start: ServiceStartMode.Automatic
            oInstaller.DisplayName = "CPU- und RAM-Protokollierung";
            oInstaller.ServiceName = "PerformanceLogger";
            oInstaller.Description = "Protokolliert die CPU- und RAM-Nutzung";

            oProcessInstaller.Account = ServiceAccount.LocalSystem;

            Installers.Add(oInstaller);
            Installers.Add(oProcessInstaller);
        }
    }
}
Download

LinksRechts