18.3 Windows-Dienstanwendungen entwickeln 

Windows-Dienste sind Anwendungen, die im Hintergrund ohne Benutzeroberfläche starten, oft automatisch, wenn der Computer gebootet wird. Sie werden vom Anwender nicht wahrgenommen und führen kontinuierlich Aufgaben aus, zum Beispiel die zur Überwachung.
Die Verwaltung aller Dienste wird von einer Komponente namens Service Control Manager (SCM) übernommen. Die Dienste eines Rechners werden in der Microsoft Management Console (MMC) angezeigt, die Sie sich über Systemsteuerung • Verwaltung • Dienste anzeigen lassen können. Eine weitere Möglichkeit bietet das Visual Studio 2008 mit dem in Abbildung 18.1 gezeigten Server-Explorer, der standardmäßig am linken Rand der IDE angeordnet ist und nicht nur bei der Entwicklung von Windows-Diensten eine Erleichterung darstellt, weil nicht immer zwischen verschiedenen Fenstern gewechselt werden muss.
Abbildung 18.1 Der »Server-Explorer« von Visual Studio 2008
Ein installierter Dienst trägt sich in die Registrierungsdatenbank ein. Wird das System hochgefahren, werden die installierten Dienste geladen und stehen danach dem Service Control Manager zur Verfügung. Windows-Dienste haben ein besonderes Merkmal: Einen Dienst zu laden bedeutet nicht, dass er gleichzeitig auch gestartet wird und seine Aufgabe ausführt.
Das Startverhalten ist eine Eigenschaft, die jedem Dienst eigen ist und drei Konfigurationswerte annehmen kann:
- manuell
- automatisch
- deaktiviert
Ein automatisch gestarteter Dienst ist von Anfang an aktiv. Ein manuell zu startender Dienst kann entweder über seine Eigenschaften im Dienste-Dialog oder programmiertechnisch aus einer anderen Anwendung heraus gestartet werden. Deaktivierte Dienste lassen sich weder vom Benutzer noch von einem anderen Programm starten. Ein gestarteter Dienst kann während seiner Ausführungszeit angehalten oder beendet werden.
Ein Dienst, der beendet wird, wird gleichzeitig auch entladen. Demgegenüber verbleibt ein angehaltener Dienst im Speicher. Sowohl beendete als auch angehaltene Dienste können erneut gestartet werden. Allerdings muss ein beendeter Dienst dazu neu geladen werden.
Abbildung 18.2 Das Eigenschaftsfenster des Nachrichtendienstes
In Abbildung 18.2 ist der Eigenschaftsdialog des Nachrichtendienstes zu sehen, dessen standardmäßig automatisches Starten auf manuelles Starten umgestellt worden ist. Um dieses Dialogfenster zu öffnen, markieren Sie im Dienste-Dialog den Dienst, öffnen das Kontextmenü und wählen Eigenschaften. Der Eigenschaftsdialog dient auch administrativen Aufgaben, denn Sie können hier nicht nur das Startverhalten des Dienstes umstellen, sondern einen Dienst auch starten, beenden, anhalten oder fortsetzen. Dienste, die in der Lage sind, Startparameter entgegenzunehmen, zeigen ein aktiviertes Eingabefeld am unteren Rand der Registerkarte Allgemein. Ausschließlich zu lesen ist die Angabe der Datei, die den Dienst bereitstellt, hier services.exe.
18.3.1 »Windows-Dienst«-Projekte in der Entwicklungsumgebung 

