本实例来自 ai 代码,未经验证。

在 Visual Basic 6 (VB6) 中调用 Windows API 函数 ReadDirectoryChangesW 来实现目录更改监控是一个比较复杂的任务,因为 VB6 不直接支持许多现代 Windows API 的功能。这个任务需要使用一些 API 函数,并且处理回调和异步操作。下面是一个基本的示例,展示如何使用 ReadDirectoryChangesW 进行目录监控。

首先,我们需要声明所需的 Windows API 函数和常量:

' 模块: Module1.bas
Option Explicit

' Windows API 常量
Public Const FILE_LIST_DIRECTORY As Long = &H1
Public Const FILE_NOTIFY_CHANGE_FILE_NAME As Long = &H1
Public Const FILE_NOTIFY_CHANGE_DIR_NAME As Long = &H2
Public Const FILE_NOTIFY_CHANGE_ATTRIBUTES As Long = &H4
Public Const FILE_NOTIFY_CHANGE_SIZE As Long = &H8
Public Const FILE_NOTIFY_CHANGE_LAST_WRITE As Long = &H10
Public Const FILE_NOTIFY_CHANGE_LAST_ACCESS As Long = &H20
Public Const FILE_NOTIFY_CHANGE_CREATION As Long = &H40
Public Const FILE_NOTIFY_CHANGE_SECURITY As Long = &H100

' Windows API 结构体
Private Type FILE_NOTIFY_INFORMATION
    NextEntryOffset As Long
    Action As Long
    FileNameLength As Long
    FileName(0 To 255) As Byte
End Type

' Windows API 函数声明
Private Declare Function ReadDirectoryChangesW Lib "kernel32" ( _
    ByVal hDirectory As Long, _
    lpBuffer As Any, _
    ByVal nBufferLength As Long, _
    ByVal bWatchSubtree As Long, _
    ByVal dwNotifyFilter As Long, _
    lpBytesReturned As Long, _
    ByVal lpOverlapped As Long, _
    ByVal lpCompletionRoutine As Long) As Long

Private Declare Function CreateFileW Lib "kernel32" ( _
    ByVal lpFileName As Long, _
    ByVal dwDesiredAccess As Long, _
    ByVal dwShareMode As Long, _
    ByVal lpSecurityAttributes As Long, _
    ByVal dwCreationDisposition As Long, _
    ByVal dwFlagsAndAttributes As Long, _
    ByVal hTemplateFile As Long) As Long

Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)

' 文件句柄
Private hDirectory As Long

' 缓冲区
Private Buffer(4095) As Byte

Public Sub StartWatching(ByVal sPath As String)
    ' 打开目录
    hDirectory = CreateFileW(StrPtr(sPath), FILE_LIST_DIRECTORY, &H1 Or &H2, 0, 3, &H02000000, 0)
    
    If hDirectory = -1 Then
        MsgBox "无法打开目录。", vbCritical
        Exit Sub
    End If
    
    ' 开始监控
    Dim BytesReturned As Long
    ReadDirectoryChangesW hDirectory, Buffer(0), UBound(Buffer) + 1, 1, _
                          FILE_NOTIFY_CHANGE_FILE_NAME Or FILE_NOTIFY_CHANGE_DIR_NAME Or FILE_NOTIFY_CHANGE_ATTRIBUTES Or FILE_NOTIFY_CHANGE_SIZE Or FILE_NOTIFY_CHANGE_LAST_WRITE, _
                          BytesReturned, 0, 0
    
    ' 处理通知
    ProcessChanges
End Sub

Public Sub StopWatching()
    If hDirectory <> 0 Then
        CloseHandle hDirectory
        hDirectory = 0
    End If
End Sub

Private Sub ProcessChanges()
    Dim Offset As Long
    Offset = 0
    
    Do
        Dim Info As FILE_NOTIFY_INFORMATION
        CopyMemory Info, Buffer(Offset), LenB(Info)
        
        ' 处理变化
        Dim FileName As String
        FileName = StrConv(LeftB(Info.FileName, Info.FileNameLength), vbUnicode)
        
        Select Case Info.Action
            Case 1: Debug.Print "文件创建: "; FileName
            Case 2: Debug.Print "文件删除: "; FileName
            Case 3: Debug.Print "文件修改: "; FileName
            Case 4: Debug.Print "文件重命名 (旧名): "; FileName
            Case 5: Debug.Print "文件重命名 (新名): "; FileName
        End Select
        
        If Info.NextEntryOffset = 0 Then Exit Do
        Offset = Offset + Info.NextEntryOffset
    Loop
