Windows监控文件变化(ReadDirectoryChangesW)

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>

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值