Click here to Skip to main content
15,880,608 members
Articles / Desktop Programming / MFC

CDirectoryChangeWatcher - ReadDirectoryChangesW all wrapped up

Rate me:
Please Sign up or sign in to vote.
4.92/5 (114 votes)
11 May 2002 2.1M   39.4K   365  
This class wraps up ReadDirectoryChangesW.
// DirectoryChanges.cpp: implementation of the CDirectoryChangeWatcher and CDirectoryChangeHandler classes.
//
///////////////////////////////////////////////////////////////////
///

/***********************************************************

  Author:	Wes Jones wesj@hotmail.com

  File:		DirectoryChanges.cpp

  Latest Changes:

		11/22/2001	--	Fixed bug causing file name's to be truncated if
						longer than 130 characters. Fixed CFileNotifyInformation::GetFileName()
						Thanks to Edric(uo_edric@hotmail.com) for pointing this out.

						Added code to enable process privileges when CDirectoryChangeWatcher::WatchDirectory() 
						is first called.	See docuementation API for ReadDirectoryChangesW() for more information of required privileges.
						
						  Currently enables these privileges: (11/22/2001)
							SE_BACKUP_NAME
							SE_CHANGE_NOTIFY_NAME 
						Implemented w/ helper class CPrivilegeEnabler.

		11/23/2001		Added classes so that all notifications are handled by the
						same thread that called CDirectoryChangeWatcher::WatchDirectory(),
						ie: the main thread.
						CDirectoryChangeHandler::On_Filexxxx() functions are now called in the 
						context of the main thread instead of the worker thread.

						This is good for two reasons:
						1: The worker thread spends less time handling each notification.
							The worker thread simply passes the notification to the main thread,
							which does the processing.
							This means that each file change notification is handled as fast as possible
							and ReadDirectoryChangesW() can be called again to receive more notifications
							faster.

						2:	This makes it easier to make an ActiveX or ATL object with this class
							because the events that are fired, fire in the context of the main thread.
							The fact that I'm using a worker thread w/ this class is totally 
							transparent to a client user.
							If I decide to turn this app into an ActiveX or ATL object
							I don't have to worry about wierd COM rules and multithreading issues,
							and neither does the client, be the client a VB app, C++ app, Delphi app, or whatever.

						Implemented with CDelayedDirectoryChangeHandler in DirectoryChangeHandler.h/.cpp


************************************************************/

#include "stdafx.h"
#include "DirectoryChanges.h"
#include "DelayedDirectoryChangeHandler.h"

#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif
//
//
//	Fwd Declares & #define's
//
//
//
// Helper classes
class CPrivilegeEnabler;	 //for use w/ enabling process priveledges when this code is first used.
class CFileNotifyInformation;//helps CDirectoryChangeWatcher class notify CDirectoryChangeHandler class of changes to files. It's a singleton.

class CDelayedDirectoryChangeHandler;	//	Helps all notifications become handled by the main
										//	thread by posting messages to a window created in the main
										//	thread.
										//	Message is dispatched when message is handled by
										//  that window.
										//
										//  The 'main' thread is the one that called CDirectoryChangeWatcher::WatchDirectory()

// Helper functions
static BOOL	EnablePrivilege(LPCTSTR pszPrivName, BOOL fEnable = TRUE);
static bool IsDirectory(const CString & strPath);
/////////////////////////////////////////////////////////////////////
//	Helper functions.
BOOL	EnablePrivilege(LPCTSTR pszPrivName, BOOL fEnable /*= TRUE*/) 
//
//	I think this code is from a Jeffrey Richter book...
//
//	Enables user priviledges to be set for this process.
//	
//	Process needs to have access to certain priviledges in order
//	to use the ReadDirectoryChangesW() API.  See documentation.
{    
	BOOL fOk = FALSE;    
	// Assume function fails    
	HANDLE hToken;    
	// Try to open this process's access token    
	if (OpenProcessToken(GetCurrentProcess(), 		
					TOKEN_ADJUST_PRIVILEGES, &hToken)) 	
	{        
		// privilege        
		TOKEN_PRIVILEGES tp = { 1 };        

		if( LookupPrivilegeValue(NULL, pszPrivName,  &tp.Privileges[0].Luid) )
		{
			tp.Privileges[0].Attributes = fEnable ?  SE_PRIVILEGE_ENABLED : 0;

			AdjustTokenPrivileges(hToken, FALSE, &tp, 			      
									sizeof(tp), NULL, NULL);

			fOk = (GetLastError() == ERROR_SUCCESS);		
		}
		CloseHandle(hToken);	
	}	
	return(fOk);
}

static bool IsDirectory(const CString & strPath)
//
//  Returns: bool
//		true if strPath is a path to a directory
//		false otherwise.
//
{
	DWORD dwAttrib	= GetFileAttributes( strPath );
	return static_cast<bool>( ( dwAttrib != 0xffffffff 
							&&	(dwAttrib & FILE_ATTRIBUTE_DIRECTORY)) );

		
}
///////////////////////////////////////////////
//Helper class:

class CFileNotifyInformation 
/*******************************

A Class to more easily traverse the FILE_NOTIFY_INFORMATION records returned 
by ReadDirectoryChangesW().

FILE_NOTIFY_INFORMATION is defined in Winnt.h as: 

 typedef struct _FILE_NOTIFY_INFORMATION {
    DWORD NextEntryOffset;
	DWORD Action;
    DWORD FileNameLength;
    WCHAR FileName[1];
} FILE_NOTIFY_INFORMATION, *PFILE_NOTIFY_INFORMATION;	

  ReadDirectoryChangesW basically puts x amount of these records in a 
  buffer that you specify.
  The FILE_NOTIFY_INFORMATION structure is a 'dynamically sized' structure (size depends on length
  of the file name (+ sizeof the DWORDs in the struct))

  Because each structure contains an offset to the 'next' file notification
  it is basically a singly linked list.  This class treats the structure in that way.
  

  Sample Usage:
  BYTE Read_Buffer[ 4096 ];

  ...
  ReadDirectoryChangesW(...Read_Buffer, 4096,...);
  ...

  CFileNotifyInformation notify_info( Read_Buffer, 4096);
  do{
	    switch( notify_info.GetAction() )
		{
		case xx:
		    notify_info.GetFileName();
		}

  while( notify_info.GetNextNotifyInformation() );
  
********************************/
{
public:
	CFileNotifyInformation( BYTE * lpFileNotifyInfoBuffer, DWORD dwBuffSize)
	: m_pBuffer( lpFileNotifyInfoBuffer ),
	  m_dwBufferSize( dwBuffSize )
	{
		ASSERT( lpFileNotifyInfoBuffer && dwBuffSize );
		
		m_pCurrentRecord = (PFILE_NOTIFY_INFORMATION) m_pBuffer;
	}

	
	BOOL GetNextNotifyInformaion();
	
	BOOL CopyCurrentRecordToBeginningOfBuffer(DWORD & dwSizeOfCurrentRecord);

	DWORD	GetAction() ;//gets the type of file change notifiation
	CString GetFileName();//gets the file name from the FILE_NOTIFY_INFORMATION record
	CString GetFileNameWithPath(const CString & strRootPath);//same as GetFileName() only it prefixes the strRootPath into the file name

	
protected:
	BYTE * m_pBuffer;//<--all of the FILE_NOTIFY_INFORMATION records 'live' in the buffer this points to...
	DWORD  m_dwBufferSize;
	PFILE_NOTIFY_INFORMATION m_pCurrentRecord;//this points to the current FILE_NOTIFY_INFORMATION record in m_pBuffer
	
};

