好的,各位观众老爷,欢迎来到今天的“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
代码解释:
- 头文件: 包含了Windows API所需的头文件。
FileSystemWatcher
类: 封装了文件系统监听的功能。start()
方法:- 打开要监听的目录,获取目录句柄
hDir
。 - 创建一个新的线程
monitorThread
,用于执行监听操作。
- 打开要监听的目录,获取目录句柄
stop()
方法:- 设置
running
标志为false
,停止监听线程。 - 关闭目录句柄
hDir
。 - 等待监听线程结束。
- 设置
setCallback()
方法: 设置回调函数,用于处理文件系统事件。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
代码解释:
- 头文件: 包含了Linux inotify API所需的头文件。
FileSystemWatcher
类: 封装了文件系统监听的功能。start()
方法:- 调用
inotify_init()
函数初始化 inotify。 - 调用
inotify_add_watch()
函数将要监听的目录添加到 inotify 监听列表中。 - 如果需要递归监听子目录,则调用
addRecursiveWatch()
函数递归添加子目录到监听列表。 - 创建一个新的线程
monitorThread
,用于执行监听操作。
- 调用
stop()
方法:- 设置
running
标志为false
,停止监听线程。 - 调用
close()
函数关闭 inotify 文件描述符。 - 调用
inotify_rm_watch()
函数移除所有监听的目录。 - 等待监听线程结束。
- 设置
setCallback()
方法: 设置回调函数,用于处理文件系统事件。addRecursiveWatch()
方法: 递归地添加子目录到 inotify 监听列表中。getDirectories()
方法: 获取指定目录下的所有子目录。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
}