In der jungen Geschichte dieses Blogs haben wir schon jetzt eine Premiere: den ersten Wunschartikel. Ich wurde darum gebeten zu schildern wie man Windows Services programmieren und vor allem wie man mit diesen aus einer Applikation heraus kommunizieren kann. Als Beispiel dafür werden wir im ersten Teil einen Dienst schreiben der nichts anderes macht als uns die aktuelle Zeit zu ermitteln. Im zweiten Teil geht es dann um die Kommunikation mit einer Applikation und den dort notwendigen Datenaustausch. Der dritte Teil beschäftigt sich dann mit den gleichen Themen aber unter dem Gesichtspunkt der WCF (Windows Communication Foundation).

Was sind Windows Services und wie erstellt man sie

Bevor wir loslegen eine kurze Erläuterung was Dienste/Services eigentlich sind. Ein Dienst ist ein spezielles Programm ohne Benutzerschnittstelle welches dauerhaft im Hintergrund des Betriebssystems läuft.

Dies bedeutet:

  • Der Service wird gestartet noch bevor der Nutzer sich angemeldet hat
  • läuft demnach auch ohne angemeldeten Nutzer
  • kann nur mit bestimmten Rechten installiert oder beendet werden
  • kann keine direkten Benutzereingaben verarbeiten
  • und stellt besondere Anforderungen an die Fehlerbehandlung, sowie das Debugging.

Typische Windows Services sind: Spooler, zur Verwaltung von Druckaufträgen und PlugPlay zur Bereitstellung der Plug and Play Funktion von Hardware. Eine ganze Liste dieser Services findet man, wie so oft, bei Wikipedia.

Der erste eigene Service

Besitzt man eine Visual Studio Version > Standard ist das Erstellen des entsprechenden Projekts sehr einfach. Denn dort gibt es den Projekttyp „Service“ der uns bereits alle notwendigen Referenzen inkludiert und eine Klasse mit den benötigten Funktionen anlegt.

Hat man eine Standard oder gar Express Version, erstellt man sich statt dessen eine Konsolenanwendung. Dieser fügt man eine Referenz auf System sowie System.ServiceProcess hinzu und erstellt zwei Klassen, Program und MyTimeService. Letztere leitet von der Klasse ServiceBase ab die sich im Namespace System.ServiceProcess befindet. Wie man sich denken kann, dient Program nur dem Programmstart und der Registrierung unseres Dienstes weshalb sie statisch zu kennzeichnen ist und eine statische Main Methode benötigt. Der Code sollte deshalb zunächst in etwa so aussehen:

using System.ServiceProcess;
namespace MyVeryOwnTimeService
{
    static class Program
    {
        static void Main()
        {
            ServiceBase.Run(new MyTimeService());
        }
    }
    public class MyTimeService : ServiceBase
    {
        // inhalt folgt
    }
}

Hat man das Template von Visual Studio genutzt, sieht Program etwas anders aus, was jedoch in der Funktionalität keinen Unterschied macht. Letztendlich ist wichtig, dass eine Instanz unseres Services an die Run Methode der ServiceBase übergeben wird.

Hat man seine Service Klasse erstellt, bietet das Studio eine Designeransicht an. In diese kann man verschiedene Ressourcen aus der Toolbox ziehen und im eigentlichen Quelltext verwenden. Ja VS erstellt sogar automatische eine InitializeComponent Methode in der alle Komponenten erstellt werden. Letzendlich empfinde ich dieses Vorgehen aber als überflüssig. Wozu brauche ich zum Beispiel ComboBoxen wenn mein Programm keine GUI hat? Möchte ich Daten ins EventLog schreiben kann ich dies auch ohne Designer machen… Ich bin Purist und empfehle daher an dieser Stelle lieber händisch zu arbeiten, so erspart man sich auch die Inkludierung von, an dieser Stelle, sinnlosen Assemblies wie System.Windows.Forms.

Nun tu doch schon endlich was

Kompiliert man den oben gezeigten Code und installiert den Service (kommen wir später zu). zeigt sich, dass er ohne Anstand läuft. Dies liegt daran, dass ServiceBase keine abstrakte Klasse ist und daher auch nicht vorschreibt welche Methoden zu überschreiben sind. Dies ist in meinen Augen etwas unglücklich gewählt, soll uns hier aber nicht stören.

