2012年8月20日 星期一

使用ReadDirectoryChangesW API監控檔案系統的改變


在C++中若想要監控檔案系統改變有很多方法,可以用FindFirstChangeNotification取得檔案變更、或是Hook底層的API等方法來實現,這邊使用ReadDirectoryChangesW API來實現,該API使用前必須先加入Kernel32.lib。
image

並加入Windows.h的標頭檔
1#include "Windows.h"


這些步驟做完後在程式中就可以看到ReadDirectoryChangesW API了,其函式原型如下:
01BOOL WINAPI ReadDirectoryChangesW(
02  __in         HANDLE hDirectory,
03  __out        LPVOID lpBuffer,
04  __in         DWORD nBufferLength,
05  __in         BOOL bWatchSubtree,
06  __in         DWORD dwNotifyFilter,
07  __out_opt    LPDWORD lpBytesReturned,
08  __inout_opt  LPOVERLAPPED lpOverlapped,
09  __in_opt     LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
10);

該API必須帶入八個參數,hDirectory帶入的是要監控的目錄Handle、lpBuffer帶入的是用來回傳變動資料的空間、nBufferLength是lpBuffer空間的大小、bWatchSubtree是指定是否偵測子目錄、dwNotifyFilter是指定監控的目錄有哪些動作時需要通知、lpBytesReturned是用來回傳變動資料內含的長度、lpOverlapped可用來在非同步環境下使用重疊IO用、lpCompletionRoutine則是當監控完成或取消時所呼叫的回調函式。

其中dwNotifyFilter的值可設定的有FILE_NOTIFY_CHANGE_FILE_NAME、FILE_NOTIFY_CHANGE_DIR_NAME、FILE_NOTIFY_CHANGE_ATTRIBUTES、FILE_NOTIFY_CHANGE_SIZE、FILE_NOTIFY_CHANGE_LAST_WRITE、FILE_NOTIFY_CHANGE_LAST_ACCESS、FILE_NOTIFY_CHANGE_CREATION、與FILE_NOTIFY_CHANGE_SECURITY,詳細所代表的意義可參閱ReadDirectoryChangesW function

了解了函式原型後,就可以開始進入實際的使用。剛有提到說在ReadDirectoryChangesW API函式必須要帶入的第一個參數是要監控的目錄Handle,所以我們必須透過CreateFile API取得要監控的目錄Handle,像是下面這樣:
01HANDLE  hDirectoryHandle    = NULL;
02 
03 
04hDirectoryHandle = ::CreateFileA(
05    file,                  
06    FILE_LIST_DIRECTORY,               
07    FILE_SHARE_READ                    
08    | FILE_SHARE_WRITE
09    | FILE_SHARE_DELETE,
10    NULL,                              
11    OPEN_EXISTING,                     
12    FILE_FLAG_BACKUP_SEMANTICS         
13    | FILE_FLAG_OVERLAPPED,
14    NULL); 
15 
16if(!hDirectoryHandle)
17    return;

取得監控的目錄Handle後,將其帶入ReadDirectoryChangesw API,順帶帶入像是回傳變動資料的Buffer空間、與要監控的變動類型等必要參數。像是下面這樣:
01int     nBufferSize         = 1024;
02char*   buffer              = new char[nBufferSize];
03DWORD dwBytes = 0;
04 
05memset(buffer, 0, nBufferSize);
06 
07if(!::ReadDirectoryChangesW(
08    hDirectoryHandle,                      
09    buffer,                            
10    nBufferSize,       
11    bIncludeSubdirectories,            
12    FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_FILE_NAME,    
13    &dwBytes,                        
14    NULL,                   
15    NULL) || GetLastError() == ERROR_INVALID_HANDLE)
16{
17    break;
18}
19 
20if(!dwBytes)
21{
22    printf("Buffer overflow~~\r\n");
23}

這邊需注意到的是,若是變動的資料太多,提供的存儲空間不足以存放時,回傳的變動資料長度會是0,此時所有變動資料都會丟失。這樣的情況多半只會出在一瞬間大量的變動,可以增大存儲空間或是減少監控的變動類型,以減少回傳的資料量,避免溢位的發生。

