CocoaShare/Classes/CocoaShare/MGMPathSubscriber.m

325 lines
No EOL
11 KiB
Objective-C

//
// MGMPathSubscriber.m
// Conmote
//
// Created by Mr. Gecko on 8/14/13.
// Copyright (c) 2013 Mr. Gecko's Media (James Coleman). http://mrgeckosmedia.com/
//
// Permission to use, copy, modify, and/or distribute this software for any purpose
// with or without fee is hereby granted, provided that the above copyright notice
// and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT,
// OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
// DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
// ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
#import "MGMPathSubscriber.h"
@interface MGMPathSubscriberFile : NSObject {
NSString *path;
int fileDescriptor;
u_int options;
}
- (id)initWithPath:(NSString*)thePath options:(u_int)theOptions;
- (NSString *)path;
- (int)fileDescriptor;
- (u_int)options;
- (BOOL)reopen;
@end
@implementation MGMPathSubscriberFile
- (id)initWithPath:(NSString*)thePath options:(u_int)theOptions {
if ((self = [super init])) {
path = [thePath copy];
options = theOptions;
fileDescriptor = open([path fileSystemRepresentation], O_EVTONLY, 0);
if (fileDescriptor<0) {
[self release];
return nil;
}
}
return self;
}
- (void)dealloc {
[path release];
if (fileDescriptor>=0)
close(fileDescriptor);
[super dealloc];
}
- (NSString *)path {
return path;
}
- (int)fileDescriptor {
return fileDescriptor;
}
- (u_int)options {
return options;
}
- (BOOL)reopen {
if (fileDescriptor>=0)
close(fileDescriptor);
fileDescriptor = open([path fileSystemRepresentation], O_EVTONLY, 0);
if (fileDescriptor<0)
return NO;
return YES;
}
@end
@interface MGMPathSubscriber (MGMPrivate)
- (void)subscriptionChanged:(FNSubscriptionRef)theSubscription;
- (void)subscriptionFSChanged:(ConstFSEventStreamRef)theSubscription;
- (void)subscriptionFileChanged:(MGMPathSubscriberFile *)theFile;
- (void)sendNotificationForPath:(NSString *)thePath;
@end
static MGMPathSubscriber *MGMSharedPathSubscriber;
NSString * const MGMSubscribedPathChangedNotification = @"MGMSubscribedPathChangedNotification";
void MGMPathSubscriptionChange(FNMessage theMessage, OptionBits theFlags, void *thePathSubscription, FNSubscriptionRef theSubscription) {
if (theMessage==kFNDirectoryModifiedMessage)
[(MGMPathSubscriber *)thePathSubscription subscriptionChanged:theSubscription];
else
NSLog(@"MGMPathSubscription: Received Unknown message: %u", theMessage);
}
void MGMPathSubscriptionFSChange(ConstFSEventStreamRef streamRef, void *thePathSubscription, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]) {
for (size_t i=0; i<numEvents; i++) {
if (eventFlags[i] & (0x00000100 /*kFSEventStreamEventFlagItemCreated*/ | 0x00000200 /*kFSEventStreamEventFlagItemRemoved*/ | 0x00000800 /*kFSEventStreamEventFlagItemRenamed*/)) {
[(MGMPathSubscriber *)thePathSubscription subscriptionFSChanged:streamRef];
break;
}
}
}
@implementation MGMPathSubscriber
+ (id)sharedPathSubscriber {
if (MGMSharedPathSubscriber==nil)
MGMSharedPathSubscriber = [MGMPathSubscriber new];
return MGMSharedPathSubscriber;
}
- (id)init {
if ((self = [super init])) {
subscriptions = [NSMutableDictionary new];
subscriptionUPP = NewFNSubscriptionUPP(MGMPathSubscriptionChange);
notificationsSending = [NSMutableArray new];
runLoop = CFRunLoopGetCurrent();
queueDescriptor = kqueue();
fileWatchLoop = NO;
}
return self;
}
- (void)dealloc {
if (!fileWatchLoop && queueDescriptor>=0)
close(queueDescriptor);
fileWatchLoop = NO;
[self removeAllPaths];
DisposeFNSubscriptionUPP(subscriptionUPP);
[subscriptions release];
[notificationsSending release];
[super dealloc];
}
- (id<MGMPathSubscriberDelegate>)delegate {
return delegate;
}
- (void)setDelegate:(id)theDelegate {
delegate = theDelegate;
}
- (void)addPath:(NSString *)thePath {
[self addPath:thePath fileOptions:MGMFODelete | MGMFOWrite | MGMFOExtend | MGMFOAttribute | MGMFOLink | MGMFORename | MGMFORevoke];
}
- (void)addPath:(NSString *)thePath fileOptions:(u_int)theOptions {
NSValue *value = [subscriptions objectForKey:thePath];
if (value!=nil)
return;
BOOL directory = NO;
BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:thePath isDirectory:&directory];
if (!exists) {
NSLog(@"MGMPathSubscription: Unable to subscribe to %@ as it does not exist", thePath);
return;
}
if (directory) {
if (NSFoundationVersionNumber>=677.00/*NSFoundationVersionNumber10_5*/) {
FSEventStreamContext context = {0, self, NULL, NULL, NULL};
FSEventStreamRef stream = FSEventStreamCreate(NULL, &MGMPathSubscriptionFSChange, &context, (CFArrayRef)[NSArray arrayWithObject:thePath], kFSEventStreamEventIdSinceNow, 0.5, 0x00000010 /* kFSEventStreamCreateFlagFileEvents */);
if (stream==NULL) {
NSLog(@"MGMPathSubscription: Unable to subscribe to %@", thePath);
return;
}
FSEventStreamScheduleWithRunLoop(stream, runLoop, kCFRunLoopDefaultMode);
FSEventStreamStart(stream);
[subscriptions setObject:[NSValue valueWithPointer:stream] forKey:thePath];
} else {
FNSubscriptionRef subscription = NULL;
OSStatus error = FNSubscribeByPath((UInt8 *)[thePath fileSystemRepresentation], subscriptionUPP, self, kFNNotifyInBackground, &subscription);
if (error!=noErr) {
NSLog(@"MGMPathSubscription: Unable to subscribe to %@ due to the error %ld", thePath, (long)error);
return;
}
[subscriptions setObject:[NSValue valueWithPointer:subscription] forKey:thePath];
}
} else {
MGMPathSubscriberFile *file = [[[MGMPathSubscriberFile alloc] initWithPath:thePath options:theOptions] autorelease];
if (file==nil) {
NSLog(@"MGMPathSubscription: Unable to subscribe to %@ due to an error", thePath);
return;
}
struct timespec nullts = { 0, 0 };
struct kevent event;
EV_SET(&event, [file fileDescriptor], EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR, theOptions, 0, file);
[subscriptions setObject:file forKey:thePath];
kevent(queueDescriptor, &event, 1, NULL, 0, &nullts);
if (!fileWatchLoop) {
fileWatchLoop = YES;
[NSThread detachNewThreadSelector:@selector(fileWatchThread) toTarget:self withObject:nil];
}
}
}
- (void)removePath:(NSString *)thePath {
NSValue *value = [subscriptions objectForKey:thePath];
if (value!=nil) {
if ([value isKindOfClass:[NSValue class]]) {
if (NSFoundationVersionNumber>=677.00/*NSFoundationVersionNumber10_5*/) {
FSEventStreamRef stream = [value pointerValue];
FSEventStreamStop(stream);
FSEventStreamUnscheduleFromRunLoop(stream, runLoop, kCFRunLoopDefaultMode);
FSEventStreamInvalidate(stream);
FSEventStreamRelease(stream);
[subscriptions removeObjectForKey:thePath];
} else {
FNUnsubscribe([value pointerValue]);
[subscriptions removeObjectForKey:thePath];
}
} else {
[subscriptions removeObjectForKey:thePath];
}
}
}
- (void)removeAllPaths {
NSArray *keys = [subscriptions allKeys];
for (int i=0; i<[keys count]; i++) {
NSValue *value = [subscriptions objectForKey:[keys objectAtIndex:i]];
if ([value isKindOfClass:[NSValue class]]) {
if (NSFoundationVersionNumber>=677.00/*NSFoundationVersionNumber10_5*/) {
FSEventStreamRef stream = [value pointerValue];
FSEventStreamStop(stream);
FSEventStreamUnscheduleFromRunLoop(stream, runLoop, kCFRunLoopDefaultMode);
FSEventStreamInvalidate(stream);
FSEventStreamRelease(stream);
} else {
FNUnsubscribe([value pointerValue]);
}
}
}
[subscriptions removeAllObjects];
}
- (NSArray *)subscribedPaths {
return [subscriptions allKeys];
}
- (void)subscriptionChanged:(FNSubscriptionRef)theSubscription {
NSArray *keys = [subscriptions allKeysForObject:[NSValue valueWithPointer:theSubscription]];
if ([keys count]>=1) {
NSString *path = [keys objectAtIndex:0];
if (![notificationsSending containsObject:path]) {
[notificationsSending addObject:path];
[self performSelector:@selector(sendNotificationForPath:) withObject:path afterDelay:0.5];
}
}
}
- (void)subscriptionFSChanged:(ConstFSEventStreamRef)theSubscription {
NSArray *keys = [subscriptions allKeysForObject:[NSValue valueWithPointer:theSubscription]];
if ([keys count]>=1) {
NSString *path = [keys objectAtIndex:0];
if (![notificationsSending containsObject:path]) {
[notificationsSending addObject:path];
[self performSelector:@selector(sendNotificationForPath:) withObject:path afterDelay:0.5];
}
}
}
- (void)subscriptionFileChanged:(MGMPathSubscriberFile *)theFile {
NSArray *keys = [subscriptions allKeysForObject:theFile];
if ([keys count]>=1) {
NSString *path = [keys objectAtIndex:0];
if (![notificationsSending containsObject:path]) {
[notificationsSending addObject:path];
[self performSelector:@selector(sendNotificationForPath:) withObject:path afterDelay:0.5];
}
}
}
- (void)sendNotificationForPath:(NSString *)thePath {
[[NSNotificationCenter defaultCenter] postNotificationName:MGMSubscribedPathChangedNotification object:thePath];
if ([delegate respondsToSelector:@selector(subscribedPathChanged:)]) [delegate subscribedPathChanged:thePath];
[notificationsSending removeObject:thePath];
}
- (void)fileWatchThread {
int status;
struct kevent event;
struct timespec timeout = {1, 0};
int queueFD = queueDescriptor;
while (fileWatchLoop) {
@try {
status = kevent(queueFD, NULL, 0, &event, 1, &timeout);
if (status>0) {
MGMPathSubscriberFile *file = [(id)event.udata retain];
if (event.filter==EVFILT_VNODE && event.fflags && file!=nil) {
if ((event.fflags & NOTE_RENAME)==NOTE_RENAME) {
[self performSelectorOnMainThread:@selector(subscriptionFileChanged:) withObject:file waitUntilDone:NO];
}
if ((event.fflags & NOTE_WRITE)==NOTE_WRITE) {
[self performSelectorOnMainThread:@selector(subscriptionFileChanged:) withObject:file waitUntilDone:NO];
}
if ((event.fflags & NOTE_DELETE)==NOTE_DELETE) {
[self performSelectorOnMainThread:@selector(subscriptionFileChanged:) withObject:file waitUntilDone:NO];
if ([file reopen]) {
struct timespec nullts = { 0, 0 };
struct kevent event;
EV_SET(&event, [file fileDescriptor], EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR, [file options], 0, file);
kevent(queueDescriptor, &event, 1, NULL, 0, &nullts);
} else {
[self removePath:[file path]];
}
}
if ((event.fflags & NOTE_ATTRIB)==NOTE_ATTRIB) {
[self performSelectorOnMainThread:@selector(subscriptionFileChanged:) withObject:file waitUntilDone:NO];
}
if ((event.fflags & NOTE_EXTEND)==NOTE_EXTEND) {
[self performSelectorOnMainThread:@selector(subscriptionFileChanged:) withObject:file waitUntilDone:NO];
}
if ((event.fflags & NOTE_LINK)==NOTE_LINK) {
[self performSelectorOnMainThread:@selector(subscriptionFileChanged:) withObject:file waitUntilDone:NO];
}
if ((event.fflags & NOTE_REVOKE)==NOTE_REVOKE) {
[self performSelectorOnMainThread:@selector(subscriptionFileChanged:) withObject:file waitUntilDone:NO];
}
}
[file release];
}
} @catch (NSException *exception) {
NSLog(@"MGMPathSubscription: Error in fileWatchThread: %@", exception);
}
}
if (close(queueFD)==-1)
NSLog(@"MGMPathSubscription: fileWatchThread couldn't close queue descriptor %d", errno);
queueDescriptor = -1;
}
@end