为什么ReadDirectoryChangesW会忽略事件?

13

我使用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!而当我删除一个目录时,我只会得到该目录的一个事件。如果我想要每个文件在目录中都有一个事件怎么办?

任何帮助将不胜感激。

2个回答

18
似乎您混淆了使用ReadDirectoryChangesW()的不同方式,您在打开目录时同时指定FILE_FLAG_OVERLAPPED标志并提供lpOverlapped参数的指针,这意味着您想在结构中等待事件并处理异步I / O; 同时,在工作线程中循环调用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;

删除一个目录确实只会返回一个改变,而对其中包含的文件没有任何影响。但这是有道理的,因为你只监视父目录的句柄。如果你需要子目录的通知,那么你可能也需要监视它们。


抱歉我的回复有些晚了。我之前使用的是同步版本(遇到了完全相同的问题),但后来转换成了异步版本,因为我找不到一种干净地终止线程的方法。然而,在示例代码中我错过了一个重要的行(即阻塞调用WaitForMultipleObjects,它可以通过文件更改事件或终止事件来终止)。我已经相应地编辑了问题。(...) - jpfollenius
什么是快照?如果你的意思是使用FindFirst、FindNext迭代所有文件:我以前使用过这种方法,但我想避免(1)在使用大目录时延迟的更改检测时间和(2)索引线程的常量I/O负载减慢所有其他I/O操作。 - jpfollenius
1
同意你的第二条评论,但是根据MSDN文档所述,你需要准备好内部缓冲区溢出的情况,在这种情况下,需要对目录进行完整的(重新)扫描。 - mghie
哇,非常感谢mghie的详细回答!我需要将其与我的解决方案进行比较,并检查我做错了什么。我可能会稍后写一些反馈。 - jpfollenius

5
我们曾经遇到过同样的问题,特别是在很多变化同时发生时,比如500个文件被复制到被监控的目录中,事件会丢失。
最终,我们找到了Cromis并使用了Directory watch。从那以后我们再也没有回头看过。

目录监视确实很好。为了64位兼容性,您需要使用getWindowLongPtr替换getWindowLong(还要用set...)。 - Gabriel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接