В настоящее время я пытаюсь использовать 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;
}
Мои вопросы будут:
Судя по документации MS, это проблема с кешированием. Вы ничего не можете с этим поделать, кроме как очистить кеш.
@molbdnilo Ну, в других операционных системах кеш в этом отношении полностью прозрачен (например, уведомления по-прежнему происходят независимо от кеширования), поэтому я не предполагал, что мне придется очищать кеш, чтобы это работало. Но добавление FlushFileBuffers()
после WriteFile()
действительно решает проблему, и это, по крайней мере, то, с чем я могу жить, добавляя его в писатель. Не стесняйтесь отвечать на вопрос, я приму его.
ReadDirectoryChangesW считывает изменения в каталоге. Но запись не обновляет каталог до тех пор, пока дескриптор не будет закрыт. (Если вы наберете «dir», вы увидите, что размер файла не изменился, поэтому каталог не был обновлен.)
Как сказал @RaymondChen в комментарии:
ReadDirectoryChangesW считывает изменения в каталоге. Но записи делают не обновлять каталог до тех пор, пока дескриптор не будет закрыт. (Если вы наберете «dir», вы можете видеть, что размер файла не изменился, поэтому каталог не обновлялся.)
WriteFile также утверждает:
При записи в файл время последней записи не обновляется полностью до тех пор, пока все ручки, используемые для письма, закрыты. Поэтому, чтобы обеспечить точное время последней записи, закройте дескриптор файла сразу после запись в файл.
Как вы проверили, что файл действительно был записан на диск?
WriteFile
не очищает файловый буфер, пока он не заполнится.