Tamed FileSystemWatcher

[Für Details siehe meinen Artikel “Gezähmte Beobachter” im Windows Developer Magazin, Sept 2015]

[July 11, 2015: Fixed bugs in code. See changelog in readme.txt]

This post shares robust wrappers around the standard FileSystemWatcher (FSW)  fixing problems commonly encountered when using it to monitor the file system in real-world applications.

FSWArchithekur

Buffering and Recovering FSW

Simply replace the standard FSW with my BufferingFileSystemWatcher and you no longer need to worry about InternalBufferOverflowExceptions. Use my RecoveringFileSystemWatcher to automatically recover from typical transient watch path accessibility problems. Download complete code. For a file system watcher using polling instead of file system events see my FilePoller. To process files detected in either way I recommend using TPL DataFlow ActionBlocks. They allow you to easily process files without having to spawn a Thread or create a Task yourself and allow to configure the degree of parallelism desired. For tips about handling lots of files and using contig.exe to defragment NTFS indexes see NTFS performance and large volumes of files and directories.

Typical FileSystemWatcher Problems

If used properly the standard FileSystemWatcher (FSW) is way better than its reputation. However, there are typical problems one may encounter when first using the FSW:

  • Unexpected events.
  • Lost events.
  • InternalBufferOverflowExceptions.
  • No option to report files existing before the FSW started.

The standard FileSystemWatcher:

  • Reports exceptions via its Error event. Not via raising exceptions!
  • Does not report files that existed before .EnableRaisingEvents =True.
  • Does detect network disruptions, but does not automatically recover from them.
  • Does automatically handle renames of its watch path.

Unexpected Event Floods

Some applications trigger lots of file system events for a single action. The FSW simply reports these events. Ex: Excel triggers 15 NTFS events for 4 different files when creating a single new .xlsx file and triggers 8 events for 3 different files of which none is changed event for the file changed one would naively expect:

ExcelNTFSEvents

File system event flood triggered by Excel for single actions like “Save”

If you have control over the file producer you can easily tame this event flood by renaming the files to the watched directory or watched extension only when they are totally complete. For .NET applications a file rename is an atomic operation. Your watcher now only needs to watch for a single renamed event per file. An easy way to create lots of files and file system events is repeatedly copying and pasting all files in a directory via CTRL+A, CTRL-C, CTRL-V.

Lost Events

The FSW only throws exceptions on problems when setting its properties. While watching for changes the FSW reports exceptions via its Error event only and does does not raise them. This is typical for the Event-based Asynchronous Pattern (EAP). To prevent exceptions going unnoticed one must handle the Error event. Strangely this so important FSW Error event does not show up in the Win Forms designer, my wrappers fix this. Maybe not discovering the need to implement an OnError handler and being misled by the FSW throwing exception until started is the reason for the wrongly perceived bad reliability of the FSW. The FSW minimizes usage of precious non-paged memory via its InternalBufferSize property and throws an InternalBufferOverflowException “Too many changes at once in directory …”when exceeded.

Robust File Operations

For file operations one should consider using those from the Microsoft.VisualBasic namespace because they are more robust than those in System.IO and offer more features like automatically overwriting existing files.

When working with files this is typically done in a situation where producers and consumers work concurrently. Here if/then constructs are not robust because changes can happen in between. Thus one should rely on exceptions instead.

With IOException.HResult no longer being protected since .NET 4.5 and C# 6.0 finally supporting exception filters (VB.NET always had this feature) it is now easy to handle typical exceptions like FileNotFound, FileInUse or NetworkNameNoLongerAvailable.

BufferingFileSystemWatcher

My BufferingFileSystemWatcher wraps the standard FSW:

  • Buffers FSW events in a BlockingCollection. It is better to buffer in a BlockingCollection than consuming precious non-paged memory by increasing InternalBufferSize.
  • Supports limiting the BlockingCollection.BoundedCapacity via the EventQueueSize property. Must be set before EnableRaisingEvents=True!
  • Offers reporting existing files via a new event Existed. Existing files are reported before any ones detected by NTFS events.
  • Offers sorting events by oldest (existing) file first. Gets enabled when subscribing to events Existed or All
  • Offers a new event All reporting all FSW events. Real-world apps typically subscribe to all change types because the FSW change types triggered often do not correspond to the action of the producer. Ex: On saving changes Excel triggers 8 events for 3 different files with no(!) change event for the changed file, see picture above.
  • Offers the Error event in Win Forms designer.
  • Wraps FSW via composition not breaking its API. Thus you can simply replace your FileSystemWatcher instances with BufferingFileSystemWatcher and your InternalBufferOverflowException are gone without increasing InternalBufferSize.

