Почему ReadDirectoryChangesW пропускает события?

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

var
  InfoPointer : PFileNotifyInformation;
  NextOffset : DWORD;
...
while (not Terminated) do begin
  if ReadDirectoryChangesW (FDirHandle, FBuffer, FBufferLength, True,
                            FFilter, @BytesRead, @FOverlap, nil) then
    begin
    WaitResult := WaitForMultipleObjects (2, @FEventArray, False, INFINITE);
    if (WaitResult = waitFileChange) then
      begin 
      InfoPointer := FBuffer;
      repeat
        NextOffset := InfoPointer.NextEntryOffset;
        ...
        PByte (InfoPointer) := PByte (InfoPointer) + NextOffset;
      until NextOffset = 0;
      end;
    end;
end;  

Фильтр есть

FFilter :=  FILE_NOTIFY_CHANGE_FILE_NAME or
            FILE_NOTIFY_CHANGE_DIR_NAME or
            FILE_NOTIFY_CHANGE_SIZE or
            FILE_NOTIFY_CHANGE_LAST_WRITE;

и дескриптор каталога получается так:

FDirHandle := CreateFile (PChar (FDirectoryWatch.WatchedDirectory),
                          FILE_LIST_DIRECTORY or GENERIC_READ,
                          FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE,
                          nil, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS or   
                          FILE_FLAG_OVERLAPPED, 0);         

Когда я удаляю несколько файлов, я получаю только одно событие, а NextOffset равен 0! И когда я удаляю каталог, я получаю только одно событие для каталога. Что, если мне нужно одно событие для каждого файла в каталоге?

Любая помощь будет оценена по достоинству.


person jpfollenius    schedule 14.05.2009    source источник


Ответы (2)


Мне кажется, что вы смешиваете различные способы использования ReadDirectoryChangesW(), вы оба указываете флаг FILE_FLAG_OVERLAPPED при открытии каталога и предоставляете указатель на lpOverlapped, означающий, что вы хотите ожидать события в структуре и обрабатывать асинхронный ввод-вывод; и в то же время вы вызываете ReadDirectoryChangesW() в цикле рабочего потока. Я бы сначала попробовал еще раз, установив для lpOverlapped значение nil, так как у вас есть выделенный поток и вы можете использовать синхронный режим.

В документации по ReadDirectoryChangesW() Функция API, описаны различные способы ее использования. Обратите внимание, что также возможно переполнение буфера, поэтому события изменения в любом случае могут быть потеряны. Возможно, вам следует переосмыслить свою стратегию, полагаясь исключительно на эту функцию, сравнение снимков содержимого каталога также может сработать.

Изменить:

Ваш отредактированный код выглядит лучше. Однако в моих тестах функция ReadDirectoryChangesW() работала, как заявлено, либо в возвращаемом буфере было несколько записей данных, либо для обработки требовалось более одного буфера. Это зависит от времени, после достижения точки останова в Delphi я получаю несколько записей в одном буфере.

Для полноты прикрепляю тестовый код, реализованный на Delphi 5:

type
  TWatcherThread = class(TThread)
  private
    fChangeHandle: THandle;
    fDirHandle: THandle;
    fShutdownHandle: THandle;
  protected
    procedure Execute; override;
  public
    constructor Create(ADirectoryToWatch: string);
    destructor Destroy; override;

    procedure Shutdown;
  end;

constructor TWatcherThread.Create(ADirectoryToWatch: string);
const
  FILE_LIST_DIRECTORY = 1;
begin
  inherited Create(TRUE);
  fChangeHandle := CreateEvent(nil, FALSE, FALSE, nil);
  fDirHandle := CreateFile(PChar(ADirectoryToWatch),
    FILE_LIST_DIRECTORY or GENERIC_READ,
    FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE,
    nil, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS or FILE_FLAG_OVERLAPPED, 0);
  fShutdownHandle := CreateEvent(nil, FALSE, FALSE, nil);
  Resume;
end;

destructor TWatcherThread.Destroy;
begin
  if fDirHandle <> INVALID_HANDLE_VALUE then
    CloseHandle(fDirHandle);
  if fChangeHandle <> 0 then
    CloseHandle(fChangeHandle);
  if fShutdownHandle <> 0 then
    CloseHandle(fShutdownHandle);
  inherited Destroy;
end;

