C++ 文件系统监听:跨平台事件通知机制的实现

好的,各位观众老爷,欢迎来到今天的“C++ 文件系统监听:跨平台事件通知机制的实现”讲座现场! 我是你们的老朋友,人称“代码界的段子手”—— 程序员小张。 今天咱们要聊聊一个既实用又略带神秘的话题:文件系统监听。

一、什么是文件系统监听? 为什么需要它?

简单来说,文件系统监听就像一个尽职尽责的保安,时刻盯着你指定的文件或目录,一旦发生任何风吹草动(比如文件被创建、修改、删除、重命名等),它都会第一时间通知你。

那么,我们为什么需要这玩意儿呢? 想象一下以下场景:

  • 实时同步工具: 像Dropbox、Google Drive这样的云存储服务,需要实时监控本地文件的变化,并同步到云端。
  • 日志分析: 监控日志文件的变化,一旦发现新的错误或警告信息,立即发出警报。
  • 构建系统: 监控源代码文件的变化,一旦发现代码被修改,自动触发编译过程。
  • 防病毒软件: 监控特定目录下的文件,一旦发现可疑文件被创建或修改,立即进行扫描。

没有文件系统监听,以上这些功能就只能通过轮询的方式来实现,也就是隔一段时间就去检查一下文件是否发生了变化。 这种方式不仅效率低下,而且会浪费大量的系统资源。

二、跨平台挑战:同一个世界,不同的规则

C++号称“一次编写,到处编译”,但理想很丰满,现实很骨感。 文件系统监听就是一个典型的跨平台难题。 不同的操作系统提供了不同的API来实现文件系统监听,而且这些API的用法和特性也各不相同。 比如:

| 操作系统 | API | 特点 |
| Windows | ReadDirectoryChangesW | 异步I/O,需要指定要监听的事件类型,可以监听目录及其子目录的变化。 |
| Linux | inotify | 基于文件描述符,需要手动处理事件队列,可以监听目录及其子目录的变化。 |
| macOS | FSEvents | 基于事件队列,可以监听目录及其子目录的变化,但API比较复杂。 |

看到这里,你是不是已经开始头大了? 别慌,接下来我们就来一步步解决这些难题。

三、跨平台解决方案:封装与抽象

为了解决跨平台问题,我们需要对不同平台的API进行封装和抽象,提供一个统一的接口给用户使用。 这样,用户只需要调用这个统一的接口,就可以在不同的平台上实现文件系统监听功能,而无需关心底层API的差异。

我们的目标是创建一个名为 FileSystemWatcher 的类,它应该提供以下功能:

  • 启动监听: start(const std::string& path, bool recursive = true)
  • 停止监听: stop()
  • 设置回调函数: setCallback(std::function<void(const std::string&, FileSystemEvent)> callback)
  • 定义事件类型: enum class FileSystemEvent { Created, Modified, Deleted, Renamed };

接下来,我们将分别针对Windows、Linux和macOS平台实现这个FileSystemWatcher类。

四、Windows平台的实现

#ifdef _WIN32
#include <windows.h>
#include <iostream>
#include <sstream>

class FileSystemWatcher {
public:
    enum class FileSystemEvent { Created, Modified, Deleted, Renamed };

    FileSystemWatcher() : running(false), hDir(INVALID_HANDLE_VALUE) {}
    ~FileSystemWatcher() { stop(); }

    bool start(const std::string& path, bool recursive = true) {
        if (running) return false;

        directoryPath = path;
        recursiveMonitoring = recursive;
        running = true;

        hDir = CreateFileW(
            std::wstring(directoryPath.begin(), directoryPath.end()).c_str(),
            GENERIC_READ,
            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
            NULL,
            OPEN_EXISTING,
            FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
            NULL);

        if (hDir == INVALID_HANDLE_VALUE) {
            std::cerr << "Error opening directory: " << directoryPath << std::endl;
            running = false;
            return false;
        }

        // Start the monitoring thread
        monitorThread = std::thread(&FileSystemWatcher::monitorDirectory, this);

        return true;
    }

    void stop() {
        if (!running) return;

        running = false;
        if (hDir != INVALID_HANDLE_VALUE) {
            CloseHandle(hDir);
            hDir = INVALID_HANDLE_VALUE;
        }

        if (monitorThread.joinable()) {
            monitorThread.join();
        }
    }

