Filesystem Messages

Problem: Salespeople upload their order files to a specific directory. Each time a file arrives, you want to add an entry to a report file available via the company intranet.

The task requires you to watch a directory and take some action when it changes. For a standard application, you might grab the directory contents and cache them in an array, then using a timer, check periodically to see if the directory contents have changed by comparing the current contents with the cached contents. Unfortunately, doing that is both resource intensive (especially if the directory is large) and prone to error, because if your application shuts down, you run the risk of missing directory changes.

Another way to check would be to create a Web Form that someone could run periodically and that, again, would check to see if the directory contents had changed. But that method is also prone to error, mostly by omission. The person designated to perform the task might be absent, might forget, or might be unable to run the page for some other reason.

You may have questions about the task, such as "Why is this a messaging task? Isn't this a timing and processing task?" The answer is that it is a messaging task because you use an instance of a .NET class called FileSystemWatcher to watch the directory for you. That's a change in perspective. Rather than your checking the directory for changes, the operating system can send a message when the directory changes, at which point you can initiate the appropriate action. You use a FileSystemWatcher to do this.

The FileSystemWatcher can watch for several different types of changes, including deletions and additions, modifications to existing files, attribute changes, file and directory name changes, file date alterations, file size changes, or changes to the file and folder security settings. You can elect to watch an entire directory and all of its subdirectories, a single directory, or a set of files (via a filter) within a directory on a local drive, network drive, or a remote machine. In other words, the FileSystemWatcher is extremely flexible.

But that doesn't answer yet another question: "How can you accomplish this task with an ASP.NET application—which may or may not be running at any given time?" Remember that an ASP.NET application shuts down after the last session times out or is abandoned. Unfortunately, the flexibility of the FileSystemWatcher class doesn't alter the fact that your application may or may not be running. You have to face facts—your application can't perform any work if it's not running; therefore, you need to look outside the ASP.NET framework for this type of functionality. The best way to watch a directory continuously is with a Windows Service, so you need to write a service that interacts with your application by running code when a pertinent directory change event occurs.

Writing a service application is extremely similar to writing any other application type in .NET. I'll run through this briefly because it's not the focus of this book, but it is such a common problem that it's worth including here.

Start a new Windows Service project in VS.NET and call it CSharpDirectoryWatcherService. When you create a new service application, VS.NET automatically creates a servicel.cs class file, which inherits from System.ServiceProcess.ServiceBase and creates two stub event subroutines— OnStart and OnStop among others. Windows uses the name of the class that defines the service as the service name by default, so you should rename the service.cs file DirectoryWatcher.cs. Note that both the namespace and the class itself are named CSharpDirectoryWatcherService. You need to edit the code to change the names as well as rename the file. The following code illustrates (but is not a direct copy of) the default code VS.NET creates for the new class.