BOOL CFileNotifyInformation::GetNextNotifyInformaion()
/***************
  Sets the m_pCurrentRecord to the next FILE_NOTIFY_INFORMATION record.

  Even if this return FALSE, (unless m_pCurrentRecord is NULL)
  m_pCurrentRecord will still point to the last record in the buffer.
****************/
{
	if( m_pCurrentRecord 
	&&	m_pCurrentRecord->NextEntryOffset != 0UL)//is there another record after this one?
	{
		//set the current record to point to the 'next' record
		m_pCurrentRecord = (PFILE_NOTIFY_INFORMATION) ((LPBYTE)m_pCurrentRecord + m_pCurrentRecord->NextEntryOffset);

		ASSERT( (DWORD)((BYTE*)m_pCurrentRecord - m_pBuffer) < m_dwBufferSize);//make sure we haven't gone too far
					
		return TRUE;
	}
	return FALSE;
}

BOOL CFileNotifyInformation::CopyCurrentRecordToBeginningOfBuffer(DWORD & dwSizeOfCurrentRecord)
/*****************************************
   Copies the FILE_NOTIFY_INFORMATION record to the beginning of the buffer
   specified in the constructor.

   The size of the current record is returned in DWORD & dwSizeOfCurrentRecord.
   
*****************************************/
{
	ASSERT( m_pBuffer && m_pCurrentRecord );
	if( !m_pCurrentRecord ) return FALSE;

	BOOL bRetVal = TRUE;

	//determine the size of the current record.
	dwSizeOfCurrentRecord = sizeof( FILE_NOTIFY_INFORMATION );
	//subtract out sizeof FILE_NOTIFY_INFORMATION::FileName[1]
	WCHAR FileName[1];//same as is defined for FILE_NOTIFY_INFORMATION::FileName
	UNREFERENCED_PARAMETER(FileName);
	dwSizeOfCurrentRecord -= sizeof(FileName);   
	//and replace it w/ value of FILE_NOTIFY_INFORMATION::FileNameLength
	dwSizeOfCurrentRecord += m_pCurrentRecord->FileNameLength;

	ASSERT( (DWORD)((LPBYTE)m_pCurrentRecord + dwSizeOfCurrentRecord) <= m_dwBufferSize );

	ASSERT( (void*)m_pBuffer != (void*)m_pCurrentRecord );//if this is the case, your buffer is way too small
	if( (void*)m_pBuffer != (void*) m_pCurrentRecord )
	{//copy the m_pCurrentRecord to the beginning of m_pBuffer
		
		ASSERT( (DWORD)m_pCurrentRecord > (DWORD)m_pBuffer + dwSizeOfCurrentRecord);//will it overlap?
		__try{
			memcpy(m_pBuffer, m_pCurrentRecord, dwSizeOfCurrentRecord);
			bRetVal = TRUE;
		}
		__except(EXCEPTION_EXECUTE_HANDLER)
		{
			TRACE(_T("EXCEPTION!  CFileNotifuInformation::CopyCurrentRecordToBeginningOfBuffer() -- probably because bytes overlapped in a call to memcpy()"));
			bRetVal = FALSE;
		}
	}
	//else
	//there was only one record in this buffer, and m_pCurrentRecord is already at the beginning of the buffer
	return bRetVal;
}

DWORD CFileNotifyInformation::GetAction()
{ 
	ASSERT( m_pCurrentRecord );
	if( m_pCurrentRecord )
		return m_pCurrentRecord->Action;
	return 0UL;
}

CString CFileNotifyInformation::GetFileName()
{
	//
	//BUG FIX:
	//		File Name's longer than 130 characters are truncated
	//
	//		Thanks Edric @ uo_edric@hotmail.com for pointing this out.
	if( m_pCurrentRecord )
	{
		WCHAR wcFileName[ MAX_PATH + 1] = {0};//L"";
		memcpy(	wcFileName, 
				m_pCurrentRecord->FileName, 
				//min( MAX_PATH, m_pCurrentRecord->FileNameLength) <-- buggy line
				min( (MAX_PATH * sizeof(WCHAR)), m_pCurrentRecord->FileNameLength));
		//wcFileName[ min( (m_pCurrentRecord->FileNameLength / sizeof(WCHAR)), MAX_PATH) ] = 0;//ensure the file name is null terminated...

		return CString( wcFileName );
	}
	return CString();
}		

CString CFileNotifyInformation::GetFileNameWithPath(const CString & strRootPath)
{
	CString strFileName( strRootPath );
	if( strFileName.Right(1) != _T("\\") )
		strFileName += _T("\\");

	strFileName += GetFileName();
	return strFileName;
}
/////////////////////////////////////////////////////////////////////////////////
class CPrivilegeEnabler
//
//	Enables privileges for this process
//	first time CDirectoryChangeWatcher::WatchDirectory() is called.
//
//	It's a singleton.
//
{
private:
	CPrivilegeEnabler();//ctor
public:
	~CPrivilegeEnabler(){};
	
	static CPrivilegeEnabler & Instance();
	friend static CPrivilegeEnabler & Instance();
};