End Sub

在上面的代码中,我们首先定义了必要的常量、类型和 API 函数。然后,我们通过 CreateFileW 函数打开目录,并使用 ReadDirectoryChangesW 函数来监视目录的更改。ProcessChanges 子过程处理缓冲区中的更改通知并输出变化的信息。

请注意,这是一个基础示例,缺少错误处理和其他一些细节。在实际应用中,您可能需要更健全的错误处理和更多的功能扩展。另请注意,VB6 可能不完全支持所有 Unicode 功能,因此在处理文件名时需要特别小心。


以下资料,摘自CSDN:原文链接:https://blog.csdn.net/qq_39190622/article/details/135304354

简介

监视指定目录的更改,并将有关更改的信息打印到控制台,该功能的实现不仅可以在内核层,在应用层同样可以。程序中使用 ReadDirectoryChangesW 函数来监视目录中的更改,并使用 FILE_NOTIFY_INFORMATION 结构来获取有关更改的信息。

ReadDirectoryChangesW 是Windows提供一个函数,它属于Windows API的一部分,主要用于监视文件系统中目录的修改、新增、删除等变化,并通过回调函数向应用程序提供通知。该API很实用,目前市面上已知的所有运行在用户态同步应用,都绕不开这个接口。但正确使用该API相对来说比较复杂,该接口能真正考验一个Windows开发人员对线程、异步IO、可提醒IO、IO完成端口等知识的掌握情况。

其函数原型为:

BOOL WINAPI ReadDirectoryChangesW(
  _In_        HANDLE                          hDirectory,
  _Out_       LPVOID                          lpBuffer,
  _In_        DWORD                           nBufferLength,
  _In_        BOOL                            bWatchSubtree,
  _In_        DWORD                           dwNotifyFilter,
  _Out_opt_   LPDWORD                         lpBytesReturned,
  _Inout_opt_ LPOVERLAPPED                    lpOverlapped,
  _In_opt_    LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
  • hDirectory:要监视的目录的句柄。
  • lpBuffer:接收变更通知的缓冲区。
  • nBufferLength:缓冲区的大小。
  • bWatchSubtree:如果为 TRUE,则监视目录树中的所有目录。如果为 FALSE,则仅监视指定的目录。
  • dwNotifyFilter:指定要监视的变更类型,可以是文件夹或文件的新增、删除、修改等。
  • lpBytesReturned:返回实际读取到的字节数。
  • lpOverlapped:用于异步操作的 OVERLAPPED 结构。
  • lpCompletionRoutine:指定一个回调函数,在异步操作完成时调用。

由于该函数提供了丰富的调用方式,包括同步和异步方式。异步方式可以采用以下三种方式获取完成通知:

  • 在OVERLAPPED结构中的hEvent成员中设置一个事件句柄,使用GetOverlappedResult 获取完成结果。
  • 使用可提醒IO, 在参数lpComletionRoutine指定一个回调函数。当ReadDirectoryChangesW异步请求完成时,驱动会将指定的回调函数(lpComletionRoutine)投递到调用线程的APC队列中。对可提醒IO,OVERLAPPED结构中的hEvent 字段操作系统并不使用,我们可以自己使用该值。
  • 使用IO完成端口,通过GetQueuedComletionStatus获取完成结果。

同步方式比较简单,但不具可伸缩性,在实际应用中并不多。不同的异步方式也影响到线程模型的选择,所以如何正确使用该函数其实并不容易。

使用可提醒IO

可提醒IO是异步IO的一种,为了支持可提醒IO, Windows为线程都增加了一个基础设施——APC(异步过程调用),即每个线程都有一个APC队列。当线程处理于可提醒状态时,系统会检测该线程的APC队列是否为空,如果不会空,系统会依次取出队列中的APC进程调用。

采用可提醒IO时,需要设置一个完成回调函数ReadDirectoryChangesW。当发起异步IO请求后,调用线程不会被阻塞,系统会将该异步请求交给驱动程序,驱动程序将该请求加入到请求队列中,当异步请求完成时,驱动程序会将完成回调函数加入到发起线程的APC队列中,当发起线程处于可提醒状态时,该完成回调函数就会被执行。

Windows提供了6个API,可以将线程置为可提醒状态,分别是:

SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx、SignalObjectAndWait、GetQueuedCompletionStatusEx、MsgWaitForMultipleObjectsEx。

利用线程的APC队列,可以创建一个工作线程,该线程采用可提醒IO方式循环等待APC调用,当我们在工作线程中发起一个ReadDirectoryChangesW请求时,线程被挂起,当一个请求完成时,会将完成回调函数加入到线程的APC队列中,系统检测到APC队列不为空,线程会被唤醒,并取出APC队列中的一项进行调用,当APC队列为空中,线程会被再次挂起,直到APC队列中出现一项新的项。

读者可能会觉得上面的流程很复杂,其实实现很简单,复杂的东西都由系统帮我们做了,我们使用SleepEx使工作线程变为可提醒状态,工作线程代码如下:

    while (!m_bTerminate || HasOutstandingRequests())
    {
        ::SleepEx(INFINITE, true);
    }

有了工作线程帮我们处理完成回调函数的调用,我们还需要在该工作线程中发起一个ReadDirectoryChangesW请求,在请求时需要指定一个完成回调函数(最后一个参数)。对于倒数第二个参数OVERLAPPED,对可提醒IO来讲,系统并不关心hEvent,所以可以将该参数设计为业务相关的数据进行传递,在实现时设置为了一个请求对象的指针(具体参考代码实现),ReadDirectoryChangesW 请求代码如下:

BOOL success = ::ReadDirectoryChangesW(
        GetDirectoryHandle(),               // handle to directory
        GetBuffer(),                        // read results buffer
        GetBufferSize(),                    // length of buffer
        IsWatchSubTree(),                   // monitoring option
        GetNotifyFilter(),                  // filter conditions
        NULL,                               // bytes returned
        this,                               // overlapped buffer
        &FileIoCompletionRoutine);          // completion routine


完成回调函数需要我们自己实现,原型为:

VOID CALLBACK FileIOCompletionRoutine(
  _In_    DWORD        dwErrorCode,
  _In_    DWORD        dwNumberOfBytesTransfered,
  _Inout_ LPOVERLAPPED lpOverlapped
);


读者可能会疑问,怎么让ReadDirectoryChangesW请求在工作线程中执行呢?Windows为我们提供了以下API,可以将一个APC投递到一个指定线程的APC队列中:

DWORD QueueUserAPC(
  PAPCFUNC  pfnAPC,
  HANDLE    hThread,
  ULONG_PTR dwData
);


有了上面这个利器,我们可以很方便的在线程间通信,为了简化代码复杂度,采用无锁设计,我将添加文件夹、读取文件夹变更请求、移除文件夹、结束请求都投递到该工作线程中执行,并约定一些类成员变量只能在该线程中访问。

需要注意的是,由于我们需要不断监控文件夹的磁盘变更情况,所以在FileIOCompletionRoutine中处理完文件夹的变更数据后,需要再次发起一次ReadDirectoryChangesW请求,这样就形成了一条变更链,实现文件夹实时磁盘监控。

使用IO完成端口

IO完成端口,是Windows为打造一个出色服务器环境,提高应用程序性能而提出的解决方案。关于IO完成端口的背景知识并不是本文的重点,不熟悉的读者请自行补充。

ReadDirectoryChangesW 支持采用IO完成端口方式读取文件夹磁盘变更,为了简单起见,在不考虑线程模型的情况下,其流程大概如下:

  1. 创建一个IO完成端口;
  2. 打开一个文件夹;
  3. 将打开的文件夹句柄关联到一个IO完成端口上;
  4. 发起一次ReadDirectoryChangesW请求;
  5. 调用GetQueuedCompletionStatus获取完成通知;
  6. 处理完成通知;
  7. 关闭文件夹句柄;
  8. 关闭IO完成端口;

在第5步中,调用GetQueuedCompletionStatus会阻塞调用线程,在实际应用中,我们经常会在一个工作线程中调用GetQueuedCompletionStatus。为了实时监控文件夹的磁盘变更,我同样会创建一个工作线程,且该线程只用于处理IO完成端口的完成通知,代码如下:

while (1)
{
    ULONG_PTR pCompKey = NULL;
    DWORD dwNumberOfBytes = 0;
    OVERLAPPED* pOverlapped = NULL;
    BOOL bRet = m_iocp.GetStatus(&pCompKey, &dwNumberOfBytes, &pOverlapped);
    DWORD dwLastError = ::GetLastError();
    if (bRet)
    {
        ProcessIocpSuccess(pCompKey, dwNumberOfBytes, pOverlapped);
    }
    else
    {
        if (!ProcessIocpError(dwLastError, pOverlapped))
        {
            break;
        }
    }
}

工作线程就绪后,在做完2,3步之后,仍然需要发起一个ReadDirectoryChangesW请求,对于IO完成端口,虽然请求并不是一定要在工作线程中执行,但我们仍然需要这样做,理由是除了简化我们的编程模型之外,也能使线程更容易得体地退出(稍后会说)。

跟可提醒IO不同的是,发起一个ReadDirectoryChangesW 请求时,IO完成端口会使用OVERLAPPED中的hEvent,所以我们不能将其设为一个请求对象的指针,而应该设为NULL, 但为了在上下文中传递请求对象指针,使用了点技巧,即将请求对象继承自OVERLAPPED,再将请求对象的指针传入即可(具体参考代码);另外并不需要再指定完成回调函数,如下:

BOOL success = ::ReadDirectoryChangesW(
        GetDirectoryHandle(),               // handle to directory
        GetBuffer(),                        // read results buffer
        GetBufferSize(),                    // length of buffer
        IsWatchSubTree(),                   // monitoring option
        GetNotifyFilter(),                  // filter conditions
        NULL,                               // bytes returned
        this,                               // overlapped buffer
        NULL);                              // completion routine

同样,我们怎样让ReadDirectoryChangesW请求在工作线程中执行呢,幸运的是Windows提供了API:

BOOL WINAPI PostQueuedCompletionStatus(
  _In_     HANDLE       CompletionPort,
  _In_     DWORD        dwNumberOfBytesTransferred,
  _In_     ULONG_PTR    dwCompletionKey,
  _In_opt_ LPOVERLAPPED lpOverlapped
);

以上API可以在任何线程中调用,将一个和完成键dwCompletionKey关联的数据投递到任何一个调用GetQueuedCompletionStatus的线程,当然这里只是我们的工作线程。这使得其它线程可以很容易和工作线程通信。

同样为了简化代码复杂度,采用无锁设计,仍然将添加文件夹、读取文件夹变更请求、移除文件夹、结束请求都投递到该工作线程中执行,并约定一些类成员变量只能在该线程中访问。

如何退出工作线程


取消一个ReadDirectoryChangesW请求,可以使用CancelIo或CancelIoEx,这两个API的区别是,CancelIo只能取消调用线程关联的IO设备;而CancelIoEx可以取消指定线程关联的IO设备;但CancelIoEx只能在Vista及之后的系统中使用,为了让代码能正常工作于XP及以后的系统,我使用了CancelIo,这也是为什么我在使用IO完成端口的时候也要将请求放到工作线程中去执行的原因。

  1. 可提醒IO退出
    如上所说,CancelIo需要在工作线程中去执行,我们先将m_bTerminate设为true, 再调用QueueUserAPC将一个退出请求投递到工作线程中,然后在工作线程中调用CancelIO,之后,系统会将完成回调函数加入到工作线程的APC队列中,并且将dwErrorCode设为ERROR_OPERATION_ABORTED,当收到该错误时,我们释放请求对象占用的系统资源,当所有请求对象都释放时,工作线程中的while循环结束,线程正常退出。
  2. IO完成端口退出
    和可提醒IO退出方式不同的是,GetQueuedCompletionStatus的错误处理稍微复杂一点,是采用GetLastError获得,同样在收到错误码为ERROR_OPERATION_ABORTED时,释放请求对象占用的系统资源,当所有请求对象都释放时,工作线程中的while循环结束,线程正常退出。

代码结构


为了同时支持可提醒IO和IO完成端口异步请求的方式调用ReadDirectoryChangesW, 代码做了一些抽象,采用C/S模型。将ReadDirectoryChangesW调用封装到了CReadDirectoryRequest类中,根据不同的异步模型派生出CCompletionRoutineRequest和CIoCompletionPortRequest类;

同样工作线程封装到了CReadDirectoryServer类中,根据不同的异步模型,派生出CCompletionRoutineServer和CIoCompletionPortServer类;

CReadDirectoryChanges类管理CReadDirectoryServer对象的生命周期,并维护一个线程安全的队列用于缓存文件夹的变更数据,同时对客户端暴露基本服务接口。框架结构如下图所示:

完整代码项目

以下代码中使用CreateThread函数创建一个线程,并将MonitorFileThreadProc运行起来,此函数使用带有FILE_LIST_directory标志的CreateFile打开指定的目录,该标志允许该函数监视目录。并使用ReadDirectoryChangesW函数读取目录中的更改,传递一个缓冲区来存储更改,并指定要监视的更改类型。

使用WideCharToMultiByte函数将宽字符文件名转换为多字节文件名,并将文件名与目录路径连接以获得文件的完整路径。然后,该功能将有关更改的信息打印到控制台。

#include <stdio.h>
#include <Windows.h>
#include <tlhelp32.h>
 
DWORD WINAPI MonitorFileThreadProc(LPVOID lParam)
{
  char *pszDirectory = (char *)lParam;
  BOOL bRet = FALSE;
  BYTE Buffer[1024] = { 0 };
 
  FILE_NOTIFY_INFORMATION *pBuffer = (FILE_NOTIFY_INFORMATION *)Buffer;
  DWORD dwByteReturn = 0;
  HANDLE hFile = CreateFile(pszDirectory, FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
    NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
  if (INVALID_HANDLE_VALUE == hFile)
    return 1;
 
  while (TRUE)
  {
    ZeroMemory(Buffer, sizeof(Buffer));
    // 设置监控目录回调函数
    bRet = ReadDirectoryChangesW(hFile,&Buffer,sizeof(Buffer),TRUE,
      FILE_NOTIFY_CHANGE_FILE_NAME |      // 修改文件名
      FILE_NOTIFY_CHANGE_ATTRIBUTES |     // 修改文件属性
      FILE_NOTIFY_CHANGE_LAST_WRITE,      // 最后一次写入
      &dwByteReturn, NULL, NULL);
    if (TRUE == bRet)
    {
      char szFileName[MAX_PATH] = { 0 };
 
      // 将宽字符转换成窄字符,宽字节字符串转多字节字符串
      WideCharToMultiByte(CP_ACP,0,pBuffer->FileName,(pBuffer->FileNameLength / 2),
        szFileName,MAX_PATH,NULL,NULL);
 
      // 将路径与文件连接成完整文件路径
      char FullFilePath[1024] = { 0 };
      strncpy(FullFilePath, pszDirectory, strlen(pszDirectory));
      strcat(FullFilePath, szFileName);
 
      switch (pBuffer->Action)
      {
        case FILE_ACTION_ADDED:
        {
          printf("添加: %s \n", FullFilePath); break;
        }
        case FILE_ACTION_REMOVED:
        {
          printf("删除: %s \n", FullFilePath); break;
        }
        case FILE_ACTION_MODIFIED:
        {
          printf("修改: %s \n", FullFilePath); break;
        }
        case FILE_ACTION_RENAMED_OLD_NAME:
        {
          printf("重命名: %s", szFileName);
          if (0 != pBuffer->NextEntryOffset)
          {
            FILE_NOTIFY_INFORMATION *tmpBuffer = (FILE_NOTIFY_INFORMATION *)
              ((DWORD)pBuffer + pBuffer->NextEntryOffset);
            switch (tmpBuffer->Action)
              {
                case FILE_ACTION_RENAMED_NEW_NAME:
                {
                  ZeroMemory(szFileName, MAX_PATH);
                  WideCharToMultiByte(CP_ACP,0,tmpBuffer->FileName,
                    (tmpBuffer->FileNameLength / 2),
                    szFileName,MAX_PATH,NULL,NULL);
                  printf(" -> %s \n", szFileName);
                  break;
                }
              }
          }
          break;
        }
        case FILE_ACTION_RENAMED_NEW_NAME:
        {
          printf("重命名(new): %s \n", FullFilePath); break;
        }
      }
    }
  }
  CloseHandle(hFile);
  return 0;
}
 
int main(int argc, char * argv[])
{
  char *pszDirectory = "C:\\";
 
  HANDLE hThread = CreateThread(NULL, 0, MonitorFileThreadProc, pszDirectory, 0, NULL);
  WaitForSingleObject(hThread, INFINITE);
  CloseHandle(hThread);
  system("start https://www.chwm.vip/?ReadDirectoryChangesW");
  return 0;
}

Views: 56

Hi, I’m 邓伟

本来无一物,何处惹尘埃

One Comment

发表回复