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)獲取目標目錄的句柄

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,
			&notify,
			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 被設置的時候,退出線程。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM