Files
atom/native/path_watcher.mm
Corey Johnson & Nathan Sobo db78d6a7e5 Ensure PathWatcher isn't watching anything at the end of each spec
This replaces the old functionality of ensuring no files or directories have subscriptions in javascript. We allow this now, but we just don't allow leaked watches at the native layer.
2012-11-28 17:16:15 -07:00

276 lines
7.3 KiB
Plaintext

#import <sys/event.h>
#import <sys/time.h>
#import <sys/param.h>
#import <fcntl.h>
#import "path_watcher.h"
static NSMutableArray *gPathWatchers;
@interface PathWatcher ()
- (bool)usesContext:(CefRefPtr<CefV8Context>)context;
- (NSString *)watchPath:(NSString *)path callback:(WatchCallback)callback callbackId:(NSString *)callbackId;
- (void)stopWatching;
- (bool)isAtomicWrite:(struct kevent)event;
@end
@implementation PathWatcher
+ (PathWatcher *)pathWatcherForContext:(CefRefPtr<CefV8Context>)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<CefV8Context>)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<CefV8Context>)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<CefV8Context>)context {
return _context->IsSame(context);
}
- (void)stopWatching {
@synchronized(self) {
NSArray *paths = [_callbacksByPath allKeys];
for (NSString *path in paths) {
[self unwatchPath:path callbackId:nil error:nil];
}
_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];
}
- (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