DecaTec

Programmieren, Fotografie, Home-Server und einiges mehr

FileSystemWatcher: Events werden mehrfach ausgelöst – Lösungsansätze

Die Klasse FileSystemWatcher ist Teil des .NET Frameworks und ermöglicht die einfache Überwachung des Dateisystems. Somit ist es möglich auf Ereignisse wie dem Anlegen oder Verändern von Dateien und Verzeichnissen zu reagieren.
Ein bekanntes Problem des FileSystemWatcher ist, dass Events u.U. mehrfach ausgelöst werden. Diesem Problem soll hier auf den Grund gegangen werden und einige Lösungsansätze gezeigt werden.

Alle Code-Beispiele sind dabei in C# aufgeführt, allerdings ist die Vorgehensweise auch auf andere .NET Sprachen übertragbar. Die Visual Studio Solution mit allen Code-Beispielen steht weiter unten zum Download bereit.

Das Problem

Um das Problem zu demonstrieren, legen wir uns zunächst ein neues Projekt als Konsolenapplikation im Visual Studio an und fügen folgenden Code ein (Projekt 01_FileSystemWatcherMultipleEvents in der Beispiel-Solution):

Hier wird einfach ein FileSystemWatcher angelegt, der das Verzeichnis C:\Test überwacht und ein Event auslösen soll, wenn in diesem Verzeichnis Dateien geändert werden. Durch Setzen der Eigenschaft EnableRaisingEvents auf true wird die Überwachung dann gestartet. Mehr wird an dieser Stelle nicht gemacht, auch wenn die Klasse FileSystemWatcher noch einige Eigenschaften bietet, die Überwachung genauer zu spezifizieren. Man könnte auch weitere Eventhandler registrieren, die ausgelöst werden, wenn Dateien/Verzeichnisse angelegt, umbenannt oder gelsöcht werden, aber wir konzentrieren uns hier lediglich auf das Changed-Event.

Nachdem das Programm gestartet wurde, legen wir im zu überwachenden Verzeichnis eine neue Text-Datei (test.txt) an. Hier wird der FileSystemWatcher noch nicht aktiv, das wir wie eben angesprochen nur auf Änderungen an Dateien/Verzeichnissen benachrichtigt werden wollen.
Wenn wir diese Datei nun allerdings mit dem Editor (Notepad) öffnen, ein beliebiges Wort schreiben und anschließend speichern, erhalten wir folgendes Ergebnis auf der Konsole:

Das Ereignis wird folglich zwei mal ausgelöst, obwohl die Datei nur einmal gespeichert wurde.

Ursachen

Die Ursache liegt nun allerdings nicht am FileSystemWatcher, sondern an den Vorgängen, die beim Speichern der Text-Datei stattfinden: Hier wird die Datei beim Speichern offensichtlich zwei mal verändert: Zuerst wird die veränderte Datei auf die Festplatte geschrieben und anschließend werden die Datei-Attribute gesetzt, was technisch auch ein Änderungs-Vorgang an der Datei darstellt. Der FileSystemWatcher löst das Changed-Event folgerichtig auch zwei mal aus.

Entgegen der häufig vertretenen Meinung, dass es sich dabei um einen Bug des FileSystemWatchers handelt, liegt das Problem hier eher an dem Programm, welches zum Speichern der Datei verwendet wird (Notepad). Auch kann es beim Einsatz von anderen Programmen passieren, dass bei einer einzelnen Operation noch sehr viel mehr Veränderungen an einer Datei vorgenommen werden und der FileSystemWatcher folglich für jede einzelne Veränderung der Datei auch das Changed-Event auslöst.

Einfacher Lösungsansatz (Pausieren der Überwachung)

Oftmals ist es allerdings nicht erforderlich, auf jede einzelne Änderung einer Datei zu reagieren, wenn diese durch eine einzelne (Programm-)Operation ausgelöst werden. Hier sollte die Änderung zwar registiert werden, aber viele Veränderung in einem zeitlich sehr kurzen Abstand sollten als eine einzige Änderung betrachtet werden.

Um das Problem nun auf einfache Art und Weise in den Griff zu bekommen, muss der Changed-Eventhandler nun um wenig Code erweitert werden (Project 02_PauseMonitoring in der Beispiel-Solution):

Wenn der Eventhandler nun bei der ersten Datei-Änderung aufgerufen wird, wird die Überwachung des Verzeichnisses deaktiviert, indem die Eigenschaft EnableRaisingEvents des FileSystemWatchers auf false gesetzt wird. Nachdem der Eventhandler durchlaufen wurde, wird die Überwachung des Verzeichnisses wieder aktiviert. Dies geschieht zur Sicherheit in einem finally Block, damit sichergestellt wird, dass diese Operation in jedem Fall ausgeführt wird.

Wird das Programm nun ein weiteres Mal ausgeführt und die Datei verändert und gespeichert, wird das Event erwartungsgemäß nur einmal ausgelöst:

Für einfache Programme kann diese Lösung bereits ausreichend sein. Jedoch kann es durch die Aussetzung der Verzeichnis-Überwachung dazu kommen, dass Änderungen (z.B. an weiteren Dateien) nicht registirert werden. Dieses Problem wird umso deutlicher, wenn der Programmcode des Changed-Eventhandlers komplexer ist und längere Zeit beansprucht.

Um dies zu verdeutlichen kann man eine künstliche Verzögerung (Thread.Sleep) im Eventhandler einfügen:

Wenn das Programm nun ein weiteres Mal ausgeführt wird und die Datei test.txt zwei Mal in kurzer Zeit geändert wird, nämlich innerhalb der fünf Sekunden, in den der Thread pausiert wurde, wird die zweite Änderung nicht registriert.
Dieses Verhalten dürfte in den meisten Fällen ebenso unerwünscht sein, daher trägt diese einfache Lösung wirklich nur für einfache Programme

Besserer Lösungsansatz (Puffern des Changed-Eventhandlers)

Ein besserer Lösungsansatz wäre, wenn die Überwachung des Verzeichnisses nicht ausgesetzt werden müsste, sondern im Eventhandler des Changed-Ereignisses eine Logik implementiert wäre, die verhindert, dass der Eventhandler in kurzer Zeit mehrfach ausgeführt wird. Das Project 03_BufferedEventHandler beinhaltet den dazu gehörigen Code:

In diesem Beispiel wurde zunächst eine einfache Liste (changedList) hinzugefügt. Im Eventhandler wird dann die gerade veränderte Datei dieser Liste angefügt, falls diese noch nicht in der Liste enthalten ist. Falls diese bereits Teil der Liste ist, wird die Ausführung der Methode durch ein return beendet. Ansonsten wird ein Timer gestartet, der nach 500ms zunächst die eigentliche Logik des Eventhandlers ausführt und anschließend die Datei wieder aus der changedList entfernt.

Auf diese Weise werden alle Veränderungen an einer Datei gepuffert, die innerhalb von einer halben Sekunde stattfinden. Erst nach Ablaufen des Timers wird dann die eigentliche Logik des Eventhandlers ausgeführt.

Nun kann über die Variable bufferDelay gesteuert werden, wie lange auf Folge-Events einer Datei-Veränderung gewartet werden soll. Im Beispiel wird diese Variable mit einer halben Sekunde definiert. Folglich werden auch alle überflüssigen Changed-Events, die innerhalb dieser Zeit die gleiche Datei betreffen „verschluckt“.
In der Praxis kann es hier notwendig sein, die Zeitspanne der Verzögerung anzupassen, um keine wichtigen Events zu übersehen. Hier muss man abschätzen, in welchen minimalen Zeitabständen Veränderungen an der gleichen Datei realisitsch sind.

BufferedFileSystemWatcher

Um nun eine wiederverwendbare Lösung zu schaffen, kann der hier vorgestellte Ansatz in eine eigene Klasse ausgelagert werden. Ebenso kann diese Logik auch für die anderen Ereignisse des FileSystemWatchers angewendet werden (Created, Deleted und Renamed). Die Implementierung ist im Project 04_BufferedFileSystemWatcher in der Beispiel-Solution zu finden.

Hier wird eine Klasse BufferedFileSystemWatcher definiert, welche sich von FileSystemWatcher ableitet:

Die Vorgehensweise ist ja schon aus dem vorherigen Lösungsansatz bekannt. Damit nicht jedes Event gepuffert wird, wurde noch ein Enum (BufferedChangeTypes) hinzugefügt, mit dem genau spezifiziert kann, welche Events gepuffert werden sollen und welche nicht. Wird hier BufferedChangeTypes.None angegeben, verhält sich der BufferedFileSystemWatcher genau so wie ein FileSystemWatcher.

Die Verwendung des BufferedFileSystemWatchers erfolgt dann analog zum FileSystemWatcher:

Bei der Ausführung dieses Codes wird beim Speichern der Textdatei mit dem Notepad das Changed-Ereignis nur einmal ausgelöst.

Zu beachten ist hier jedoch wieder die Zeitspanne, in der Events gepuffert werden (Property BufferDelay). Hier gilt es wieder einen praxistauglichen Wert zu ermitteln, damit keine Änderungen an Dateien/Verzeichnissen „übersehen“ werden.

Download

Hier gibt es die komplette Visual Studio Solution mit dem kompletten Code dieses Beitrags zum Download:

FileSystemWatcherSamples.zip

Links

, , , , ,

Kommentare: 2

  • Cesar Mayorine sagt:

    Das ist genau mein Problem.
    Ich soll ein Ordner überwachen, wenn ein Datei im dieser Ordner gespeichert wird, läuft ein Prozez die eigene Zeit braucht, ich soll auch das Systemwatcher „pausieren“ (weil in diese Moment könnte noch ein andere Datei gepeichert werden.) dann wenn das Prozez fertig ist, das fsw. wieder starten..

    hast du ein Beispiel dafür?

    • Jan sagt:

      Hallo Cesar,

      ich würde sagen, das kommt ganz darauf an, welche Änderungen an Dateien du mit dem FileSystemWatcher mitbekommen willst.
      Wenn es dir nur um diese eine Datei geht (also unabhängig davon, ob weitere Dateien geschrieben werden), dann kannst du natürlich im Event-Handler des FileSystemWatchers diesen zunächst pausieren, anschließend den Verarbeitungs-Prozess starten und dann den FileSystemWatcher wieder aktiv schalten.

      Falls du allerdings weitere Änderungen an (anderen) Dateien mitbekommen und evtl. darauf reagieren musst, solltest du den FileSystemWatcher nicht pausieren. In diesem Fall muss die Art der Datei-Änderung im Event-Handler des FileSystemwatchers genauer untersucht werden und dementsprechend reagiert werden.

      Evtl. ist es auch sinnvoll, den Verarbeitungs-Prozess in einen eigenen Thread auszulagern, damit der Thread „entlastet“ wird, in dem der Event-Handler des FileSystemWatchers läuft. Dann musst du dich allerdings auch um eine evtl. notwendige Thread-Synchonisierung kümmern.

      Oder du probierst es erst einmal mit einem gepufferten FileSystemWatcher, wie im Artikel gezeigt.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.