CPrivilegeEnabler::CPrivilegeEnabler()
{
	LPCTSTR arPrivelegeNames[]	=	{
										SE_BACKUP_NAME,
										SE_CHANGE_NOTIFY_NAME //just to make sure
										//<others here as needed>
									};
	for(int i = 0; i < sizeof(arPrivelegeNames) / sizeof(arPrivelegeNames[0]); ++i)
	{
		if( !EnablePrivilege(arPrivelegeNames[i], TRUE) )
		{
			TRACE(_T("Unable to enable privilege: %s	--	GetLastError(): %d\n"), arPrivelegeNames[i], GetLastError());
			TRACE(_T("CDirectoryChangeWatcher notifications may not work as intended due to insufficient access rights/process privileges.\n"));
			TRACE(_T("File: %s Line: %d\n"), _T(__FILE__), __LINE__);
		}
	}
}

CPrivilegeEnabler & CPrivilegeEnabler::Instance()
{
	static CPrivilegeEnabler theInstance;//constructs this first time it's called.
	return theInstance;
}
//
//
//
///////////////////////////////////////////////////////////

	
//
//
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
CDirectoryChangeHandler::CDirectoryChangeHandler()
: m_nRefCnt( 1 ),
  m_pDirChangeWatcher( NULL ),
  m_nWatcherRefCnt( 0L )
{
}

CDirectoryChangeHandler::~CDirectoryChangeHandler()
{
	UnwatchDirectory();
}

BOOL CDirectoryChangeHandler::UnwatchDirectory()
{
	CSingleLock lock(&m_csWatcher, TRUE);	
	ASSERT( lock.IsLocked() );
	
	if( m_pDirChangeWatcher )
		return m_pDirChangeWatcher->UnwatchDirectory( this );
	return TRUE;
}

long  CDirectoryChangeHandler::ReferencesWatcher(CDirectoryChangeWatcher * pDirChangeWatcher)
{
	ASSERT( pDirChangeWatcher );
	CSingleLock lock(&m_csWatcher, TRUE);
	if( m_pDirChangeWatcher 
	&&  m_pDirChangeWatcher != pDirChangeWatcher )
	{
		TRACE(_T("CDirecotryChangeHandler...is becoming used by a different CDirectoryChangeWatcher!\n"));
		TRACE(_T("Directories being handled by this object will now be unwatched.\nThis object is now being used to ")
			  _T("handle changes to a directory watched by different CDirectoryChangeWatcher object, probably on a different directory"));
		
		if( UnwatchDirectory() )
		{
			m_pDirChangeWatcher = pDirChangeWatcher;
			m_nWatcherRefCnt = 1; //when this reaches 0, set m_pDirChangeWatcher to NULL
			return m_nWatcherRefCnt;
		}
		else
		{
			ASSERT( FALSE );//shouldn't get here!
		}
	}
	else
	{
		ASSERT( !m_pDirChangeWatcher || m_pDirChangeWatcher == pDirChangeWatcher );
		
		m_pDirChangeWatcher = pDirChangeWatcher;	
		
		if( m_pDirChangeWatcher )
			return InterlockedIncrement(&m_nWatcherRefCnt);
		
	}
	return m_nWatcherRefCnt;
}

long CDirectoryChangeHandler::ReleaseReferenceToWatcher(CDirectoryChangeWatcher * pDirChangeWatcher)
{
	ASSERT( m_pDirChangeWatcher == pDirChangeWatcher );
	CSingleLock lock(&m_csWatcher, TRUE);
	long nRef;
	if( (nRef = InterlockedDecrement(&m_nWatcherRefCnt)) <= 0L )
	{
		m_pDirChangeWatcher = NULL; //Setting this to NULL so that this->UnwatchDirectory() which is called in the dtor
									//won't call m_pDirChangeWatcher->UnwatchDirecotry(this).
									//m_pDirChangeWatcher may point to a destructed object depending on how
									//these classes are being used.
		m_nWatcherRefCnt = 0L;
	}
	return nRef;
}

void CDirectoryChangeHandler::On_FileAdded(const CString & strFileName)
{
	TRACE(_T("The following file was added: %s\n"), strFileName);
}

void CDirectoryChangeHandler::On_FileRemoved(const CString & strFileName)
{
	TRACE(_T("The following file was removed: %s\n"), strFileName);
}

void CDirectoryChangeHandler::On_FileModified(const CString & strFileName)
{
	TRACE(_T("The following file was modified: %s\n"), strFileName);
}

void CDirectoryChangeHandler::On_FileNameChanged(const CString & strOldFileName, const CString & strNewFileName)
{
	TRACE(_T("The file %s was RENAMED to %s\n"), strOldFileName, strNewFileName);
}

void CDirectoryChangeHandler::SetChangedDirectoryName(const CString & strChangedDirName)
{
	m_strChangedDirectoryName = strChangedDirName;
}
////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////

CDirectoryChangeWatcher::CDirectoryChangeWatcher()
: m_hCompPort(NULL),
  m_hThread( NULL )
//  m_dwThreadID(0UL)
{

}

CDirectoryChangeWatcher::~CDirectoryChangeWatcher()
{

	UnwatchAllDirectories();

	if( m_hCompPort )
	{
		CloseHandle( m_hCompPort );
	}
}

BOOL CDirectoryChangeWatcher::IsWatchingDirectory(const CString & strDirName)
/*********************************************
  Determines whether or not a directory is being watched
**********************************************/
{
	CSingleLock lock( &m_csDirWatchInfo, TRUE);
	ASSERT( lock.IsLocked() );
	
	CDirWatchInfo * pDirInfo;
	int max = m_DirectoriesToWatch.GetSize();
	for(int i = 0; i < max; ++i)
	{
		if( (pDirInfo = m_DirectoriesToWatch[i]) != NULL 
		&&  pDirInfo->m_strDirName.CompareNoCase( strDirName ) == 0 )
			return TRUE;
	}
	return FALSE;
}

DWORD CDirectoryChangeWatcher::WatchDirectory(const CString & strDirToWatch, 
									   DWORD dwChangesToWatchFor, 
									   CDirectoryChangeHandler * pChangeHandler,
									   BOOL bWatchSubDirs /*=FALSE*/
									   )
