Oktober 2016

Band 31, Nummer 10

Dieser Artikel wurde maschinell übersetzt.

Windows-Dienst: Erstellen eines anpassbaren FileSystemWatcher-Windows-Diensts

Von Diego Ordonez

Die FileSystemWatcher-Klasse ist ein sehr leistungsfähiges Tool, das seit Version 1.1 Bestandteil von Microsoft .NET Framework ist. Laut ihrer offiziellen Definition (bit.ly/2b8iOvQ) gilt für sie Folgendes: Sie lauscht auf die Änderungsbenachrichtigungen des Dateisystems und löst Ereignisse aus, wenn sich ein Verzeichnis oder eine Datei in einem Verzeichnis ändert.

Diese Klasse ist in der Lage, Ereignisse im Dateisystem zu erkennen, z. B. das Erstellen, Ändern oder Löschen von Dateien und Ordnern. Sie kann vollständig angepasst werden, und ihr Konstruktor nimmt Parameter wie den Ordnerspeicherort und die Dateierweiterung an, auf den bzw. die gelauscht werden soll, sowie einen booleschen Parameter zum Angeben, ob der Lauschprozess rekursiv durch die Ordnerstruktur erfolgen soll. Das Verwenden dieser Parameter in Ihrem Quellcode ist jedoch kein guter Ansatz, weil sie nicht hilfreich sind, wenn die Anwendung neue Ordner und Dateierweiterungen berücksichtigen muss, für die außerdem Codierung, ein Buildvorgang und eine erneute Bereitstellung erfolgen müssen. Wenn Sie nicht sicher sind, dass diese Einstellungen in Ihrer Anwendung niemals geändert werden, empfiehlt es sich, einen Mechanismus zu implementieren, der die Konfiguration ändern kann, ohne dass eine Anpassung des Quellcodes erforderlich ist.

In diesem Artikel beschreibe ich, wie eine Anwendung erstellt werden kann, die die FileSystemWatcher-Klasse nur ein Mal verwendet, anschließend jedoch mithilfe von XML-Serialisierung weitere Änderungen an den Einstellungen der Anwendung (z. B. Ordnernamen, Dateierweiterungen und beim Auslösen eines Ereignisses auszuführende Aktionen) ermöglicht. Auf diese Weise können alle Änderungen durch einfaches Aktualisieren einer XML-Datei und Neustarten des Windows-Diensts erreicht werden.

Aus Gründen der Einfachheit werde ich nicht die Details erläutern, wie diese C#-Konsolenanwendung als ein Windows-Dienst ausgeführt werden kann. Online sind jedoch zahlreiche Ressourcen zu diesem Thema verfügbar.

Die Struktur angepasster Ordnereinstellungen

Da ich plane, die XML-Einstellungsdatei in eine gut strukturierte C#-Klasse zu deserialisieren, muss die erste Komponente der Anwendung die Definition der Parameter sein, die FileSystemWatcher benötigt, um zu funktionieren. Abbildung1 zeigt den Code, der diese Klasse definiert.

Abbildung 1: Definition der CustomFolderSettings-Klasse

/// <summary>
/// This class defines an individual type of file and its associated
/// folder to be monitored by the File System Watcher
/// </summary>
public class CustomFolderSettings
{
  /// <summary>Unique identifier of the combination File type/folder.
  /// Arbitrary number (for instance 001, 002, and so on)</summary>
  [XmlAttribute]
  public string FolderID { get; set; }
  /// <summary>If TRUE: the file type and folder will be monitored</summary>
  [XmlElement]
  public bool FolderEnabled { get; set; }
  /// <summary>Description of the type of files and folder location –
  /// Just for documentation purpose</summary>
  [XmlElement]
  public string FolderDescription { get; set; }
  /// <summary>Filter to select the type of files to be monitored.
  /// (Examples: *.shp, *.*, Project00*.zip)</summary>
  [XmlElement]
  public string FolderFilter { get; set; }
  /// <summary>Full path to be monitored
  /// (i.e.: D:\files\projects\shapes\ )</summary>
  [XmlElement]
  public string FolderPath { get; set; }
  /// <summary>If TRUE: the folder and its subfolders will be monitored</summary>
  [XmlElement]
  public bool FolderIncludeSub { get; set; }
  /// <summary>Specifies the command or action to be executed
  /// after an event has raised</summary>
  [XmlElement]
  public string ExecutableFile { get; set; }
  /// <summary>List of arguments to be passed to the executable file</summary>
  [XmlElement]
  public string ExecutableArguments { get; set; }
  /// <summary>Default constructor of the class</summary>       
  public CustomFolderSettings()
  {
  }
}

