---
# System prepended metadata

title: 目錄檔案監控
tags: [C++, Windows API]

---

---
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;
}
```