How to automatically detect a file modification ?

You may have wondered how these antivirus programs detect a modification on any file as soon a it is done. Instead of spending their time checking all the files, they use an event approach provided by microsoft.

The ReadDirectoryChangesW method allows you to monitor a given directory, and to wait (without consumming processos resource) until a change occurs.
The sample code below illustrates how to detect a change (modification of the last time written attribute) of a given file.

char szFilename[ MAX_PATH ] = “monitoredfile.txt”; // The file monitoredchar szFiledir[ MAX_PATH ] = “.”;  // Monitoring the previous file in the current directorym_hMonitoredDir = CreateFile( szFiledir, FILE_LIST_DIRECTORY,
FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS, NULL );

if ( m_hMonitoredDir == INVALID_HANDLE_VALUE )
{
DWORD dwErr = GetLastError();
return;
}

char szBuf[ MAX_PATH ];
DWORD dwBytesRead = 0;

// Waiting file changes
while ( ReadDirectoryChangesW( m_hMonitoredDir, szBuf, MAX_PATH,
FALSE, FILE_NOTIFY_CHANGE_LAST_WRITE,
&dwBytesRead, NULL, NULL )  )
{
PFILE_NOTIFY_INFORMATION pstFileNotif = (PFILE_NOTIFY_INFORMATION)( szBuf );
if ( pstFileNotif->Action == FILE_ACTION_MODIFIED )
{
char szNotifFilename[ MAX_PATH ] = { 0 };
if ( int iNotifFilenameLen = WideCharToMultiByte( CP_OEMCP, NULL,
pstFileNotif->FileName,
pstFileNotif->FileNameLength / sizeof( WCHAR ),
szNotifFilename, sizeof( szNotifFilename ) / sizeof( char ),
NULL, NULL ) )
{
if ( strcmp( szFilename, szNotifFilename ) == 0 )
{
DoYourStuff();
}
}
}
}

This code works fine, but imagine you want to execute it in a thread. You’ll get into trouble when you’ll want to terminate your thread. Why ? Because ReadDirectoryChangesW() as it is used here works synchronously. It hangs until a file change occurs. If you don’t want to change your code, the only way to lunock it is to make a change to the monitored file so that we get out of ReadDirectoryChangesW() and that we can check if we must break our loop.

That’s not a very nice way to manage a clean exit…

There is a better solution : using ReadDirectoryChangesW() asynchronously.
To do so :

  1. Get the handle to your directory with an additionnal flag : FILE_FLAG_OVERLAPPED.
    m_hMonitoredDir = CreateFile( szFiledir, FILE_LIST_DIRECTORY, FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OVERLAPPED, NULL );
  2. give a non-signaled event handle to the ReadDirectoryChangesW function through the OVERLAPPED structure optionnal parameter. The ReadDirectoryChangesW () will return immediately, and will indicate any file change by firing the event
  3. Using the wait events mechanisms allows you to wait for the event to be triggered. But you will still be stucked waiting for a file change if you use WaitForSingleObject(). To wait untile a file change or end of thread, use a second event that your main thread will fire to notify terminate to the thread. And the use WaitForMultipleObjects on the two events. You will get out of the wait state as soon as one of the two events is triggered.

The code below illustrates the solution.

/** Index of the file change event in CFileChangeMonitor::dwEvents*/
const int EVENT_INDEX_FILECHANGE = 0;
/** Index of the terminate event in CFileChangeMonitor::dwEvents*/
const int EVENT_INDEX_TERMINATE = 1;

HANDLE m_events[ 2 ] ; // Array containing the events used

unsigned int CFileChangeMonitor::DoWaitFileChange( const char* szFilename ){    m_hMonitoredDir = CreateFile( szFiledir, FILE_LIST_DIRECTORY,                                             FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,                                             NULL, OPEN_EXISTING,                                             FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OVERLAPPED,                                             NULL );    OVERLAPPED stOverlapped;
memset( &stOverlapped, NULL, sizeof( stOverlapped ) );
m_events[ EVENT_INDEX_FILECHANGE ]  = CreateEvent( NULL, FALSE, FALSE, NULL );
m_events[ EVENT_INDEX_TERMINATE ]   = CreateEvent( NULL, FALSE, FALSE, NULL );
stOverlapped.hEvent = m_events[ EVENT_INDEX_FILECHANGE ];

char szBuf[ MAX_PATH ];
DWORD dwBytesRead = 0;

while ( ReadDirectoryChangesW( m_hMonitoredDir, szBuf, MAX_PATH,
FALSE, FILE_NOTIFY_CHANGE_LAST_WRITE,
&dwBytesRead, &stOverlapped, NULL ) )
{
// Wait either for a file change or a terminate event
DWORD dwWaitRes = WaitForMultipleObjects( 2, m_events, FALSE, INFINITE );
switch ( dwWaitRes )
{
case EVENT_INDEX_FILECHANGE: // Change on the monitored file
if ( GetOverlappedResult( m_hMonitoredDir, &stOverlapped, &dwBytesRead, TRUE ) )
{
PFILE_NOTIFY_INFORMATION pstFileNotif = ( PFILE_NOTIFY_INFORMATION )( szBuf );
if ( pstFileNotif->Action == FILE_ACTION_MODIFIED )
{
char szNotifFilename[ MAX_PATH ] = { 0 };
if ( int iNotifFilenameLen = WideCharToMultiByte( CP_OEMCP, NULL,
pstFileNotif->FileName,
pstFileNotif->FileNameLength / sizeof( WCHAR ), szNotifFilename,
sizeof( szNotifFilename ) / sizeof( char ),
NULL, NULL ) )
{
if ( strcmp( szFilename, szNotifFilename ) == 0 )
OnFileChanged();
}
}
}
break;
default: // EVENT_INDEX_TERMINATE : must terminate
CloseHandle( m_hMonitoredDir );
CloseHandle( m_events[ EVENT_INDEX_FILECHANGE ] );
CloseHandle( m_events[ EVENT_INDEX_TERMINATE ] );
m_events[ EVENT_INDEX_TERMINATE ]   = INVALID_HANDLE_VALUE;
m_events[ EVENT_INDEX_FILECHANGE ]  = INVALID_HANDLE_VALUE;
m_hMonitoredDir                     = INVALID_HANDLE_VALUE;
return 0;
}
}

return 0;
} // unsigned int CFileChangeMonitor::DoWaitFileChange

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *