Using ReadDirectoryChangesW asynchronously in a loop

我的未来我决定 提交于 2019-12-23 02:16:39

问题


INTRODUCTION:

I am trying to use ReadDirectoryChangesW asynchronously in a loop.

Below snippet illustrates what I am trying to achieve:

DWORD example()
{
    DWORD error = 0;

    OVERLAPPED ovl = { 0 };
    ovl.hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);

    if (NULL == ovl.hEvent) return ::GetLastError();

    char buffer[1024];

    while(1)
    {
        process_list_of_existing_files();

        error = ::ReadDirectoryChangesW(
            m_hDirectory, // I have added FILE_FLAG_OVERLAPPED in CreateFile
            buffer, sizeof(buffer), FALSE,
            FILE_NOTIFY_CHANGE_FILE_NAME,
            NULL, &ovl, NULL);

        // we have new files, append them to the list
        if(error) append_new_files_to_the_list(buffer);
        // just continue with the loop
        else if(::GetLastError() == ERROR_IO_PENDING) continue;
        // RDCW error, this is critical -> exit
        else return ::GetLastError(); 
    }
}

PROBLEM:

I do not know how to handle the case when ReadDirectoryChangesW returns FALSE with GetLastError() code being ERROR_IO_PENDING.

In that case I should just continue with the loop and keep looping until ReadDirectoryChangesW returns buffer I can process.

MY EFFORTS TO SOLVE THIS:

I have tried using WaitForSingleObject(ovl.hEvent, 1000) but it crashes with error 1450 ERROR_NO_SYSTEM_RESOURCES. Below is the MVCE that reproduces this behavior:

#include <iostream>
#include <Windows.h>

DWORD processDirectoryChanges(const char *buffer)
{
    DWORD offset = 0;
    char fileName[MAX_PATH] = "";
    FILE_NOTIFY_INFORMATION *fni = NULL;

    do
    {
        fni = (FILE_NOTIFY_INFORMATION*)(&buffer[offset]);
        // since we do not use UNICODE, 
        // we must convert fni->FileName from UNICODE to multibyte
        int ret = ::WideCharToMultiByte(CP_ACP, 0, fni->FileName,
            fni->FileNameLength / sizeof(WCHAR),
            fileName, sizeof(fileName), NULL, NULL);

        switch (fni->Action)
        {
        case FILE_ACTION_ADDED:     
        {
            std::cout << fileName << std::endl;
        }
        break;
        default:
            break;
        }

        ::memset(fileName, '\0', sizeof(fileName));
        offset += fni->NextEntryOffset;

    } while (fni->NextEntryOffset != 0);

    return 0;
}

int main()
{
    HANDLE hDir = ::CreateFile("C:\\Users\\nenad.smiljkovic\\Desktop\\test", 
        FILE_LIST_DIRECTORY,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        NULL, OPEN_EXISTING, 
        FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL);

    if (INVALID_HANDLE_VALUE == hDir) return ::GetLastError();

    OVERLAPPED ovl = { 0 };
    ovl.hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);

    if (NULL == ovl.hEvent) return ::GetLastError();

    DWORD error = 0, br;
    char buffer[1024];

    while (1)
    {
        error = ::ReadDirectoryChangesW(hDir,
            buffer, sizeof(buffer), FALSE,
            FILE_NOTIFY_CHANGE_FILE_NAME,
            NULL, &ovl, NULL);

        if (0 == error)
        {
            error = ::GetLastError();

            if (ERROR_IO_PENDING != error)
            {
                ::CloseHandle(ovl.hEvent);
                ::CloseHandle(hDir);
                return error;
            }
        }

        error = ::WaitForSingleObject(ovl.hEvent, 0);

        switch (error)
        {
        case WAIT_TIMEOUT:
            break;
        case WAIT_OBJECT_0:
        {
            error = processDirectoryChanges(buffer);

            if (error > 0)
            {
                ::CloseHandle(ovl.hEvent);
                ::CloseHandle(hDir);
                return error;
            }

            if (0 == ::ResetEvent(ovl.hEvent))
            {
                error = ::GetLastError();
                ::CloseHandle(ovl.hEvent);
                ::CloseHandle(hDir);
                return error;
            }
        }
        break;
        default:
            error = ::GetLastError();
            ::CloseHandle(ovl.hEvent);
            ::CloseHandle(hDir);
            return error;
            break;
        }
    }

    return 0;
}

Reading through the documentation, it seems that I need GetOverlappedResult with last parameter set to FALSE but I do not know how to use this API properly.

QUESTION:

Since the MVCE illustrates very well what I am trying to do (print the names of the newly added files), can you show me what must be fixed in the while loop in order for it to work?

Again, the point is to use ReadDirectoryChangesW asynchronously, in a loop, as shown in the snippet from the INTRODUCTION.