    void setCallback(std::function<void(const std::string&, FileSystemEvent)> callback) {
        this->callback = callback;
    }

private:
    void monitorDirectory() {
        DWORD bytesReturned;
        BYTE buffer[4096];
        OVERLAPPED overlapped;
        ZeroMemory(&overlapped, sizeof(OVERLAPPED));
        overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

        while (running) {
            BOOL success = ReadDirectoryChangesW(
                hDir,
                buffer,
                sizeof(buffer),
                recursiveMonitoring,
                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,
                &bytesReturned,
                &overlapped,
                NULL);

            if (!success) {
                std::cerr << "ReadDirectoryChangesW failed: " << GetLastError() << std::endl;
                break; // Exit the loop on error
            }

            DWORD waitResult = WaitForSingleObject(overlapped.hEvent, INFINITE);
            if (waitResult == WAIT_OBJECT_0) {
                DWORD offset = 0;
                FILE_NOTIFY_INFORMATION* fni;
                do {
                    fni = (FILE_NOTIFY_INFORMATION*)((BYTE*)buffer + offset);

                    std::wstring filenameW(fni->FileName, fni->FileNameLength / sizeof(wchar_t));
                    std::string filename(filenameW.begin(), filenameW.end());
                    std::string fullPath = directoryPath + "\" + filename; // Construct the full path

                    FileSystemEvent eventType;
                    switch (fni->Action) {
                    case FILE_ACTION_ADDED:
                        eventType = FileSystemEvent::Created;
                        break;
                    case FILE_ACTION_REMOVED:
                        eventType = FileSystemEvent::Deleted;
                        break;
                    case FILE_ACTION_MODIFIED:
                        eventType = FileSystemEvent::Modified;
                        break;
                    case FILE_ACTION_RENAMED_OLD_NAME:
                        // We need to handle the new name action to get the complete rename event
                        // For simplicity, we'll ignore this and handle the RENAMED_NEW_NAME only
                        break;
                    case FILE_ACTION_RENAMED_NEW_NAME:
                        eventType = FileSystemEvent::Renamed;
                        break;
                    default:
                        eventType = FileSystemEvent::Modified; // Default to modified
                        break;
                    }
                    if (fni->Action != FILE_ACTION_RENAMED_OLD_NAME && callback) {
                      callback(fullPath, eventType);
                    }

                    offset += fni->NextEntryOffset;

                } while (fni->NextEntryOffset != 0);

                ResetEvent(overlapped.hEvent); // Reset the event for the next iteration
            } else {
                std::cerr << "WaitForSingleObject failed: " << GetLastError() << std::endl;
                break; // Exit the loop on error
            }
        }
        CloseHandle(overlapped.hEvent); // Clean up the event handle

        std::cout << "Monitoring thread stopped for: " << directoryPath << std::endl;
    }

private:
    std::string directoryPath;
    bool recursiveMonitoring;
    bool running;
    HANDLE hDir;
    std::thread monitorThread;
    std::function<void(const std::string&, FileSystemEvent)> callback;
};

#endif

代码解释:

  1. 头文件: 包含了Windows API所需的头文件。
  2. FileSystemWatcher类: 封装了文件系统监听的功能。
  3. start()方法:
    • 打开要监听的目录,获取目录句柄 hDir
    • 创建一个新的线程 monitorThread,用于执行监听操作。
  4. stop()方法:
    • 设置 running 标志为 false,停止监听线程。
    • 关闭目录句柄 hDir
    • 等待监听线程结束。
  5. setCallback()方法: 设置回调函数,用于处理文件系统事件。
  6. monitorDirectory()方法:
    • 调用 ReadDirectoryChangesW() 函数来异步监听目录的变化。
    • 使用 WaitForSingleObject() 函数等待事件发生。
    • 如果事件发生,解析 FILE_NOTIFY_INFORMATION 结构体,获取事件类型和文件名。
    • 调用回调函数 callback(),将事件信息传递给用户。

五、Linux平台的实现

#ifdef __linux__
#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <functional>
#include <sys/inotify.h>
#include <unistd.h>
#include <limits.h>
#include <algorithm>
#include <sstream>

class FileSystemWatcher {
public:
    enum class FileSystemEvent { Created, Modified, Deleted, Renamed };

    FileSystemWatcher() : running(false), fd(-1) {}
    ~FileSystemWatcher() { stop(); }

    bool start(const std::string& path, bool recursive = true) {
        if (running) return false;

        directoryPath = path;
        recursiveMonitoring = recursive;
        running = true;

        fd = inotify_init();
        if (fd < 0) {
            std::cerr << "Error initializing inotify" << std::endl;
            running = false;
            return false;
        }

        // Add the directory to watch
        int wd = inotify_add_watch(fd, directoryPath.c_str(),
            IN_CREATE | IN_MODIFY | IN_DELETE | IN_MOVE | IN_CLOSE_WRITE);

        if (wd < 0) {
            std::cerr << "Error adding watch to " << directoryPath << std::endl;
            close(fd);
            running = false;
            return false;
        }
        watchDescriptors[wd] = directoryPath; // Store the watch descriptor and path

        if (recursiveMonitoring) {
            addRecursiveWatch(directoryPath);
        }

        // Start the monitoring thread
        monitorThread = std::thread(&FileSystemWatcher::monitorDirectory, this);

        return true;
    }