The following listing shows key parts of the BufferingFileSystemWatcher in C# 6.0:

public class BufferingFileSystemWatcher : Component
  {
  private FileSystemWatcher _containedFSW = null;
  ...
  public BufferingFileSystemWatcher()
    {
      _containedFSW = new FileSystemWatcher();
    }
    ...
  public bool EnableRaisingEvents
  {
    get
    {
      return _containedFSW.EnableRaisingEvents;
    }
    set
    {
      if (_containedFSW.EnableRaisingEvents == value) return;
	  
      StopRaisingBufferedEvents();
      _cancellationTokenSource = new CancellationTokenSource();
  
      _containedFSW.EnableRaisingEvents = value;
      if (value) RaiseBufferedEventsUntilCancelled();
    }
  }
  ...
  public event FileSystemEventHandler All
  {
    add
   {
     if (_onAllChangesHandler == null)
     {
       _containedFSW.Created += BufferEvent;
       _containedFSW.Changed += BufferEvent;
       _containedFSW.Renamed += BufferEvent;
       _containedFSW.Deleted += BufferEvent;
    }
    _onAllChangesHandler += value;
  }
  ...
  private void BufferEvent(object _, FileSystemEventArgs e)
  {
    if (!_fileSystemEventBuffer.TryAdd(e))
    {
      var ex = new EventQueueOverflowException($"Event queue size {_fileSystemEventBuffer.BoundedCapacity} events exceeded.");
      InvokeHandler(_onErrorHandler, new ErrorEventArgs(ex));
    }
  }
  ...
  private void RaiseBufferedEventsUntilCancelled()
  {
    Task.Run(() =>;
      {
        try
        {
          if (_onExistedHandler != null || _onAllChangesHandler != null)
            NotifyExistingFiles();
  
          foreach (FileSystemEventArgs e in _fileSystemEventBuffer.GetConsumingEnumerable(_cancellationTokenSource.Token))
          {
            if (_onAllChangesHandler != null)
              InvokeHandler(_onAllChangesHandler, e);
            else
            {
              switch (e.ChangeType)
              {
              case WatcherChangeTypes.Created:
                InvokeHandler(_onCreatedHandler, e);
                break;
              case WatcherChangeTypes.Changed:
                InvokeHandler(_onChangedHandler, e);
                break;
              case WatcherChangeTypes.Deleted:
                InvokeHandler(_onDeletedHandler, e);
                break;
              case WatcherChangeTypes.Renamed:
                InvokeHandler(_onRenamedHandler, e as RenamedEventArgs);
                break;

        catch (OperationCanceledException)
         { } //ignore
        catch (Exception ex)
         {
           BufferingFileSystemWatcher_Error(this, new ErrorEventArgs(ex));

  
  private void NotifyExistingFiles()
  {
    if (OrderByOldestFirst)
    {
      var searchSubDirectoriesOption = (IncludeSubdirectories) ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
      var sortedFileNames = from fi in new DirectoryInfo(Path).GetFiles(Filter, searchSubDirectoriesOption)
          orderby fi.LastWriteTime ascending
          select fi.Name;
      foreach (var fileName in sortedFileNames)
      {
        InvokeHandler(_onExistedHandler, new FileSystemEventArgs(WatcherChangeTypes.All, Path, fileName));
        InvokeHandler(_onAllChangesHandler, new FileSystemEventArgs(WatcherChangeTypes.All, Path, fileName));
      }
    }
    else
    {
      foreach (var fileName in Directory.EnumerateFiles(Path))
      {
        InvokeHandler(_onExistedHandler, new FileSystemEventArgs(WatcherChangeTypes.All, Path, fileName));
        InvokeHandler(_onAllChangesHandler, new FileSystemEventArgs(WatcherChangeTypes.All, Path, fileName));
...

RecoveringFileSystemWatcher

My RecoveringFileSystemWatcher wraps the BufferingFileSystemWatcher:

  • Detects and reports watch path accessibility problems. Using a poll timer monitoring the watch path and the FSW Error event. For Robustness restarting from the Error event is not done directly but also done via the timer!
  • Automatically recovers from watch path accessibility problems. By restarting the BufferingFileSysteWatcher. New files created during the outage are reported via the Existed event.
  • Allows consumer to cancel auto recovery for selected exceptions using e.Handled=True.

The following listing shows key parts of the RecoveringFileSystemWatcher:

public class RecoveringFileSystemWatcher : BufferingFileSystemWatcher
{
  public TimeSpan DirectoryMonitorInterval = TimeSpan.FromMinutes(5);
  public TimeSpan DirectoryRetryInterval = TimeSpan.FromSeconds(5);
  private System.Threading.Timer _monitorTimer = null;
  ...
  public new bool EnableRaisingEvents
  {
    get { return base.EnableRaisingEvents; }
    set
    {
    if (value == EnableRaisingEvents) return;
    
      base.EnableRaisingEvents = value;
      if (EnableRaisingEvents)
      {
        base.Error += BufferingFileSystemWatcher_Error;
        Start();
      }
      else
      {
        base.Error -= BufferingFileSystemWatcher_Error;
      }
  
  private void Start()
  {
    try 
    {
      _monitorTimer = new System.Threading.Timer(_monitorTimer_Elapsed);
      
      Disposed += (_, __) =>;
      {
        _monitorTimer.Dispose();
        _trace.Info("Obeying cancel request");
      };
      
      ReStartIfNeccessary(TimeSpan.Zero);
  
  private void _monitorTimer_Elapsed(object state)
  {
    try
    {
      if (!Directory.Exists(Path))
      {
       throw new DirectoryNotFoundException($"Directory not found {Path}");
      }
      else
      {
        _trace.Info($"Directory {Path} accessibility is OK.");
        if (!EnableRaisingEvents)
        {
          EnableRaisingEvents = true;
          if (_isRecovering)
            _trace.Warn("<== Watcher recovered");
        }
        ReStartIfNeccessary(DirectoryMonitorInterval);
      }
    }
    catch (Exception ex) when (
      ex is FileNotFoundException
      || ex is DirectoryNotFoundException)
    {
      if (ExceptionWasHandledByCaller(ex))
        return;
    
      if (_isRecovering)
      {
        _trace.Warn("...retrying");
      }
      else
      {
       _isRecovering = true;
      }
    
      EnableRaisingEvents = false;
      _isRecovering = true;
      ReStartIfNeccessary(DirectoryRetryInterval);
    }
    catch (Exception ex)
    {
      _trace.Error($"Unexpected error: {ex}");
      throw;

  private void ReStartIfNeccessary(TimeSpan delay)
  {
    try
    {
      _monitorTimer.Change(delay, Timeout.InfiniteTimeSpan);
    }
    catch (ObjectDisposedException)
      { } //ignore timer disposed
    }
    
  private void BufferingFileSystemWatcher_Error(object sender, ErrorEventArgs e)
  {
    var ex = e.GetException();
    if (ExceptionWasHandledByCaller(e.GetException()))
        return;
  
    EnableRaisingEvents = false;
   
    if (ex is InternalBufferOverflowException || ex is EventQueueOverflowException)
    {
      ReStartIfNeccessary(DirectoryRetryInterval);
    }
    else if (ex is Win32Exception && (ex.HResult == NetworkNameNoLongerAvailable | ex.HResult == AccessIsDenied))
      ReStartIfNeccessary(DirectoryRetryInterval);
    }

The following picture shows a console trace of the RecoveringFileSystemWatcher working and auto recovering:

FSWTestGTrace

RecoveringFileSystemWatcher working and auto recovering

About Peter Meinl

IT Consultant
This entry was posted in Computers and Internet and tagged , , . Bookmark the permalink.

8 Responses to Tamed FileSystemWatcher

  1. Peter,

    Thanks for sharing!

    I noticed that delete handler is not working, It is due to missing: _onDeletedHandler += value in delete event handler.

    Regards,
    Michal

  2. Bart says:

    Is there a chance that the dlls are going to be available via nuget?

  3. Joy says:

    Hi Peter,
    Thanks for sharing this very useful application.
    I have created a filewatcher service which was failing when there were more then 40 files. This has been resolved by using your BufferingFileSystemWatcher API in my application. I have tested with more than 200 files and they all processed! But the processing time increased significantly. Is there any place in you code I may look into to improve the performance?

    Thanks a lot!
    Joy

  4. Anghell says:

    Hi Peter, Thanks for sharing!
    Greetings from Mexico City!
    Angel

  5. Pingback: Thoughts and Experiments about Cloud Encryption | Peter Meinl: Software Development Tips

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s