Sehen wir uns nun an, wie eine XML-Datei in diese C#-Klasse mithilfe des Deserialisierungsvorgangs übersetzt werden kann. Beachten Sie, dass nicht eine einzelne Instanz der CustomFolderSettings-Klasse vorhanden sein wird. Stattdessen wird eine Liste (List<CustomFolderSettings>) verwendet, die es dem Windows-Dienst ermöglicht, auf viele verschiedene Ordnerspeicherorte und Dateierweiterungen zu lauschen.

Abbildung 2 zeigt ein Beispiel für eine XML-Einstellungsdatei, aus der alle benötigten Argumente für FileSystemWatcher bereitgestellt werden können. Machen Sie sich an diesem Punkt bewusst, dass die in der XML-Datei enthaltenen Informationen (Abbildung 2) die C#-Klasse (Abbildung 1) mit Daten versorgen.

Abbildung 2: Struktur der XML-Einstellungsdatei

<?xml version="1.0" encoding="utf-8"?>
<ArrayOfCustomFolderSettings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <CustomFolderSettings FolderID="ExampleKML_files">
    <FolderEnabled>true</FolderEnabled>   
    <FolderDescription>Files in format KML corresponding to the example project
      </FolderDescription>
    <FolderFilter>*.KML</FolderFilter>
    <FolderPath>C:\Temp\testKML\</FolderPath>
    <FolderIncludeSub>false</FolderIncludeSub>
    <ExecutableFile>CMD.EXE</ExecutableFile>
    <!-- The block {0} will be automatically replaced with the
      corresponding file name -->
    <ExecutableArguments>/C echo It works properly for .KML extension-- File {0}
      &gt; c:\temp\it_works_KML.txt</ExecutableArguments>
  </CustomFolderSettings>
  <CustomFolderSettings FolderID="ExampleZIP_files">
    <FolderEnabled>false</FolderEnabled>
    <FolderDescription>Files in format ZIP corresponding to the example project
      </FolderDescription>
    <FolderFilter>*.ZIP</FolderFilter>
    <FolderPath>C:\Temp\testZIP\</FolderPath>
    <FolderIncludeSub>false</FolderIncludeSub>
    <ExecutableFile>CMD.EXE</ExecutableFile>
    <!-- The block {0} will be automatically replaced with the
      corresponding file name -->
    <ExecutableArguments>/C echo It works properly for .ZIP extension -- File {0}
      &gt; c:\temp\it_works_ZIP.txt</ExecutableArguments>
  </CustomFolderSettings>
</ArrayOfCustomFolderSettings>

Werfen wir einen genaueren Blick auf die jetzt in der XML-Datei enthaltenen Parameter. Beachten Sie zuerst, dass das XML-Stammelement <ArrayOfCustomFolderSettings> ist, das jede erforderliche Anzahl von <CustomFolderSettings>-Elementen zulässt. Hier liegt der Schlüssel für die gleichzeitige Überwachung mehrerer Ordnerspeicherorte und Dateierweiterungen.

Beachten Sie außerdem, dass der Parameter <FolderEnabled> für den ersten Ordner TRUE, für den zweiten Ordner aber FALSE ist. Dies stellt eine einfache Möglichkeit dar, einen der FileSystemWatchers zu deaktivieren, ohne ihn aus der XML-Datei löschen zu müssen. Das bedeutet, dass die Klasse die Konfiguration selbst dann übergeht, wenn sie vorhanden ist.

Schließlich müssen Sie verstehen, wie angegeben wird, welche Aktion ausgelöst wird, wenn eine erstellte, gelöschte oder geänderte Datei erkannt wird. Dies ist schließlich das Ziel der FileSystemWatcher-Klasse.

Der Parameter <ExecutableFile> enthält die Anwendung, die gestartet wird. In diesem Beispiel ist dies die DOS-Befehlszeile („CMD.EXE“).

Der Parameter <ExecutableArguments> enthält die Optionen, die an die ausführbare Datei als Argumente übergeben werden. Dies ist das Beispiel aus Abbildung 2:

