//
// 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);
}