Nach dieser Einführung in die Welt der Windows-Dienste wollen wir nun sehen, wie wir einen Dienst mit dem Visual Studio programmieren. Dazu wählen Sie zunächst die Projektvorlage Windows-Dienst. Als Projektbezeichner wird der Name FileWatchService vergeben.
Wir sollten zunächst einen Blick auf das Grundgerüst des automatisch erzeugten Programmcodes werfen, der sich von dem einer Konsolen- oder Windows-Anwendung in wesentlichen Punkten unterscheidet. Das Listing enthält weder Attribute noch Kommentare, die automatisch generiert wurden.
'...\Programmiertechniken\FileWatchService\Service1.Designer.vb |
Imports System.ServiceProcess
Partial Class Service1 : Inherits System.ServiceProcess.ServiceBase
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
' components.Dispose()
End Sub
Shared Sub Main()
Dim ServicesToRun() As System.ServiceProcess.ServiceBase
ServicesToRun = New System.ServiceProcess.ServiceBase() _
{New Service1()}
System.ServiceProcess.ServiceBase.Run(ServicesToRun)
End Sub
Private components As System.ComponentModel.IContainer
Private Sub InitializeComponent()
components = New System.ComponentModel.Container()
Me.ServiceName = "Service1"
End Sub
End Class
'...\Programmiertechniken\FileWatchService\Service1.vb |
Public Class Service1
Protected Overrides Sub OnStart(ByVal args() As String)
End Sub
Protected Overrides Sub OnStop()
End Sub
End Class
Ohne Umbenennung ist der Klassenname Service1. Die Klasse ist von ServiceBase abgeleitet, die die Basisklasse der Windows-Dienste ist und zum Namensraum System.ServiceProcess gehört. Wie jedes andere Programm wird auch ein Dienst durch den Aufruf der Methode Main gestartet. Darin wird zuerst ein Array vom Typ ServiceBase deklariert und diesem die Referenz auf das Dienst-Objekt vom Typ Service1 übergeben. Damit ist es auch möglich, innerhalb einer Windows-Dienst-Anwendung mehrere Dienste gleichzeitig bereitzustellen, die alle von ServiceBase abgeleitet sind. Die Initialisierung des Arrays muss dann nur durch die entsprechenden Objektreferenzen ergänzt werden. Im letzten Schritt wird die statische Methode Run der Klasse ServiceBase aufgerufen, und alle Dienste werden geladen. Ist die Methode ausgeführt, übernimmt der Service Control Manager die Steuerung der Dienste.
Zwei geschützte Methoden der Basisklasse werden bereits überschrieben bereitgestellt: OnStart und OnStop. Sie werden ausgeführt, wenn der Dienst gestartet beziehungsweise beendet wird. OnStart enthält damit die Funktionalität des Dienstes, in OnStop implementieren Sie den Code, der bei Dienstende ausgeführt werden soll.
Bei OnStart und OnStop handelt es sich genau genommen um Ereignisse, die aber von der Basisklasse nicht direkt veröffentlicht werden.
Hinweis |
Wenn OnStart länger als 30 Sekunden braucht, bricht der Service Control Manager (SCM) den Start ab und schreibt eine Fehlermeldung in das Ereignisprotokoll. Alles, was länger dauert, muss in einem eigenen Thread laufen, der von OnStart gestartet wird. Für sich wiederholende Operationen können Sie das Timer-Steuerelement verwenden. |
Angestoßen wird OnStart durch den Service Control Manager. Beim Booten des Systems ist das der Fall, wenn im Eigenschaftsdialog des Dienstes unter Starttyp Automatisch eingestellt ist (siehe Abbildung 18.2, »Das Eigenschaftsfenster des Nachrichtendienstes«). Mit Manuell wird der Dienst beim Booten zwar in den Speicher geladen, wartet aber auf ein äußeres Signal zum Start. Das kann durch Klicken auf die Schaltfläche Starten im Dialog erfolgen oder durch Programmcode. Analog verhält sich auch die Methode OnStop. Sie erhält einen Anstoß von außen, wenn der Dienst beendet wird – entweder durch Klicken auf die Schaltfläche Beenden oder mittels Programmcode.
Wird ein Dienst gestartet, können ihm Startparameter übergeben werden, die im Dienste-Dialog im untersten Eingabefeld eingetragen werden (siehe Abbildung 18.2, »Das Eigenschaftsfenster des Nachrichtendienstes«). An die OnStart-Methode werden diese Parameter als String-Array übergeben, und sie dienen zur Kontrolle des Laufzeitverhaltens des Dienstes.
Protected Overridable Sub OnStart(ByVal args As String()) |
Die Methode OnStop hingegen ist parameterlos, ebenso wie alle anderen OnXxx-Methoden, die im folgenden Abschnitt beschrieben werden.
18.3.2 Methoden eines Dienstes 

Beim Beenden eines Dienstes wird automatisch die Methode OnStop aufgerufen. Wird der Dienst nur angehalten, ist es OnPause. Ein unterbrochener Dienst kann natürlich auch wieder gestartet werden. Dann wird automatisch die Methode OnContinue aufgerufen. Bei Beendigung des Systems kommen zwei weitere Methoden ins Spiel. Während das System herunterfährt, wird OnShutDown aufgerufen, während OnPowerEvent bei Änderung der Stromversorgung (StandBy, Aufwachen, Netzkabel ein-/ausgesteckt) dran ist. Tabelle 18.6 fasst die Methoden zusammen.
Methode | Ausführung |
OnContinue |
Wenn der Dienst nach dem Anhalten wieder fortgesetzt wird |
OnPause |
Wenn der Dienst angehalten wird |
OnPowerEvent |
Wenn sich die Stromversorgung ändert (Standby oder Netzkabel) |
OnStart |
Wenn der Dienst gestartet wird |
OnStop |
Wenn der Dienst beendet wird |
OnShutDowm |
Wenn das System heruntergefahren wird |
Damit steht Ihnen eine Reihe von Methoden zur Verfügung, um das Verhalten eines Dienstes in mannigfaltigen Situationen festzulegen. Welche Methoden überschrieben werden müssen, hängt von der Arbeitsweise des Dienstes ab. Mit Sicherheit werden Sie aber immer OnStart implementieren, damit der Dienst überhaupt etwas macht.
18.3.3 Eigenschaften eines Dienstes 

