How to Build a 'Folder Watcher' Service in C#

Build a service that uses .NET's FileSystemWatcher object to monitor changes to files and folders in a directory

The Windows Task Scheduler has come far since its humble beginnings in Windows 2000. In addition to letting you schedule tasks on a recurring schedule, Task Scheduler 2.0 allows you to launch a process depending on a number of events, including custom ones. However, one thing that you can't do via a scheduled task is react to file system events. For those, you need to use the FileSystemWatcher object that comes with the .NET Framework. The FileSystemWatcher object allows you to monitor changes within a directory, specifically when files or folders are created, modified, deleted, or renamed. In this article, I will explain how to create a Windows service in C# that uses the FileSystemWatcher object to provide more granular file system monitoring than Task Scheduler allows. I'll build the service using SharpDevelop 4.0, but you can use Visual Studio or another preferred IDE to do so.

Windows Services vs. Scheduled Tasks

Although there is some overlap between scheduled tasks and Windows services (originally introduced in Windows NT as NT Services), they are two distinct programming APIs. The Task Scheduler 2.0 API exposes the functionality of the Task Scheduler so that you can dynamically create, modify, and remove scheduled tasks. Hence, the API's capabilities are limited to whatever you can do via the Task Scheduler Window. .NET Windows services take over where scheduled tasks leave off, giving us access to folder-specific events.

Unlike a scheduled task, a service is a program that executes in the background in its own Windows session. The function of services is to monitor applications and/or folders as well as react to events. Services do not usually include a GUI component because they are meant to run unattended. In fact, Windows services do not even require a user to be logged on to Windows. This allows us to configure services to start automatically and even execute before the logon screen comes up. Any process output is generally logged to the event log, a file, or a database or emailed to support personnel.

Creating the Project

Although I will be using SharpDevelop for the purposes of this article, feel free to use whatever IDE you're comfortable with. I chose SharpDevelop for two reasons: First, it's free; second, it has a project template for creating Windows services. Some free C# IDEs, including Visual Studio Express, don't have a Windows services template, so you have to do some extra work to set up your project files. You can download the latest version of SharpDevelop here.

Because FileSystemWatcher is part of the .NET libraries, you must have .NET installe­d on your machine to use the class. SharpDevelop 4.1 was built to run with the .NET 3.5 SP1 and .NET 4 runtimes; the SharpDevelop download page provides links to the required .NET runtime downloads on the Microsoft Download Center website.

If you want, you can download both the Windows SDK and the .NET 4 Framework together (via a link on the SharpDevelop download page). SharpDevelop can utilize the Windows SDK's collection of tools, compilers, headers, libraries, code samples, and Help system.

We are now ready to create the project, by performing the following tasks:

  1. Launch SharpDevelop and select File, New, Solution from the main menu, to display the New Project dialog.
  2. Expand the C# item and click the Windows Applications folder in the left pane.
  3. A number of templates for various Windows apps are displayed, as shown in Figure 1. The template we want is the last one, Windows Service. Select it and place the cursor in the Name text box.

Figure 1: Selecting a template to create the Folder Watcher project
Figure 1: Selecting a template to create the Folder Watcher project

  1. Name the project CsharpFolderWatcher. The solution name will be set to the same name by default.
  2. Click the Create button to create the project/solution.

The advantage of starting from a template is that the IDE will create the entire project framework for you, including the basic source code, imports, and support files, as shown in Figure 2.

Figure 2: CsharpFolderWatcher project files
Figure 2: CsharpFolderWatcher project files

Coding the CsharpFolderWatcher.cs File

The IDE has created a basic working service class for you, including the service name, a constructor, an initializer function, and the OnStart(), OnStop(), and Dispose() event handlers. (If the CsharpFolderWatcher.cs file is not already open in the main coding pane, just drag it over or double-click it to open the file.) Note that the class inherits from ServiceBase, which contains the methods required to correctly operate a Windows service:

public class CsharpFolderWatcher : ServiceBase 

Using the FileSystemWatcher Class

To configure an instance of the FileSystemWatcher component, we need to do three things:

  1. Instantiate a new FileSystemWatcher object.
  2. Configure necessary properties and methods for the component.
  3. Create handlers for FileSystem events.

The FileSystemWatcher is part of the System.IO namespace, so you'll have to add the following using statement to the top of the file, if your IDE has not already done so:

using System.IO; 

Setting Properties