namespace CSharpDirectoryWatcherService {

public class CSharpDirectoryWatcherService : System.ServiceProcess.ServiceBase {

protected override void OnStart(string[] args) {

/// Set things in motion so your service can do its work.

protected override void OnStop() {

// Add code here to perform any tear-down necessary // to stop your service.

Assume that the salespeople save their files to the CSharpASP\chll\orders_in folder. On my machine, that directory is c:\inetpub\wwwroot\CSharpASP\chll\orders_in. Whenever a file arrives, the DirectoryWatcher service should move the file to the folder c:\inetpub\wwwroot\CSharpASP\chll\orders_out and log the file arrival and its destination.

Because you know in advance which directory you want to watch, you can hard-code the name of the directory you want to watch into the code.

Note I don't normally recommend that you hard-code the path to any external resource, but in this particular instance, it's better to have the service fail than have an administrator accidentally enter the wrong name.

The service defines two private variables to hold the names—m_pathin and m_pathout—a variable called fsw that holds a FileSystemWatcher instance, and two DirectoryInfo variables that correspond to the file paths.

using System.ServiceProcess; using System.IO;

public class CSharpDirectoryWatcherService : System.ServiceProcess.ServiceBase {

/// Required designer variable. /// </summary>

private System.ComponentModel.Container components = null;

FileSystemWatcher fsw;

private static String m pathin =

"c:\\inetpub\\wwwroot\\CSharpASP\\ch11\\ orders in"; private static String m pathout =

"c:\\inetpub\\wwwroot\\CSharpASP\\ch11\\ orders out"; DirectoryInfo di_in = new DirectoryInfo

(m pathin); DirectoryInfo di_out = new DirectoryInfo

// more code here

It's worth looking at the autogenerated code. Click the plus (+) sign next to the line Component Designer Generated Code to expand the region. Note that the IDE creates a main method that defines the services to run in this namespace—yes, the plural is correct—you can define more than one service in a namespace.

The ServiceBase class has several events that you can handle to define how your service responds to system events. In particular, you should override or at least consider overriding the OnStart, OnStop, OnPause, and OnContinue events. The DirectoryWatcher service overrides all these events.

Because services don't have a visual interface, you need to create a way for an administrator to know whether the service is running. The easiest way is to write to a log file—this service uses the standard Event log application file. At minimum, you should log an event when the service starts or stops, but you don't have to write the code to do that—the ServiceBase class has a Boolean property called AutoLog that, when true, writes the entries for you automatically. The AutoLog property's default value is true, but in the example I've turned it off because the component writes custom messages when its status changes. You can set the AutoLog property in the OnStart event code.

base.AutoLog = false;

You wouldn't want your service to fail if the directories don't exist, so the first task is to check the orders_in and orders_out directories and create them if they don't already exist. The code that creates the directories and writes the log entries should look familiar if you read Chapter 10, "File and Event Log Access with ASP.NET." Create a Directoryinfo object bound to the m_pathin variable that defines the input path. Check to see if the directory exists inside a try block. If not, create the directory. Repeat the operation with the m_pathout directory. If any operation fails, write a message to the Event log. If both directories exist or can be created, you can create the FileSystemWatcher object and set its properties (see the highlighted code, below).

protected override void OnStart(string[] args) { base.AutoLog = false; try {

// create the orders_in directory if it doesn't already exist if (!di in.Exists) { di in.Create();

EventLog.WriteEntry

("CSharpDirectoryWatcherService", "DirectoryWatcher starting."); // create a FileSystemWatcher fsw = new FileSystemWatcher(m_pathin); fsw.Created += new

System.IO.FileSystemEventHandler (this.fsw created);

// don't watch subdirectories fsw.IncludeSubdirectories = false;

// enable watching fsw.EnableRaisingEvents = true;

EventLog.WriteEntry

("CSharpDirectoryWatcherService", "Service started successfully.");

EventLog.WriteEntry

("CSharpDirectoryWatcherService", "Service starting.");

catch (Exception exl) { EventLog.WriteEntry

("CSharpDirectoryWatcherService", "Unable to watch the directory: " + m pathin + System.Environment.NewLine + "Error Description: " + exl.Message);

catch (Exception ex) { EventLog.WriteEntry

("CSharpDirectoryWatcherService", "Unable to create the directory " + m pathout + System.Environment.NewLine + "Error Description: " + ex.Message);

catch (Exception ex) {

EventLog.WriteEntry("CSharpDirectoryWatcherService", "Unable to create the directory " + m pathin + System.Environment.NewLine + "Error Description: " + ex.Message);

Most of the code consists of error trapping because you don't want to let a service fail without generating some type of error message. The FileSystemWatcher constructor requires a string parameter containing the name of the directory you want to watch.

fsw = new FileSystemWatcher(m pathin);

In this case, you don't want to watch subdirectories, so turn off the IncludeSubdirectories property:

// don't watch subdirectories fsw.IncludeSubdirectories = false;

You must enable the FileSystemWatcher using the EnableRaisingEvents property before it will raise any events.

// enable watching fsw.EnableRaisingEvents = true;

Both the OnStop and OnStart event handlers override ServiceBase class methods. Administrators can pause and continue a service via the Windows Service applet interface. The DirectoryWatcher service pauses the FileSystemWatcher when the overridden OnPause event fires and restarts it when the overridden OnContinue event fires. It logs both events.

protected override void OnPause() { fsw.EnableRaisingEvents = false;

EventLog.WriteEntry("CSharpDirectoryWatcherService",

"Service paused for path " + m pathin);

protected override void OnContinue() {

fsw.EnableRaisingEvents = true; EventLog.WriteEntry("CSharpDirectoryWatcherService", "Service continued for path " + m pathin);

When the service stops for any reason, the service calls the Dispose method and then releases the FileSystemWatcher by setting it to Nothing. The only directory change that this service traps is the created event.

When a salesperson adds a new file to the orders_in directory, the service moves the file to the orders_out directory and then logs the move in a file called report.txt. The FileSystemEventArgs parameter contains the Name and Fullname property values of the file that was just created. I've removed the error-trapping and cleanup code from the snippet below so it's easier to read.

private void fsw created(Object sender, FileSystemEventArgs e) { // move the file

Filelnfo fi = new Filelnfo(e.FullPath); StreamWriter sw; try {

fi.MoveTo(m_pathout + "\\" + e.Name); // add to processed file report try {

fi = new FileInfo(m pathout + "\\report.txt"); if (!fi.Exists) { _

sw.WriteLine(e.FullPath + "," + m_pathout + "\\" +

catch {

EventLog.WriteEntry("CSharpDirectoryWatcherService", "Unable to write to the file " + fi.FullName);

finally {

// do nothing here

catch {

EventLog.WriteEntry("CSharpDirectoryWatcherService",

"Unable to create or append to the file " + m pathout + "\\" + e.Name);

catch {

EventLog.WriteEntry("CSharpDirectoryWatcherService", "Unable to move the file " + e.FullPath + " to " + m_pathout + "\\" + e.Name);

After you complete the service code, save and compile it, and then follow this procedure to make it run as a service.

1. Select the DirectoryWatcher.cs class and then double-click the class in the Solution Explorer to switch to Design mode.

2. In Design view, below the Properties window, click the Add Installer link. Clicking the link adds two components: a ServiceProcessInstaller and a ServiceInstaller. There will be one ServiceInstaller for each service exposed by your project (see Figure 11.1).

Figure 11.1: DirectoryWatcher-Service project in Design mode after clicking the Add Installer link

3. Select the serviceInstaller1 item from the Design window. In the Properties window, set the

ServiceName property to CSharpDirectoryWatcherService (the name may already be set). This is the name that appears in the Event log by default.

4. Set the StartType property to Automatic. The StartType property is a ServiceStartMode enumeration that controls whether the service starts automatically when the server boots. Other possible settings are Manual (you must start the service through the Services applet) and Disabled, which prevents the service from running.

5. Click the ServiceProcessInstaller1 component and set the Account property to LocalSystem. The Account property controls the account under which your service runs.

6. Compile the service. Compiling a service application generates an EXE file.

7. Visual Studio ships with a utility named InstallUtil.exe that installs and uninstalls .NETgenerated services. Run the InstallUtil.exe utility using the full path to the EXE file you generated in step 6. For example, on my machine you would use the following one-line command at a command prompt to install the service (watch out for line breaks in the code below—the command is a single line).

Warning Your path will differ from the path shown below. Make sure you enter the correct path. Enter the command on one line in the Command window.

c:\WINNT\Microsoft.NET\Framework\v1.0.3512\InstallUtil.exe "c:\documents and settings\administrator.RUSSELL-DUAL\ My Documents\Visual Studio Projects\DirectoryWatcher\bin\Debug\ CSharpDirectoryWatcherService.exe"

8. Open the Services applet by clicking Start ® Programs ® Administrative Tools ® Services. Find the CSharpDirectoryWatcherService entry, right-click it, and then select Start from the context menu. Your service will start.

If your service does not start, or if it doesn't work the way you expect, you'll need to debug it. Debugging a service is slightly different from the way you've debugged Web Forms so far. To debug a service, you need to manually attach the debugger to the service process. To do that, load the service project into Visual Studio. Click the Debug menu, and select the Processes entry. You'll see the Processes dialog containing a list of processes running on your server (see Figure 11.2).

Figure 11.2: VS.NET debugger Processes dialog

Click the Show System Processes check box to force the dialog to show all the system processes. Select the CSharpDirectoryWatcherService.exe entry, then click the Attach button. You'll see the Attach to Process dialog (see Figure 11.3).

Figure 11.3: VS.NET debugger Attach to Process dialog

Check the Common Language Runtime entry and then click OK to close the dialog. Click Close to close the Processes dialog. After attaching to the process, you can set breakpoints and debug normally. If you need to make changes, follow these steps:

■ Stop the service using the Services applet.

■ Close the Services applet (you won't be able to uninstall the service with the Services applet open).

■ From a command prompt, run the InstallUtil.exe utility with a -u (for uninstall) option:

c:\WINNT\Microsoft.NET\Framework\v1.0.3512\InstallUtil.exe -u "c:\documents and settings\administrator.RUSSELL-DUAL\ My Documents\Visual Studio Projects\DirectoryWatcher\ bin\debug\DirectoryWatcher.exe

Warning Your path will differ from the preceding path. Make sure you enter the correct path.

Listing 11.1 shows the complete code for the DirectoryWatcherService.cs class. Unlike many code examples in this book, I've included the generated code in the listing because the comments explain how to generate an assembly that includes more than one service. Note that this code was compiled in Debug mode—you would definitely want to switch this off for a production version and use the .exe file generated in the Release folder rather than the Debug folder.

Listing 11.1: The Complete CSharpDirectoryWatcherService Code (DirectoryWatcher.cs)

using System;

using System.Collections;

using System.ComponentModel;

using System.Data;

using System.Diagnostics;

using System.ServiceProcess;

using System.IO;

namespace CSharpDirectoryWatcherService {

public class CSharpDirectoryWatcherService : System.ServiceProcess.ServiceBase {

/// Required designer variable. /// </summary>

private System.ComponentModel.Container components = null; private FileSystemWatcher fsw; private static String m pathin =

"c:\\inetpub\\wwwroot\\CSharpASP\\ch11 \\orders in"; private static String m pathout =

"c:\\inetpub\\wwwroot\\CSharpASP\\ch11\\ orders out"; Directoryinfo di in = new Directoryinfo

(m pathin); Directoryinfo di out = new Directoryinfo

public CSharpDirectoryWatcherService() {

// This call is required by the Windows.Forms // Component Designer. InitializeComponent();

// TODO: Add any initialization after the InitComponent call

// The main entry point for the process static void Main() {

System.ServiceProcess.ServiceBase[] ServicesToRun;

// More than one user Service may run within the // same process. To add

// another service to this process, change the // following line to create a second service object.

// ServicesToRun = New System.

ServiceProcess.ServiceBase[] {new Service1(), new MySecondUserService()};

ServicesToRun = new System.ServiceProcess.ServiceBase[] { new CSharpDirectoryWatcherService() };

System.ServiceProcess.ServiceBase.Run(ServicesToRun);

/// Required method for Designer support - do not modify

/// the contents of this method with the code editor. /// </summary>

private void InitializeComponent() { //

// CSharpDirectoryWatcherService //

this.CanShutdown = true;

this.ServiceName = "CSharpDirectoryWatcherService";

/// Clean up any resources being used. /// </summary>

protected override void Dispose( bool disposing ) { if( disposing ) {

if (components != null) { components.Dispose();

base.Dispose( disposing );

/// Set things in motion so your service can do its work.

protected override void OnStart(string[] args) { base.AutoLog = false; try {

// create the orders in directory if it // doesn't already exist if (!di in.Exists) { di in.Create();

EventLog.WriteEntry("CSharpDirectoryWatcherService", "DirectoryWatcher starting."); // create a FileSystemWatcher fsw = new FileSystemWatcher(m pathin); fsw.Created += new

System.IO.FileSystemEventHandler(this.fsw created);

// don't watch subdirectories fsw.IncludeSubdirectories = false; // enable watching fsw.EnableRaisingEvents = true;

EventLog.WriteEntry("CSharpDirectoryWatcherService", "Service started successfully.");

EventLog.WriteEntry

("CSharpDirectoryWatcherService", "Service starting.");

catch (Exception ex1) { EventLog.WriteEntry

("CSharpDirectoryWatcherService", "Unable to watch the directory: " + m pathin + System.Environment.NewLine + "Error Description: " + exl.Message);

catch (Exception ex) { EventLog.WriteEntry

("CSharpDirectoryWatcherService", "Unable to create the directory " + m pathout + System.Environment.NewLine + "Error Description: " + ex.Message);

catch (Exception ex) { EventLog.WriteEntry

("CSharpDirectoryWatcherService", "Unable to create the directory "

+ m pathin + System.Environment.NewLine +

"Error Description: " + ex.Message);

protected override void OnStop() { // Add code here to perform any tear-down necessary // to stop your service. // log a stop event try {

fsw.Dispose();

EventLog.WriteEntry

("CSharpDirectoryWatcherService", "Service stopped");

catch (Exception ex) { EventLog.WriteEntry

("CSharpDirectoryWatcherService", "Error stopping the

CSharpDirectoryWatcherService " + for path: " + m pathin +

System.Environment.NewLine + "Error Description: " + ex.Message);

protected override void OnPause() { fsw.EnableRaisingEvents = false; EventLog.WriteEntry

("CSharpDirectoryWatcherService", "Service paused for path " + m pathin);

protected override void OnContinue() { fsw.EnableRaisingEvents = true; EventLog.WriteEntry

("CSharpDirectoryWatcherService",

"Service continued for path " + m pathin);

private void fsw created(Object sender,

FileSystemEventArgs e)

Filelnfo fi = new Filelnfo(e.FullPath); StreamWriter sw; try {

fi.MoveTo(m_pathout + "\\" + e.Name); // add to processed file report try {

fi = new FileInfo(m pathout +

sw.WriteLine(e.FullPath + "," + m_pathout + "\\" + e.Name);

catch {

EventLog.WriteEntry

("CSharpDirectoryWatcherService", "Unable to write to the file " + fi.FullName);

finally {

// do nothing here

catch {

EventLog.WriteEntry

("CSharpDirectoryWatcherService", "Unable to create or append to the file " + m_pathout + "\\" + e.Name);

catch {

EventLog.WriteEntry ("CSharpDirectoryWatcherService", "Unable to move the file " + e.FullPath + " to " + m_pathout + "\\" + e.Name);

When you start the service, you can see immediately if it worked, because it creates the orders_in and orders_out subdirectories under the ch11 directory.

To test the service, save a file in the orders_in directory. The file should immediately disappear. Look in the orders_out directory. The service will move the file there and create the report.txt file.

The method used for creating the report file is not a good example of production-quality work, for at least two reasons. The first reason is that there's not enough security on the service or the report file. For example, what happens if someone deletes the report file? There goes your data. In a production application, you should use an account that has specific rights to the directories and files involved and no more. Then you can set the report file permissions so that only administrators and the system account can write to or delete the file, and so that the IIS service can read it. Second, the report itself doesn't contain enough data to be very useful. For example, it doesn't contain the file dates, the ID of the user who created the file in the orders_in folder, or any other useful data besides the filename itself. However, it provides a reasonable example to show how you might begin to go about using the report file to produce a useful report.

Now that the service is working, the last part of the task is to make the data available as a report. It's questionable whether using a flat file for the report data is a good idea. When there are only a few entries in the report, it's no problem to read them, format the data as HTML, and send it to a browser. However, as the number of entries in the report grows, showing all the rows becomes less and less useful. There are several ways to circumvent this problem; perhaps the best way is to log the entries in a database, where you can use the database engine's select capability to select the most recent entries. But databases require a lot of overhead, whereas appending data to a file requires very little.

In a real application, there would be little point in showing all the entries. Typically, you would want to display the entries in reverse order and display only the last 10, 50, or 100 entries—whatever seemed the most reasonable. A simple sequential file, like the report.txt file generated by the service, consists of records delimited by carriage returns. So you could read the file into an array of strings, where each string would contain one record. In this example, the records themselves consist solely of the input filename and the output filename, so you can use the regular expression Split function to isolate the individual data items and generate a report. The Web Form chii-i.aspx contains a simple report that shows all the data (see Listing 11.2).

Listing 11.2: Report That Shows All Data Collected by the DirectoryWatcher Service (ch11-

1.aspx.cs)

private void Page Load(object sender, System.EventArgs e) { // Read the report.txt file StreamReader sr; String s;

String[] lines, orders;

sr = File.OpenText(Server.MapPath(".") + "\\orders out\\report.txt"); s = sr.ReadToEnd().Trim(); sr.Close();

Response.Write("<table border=\"1\">");

Response.Write("<tr><th>Order In</th><th>Order Out</th></tr>"); foreach (String aString in lines) { orders = aString.Split(',');

Response.Write("<tr><td>" + orders[0] + "</td><td>" + orders[1] + "</td></tr>");

Obviously, as the number of records grows, this method will quickly become unwieldy. The Web Form chll-2.aspx contains a better version of the report. It opens the report.txt file in read-only binary mode, grabs the last 2,000 bytes of the file, splits the records, reverses them, and displays those as a table. Listing 11.3 shows the code.

Listing 11.3: Report That Shows All Data Collected by the DirectoryWatcher Service (ch11-

2.aspx.cs)

private void Page Load(object sender, System.EventArgs e) { // Read the report.txt file FileStream fs; String s;

String[] lines; String[] orders; StreamReader sr;

fs = File.Open(Server.MapPath(".") + "\\orders_out\Xreport.txt", FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite); if (fs.Length > 2000) {

fs.Seek(-2000, SeekOrigin.End);

sr = new StreamReader(fs); s = (sr.ReadToEnd()).Trim(); sr.Close(); fs.Close();

Response.Write("<table border=\"1\">");

Response.Write("<tr><th>Order In</th><th>Order Out</th></tr>");

// don't use the first line // and show items in reverse for (int i = lines.Length - 1; i > 0; i--) { orders = lines[i].Split(','); if (orders.Length > 1) {

Response.Write("<tr><td>" + orders[0] + "</td><td>" + orders[1] + "</td></tr>");

Of course, this entire operation would be much easier if the service saved the report data as XML. You'll see more about that later in this book; it's time to move on to other message types.

Was this article helpful?

0 0

Post a comment