You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

FSEventStreamRef事件异常致文件夹实时镜像故障求解

解决FSEventStream同步File Package(.app)时的事件混乱问题

针对你在使用FSEventStream实现Finder文件夹镜像时,遇到的File Package(如.app)复制事件顺序混乱、同步失败的问题,可以通过以下核心策略解决:

1. 事件去重与层级合并

FSEventStream处理File Package复制时,会同时触发父路径(.app)和子路径的创建事件,导致重复同步操作。需要维护待处理事件队列,自动忽略子路径事件:

  • 用字典存储待处理任务,键为文件/文件夹路径,值为对应的延迟处理任务。
  • 当收到File Package父路径的创建事件时,遍历队列并取消所有子路径的待处理任务,确保只执行父路径的完整同步。
  • 同一路径的重复事件,直接取消之前的任务,只保留最新的一次。

2. 延迟处理等待写入完成

Finder复制File Package时,内容会分批写入磁盘,事件也会分批触发。设置短延迟(1-2秒,略长于FSEventStream的刷新间隔),等待所有写入操作完成后再执行同步:

  • 使用GCD的dispatch_after实现延迟任务,确保同一路径只保留最新的延迟任务,避免重复执行。
  • 延迟时间可根据实际情况调整,平衡同步实时性和写入完整性。

3. 检查File Package的就绪状态

处理父路径事件时,先验证File Package是否已完全写入,避免复制不完整的内容:

  • 通过LSCopyItemInfoForURL判断路径是否为File Package。
  • 检查关键文件(如Contents/Info.plist)是否存在,并验证其文件大小是否稳定(短时间内无变化),确认写入已完成。
  • 若未就绪,可重试3-5次,每次间隔1秒,直到就绪后再执行同步。

4. 优化同步逻辑

调整同步操作的执行方式,避免错误覆盖或清空已同步内容:

  • 处理File Package/文件夹事件时,直接同步整个目录,而非逐个处理子文件。
  • 目标路径已存在时,可选择先删除再复制(镜像场景下),或增量更新(对比文件修改时间/哈希),避免部分内容残留。
  • 保留kFSEventStreamCreateFlagMarkSelf参数,确保自身的同步操作不会触发FSEventStream事件,避免循环触发。

具体代码示例

以下是修改后的事件处理函数核心逻辑:

#include <CoreServices/CoreServices.h>
#include <Foundation/Foundation.h>

static dispatch_queue_t syncQueue = NULL;
static NSMutableDictionary *pendingTasks = NULL;

// 初始化队列和任务字典
void syncInit() {
    syncQueue = dispatch_queue_create("com.your.sync.queue", DISPATCH_QUEUE_SERIAL);
    pendingTasks = [[NSMutableDictionary alloc] init];
}

// 判断是否为File Package
BOOL isFilePackage(NSString *path) {
    NSURL *url = [NSURL fileURLWithPath:path];
    LSItemInfoRef itemInfo = LSCopyItemInfoForURL((__bridge CFURLRef)url, kLSRequestAllFlags);
    if (!itemInfo) return NO;
    BOOL isPackage = LSItemInfoGetIsPackage(itemInfo);
    CFRelease(itemInfo);
    return isPackage;
}

// 检查File Package是否就绪
BOOL isPackageReady(NSString *packagePath) {
    NSFileManager *fm = [NSFileManager defaultManager];
    NSString *infoPlistPath = [packagePath stringByAppendingPathComponent:@"Contents/Info.plist"];
    if (![fm fileExistsAtPath:infoPlistPath]) return NO;
    
    // 检查文件大小是否稳定
    NSDictionary *attrs = [fm attributesOfItemAtPath:infoPlistPath error:nil];
    if (!attrs) return NO;
    unsigned long long initialSize = [attrs fileSize];
    sleep(0.5);
    
    NSDictionary *newAttrs = [fm attributesOfItemAtPath:infoPlistPath error:nil];
    if (!newAttrs) return NO;
    return [newAttrs fileSize] == initialSize;
}

void realTimeSyncFoundChanges(ConstFSEventStreamRef streamRef, void *clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]) {
    for (size_t i = 0; i < numEvents; i++) {
        NSString *path = (__bridge NSString *)((const char **)eventPaths)[i];
        FSEventStreamEventFlags flags = eventFlags[i];
        BOOL isPackage = isFilePackage(path);
        BOOL isDir = (flags & kFSEventStreamEventFlagItemIsDirectory) != 0;
        
        // 取消该路径之前的待处理任务
        NSString *taskKey = path;
        dispatch_block_t existingTask = pendingTasks[taskKey];
        if (existingTask) {
            dispatch_block_cancel(existingTask);
            [pendingTasks removeObjectForKey:taskKey];
        }
        
        // 创建新的延迟处理任务
        dispatch_block_t task = ^{
            [pendingTasks removeObjectForKey:taskKey];
            
            // 处理File Package/文件夹
            if (isPackage || isDir) {
                // 等待包就绪,最多重试3次
                int retry = 0;
                while (!isPackageReady(path) && retry < 3) {
                    sleep(1);
                    retry++;
                }
                if (retry >= 3) return;
                
                // 执行镜像同步
                NSString *destPath = [path stringByReplacingOccurrencesOfString:@"/A/" withString:@"/B/"];
                NSFileManager *fm = [NSFileManager defaultManager];
                if ([fm fileExistsAtPath:destPath]) {
                    [fm removeItemAtPath:destPath error:nil];
                }
                [fm copyItemAtPath:path toPath:destPath error:nil];
            } 
            // 处理普通文件
            else {
                NSString *destPath = [path stringByReplacingOccurrencesOfString:@"/A/" withString:@"/B/"];
                NSFileManager *fm = [NSFileManager defaultManager];
                [fm createDirectoryAtPath:[destPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
                [fm copyItemAtPath:path toPath:destPath error:nil];
            }
        };
        
        // 延迟1.5秒执行
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), syncQueue, task);
        pendingTasks[taskKey] = task;
        
        // 取消当前包下所有子路径的待处理任务
        if (isPackage || isDir) {
            NSString *prefix = [path stringByAppendingString:@"/"];
            NSMutableArray *keysToRemove = [NSMutableArray array];
            for (NSString *key in pendingTasks) {
                if ([key hasPrefix:prefix] && ![key isEqualToString:path]) {
                    dispatch_block_t subTask = pendingTasks[key];
                    dispatch_block_cancel(subTask);
                    [keysToRemove addObject:key];
                }
            }
            [pendingTasks removeObjectsInArray:keysToRemove];
        }
    }
}

额外注意事项

  • 使用串行队列处理同步任务,避免并发操作导致的文件冲突。
  • 若追求更高性能,可替换NSFileManager的复制操作,使用rsync命令行工具实现增量同步,减少不必要的文件复制。

内容的提问来源于stack exchange,提问作者Leonardo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 05:50:59