/*************************************************************
FUNCTION:	WatchDirectory(const CString & strDirToWatch,   --the name of the directory to watch
						   DWORD dwChangesToWatchFor, --the changes to watch for see dsk docs..for ReadDirectoryChangesW
						   CDirectoryChangeHandler * pChangeHandler -- handles changes in specified directory
						   BOOL bWatchSubDirs      --specified to watch sub directories of the directory that you want to watch
						   )
  Starts watching the specified directory(and optionally subdirectories) for the specified changes

  When specified changes take place the appropriate CDirectoryChangeHandler::On_Filexxx() function is called.

  dwChangesToWatchFor can be a combination of the following flags, and changes map out to the 
  following functions:
	FILE_NOTIFY_CHANGE_FILE_NAME    -- CDirectoryChangeHandler::On_FileNameChanged, CDirectoryChangeHandler::On_FileRemoved
	FILE_NOTIFY_CHANGE_DIR_NAME     -- CDirectoryChangeHandler::On_FileNameChanged, CDirectoryChangeHandler::On_FileRemoved
	FILE_NOTIFY_CHANGE_ATTRIBUTES   -- CDirectoryChangeHandler::On_FileModified
	FILE_NOTIFY_CHANGE_SIZE         -- CDirectoryChangeHandler::On_FileModified
	FILE_NOTIFY_CHANGE_LAST_WRITE   -- CDirectoryChangeHandler::On_FileModified
	FILE_NOTIFY_CHANGE_LAST_ACCESS  -- CDirectoryChangeHandler::On_FileModified
	FILE_NOTIFY_CHANGE_CREATION     -- CDirectoryChangeHandler::On_FileAdded
	FILE_NOTIFY_CHANGE_SECURITY     -- CDirectoryChangeHandler::On_FileModified?
	

  Returns ERROR_SUCCESS if the directory will be watched, 
  or a windows error code if the directory couldn't be watched.
  The error code will most likely be related to a call to CreateFile(), or 
  from the initial call to ReadDirectoryChangesW().  It's also possible to get an
  error code related to being unable to create an io completion port or being unable 
  to start the worker thread.

  This function will fail if the directory to be watched resides on a 
  computer that is not a Windows NT/2000 machine.
**************************************************************/
{
	ASSERT( dwChangesToWatchFor != 0);

	if( strDirToWatch.IsEmpty()
	||  dwChangesToWatchFor == 0 
	|| !pChangeHandler )
		return ERROR_INVALID_PARAMETER;

	
	//double check that it's really a directory
	if( !IsDirectory( strDirToWatch ) )
	{
		TRACE(_T("%s is not a directory!\n"), strDirToWatch);
		return ERROR_BAD_PATHNAME;
	}

	//double check that this directory is not already being watched....
	//if it is, then unwatch it before restarting it...
	if( IsWatchingDirectory( strDirToWatch) )
	{
		VERIFY( 
			UnwatchDirectory( strDirToWatch ) 
			);
	}
	//
	//
	//	Reference this singleton so that privileges for this
	//	process so that it has required permissions to use the ReadDirectoryChangesW API.
	//
	CPrivilegeEnabler::Instance();
	//
	//open the directory to watch....
	HANDLE hDir = CreateFile(strDirToWatch, 
								FILE_LIST_DIRECTORY, 
								FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
								NULL, //security attributes
								OPEN_EXISTING,
								FILE_FLAG_BACKUP_SEMANTICS | //<- does the process token require this priviledge to be set? -- do I need to adjust the token?
                                FILE_FLAG_OVERLAPPED, //OVERLAPPED!
								NULL);
	if( hDir == INVALID_HANDLE_VALUE )
	{
		TRACE(_T("Couldn't open directory for monitoring. %d\n"), GetLastError());
		return GetLastError();
	}
	//opened the dir!
	
	CDirWatchInfo * pDirInfo = new CDirWatchInfo( hDir, strDirToWatch, pChangeHandler, dwChangesToWatchFor, bWatchSubDirs);
	if( !pDirInfo )
	{
		CloseHandle( hDir );
		return ERROR_OUTOFMEMORY;
	}
	
	//create a IO completion port/or associate this key with
	//the existing IO completion port
	m_hCompPort = CreateIoCompletionPort(hDir, 
										m_hCompPort, //if m_hCompPort is NULL, hDir is associated with a NEW completion port,
													 //if m_hCompPort is NON-NULL, hDir is associated with the existing completion port that the handle m_hCompPort references
										(DWORD)pDirInfo, //the completion 'key'... this ptr is returned from GetQueuedCompletionStatus() when one of the events in the dwChangesToWatchFor filter takes place
										0);
	if( m_hCompPort == NULL )
	{
		delete pDirInfo;
		return GetLastError();
	}
	else
	{//completion port created/directory associated w/ it successfully

		//if the thread isn't running start it....
		//when the thread starts, it will call ReadDirectoryChangesW and wait 
		//for changes to take place
		if( m_hThread == NULL )
		{
			//start the thread
			CWinThread * pThread = AfxBeginThread(MonitorDirectoryChanges, this);
			if( !pThread )
			{//couldn't create the thread!
				delete pDirInfo;
				return (GetLastError() == ERROR_SUCCESS)? ERROR_MAX_THRDS_REACHED : GetLastError();
			}
			else
			{
				m_hThread	 = pThread->m_hThread;
				//m_dwThreadID = pThread->m_nThreadID;
			}
		}
		if( m_hThread != NULL )
		{//thread is running, 
			//signal the thread to issue the initial call to
			//ReadDirectoryChangesW()
		   DWORD dwStarted = pDirInfo->StartMonitor( m_hCompPort );

		   if( dwStarted != ERROR_SUCCESS )
		   {//there was a problem!
			   delete pDirInfo;
			   return dwStarted;
		   }
		   else
		   {//ReadDirectoryChangesW was successfull!
				//add the directory info to the first empty slot in the array

			   //associate the pChangeHandler with this object
			   pChangeHandler->ReferencesWatcher( this );//reference is removed when directory is unwatched.

				//add the CDirWatchInfo* to the array...
				CSingleLock lock( &m_csDirWatchInfo, TRUE);
				ASSERT( lock.IsLocked() );
			   //first try to add it to the first empty slot in m_DirectoriesToWatch
				int max = m_DirectoriesToWatch.GetSize();
				for(int i = 0; i < max; ++i)
				{
					if( m_DirectoriesToWatch[i] == NULL )
					{
						m_DirectoriesToWatch[i] = pDirInfo;
						break;
					}
				}
				if( i == max )
					m_DirectoriesToWatch.Add(pDirInfo);
			
				return dwStarted;
		   }

		}
		else
		{
			ASSERT(FALSE);//control path shouldn't get here
			return ERROR_MAX_THRDS_REACHED;
		}
		
	}
	
}