procedure TWatcherThread.Execute;
type
  PFileNotifyInformation = ^TFileNotifyInformation;
  TFileNotifyInformation = record
    NextEntryOffset: DWORD;
    Action: DWORD;
    FileNameLength: DWORD;
    FileName: WideChar;
  end;
const
  BufferLength = 65536;
var
  Filter, BytesRead: DWORD;
  InfoPointer: PFileNotifyInformation;
  Offset, NextOffset: DWORD;
  Buffer: array[0..BufferLength - 1] of byte;
  Overlap: TOverlapped;
  Events: array[0..1] of THandle;
  WaitResult: DWORD;
  FileName, s: string;
begin
  if fDirHandle <> INVALID_HANDLE_VALUE then begin
    Filter := FILE_NOTIFY_CHANGE_FILE_NAME or FILE_NOTIFY_CHANGE_DIR_NAME
      or FILE_NOTIFY_CHANGE_SIZE or FILE_NOTIFY_CHANGE_LAST_WRITE;

    FillChar(Overlap, SizeOf(TOverlapped), 0);
    Overlap.hEvent := fChangeHandle;

    Events[0] := fChangeHandle;
    Events[1] := fShutdownHandle;

    while not Terminated do begin
      if ReadDirectoryChangesW (fDirHandle, @Buffer[0], BufferLength, TRUE,
        Filter, @BytesRead, @Overlap, nil)
      then begin
        WaitResult := WaitForMultipleObjects(2, @Events[0], FALSE, INFINITE);
        if WaitResult = WAIT_OBJECT_0 then begin
          InfoPointer := @Buffer[0];
          Offset := 0;
          repeat
            NextOffset := InfoPointer.NextEntryOffset;
            FileName := WideCharLenToString(@InfoPointer.FileName,
              InfoPointer.FileNameLength);
            SetLength(FileName, StrLen(PChar(FileName)));
            s := Format('[%d] Action: %.8xh, File: "%s"',
               [Offset, InfoPointer.Action, FileName]);
            OutputDebugString(PChar(s));
            PByte(InfoPointer) := PByte(DWORD(InfoPointer) + NextOffset);
            Offset := Offset + NextOffset;
          until NextOffset = 0;
        end;
      end;
    end;
  end;
end;

procedure TWatcherThread.Shutdown;
begin
  Terminate;
  if fShutdownHandle <> 0 then
    SetEvent(fShutdownHandle);
end;

////////////////////////////////////////////////////////////////////////////////

procedure TForm1.FormCreate(Sender: TObject);
begin
  fThread := TWatcherThread.Create('D:\Temp');
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  if fThread <> nil then begin
    TWatcherThread(fThread).Shutdown;
    fThread.Free;
  end;
end;

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

person mghie    schedule 14.05.2009
comment
Извините за мой запоздалый комментарий. Раньше я использовал синхронную версию (с точно такими же проблемами), но переключился на асинхронную версию, потому что не нашел способа аккуратно завершить поток. Однако я пропустил одну важную строку в коде примера (блокирующий вызов WaitForMultipleObjects, который может быть прерван либо событием изменения файла, либо событием завершения). Я редактирую вопрос соответственно. (...) - person jpfollenius; 15.05.2009
comment
Что вы имеете в виду под снимком? Если вы имеете в виду перебор всех файлов с помощью FindFirst, FindNext: я ранее использовал такой подход, но я хотел избежать (1) задержки обнаружения изменений при использовании больших каталогов и (2) постоянной нагрузки ввода-вывода для потока индексирования, замедляющего все другие операции ввода-вывода. - person jpfollenius; 15.05.2009
comment
Согласен с вашим вторым комментарием, но, как указано в документации MSDN, вам нужно быть готовым к переполнению внутреннего буфера, и в этом случае необходимо полное (повторное) сканирование каталога. - person mghie; 15.05.2009
comment
Вау, большое спасибо, mghie, за такой подробный ответ! Я должен сравнить это с моим решением и проверить, что я делаю неправильно. Возможно, позже напишу отзыв. - person jpfollenius; 15.05.2009

У нас была такая же проблема с потерей событий, особенно если одновременно происходит много изменений, т.е. 500 файлов копируются в отслеживаемый каталог.

В конце концов мы нашли Cromis и использовали файл Наблюдение за каталогом. Мы никогда не оглядывались назад.

person Pieter van Wyk    schedule 04.05.2012
comment
каталог смотреть действительно хорошо. для 64-битной совместимости вам нужно заменить getWindowLong на getWindowLongPtr (также для set...) - person Z80; 16.06.2017