Jeder Dienst wird durch Signale des SCM gesteuert. Beim Starten ist es ein Signal, das die Ausführung von OnStart im Dienst nach sich zieht, beim Beenden ein Signal, infolge dessen OnStop aufgerufen wird. Analog gilt das auch für die anderen Methoden. Auf einem anderen Blatt steht jedoch, ob der Dienst überhaupt in der Lage ist, das Signal zu empfangen und an die passende Methode weiterzuleiten. Diese Festlegung erfolgt in den Eigenschaften der von ServiceBase abgeleiteten Klasse (siehe Tabelle 18.7). Um sich die Eigenschaften im Eigenschaftsfenster anzuzeigen zu lassen, müssen Sie die Dienstklassendatei in der Designeransicht öffnen.
Eigenschaft | Beschreibung |
AutoLog |
Gibt an, ob das Starten, Beenden, Anhalten und Fortsetzen im Ereignisprotokoll Anwendung aufgezeichnet wird. |
CanHandlePowerEvent |
Der Dienst reagiert auf Änderung der Stromversorgung. |
CanHandleSessionChangeEvent |
Der Dienst reagiert auf Wechsel einer Terminal-Session. |
CanPauseAndContinue |
Der Dienst kann angehalten und wieder fortgesetzt werden. |
CanShutDown |
Der Dienst wird beim Herunterfahren des Systems informiert. |
CanStop |
Gibt an, ob der Dienst nach dem Starten beendet werden kann. |
EventLog |
Zugriff auf Systemprotokolle |
ExitCode |
Beendigungscode des Dienstes |
ServiceName |
Bezeichnung des Dienstes |
Insgesamt vier Eigenschaften legen die Empfangsbereitschaft eines Dienstes fest:
- CanHandlePowerEvent
- CanPauseAndContinue
- CanShutDown
- CanStop
Bis auf CanStop sind alle genannten Eigenschaften mit False vorinitialisiert. Die Konsequenzen lassen sich am besten erkennen, wenn Sie sich den Eigenschaftsdialog eines Dienstes ansehen. In Abbildung 18.2, »Das Eigenschaftsfenster des Nachrichtendienstes«, sind die beiden Schaltflächen Anhalten und Fortsetzen nur dann aktiv, wenn der Dienst gestartet und die Eigenschaft CanPauseAndContinue=True festgelegt ist. Auch wenn das nur selten angewendet wird, ist es möglich, einen Dienst zu starten, ohne ihn jemals beenden zu können. Dazu muss CanStop=False gesetzt werden.
Belassen Sie den Standardwert True von AutoLog, schreibt ein Dienst seine Zustandsänderungen in das in Abbildung 18.3 gezeigte Anwendungsereignisprotokoll des Systems.
Abbildung 18.3 Anwendungsereignisprotokoll
Wie Abbildung 18.3 zeigt, wird im Protokoll in der Spalte Quelle der Bezeichner ausgegeben, der durch die Eigenschaft ServiceName der Dienstklasse angegeben ist. Ein Doppelklick auf einen Eintrag öffnet das Eigenschaftsfenster des Ereignisses, in dem über die allgemeinen Angaben zum Ereignis hinaus auch noch eine Beschreibung angezeigt wird. Soll sie nicht leer sein, müssen Sie im Programmcode mit der Methode WriteEntry der Eigenschaft EventLog selbst in das Protokoll schreiben. Erlaubte Ziele der Ausgabe werden durch EventLog bestimmt. Eine Erweiterung ist nicht möglich, EventLog ist schreibgeschützt. Das folgende Codefragment nutzt die einfachste Überladung der Methode.
Protected Overrides Sub OnStart(args As String())
Me.EventLog.WriteEntry("DiskWatcher gestartet")
' weitere Anweisungen
End Sub
Die Ausgabe des Beispiels sehen Sie in Abbildung 18.4. Ein lauffähiges Beispiel, das Write-Entry benutzt, folgt in Abschnitt 18.3.5, »Das Beispielprogramm FileWatchService«.
Abbildung 18.4 Beschreibungstext im Ereignisprotokoll
Hinweis |
EventLog kann auch in andere Systemprotokolldateien schreiben. In der Toolbox wird Ihnen zur Erleichterung Ihrer Arbeit für diese Klasse auch ein Control angeboten. |
18.3.4 Installation eines Windows-Dienstes 

