ReadDirectoryChangesW: нет немедленного FILE_ACTION_MODIFIED при записи, ожидание закрытия или открытия дескриптора файла в файле

В настоящее время я пытаюсь использовать ReadDirectoryChangesW() для прослушивания событий файловой системы в Windows в каталоге. В целом это работает, но я столкнулся с проблемой: когда что-то просто записывается в файл (например, с помощью WriteFile), ни одно событие не запускается сразу. Вместо этого событие будет вызвано только тогда, когда файл будет закрыт автором записи или кто-то откроет новый дескриптор для того же файла (при условии, что общий доступ включен), но сама запись не вызывает срабатывание события, даже если файл фактически записан. (Кроме того, это не срабатывает даже после разумного времени ожидания - если никто не открывает и/или не закрывает дескриптор этого файла после операции записи, никакое событие не будет сгенерировано вообще.)

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

Я свел логику в два примера программы: одна для обработки уведомлений (просто выводит некоторую информацию на консоль), а другая для выполнения некоторых операций записи в файл.

Программа писателя:

#include <iostream>
#include <string>
#include <string_view>
#include <system_error>
#include <stdexcept>
#include <cwchar>
#include <thread>
#include <chrono>
#include <windows.h>

[[noreturn]]
void throwSystemError(DWORD error, std::string message)
{
    std::error_code ec{ static_cast<int>(error), std::system_category() };
    throw std::system_error(ec, std::move(message));
}

