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)獲取目標目錄的句柄
HANDLE m_hDirectory=CreateFile(m_szWatchDirectory, GENERIC_READ | GENERIC_WRITE | FILE_LIST_DIRECTORY , FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL); if(m_hDirectory==INVALID_HANDLE_VALUE) { DWORD dwErr=GetLastError(); return; }
注:FILE_FLAG_BACKUP_SEMANTICS ,使用這個標志需要管理員權限。
共享模式中如果不使用 FILE_SHARE_DELETE,會導致其他進程無法重命名或者刪除這個目錄下的文件。
這個函數有一個風險:被引用的目錄本身處於”使用中“,並且無法被刪除。如果希望在監控目錄的同時,還允許目錄被刪除,則應當監控該目錄的父目錄及父目錄下的文件和子目錄。
(2)調用 ReadDirectoryChangesW
由於ReadDirectoryChangesW為阻塞函數,以下代碼建議在線程中進行。(VS2010中編譯通過)
char notify[1024]; memset(notify, 0, sizeof(notify)); FILE_NOTIFY_INFORMATION *pNotification=(FILE_NOTIFY_INFORMATION *)notify; DWORD BytesReturned=0; while (TRUE) { ZeroMemory(pNotification, sizeof(notify)); watch_state=ReadDirectoryChangesW(hDirectory, ¬ify, sizeof(notify), TRUE, //監控子目錄 FILE_NOTIFY_CHANGE_FILE_NAME |FILE_NOTIFY_CHANGE_LAST_WRITE , //FILE_NOTIFY_CHANGE_DIR_NAME FILE_NOTIFY_CHANGE_CREATION FILE_NOTIFY_CHANGE_SIZE (LPDWORD)&BytesReturned, NULL, NULL); if (GetLastError()==ERROR_INVALID_FUNCTION) { LOG_INFO(_T("文件監控,系統不支持! %s"), szWatchDirectory); break; } else if(watch_state == FALSE) { DWORD dwErr = GetLastError(); LOG_INFO(_T("文件監控,監控失敗! %s (LastError: %d)"), szWatchDirectory, dwErr); break; } else if (GetLastError()==ERROR_NOTIFY_ENUM_DIR) { LOG_INFO(_T("文件監控,內存溢出! %s"), szWatchDirectory); continue; } else { //這里主要就是檢測返回的信息,(FILE_NOTIFY_INFORMATION) CString szFileName(pNotification->FileName, pNotification->FileNameLength / sizeof(wchar_t)); if (pNotification->Action==FILE_ACTION_ADDED) { LOG_INFO(_T("文件監控,新增文件! %s\\%s"), szWatchDirectory, szFileName); } else if (pNotification->Action==FILE_ACTION_REMOVED) { LOG_INFO(_T("文件監控,刪除文件! %s\\%s"), szWatchDirectory, szFileName); } else if (pNotification->Action==FILE_ACTION_MODIFIED) { LOG_INFO(_T("文件監控,修改文件! %s\\%s"), szWatchDirectory, szFileName); } else if (pNotification->Action==FILE_ACTION_RENAMED_OLD_NAME) { LOG_INFO(_T("文件監控,重命名文件! %s\\%s"), szWatchDirectory, szFileName); } else if (pNotification->Action==FILE_ACTION_RENAMED_NEW_NAME) //還沒出現過這種情況 { LOG_INFO(_T("文件監控,重命名文件2! %s\\%s"), szWatchDirectory, szFileName); } //PostMessage通知主線程 } } CloseHandle(hDirectory);
(3)說明:
A、ReadDirectoryChangesW 數據緩沖區中使用的都是寬字節Unicode,字符串不是 NULL 結尾的,所以不能使用 wcscpy。如果你使用 ATL 或 MFC 的 CString 類,方法在上面代碼中有。
B、使用While循環,就是要在每次監測到一次變化后,重新發起新的 ReadDirectoryChangesW 調用。
C、如果很多文件在短時間內發生變更,則有可能會丟失部分通知。
D、如果緩沖區溢出,整個緩沖區的內容都會被丟棄,BytesReturned會返回0。 E、在MSDN中,FILE_NOTIFY_INFORMATION的文檔有一個關鍵的描述:如果文件既有長文件名,又有短文件名,那么文件會返回其中的一個名字,但不確定是返回哪一個。大多數時候,在短文件名和長文件名之間轉換都很容易,但是如果文件被刪除,情況就不一樣了。最好的方法是維護一個跟蹤文件的列表,同時跟蹤長文件名和短文件名。(這種情況目前還沒有遇到過)
(4)以下為本人測試的結果:(本人使用的是WINDOWS10 64位系統)
其中的newName:從pNotification->FileName 偏移pNotification->NextEntryOffset處獲取的新名稱
方法為:
TCHAR newName[1024]={0}; ZeroMemory(newName, sizeof(newName)); int length = sizeof(notify) - pNotification->NextEntryOffset; CopyMemory(newName, pNotification->FileName + pNotification->NextEntryOffset / sizeof(wchar_t), length);
//新建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
(5)關於線程退出機制
ReadDirectoryChangesW 為阻塞型函數,很多人會使用TerminateThread強制結束該線程,但這樣會導致資源無法釋放。
最好的方法是:創建一個手動重置的 Event 對象,作為 WaitForMultipleObjects 等待的第二個句柄。當 Event 被設置的時候,退出線程。