ReadDirectoryChangesW:写入时不会立即执行 FILE_ACTION_MODIFIED,等待文件句柄关闭或打开文件

问题描述 投票:0回答:1

我目前正在尝试使用

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 函数来强制某些状态同步或其他什么?我还控制编写器进程,因此我可以在那里添加一些代码,但是我发现打开文件的附加句柄然后立即再次关闭它的解决方法并不适合我。 (此外,理想情况下,如果可能的话,我希望避免更改编写器,因为从抽象的角度来看,我应该更改软件的这一部分对我来说没有多大意义。)
c++ winapi
1个回答
0
投票

正如@RaymondChen 在评论中所说

ReadDirectoryChangesW 读取目录的更改。但写做 在句柄关闭之前不更新目录。 (如果您输入 “dir”,可以看到文件大小没有变化,所以目录 还没更新。)

WriteFile 还指出,

写入文件时,最后写入时间不会完全更新,直到 所有用于写入的句柄都已关闭。因此,为了确保 准确的上次写入时间,之后立即关闭文件句柄 写入文件。

© www.soinside.com 2019 - 2024. All rights reserved.