#import #import #import #import #import "path_watcher.h" static NSMutableArray *gPathWatchers; @interface PathWatcher () - (bool)usesContext:(CefRefPtr)context; - (NSString *)watchPath:(NSString *)path callback:(WatchCallback)callback callbackId:(NSString *)callbackId; - (void)stopWatching; - (bool)isAtomicWrite:(struct kevent)event; @end @implementation PathWatcher + (PathWatcher *)pathWatcherForContext:(CefRefPtr)context { if (!gPathWatchers) gPathWatchers = [[NSMutableArray alloc] init]; PathWatcher *pathWatcher = nil; for (PathWatcher *p in gPathWatchers) { if ([p usesContext:context]) { pathWatcher = p; break; } } if (!pathWatcher) { pathWatcher = [[[PathWatcher alloc] initWithContext:context] autorelease]; [gPathWatchers addObject:pathWatcher]; } return pathWatcher; } + (void)removePathWatcherForContext:(CefRefPtr)context { PathWatcher *pathWatcher = nil; for (PathWatcher *p in gPathWatchers) { if ([p usesContext:context]) { pathWatcher = p; break; } } if (pathWatcher) { [pathWatcher stopWatching]; [gPathWatchers removeObject:pathWatcher]; } } - (void)dealloc { @synchronized(self) { close(_kq); for (NSString *path in [_callbacksByPath allKeys]) { [self removeKeventForPath:path]; } [_callbacksByPath release]; _context = nil; _keepWatching = false; } [super dealloc]; } - (id)initWithContext:(CefRefPtr)context { self = [super init]; _keepWatching = YES; _callbacksByPath = [[NSMutableDictionary alloc] init]; _fileDescriptorsByPath = [[NSMutableDictionary alloc] init]; _kq = kqueue(); _context = context; if (_kq == -1) { [NSException raise:@"PathWatcher" format:@"Could not create kqueue"]; } [self performSelectorInBackground:@selector(watch) withObject:NULL]; return self; } - (bool)usesContext:(CefRefPtr)context { return _context->IsSame(context); } - (void)stopWatching { @synchronized(self) { [self unwatchAllPaths]; _keepWatching = false; } } - (NSString *)watchPath:(NSString *)path callback:(WatchCallback)callback { NSString *callbackId = [[NSProcessInfo processInfo] globallyUniqueString]; return [self watchPath:path callback:callback callbackId:callbackId]; } - (NSString *)watchPath:(NSString *)path callback:(WatchCallback)callback callbackId:(NSString *)callbackId { path = [path stringByStandardizingPath]; @synchronized(self) { if (![self createKeventForPath:path]) { NSLog(@"WARNING: Failed to create kevent for path '%@'", path); return nil; } NSMutableDictionary *callbacks = [_callbacksByPath objectForKey:path]; if (!callbacks) { callbacks = [NSMutableDictionary dictionary]; [_callbacksByPath setObject:callbacks forKey:path]; } [callbacks setObject:callback forKey:callbackId]; } return callbackId; } - (void)unwatchPath:(NSString *)path callbackId:(NSString *)callbackId error:(NSError **)error { path = [path stringByStandardizingPath]; @synchronized(self) { NSMutableDictionary *callbacks = [_callbacksByPath objectForKey:path]; if (callbacks) { if (callbackId) { [callbacks removeObjectForKey:callbackId]; } else { [callbacks removeAllObjects]; } if (callbacks.count == 0) { [self removeKeventForPath:path]; [_callbacksByPath removeObjectForKey:path]; } } } } - (NSArray *)watchedPaths { return [_callbacksByPath allKeys]; } - (void)unwatchAllPaths { @synchronized(self) { NSArray *paths = [_callbacksByPath allKeys]; for (NSString *path in paths) { [self unwatchPath:path callbackId:nil error:nil]; } } } - (bool)createKeventForPath:(NSString *)path { path = [path stringByStandardizingPath]; @synchronized(self) { if ([_fileDescriptorsByPath objectForKey:path]) { NSLog(@"we already have a kevent"); return YES; } int fd = open([path fileSystemRepresentation], O_EVTONLY, 0); if (fd < 0) { NSLog(@"WARNING: Could create file descriptor for path '%@'", path); return NO; } [_fileDescriptorsByPath setObject:[NSNumber numberWithInt:fd] forKey:path]; struct timespec timeout = { 0, 0 }; struct kevent event; int filter = EVFILT_VNODE; int flags = EV_ADD | EV_ENABLE | EV_CLEAR; int filterFlags = NOTE_WRITE | NOTE_DELETE | NOTE_RENAME; EV_SET(&event, fd, filter, flags, filterFlags, 0, path); kevent(_kq, &event, 1, NULL, 0, &timeout); return YES; } } - (void)removeKeventForPath:(NSString *)path { path = [path stringByStandardizingPath]; @synchronized(self) { NSNumber *fdNumber = [_fileDescriptorsByPath objectForKey:path]; if (!fdNumber) { NSLog(@"WARNING: Could not find file descriptor for path '%@'", path); return; } close([fdNumber integerValue]); [_fileDescriptorsByPath removeObjectForKey:path]; } } - (bool)isAtomicWrite:(struct kevent)event { if (!event.fflags & NOTE_DELETE) return NO; const char *path = [(NSString *)event.udata fileSystemRepresentation]; bool fileExists = access(path, F_OK) != -1; return fileExists; } - (void)changePath:(NSString *)path toNewPath:(NSString *)newPath { @synchronized(self) { NSDictionary *callbacks = [NSDictionary dictionaryWithDictionary:[_callbacksByPath objectForKey:path]]; [self unwatchPath:path callbackId:nil error:nil]; for (NSString *callbackId in [callbacks allKeys]) { [self watchPath:newPath callback:[callbacks objectForKey:callbackId] callbackId:callbackId]; } } } - (void)watch { struct kevent event; struct timespec timeout = { 5, 0 }; // 5 seconds timeout. while (_keepWatching) { @autoreleasepool { int numberOfEvents = kevent(_kq, NULL, 0, &event, 1, &timeout); if (numberOfEvents == 0) { continue; } NSString *eventFlag = nil; NSString *newPath = nil; NSString *path = [(NSString *)event.udata retain]; if (event.fflags & NOTE_WRITE) { eventFlag = @"contents-change"; } else if ([self isAtomicWrite:event]) { eventFlag = @"contents-change"; // Atomic writes require the kqueue to be recreated [self removeKeventForPath:path]; [self createKeventForPath:path]; } else if (event.fflags & NOTE_DELETE) { eventFlag = @"remove"; } else if (event.fflags & NOTE_RENAME) { eventFlag = @"move"; char pathBuffer[MAXPATHLEN]; fcntl((int)event.ident, F_GETPATH, &pathBuffer); close(event.ident); newPath = [NSString stringWithUTF8String:pathBuffer]; if (!newPath) { NSLog(@"WARNING: Ignoring rename event for deleted file '%@'", path); continue; } } NSDictionary *callbacks; @synchronized(self) { callbacks = [NSDictionary dictionaryWithDictionary:[_callbacksByPath objectForKey:path]]; } if ([eventFlag isEqual:@"move"]) { [self changePath:path toNewPath:newPath]; } if ([eventFlag isEqual:@"remove"]) { [self unwatchPath:path callbackId:nil error:nil]; } dispatch_sync(dispatch_get_main_queue(), ^{ for (NSString *key in callbacks) { WatchCallback callback = [callbacks objectForKey:key]; callback(eventFlag, newPath ? newPath : path); } }); [path release]; } } } @end