There are several properties that you set on your FileSystemWatcher component instances to determine how they behave. These properties determine what directories and subdirectories the component instance will monitor and the exact occurrences within those directories that will raise events. Here are the properties you need to set:

  • Path: Sets or gets the path of the directory to watch. The Path property indicates the fully qualified path of the root directory you want to watch. This can be a standard Windows path (C:\directory) or in Universal Naming Convention (UNC) format (\\server\directory).
  • EnableRaisingEvents: Enables or disables the FileSystemWatcher component.
  • Filter: Gets or sets the filter string; used to determine what files are monitored in a directory. Filter accepts the same wildcards as Windows search (e.g., "?" for one character, "*" for many characters). The default value is all files (*.*). Note that the use of multiple filters such as "*.txt|*.doc" is not allowed.
  • IncludeSubdirectories: A flag for including subdirectories in the monitored path.
  • InternalBufferSize: Gets or sets the size of the internal buffer. The default is 8,192 bytes, or 8KB. You can set the buffer from 4KB to 64KB. If you try to set the InternalBufferSize property to less than 4,096 bytes, your value will be discarded and the InternalBufferSize property will automatically be set to 4,096 bytes. For best performance, it is recommended to use multiples of 4KB.
  • NotifyFilter: Gets or sets the type of changes to watch for, as enumerated in the NotifyFilters collection. The default is the bitwise OR combination of LastWrite, FileName, and DirectoryName.

Choosing Constructors

There are three constructors to choose from, with zero to two arguments. The following example initializes a basic new FileSystemWatcher instance:

FileSystemWatcher myWatcher = new FileSystemWatcher();

This code uses one argument to initialize a new instance with a Path property:

FileSystemWatcher myWatcher = new FileSystemWatcher("c:\\");

The following two-argument constructor initializes a new instance with both Path and Filter properties:

FileSystemWatcher myWatcher = new FileSystemWatcher("c:\\", "*.txt");

Types of Changes You Can Watch For

You can combine the members of the NotifyFilters enumeration to watch for more than one kind of change. For example, you can watch for changes in the size of a file or folder and for changes in security settings. Figure 3 lists all the NotifyFilter values.

Member name Description
Figure 3: NotifyFilter values
FileName Name of the file
DirectoryName Name of the directory
Attributes Attributes of the file or folder
Size Size of the file or folder
LastWrite Last date the file or folder had anything written to it
LastAccess Last date the file or folder was opened
CreationTime Time the file or folder was created
Security Security settings of the file or folder

Our class will combine four NotifyFilters as follows:

watcher.NotifyFilter = NotifyFilters.LastAccess
       | NotifyFilters.LastWrite
       | NotifyFilters.FileName
       | NotifyFilters.DirectoryName;

Checking the Event Type After the Fact

FileSystem events possess a ChangeType property that corresponds to the WatcherChangeTypes enumeration. There's even a toString() method to convert the change type to a readable description, as Figure 4 shows.

//This method is called when a file is created, changed, or deleted.
private static void OnChanged(object source, FileSystemEventArgs e)
{
    //Show that a file has been created, changed, or deleted.
    WatcherChangeTypes wct = e.ChangeType;
    Console.WriteLine("File {0} {1}", e.FullPath, wct.ToString());
}

In addition to helping with logging, the WatcherChangeTypes enumeration can help you code your responses with more precision -- for instance, separating folder changes from file changes, as shown in Figure 5.

protected void FileCreated(object sender, FileSystemEventArgs e)
{
    if (e.ChangeType == WatcherChangeTypes.Created)
    {
    if (Directory.Exists(e.FullPath))
        // a directory
    else
        // a file
    }
}

While we're on the subject of folder changes, it should be noted that because the FileSystemWatcher class watches for changes within a directory and not changes to the root directory's attributes themselves, you can't watch for changes to the root folder. For example, if you are monitoring a folder called C:\My Documents, the class will not respond if you rename it to C:\My Docs. It will, however, continue to monitor whatever files and folders are created and/or contained therein. That happens because the watched directory is based on a handle rather than on the directory's name. Thus, renaming either the root folder or any subdirectories will not break the watcher.

The OnStart and OnStop Events

The OnStart event specifies what actions to take when the service starts. It fires when a Start command is sent to the service by the Windows Service Control Manager (SCM) or, in the case of a service that starts automatically, when the operating system starts.

After we instantiate a new watcher and tell it what to look for using the various ChangeType flags, we can specify the actions to take by assigning event handlers to a number of FileSystemWatcher events, which include changed, created, deleted, and renamed. In this application, we are using the same event handler for creation, change, and deletion because we are logging each of these changes in the same way. The specific ChangeType is fetched from the event handler's Event parameter.

There are two kinds of built-in event handlers: FileSystemEventHandler and RenamedEventHandler. Each of these accepts a pointer to the event handler function as its argument. In C#, all that's required is the function name. Simple!Finally, we have to turn on the FileSystemWatcher by setting EnableRaisingEvents to true. We can do that in the OnStart event because we are activating it right away, as shown in Figure 6.

public const string MyServiceName = "CsharpFolderWatcher";
private FileSystemWatcher watcher = null;
      