Folgende Methoden können überschrieben werden:

  • OnStart – Service wird neu gestartet
  • OnPause – Service soll sich in den Pausemodus begeben
  • OnContinue – Service wird aus dem Pausemodus geweckt
  • OnStop – Service wird komplett angehalten
  • OnShutdown – Die Maschine wird herunter gefahren
  • OnPowerEvent - Wird aufgerufen wenn zum Beispiel ein Laptop von Akku auf Netzstrom umschaltet
  • OnCustomEvent – Ermöglicht die Behandlung eigener Ereignisse die über die Start, Stop usw. hinaus gehen

Es wird demnach offensichtlich wie das System funktioniert. Wenn ein Service gestartet wird, ruft BaseService dessen OnStart Methode auf damit er seine Arbeit aufnimmt. Soll er angehalten werden wird OnStop aufgerufen, möchte man ihn pausieren OnPause und soll er dann wieder aufgeweckt werden kommt OnContinue ins Spiel. Ob diese Methoden tatsächlich aufgerufen werden hängt bei allen, bis auf OnStart, von ihren Can Properties ab. Sind diese auf true gesetzt sagen wird, dass wir ein entsprechendes Ereignis auch wirklich handhaben können.

Wichtig ist hier, dass diese Methoden uns im Grunde nur Signale geben welches Verhalten vom Betriebsystem verlangt wird. Sie kapseln nicht die eigentliche Funktionalität und müssen daher möglichst schnell wieder enden. Damit unser Service die aktuelle Zeit ermittelt verwenden wir einen Timer. Dieser soll einmal pro Sekunde die Property TimeValue vom Typ DateTime beschreiben, tritt ein Stop Event auf wird der Timer angehalten.

Eine Mögliche Implementierung unseres Dienstes kann daher wie folgt aussehe:

    public class MyTimeService : ServiceBase
    {
        private Timer _timer;
        public MyTimeService()
        {
            CanStop = true;
        }
        private DateTime TimeValue
        {
            get;
            set;
        }
        protected override void Dispose(bool disposing)
        {
            if (_timer != null && disposing)
                _timer.Dispose();
            base.Dispose(disposing);
        }
        protected override void OnStart(string[] args)
        {
                if (_timer == null)
                {
                        _timer = new Timer(1000);
                        _timer.AutoReset = true;
                        _timer.Elapsed += OnTimerElapsed;
                }
                 _timer.Start();
         }
         protected override void OnStop()
         {
                 _timer.Stop();
                 _timer.Dispose();
                 _timer = null;
         }
         private void OnTimerElapsed(object sender, ElapsedEventArgs e)
         {
                 TimeValue = DateTime.Now;
         }
 }

Da der Timer indirekt eine Systemressource darstellt muss nach der Verwendung unbedingt mit Dispose wieder frei gegeben werden, welche von ServiceBase geerbt wird und überschrieben werden kann. Ganz wichtig ist an dieser Stelle noch einmal: OnStart und OnStop dienen nur dazu den Timer zu steuern, sie enthalten nicht die eigtlich umzusetzende Logik!

Installation

Das sieht doch schon mal ganz gut aus. Wie bekommen wir den Service aber jetzt zum Laufen? Zunächst müssen wir ihn installieren und damit wir ihn installieren können müssen wir einen Installer erstellen. Dies ist nichts anderes als eine Klasse die von Installer ableitet und deren Property Installers mit den notwendigen Objekten bestückt. Notwendige Objekte sind hier ServiceInstaller (einen für jeden Dienst den man installieren will) und ServiceProcessInstaller (einmalig).

    [RunInstaller(true)]
    public class MyInstaller : Installer
    {
        public MyInstaller()
        {
            var serviceInstaller = new ServiceInstaller();
            serviceInstaller.StartType = ServiceStartMode.Manual;
            serviceInstaller.ServiceName = "My time service";
            serviceInstaller.Description = "A service which measures current time";;
            Installers.Add(serviceInstaller);
            var processInstaller  = new ServiceProcessInstaller();
            processInstaller.Account = ServiceAccount.LocalSystem;
            Installers.Add(processInstaller);
        }
    }

Alle Installer bieten grundsätzliche Funktionalitäten um die notwendigen Registrierungen innerhalb des Betriebssystems auszuführen. Die ServiceInstaller sind dahingehend spezialisiert, dass mit ihnen bestimmt wird wann ein Service gestartet wird (automatisch bei Systemstart, manuell über ein Programm oder gar nicht) und unter welchem (einzigartigen) Namen er hinterlegt wird. Wie man sieht bietet letzteres wunderbares Konfliktpotential…