int main()
{
    HANDLE fileHandle{ INVALID_HANDLE_VALUE };
    try
    {
        fileHandle = CreateFileW(L"C:\\TestDir\\test.txt", FILE_APPEND_DATA, FILE_SHARE_READ, nullptr, OPEN_ALWAYS,
            FILE_ATTRIBUTE_NORMAL, nullptr);
        if (fileHandle == INVALID_HANDLE_VALUE)
        {
            DWORD error = GetLastError();
            throwSystemError(error, "Could not open \"C:\\TestDir\\test.txt\"");
        }

        for (int i = 0; i < 10; ++i)
        {
            DWORD written = 0;
            BOOL ok = WriteFile(fileHandle, "Entry\n", 6, &written, nullptr);
            if (!ok)
            {
                DWORD error = GetLastError();
                throwSystemError(error, "Could not write to \"C:\\TestDir\\test.txt\"");
            }
            std::cout << "Written new entry to file (bytes written = " << written << ", should be 6)\n" << std::flush;

            /* Change the #if 0 to #if 1 to create a new handle to the file,
             * which in turn will actually cause ReadDirectoryChangesW() to
             * produce an event for the file being modified. But leave as-is,
             * so that only WriteFile() is executed, and the event will only
             * occur once we close the file at the end of the program.
             */
#if 0
            HANDLE temp = CreateFileW(L"C:\\TestDir\\test.txt", FILE_GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
            if (temp != INVALID_HANDLE_VALUE)
                CloseHandle(temp);
#endif
            
            std::this_thread::sleep_for(std::chrono::seconds{ 2 });
        }

        CloseHandle(fileHandle);
    }
    catch (std::exception& e)
    {
        if (fileHandle != INVALID_HANDLE_VALUE)
            CloseHandle(fileHandle);
        std::cerr << e.what() << std::endl;
        return 1;
    }
    return 0;
}

Программа-наблюдатель:

#include <iostream>
#include <string>
#include <string_view>
#include <system_error>
#include <stdexcept>
#include <cwchar>
#include <windows.h>

[[noreturn]]
void throwSystemError(DWORD error, std::string message)
{
    std::error_code ec{ static_cast<int>(error), std::system_category() };
    throw std::system_error(ec, std::move(message));
}

int main()
{
    HANDLE dirHandle{ INVALID_HANDLE_VALUE };
    OVERLAPPED ol{};

    try
    {
        dirHandle = CreateFileW(L"C:\\TestDir", GENERIC_READ, FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
            nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, nullptr);
        if (dirHandle == INVALID_HANDLE_VALUE)
        {
            DWORD error = GetLastError();
            throwSystemError(error, "Could not create directory handle for \"C:\\TestDir\"");
        }

        ol.hEvent = CreateEvent(nullptr, false, false, nullptr);
        if (ol.hEvent == NULL || ol.hEvent == INVALID_HANDLE_VALUE)
        {
            DWORD error = GetLastError();
            throwSystemError(error, "Could not create event object for asynchronous I/O");
        }

        alignas(FILE_NOTIFY_INFORMATION) char buffer[16384];

        while (true)
        {
            DWORD returned{};

            BOOL ok = ReadDirectoryChangesW(dirHandle, buffer, sizeof(buffer), false,
                FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE,
                &returned, &ol, nullptr);
            if (!ok)
            {
                DWORD error = GetLastError();
                throwSystemError(error, "Could not listen for directory changes in \"C:\\TestDir\"");
            }

            DWORD error = ERROR_IO_INCOMPLETE;
            while (error == ERROR_IO_INCOMPLETE)
            {
                DWORD waitResult = WaitForSingleObject(ol.hEvent, INFINITE);
                if (waitResult == WAIT_FAILED)
                {
                    error = GetLastError();
                    throwSystemError(error, "Could not wait for async event for directory changes in \"C:\\TestDir\"");
                }
                if (waitResult == WAIT_OBJECT_0)
                {
                    ok = GetOverlappedResult(dirHandle, &ol, &returned, false);
                    error = GetLastError();
                    if (ok)
                        break;
                    else if (error != ERROR_IO_INCOMPLETE)
                        throwSystemError(error, "Could not wait for async event for directory changes in \"C:\\TestDir\"");
                }
                else
                {
                    error = ERROR_IO_INCOMPLETE;
                }
            }

            auto getPointer = [&](std::size_t offset) {
                if (returned > sizeof(buffer) || offset > returned || (offset + sizeof(FILE_NOTIFY_INFORMATION)) > returned)
                    throw std::runtime_error("Internal error (data exceeds size of the buffer)");
                return reinterpret_cast<FILE_NOTIFY_INFORMATION*>(buffer + offset);
            };

            if (returned == 0)
                continue;

            std::size_t offset = 0;
            DWORD nextOffset = 0;

            do
            {
                auto event = getPointer(offset);

                std::wcout << L"Got change notification for file \"" << std::wstring_view{ event->FileName, event->FileNameLength / sizeof(wchar_t) } << L"\": type " << event->Action << std::endl;

                nextOffset = event->NextEntryOffset;
                offset += nextOffset;
            }
            while (nextOffset > 0);
        }

        if (dirHandle != INVALID_HANDLE_VALUE)
        {
            CancelIo(dirHandle);
            while (!HasOverlappedIoCompleted(&ol))
                (void)SleepEx(1, true);
            if (ol.hEvent != NULL && ol.hEvent != INVALID_HANDLE_VALUE)
                CloseHandle(ol.hEvent);
            CloseHandle(dirHandle);
        }
    }
    catch (std::exception& e)
    {
        if (dirHandle != INVALID_HANDLE_VALUE)
        {
            CancelIo(dirHandle);
            while (!HasOverlappedIoCompleted(&ol))
                (void)SleepEx(1, true);
            if (ol.hEvent != NULL && ol.hEvent != INVALID_HANDLE_VALUE)
                CloseHandle(ol.hEvent);
            CloseHandle(dirHandle);
        }

        std::cerr << e.what() << std::endl;
        return 1;
    }
    return 0;
}

Мои вопросы будут:

  1. Я делаю что-то не так, чтобы это не работало должным образом?
  2. Однако, если такое поведение Windows, есть ли способ добиться того, чего я пытаюсь сделать? Например, вызов какой-либо другой функции WinApi, которая заставляет какое-то состояние синхронизироваться или что-то в этом роде? Я также контролирую процесс записи, поэтому я мог бы добавить туда некоторый код, но найденный мною обходной путь открытия дополнительного дескриптора файла и его немедленного закрытия меня не устраивает. (Кроме того, в идеале я хотел бы, если это вообще возможно, избегать изменения автора, потому что с точки зрения абстракции для меня не имеет особого смысла менять эту часть программного обеспечения.)

Как вы проверили, что файл действительно был записан на диск? WriteFile не очищает файловый буфер, пока он не заполнится.

molbdnilo 12.07.2024 10:09

Судя по документации MS, это проблема с кешированием. Вы ничего не можете с этим поделать, кроме как очистить кеш.

john 12.07.2024 10:10

@molbdnilo Ну, в других операционных системах кеш в этом отношении полностью прозрачен (например, уведомления по-прежнему происходят независимо от кеширования), поэтому я не предполагал, что мне придется очищать кеш, чтобы это работало. Но добавление FlushFileBuffers() после WriteFile() действительно решает проблему, и это, по крайней мере, то, с чем я могу жить, добавляя его в писатель. Не стесняйтесь отвечать на вопрос, я приму его.

chris_se 12.07.2024 10:16

ReadDirectoryChangesW считывает изменения в каталоге. Но запись не обновляет каталог до тех пор, пока дескриптор не будет закрыт. (Если вы наберете «dir», вы увидите, что размер файла не изменился, поэтому каталог не был обновлен.)

Raymond Chen 12.07.2024 17:23
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
4
90
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Как сказал @RaymondChen в комментарии:

ReadDirectoryChangesW считывает изменения в каталоге. Но записи делают не обновлять каталог до тех пор, пока дескриптор не будет закрыт. (Если вы наберете «dir», вы можете видеть, что размер файла не изменился, поэтому каталог не обновлялся.)

WriteFile также утверждает:

При записи в файл время последней записи не обновляется полностью до тех пор, пока все ручки, используемые для письма, закрыты. Поэтому, чтобы обеспечить точное время последней записи, закройте дескриптор файла сразу после запись в файл.

Другие вопросы по теме