protected override void OnStart(string[] args)
{
    this.ServiceName = MyServiceName;
    // Create a new FileSystemWatcher with the path
    //and text file filter
    FileSystemWatcher watcher = new FileSystemWatcher("c:\\", "*.txt");

//Watch for changes in LastAccess and LastWrite times, and
    //the renaming of files or directories.
    watcher.NotifyFilter = NotifyFilters.LastAccess
             | NotifyFilters.LastWrite
             | NotifyFilters.FileName
             | NotifyFilters.DirectoryName;

// Add event handlers.
    watcher.Changed += new FileSystemEventHandler(OnChanged);
    watcher.Created += new FileSystemEventHandler(OnChanged);
    watcher.Deleted += new FileSystemEventHandler(OnChanged);
    watcher.Renamed += new RenamedEventHandler(OnRenamed);

// Begin watching.
    watcher.EnableRaisingEvents = true;
}

Likewise, the OnStop event fires when a Stop command is sent to the service by the SCM. We can turn off the FileSystemWatcher here by setting EnableRaisingEvents to false, as shown in Figure 7.

protected override void OnStop()
{
    watcher.EnableRaisingEvents = false;
    watcher.Dispose();

LogEvent("Monitoring Stopped");
}

void OnChanged(object sender, FileSystemEventArgs e) {
    string mgs = string.Format("File {0} | {1}",
               e.FullPath, e.ChangeType);
    LogEvent(msg);
}

void OnRenamed(object sender, RenamedEventArgs e) {
    string log = string.Format("{0} | Renamed from {1}",
               e.FullPath, e.OldName);
    LogEvent(log);
}

Writing Events to the Windows Event Log

As mentioned earlier, Window services do not usually interact with a user, so whatever output is produced is typically written to some sort of log. One good place to log to is the Windows event log. To save the Folder Watcher service output to the event log, first ensure that the following using directive appears at the top of the code in your FolderWatcher class:

using System.Diagnostics;

To simplify the code, we will create one method to log events, shown in Figure 8.

private void LogEvent(string message)
{
    string eventSource = "File Monitor Service";
    DateTime dt = new DateTime();
    dt = System.DateTime.UtcNow;
    message = dt.ToLocalTime() + ": " + message;

EventLog.WriteEntry(eventSource, message);
}

Our logging method calls the one-argument WriteEntry signature, passing in a message string. There are several other overloaded WriteEntry methods that accept additional parameters, including an EventLogEntryType of error, warning, information, success audit, or failure audit as well as an application-defined event ID.

The ProjectInstaller File

This file is required to install the CsharpFolderWatcher Windows service on the target PC. In it, you need to instantiate one ServiceProcessInstaller instance per service application and one ServiceInstaller instance for each service in the application. Figure 9 shows the code that installs the service.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration.Install;
using System.ServiceProcess;

namespace CsharpFolderWatcher
{
    [RunInstaller(true)]
    public class ProjectInstaller : Installer
    {
    private ServiceProcessInstaller serviceProcessInstaller;
    private ServiceInstaller serviceInstaller;
  
    public ProjectInstaller()
    {
        serviceProcessInstaller = new ServiceProcessInstaller();
        serviceInstaller = new ServiceInstaller();
        // Here you can set properties on serviceProcessInstaller
        //or register event handlers
        serviceProcessInstaller.Account = ServiceAccount.LocalService;
            serviceInstaller.ServiceName = CsharpFolderWatcher.MyServiceName;
        this.Installers.AddRange(new Installer[] {
        serviceProcessInstaller, serviceInstaller });
        }
    }
}

The ServiceProcessInstaller installs an executable containing classes that extend ServiceBase. This class is called by installation utilities, such as installutil.exe, when installing a service application. It is used by the installation utility to write registry values associated with services you want to install.

Normally, you do not call the methods on ServiceInstaller within your code; they are generally called only by the install utility, which automatically calls the ServiceProcessInstaller.Install() and ServiceInstaller.Install() methods during the installation process. If the installation fails, ServiceInstaller calls the Rollback() method for all previously installed components.

ServiceInstaller performs work specific to the service with which it is associated. It is used by the installation utility to write registry values associated with the service to a subkey within the HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services registry key. The service is identified by its ServiceName within this subkey. The subkey also includes the name of the executable or DLL to which the service belongs.

Other Files

The CsharpFolderWatcher project includes several additional files. The program.cs file contains the program's Main() method. In it, there is a call to the ServiceBase's static Run method, passing in a new ServiceBase array that instantiates our CsharpFolderWatcher class. If you created the project using a Windows Service template, you will already have this code. Otherwise, you'll have to create the file, using the code in Figure 10.

using System;
using System.Collections.Generic;
using System.ServiceProcess;
using System.Text;