Neben dem ServiceInstaller bedarf es noch eines ServiceProcessInstaller. Dieser übernimmt die Definition von übergreifenden Einstellungen die für alle zu installierenden Dienste gelten und wird in unserem Beispiel verwendet um anzugeben, dass die Dienste im speziellen System account (LocalSystem)mit vollen Rechten laufen soll. Darüber hinaus können Dienste als Netzwerkdienst (NetworkService), unter dem aktuellen (LocalService) oder unter einem bestimmten Nutzerkonto (User) installiert werden.

Bei letzterem müssen zusätzlich der Benutzername und das entsprechende Kennwort in weiteren Properties von ServiceProcessInstaller hinterlegt werden. Nicht zu vergessen, dass die Installation selbst nur mit Administratorrechten und aus einer vertrauenswürdigen Quelle heraus geschehen kann. Damit der Installer beim Assemblyaufruf auch als solcher zu erkennen ist, bekommt er von uns noch ein [RunInstaller(true)] Attribut verpasst. Ist dies nicht vorhanden gibt es auch keine Installation!

Installation (jetzt aber wirklich)

Dank unseres Installers haben wir nun die Möglichkeit den Dienst zu registrieren. Ich beschreibe hier nur die manuelle Fassung mit Installutil.exe. Es ist aber auch möglich sich ein entsprechendes Setupprojekt fürs Deploying zu basteln.

Wenn ihr als Administrator angemeldet seid müsst ihr die Console aufrufen und in das .NET Frameworkverzeichnis wechseln, welches bei mir zum Beispiel unter

C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319

liegt. Dort angekommen gebt ihr einfach

InstallUtil „Pfad zur Assembly“

ein. Sollte man User als ServiceAccount angegeben haben ohne Usernamen und Passwort, poppt ein neues Fenster auf. In diesem werden der Name und das Passwort für den neuen Account angegeben. Hat alles geklappt liegen im Verzeichnis zwei neue Logdateien. Zum Deinstallieren nutz man übrigens die gleiche Zeile mit /u als ersten Parameter und danach die zu deinstallierenden Assemblies.

Vorsicht: Zum Deinstallieren braucht man die dll des Services der installiert wurde. Wenn man sie vorzeitig löschthat man einen Service den man nicht mehr los wird!

Laufzeitsteuerung

Da wir unseren Dienst auf Manuel gestellt haben, müssen wir ihn nun auch manuell starten. Dazu geht man in der Computerverwaltung unter Dienste und klickt mit rechter Maustaste auf eben jenen Dienst. Hätten wir ihn auf Automatic gestellt würde er bei einem Systemneustart automatisch anlaufen.

Will man den Service aus einer Applikation heraus steuern kann man sich der ServiceController Klasse bedienen:

ServiceController controller = new ServiceController();
controller.ServiceName = "My time service";
controller.Start();
controller.WaitForStatus(ServiceControllerStatus.Running);
MessageBox.Show("Service is now running");

Ein Wort zum Debugging

Das Visual Studio bietet einige Helferlein wenn es um die Verwaltung von Diensten geht. Das Debugging gestalltet sich aber dennoch oft schwierig, da man nicht wie sonst einfach F5 drücken und auf Erkenntnis hoffen kann. Statt dessen muss man den Service starten und sich dann über Debug -> Attach to Process, zu dem Dienst vorhangeln. Dafür muss er aber bereits laufen, sprich es dürfen keine Fehler bei der Installation und dem Start auftreten.

Ich habe mir deshalb mittlerweile angewöhnt eine einfache Konsolenapplikation um den Dienst herum zu stricken, diese dann zu debuggen und ihn zunächst im lokal Benutzerkonto zu installieren wenn ich glaube das er funktionstüchtig ist. Alle Fehler die danach auftreten sind aller Wahrscheinlichkeit nach auf fehlende Rechte oder Unkooperativität seitens des Betriebssystems zurück zu führen. So oder so: „Service debugging is a pain in the ass…“

Vorläufiges Fazit

Mit den bisher beschriebenen Dingen kann man ohne weiteres einen Service auf der lokalen Maschine erstellen, installieren und steuern. Dadurch wäre es zum Beispiel möglich Systemressourcen zu überwachen. Wirklich interessant wird die Sache jedoch. Wenn man einen Service nutzen könnte um einen Server zu realisieren. Dafür ist es jedoch notwendig Daten zu übertragen und genau dies sind die Themen der folgenden Teile.

Quellcode eines eigenen Windowsdienstes (1726)