--- title: 目錄檔案監控 tags: - Windows API - C++ --- # 目錄檔案監控 ## 引用的函式庫 ```cpp #include <windows.h> #include <iostream> #include <string> #include <vector> ``` - `windows.h` - `ReadDirectoryChangesW()` 監控目錄 - `CreateFileW()`、`CloseHandle()` 與作業系統相關 - `iostream` 用來輸出 log 的 - `string`、`vector` 處理字串 ## 從 main() 開始 ```cpp int main(int argc, char* argv []) { // 判斷是否滿足參數需求 if(argc < 2) { std::wcout << "Usage: a.exe <path>" << std::endl; } std::wstring directory = StringToWString(argv[1]); WatchDirectory(directory); return 0; } ``` 會接收一個參數作為監控的根目錄,使用 `StringToWString()` 將 `char[]` 轉換成 `std::wstring`,利於後續根作業系統傳輸資料。 接著將要監控的目錄路徑 `directory `,傳遞參數呼叫 `WatchDirectory()`,會是整段程式的主角。 ### 轉換寬字元字串 StringToWString(const std::string& str) ```cpp // 把 UTF-8 字串 轉換成 wstring std::wstring StringToWString(const std::string& str) { int sizeNeeded = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, nullptr, 0); std::wstring wstr(sizeNeeded, 0); MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, &wstr[0], sizeNeeded); return wstr; } ``` 函式的參數會取參考進來,用 Windows API 的 [`MultiByteToWideChar()`](https://learn.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-multibytetowidechar) 來獲得轉換後需要的字串長度。 > [!Note] > [cpp reference](https://en.cppreference.com/w/cpp/string/basic_string/c_str) > ```cpp > str.c_str() > ``` > 回傳 `std::string` 物件中 `const char*` 指標的方法。 接著用拿到的長度 `sizeNeeded` 初始化一個新的 `std::wstring` 型別的 `wstr` 字串。然後再利用同樣的函式 [`MultiByteToWideChar()`](https://learn.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-multibytetowidechar) 來把 `str` 轉換成寬字串並放進 `wstr` 裡面。最後再回傳 `wstr` 並結束函式。 > [!Note] > ```cpp > int MultiByteToWideChar( > [in] UINT CodePage, > [in] DWORD dwFlags, > [in] _In_NLS_string_(cbMultiByte)LPCCH lpMultiByteStr, > [in] int cbMultiByte, > [out, optional] LPWSTR lpWideCharStr, > [in] int cchWideChar > ); > ``` > - **`CodePage`**,執行轉換時要使用的 Code Page,可以當作是要轉換的編碼。`CP_TUF8` 為 `65001` > - **`dwFlags`**,指出轉換類型的 flag。 > - **`lpMultiByreStr`**,要轉換之字元字串的指標。 > - **`cbMultiByte`**,`lpMultiByteStr` 參數所指示之字串的大小,以 byte 為單位。 > - **`lpWideCharStr`**,接收已轉換字串之緩衝區的指標。 > - **`cchWideChar`**,`lpWideCharStr` 的大小,以字元為單位。如果此值是 0,函式會傳回所需的緩衝區大小,以字元為單位。 ## WatchDirectory(const std::wstring& directory) ```cpp // 監控目錄並輸出 log void WatchDirectory(const std::wstring& directory) { {% 建立目錄 handle %} BYTE buffer[1024]; DWORD bytesReturned; // 監控迴圈 while(TRUE) { if(ReadDirectoryChangesW(...)) { FILE_NOTIFY_INFORMATION* pNotify = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(buffer); {% 處理 Notify 的 Action %} } else { std::wcerr << L"Failed to read directory changes. Error: " << GetLastError() << std::endl; break; } } CloseHandle(hDir); } ``` ### 建立目錄 handle ```cpp // 建立一個目錄的 handle HANDLE hDir = CreateFileW( directory.c_str(), FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL ); // 檢查 handle if(hDir == INVALID_HANDLE_VALUE) { std::wcerr << L"[-] Failed to open directory. Error: " << GetLastError() << std::endl; return; } ``` ### 監控迴圈 ```cpp // 監控迴圈 while(TRUE) { if(ReadDirectoryChangesW( hDir, buffer, sizeof(buffer), TRUE, // 設定成 TRUE 監控子目錄 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_CREATION, &bytesReturned, NULL, NULL)) { FILE_NOTIFY_INFORMATION* pNotify = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(buffer); {% 處理 Notify 的 Action %} } else { std::wcerr << L"Failed to read directory changes. Error: " << GetLastError() << std::endl; break; } } ``` > [!Note] > 擷取描述指定目錄內變更的資訊。 > ```cpp > BOOL ReadDirectoryChangesW( > [in] HANDLE hDirectory, > [out] LPVOID lpBuffer, > [in] DWORD nBufferLength, > [in] BOOL bWatchSubtree, > [in] DWORD dwNotifyFilter, > [out, optional] LPDWORD lpBytesReturned, > [in, out, optional] LPOVERLAPPED lpOverlapped, > [in, optional] LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine > ); > ``` > - **`hDirectory`**,要監視的目錄的 handle > - **`lpBuffer`**,要傳回讀取結果的 `DWORD` 對齊格式化緩衝區指標。 > - **`nBufferLength`**,`lpBuffer` 的 byte 大小。 > - **`bWatchSubtree`**,是否要監視指定目錄的所有子目錄。 > - **`dwNotifyFilter`** > - **`lpBytesReturned`** > - **`lpOverlapped`** > - **`lpCompletionRoutine`** ### 處理 Notify 的 Action ```cpp while(TRUE) { // 計算 filename 的字數 int count = pNotify->FileNameLength / sizeof(WCHAR); // 把 WCHAR array 轉換成 std::wstring std::wstring filename(pNotify->FileName, count); // 不同的 Action 輸出 log switch(pNotify->Action) {...} // 檢查有沒有下一個 Notify if(pNotify->NextEntryOffset == 0) break; // 換下一個 Notify pNotify = reinterpret_cast<FILE_NOTIFY_INFORMATION*>( reinterpret_cast<BYTE*>(pNotify) + pNotify->NextEntryOffset); } ``` #### switch(pNotify->Action) ```cpp // 不同的 Action 輸出 log switch(pNotify->Action) { case FILE_ACTION_ADDED: std::wcout << L"[+] File added: " << filename << std::endl; break; case FILE_ACTION_REMOVED: std::wcout << L"[-] File removed: " << filename << std::endl; break; case FILE_ACTION_MODIFIED: std::wcout << L"[!] File modified: " << filename << std::endl; break; case FILE_ACTION_RENAMED_OLD_NAME: std::wcout << L"[!] File renamed from: " << filename << std::endl; break; case FILE_ACTION_RENAMED_NEW_NAME: std::wcout << L"[!] File renamed to: " << filename << std::endl; break; default: std::wcout << L"[-] Unknown action." << std::endl; break; } ``` ### 函式程式碼 ## 完整程式碼 ```cpp #include <windows.h> #include <iostream> #include <string> #include <vector> // 把 UTF-8 字串 轉換成 wstring std::wstring StringToWString(const std::string& str) { int sizeNeeded = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, nullptr, 0); std::wstring wstr(sizeNeeded, 0); MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, &wstr[0], sizeNeeded); return wstr; } // 監控目錄並輸出 log void WatchDirectory(const std::wstring& directory) { // 建立一個目錄的 handle HANDLE hDir = CreateFileW( directory.c_str(), FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL ); if(hDir == INVALID_HANDLE_VALUE) { std::wcerr << L"[-] Failed to open directory. Error: " << GetLastError() << std::endl; return; } BYTE buffer[1024]; DWORD bytesReturned; while(TRUE) { if(ReadDirectoryChangesW( hDir, buffer, sizeof(buffer), TRUE, // Set to TRUE to watch subdirectories 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_CREATION, &bytesReturned, NULL, NULL)) { FILE_NOTIFY_INFORMATION* pNotify = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(buffer); do { // Calculate the length of the filename in characters int count = pNotify->FileNameLength / sizeof(WCHAR); // Convert WCHAR array to a std::wstring std::wstring filename(pNotify->FileName, count); // Process the change switch(pNotify->Action) { case FILE_ACTION_ADDED: std::wcout << L"[+] File added: " << filename << std::endl; break; case FILE_ACTION_REMOVED: std::wcout << L"[-] File removed: " << filename << std::endl; break; case FILE_ACTION_MODIFIED: std::wcout << L"[!] File modified: " << filename << std::endl; break; case FILE_ACTION_RENAMED_OLD_NAME: std::wcout << L"[!] File renamed from: " << filename << std::endl; break; case FILE_ACTION_RENAMED_NEW_NAME: std::wcout << L"[!] File renamed to: " << filename << std::endl; break; default: std::wcout << L"[-] Unknown action." << std::endl; break; } // Move to the next notification if(pNotify->NextEntryOffset == 0) { break; } pNotify = reinterpret_cast<FILE_NOTIFY_INFORMATION*>( reinterpret_cast<BYTE*>(pNotify) + pNotify->NextEntryOffset); } while(TRUE); } else { std::wcerr << L"Failed to read directory changes. Error: " << GetLastError() << std::endl; break; } } CloseHandle(hDir); } int main(int argc, char* argv []) { // 判斷是否滿足參數需求 if(argc < 2) { std::wcout << "Usage: a.exe <path>" << std::endl; } std::wstring directory = StringToWString(argv[1]); WatchDirectory(directory); return 0; } ```