Saturday, February 4, 2012

How to monitor the contents of a directory and its subdirectories on Windows.

I need a function that is notified about changes in a directory tree. How can I implement it?

First of all we may use FindFirstChangeNotification / FindNextChangeNotification functions. They return a handle that can be used in one of the wait functions (for instance, WaitForSingleObject, WaitForMultipleObjects etc). An example can be found here: http://msdn.microsoft.com/en-us/library/windows/desktop/aa365261%28v=vs.85%29.aspx

This solution has a small problem: a caller is notified when a change occurs but this API does not provide any way to check what exactly was changed. Of course it is possible to store a list of files with their attributes (such as file size and file modification time) but probably there is a better solution?

I found the function ReadDirectoryChangesW a bit more useful. It provides information about what exactly was changed (file name and action description, i.e. add/change etc).

To use it, first of all we need to get a handle of the specified directory with the help of CreateFile (dwDesiredAccess argument should have FILE_LIST_DIRECTORY, dwFlagsAndAttributes should have FILE_FLAG_BACKUP_SEMANTICS). Pass this handle with a buffer into ReadDirectoryChangesW. The buffer should be large enough to hold one or several items of type FILE_NOTIFY_INFORMATION. The synchronous version of function returns when the content of directory is changed (actually it is possible to make asynchronous calls as well, but this is beyond the scope of this post). 

So a very simple synchronous sample looks like (Unicode version):


#include <windows.h>

void TestDirChanges(LPCWSTR path)
{
    /*
    FileName member of FILE_NOTIFY_INFORMATION has only one WCHAR according to definition. Most likely, this field will have more characters.
    So the expected size of one item is (sizeof(FILE_NOTIFY_INFORMATION) + MAX_PATH * sizeof(WCHAR)).
    Prepare buffer for 256 items.
    */
    char buf[256 * (sizeof(FILE_NOTIFY_INFORMATION) + MAX_PATH * sizeof(WCHAR))] = {0};
    DWORD bytesReturned = 0;
    BOOL result = FALSE;
    FILE_NOTIFY_INFORMATION *fni = NULL;

    HANDLE hDir = CreateFile(path,
        FILE_LIST_DIRECTORY | STANDARD_RIGHTS_READ,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        NULL,
        OPEN_EXISTING,
        FILE_FLAG_BACKUP_SEMANTICS,
        NULL);
         
    if (!hDir || hDir == INVALID_HANDLE_VALUE)
    {
        wprintf(L"CreateFile failed\n");
        return;
    }

    while (1)
    {
        result = ReadDirectoryChangesW(hDir,
            buf,
            sizeof(buf) / sizeof(*buf),
            TRUE, /* monitor the entire subtree */
            FILE_NOTIFY_CHANGE_FILE_NAME |
                FILE_NOTIFY_CHANGE_DIR_NAME |
                FILE_NOTIFY_CHANGE_ATTRIBUTES |
                FILE_NOTIFY_CHANGE_SIZE |
                FILE_NOTIFY_CHANGE_LAST_WRITE |
                FILE_NOTIFY_CHANGE_LAST_ACCESS |
                FILE_NOTIFY_CHANGE_CREATION |
                FILE_NOTIFY_CHANGE_SECURITY,
            &bytesReturned,
            NULL,
            NULL);

        if (result && bytesReturned)
        {
            wchar_t filename[MAX_PATH];
            wchar_t action[256];
            for (fni = (FILE_NOTIFY_INFORMATION*)buf; fni; )
            {
                switch (fni->Action)
                {
                case FILE_ACTION_ADDED:
                    wcscpy_s(action, sizeof(action) / sizeof(*action), L"File added:");
                    break;

                case FILE_ACTION_REMOVED:
                    wcscpy_s(action, sizeof(action) / sizeof(*action), L"File removed:");
                    break;

                case FILE_ACTION_MODIFIED:
                    wcscpy_s(action, sizeof(action) / sizeof(*action), L"File modified:");
                    break;

                case FILE_ACTION_RENAMED_OLD_NAME:
                    wcscpy_s(action, sizeof(action) / sizeof(*action), L"File renamed, was:");
                    break;

                case FILE_ACTION_RENAMED_NEW_NAME:
                    wcscpy_s(action, sizeof(action) / sizeof(*action), L"File renamed, now is:");
                    break;

                default:
                    swprintf_s(action, sizeof(action) / sizeof(*action), L"Unkonwn action: %ld. File name is:", fni->Action);
                }

                if (fni->FileNameLength)
                {
                    wcsncpy_s(filename, MAX_PATH, fni->FileName, fni->FileNameLength / 2);
                    filename[fni->FileNameLength / 2] = 0;
                    wprintf(L"%s '%s'\n", action, filename);
                }
                else
                {
                    wprintf(L"%s <EMPTY>\n", action);
                }               

                if (fni->NextEntryOffset)
                {
                    char *p = (char*)fni;
                    fni = (FILE_NOTIFY_INFORMATION*)(p + fni->NextEntryOffset);
                }
                else
                {
                    fni = NULL;
                }
            }
        }
        else
        {
            wprintf(L"ReadDirectoryChangesW failed\n");
        }
    }

    CloseHandle(hDir);
}

The call might look similar to
TestDirChanges(L"C:\\Documents and Settings\\SomeUser\\1\\").

In order to make the things simpler I didn't introduce any way to stop, so this sample runs forever (until Ctrl-C or kill).

No comments:

Post a Comment