Windows监控文件变化(ReadDirectoryChangesW)

来源:互联网 发布:淘宝魔镜免费版 编辑:程序博客网 时间:2024/04/30 01:03

Windows提供了几种方式对文件和目录进行监控,包括:FindFirstChangeNotification、ReadDirectoryChangesW、变更日志(Change Journal)等。

(1)FindFirstChangeNotification函数,可以监控到目标目录及其子目录中所有文件的变化,但不能监控到具体是哪一个文件发生改变。

(2)ReadDirectoryChangesW 能监控到目标目录下某一文件发生改变,并且可以知道发生变化的是哪一个文件。

注意,FindFirstChangeNotification 和 ReadDirectoryChangesW 是互斥的,不能同时使用。

(3)变更日志(Change Journal)可以跟踪每一个变更的细节,即使你的软件没有运行。很帅的技术,但也相当难用。


以下就我使用的ReadDirectoryChangesW 进行说明。

该函数定义为:

 BOOL WINAPI ReadDirectoryChangesW(        HANDLE hDirectory,   // 对目录进行监视的句柄        LPVOID lpBuffer,     // 一个指向FILE_NOTIFY_INFORMATION结构体的缓冲区,其中可以将获取的数据结果将其返回。        DWORD nBufferLength, // 指lpBuffer的缓冲区的大小值,以字节为单位。        BOOL bWatchSubtree, // 是否监视子目录.         DWORD dwNotifyFilter, // 对文件过滤的方式和标准       LPDWORD lpBytesReturned, // 将接收的字节数转入lpBuffer参数       LPOVERLAPPED lpOverlapped, // 一般选择 NULL      LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 一般选择 NULL );
其中结构体FILE_NOTIFY_INFORMATION 的用法下一章将会讲到。

 利用ReadDirectoryChangesW函数实现对一个目录进行监控的简单做法是:首先使用CreateFile获取要监控目录的句柄;然后在一个判断循环里面调用ReadDirectoryChangesW,并且把自己分配的用来存放目录变化通知的内存首地址、内存长度、目录句柄传给该函数。用户代码在该函数的调用中进行同步等待。当目录中有文件发生改变,控制函数把目录变化通知存放在指定的内存区域内,并把发生改变的文件名、文件所在目录和改变通知处理。

(1)获取目标目录的句柄

<span style="white-space:pre">HANDLE m_hDirectory=CreateFile(m_szWatchDirectory,<span style="white-space:pre"></span>GENERIC_READ | GENERIC_WRITE | FILE_LIST_DIRECTORY ,<span style="white-space:pre"></span>FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,<span style="white-space:pre"></span>NULL,<span style="white-space:pre"></span>OPEN_EXISTING,<span style="white-space:pre"></span>FILE_FLAG_BACKUP_SEMANTICS  | FILE_FLAG_OVERLAPPED,  <span style="white-space:pre"></span>NULL);<span style="white-space:pre"></span>if(m_hDirectory==INVALID_HANDLE_VALUE)<span style="white-space:pre"></span>{<span style="white-space:pre"></span>DWORD dwErr=GetLastError();<span style="white-space:pre"></span>LOG_INFO(_T("文件监控,打开目录错误! %s (LastError: %d)"), m_szWatchDirectory, dwErr);<span style="white-space:pre"></span>return;<span style="white-space:pre"></span>}</span>
<span style="white-space:pre"></span>注:FILE_FLAG_BACKUP_SEMANTICS ,使用这个标志需要管理员权限。
<span style="font-family:Arial;"></span><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; font-family: Arial; font-size: 14px; line-height: 26px;"><span style="white-space:pre"></span>共享模式中如果不使用 FILE_SHARE_DELETE,会导致其他进程无法重命名或者删除这个目录下的文件。</p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; font-family: Arial; font-size: 14px; line-height: 26px;">  <span style="white-space:pre"></span>这个函数有一个风险在于,被引用的目录本身处于”使用中“,并且无法被删除。如果希望在监控目录的同时,还允许目录被删除,则应当监控该目录的父目录及父目录下的文件和子目录。</p>(2)<span style="font-family: Arial;">调用 ReadDirectoryChangesW</span>
<span style="font-family: Arial;">由于</span><span style="font-family: Arial;">ReadDirectoryChangesW</span><span style="font-family: Arial;">为阻塞函数,以下代码建议在线程中进行。(VS2010)</span>
<span style="font-family: Arial;">char notify[1024]; memset(notify, 0, sizeof(notify)); FILE_NOTIFY_INFORMATION *pNotification=(FILE_NOTIFY_INFORMATION *)notify; DWORD BytesReturned=0; while (TRUE){<span style="white-space:pre"></span>memset(pNotification,'\0', sizeof(notify));<span style="white-space:pre"></span>watch_state=ReadDirectoryChangesW(m_hDirectory,<span style="white-space:pre"></span>&notify,<span style="white-space:pre"></span>sizeof(notify),<span style="white-space:pre"></span>TRUE,<span style="white-space:pre"></span>//监控子目录<span style="white-space:pre"></span>FILE_NOTIFY_CHANGE_FILE_NAME| FILE_NOTIFY_CHANGE_CREATION |FILE_NOTIFY_CHANGE_LAST_WRITE , <span style="white-space:pre"></span>(LPDWORD)&BytesReturned,<span style="white-space:pre"></span>NULL,<span style="white-space:pre"></span>NULL);<span style="white-space:pre"></span>if (GetLastError()==ERROR_INVALID_FUNCTION)<span style="white-space:pre"></span>{<span style="white-space:pre"></span>LOG_INFO(_T("文件监控,系统不支持! %s"), m_szWatchDirectory);<span style="white-space:pre"></span>return;<span style="white-space:pre"></span>}<span style="white-space:pre"></span>else if(watch_state == FALSE)<span style="white-space:pre"></span>{<span style="white-space:pre"></span>DWORD dwErr = GetLastError();<span style="white-space:pre"></span>LOG_INFO(_T("文件监控,监控失败! %s (LastError: %d)"), m_szWatchDirectory, dwErr);<span style="white-space:pre"></span>return;<span style="white-space:pre"></span>}<span style="white-space:pre"></span>else if (GetLastError()==ERROR_NOTIFY_ENUM_DIR)<span style="white-space:pre"></span>{<span style="white-space:pre"></span>LOG_INFO(_T("文件监控,内存溢出! %s"), m_szWatchDirectory);<span style="white-space:pre"></span>return;<span style="white-space:pre"></span>}<span style="white-space:pre"></span>else<span style="white-space:pre"></span>{<span style="white-space:pre"></span>//这里主要就是检测返回的信息,<span style="white-space:pre"></span>CString szFileName(pNotification->FileName, pNotification->FileNameLength / sizeof(wchar_t));<span style="white-space:pre"></span>if (pNotification->Action==FILE_ACTION_ADDED)<span style="white-space:pre"></span>{<span style="white-space:pre"></span>LOG_INFO(_T("文件监控,新增文件! %s\\%s"), m_szWatchDirectory, szFileName);<span style="white-space:pre"></span>}<span style="white-space:pre"></span>else if (pNotification->Action==FILE_ACTION_REMOVED)<span style="white-space:pre"></span>{<span style="white-space:pre"></span>LOG_INFO(_T("文件监控,删除文件! %s\\%s"), m_szWatchDirectory, szFileName); <span style="white-space:pre"></span>}<span style="white-space:pre"></span>else if (pNotification->Action==FILE_ACTION_MODIFIED)<span style="white-space:pre"></span>{<span style="white-space:pre"></span>LOG_INFO(_T("文件监控,修改文件! %s\\%s"), m_szWatchDirectory, szFileName);<span style="white-space:pre"></span>}<span style="white-space:pre"></span>else if (pNotification->Action==FILE_ACTION_RENAMED_OLD_NAME)<span style="white-space:pre"></span>{<span style="white-space:pre"></span>LOG_INFO(_T("文件监控,重命名文件! %s\\%s"), m_szWatchDirectory, szFileName); <span style="white-space:pre"></span><span style="white-space:pre"></span>//szFileName为旧名称,从pNotification->FileName 偏移pNotification->NextEntryOffset处获取的新名称<span style="white-space:pre"></span>}<span style="white-space:pre"></span>else if (pNotification->Action==FILE_ACTION_RENAMED_NEW_NAME) <span style="white-space:pre"></span>{<span style="white-space:pre"></span>//还没出现过这种情况<span style="white-space:pre"></span>LOG_INFO(_T("文件监控,重命名文件2! %s\\%s"), m_szWatchDirectory, szFileName);<span style="white-space:pre"></span>}<span style="white-space:pre"></span>//PostMessage 通知主线程<span style="white-space:pre"></span>}}CloseHandle(m_hDirectory);</span>
<span style="font-family: Arial;"></span><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; font-family: Arial; font-size: 14px; line-height: 26px;">(3)说明:</p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; font-family: Arial; font-size: 14px; line-height: 26px;">A、ReadDirectoryChangesW 数据缓冲区中使用的都是宽字节Unicode,字符串不是 NULL 结尾的,所以不能使用 wcscpy。如果你使用 ATL 或 MFC 的 CString 类,方法在上面代码中有。</p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; font-family: Arial; font-size: 14px; line-height: 26px;">B、使用While循环,就是要在每次监测到一次变化后,重新发起新的 ReadDirectoryChangesW 调用。</p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; font-family: Arial; font-size: 14px; line-height: 26px;"><span style="font-family: Arial; font-size: 14px; line-height: 26px;"><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px; white-space: pre-wrap;">C、如果很多文件在短时间内发生变更,则有可能会丢失部分通知。</span></span></p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; font-family: Arial; font-size: 14px; line-height: 26px;"><span style="font-family: Arial; font-size: 14px; line-height: 26px;"><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px; white-space: pre-wrap;">D、如果缓冲区溢出,整个缓冲区的内容都会被丢弃,BytesReturned会返回0。</span></span></p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; font-family: Arial; font-size: 14px; line-height: 26px;"><span style="font-family: Arial; font-size: 14px; line-height: 26px;"><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px; white-space: pre-wrap;">E、在MSDN中,<span style="font-family: Arial; font-size: 14px; line-height: 26px;">FILE_NOTIFY_INFORMATION的文档有一个关键的描述:如果文件既有长文件名,又有短文件名,那么文件会返回其中的一个名字,但不确定是返回哪一个。大多数时候,在短文件名和长文件名之间转换都很容易,但是如果文件被删除,情况就不一样了。最好的方法是维护一个跟踪文件的列表,同时跟踪长文件名和短文件名。(这种情况目前还没有遇到过)</span></span></span></p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; font-family: Arial; font-size: 14px; line-height: 26px;"><span style="font-family: Arial; font-size: 14px; line-height: 26px;"><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px; white-space: pre-wrap;"><span style="font-family: Arial; font-size: 14px; line-height: 26px;"></span></span></span></p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; font-family: Arial; font-size: 14px; line-height: 26px;">(4)以下为本人测试的结果:</p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; font-family: Arial; font-size: 14px; line-height: 26px;">(<span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px; white-space: pre-wrap;">newName:从pNotification->FileName 偏移pNotification->NextEntryOffset处获取的新名称</span></p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; font-family: Arial; font-size: 14px; line-height: 26px;"><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px; white-space: pre-wrap;">TCHAR newName[1024]={0};ZeroMemory(newName, sizeof(newName));int length = sizeof(notify) - pNotification->NextEntryOffset;CopyMemory(newName, pNotification->FileName + pNotification->NextEntryOffset / sizeof(wchar_t), length);</span></p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; font-family: Arial; font-size: 14px; line-height: 26px;"><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px; white-space: pre-wrap;">)</span></p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; font-family: Arial; font-size: 14px; line-height: 26px;">//新建1:在目标主目录新建文件,只会出现:1次文件相关的FILE_ACTION_ADDED。//新建2:但在子目录中新建一个文件,会先后出现:1次与文件相关的FILE_ACTION_ADDED,1次与子目录相关的FILE_ACTION_MODIFIED。//复制:复制一个文件到目标主目录或子目录,会先后出现:1次文件相关的FILE_ACTION_ADDED(只有oldName,无newName),1次文件相关的FILE_ACTION_MODIFIED(oldName和newName都有)。 //fix //修改:修改文件后,会出现2次FILE_ACTION_MODIFIED。//fix (都只有oldName)//剪切再粘贴:在目标目录的两个子目录间剪切粘贴一个文件,会先出现1次FILE_ACTION_REMOVED,再出现1次FILE_ACTION_ADDED//删除:删除一个文件,先出现1次文件相关的FILE_ACTION_REMOVED,再出现1次目录相关的FILE_ACTION_MODIFIED//重命令:1次FILE_ACTION_RENAMED_OLD_NAME</p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; font-family: Arial; font-size: 14px; line-height: 26px;"></p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; font-family: Arial; font-size: 14px; line-height: 26px;"><strong>(5)关于线程退出机制</strong></p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; font-family: Arial; font-size: 14px; line-height: 26px;"><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px; white-space: pre-wrap;"><span style="white-space:pre"></span>ReadDirectoryChangesW 为阻塞型函数,很多人会使用<span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px; white-space: pre-wrap;">TerminateThread强制结束该线程,但这样会导致</span></span>资源无法释放。最好的方法是:创建一个手动重置的 Event 对象,作为 WaitForMultipleObjects 等待的第二个句柄。当 Event 被设置的时候,退出线程。</p>
<span style="font-family: Arial;"></span>

0 0