Damit haben wir die Beschreibung der Definition eines Windows-Dienstes abgeschlossen, und es bleibt nur noch, den Dienst zu installieren. Damit der SCM den Dienst auch finden kann, muss er in die Registrierungsdatenbank eingetragen werden. Obwohl diese Aufgabe im ersten Moment schwierig erscheint, unterstützt das Visual Studio uns in dieser Hinsicht geradezu vorbildlich, denn dazu sind nur wenige Klicks und Einträge erforderlich.
Öffnen Sie die Dienstklasse in der Designeransicht, finden Sie unten im Eigenschaftsfenster den Link Installer hinzufügen. Klicken Sie auf den Link, wird eine neue Quellcodedatei hinzugefügt, die zwei Installationsobjekte bereitstellt: Das erste ist vom Typ ServiceProcessInstaller und übernimmt die Installation der Installationsdatei des Dienstes auf dem System. Das zweite ist vom Typ ServiceInstaller trägt die Dienstkomponente in die Registry unter
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services
ein, In beiden Objekten sind wenige Eigenschaften bis zur endgültigen Fertigstellung der installationsfähigen Windows-Dienstanwendung festzulegen. Im ServiceProcessInstaller-Objekt ist das die Eigenschaft Account, mit dem die Benutzerrechte des Dienstes bestimmt werden. An den meisten Fällen ist LocalSystem am besten geeignet.
Hinweis |
Weitere Informationen zu den drei weiteren Alternativen, um den Sicherheitskontext eines Dienstes festzulegen, entnehmen Sie bitte der Online-Dokumentation. |
Im ServiceInstaller-Objekt sollten wir noch drei Eigenschaften setzen. Zuerst wird mit StartType festgelegt, wie sich der Dienst nach dem Laden verhalten soll. Bekanntlich kann ein Dienst automatisch oder manuell gestartet werden. Mit der dritten Möglichkeit, der Deaktivierung, kann er weder vom Benutzer noch durch ein Programm gestartet werden.
Einen weniger gravierenden Einfluss haben die Eigenschaften DisplayName und ServiceName. Trotzdem sollten auch diese Werte sorgfältig vergeben werden. Der Text, den Sie unter DisplayName eintragen, wird im Dialog Dienste angezeigt, und der Wert von ServiceName wird als Schlüssel in der Registry benutzt.
Damit sind alle vorbereitenden Arbeiten erledigt, und der Installer ist konfiguriert. Das Projekt muss anschließend nur noch kompiliert werden. Das Ergebnis ist eine EXE-Datei, die Installationsdatei des Windows-Dienstes.
Jetzt kommt es zur Installation auf dem Zielsystem. Dazu bieten sich zwei Alternativen an:
- eine Installationsroutine mit einem Weitergabeprojekt
- das Tool InstallUtil.exe
An dieser Stelle wollen wir die zweite Möglichkeit nutzen. In Abschnitt 8.4, »Weitergabe mit MS-Installer«, haben wir uns mit Weitergabeprojekten beschäftigt. Hier wollen wir das Tool InstallUtil.exe benutzen, das im Verzeichnis
\Windows\Microsoft.NET\Framework\v<Versionsnummer>
zu finden ist. Zum Installieren rufen Sie das Tool an der Konsole auf und übergeben als Parameter die Installationsdatei des Windows-Dienstes, also beispielsweise:
InstallUtil FileWatchService.exe
Ähnlich einfach kann ein Dienst auch deinstalliert werden. Dazu wird der Aufruf des Tools um den Optionsschalter /u ergänzt:
InstallUtil /u FileWatchService.exe
Wenn die Installation erfolgreich abgelaufen ist, werden Sie den selbst geschriebenen Windows-Dienst nun im Dienste-Dialog zu sehen bekommen.
Hinweis |
Gegebenenfalls müssen Sie die Vertrauenswürdigkeit anpassen, siehe zum Beispiel Abschnitt 2.3.2, »Start und Test«. Eine Installation aus dem Netzwerk heraus kann scheitern. |
18.3.5 Das Beispielprogramm FileWatchService 