BOOL CDirectoryChangeWatcher::UnwatchAllDirectories()
{
	
	//unwatch all of the watched directories
	//delete all of the CDirWatchInfo objects,
	//kill off the worker thread
	if( m_hThread != NULL )
	{
		ASSERT( m_hCompPort != NULL );
		
		CSingleLock lock( &m_csDirWatchInfo, TRUE);
		ASSERT( lock.IsLocked() );

		CDirWatchInfo * pDirInfo;
		//Unwatch each of the watched directories
		//and delete the CDirWatchInfo associated w/ that directory...
		int max = m_DirectoriesToWatch.GetSize();
		for(int i = 0; i < max; ++i)
		{
			if( (pDirInfo = m_DirectoriesToWatch[i]) != NULL )
			{
				VERIFY( pDirInfo->UnwatchDirectory( m_hCompPort ) );
				ASSERT( pDirInfo->GetRealChangeHandler() );
				
				pDirInfo->GetRealChangeHandler()->ReleaseReferenceToWatcher( this );

				m_DirectoriesToWatch.SetAt(i, NULL)	;
				delete pDirInfo;
			}
			
		}
		m_DirectoriesToWatch.RemoveAll();
		//kill off the thread
		PostQueuedCompletionStatus(m_hCompPort, 0, 0, NULL);//The thread will catch this and exit the thread
		//wait for it to exit
		WaitForSingleObject(m_hThread, INFINITE);
		//CloseHandle( m_hThread );//Since thread was started w/ AfxBeginThread() this handle is closed automatically, closing it again will raise an exception
		m_hThread = NULL;
		//m_dwThreadID = 0UL;		

		//close the completion port...
		CloseHandle( m_hCompPort );
		m_hCompPort = NULL;


		return TRUE;
	}
	else
	{
#ifdef _DEBUG
		//make sure that there aren't any 
		//CDirWatchInfo objects laying around... they should have all been destroyed 
		//and removed from the array m_DirectoriesToWatch
		if( m_DirectoriesToWatch.GetSize() > 0 )
		{
			for(int i = 0; i < m_DirectoriesToWatch.GetSize(); ++i)
			{
				ASSERT( m_DirectoriesToWatch[i] == NULL );
			}
		}
#endif
	}
	return FALSE;
}

BOOL CDirectoryChangeWatcher::UnwatchDirectory(const CString & strDirToStopWatching)
/***************************************************************
FUNCTION:	UnwatchDirectory(const CString & strDirToStopWatching -- if this directory is being watched, the watch is stopped

****************************************************************/
{
	BOOL bRetVal = FALSE;

	CSingleLock lock(&m_csDirWatchInfo, TRUE);
	ASSERT( lock.IsLocked() );

	if( m_hCompPort != NULL )//The io completion port must be open
	{
		ASSERT( !strDirToStopWatching.IsEmpty() );
	
		int max = m_DirectoriesToWatch.GetSize();
		CDirWatchInfo * pDirInfo;
		for(int i = 0; i < max; ++i)
		{
			if( (pDirInfo = m_DirectoriesToWatch[i]) != NULL //may be NULL
			&&  pDirInfo->m_strDirName.CompareNoCase( strDirToStopWatching ) == 0 )
			{
				//stop watching this directory
				VERIFY( pDirInfo->UnwatchDirectory( m_hCompPort ) );

				ASSERT( pDirInfo->GetRealChangeHandler() );
				
				pDirInfo->GetRealChangeHandler()->ReleaseReferenceToWatcher(this);

				//cleanup the object used to watch the directory
				m_DirectoriesToWatch.SetAt(i,NULL);
				delete pDirInfo;
				bRetVal = TRUE;
				break;
			}
		}
	}

	return bRetVal;
}

BOOL CDirectoryChangeWatcher::UnwatchDirectory(CDirectoryChangeHandler * pChangeHandler)
/************************************

  This function is called from the dtor of CDirectoryChangeHandler automatically,
  but may also be called by a programmer because it's public...
  
  A single CDirectoryChangeHandler may be used for any number of watched directories.

  Unwatch any directories that may be using this 
  CDirectoryChangeHandler * pChangeHandler to handle changes to a watched directory...
  
  The CDirWatchInfo::m_pChangeHandler member of objects in the m_DirectoriesToWatch
  array will == pChangeHandler if that handler is being used to handle changes to a directory....
************************************/
{
	ASSERT( pChangeHandler );

	CSingleLock lock(&m_csDirWatchInfo, TRUE);
	
	ASSERT( lock.IsLocked() );
	
	int nUnwatched = 0;
	int max = m_DirectoriesToWatch.GetSize();
	CDirWatchInfo * pDirInfo;
	for(int i = 0; i < max; ++i)
	{
		if( (pDirInfo = m_DirectoriesToWatch[i]) != NULL//elements may be NULL
		&&  pDirInfo->GetRealChangeHandler() == pChangeHandler )
		{
			VERIFY( pDirInfo->UnwatchDirectory( m_hCompPort ) );

			ASSERT( pDirInfo->GetRealChangeHandler() );

			pDirInfo->GetRealChangeHandler()->ReleaseReferenceToWatcher( this );

			nUnwatched++;
			m_DirectoriesToWatch.SetAt(i, NULL);
			delete pDirInfo;	
		}
	}
	return (BOOL)(nUnwatched != 0);
}
CDirectoryChangeWatcher::CDirWatchInfo::CDirWatchInfo(HANDLE hDir, 
													  const CString & strDirectoryName, 
													  CDirectoryChangeHandler * pChangeHandler,
													  DWORD dwChangeFilter, 
													  BOOL bWatchSubDir)
 :	m_pChangeHandler( NULL ), 
	m_hDir(hDir),
	m_dwChangeFilter( dwChangeFilter ),
	m_bWatchSubDir( bWatchSubDir ),
	m_strDirName( strDirectoryName ),
	m_dwBufLength(0),
	m_dwReadDirError(ERROR_SUCCESS),
	m_StartStopEvent(FALSE, TRUE), //NOT SIGNALLED, MANUAL RESET
	m_RunningState( RUNNING_STATE_NOT_SET )
{ 
	
	ASSERT( hDir != INVALID_HANDLE_VALUE 
		&& !strDirectoryName.IsEmpty() );
	
	//
	//	This object 'decorates' the pChangeHandler passed in
	//	so that notifications fire in the context of the main thread(thread executing this code right here)
	//
	m_pChangeHandler = new CDelayedDirectoryChangeHandler( pChangeHandler );
	ASSERT( m_pChangeHandler );

	ASSERT( GetChangeHandler() );
	ASSERT( GetRealChangeHandler() );
	if( GetRealChangeHandler() )
		GetRealChangeHandler()->AddRef();
	
	memset(&m_Overlapped, 0, sizeof(m_Overlapped));
	//memset(m_Buffer, 0, sizeof(m_Buffer));
}

CDirectoryChangeWatcher::CDirWatchInfo::~CDirWatchInfo()
{
	if( GetChangeHandler() )
	{//If this call to CDirectoryChangeHandler::Release() causes m_pChangeHandler to delete itself,
		//the dtor for CDirectoryChangeHandler will call CDirectoryChangeWatcher::UnwatchDirectory( CDirectoryChangeHandler * ),
		//which will make try to delete this object again.
		//if m_pChangeHandler is NULL, it won't try to delete this object again...
		CDirectoryChangeHandler * pTmp = SetRealDirectoryChangeHandler( NULL );
		if( pTmp )
			pTmp->Release();
		else{
			ASSERT( FALSE );
		}
	}
	
	
	if( m_hDir != INVALID_HANDLE_VALUE ) 
	{ 
		CloseHandle( m_hDir ), m_hDir = INVALID_HANDLE_VALUE;
	}

	delete m_pChangeHandler;
	m_pChangeHandler = NULL;
	
}
CDelayedDirectoryChangeHandler* CDirectoryChangeWatcher::CDirWatchInfo::GetChangeHandler()const 
{ 
	return m_pChangeHandler; 
}

CDirectoryChangeHandler * CDirectoryChangeWatcher::CDirWatchInfo::GetRealChangeHandler()const
{	
	ASSERT( m_pChangeHandler ); 
	return m_pChangeHandler->GetRealChangeHandler(); 
}

CDirectoryChangeHandler * CDirectoryChangeWatcher::CDirWatchInfo::SetRealDirectoryChangeHandler(CDirectoryChangeHandler * pChangeHandler)
{
	CDirectoryChangeHandler * pOld = GetRealChangeHandler();
	m_pChangeHandler->GetRealChangeHandler() = pChangeHandler;
	return pOld;
}
DWORD CDirectoryChangeWatcher::CDirWatchInfo::StartMonitor(HANDLE hCompPort)
/*********************************************
  Sets the running state of the object to perform the initial call to ReadDirectoryChangesW()
  , wakes up the thread waiting on GetQueuedCompletionStatus()
  and waits for an event to be set before returning....

  The return value is either ERROR_SUCCESS if ReadDirectoryChangesW is successfull,
  or is the value of GetLastError() for when ReadDirectoryChangesW() failed.
**********************************************/
{
	ASSERT( hCompPort );

	//Guard the properties of this object 
	VERIFY( LockProperties() );
	

		
	m_RunningState = DO_START_MONITOR;//set the state member to indicate that the object is to START monitoring the specified directory
	PostQueuedCompletionStatus(hCompPort, sizeof(this), (DWORD)this, &m_Overlapped);//make the thread waiting on GetQueuedCompletionStatus() wake up

	VERIFY( UnlockProperties() );//unlock this object so that the thread can get at them...

	//wait for signal that the initial call 
	//to ReadDirectoryChanges has been made
	DWORD dwWait = WaitForSingleObject(m_StartStopEvent, INFINITE);
	ASSERT( dwWait == WAIT_OBJECT_0 );
	m_StartStopEvent.ResetEvent();
	
	return m_dwReadDirError;//This value is set in the worker thread when it first calls ReadDirectoryChangesW().
}

BOOL CDirectoryChangeWatcher::CDirWatchInfo::UnwatchDirectory(HANDLE hCompPort )
/*******************************************

    Sets the running state of the object to stop monitoring a directory,
	Causess the thread to wake up and to stop monitoring the dierctory
	
********************************************/
{
	ASSERT( hCompPort );
	ASSERT( m_hDir != INVALID_HANDLE_VALUE );

	VERIFY( LockProperties() );
	
	//set the state member to indicate that the object is to stop monitoring the 
	//directory that this CDirWatchInfo is responsible for...
	m_RunningState = CDirectoryChangeWatcher::CDirWatchInfo::DO_STOP_MONITOR;
	PostQueuedCompletionStatus(hCompPort, sizeof(CDirWatchInfo*), (DWORD)this, &m_Overlapped);

	VERIFY( UnlockProperties() );

	//Wait for the Worker thread to indicate that the watch has been stopped
	DWORD dwWait = WaitForSingleObject(m_StartStopEvent, INFINITE);
		
	ASSERT( dwWait == WAIT_OBJECT_0 );
	
	m_StartStopEvent.ResetEvent();

	
	return (BOOL) (dwWait == WAIT_OBJECT_0 );
}