回答1:


The basic structure of your program looks more or less OK, you're just using the asynchronous I/O calls incorrectly. Whenever there are no new files, the wait on the event handle times out immediately, which is fine, but you then issue a brand new I/O request, which isn't.

That's why you're running out of system resources; you're issuing I/O requests full tilt without waiting for any of them to complete. You should only issue a new request after the existing request has completed.

(Also, you should be calling GetOverlappedResult to check whether the I/O was successful or not.)

So your loop should look more like this:

    ::ReadDirectoryChangesW(hDir,
        buffer, sizeof(buffer), FALSE,
        FILE_NOTIFY_CHANGE_FILE_NAME,
        NULL, &ovl, NULL);

    while (1)
    {
        DWORD dw;
        DWORD result = ::WaitForSingleObject(ovl.hEvent, 0);

        switch (result)
        {
        case WAIT_TIMEOUT:

            processBackgroundTasks();

            break;

        case WAIT_OBJECT_0:

            ::GetOverlappedResult(hDir, &ovl, &dw, FALSE);

            processDirectoryChanges(buffer);

            ::ResetEvent(ovl.hEvent);

            ::ReadDirectoryChangesW(hDir,
                buffer, sizeof(buffer), FALSE,
                FILE_NOTIFY_CHANGE_FILE_NAME,
                NULL, &ovl, NULL);

            break;
        }
    }

Notes:

  • The error handling has been elided for simplicity; I have not done any testing or checked your code for any other problems.

  • If there might not be any background tasks to perform, you should test for that case and set the timeout to INFINITE rather than 0 when it occurs, otherwise you will be spinning.

  • I wanted to only show the minimal changes necessary to make it work, but calling WaitForSingleObject followed by GetOverlappedResult is redundant; a single call to GetOverlappedResult can both check whether the I/O is complete and retrieve the results if it is.


As requested, the modified version using only GetOverlappedResult and with minimal error checking. I've also added an example of how you might deal with the case where you've run out of work to do; if whatever processing you're doing on the files really does run forever, you don't need that bit.

    ::ResetEvent(ovl.hEvent);

    if (!::ReadDirectoryChangesW(hDir,
        buffer, sizeof(buffer), FALSE,
        FILE_NOTIFY_CHANGE_FILE_NAME,
        NULL, &ovl, NULL))
    {
       error = GetLastError();
       if (error != ERROR_IO_PENDING) fail();
    }

    while (1)
    {
        BOOL wait;

        result = process_list_of_existing_files();

        if (result == MORE_WORK_PENDING)
        {
           wait = FALSE;
        } 
        else if (result == NO_MORE_WORK_PENDING)
        {
           wait = TRUE;
        } 

        if (!::GetOverlappedResult(hDir, &ovl, &dw, wait))
        {
           error = GetLastError();
           if (error == ERROR_IO_INCOMPLETE) continue;
           fail();
        }

        processDirectoryChanges(buffer);

        ::ResetEvent(ovl.hEvent);

        if (!::ReadDirectoryChangesW(hDir,
            buffer, sizeof(buffer), FALSE,
            FILE_NOTIFY_CHANGE_FILE_NAME,
            NULL, &ovl, NULL))
        {
           error = GetLastError();
           if (error != ERROR_IO_PENDING) fail();
        } 
    }



回答2:


variant of indirect using IOCP

  1. create class/struct inherited (containing) OVERLAPPED (or IO_STATUS_BLOCK), reference count, directory handle and data which you need
  2. call BindIoCompletionCallback (RtlSetIoCompletionCallback) for directory handle, for setup your callback
  3. have DoRead() routine, which we first time call from main thread and then from callback
  4. in DoRead() before every call to ReadDirectoryChangesW call AddRef(); because we pass reference (across OVERLAPPED) to our struct to kernel
  5. main (say GUI thread) can continue do own task after initial call DoRead(), unlike APC variant he not need wait in alertable state
  6. in callback we got pointer to our struct from inherited (containing) OVERLAPPED. do any tasks (processDirectoryChanges) ,if need continue spy - call DoRead() and finally call Release()
  7. if ReadDirectoryChangesW from DoRead() fail (as result will be no callback) - we need direct call callback with error code
  8. for stop we can simply close directory handle - as result we got STATUS_NOTIFY_CLEANUP in callback

==================================

//#define _USE_NT_VERSION_

class SPYDATA : 
#ifdef _USE_NT_VERSION_
    IO_STATUS_BLOCK
#else
    OVERLAPPED 
#endif
{
    HANDLE _hFile;
    LONG _dwRef;
    union {
        FILE_NOTIFY_INFORMATION _fni;
        UCHAR _buf[PAGE_SIZE];
    };

    void DumpDirectoryChanges()
    {
        union {
            PVOID buf;
            PBYTE pb;
            PFILE_NOTIFY_INFORMATION pfni;
        };

        buf = _buf;

        for (;;)
        {
            DbgPrint("%x <%.*S>\n", pfni->Action, pfni->FileNameLength >> 1, pfni->FileName);

            ULONG NextEntryOffset = pfni->NextEntryOffset;

            if (!NextEntryOffset)
            {
                break;
            }

            pb += NextEntryOffset;
        }
    }

#ifdef _USE_NT_VERSION_
    static VOID WINAPI _OvCompRoutine(
        _In_    NTSTATUS dwErrorCode,
        _In_    ULONG_PTR dwNumberOfBytesTransfered,
        _Inout_ PIO_STATUS_BLOCK Iosb
        )
    {
        static_cast<SPYDATA*>(Iosb)->OvCompRoutine(dwErrorCode, (ULONG)dwNumberOfBytesTransfered);
    }
#else
    static VOID WINAPI _OvCompRoutine(
        _In_    DWORD dwErrorCode, // really this is NTSTATUS
        _In_    DWORD dwNumberOfBytesTransfered,
        _Inout_ LPOVERLAPPED lpOverlapped
        )
    {
        static_cast<SPYDATA*>(lpOverlapped)->OvCompRoutine(dwErrorCode, dwNumberOfBytesTransfered);
    }
#endif

    VOID OvCompRoutine(NTSTATUS status, DWORD dwNumberOfBytesTransfered)
    {
        DbgPrint("[%x,%x]\n", status, dwNumberOfBytesTransfered);

        if (0 <= status) 
        {
            if (status != STATUS_NOTIFY_CLEANUP)
            {
                if (dwNumberOfBytesTransfered) DumpDirectoryChanges();
                process_list_of_existing_files();// so hard do this here ?!?
                DoRead();
            }
            else
            {
                DbgPrint("\n---- NOTIFY_CLEANUP -----\n");
            }
        }

        Release();
        MyReleaseRundownProtection();
    }

    ~SPYDATA()
    {
        Cancel();
    }

public:

    void DoRead()
    {
        if (MyAcquireRundownProtection())
        {
            AddRef();
#ifdef _USE_NT_VERSION_
            NTSTATUS status = ZwNotifyChangeDirectoryFile(_hFile, 0, 0, this, this, &_fni, sizeof(_buf), FILE_NOTIFY_VALID_MASK, TRUE);
            if (NT_ERROR(status))
            {
                OvCompRoutine(status, 0);
            }
#else
            if (!ReadDirectoryChangesW(_hFile, _buf, sizeof(_buf), TRUE, FILE_NOTIFY_VALID_MASK, (PDWORD)&InternalHigh, this, 0))
            {
                OvCompRoutine(RtlGetLastNtStatus(), 0);
            }
#endif
        }
    }

    SPYDATA()
    {
        _hFile = 0;// ! not INVALID_HANDLE_VALUE because use ntapi for open file
        _dwRef = 1;
#ifndef _USE_NT_VERSION_
        RtlZeroMemory(static_cast<OVERLAPPED*>(this), sizeof(OVERLAPPED));
#endif
    }

    void AddRef()
    {
        InterlockedIncrement(&_dwRef);
    }

    void Release()
    {
        if (!InterlockedDecrement(&_dwRef))
        {
            delete this;
        }
    }

    BOOL Create(POBJECT_ATTRIBUTES poa)
    {
        IO_STATUS_BLOCK iosb;
        NTSTATUS status = ZwOpenFile(&_hFile, FILE_GENERIC_READ, poa, &iosb, FILE_SHARE_VALID_FLAGS, FILE_DIRECTORY_FILE);
        if (0 <= status)
        {
            return
#ifdef _USE_NT_VERSION_
                0 <= RtlSetIoCompletionCallback(_hFile, _OvCompRoutine, 0);
#else
                BindIoCompletionCallback(_hFile, _OvCompRoutine, 0);
#endif
        }
        return FALSE;
    }

    void Cancel()
    {
        if (HANDLE hFile = InterlockedExchangePointer(&_hFile, 0))
        {
            NtClose(hFile);
        }
    }
};

void DemoF()
{
    if (MyInitializeRundownProtection())
    {
        STATIC_OBJECT_ATTRIBUTES(oa, "<SOME_DIRECTORY>");

        if (SPYDATA* p = new SPYDATA)
        {
            if (p->Create(&oa))
            {
                p->DoRead();
            }

            //++ GUI thread run
            MessageBoxW(0, L"wait close program...", L"", MB_OK);
            //-- GUI thread end

            p->Cancel();

            p->Release();
        }

        MyWaitForRundownProtectionRelease();
    }
}


来源:https://stackoverflow.com/questions/40593500/using-readdirectorychangesw-asynchronously-in-a-loop

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!