Nun wollen wir unsere Kenntnisse einsetzen und einen Windows-Dienst vollständig implementieren. Dieser Dienst, dessen Name FileWatchService lautet, soll in der Lage sein, alle Änderungen am Dateisystem in einer Datei zu protokollieren, beispielsweise das Löschen, Ändern und Hinzufügen einer Datei. Außerdem soll der Dienst auch angehalten und erneut gestartet werden können.
Weil der Dienst permanent auf Änderungsmitteilungen lauscht und die OnStart-Methode das Zeitlimit von 30 Sekunden nicht überschreiten darf, wird die gesamte Funktionalität in der separaten Klasse Worker implementiert und in einem eigenen Thread ausgeführt.
'...\Programmiertechniken\FileWatchService\Service1.vb |
Imports System.Threading
Public Class Service1
Private th As Thread
Private path As String = ""
Protected Overrides Sub OnStart(ByVal args() As String)
If args.Length <> 0 Then Me.path = args(0)
Dim w As New Worker(Me.path)
th = New Thread(New ThreadStart(AddressOf w.StartWorker))
th.Start()
Me.EventLog.WriteEntry("FileWatchService wurde erfolgreich gestartet")
End Sub
Protected Overrides Sub OnStop()
th.Abort()
th = Nothing
Me.EventLog.WriteEntry("FileWatchService wurde beendet")
End Sub
Protected Overrides Sub OnPause()
th.Suspend() ' der Einfachheit halber; Problem, wenn Thread aktiv
Me.EventLog.WriteEntry("FileWatchService wurde angehalten")
End Sub
Protected Overrides Sub OnContinue()
th.Resume()
Me.EventLog.WriteEntry("FileWatchService wurde fortgesetzt")
End Sub
End Class
In OnStart wird zuerst ein Objekt vom Typ Worker erzeugt. Dem Konstruktor der Klasse wird das zu protokollierende Verzeichnis übergeben. In diesem und allen untergeordneten Verzeichnissen sollen die Änderungen protokolliert werden. Im Eigenschaftsdialog des Dienstes kann der Anwender das Verzeichnis im Eingabefeld Startparameter selbst bestimmen, ansonsten werden sämtliche Änderungen im Laufwerk C:\ festgehalten. Nach der Initialisierung des Arbeitsthreads und dem Aufruf der Startmethode schreibt OnStart nur noch eine Mitteilung in das Anwendungsereignisprotokoll.
In den anderen Methoden wird nur noch das Verhalten des Threads gesteuert: In OnPause wird der Thread in den Wartezustand versetzt, in OnContinue wird er bereitgeschaltet. Das Stoppen des Dienstes durch Aufruf der Methode OnStop zerstört den Thread.
Sehen wir uns nun an, wie die Klasse Worker konstruiert ist:
'...\Programmiertechniken\FileWatchService\Worker.vb |
Public Class Worker
Private protocolFile As String = IO.Path.Combine( _
IO.Path.GetDirectoryName( _
Process.GetCurrentProcess().MainModule.FileName), "FileWatchLog.log")
Private path As String = "c:\\"
Public Sub New(ByVal path As String)
If Not String.IsNullOrEmpty(path) Then Me.path = path
End Sub
' Startmethode des Threads
Public Sub StartWorker()
Dim fsw As New IO.FileSystemWatcher(Me.path)
fsw.IncludeSubdirectories = True
fsw.EnableRaisingEvents = True
AddHandler fsw.Changed, AddressOf ChangeFile
AddHandler fsw.Deleted, AddressOf DeleteFile
AddHandler fsw.Created, AddressOf CreateFile
AddHandler fsw.Renamed, AddressOf RenameFile
Try
' in der Schleife wird auf etwaige Änderungen im Dateisystem gewartet
While True
fsw.WaitForChanged(IO.WatcherChangeTypes.All)
End While
Catch : End Try
fsw.EnableRaisingEvents = False
End Sub
' Änderungen der Protokolldatei nicht aufzeichnen
Private Function IsLog(ByVal path As String) As Boolean
Dim fi As New IO.FileInfo(path)
Return fi.Name.ToLower().Equals("FileWatchLog.log".ToLower())
End Function
' Datei wurde umbenannt
Private Sub RenameFile(sender As Object, e As IO.RenamedEventArgs)
Dim entry As String = "[" & Now.ToShortTimeString() & "] " & _
String.Format("Pfad: {0}; Datei umbenannt: von {1} nach {2}", _
IO.Path.GetDirectoryName(e.FullPath), e.OldName, e.Name)
WriteToLogFile(entry)
End Sub
' Datei wurde gelöscht
Private Sub DeleteFile(sender As Object, e As IO.FileSystemEventArgs)
Dim entry As String = "[" & Now.ToShortTimeString() & "] " & _
String.Format("Pfad: {0}; Datei gelöscht: {1}", _
IO.Path.GetDirectoryName(e.FullPath), e.Name)
WriteToLogFile(entry)
End Sub
' Datei wurde neu erzeugt
Private Sub CreateFile(sender As Object, e As IO.FileSystemEventArgs)
Dim entry As String = "[" & Now.ToShortTimeString() & "] " & _
String.Format("Pfad: {0}; Datei erstellt: {1}", _
IO.Path.GetDirectoryName(e.FullPath), e.Name)
WriteToLogFile(entry)
End Sub
' Datei wurde geändert
Private Sub ChangeFile(sender As Object, e As IO.FileSystemEventArgs)
If IsLog(e.FullPath) Then Return
Dim entry As String = "[" & Now.ToShortTimeString() & "] " & _
String.Format("Pfad: {0}; Datei geändert: {1}", _
IO.Path.GetDirectoryName(e.FullPath), e.Name)
WriteToLogFile(entry)
End Sub
' Logdatei lesen
Private Function ReadFromLogFile() As Specialized.StringCollection
Dim strCol As New Specialized.StringCollection()
Dim sr As New IO.StreamReader(Me.protocolFile)
Dim line As String = ""
Dim count As Integer = 1
line = sr.ReadLine()
While line IsNot Nothing AndAlso count < 999
strCol.Add(line)
count += 1
line = sr.ReadLine()
End While
sr.Close()
Return strCol
End Function
' Schreibt eine Änderung an den Anfang der Protokolldatei
Private Sub WriteToLogFile(ByVal entry As String)
Dim changedEntriesCol As Specialized.StringCollection
If IO.File.Exists(Me.protocolFile) Then
changedEntriesCol = ReadFromLogFile()
Else
changedEntriesCol = New Specialized.StringCollection()
End If
' neuen Eintrag an erster Stelle einfügen
changedEntriesCol.Insert(0, entry)
' Einträge in eine Log-Datei, neue erzeugen / alte überschreiben
Dim fs As New IO.FileStream(Me.protocolFile, IO.FileMode.Create)
Dim sw As New IO.StreamWriter(fs)
For Each str As String In changedEntriesCol
sw.WriteLine(str)
Next
sw.Close()
End Sub
End Class
Tipp |
Einen Windows-Dienst müssen Sie auch zum Testen installieren. Um aus dem Test keine Installations- bzw. Deinstallationsorgie zu machen, sollten Sie dienstunabhängige Funktionalität, hier die Klasse Worker, in einem normalen Projekt zuvor ausgiebig testen. |
Der gesamten Funktionalität des Dienstes liegt eine Klasse aus dem Namensraum System.IO zugrunde: FileSystemWatcher. Auch diese Klasse wird Ihnen als Steuerelement in der Toolbox zur Verfügung gestellt. Objekte dieses Typs lösen Ereignisse aus, wenn eine Datei oder ein Verzeichnis verändert wird. Das zu überwachende Verzeichnis wird dem Konstruktor übergeben. Die ausgelösten Ereignisse heißen Changed, Created, Deleted und Renamed.
In der bei Dienststart aufgerufenen OnStart-Methode wird zuerst ein Objekt vom Typ der Klasse Worker erzeugt. Es wird das zu überwachende Verzeichnis übergeben und im Feld path gespeichert. In der Startmethode des Threads namens StartWorker wird das FileSystem-Watcher-Objekt erzeugt. Mit der Eigenschaft IncludeSubdirectories schließen wir auch alle Unterverzeichnisse in die Überwachung ein, und mit EnableRaisingEvents wird die Ereignisauslösung aktiviert. Nach dem Binden der Handler an die Ereignisse kommt der Kern des gesamten Dienstes: der Aufruf der Methode WaitForChanged des FileSystemWatcher-Objekts. Der Methodenaufruf wird erst nach dem Eintritt einer Änderung beendet und löst die entsprechenden Ereignisse aus. Damit nicht nur eine Änderung erfasst wird, packen wir den Aufruf in eine Endlosschleife. Sie wird bei Beendigung des Dienstes durch die Ausnahme ThreadAbortException beendet, die durch den Aufruf von Abort in der Methode OnStop ausgelöst wird.
Aufgezeichnet werden die Veränderungen in einer Protokolldatei, die sich im gleichen Verzeichnis wie die ausführbare Datei des Dienstes befindet. In allen vier Ereignishandlern werden entsprechende Textinformationen über den Aufruf der benutzerdefinierten Methode WriteToLogFile in die Protokolldatei geschrieben. Allerdings müssen wir bei der Überwachung von Änderungen vorsichtig sein. Befindet sich nämlich die Protokolldatei im überwachten Verzeichnis, wird sie natürlich selbst auch verändert und löst damit das Change-Ereignis erneut aus – der klassische Fall einer Endlosschleife. Daher wird im Ereignishandler ChangeFile zuerst geprüft, ob eine Änderung der Protokolldatei das Ereignis ausgelöst hat. In diesem Fall muss sich die Protokolldatei nicht selbst protokollieren.
Der in die Protokolldatei zu schreibende Eintrag wird der Methode WriteToLogFile übergeben. Sie überprüft zuerst, ob die Protokolldatei bereits existiert. Wenn ja, wird sie in der Methode ReadFromLogFile eingelesen. Jeder Eintrag in der Protokolldatei nimmt eine Zeile in Anspruch. Damit die Datei nicht zu groß wird, ist die Maximalgröße im Programmcode auf 1000 Einträge beschränkt. Das erreichen wir durch Einlesen von maximal 999 Einträgen. Zur einfacheren Verwaltung wird jeder Dateieintrag einem StringCollection-Objekt übergeben. Der neue Eintrag wird danach mit Insert der Auflistung vorangestellt. Damit ist garantiert, dass ein Anwender nach dem Öffnen der Protokolldatei die aktuellsten Einträge immer oben findet.
18.3.6 Dienste mittels Programmcode steuern 

In den letzten Abschnitten haben Sie erfahren, wie ein Windows-Dienst entwickelt wird, der unbemerkt vom Anwender im Hintergrund seine Aufgaben erledigt. Den Status eines Dienstes können Sie mit der SCM-Komponente (Service Control Manager) kontrollieren: Mit SCM kann ein Dienst gestartet, beendet und – wenn ein Dienst auch diese Möglichkeit zulässt – auch angehalten und später wieder fortgesetzt werden.
Manchmal kommt es vor, dass ein Dienst aus einer Anwendung heraus gesteuert werden soll, ähnlich wie es der SCM in der Microsoft Management Console (MMC) auch macht. Zu diesem Zweck stellt das .NET Framework eine eigene Klasse bereit: ServiceController. Diese Klasse hat mehrere Aufgaben:
- Eine statische Methode liefert die Referenzen installierter Dienste.
- Ihre Instanzeigenschaften beschreiben einen Dienst, beispielsweise ob er angehalten oder beendet werden kann.
- Alle installierten Gerätetreiber können abgerufen werden.
Um die Klasse ServiceController nutzen zu können, müssen Sie zuerst die Datei System. ServiceProcess.dll unter Verweise einbinden. Außerdem sollten Sie auch noch mit
Imports System.ServiceProcess
den Namensraum bekannt geben.
Installierte Windows-Dienste ermitteln
Mit der überladenen klassengebundenen Methode GetServices ermitteln Sie alle Dienste des lokalen oder sogar eines entfernten Rechners:
Public Shared Function GetServices() As ServiceController() |
Der Rückgabewert ist ein Array vom Typ ServiceController. Jedes Element des Arrays beschreibt einen Dienst, unabhängig von seinem aktuellen Laufzustand.
Im Array sind die Gerätetreiberdienste nicht enthalten. Wollen Sie diese ermitteln, steht Ihnen mit GetDevices eine analoge klassengebundene Methode zur Verfügung:
Public Shared Function GetDevices() As ServiceController() |
Eigenschaften eines Dienstes abfragen
Gestartet werden kann jeder Dienst. Das Anhalten und Fortsetzen ist aber schon eine Option, die nicht jeder Dienst hat. Manche Dienste sind sogar von so elementarer Natur, dass sie sich noch nicht einmal beenden lassen. Andere Dienste wiederum werden beim Herunterfahren des Systems benachrichtigt und können darauf spezifisch reagieren.
Anwendungen mit der Fähigkeit, Dienste zu steuern, müssen deren Verhaltensweisen kennen und darüber hinaus auch noch den aktuellen Zustand ermitteln können. Die in Tabelle 18.8 gezeigten Eigenschaften der Klasse ServiceController liefern diese Informationen.
Eigenschaft | Beschreibung | |
CanPauseAndContinue |
Gibt an, ob ein Dienst angehalten und wieder fortgesetzt werden kann. |
R |
CanShutdown |
Der Dienst wird beim Herunterfahren des Systems informiert. |
R |
CanStop |
Gibt an, ob der Dienst beendet werden kann. |
R |
DependentServices |
Dienste, die dieser Dienst bei seinem Ende stoppt |
R |
DisplayName |
Der angezeigte Name des Dienstes (z. B. im SCM) |
|
MachineName |
Der Name des Rechners, auf dem der Dienst ausgeführt wird |
|
ServiceHandle |
Korrespondierende Datenstruktur im Betriebssystem |
R |
ServiceName |
Der Name des Dienstes, unter dem er im SCM identifiziert wird |
|
ServicesDependedOn |
Von dem Dienst benötigte andere Dienste |
R |
ServiceType |
Die Art des Dienstes (aufgelistet in der Enumeration ServiceType) |
R |
Status |
Der Zustand des Dienstes |
R |
Die Eigenschaft Status, die den aktuellen Zustand eines Dienstes beschreibt, sehen wir uns noch etwas genauer an. Die anderen sind halbwegs selbsterklärend.
Public ReadOnly Property Status As ServiceControllerStatus |
Der in Tabelle 18.9 gezeigte Typ ServiceControllerStatus ist eine Enumeration, deren Konstanten beschreiben, ob ein Dienst gestartet, angehalten oder beendet wurde bzw. ob er sich derzeit in einem Zustandswechsel befindet.
Konstante | Beschreibung |
ContinuePending |
Es wird versucht, den angehaltenen Dienst fortzusetzen. |
Paused |
Der Dienst ist angehalten. |
PausePending |
Der Dienst wird angehalten. |
Running |
Der Dienst wird ausgeführt. |
StartPending |
Der Dienst wird gestartet. |
Stopped |
Der Dienst ist beendet. |
StopPending |
Der Dienst wird beendet. |
Methoden eines Dienstes
Mit den Instanzmethoden Start, Stop, Pause und Continue eines ServiceController-Objekts wird der Zustand eines Dienstes geändert. Diese Methoden waren eigentlich auch zu erwarten, aber neben einer Reihe weiterer ragt noch eine andere Methode heraus: WaitForStatus.
Public Sub WaitForStatus(desired As ServiceControllerStatus) |
Diese Methode unterbricht den weiteren Programmablauf so lange, bis der Dienst den im Parameter angegebenen Zustand aufweist. Die Methode ist besonders nützlich beim Start und Ende eines Dienstes, denn diese beiden Aktionen nehmen etwas Zeit in Anspruch. Die maximale Ausführungszeit kann in einem optionalen zweiten Parameter angegeben werden. Mit
myService.WaitForStatus(ServiceControllerStatus.Running, _
New TimeSpan(0, 0, 15))
geben Sie zum Beispiel an, dass der Start des unter myService referenzierten Dienstes maximal 15 Sekunden dauern darf. Bei Überschreitung dieser Zeit wird eine Ausnahme ausgelöst, die abgefangen werden muss.
Tipp |
Eine zu kurz gewählte Zeitspanne verhindert eine erfolgreiche Ausführung auf langsameren Rechnern. Wenn Sie die Zeitspanne aus einer Konfigurationsdatei oder der Registry auslesen, kann der Benutzer den Wert gegebenenfalls anpassen. |
In Tabelle 18.10 finden Sie noch einmal alle angesprochenen Methoden wieder.
Methode | Beschreibung | |
Close |
Gibt von der ServiceController-Instanz reservierte Ressourcen frei. |
|
Continue |
Setzt einen angehaltenen Dienst fort. |
|
ExecuteCommand |
Aufruf eines der in OnCustomCommand selbst definierten Befehle |
|
GetDevices |
Ruft ein ServiceController-Array aller Gerätetreiberdienste ab. |
S |
GetServices |
ServiceController-Array aller Dienste ohne Gerätetreiberdienste |
S |
Pause |
Hält einen laufenden Dienst an. |
|
Refresh |
Aktualisiert alle Eigenschaftswerte. |
|
Start |
Startet den Dienst. |
|
Stop |
Beendet den Dienst. |
|
WaitForStatus |
Wartet so lange mit der weiteren Ausführung des Programms, bis der Dienst einen bestimmten Zustand eingenommen hat. |
Ein Beispielprogramm
Im folgenden Beispiel ListServices werden alle lokal installierten Dienste in einer Listbox angezeigt. Über vier Schaltflächen kann der jeweils ausgewählte Dienst gestartet, gestoppt, angehalten oder fortgesetzt werden (siehe Abbildung 18.5). Bitte achten Sie beim Testen darauf, dass Sie sich unter einem Windows-Benutzerkonto mit ausreichenden Rechten einloggen.
Abbildung 18.5 Benutzeroberfläche des Beispielprogramms »ListServices«
Die Listbox wird im Load-Ereignis der Form gefüllt. Angezeigt wird die Zeichenfolge, die von der Eigenschaft DisplayName des referenzierten Dienstes zurückgeliefert wird.
'...\Programmiertechniken\Dienste\ListServices.vb |
Private Sub Laden(sender As Object, e As EventArgs) Handles MyBase.Load
For Each ctrl As ServiceController In ServiceController.GetServices()
Dienste.Items.Add(ctrl.DisplayName)
Next
Me.SetButtons()
End Sub
Die Auswahl eines Dienstes löst das Ereignis SelectedIndexChanged der Listbox aus. Im Ereignishandler wird im ServiceController-Array zuerst der ausgewählte Dienst anhand des angezeigten Namens gesucht. Wird die Routine fündig, wird die Referenz auf den Dienst in der Variablen dienst festgehalten, die auf Klassenebene deklariert ist.
Private dienst As ServiceController
Private Sub Auswahl(sender As Object, e As EventArgs) _
Handles Dienste.SelectedIndexChanged
Me.dienst = Nothing
For Each ctrl As ServiceController In ServiceController.GetServices()
If Dienste.Text = ctrl.DisplayName Then
Me.dienst = ctrl
Me.SetButtons()
Return
End If
Next
End Sub
Sobald der markierte Dienst im Array gefunden wird, können auch die vier Schaltflächen sowohl an den Zustand des Dienstes als auch an dessen Fähigkeiten angepasst werden. Ist der Dienst beispielsweise gestartet, muss die entsprechende Schaltfläche zum Starten deaktiviert angezeigt werden. Bei einem Dienst, der nicht angehalten werden kann, sind die beiden Schaltflächen Fortsetzen und Anhalten grundsätzlich deaktiviert. Die private Methode SetButtons setzt den Aktivierungszustand der Schaltflächen abhängig vom Zustand des Dienstes mit der Methode SetButtonStatus.
' Schaltflächen aktivieren/deaktivieren
Private Sub SetButtons()
If dienst Is Nothing Then
SetButtonStatus(False, False, False, False)
ElseIf dienst.Status = ServiceControllerStatus.Stopped Then
SetButtonStatus(True, False, False, False)
ElseIf dienst.Status = ServiceControllerStatus.Running Then
If dienst.CanPauseAndContinue Then
SetButtonStatus(False, True, True, False)
Else
SetButtonStatus(False, True, False, False)
End If
ElseIf dienst.Status = ServiceControllerStatus.Paused Then
SetButtonStatus(False, True, False, True)
End If
End Sub
Private Sub SetButtonStatus(ByVal st As Boolean, ByVal sp As Boolean, _
ByVal ht As Boolean, ByVal ft As Boolean)
Start.Enabled = st : Stopp.Enabled = sp
Anhalten.Enabled = ht : Fortsetzen.Enabled = ft
End Sub
Kommen wir zum Schluss noch zur Implementierung des Programmcodes der Schaltflächen in der Methode Handlung. Um den Anwender über die möglicherweise länger andauernde Operation zu informieren, wird während der gesamten Ausführungszeit als Mauszeiger eine Sanduhr angezeigt. Der ausgewählte Dienst bestimmt die im Delegate gespeicherte Methode, die in der Methode Handlung aufgerufen wird. Der eingeleiteten Operation werden 15 Sekunden zugestanden, um den Dienst erfolgreich zu kontrollieren. Bei Zeitüberschreitung wird eine Ausnahme ausgelöst. Daher ist der Aufruf der WaitForStatus-Methode in einem Try-Block codiert. Nach Beendigung des Kontrolle wird erneut die Methode SetButtons aufgerufen, um den Aktivierungszustand der Schaltflächen an den möglicherweise neuen Dienstzustand anzupassen. Die vier Schaltflächen rufen Handlung dann mit den passenden Kontrollfunktionen und gewünschten Zuständen auf.
' Dienstkontrolle
Private Delegate Sub aktion()
Private Sub Handlung(ByVal was As aktion, ByVal st As ServiceControllerStatus)
Me.Cursor = Cursors.WaitCursor
Try
was.Invoke()
dienst.WaitForStatus(st, New TimeSpan(0, 0, 15))
Catch ex As Exception
MessageBox.Show(ex.Message, "Fehler")
Finally
Me.Cursor = Cursors.Default
Me.SetButtons()
End Try
End Sub
Private Sub Starten(sender As Object, e As EventArgs) Handles Start.Click
Handlung(AddressOf dienst.Start, ServiceControllerStatus.Running)
End Sub
Private Sub Weiter(sender As Object, e As EventArgs) Handles Fortsetzen.Click
Handlung(AddressOf dienst.Continue, ServiceControllerStatus.Running)
End Sub
Private Sub Pause(sender As Object, e As EventArgs) Handles Anhalten.Click
Handlung(AddressOf dienst.Pause, ServiceControllerStatus.Paused)
End Sub
Private Sub Ende(sender As Object, e As EventArgs) Handles Stopp.Click
Handlung(AddressOf dienst.Stop, ServiceControllerStatus.Stopped)
End Sub
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.