Understanding Directory Monitoring

By: mykle hoban

Abstract: Creating a ListBox Component that will automatically monitor the contents of a directory and update itself when it changes.


Using a Custom ListBox Component to Illustrate Directory Monitoring



Have you ever wondered how it is that shell windows and some applications can (almost magically, it seems) know when the contents of a directory has changed? (No, they don't sit there looping with an endless if statement!) The answer is actually quite simple: it is through the use of a function called FindFirstChangeNotification. The concept of it is simple enough, but the actual implementation takes a bit of explaining.

Table Of Contents
Objective
Concept
Implementation
Project Source and Test App

Objective

The purpose of this article is to describe the concepts and implementation of directory monitoring. I will explain how it works and provide sample code for a custom component that uses it. I am assuming that you have a grasp of the following concepts:

  • Borland C++Builder
  • Component Creation
  • Win32 API

Concept

The concept behind directory monitoring is fairly simple. As I said above, this can be done using the function FindFirstChangeNotification. That said and done, how do we go about actually using the thing? First lets look at the prototype, as given by Microsoft:

HANDLE FindFirstChangeNotification(
  LPCTSTR lpPathName,    // directory name
 
BOOL bWatchSubtree,    // monitoring option
 
DWORD dwNotifyFilter   // filter conditions
);


Parameters
 lpPathName: the path of the directory to monitor
 bWatchSubtree: whether or not to recursively monitor subdirectories
 dwNotifyFilter: flags to control monitoring conditions

The options available for dwNotifyFilter are as follows:

ValueMeaning
FILE_NOTIFY_CHANGE_FILE_NAME Any file name change in the watched directory or subtree causes a change notification wait operation to return. Changes include renaming, creating, or deleting a file name.
FILE_NOTIFY_CHANGE_DIR_NAME Any directory-name change in the watched directory or subtree causes a change notification wait operation to return. Changes include creating or deleting a directory.
FILE_NOTIFY_CHANGE_ATTRIBUTES Any attribute change in the watched directory or subtree causes a change notification wait operation to return.
FILE_NOTIFY_CHANGE_SIZE Any file-size change in the watched directory or subtree causes a change notification wait operation to return. The operating system detects a change in file size only when the file is written to the disk. For operating systems that use extensive caching, detection occurs only when the cache is sufficiently flushed.
FILE_NOTIFY_CHANGE_LAST_WRITE Any change to the last write-time of files in the watched directory or subtree causes a change notification wait operation to return. The operating system detects a change to the last write-time only when the file is written to the disk. For operating systems that use extensive caching, detection occurs only when the cache is sufficiently flushed.
FILE_NOTIFY_CHANGE_SECURITY Any security-descriptor change in the watched directory or subtree causes a change notification wait operation to return.


The return value of this function is a windows HANDLE type. It gives us a Windows event object, which is an object that can be set to have a signalled state. Using this event handle, we can call the WaitForSingleObject or WaitForMultipleObjects functions to wait until it is signalled (the monitored directory has changed in some way). Once the event becomes signalled (i.e. a change was detected), it is necessary to call the FindNextChangeNotification. function to reset it. When you are finished, you must free the object with FindCloseChangeNotification

One way to utilize this function in a component is to put it within a thread with a loop that calls FindFirstChangeNotification, WaitForSingleObject, FindNextChangeNotification, and FindCloseChangeNotification. Every time the event object becomes signalled, we can call a function in the ListBox component that will notify it of change.


Implementation

In implementing a directory monitoring and auto-updating file ListBox, there are a couple of things to keep in mind: how to monitor the directory without locking your application, and how to notify the control.One relatively simple way of accomplishing this is to create a thread that monitors the directory and calls back to the ListBox component when there is a change. In the following source, both code for a ListBox and for the monitoring thread are provided.

Here is the code for the component:

ActiveListBox.h
//---------------------------------------------------------------------------

#ifndef ActiveListBoxH
#define ActiveListBoxH
//---------------------------------------------------------------------------
#include <SysUtils.hpp>
#include <Controls.hpp>
#include <Classes.hpp>
#include <Forms.hpp>
#include <StdCtrls.hpp>
//---------------------------------------------------------------------------

//Here we combine the flags into one easy-to-use dword
const unsigned long dwFlags = FILE_NOTIFY_CHANGE_FILE_NAME|
                            FILE_NOTIFY_CHANGE_DIR_NAME|
                            FILE_NOTIFY_CHANGE_ATTRIBUTES|
                            FILE_NOTIFY_CHANGE_SIZE|
                            FILE_NOTIFY_CHANGE_LAST_WRITE;
class TActiveListBox;

class TDirectoryThread : public TThread
{
private:
  HANDLE hNotify;    //Event object handle
  TActiveListBox *control;    //ListBox back pointer (to notify it)
  String path;    //Path to monitor
  void __fastcall Update();
  void __fastcall ThreadFinish(TObject *Sender);
protected:
  void __fastcall Execute();
public:
  __fastcall TDirectoryThread(TActiveListBox *ctrl);
  void __fastcall SetMonitorPath(String path);
};
//---------------------------------------------------------------------------
class PACKAGE TActiveListBox : public TListBox
{
private:
  bool path_set;
  TNotifyEvent FOnChangeFile;    //Change event
  TDirectoryThread *thrd;    //Monitoring thread
  AnsiString FPath;
  void __fastcall SetPath(String path);
  void __fastcall PopulateFileList();
protected:
public:
  __fastcall TActiveListBox(TComponent* Owner);
  __fastcall ~TActiveListBox();
  void __fastcall UpdateNotify();
  void __fastcall Restart();
__published:
  __property AnsiString MonitorPath = {read=FPath,write=SetPath};
  __property TNotifyEvent OnDirectoryChange = {read=FOnChangeFile,write=FOnChangeFile};
};
//---------------------------------------------------------------------------
#endif

ActiveListBox.cpp
//---------------------------------------------------------------------------

#include <vcl.h>
#pragma hdrstop

#include "ActiveListBox.h"
#pragma package(smart_init)
//---------------------------------------------------------------------------
// ValidCtrCheck is used to assure that the components created do not have
// any pure virtual functions.
//

static inline void ValidCtrCheck(TActiveListBox *)
{
  new TActiveListBox(NULL);
}
//---------------------------------------------------------------------------
__fastcall TActiveListBox::TActiveListBox(TComponent* Owner)
  : TListBox(Owner)
{
  path_set=false;                     //boolean to check if path has been set
  FPath="";
  FOnChangeFile=NULL;
  thrd = new TDirectoryThread(this);  //create thread
}
//---------------------------------------------------------------------------
__fastcall TActiveListBox::~TActiveListBox()
{
  thrd->Terminate();
}
//---------------------------------------------------------------------------
void __fastcall TActiveListBox::SetPath(String path)
{
  FPath = path;
  PopulateFileList();
  thrd->SetMonitorPath(FPath);
  if (!path_set)                //if the path hasn't been set yet...
  {
    path_set=true;
    thrd->Resume();             //start the thread
  }
}
//---------------------------------------------------------------------------
void __fastcall TActiveListBox::UpdateNotify() //notify the listbox of changes
{
  PopulateFileList();
  if (FOnChangeFile)
    FOnChangeFile(this);
}
//---------------------------------------------------------------------------
void __fastcall TActiveListBox::PopulateFileList() //fill list box w/ files
{
  TSearchRec sr;
  Items->Clear();
  if (FPath == "") return;
  FindFirst(FPath+"*.*",faAnyFile,sr);
  do {
    Items->Add(sr.Name);
  } while (!FindNext(sr));
  FindClose(sr);
}
//---------------------------------------------------------------------------
void __fastcall TActiveListBox::Restart()
{
  path_set=false;
  thrd = new TDirectoryThread(this);
}
//---------------------------------------------------------------------------
namespace Activelistbox
{
  void __fastcall PACKAGE Register()
  {
     TComponentClass classes[1] = {__classid(TActiveListBox)};
     RegisterComponents("Samples", classes, 0);
  }
}
//---------------------------------------------------------------------------
__fastcall TDirectoryThread::TDirectoryThread(TActiveListBox *ctrl)
  : TThread(true)
{
  control = ctrl;
  path = "";
  FreeOnTerminate = true;
  OnTerminate = ThreadFinish;
  hNotify = INVALID_HANDLE_VALUE;
}
//---------------------------------------------------------------------------
void __fastcall TDirectoryThread::Execute()
{
  if (path.IsEmpty() || (hNotify == INVALID_HANDLE_VALUE))
  {
    control->Restart();
    throw Exception("couldn't start");
  }
  bool to=true;  //time-out check
  //while we're still going...
  while (!Terminated && (hNotify != INVALID_HANDLE_VALUE))
  {
    //while we're still timed out...
    while (to && !Suspended && !Terminated)
    {
      unsigned long stat = WaitForSingleObject(hNotify,2000);
      //our call to WaitForSingleObject has a 2 second timeout to avoid thread-lock
      if (stat == WAIT_OBJECT_0) //if it got signalled
      {
        to=true;
        Synchronize(Update());  //update list box
        //use Synchronize to keep it thread-safe
        if (!FindNextChangeNotification(hNotify)) //renew the handle
        {
          to=false;
          break;
        }
      }
      else if (stat == WAIT_TIMEOUT)      //just keep going
        to=true;
      else {
        to=false;
        break;
      }
    }
  }
}
//---------------------------------------------------------------------------
void __fastcall TDirectoryThread::SetMonitorPath(String Path)
{
  if (path != Path)
  {
    path = Path;
    if (hNotify != INVALID_HANDLE_VALUE)    //close our old handle
      FindCloseChangeNotification(hNotify);

    hNotify = FindFirstChangeNotification(path.c_str(),false,dwFlags);
    //get the handle to the event object
    if (hNotify == INVALID_HANDLE_VALUE)
      throw Exception("couldn't make handle");
  }
}
//---------------------------------------------------------------------------
void __fastcall TDirectoryThread::ThreadFinish(TObject *Sender)
{
  FindCloseChangeNotification(hNotify); //cleanup
}
//---------------------------------------------------------------------------
void __fastcall TDirectoryThread::Update()
{
  control->UpdateNotify();
}

Back to top

Server Response from: ETNASC03