    void stop() {
        if (!running) return;

        running = false;
        close(fd); // Close the inotify file descriptor

        // Remove all watches
        for (auto const& [wd, path] : watchDescriptors) {
            inotify_rm_watch(fd, wd);
        }
        watchDescriptors.clear();

        if (monitorThread.joinable()) {
            monitorThread.join();
        }
    }

    void setCallback(std::function<void(const std::string&, FileSystemEvent)> callback) {
        this->callback = callback;
    }

private:
    void addRecursiveWatch(const std::string& path) {
        for (const auto& entry : getDirectories(path)) {
            std::string fullPath = path + "/" + entry;

            int wd = inotify_add_watch(fd, fullPath.c_str(),
                IN_CREATE | IN_MODIFY | IN_DELETE | IN_MOVE | IN_CLOSE_WRITE);
            if (wd < 0) {
                std::cerr << "Error adding recursive watch to " << fullPath << std::endl;
                continue; // Don't fail entirely, just skip this directory
            }
            watchDescriptors[wd] = fullPath; // Store the watch descriptor and path
            addRecursiveWatch(fullPath); // Recursively add watches to subdirectories
        }
    }

    std::vector<std::string> getDirectories(const std::string& path) {
      std::vector<std::string> directories;
        DIR *dir;
        struct dirent *ent;
        if ((dir = opendir(path.c_str())) != NULL) {
            while ((ent = readdir(dir)) != NULL) {
                if (ent->d_type == DT_DIR) {
                    if (strcmp(ent->d_name, ".") != 0 && strcmp(ent->d_name, "..") != 0) {
                        directories.push_back(ent->d_name);
                    }
                }
            }
            closedir(dir);
        } else {
            std::cerr << "Could not open directory " << path << std::endl;
        }
        return directories;
    }

    void monitorDirectory() {
        char buffer[4096];

        while (running) {
            ssize_t length = read(fd, buffer, sizeof(buffer));
            if (length < 0) {
                if (errno == EINTR) continue; // Interrupted by signal, try again
                std::cerr << "Error reading inotify events" << std::endl;
                break;
            }

            ssize_t i = 0;
            while (i < length) {
                struct inotify_event* event = (struct inotify_event*)&buffer[i];
                if (event->len) {
                    std::string eventName(event->name);
                    std::string fullPath;

                    // Find the path associated with the watch descriptor
                    auto it = watchDescriptors.find(event->wd);
                    if (it != watchDescriptors.end()) {
                        fullPath = it->second + "/" + eventName;
                    } else {
                        std::cerr << "Watch descriptor not found" << std::endl;
                        fullPath = directoryPath + "/" + eventName; // Fallback
                    }

                    FileSystemEvent eventType;
                    if (event->mask & IN_CREATE) {
                        eventType = FileSystemEvent::Created;
                    } else if (event->mask & IN_MODIFY || event->mask & IN_CLOSE_WRITE) {
                        eventType = FileSystemEvent::Modified;
                    } else if (event->mask & IN_DELETE) {
                        eventType = FileSystemEvent::Deleted;
                    } else if (event->mask & IN_MOVE) {
                        eventType = FileSystemEvent::Renamed;
                    } else {
                        eventType = FileSystemEvent::Modified; // Default
                    }

                    if (callback) {
                        callback(fullPath, eventType);
                    }
                }
                i += sizeof(struct inotify_event) + event->len;
            }
        }
        std::cout << "Monitoring thread stopped for: " << directoryPath << std::endl;
    }

private:
    std::string directoryPath;
    bool recursiveMonitoring;
    bool running;
    int fd; // File descriptor for inotify
    std::thread monitorThread;
    std::function<void(const std::string&, FileSystemEvent)> callback;
    std::map<int, std::string> watchDescriptors; // Map watch descriptors to paths
};

#endif