>/C echo It works properly for .ZIP extension -- File {0} &gt;
  c:\temp\it_ZIP_works.txt

Zur Laufzeit erfolgt die Übersetzung in Folgendes:

CMD.EXE /C echo it works properly for .ZIP extension –– File
  d:\tests\file_modified_detected.doc > c:\temp\it_works_ZIP.txt

Die Zeichenfolge wird in die Datei „c:\temp\it_works_ZIP.txt“ geschrieben, und der Wert {0} in der XML-Datei wird durch den tatsächlichen Namen der Datei ersetzt, die durch FileSystemWatcher erkannt wurde. Wenn Sie mit der C#-Methode „string.Format“ vertraut sind, kommt ihnen diese Funktionsweise bestimmt bekannt vor.

An diesem Punkt verfüge ich nun über eine XML-Konfigurationsdatei und eine C#-Klasse mit übereinstimmenden Attributen. Der nächste Schritt besteht daher im Deserialisieren der XML-Informationen in eine Liste von Klassen (List<CustomFolderSettings>). Abbildung 3 zeigt die Methode, die diesen wesentlichen Schritt ausführt.

Abbildung 3: Deserialisierung der XML-Einstellungsdatei

/// <summary>Reads an XML file and populates a list of <CustomFolderSettings> </summary>
private void PopulateListFileSystemWatchers()
{
  // Get the XML file name from the App.config file
  fileNameXML = ConfigurationManager.AppSettings["XMLFileFolderSettings"];
  // Create an instance of XMLSerializer
  XmlSerializer deserializer =
    new XmlSerializer(typeof(List<CustomFolderSettings>));
  TextReader reader = new StreamReader(fileNameXML);
  object obj = deserializer.Deserialize(reader);
  // Close the TextReader object
  reader.Close();
  // Obtain a list of CustomFolderSettings from XML Input data
  listFolders = obj as List<CustomFolderSettings>;
}

Sobald diese Methode ausgeführt wird, ist eine Liste mit allen erforderlichen FileSystemWatcher-Instanzen verfügbar. Der nächste Schritt besteht also im Starten der FileSystemWatcher-Klasse, die den Lauschvorgang einleitet.

Die Methode muss natürlich wissen, wo sich die XML-Einstellungsdatei befindet. Ich verwenden die Datei „App.config“ zum Definieren des Speicherorts der XML-Datei. Die Datei „App.config“ weist den folgenden Inhalt auf:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="XMLFileFolderSettings" value=
      "C:\Work\CSharp_FileSystemW\CustomSettings.xml" />
  </appSettings>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
</configuration>

Denken Sie unbedingt daran, dass nach jeder Änderung in der XML-Einstellungsdatei oder in der Datei „App.config“ ein Neustart des Windows-Diensts erforderlich ist, damit diese Änderungen angewendet werden.

Starten des FileSystemWatcher-Prozesses (Lauschen auf Änderungen)

Nun sind alle Einstellungen, die für die verschiedenen Instanzen (oder mindestens eine Instanz) von FileSystemWatcher erforderlich sind, in der Liste verfügbar, die in Abbildung 3 erstellt wird.

Es ist nun an der Zeit, den Lauschvorgang zu starten. Zu diesem Zweck muss ich die Liste in einer Schleife durchlaufen und die Instanzen nacheinander starten. Der Code in Abbildung 4 zeigt, wie der Initialisierungsprozess ausgeführt wird und wie alle aus der XML-Datei abgerufenen Parameter zugewiesen werden.

Abbildung 4. Initialisierung der FileSystemWatcher-Instanzen

