mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-09-22 00:19:24 +03:00
281 lines
7.4 KiB
Plaintext
281 lines
7.4 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) {
|
|
[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 not create file descriptor for path '%@'. Error code %d.", path, errno);
|
|
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
|