Introduction to File System Monitoring

Microsoft Windows operating systems are made up of different subsystems. One of these subsystems handles and controls the file system. Any operation performed by the user that involves files is supervised and controlled by this subsystem. One of the many functions of the Windows File Subsystem is to notify third-party applications of any file activity done. This notification is received as an event message. The event message is placed in the OS internal message queue. That is, every time a file is created, deleted, moved, or updated a number of events are raised by the operating system. These events can be handled by applications to perform any other task. For example, Windows Explorer listens for these events to refresh the list of files inside the currently opened directory. Sometimes we would like to be notified when a file of our interest is modified. For example, if our application makes use of a particular file to keep settings we would like the application to get notified if the file was modified outside the application.

In .NET framework 1.1, a new class that monitors file subsystem events is introduced. The class is called FileSystemWatcher and resides under the System.IO namespace. By creating an instance of the FileSystemWatcher one is able to listen for file events and perform any required action.

The FileSystemWatcher has a number of properties that control the behaviour of the class instance. For better understanding of how to use the FileSystemWatcher we will build a simple application that logs the events raised by files in a particular directory. The example provided in this article is based on the example found in MSDN.

Initialising an instance of FileSystemWatcher

Initialising an instance of FileSystemWatcher is no different than creating a new instance of the class. FileSystemWatcher has 3 constructors to choose from. The choice of the constructor does not affect the class execution but they provide quick setup of some basic properties. For the purpose of this article, we will use the parameterless constructor. This helps us to explain better the class properties.

  1. FileSystemWatcher fileSystemWatcher = new FileSystemWatcher();

Now that the class instance has been created we define which path (directory) to monitor. The path property needs to be specified irrespective of whether we want to specify a single file or not. The reason is that the file system watcher is designed to watch changes in directories. By default the Path property is set to an empty string. This property must be set, otherwise an ArgumentException will be thrown.

  1. fileSystemWatcher.Path = <directory path>;

Filtering the monitoring events

Monitoring a directory for changes will raise an event for every change within the directory. It will not raise events for changes within subdirectories. To monitor subdirectories the FileSystemWatcher instance needs to be instructed that subdirectories needs to be monitored. The monitoring of subdirectories is done by setting the property IncludeSubdirectories to true.

Once the path to be monitored is specified we can control which events we want to be notified of. The notification filtering is made up of two different types. The first type filters activity that relates to files or directories attribute changes. The attributes to monitor can be selected with a bitwise OR operation on a set of enumeration flags defined in NotifyFilters. The default attributes that are monitored are LastWrite, FileName and DirectoryName. LastWrite is changed every time a file is modified, while FileName and DirectoryName are triggered when changes to a file or a directory name are performed.

The second filter type provides a regular expression against which file names are compared. When a specific file needs to be monitored the filter is set to the file name. To monitor all files the filter must be set to an empty string. Otherwise, wildcards can be used to monitor a group of files. For example, to monitor text files the filter must be set to “*.txt”. Note that the character “*” in the filter is a wildcard specifying that the name of the file can be anything.

To illustrate the filtering in practice we will monitor the files residing in the temporary folder on all attributes.

  1. fileSystemWatcher.Path = System.IO.Path.GetTempPath();
  2. fileSystemWatcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.Attributes | NotifyFilters.Size | NotifyFilters.LastWrite | NotifyFilters.LastAccess | NotifyFilters.CreationTime | NotifyFilters.Security;
  3. fileSystemWatcher.Filter = string.Empty;

Handling events

Once the file system watcher has been initialised we need to handle the events that are received. There are four types of events that can be handled: Created, Deleted, Changed, or Renamed. In our example we will log the notification changes to a console window. Since the Created, Deleted, and Changed event types use the same delegate definition we can link them to the same event handler.

  1. fileSystemWatcher.Changed += new FileSystemEventHandler(FileSystemWatcher_Changed);
  2. fileSystemWatcher.Created += new FileSystemEventHandler(FileSystemWatcher_Changed);
  3. fileSystemWatcher.Deleted += new FileSystemEventHandler(FileSystemWatcher_Changed);
  4. fileSystemWatcher.Renamed += new RenamedEventHandler(FileSystemWatcher_Renamed);
  5. ...
  6. private void FileSystemWatcher_Changed(
  7.     object sender, 
  8.     FileSystemEventArgs e)
  9. {
  10.     System.Console.WriteLine(
  11.         "{0} – [{1}] {2}", 
  12.         DateTime.Now, e.ChangeType.ToString(), e.FullPath);
  13. }
  14.  
  15. private void FileSystemWatcher_Renamed(
  16.     object sender, 
  17.     RenamedEventArgs e)
  18. {
  19.     System.Console.WriteLine(
  20.         "{0} – [{1}] {2} renamed to {3}", 
  21.         DateTime.Now, e.ChangeType.ToString(), e.OldFullPath, e.FullPath);
  22. }

A word of caution – Loosing event notifications

When creating an instance of the FileSystemWatcher the instance registers with the Windows File Subsystem to receive the file events. As the Windows File Subsystem is not aware of the filters set in the FileSystemWatcher all events will be sent to the instance. As for each event received the FileSystemWatcher needs to perform a number of actions the event received is queued in an internal buffer, as shown in Figure 1. Once the internal buffer contains an event the FileSystemWatcher iterates on the buffer and pops an event. The event read is compared with the notification filters and if the event passes the filters test the method linked with the notification is performed.

Figure 1 - File event notification flow inside the FileSystemWatcher

Figure 1 - File event notification flow inside the FileSystemWatcher


To minimise the loss of events, the FileSytemWatcher class provides a property to set the internal buffer. The internal buffer is a queue where events are stored until they are processed. When the internal buffer limit is reached any further events are discarded to protect against any buffer overflows.

  1. fileSystemWatcher.InternalBufferSize = 65536; // 64 KB – the maximum allowed buffer size.

Starting the FileSystemWatcher

The FileSystemWatcher does not start listening to events immediately. For the FileSystemWatcher to start listening to the events the property EnableRaisingEvents needs to be set to true. It is important to note that during the assignment a number of checks are done at runtime. These checks ensure that the FileSystemWatcher instance is initialised and is monitoring a valid location.

  1. try
  2. {
  3.     fileSystemWatcher.EnableRaisingEvents = true;
  4. }
  5. catch (ObjectDisposedException odex)
  6. {
  7.     // The FileSystemWatcher object has been disposed.
  8.     System.Console.WriteLine(odex.Message);
  9. }
  10. catch (PlatformNotSupportedException pnsex)
  11. {
  12.     // The current operating system is not Microsoft Windows NT or later.
  13.     System.Console.WriteLine(pnsex.Message);
  14. }
  15. catch (FileNotFoundException fnfex)
  16. {
  17.     // The directory specified in Path could not be found.
  18.     System.Console.WriteLine(fnfex.Message);
  19. }
  20. catch (ArgumentException aex)
  21. {
  22.     // Path has not been set or is invalid.
  23.     System.Console.WriteLine(aex.Message);
  24. }

To stop the system watcher the code above is repeat but instead of setting the EnableRaisingEvents property to true the property is set to false.

Cleaning up the FileSystemWatcher object

The FileSystemWatcher makes use of methods and objects that reside outside the memory controlled by the garbage collector. As the memory cannot be cleaned by the garbage collector it is important that the memory is properly disposed. The FileSystemWatcher provides a Dispose() method that cleans up the memory used by the class instance.

  1. fileSystemWatcher.Dispose();
  2. fileSystemWatcher = null;

References

MSDN: FileSystemWatcher Class
MSDN: NotifyFilters Enumeration