Использование ReadDirectoryChangesW асинхронно в цикле

ВСТУПЛЕНИЕ:

Я пытаюсь использовать ReadDirectoryChangesW асинхронно в цикле.

Ниже приведен фрагмент кода, который я пытаюсь достичь:

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(); 
    }
}

ПРОБЛЕМА:

Я не знаю, как справиться со случаем, когда ReadDirectoryChangesW возвращается FALSE с GetLastError() код ERROR_IO_PENDING,

В этом случае я должен просто продолжить цикл и продолжать цикл до ReadDirectoryChangesW возвращается buffer Я могу обработать.

МОИ УСИЛИЯ ДЛЯ РЕШЕНИЯ ЭТОГО:

Я пытался использовать WaitForSingleObject(ovl.hEvent, 1000) но вылетает с ошибкой 1450 ERROR_NO_SYSTEM_RESOURCES, Ниже MVCE, который воспроизводит это поведение:

#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;
}

Читая документацию, кажется, что мне нужен GetOverlappedResult с последним параметром, установленным в FALSE но я не знаю, как правильно использовать этот API.

ВОПРОС:

Поскольку MVCE очень хорошо иллюстрирует то, что я пытаюсь сделать (напечатать имена вновь добавленных файлов), можете ли вы показать мне, что должно быть исправлено в while цикл для того, чтобы он работал?

Опять же, дело в том, чтобы использовать ReadDirectoryChangesW асинхронно, в цикле, как показано во фрагменте ВВЕДЕНИЯ.

2 ответа

Решение

Базовая структура вашей программы выглядит более или менее нормально, вы просто используете асинхронные вызовы ввода / вывода неправильно. Всякий раз, когда нет новых файлов, время ожидания для обработчика события сразу же истекает, и это нормально, но вы затем выдаете новый запрос ввода-вывода, который не отвечает.

Вот почему у вас заканчиваются системные ресурсы; вы отправляете запросы ввода-вывода на полный наклон, не дожидаясь завершения какого-либо из них. Вы должны подать новый запрос только после того, как существующий запрос будет выполнен.

(Кроме того, вы должны вызывать GetOverlappedResult, чтобы проверить, был ли ввод / вывод успешным или нет.)

Так что ваш цикл должен выглядеть примерно так:

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

    while (1)
    {
        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;
        }
    }

Заметки:

  • Обработка ошибок была исключена для простоты; Я не проводил никакого тестирования и не проверял ваш код на наличие других проблем.

  • Если, возможно, нет каких-либо фоновых задач для выполнения, вы должны проверить для этого случая и установить время ожидания INFINITE, а не 0, когда это происходит, в противном случае вы будете вращаться.

  • Я хотел показать только минимальные изменения, необходимые для его работы, но вызов WaitForSingleObject с последующим GetOverlappedResult является избыточным; один вызов GetOverlappedResult может одновременно проверить, завершен ли ввод-вывод, и получить результаты, если это так.


По запросу модифицированная версия использует только GetOverlappedResult и с минимальной проверкой ошибок. Я также добавил пример того, как вы можете справиться со случаем, когда у вас закончилась работа; если какая-либо обработка, которую вы выполняете над файлами, действительно выполняется вечно, вам это не нужно.

    ::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();
        } 
    }

вариант косвенного использования IOCP

  1. создать унаследованный класс / структуру (содержащий) OVERLAPPED (или IO_STATUS_BLOCK), счетчик ссылок, дескриптор каталога и данные, которые вам нужны
  2. вызов BindIoCompletionCallback (RtlSetIoCompletionCallback) для дескриптора каталога, для настройки вашего обратного вызова
  3. иметь DoRead() подпрограмма, которую мы сначала вызываем из основного потока, а затем из обратного вызова
  4. в DoRead() перед каждым звонком ReadDirectoryChangesW вызов AddRef(); потому что мы передаем ссылку (через OVERLAPPED) к нашей структуре к ядру
  5. main (скажем, в GUI-потоке) может продолжать выполнять свою задачу после первоначального вызова DoRead() В отличие от варианта APC ему не нужно ждать в аварийном состоянии
  6. в обратном вызове мы получили указатель на нашу структуру из унаследованного (содержащего) OVERLAPPED. делать какие-либо задачи (processDirectoryChanges), если нужно продолжить разведку - позвоните DoRead() и, наконец, вызвать Release ()
  7. если ReadDirectoryChangesW от DoRead() сбой (в результате не будет обратного вызова) - нам нужен прямой обратный вызов с кодом ошибки
  8. для остановки мы можем просто закрыть дескриптор каталога - в результате мы получили STATUS_NOTIFY_CLEANUP в обратном вызове

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

//#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();
    }
}
Другие вопросы по тегам