namespace CsharpFolderWatcher
{
    static class Program
    {
    /// 
    /// This method starts the service.
    /// 
    static void Main()
    {
        // To run more than one service you have to add them here
        ServiceBase.Run(new ServiceBase[] { new CsharpFolderWatcher() });
    }
    }
}

Configuration files are XML files that can be changed as needed. Developers can use configuration files to change settings without recompiling applications, while administrators can use them to set policies that affect how applications run on their computers.
The top-level element for all configuration files is the element. At the next level, the element specifies the version of the Common Language Runtime (CLR) that should run the application and contains the and/or elements, as shown in Figure 11.



    
    
    

The AssemblyInfo file, shown in Figure 12, contains component information, including the product Name, Description, Trademark, and Copyright.
 

#region Using directives

using System;
using System.Reflection;
using System.Runtime.InteropServices;

#endregion

// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("CsharpFolderWatcher")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("CsharpFolderWatcher")]
[assembly: AssemblyCopyright("Copyright 2011")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// This sets the default COM visibility of types in the assembly to invisible.
// If you need to expose a type to COM, use [ComVisible(true)] on that type.
[assembly: ComVisible(false)]

// The assembly version has following format:
//
// Major.Minor.Build.Revision
//
// You can specify all the values or you can use the default the Revision and
// Build Numbers by using the '*' as shown below:
[assembly: AssemblyVersion("1.0.*")]

After compilation, the file contents become part of the assembly so that the information can be read at runtime. Each of the Assembly attributes has a defined class, which is used to read the information from the AssemblyInfo file.

Building and Installing the Service

The .NET Framework comes with the handy installutil.exe utility to install your services. It accepts an /i flag for installing and /u for uninstalling.

If you don't add the installutil.exe utility to your PATH environment variable, you'll have to include the entire path in your DOS command. Here is what mine looks like:

C:\My Documents\SharpDevelopProjects\
   CsharpFolderWatcher\Debug>C:\WINDOWS\
   Microsoft.NET\Framework\v2.0.50727\
   installutil /i CsharpFolderWatcher.exe

To simplify installation of the service, you can create a simple batch file like the one shown in Figure 13.

@ECHO OFF
REM The following directory is for .NET 2.0
set DOTNETFX2=%SystemRoot%\Microsoft.NET\Framework\v2.0.50727
set PATH=%PATH%;%DOTNETFX2%

echo Installing WindowsService...
echo ---------------------------------------------------
InstallUtil /i 
echo ---------------------------------------------------
echo Done.

To uninstall the service, simply create another batch file called uninstall.bat with exactly the same script, only substituting the /i (for install) with /u (for uninstall).

Configuring Access Privileges for Your Service

The ServiceProcessInstaller has an Account property that sets the security context of the service being installed. The security context determines the privileges a service has on both the local system and on the network. The ServiceAccount provides four enumerated constants with a range of privileges from basic to highly elevated.

In earlier versions of Windows, most system services ran under the über-powerful SYSTEM account. Since most services do not require such an elevated privilege level, Microsoft created the two new lower-level accounts, LocalService and NetworkService. Should your service require higher-access privileges, you can use the LocalSystem account; it has extensive privileges on the local computer and presents the computer's credentials to remote servers. To specify the exact privileges you require for a particular service, set the ServiceProcessInstaller.Account to User. Then the system will use the ServiceProcessInstaller's Username and Password properties (if set) or prompt for a username and password when the service is installed. <

Testing the Service

To test the Folder Watcher service, follow these steps:

1. Open the Services Window via the Start Menu, Control Panel, Administrative Tools, Services.

2. Open the Event Viewer from Administrative Tools to see the log entries.

Now try starting the service. You should find that a new event is logged and can be viewed using the Event Viewer. Try creating, modifying, renaming, and deleting files to see further events logged.

Uninstalling the Service

When you no longer need the service, you can uninstall it. Be sure to stop the service first. You can then remove it by executing the same command from a DOS window as you did to install it, except with the /u flag instead of /i:

installutil /u FileMonitorService.exe

More Control over Folder Monitoring

As you've seen, the .NET FileSystemWatcher object is a useful tool for anyone who requires fine-grained folder monitoring. Although coding a service requires slightly more technical proficiency than setting up scheduled tasks, a service can be developed by anyone who possesses some degree of programming know-how. By using an IDE such as SharpDevelop or any version of Visual Studio other than Visual Studio Express, which provides a template for Windows services, there is even less programming to do as it will lay out all the groundwork for you.

Rob Gravelle resides in Ottawa, Canada, and is the founder of GravelleConsulting.com. Rob has built systems for intelligence-related organizations such as Canada Border Services and for commercial businesses. In his spare time, Rob has become an accomplished guitar player and has released several CDs.

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish