// // PSFileEvents.m // Perforce // // Created by Adam Czubernat on 03.07.2013. // Copyright (c) 2013 Perforce Software, Inc. All rights reserved. // #import "PSFileEvents.h" //#define LOG_EVENTS static float kFSEventStreamLatency = 0.25; static float kFSEventFlushInterval = 1.0; NSString * const kFSEventCreated = @"kFSEventCreated"; NSString * const kFSEventRemoved = @"kFSEventRemoved"; NSString * const kFSEventModified = @"kFSEventModified"; void eventStreamCallback(ConstFSEventStreamRef streamRef, void *clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]); void eventStreamDescription(char *path, FSEventStreamEventFlags flags, FSEventStreamEventId eventId); @interface PSFileEventsTimer : NSObject @property (nonatomic, retain) NSTimer *timer; @property (nonatomic, weak) PSFileEvents *fileEvents; - (void)start; - (void)invalidate; - (void)action; @end @interface PSFileEvents () { FSEventStreamRef eventStream; NSMutableDictionary *queue; PSFileEventsTimer *queueTimer; BOOL ignoreSelf; } - (void)pathCreated:(NSString *)path; - (void)pathRemoved:(NSString *)path; - (void)pathMoved:(NSString *)path toPath:(NSString *)newPath; - (void)pathModified:(NSString *)path; @end @implementation PSFileEvents @synthesize delegate, root; - (id)initWithRoot:(NSString *)rootPath { return [self initWithRoot:rootPath ignoreSelf:NO]; } - (id)initWithRoot:(NSString *)rootPath ignoreSelf:(BOOL)ignore { if (self = [super init]) { NSAssert(PSInstanceCount([self class]) < 2, @"Should be one instance"); PSInstanceCreated([self class]); ignoreSelf = ignore; [self setRoot:rootPath]; } return self; } - (void)dealloc { [queueTimer invalidate]; [self setRoot:nil]; PSInstanceDeallocated([self class]); } #pragma mark - Public - (void)flush { if (!queue.count) return; // PSLog(@"Flushing... %@", [NSDate date]); NSDictionary *dict = queue; queue = [NSMutableDictionary dictionaryWithCapacity:512]; NSMutableArray *created = [NSMutableArray arrayWithCapacity:512]; NSMutableArray *removed = created.mutableCopy; NSMutableArray *modified = created.mutableCopy; NSMutableArray *movedFrom = created.mutableCopy; NSMutableArray *movedTo = created.mutableCopy; NSMutableArray *renamedFrom = created.mutableCopy; NSMutableArray *renamedTo = created.mutableCopy; // Notify delegate [dict enumerateKeysAndObjectsUsingBlock:^(NSString *path, id event, BOOL *stop) { if (event == kFSEventCreated) { [created addObject:path]; } else if (event == kFSEventRemoved) { [removed addObject:path]; } else if (event == kFSEventModified) { [modified addObject:path]; } else { NSString *newDir = [path stringByDeletingLastPathComponent]; NSString *oldDir = [event stringByDeletingLastPathComponent]; if ([newDir isEqualToString:oldDir]) { [renamedFrom addObject:path]; [renamedTo addObject:event]; } else { [movedFrom addObject:path]; [movedTo addObject:event]; } } }]; // Notify delegate if (created.count && [delegate respondsToSelector:@selector(fileEvents:created:)]) [delegate fileEvents:self created:created]; if (removed.count && [delegate respondsToSelector:@selector(fileEvents:removed:)]) [delegate fileEvents:self removed:removed]; if (modified.count && [delegate respondsToSelector:@selector(fileEvents:modified:)]) [delegate fileEvents:self modified:modified]; if (renamedTo.count && [delegate respondsToSelector:@selector(fileEvents:renamed:to:)]) [delegate fileEvents:self renamed:renamedFrom to:renamedTo]; if (movedTo.count && [delegate respondsToSelector:@selector(fileEvents:moved:to:)]) [delegate fileEvents:self moved:movedFrom to:movedTo]; } - (void)setRoot:(NSString *)rootPath { if (eventStream) { FSEventStreamStop(eventStream); FSEventStreamUnscheduleFromRunLoop(eventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); FSEventStreamInvalidate(eventStream); FSEventStreamRelease(eventStream); eventStream = NULL; } [queueTimer invalidate]; queueTimer = nil; if (!(root = [rootPath copy])) return; queue = [NSMutableDictionary dictionaryWithCapacity:512]; FSEventStreamContext context; context.version = 0; context.info = (__bridge void *)self; context.retain = NULL; context.release = NULL; context.copyDescription = NULL; FSEventStreamCreateFlags flags = (kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagFileEvents); if (ignoreSelf) flags |= kFSEventStreamCreateFlagIgnoreSelf; eventStream = FSEventStreamCreate(NULL, &eventStreamCallback, &context, (__bridge CFArrayRef)@[ root ], kFSEventStreamEventIdSinceNow, kFSEventStreamLatency, flags); FSEventStreamScheduleWithRunLoop(eventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); FSEventStreamStart(eventStream); queueTimer = [[PSFileEventsTimer alloc] init]; queueTimer.fileEvents = self; [queueTimer start]; } - (void)pathCreated:(NSString *)path { id event = [queue objectForKey:path]; if (!event) [queue setObject:kFSEventCreated forKey:path]; else if (event == kFSEventRemoved) [queue setObject:kFSEventModified forKey:path]; } - (void)pathRemoved:(NSString *)path { id event = [queue objectForKey:path]; if (event == kFSEventCreated) [queue removeObjectForKey:path]; else [queue setObject:kFSEventRemoved forKey:path]; } - (void)pathMoved:(NSString *)path toPath:(NSString *)newPath { [queue setObject:newPath forKey:path]; } - (void)pathModified:(NSString *)path { id event = [queue objectForKey:path]; if (!event) [queue setObject:kFSEventModified forKey:path]; else if (event == kFSEventRemoved) [queue setObject:kFSEventModified forKey:path]; } @end #pragma mark - PSFileEventsTimer @implementation PSFileEventsTimer @synthesize timer, fileEvents; - (void)start { timer = [NSTimer scheduledTimerWithTimeInterval:kFSEventFlushInterval target:self selector:@selector(action) userInfo:nil repeats:YES]; } - (void)invalidate { [timer invalidate]; timer = nil; } - (void)action { [fileEvents flush]; } @end #pragma mark - C callbacks void eventStreamCallback(ConstFSEventStreamRef streamRef, void *clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]) { FSEventStreamStop((FSEventStreamRef)streamRef); // Stop clears previous flags NSFileManager *filemanager = [NSFileManager defaultManager]; PSFileEvents *fileEvents = (__bridge id)(clientCallBackInfo); FSEventStreamEventId renameEventId = 0; NSString *renamePath = nil; for (int i=0; i