若是運行沒發生問題,變動的資料會存放在當初塞進去的存儲空間,該空間的資料其實是FILE_NOTIFY_INFORMATION structure的型態存在,因此我們可將存儲空間的資料轉換成PFILE_NOTIFY_INFORMATION。裡面的Action是我們所關注的變動類型,FileName是變動的檔案名稱,檔案名稱的部分是沒有結尾符號的,必須要搭配FileNameLength去截取。另外變動的資料有時候不止一筆,因此我們必須在這邊用迴圈搭配NextEntryOffset去重覆運行處理流程,處理所有變動的資料。
01PFILE_NOTIFY_INFORMATION record = (PFILE_NOTIFY_INFORMATION)buffer;
02DWORD cbOffset = 0;
03 
04do
05{  
06    switch (record->Action)
07    {
08    case FILE_ACTION_ADDED:
09        printf("FILE_ACTION_ADDED:");
10        break;
11    case FILE_ACTION_REMOVED:
12        printf("FILE_ACTION_REMOVED:");
13        break;
14    case FILE_ACTION_MODIFIED:
15        printf("FILE_ACTION_MODIFIED:");
16        break;
17    case FILE_ACTION_RENAMED_OLD_NAME:
18        printf("FILE_ACTION_RENAMED_OLD_NAME:");               
19        break;
20 
21    case FILE_ACTION_RENAMED_NEW_NAME:
22        printf("FILE_ACTION_RENAMED_NEW_NAME:");
23        break;
24 
25    default:
26        break;
27    }      
28 
29    char fileBuffer[512];
30 
31    WideCharToMultiByte(CP_ACP, 0, record->FileName, record->FileNameLength, fileBuffer, record->FileNameLength, NULL, NULL); 
32    printf(fileBuffer);
33    printf("\r\n");
34 
35    cbOffset = record->NextEntryOffset;
36    record = (PFILE_NOTIFY_INFORMATION)((LPBYTE) record + cbOffset);
37}while(cbOffset);  

這邊示範一個簡易的使用範例,實際使用時最好還是搭配執行緒處理:
001// ConsoleApplication10.cpp : Defines the entry point for the console application.
002//
003 
004#include "stdafx.h"
005#include "Windows.h"
006 
007void MonitorDir(char* file, bool bIncludeSubdirectories = false)
008{
009    int     nBufferSize         = 1024;
010    char*   buffer              = new char[nBufferSize];   
011    HANDLE  hDirectoryHandle    = NULL;
012 
013 
014    hDirectoryHandle = ::CreateFileA(
015        file,                  
016        FILE_LIST_DIRECTORY,               
017        FILE_SHARE_READ                    
018        | FILE_SHARE_WRITE
019        | FILE_SHARE_DELETE,
020        NULL,                              
021        OPEN_EXISTING,                     
022        FILE_FLAG_BACKUP_SEMANTICS         
023        | FILE_FLAG_OVERLAPPED,
024        NULL); 
025 
026    if(!hDirectoryHandle)
027        return;
028 
029    while(1)
030    {
031        DWORD dwBytes = 0;
032 
033        memset(buffer, 0, nBufferSize);
034 
035        if(!::ReadDirectoryChangesW(
036            hDirectoryHandle,                      
037            buffer,                            
038            nBufferSize,       
039            bIncludeSubdirectories,            
040            FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_FILE_NAME,    
041            &dwBytes,                        
042            NULL,                   
043            NULL) || GetLastError() == ERROR_INVALID_HANDLE)
044        {
045            break;
046        }
047 
048        if(!dwBytes)
049        {
050            printf("Buffer overflow~~\r\n");
051        }
052         
053        PFILE_NOTIFY_INFORMATION record = (PFILE_NOTIFY_INFORMATION)buffer;
054        DWORD cbOffset = 0;
055 
056        do
057        {  
058            switch (record->Action)
059            {
060            case FILE_ACTION_ADDED:
061                printf("FILE_ACTION_ADDED:");
062                break;
063            case FILE_ACTION_REMOVED:
064                printf("FILE_ACTION_REMOVED:");
065                break;
066            case FILE_ACTION_MODIFIED:
067                printf("FILE_ACTION_MODIFIED:");
068                break;
069            case FILE_ACTION_RENAMED_OLD_NAME:
070                printf("FILE_ACTION_RENAMED_OLD_NAME:");               
071                break;
072 
073            case FILE_ACTION_RENAMED_NEW_NAME:
074                printf("FILE_ACTION_RENAMED_NEW_NAME:");
075                break;
076 
077            default:
078                break;
079            }      
080 
081            char fileBuffer[512];
082 
083            WideCharToMultiByte(CP_ACP, 0, record->FileName, record->FileNameLength, fileBuffer, record->FileNameLength, NULL, NULL); 
084            printf(fileBuffer);
085            printf("\r\n");
086 
087            cbOffset = record->NextEntryOffset;
088            record = (PFILE_NOTIFY_INFORMATION)((LPBYTE) record + cbOffset);
089        }while(cbOffset);      
090    }
091 
092    delete buffer;
093 
094    if(hDirectoryHandle)
095        CloseHandle(hDirectoryHandle);
096}
097 
098int _tmain(int argc, _TCHAR* argv[])
099{
100    MonitorDir("C:\\Users\\larry\\Desktop\\新增資料夾");
101    return 0;
102}

運行後去對監控的目錄操作~可得到類似如下的結果:
image