代码解释:

  1. 头文件: 包含了Linux inotify API所需的头文件。
  2. FileSystemWatcher类: 封装了文件系统监听的功能。
  3. start()方法:
    • 调用 inotify_init() 函数初始化 inotify。
    • 调用 inotify_add_watch() 函数将要监听的目录添加到 inotify 监听列表中。
    • 如果需要递归监听子目录,则调用 addRecursiveWatch() 函数递归添加子目录到监听列表。
    • 创建一个新的线程 monitorThread,用于执行监听操作。
  4. stop()方法:
    • 设置 running 标志为 false,停止监听线程。
    • 调用 close() 函数关闭 inotify 文件描述符。
    • 调用 inotify_rm_watch() 函数移除所有监听的目录。
    • 等待监听线程结束。
  5. setCallback()方法: 设置回调函数,用于处理文件系统事件。
  6. addRecursiveWatch()方法: 递归地添加子目录到 inotify 监听列表中。
  7. getDirectories()方法: 获取指定目录下的所有子目录。
  8. monitorDirectory()方法:
    • 调用 read() 函数读取 inotify 事件。
    • 解析 inotify_event 结构体,获取事件类型和文件名。
    • 调用回调函数 callback(),将事件信息传递给用户。

六、macOS平台的实现


#ifdef __APPLE__
#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <functional>
#include <CoreServices/CoreServices.h>
#include <unistd.h>

class FileSystemWatcher {
public:
    enum class FileSystemEvent { Created, Modified, Deleted, Renamed };

    FileSystemWatcher() : running(false), stream(nullptr) {}
    ~FileSystemWatcher() { stop(); }

    bool start(const std::string& path, bool recursive = true) {
        if (running) return false;

        directoryPath = path;
        recursiveMonitoring = recursive;
        running = true;

        CFStringRef cfPath = CFStringCreateWithCString(NULL, path.c_str(), kCFStringEncodingUTF8);
        CFArrayRef pathsToWatch = CFArrayCreate(NULL, (const void**)&cfPath, 1, NULL);
        CFRelease(cfPath);

        FSEventStreamContext context = {0, this, NULL, NULL, NULL};
        stream = FSEventStreamCreate(
            NULL,
            &FileSystemWatcher::eventCallback,
            &context,
            pathsToWatch,
            kFSEventStreamEventIdSinceNow, // Or use a saved event ID for persistent monitoring
            1.0, // Latency in seconds
            kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagWatchRoot | (recursiveMonitoring ? kFSEventStreamCreateFlagNone : kFSEventStreamCreateFlagNoDefer)
        );
        CFRelease(pathsToWatch);

        if (stream == nullptr) {
            std::cerr << "Error creating FSEventStream" << std::endl;
            running = false;
            return false;
        }

        FSEventStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
        if (!FSEventStreamStart(stream)) {
            std::cerr << "Error starting FSEventStream" << std::endl;
            FSEventStreamInvalidate(stream);
            FSEventStreamRelease(stream);
            stream = nullptr;
            running = false;
            return false;
        }

        // Start the run loop in a separate thread
        monitorThread = std::thread([this]() {
            CFRunLoopRun();
        });

        return true;
    }

    void stop() {
        if (!running) return;

        running = false;
        if (stream != nullptr) {
            FSEventStreamStop(stream);
            FSEventStreamInvalidate(stream);
            FSEventStreamRelease(stream);
            stream = nullptr;
        }
        CFRunLoopStop(CFRunLoopGetCurrent());

        if (monitorThread.joinable()) {
            monitorThread.join();
        }
    }

    void setCallback(std::function<void(const std::string&, FileSystemEvent)> callback) {
        this->callback = callback;
    }

private:
    static void eventCallback(
        ConstFSEventStreamRef streamRef,
        void* userData,
        size_t numEvents,
        void* eventPaths,
        const FSEventStreamEventFlags eventFlags[],
        const FSEventStreamEventId eventIds[]) {
        FileSystemWatcher* watcher = static_cast<FileSystemWatcher*>(userData);
        char** paths = (char**)eventPaths;

        for (size_t i = 0; i < numEvents; ++i) {
            std::string filePath(paths[i]);

            FileSystemEvent eventType;
            if (eventFlags[i] & kFSEventStreamEventFlagItemCreated) {
                eventType = FileSystemEvent::Created;
            } else if (eventFlags[i] & kFSEventStreamEventFlagItemModified || eventFlags[i] & kFSEventStreamEventFlagItemChangeOwner) {
                eventType = FileSystemEvent::Modified;
            } else if (eventFlags[i] & kFSEventStreamEventFlagItemRemoved) {
                eventType = FileSystemEvent::Deleted;
            } else if (eventFlags[i] & kFSEventStreamEventFlagItemRenamed) {
                eventType = FileSystemEvent::Renamed;
            } else {
                eventType = FileSystemEvent::Modified; // Default
            }

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注