UINT CDirectoryChangeWatcher::MonitorDirectoryChanges(LPVOID lpvThis)
/********************************************
   The worker thread function which monitors directory changes....
********************************************/
{
    DWORD numBytes;

    CDirWatchInfo * pdi;
    LPOVERLAPPED lpOverlapped;
    
	CDirectoryChangeWatcher * pThis = reinterpret_cast<CDirectoryChangeWatcher*>(lpvThis);
	ASSERT( pThis );
	CString strLastFileName;

	pThis->On_ThreadInitialize();


    do
    {
        // Retrieve the directory info for this directory
        // through the io port's completion key
        if( !GetQueuedCompletionStatus( pThis->m_hCompPort,
                                   &numBytes,
                                   (LPDWORD) &pdi,//<-- completion Key
                                   &lpOverlapped,
                                   INFINITE) )
		{//The io completion request failed...
		 //probably because the handle to the directory that
		 //was used in a call to ReadDirectoryChangesW has been closed.
			TRACE(_T("GetQuedCompletionStatus() returned FALSE\nGetLastError(): %d Completion Key: %p lpOverlapped: %p"), GetLastError(), pdi, lpOverlapped);
#ifdef _DEBUG
			MessageBeep( -1 );
#endif
		}
		
		if ( pdi )//pdi will be null if I call PostQueuedCompletionStatus(m_hCompPort, 0,0,NULL);
        {
			/***********************************
			The CDirWatchInfo::m_RunningState is pretty much the only member
			of CDirWatchInfo that can be modified from the other thread.
			The functions StartMonitor() and UnwatchDirecotry() are the functions that 
			can modify that variable.

			So that I'm sure that I'm getting the right value, 
			I'm using a critical section to guard against another thread modyfying it when I want
			to read it...
			
			************************************/
		    VERIFY( pdi->LockProperties() );//don't give the main thread a chance to change this object
										    //while we're working with this object...

			CDirWatchInfo::eRunningState Run_State = pdi->m_RunningState ;
			
			VERIFY( pdi->UnlockProperties() );//let another thread back at the properties...
			/***********************************
			 Unlock it so that there isn't a DEADLOCK if 
			 somebody tries to call a function which will 
			 cause CDirWatchInfo::UnwatchDirectory() to be called
			 from within the context of this thread (eg: a function called because of
			 the handler for one of the CDirectoryChangeHandler::On_Filexxx() functions)

			************************************/
			
			ASSERT( pdi->GetChangeHandler() );
			switch( Run_State )
			{
			case CDirWatchInfo::DO_START_MONITOR:
				{
					//Issue the initial call to ReadDirectoryChangesW()
					
					if( !ReadDirectoryChangesW( pdi->m_hDir,
										pdi->m_Buffer,//<--FILE_NOTIFY_INFORMATION records are put into this buffer
										READ_DIR_CHANGE_BUFFER_SIZE,
										pdi->m_bWatchSubDir,
										pdi->m_dwChangeFilter,
										&pdi->m_dwBufLength,//this var not set when using asynchronous mechanisms...
										&pdi->m_Overlapped,
										NULL) )//no completion routine!
					{
						pdi->m_dwReadDirError = GetLastError();
					}
					else
					{//read directory changes was successful!
					 //allow it to run normally
						pdi->m_RunningState = CDirWatchInfo::DO_RUN_NORMAL;
						pdi->m_dwReadDirError = ERROR_SUCCESS;
					}
					pdi->m_StartStopEvent.SetEvent();//signall that the ReadDirectoryChangesW has been called
													 //check CDirWatchInfo::m_dwReadDirError to see whether or not ReadDirectoryChangesW succeeded...
					

				}break;
			case CDirWatchInfo::DO_STOP_MONITOR:
				{
					//We want to shut down the monitoring of the directory
					//that pdi is managing...
					
					if( pdi->m_hDir != INVALID_HANDLE_VALUE )
					{
					 //Since I've called ReadDirectoryChangesW() asynchronously, I am waiting
					 //for it to return via GetQueuedCompletionStatus().  When I close the
					 //handle that ReadDirectoryChangesW() is waiting on, it will
					 //cause GetQueuedCompletionStatus() to return again with this pdi object....
					 // Close the handle, and then wait for the call to GetQueuedCompletionStatus()
					 //to return again by breaking out of the switch, and letting GetQueuedCompletionStatus()
					 //get called again
						CloseHandle( pdi->m_hDir );
						pdi->m_hDir = INVALID_HANDLE_VALUE;
						pdi->m_RunningState = CDirWatchInfo::DO_STOP_MONITOR_STEP2;//back up step...GetQueuedCompletionStatus() will still need to return from the last time that ReadDirectoryChangesW() was called.....
					}
					else
					{
						//either we weren't watching this direcotry in the first place,
						//or we've already stopped monitoring it....
						pdi->m_StartStopEvent.SetEvent();//set the event that ReadDirectoryChangesW has returned and no further calls to it will be made...
					}
					
				
				}break;
			case CDirWatchInfo::DO_STOP_MONITOR_STEP2:
				{
					//GetQueuedCompletionStatus() has returned from the last
					//time that ReadDirectoryChangesW was called...
					//Using CloseHandle() on the directory handle used by
					//ReadDirectoryChangesW will cause it to return via GetQueuedCompletionStatus()....
					if( pdi->m_hDir == INVALID_HANDLE_VALUE )
						pdi->m_StartStopEvent.SetEvent();//signal that no further calls to ReadDirectoryChangesW will be made
														 //and this pdi can be deleted 
					else
					{//for some reason, the handle is still open..
						
						CloseHandle( pdi->m_hDir );
						pdi->m_hDir = INVALID_HANDLE_VALUE;
						//wait for GetQueuedCompletionStatus() to return this pdi object again
					}

				}break;
														
			case CDirWatchInfo::DO_RUN_NORMAL:
				{
					
					//if( pdi->m_pChangeHandler )
					//	pdi->m_pChangeHandler->m_strChangedDirectoryName = pdi->m_strDirName;
					if( pdi->GetChangeHandler() )
						pdi->GetChangeHandler()->SetChangedDirectoryName( pdi->m_strDirName );

					//process the FILE_NOTIFY_INFORMATION records:
					CFileNotifyInformation notify_info( (LPBYTE)pdi->m_Buffer, READ_DIR_CHANGE_BUFFER_SIZE);

					

					//The FileName member of the FILE_NOTIFY_INFORMATION
					//structure contains the NAME of the file RELATIVE to the 
					//directory that is being watched...
					//ie, if watching C:\Temp and the file C:\Temp\MyFile.txt is changed,
					//the file name will be "MyFile.txt"
					//If watching C:\Temp, AND you're also watching subdirectories
					//and the file C:\Temp\OtherFolder\MyOtherFile.txt is modified,
					//the file name will be "OtherFolder\MyOtherFile.txt"

					//The CDirectoryChangeHandler::On_Filexxx() functions will receive the name of the file
					//which includes the full path to the directory being watched
					DWORD dwLastAction = 0;
					DWORD dwReadBuffer_Offset = 0UL;//used in case ...see case for FILE_ACTION_RENAMED_OLD_NAME
				    do
					{
						//get the file name for the change
						strLastFileName = notify_info.GetFileNameWithPath( pdi->m_strDirName );
						
						
						
						switch( notify_info.GetAction() )
						{
						case FILE_ACTION_ADDED:
							// a file was added!
							//pdi->m_pChangeHandler->On_FileAdded( strLastFileName ); break;
							pdi->GetChangeHandler()->On_FileAdded(strLastFileName); break;

						case FILE_ACTION_REMOVED:
							//a file was removed
							//pdi->m_pChangeHandler->On_FileRemoved( strLastFileName ); break;
							pdi->GetChangeHandler()->On_FileRemoved( strLastFileName ); break;

						case FILE_ACTION_MODIFIED:
							//a file was changed
							//pdi->m_pChangeHandler->On_FileModified( strLastFileName ); break;
							pdi->GetChangeHandler()->On_FileModified( strLastFileName ); break;

						case FILE_ACTION_RENAMED_OLD_NAME:
							{//a file name has changed, and this is the OLD name
							 //This record is followed by another one w/
							 //the action set to FILE_ACTION_RENAMED_NEW_NAME (contains the new name of the file

								
								if( notify_info.GetNextNotifyInformaion() )
								{//there is another PFILE_NOTIFY_INFORMATION record following the one we're working on now...
								 //it will be the record for the FILE_ACTION_RENAMED_NEW_NAME record
							

									ASSERT( notify_info.GetAction() == FILE_ACTION_RENAMED_NEW_NAME );//making sure that the next record after the OLD_NAME record is the NEW_NAME record
	
									//get the new file name
									CString strNewFileName = notify_info.GetFileNameWithPath( pdi->m_strDirName );

									//pdi->m_pChangeHandler->On_FileNameChanged( strLastFileName, strNewFileName);
									pdi->GetChangeHandler()->On_FileNameChanged( strLastFileName, strNewFileName);
								}
								else
								{
									//this OLD_NAME was the last record returned by ReadDirectoryChangesW
									//I will have to call ReadDirectoryChangesW again so that I will get 
									//the record for FILE_ACTION_RENAMED_NEW_NAME

									//Adjust an offset so that when I call ReadDirectoryChangesW again,
									//the FILE_NOTIFY_INFORMATION will be placed after 
									//the record that we are currently working on.

									/***************
									Let's say that 200 files all had their names changed at about the same time
									There will be 400 FILE_NOTIFY_INFORMATION records (one for OLD_NAME and one for NEW_NAME for EACH file which had it's name changed)
									that ReadDirectoryChangesW will have to report to
									me.   There might not be enough room in the buffer
									and the last record that we DID get was an OLD_NAME record,
									I will need to call ReadDirectoryChangesW again so that I will get the NEW_NAME 
									record.    This way I'll always have to strOldFileName and strNewFileName to pass
									to CDirectoryChangeHandler::On_FileRenamed().

								   After ReadDirecotryChangesW has filled out our buffer with
								   FILE_NOTIFY_INFORMATION records,
								   our read buffer would look something like this:
																										 End Of Buffer
																											  |
																											 \-/	
									|_________________________________________________________________________
									|																		  |
									|file1 OLD name record|file1 NEW name record|...|fileX+1 OLD_name record| |(the record we want would be here, but we've ran out of room, so we adjust an offset and call ReadDirecotryChangesW again to get it) 
									|_________________________________________________________________________|

									Since the record I need is still waiting to be returned to me,
									and I need the current 'OLD_NAME' record,
									I'm copying the current FILE_NOTIFY_INFORMATION record 
									to the beginning of the buffer used by ReadDirectoryChangesW()
									and I adjust the offset into the read buffer so the the NEW_NAME record
									will be placed into the buffer after the OLD_NAME record now at the beginning of the buffer.

									Before we call ReadDirecotryChangesW again,
									modify the buffer to contain the current OLD_NAME record...

									|_______________________________________________________
									|														|
									|fileX old name record(saved)|<this is now garbage>.....|
									|_______________________________________________________|
															 	 /-\
																  |
															 Offset for Read
									Re-issue the watch command to get the rest of the records...

									ReadDirectoryChangesW(..., pBuffer + (an Offset),

									After GetQueuedCompletionStatus() returns, 
									our buffer will look like this:

									|__________________________________________________________________________________________________________
									|																										   |
									|fileX old name record(saved)|fileX new name record(the record we've been waiting for)| <other records>... |
									|__________________________________________________________________________________________________________|

									Then I'll be able to know that a file name was changed
									and I will have the OLD and the NEW name of the file to pass to CDirectoryChangeHandler::On_FileNameChanged

									****************/
									//NOTE that this case has never happened to me in my testing
									//so I can only hope that the code works correctly.
									//It would be a good idea to set a breakpoint on this line of code:
									VERIFY( notify_info.CopyCurrentRecordToBeginningOfBuffer( dwReadBuffer_Offset ) );
									

								}
								break;
							}
						case FILE_ACTION_RENAMED_NEW_NAME:
							{
								//This should have been handled in FILE_ACTION_RENAMED_OLD_NAME
								ASSERT( dwLastAction == FILE_ACTION_RENAMED_OLD_NAME );
								ASSERT( FALSE );//this shouldn't get here
							}
						
						default:
							break;//unknown action
						}

						dwLastAction = notify_info.GetAction();
						
                    

				} while( notify_info.GetNextNotifyInformaion() );

				// Reissue the watch command
				if( !ReadDirectoryChangesW( pdi->m_hDir,
									  pdi->m_Buffer + dwReadBuffer_Offset,//<--FILE_NOTIFY_INFORMATION records are put into this buffer 
                                      READ_DIR_CHANGE_BUFFER_SIZE - dwReadBuffer_Offset,
									  pdi->m_bWatchSubDir,
                                      pdi->m_dwChangeFilter,
                                      &pdi->m_dwBufLength,//this var not set when using asynchronous mechanisms...
                                      &pdi->m_Overlapped,
                                      NULL) )//no completion routine!
				{

					//pdi->m_pChangeHandler->On_ReadDirectoryChangesError( pdi->m_dwReadDirError = GetLastError() );
					pdi->GetChangeHandler()->On_ReadDirectoryChangesError( pdi->m_dwReadDirError = GetLastError() );
				}
				else
				{//success, continue as normal
					pdi->m_dwReadDirError = ERROR_SUCCESS;
				}
				}break;
			default:
				TRACE(_T("MonitorDirectoryChanges() -- how did I get here?\n"));
				break;//how did I get here?
			}//end switch( pdi->m_RunningState )


	
									  
        }//end if( pdi )
    } while( pdi );

	pThis->On_ThreadExit();
	return 0; //thread is ending
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions