mirror of
https://github.com/atom/atom.git
synced 2026-04-06 03:02:13 -04:00
Path watching resumes once the file is saved again. This commit allows files to be created for as-yet nonexistent paths. We won't call `$native.watchPath` until we have at least 1 subscription to the file in JS and the file exists on disk. Also, we moved execution of the path watcher callbacks until after the callbacks data structure is updated in order to avoid confusing behavior in specs.
272 lines
7.3 KiB
Plaintext
272 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];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
- (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
|