/// <summary>Start the file system watcher for each of the file
/// specification and folders found on the List<>/// </summary>
private void StartFileSystemWatcher()
{
  // Creates a new instance of the list
  this.listFileSystemWatcher = new List<FileSystemWatcher>();
  // Loop the list to process each of the folder specifications found
  foreach (CustomFolderSettings customFolder in listFolders)
  {
    DirectoryInfo dir = new DirectoryInfo(customFolder.FolderPath);
    // Checks whether the folder is enabled and
    // also the directory is a valid location
    if (customFolder.FolderEnabled && dir.Exists)
    {
      // Creates a new instance of FileSystemWatcher
      FileSystemWatcher fileSWatch = new FileSystemWatcher();
      // Sets the filter
      fileSWatch.Filter = customFolder.FolderFilter;
      // Sets the folder location
      fileSWatch.Path = customFolder.FolderPath;
      // Sets the action to be executed
      StringBuilder actionToExecute = new StringBuilder(
        customFolder.ExecutableFile);
      // List of arguments
      StringBuilder actionArguments = new StringBuilder(
        customFolder.ExecutableArguments);
      // Subscribe to notify filters
      fileSWatch.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName |
        NotifyFilters.DirectoryName;
      // Associate the event that will be triggered when a new file
      // is added to the monitored folder, using a lambda expression                   
      fileSWatch.Created += (senderObj, fileSysArgs) =>
        fileSWatch_Created(senderObj, fileSysArgs,
         actionToExecute.ToString(), actionArguments.ToString());
      // Begin watching
      fileSWatch.EnableRaisingEvents = true;
      // Add the systemWatcher to the list
      listFileSystemWatcher.Add(fileSWatch);
      // Record a log entry into Windows Event Log
      CustomLogEvent(String.Format(
        "Starting to monitor files with extension ({0}) in the folder ({1})",
        fileSWatch.Filter, fileSWatch.Path));
    }
  }
}

In diesem Code lauscht der FileSystemWatcher nur auf ein Erstellungsereignis. Es sind jedoch auch andere Ereignisse verfügbar, z. B. für Lösch- und Umbenennungsvorgänge.

Insbesondere möchte ich auf die Zeile hinweisen, in der eine Funktion das Ereignis „Created“ von FileSystemWatcher abonniert. Hier verwende ich aus einem wichtigen Grund einen Lambdaausdruck: Da ich eine Liste der Instanzen der FileSystemWatcher-Klasse verwende, muss ich jeder Instanz eine bestimmte ausführbare Datei zuweisen. Wenn ich hier anders vorgehen würde (z. B. direktes Zuweisen der Funktion, anstatt einen Lambdaausdruck zu verwenden), würde nur die letzte ausführbare Datei beibehalten, und alle FileSystemWatcher-Instanzen würden die gleiche Aktion ausführen.

Abbildung 5 zeigt den Code für die Funktion, die die Aktion tatsächlich basierend auf speziellen Kriterien für jede einzelne Instanz von FileSystemWatcher ausführt.

Abbildung 5: Ausführen einer Aktion basierend auf den Kriterien für jede Instanz

/// <summary>This event is triggered when a file with the specified
/// extension is created on the monitored folder</summary>
/// <param name="sender">Object raising the event</param>
/// <param name="e">List of arguments - FileSystemEventArgs</param>
/// <param name="action_Exec">The action to be executed upon detecting a change in the File system</param>
/// <param name="action_Args">arguments to be passed to the executable (action)</param>
void fileSWatch_Created(object sender, FileSystemEventArgs e,
  string action_Exec, string action_Args)
{
  string fileName = e.FullPath;
  // Adds the file name to the arguments. The filename will be placed in lieu of {0}
  string newStr = string.Format(action_Args, fileName);
  // Executes the command from the DOS window
  ExecuteCommandLineProcess(action_Exec, newStr);
}

Abbildung 6 zeigt schließlich die ExecuteCommandLineProcess-Funktion, ein Standardverfahren zum Ausführen von Befehlszeilenanweisungen (eine DOS-Konsole).

Abbildung 6: Ausführen von Befehlszeilenanweisungen

/// <summary>Executes a set of instructions through the command window</summary>
/// <param name="executableFile">Name of the executable file or program</param>
/// <param name="argumentList">List of arguments</param>
private void ExecuteCommandLineProcess(string executableFile, string argumentList)
{
  // Use ProcessStartInfo class
  ProcessStartInfo startInfo = new ProcessStartInfo();
  startInfo.CreateNoWindow = true;
  startInfo.UseShellExecute = false;
  startInfo.FileName = executableFile;
  startInfo.WindowStyle = ProcessWindowStyle.Hidden;
  startInfo.Arguments = argumentList;
try
  {
    // Start the process with the info specified
    // Call WaitForExit and then the using-statement will close
    using (Process exeProcess = Process.Start(startInfo))
    {
      exeProcess.WaitForExit();
      // Register a log of the successful operation
      CustomLogEvent(string.Format(
        "Succesful operation --> Executable: {0} --> Arguments: {1}",
        executableFile, argumentList));
    }
  }
  catch (Exception exc)
  {
    // Register a Log of the Exception
  }
}

