[C++]使用ReadDirectoryChangesW API監控檔案系統的改變


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


image



並加入Windows.h的標頭檔


#include “Windows.h”




這些步驟做完後在程式中就可以看到ReadDirectoryChangesW API了,其函式原型如下:



BOOL WINAPI ReadDirectoryChangesW(
in HANDLE hDirectory, out LPVOID lpBuffer,
in DWORD nBufferLength, in BOOL bWatchSubtree,
in DWORD dwNotifyFilter, out_opt LPDWORD lpBytesReturned,
inout_opt LPOVERLAPPED lpOverlapped, in_opt LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);



該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,像是下面這樣:



HANDLE hDirectoryHandle = NULL;

hDirectoryHandle = ::CreateFileA(
file,
FILE_LIST_DIRECTORY,
FILE_SHARE_READ
| FILE_SHARE_WRITE
| FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS
| FILE_FLAG_OVERLAPPED,
NULL);

if(hDirectoryHandle == INVALID_HANDLE_VALUE)
return;



取得監控的目錄Handle後,將其帶入ReadDirectoryChangesw API,順帶帶入像是回傳變動資料的Buffer空間、與要監控的變動類型等必要參數。像是下面這樣:



int nBufferSize = 1024;
char* buffer = new char[nBufferSize];
DWORD dwBytes = 0;

memset(buffer, 0, nBufferSize);

if(!::ReadDirectoryChangesW(
hDirectoryHandle,
buffer,
nBufferSize,
bIncludeSubdirectories,
FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_FILE_NAME,
&dwBytes,
NULL,
NULL) || GetLastError() == ERROR_INVALID_HANDLE)
{
break;
}

if(!dwBytes)
{
printf(“Buffer overflow~~\r\n”);
}



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



若是運行沒發生問題,變動的資料會存放在當初塞進去的存儲空間,該空間的資料其實是FILE_NOTIFY_INFORMATION structure的型態存在,因此我們可將存儲空間的資料轉換成PFILE_NOTIFY_INFORMATION。裡面的Action是我們所關注的變動類型,FileName是變動的檔案名稱,檔案名稱的部分是沒有結尾符號的,必須要搭配FileNameLength去截取。另外變動的資料有時候不止一筆,因此我們必須在這邊用迴圈搭配NextEntryOffset去重覆運行處理流程,處理所有變動的資料。



PFILE_NOTIFY_INFORMATION record = (PFILE_NOTIFY_INFORMATION)buffer;
DWORD cbOffset = 0;

do
{
switch (record->Action)
{
case FILE_ACTION_ADDED:
printf(“FILE_ACTION_ADDED:”);
break;
case FILE_ACTION_REMOVED:
printf(“FILE_ACTION_REMOVED:”);
break;
case FILE_ACTION_MODIFIED:
printf(“FILE_ACTION_MODIFIED:”);
break;
case FILE_ACTION_RENAMED_OLD_NAME:
printf(“FILE_ACTION_RENAMED_OLD_NAME:”);
break;

case FILE_ACTION_RENAMED_NEW_NAME:
    printf("FILE_ACTION_RENAMED_NEW_NAME:");
    break;

default:
    break;
}        

char fileBuffer[512];

WideCharToMultiByte(CP_ACP, 0, record->FileName, record->FileNameLength, fileBuffer, record->FileNameLength, NULL, NULL);  
printf(fileBuffer);
printf("\r\n");

cbOffset = record->NextEntryOffset;
record = (PFILE_NOTIFY_INFORMATION)((LPBYTE) record + cbOffset);

}while(cbOffset);



這邊示範一個簡易的使用範例,實際使用時最好還是搭配執行緒處理:



// ConsoleApplication10.cpp : Defines the entry point for the console application.
//

#include “stdafx.h”

#include “Windows.h”

void MonitorDir(char file, bool bIncludeSubdirectories = false)
{
int nBufferSize = 1024;
char
buffer = new char[nBufferSize];
HANDLE hDirectoryHandle = NULL;

hDirectoryHandle = ::CreateFileA(
    file,                    
    FILE_LIST_DIRECTORY,                
    FILE_SHARE_READ                        
    | FILE_SHARE_WRITE
    | FILE_SHARE_DELETE,
    NULL,                               
    OPEN_EXISTING,                      
    FILE_FLAG_BACKUP_SEMANTICS            
    | FILE_FLAG_OVERLAPPED,
    NULL);  

if(hDirectoryHandle == INVALID_HANDLE_VALUE)
    return;

while(1)
{
    DWORD dwBytes = 0;

    memset(buffer, 0, nBufferSize);

    if(!::ReadDirectoryChangesW(
        hDirectoryHandle,                        
        buffer,                                
        nBufferSize,        
        bIncludeSubdirectories,             
        FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_FILE_NAME,     
        &dwBytes,                         
        NULL,                    
        NULL) || GetLastError() == ERROR_INVALID_HANDLE)
    {
        break;
    }

    if(!dwBytes)
    {
        printf("Buffer overflow~~\r\n");
    }

    PFILE_NOTIFY_INFORMATION record = (PFILE_NOTIFY_INFORMATION)buffer;
    DWORD cbOffset = 0;

    do
    {    
        switch (record->Action)
        {
        case FILE_ACTION_ADDED:    
            printf("FILE_ACTION_ADDED:");
            break;
        case FILE_ACTION_REMOVED:
            printf("FILE_ACTION_REMOVED:");
            break;
        case FILE_ACTION_MODIFIED:
            printf("FILE_ACTION_MODIFIED:");
            break;
        case FILE_ACTION_RENAMED_OLD_NAME:
            printf("FILE_ACTION_RENAMED_OLD_NAME:");                
            break;

        case FILE_ACTION_RENAMED_NEW_NAME:
            printf("FILE_ACTION_RENAMED_NEW_NAME:");
            break;

        default:
            break;
        }        

        char fileBuffer[512];

        WideCharToMultiByte(CP_ACP, 0, record->FileName, record->FileNameLength, fileBuffer, record->FileNameLength, NULL, NULL);  
        printf(fileBuffer);
        printf("\r\n");

        cbOffset = record->NextEntryOffset;
        record = (PFILE_NOTIFY_INFORMATION)((LPBYTE) record + cbOffset);
    }while(cbOffset);        
}

delete buffer;

if(hDirectoryHandle)
    CloseHandle(hDirectoryHandle);

}

int _tmain(int argc, _TCHAR* argv[])
{
MonitorDir(“C:\Users\larry\Desktop\新增資料夾”);
return 0;
}



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


image



Link



  • ReadDirectoryChangesW function


  • Obtaining Directory Change Notifications