// // 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<numEvents; i++) { FSEventStreamEventFlags event = eventFlags[i]; FSEventStreamEventId eventId = eventIds[i]; char *eventPath = ((char **)eventPaths)[i]; // Don't track hidden files with dot prefix if (eventPath[0] == '.' || strstr(eventPath, "/.")) continue; eventStreamDescription(((char **)eventPaths)[i], eventFlags[i], eventIds[i]); size_t len = strlen(eventPath); // Use zero terminator as place for slash if (event & kFSEventStreamEventFlagItemIsDir) eventPath[len++] = '/'; // Apend slash on directories NSString *path = [[NSString alloc] initWithBytes:eventPath length:len encoding:NSUTF8StringEncoding]; // Check if file exist NSURL *url = [NSURL fileURLWithPath:path]; BOOL exists = [url checkResourceIsReachableAndReturnError:NULL]; // Check if hidden NSNumber *hidden = nil; [url getResourceValue:&hidden forKey:NSURLIsHiddenKey error:NULL]; if (hidden.boolValue) continue; if (event & kFSEventStreamEventFlagItemRenamed) { if (!renamePath) { // Store first event of rename renamePath = path; renameEventId = eventId; continue; } // Check if consecutive rename events (events inside root) if (eventId == renameEventId+1) { [fileEvents pathMoved:renamePath toPath:path]; renamePath = nil; // Not consecutive (events outside root) } else { // Finish last rename event if ([filemanager fileExistsAtPath:renamePath]) [fileEvents pathCreated:renamePath]; else [fileEvents pathRemoved:renamePath]; // Store new rename event renamePath = path; renameEventId = eventId; } } else if (!exists) { [fileEvents pathRemoved:path]; } else if (event & kFSEventStreamEventFlagItemCreated && ~event & kFSEventStreamEventFlagItemRemoved) { [fileEvents pathCreated:path]; } else { [fileEvents pathModified:path]; } } if (renamePath) { // Complete last rename event if ([filemanager fileExistsAtPath:renamePath]) [fileEvents pathCreated:renamePath]; else [fileEvents pathRemoved:renamePath]; } FSEventStreamStart((FSEventStreamRef)streamRef); } void eventStreamDescription(char *path, FSEventStreamEventFlags flags, FSEventStreamEventId eventId) { #ifndef LOG_EVENTS return; #endif printf("# Event %lld %s\n# ", eventId, path); //#define FLAG_CHECK(x, y) if (((x) & (y)) == (y)) printf("%s ", #y); #define FLAG_CHECK(x, y, z) if (((x) & (y)) == (y)) printf("%s ", z); else printf(". "); FLAG_CHECK(flags, kFSEventStreamEventFlagMustScanSubDirs, "?"); FLAG_CHECK(flags, kFSEventStreamEventFlagUserDropped, "?"); FLAG_CHECK(flags, kFSEventStreamEventFlagKernelDropped, "?"); FLAG_CHECK(flags, kFSEventStreamEventFlagEventIdsWrapped, "?"); FLAG_CHECK(flags, kFSEventStreamEventFlagHistoryDone, "?"); FLAG_CHECK(flags, kFSEventStreamEventFlagRootChanged, "?"); FLAG_CHECK(flags, kFSEventStreamEventFlagMount, "?"); FLAG_CHECK(flags, kFSEventStreamEventFlagUnmount, "?"); FLAG_CHECK(flags, kFSEventStreamEventFlagItemCreated, "C"); FLAG_CHECK(flags, kFSEventStreamEventFlagItemRemoved, "X"); FLAG_CHECK(flags, kFSEventStreamEventFlagItemInodeMetaMod, "I"); FLAG_CHECK(flags, kFSEventStreamEventFlagItemRenamed, "R"); FLAG_CHECK(flags, kFSEventStreamEventFlagItemModified, "M"); FLAG_CHECK(flags, kFSEventStreamEventFlagItemFinderInfoMod, "F"); FLAG_CHECK(flags, kFSEventStreamEventFlagItemChangeOwner, "O"); FLAG_CHECK(flags, kFSEventStreamEventFlagItemXattrMod, "A"); FLAG_CHECK(flags, kFSEventStreamEventFlagItemIsFile, "."); FLAG_CHECK(flags, kFSEventStreamEventFlagItemIsDir, "D"); FLAG_CHECK(flags, kFSEventStreamEventFlagItemIsSymlink, "S"); printf("\t%d\n", flags); }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#1 | 15071 | alan_petersen |
Populate -o //guest/perforce_software/piper/... //guest/alan_petersen/piper/.... |
||
//guest/perforce_software/piper/mac/R1.0/Perforce/Classes/PSFileEvents.m | |||||
#1 | 11254 | alan_petersen |
Populate //guest/perforce_software/piper/mac/R1.0/... from //guest/perforce_software/piper/mac/main/.... |
||
//guest/perforce_software/piper/mac/main/Perforce/Classes/PSFileEvents.m | |||||
#1 | 11252 | alan_petersen | Rename/move file(s) | ||
//guest/perforce_software/piper/mac/Perforce/Classes/PSFileEvents.m | |||||
#1 | 10744 | alan_petersen | Rename/move file(s) | ||
//guest/perforce_software/piper/Perforce/Classes/PSFileEvents.m | |||||
#1 | 8919 | Matt Attaway | Initial add of Piper, a lightweight Perforce client for artists and designers. |