Starten und Beenden von FileSystemWatcher in einem Windows-Dienst

Wie zu Beginn gesagt, ist diese Anwendung für die Ausführung als Windows-Dienst konzipiert. Ich benötige daher eine Möglichkeit zum automatischen Starten oder Beenden von FileSystemWatcher-Instanzen, wenn der Windows-Dienst gestartet, beendet oder neu gestartet wird. Ich werde hier nicht näher auf die Definition eines Windows-Diensts eingehen. Die beiden Hauptmethoden der Implementierung des Windows-Diensts sollen jedoch nicht unerwähnt bleiben: OnStart und OnStop. Bei jedem Start des Windows-Diensts muss dieser anfangs zwei Aktionen ausführen: Auffüllen der Liste der FileSystemWatcher-Instanzen mit Daten aus der XML-Datei (Abbildung 3) und anschließendes Starten der Instanzen (Abbildung 4).

Der folgende Code ist zum Starten des Prozesses aus dem Windows-Dienst erforderlich:

/// <summary>Event automatically fired when the service is started by Windows</summary>
/// <param name="args">array of arguments</param>
protected override void OnStart(string[] args)
{
  // Initialize the list of FileSystemWatchers based on the XML configuration file
  PopulateListFileSystemWatchers();
  // Start the file system watcher for each of the file specification
  // and folders found on the List<>
  StartFileSystemWatcher();
}

Abschließend implementiert die Methode in Abbildung 7 die Programmlogik zum Beenden von FileSystemWatcher. Das Beenden oder Neustarten des Windows-Diensts ist erforderlich.

Abbildung 7: Beenden von FileSystemWatcher

/// <summary>Event automatically fired when the service is stopped by Windows</summary>
protected override void OnStop()
{
  if (listFileSystemWatcher != null)
  {
    foreach (FileSystemWatcher fsw in listFileSystemWatcher)
    {
      // Stop listening
      fsw.EnableRaisingEvents = false;
      // Dispose the Object
      fsw.Dispose();
    }
    // Clean the list
    listFileSystemWatcher.Clear();
  }
}

Zusammenfassung

FileSystemWatcher ist eine leistungsfähige Klasse, die das Überwachen von (Lauschen auf) Änderungen ermöglicht, die im Dateisystem auftreten, z. B. das Erstellen, Löschen, Umbenennen und Ändern von Dateien und Ordnern. Diese Anwendung (die als Windows-Dienst ausgeführt werden soll) wurde dafür konzipiert, die einfache Änderung der zu überwachenden Dateien und Ordner einschließlich der Dateierweiterungen zu ermöglichen. Der Ansatz, den ich verwendet habe, nutzt ein sehr praktisches Konzept, das in .NET Framework verfügbar ist: Serialisierung und Deserialisierung. Auf diese Weise kann die FileSystemWatcher-Klasse aus einer XML-Datei mit Daten versorgt werden, ohne dass Änderungen am Quellcode erforderlich werden. Stattdessen muss nach jeder Änderung in der XML-Einstellungsdatei nur der Windows-Dienst neu gestartet werden. Schon werden die Änderungen angewendet.


Diego Ordonez ist Bauingenieur. Er besitzt mehr als 15 Jahre IT-Erfahrung und arbeitet hauptsächlich mit GIS- und CAD-Technologien als Analyst, Entwickler und Architekt. Er ist Microsoft Certified Professional Developer für C#, ASP.NET, ADO.NET, SQL Server und beschäftigt sich ausgesprochen gern mit den Technologien rund um .NET Framework. Er lebt mit seiner Frau und zwei Töchtern in Calgary (Alberta, Kanada) und arbeitet für Altus Geomatics als GIS-Teamleiter (bit.ly/2aWfi34).

Unser Dank gilt dem folgenden technischen Experten bei Microsoft für die Durchsicht dieses Artikels: James McCaffrey
Dr. James McCaffrey ist in Redmond (Washington) für Microsoft Research tätig. Er hat an verschiedenen Microsoft-Produkten mitgearbeitet, unter anderem an Internet Explorer und Bing. Dr. McCaffrey erreichen Sie unter jammc@microsoft.com.