【问题标题】:Why does ReadDirectoryChangesW omit events?为什么 ReadDirectoryChangesW 忽略事件?
【发布时间】:2009-05-14 12:47:58
【问题描述】:

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

任何帮助将不胜感激。

【问题讨论】:

    标签: windows delphi filesystems delphi-2009 readdirectorychangesw


    【解决方案1】:

    在我看来,您正在混合使用各种方法来使用 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 的阻塞调用,它可以由文件更改事件或终止事件终止)。我相应地编辑问题。 (...)
    • 快照是什么意思?如果您的意思是使用 FindFirst 遍历所有文件,FindNext:我以前使用过这种方法,但我想避免 (1) 使用大目录时延迟更改检测时间和 (2) 索引线程的恒定 I/O 负载减慢所有其他 I/O 操作。
    • 同意您的第二条评论,但正如 MSDN 文档所述,您需要为内部缓冲区溢出做好准备,在这种情况下,需要对目录进行全面(重新)扫描。
    • 哇,非常感谢 mghie 做出如此详细的回答!我必须将此与我的解决方案进行比较并检查我做错了什么。稍后我可能会写一些反馈。
    【解决方案2】:

    我们在丢失事件时遇到了同样的问题,特别是如果同时发生很多变化,即。 500个文件被复制到监控目录。

    最后我们找到了Cromis,并使用了Directory watch。我们再也没有回头。

    【讨论】:

    • 目录表确实不错。对于 64 位兼容性,您需要将 getWindowLong 替换为 getWindowLongPtr(也用于 set...)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-18
    • 2013-10-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-06
    相关资源
    最近更新 更多