cog/FileDrawer/UKKQueue/UKKQueue.m

481 lines
15 KiB
Objective-C

/* =============================================================================
FILE: UKKQueue.m
PROJECT: Filie
COPYRIGHT: (c) 2003 M. Uli Kusterer, all rights reserved.
AUTHORS: M. Uli Kusterer - UK
LICENSES: MIT License
REVISIONS:
2006-03-13 UK Clarified license, streamlined UKFileWatcher stuff,
Changed notifications to be useful and turned off by
default some deprecated stuff.
2004-12-28 UK Several threading fixes.
2003-12-21 UK Created.
========================================================================== */
// -----------------------------------------------------------------------------
// Headers:
// -----------------------------------------------------------------------------
#import "UKKQueue.h"
#import "UKMainThreadProxy.h"
#import <unistd.h>
#import <fcntl.h>
// -----------------------------------------------------------------------------
// Macros:
// -----------------------------------------------------------------------------
// @synchronized isn't available prior to 10.3, so we use a typedef so
// this class is thread-safe on Panther but still compiles on older OSs.
#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_3
#define AT_SYNCHRONIZED(n) @synchronized(n)
#else
#define AT_SYNCHRONIZED(n)
#endif
// -----------------------------------------------------------------------------
// Globals:
// -----------------------------------------------------------------------------
static UKKQueue * gUKKQueueSharedQueueSingleton = nil;
@implementation UKKQueue
// Deprecated:
#if UKKQUEUE_OLD_SINGLETON_ACCESSOR_NAME
+(UKKQueue*) sharedQueue
{
return [self sharedFileWatcher];
}
#endif
// -----------------------------------------------------------------------------
// sharedQueue:
// Returns a singleton queue object. In many apps (especially those that
// subscribe to the notifications) there will only be one kqueue instance,
// and in that case you can use this.
//
// For all other cases, feel free to create additional instances to use
// independently.
//
// REVISIONS:
// 2006-03-13 UK Renamed from sharedQueue.
// 2005-07-02 UK Created.
// -----------------------------------------------------------------------------
+(id) sharedFileWatcher
{
AT_SYNCHRONIZED( self )
{
if( !gUKKQueueSharedQueueSingleton )
gUKKQueueSharedQueueSingleton = [[UKKQueue alloc] init]; // This is a singleton, and thus an intentional "leak".
}
return gUKKQueueSharedQueueSingleton;
}
// -----------------------------------------------------------------------------
// * CONSTRUCTOR:
// Creates a new KQueue and starts that thread we use for our
// notifications.
//
// REVISIONS:
// 2004-11-12 UK Doesn't pass self as parameter to watcherThread anymore,
// because detachNewThreadSelector retains target and args,
// which would cause us to never be released.
// 2004-03-13 UK Documented.
// -----------------------------------------------------------------------------
-(id) init
{
self = [super init];
if( self )
{
queueFD = kqueue();
if( queueFD == -1 )
{
[self release];
return nil;
}
watchedPaths = [[NSMutableArray alloc] init];
watchedFDs = [[NSMutableArray alloc] init];
// Start new thread that fetches and processes our events:
keepThreadRunning = YES;
[NSThread detachNewThreadSelector:@selector(watcherThread:) toTarget:self withObject:nil];
}
return self;
}
// -----------------------------------------------------------------------------
// release:
// Since NSThread retains its target, we need this method to terminate the
// thread when we reach a retain-count of two. The thread is terminated by
// setting keepThreadRunning to NO.
//
// REVISIONS:
// 2004-11-12 UK Created.
// -----------------------------------------------------------------------------
-(oneway void) release
{
AT_SYNCHRONIZED(self)
{
//NSLog(@"%@ (%d)", self, [self retainCount]);
if( [self retainCount] == 2 && keepThreadRunning )
keepThreadRunning = NO;
}
[super release];
}
// -----------------------------------------------------------------------------
// * DESTRUCTOR:
// Releases the kqueue again.
//
// REVISIONS:
// 2004-03-13 UK Documented.
// -----------------------------------------------------------------------------
-(void) dealloc
{
delegate = nil;
[delegateProxy release];
if( keepThreadRunning )
keepThreadRunning = NO;
// Close all our file descriptors so the files can be deleted:
NSEnumerator* enny = [watchedFDs objectEnumerator];
NSNumber* fdNum;
while( (fdNum = [enny nextObject]) )
{
if( close( [fdNum intValue] ) == -1 )
NSLog(@"dealloc: Couldn't close file descriptor (%d)", errno);
}
[watchedPaths release];
watchedPaths = nil;
[watchedFDs release];
watchedFDs = nil;
[super dealloc];
//NSLog(@"kqueue released.");
}
// -----------------------------------------------------------------------------
// queueFD:
// Returns a Unix file descriptor for the KQueue this uses. The descriptor
// is owned by this object. Do not close it!
//
// REVISIONS:
// 2004-03-13 UK Documented.
// -----------------------------------------------------------------------------
-(int) queueFD
{
return queueFD;
}
// -----------------------------------------------------------------------------
// addPathToQueue:
// Tell this queue to listen for all interesting notifications sent for
// the object at the specified path. If you want more control, use the
// addPathToQueue:notifyingAbout: variant instead.
//
// REVISIONS:
// 2004-03-13 UK Documented.
// -----------------------------------------------------------------------------
-(void) addPathToQueue: (NSString*)path
{
[self addPath: path];
}
-(void) addPath: (NSString*)path
{
[self addPathToQueue: path notifyingAbout: UKKQueueNotifyAboutRename
| UKKQueueNotifyAboutWrite
| UKKQueueNotifyAboutDelete
| UKKQueueNotifyAboutAttributeChange];
}
// -----------------------------------------------------------------------------
// addPathToQueue:notfyingAbout:
// Tell this queue to listen for the specified notifications sent for
// the object at the specified path.
//
// REVISIONS:
// 2005-06-29 UK Files are now opened using O_EVTONLY instead of O_RDONLY
// which allows ejecting or deleting watched files/folders.
// Thanks to Phil Hargett for finding this flag in the docs.
// 2004-03-13 UK Documented.
// -----------------------------------------------------------------------------
-(void) addPathToQueue: (NSString*)path notifyingAbout: (u_int)fflags
{
struct timespec nullts = { 0, 0 };
struct kevent ev;
int fd = open( [path fileSystemRepresentation], O_EVTONLY, 0 );
if( fd >= 0 )
{
EV_SET( &ev, fd, EVFILT_VNODE,
EV_ADD | EV_ENABLE | EV_CLEAR,
fflags, 0, (void*)path );
AT_SYNCHRONIZED( self )
{
[watchedPaths addObject: path];
[watchedFDs addObject: [NSNumber numberWithInt: fd]];
kevent( queueFD, &ev, 1, NULL, 0, &nullts );
}
}
}
-(void) removePath: (NSString*)path
{
[self removePathFromQueue: path];
}
// -----------------------------------------------------------------------------
// removePathFromQueue:
// Stop listening for changes to the specified path. This removes all
// notifications. Use this to balance both addPathToQueue:notfyingAbout:
// as well as addPathToQueue:.
//
// REVISIONS:
// 2004-03-13 UK Documented.
// -----------------------------------------------------------------------------
-(void) removePathFromQueue: (NSString*)path
{
int index = 0;
int fd = -1;
AT_SYNCHRONIZED( self )
{
index = [watchedPaths indexOfObject: path];
if( index == NSNotFound )
return;
fd = [[watchedFDs objectAtIndex: index] intValue];
[watchedFDs removeObjectAtIndex: index];
[watchedPaths removeObjectAtIndex: index];
}
if( close( fd ) == -1 )
NSLog(@"removePathFromQueue: Couldn't close file descriptor (%d)", errno);
}
// -----------------------------------------------------------------------------
// removeAllPathsFromQueue:
// Stop listening for changes to all paths. This removes all
// notifications.
//
// REVISIONS:
// 2004-12-28 UK Added as suggested by bbum.
// -----------------------------------------------------------------------------
-(void) removeAllPathsFromQueue;
{
AT_SYNCHRONIZED( self )
{
NSEnumerator * fdEnumerator = [watchedFDs objectEnumerator];
NSNumber * anFD;
while( (anFD = [fdEnumerator nextObject]) != nil )
close( [anFD intValue] );
[watchedFDs removeAllObjects];
[watchedPaths removeAllObjects];
}
}
// -----------------------------------------------------------------------------
// watcherThread:
// This method is called by our NSThread to loop and poll for any file
// changes that our kqueue wants to tell us about. This sends separate
// notifications for the different kinds of changes that can happen.
// All messages are sent via the postNotification:forFile: main bottleneck.
//
// This also calls sharedWorkspace's noteFileSystemChanged.
//
// To terminate this method (and its thread), set keepThreadRunning to NO.
//
// REVISIONS:
// 2005-08-27 UK Changed to use keepThreadRunning instead of kqueueFD
// being -1 as termination criterion, and to close the
// queue in this thread so the main thread isn't blocked.
// 2004-11-12 UK Fixed docs to include termination criterion, added
// timeout to make sure the bugger gets disposed.
// 2004-03-13 UK Documented.
// -----------------------------------------------------------------------------
-(void) watcherThread: (id)sender
{
int n;
struct kevent ev;
struct timespec timeout = { 5, 0 }; // 5 seconds timeout.
int theFD = queueFD; // So we don't have to risk accessing iVars when the thread is terminated.
while( keepThreadRunning )
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
NS_DURING
n = kevent( queueFD, NULL, 0, &ev, 1, &timeout );
if( n > 0 )
{
if( ev.filter == EVFILT_VNODE )
{
if( ev.fflags )
{
NSString* fpath = [[(NSString *)ev.udata retain] autorelease]; // In case one of the notified folks removes the path.
//NSLog(@"UKKQueue: Detected file change: %@", fpath);
[[NSWorkspace sharedWorkspace] noteFileSystemChanged: fpath];
//NSLog(@"ev.flags = %u",ev.fflags); // DEBUG ONLY!
if( (ev.fflags & NOTE_RENAME) == NOTE_RENAME )
[self postNotification: UKFileWatcherRenameNotification forFile: fpath];
if( (ev.fflags & NOTE_WRITE) == NOTE_WRITE )
[self postNotification: UKFileWatcherWriteNotification forFile: fpath];
if( (ev.fflags & NOTE_DELETE) == NOTE_DELETE )
[self postNotification: UKFileWatcherDeleteNotification forFile: fpath];
if( (ev.fflags & NOTE_ATTRIB) == NOTE_ATTRIB )
[self postNotification: UKFileWatcherAttributeChangeNotification forFile: fpath];
if( (ev.fflags & NOTE_EXTEND) == NOTE_EXTEND )
[self postNotification: UKFileWatcherSizeIncreaseNotification forFile: fpath];
if( (ev.fflags & NOTE_LINK) == NOTE_LINK )
[self postNotification: UKFileWatcherLinkCountChangeNotification forFile: fpath];
if( (ev.fflags & NOTE_REVOKE) == NOTE_REVOKE )
[self postNotification: UKFileWatcherAccessRevocationNotification forFile: fpath];
}
}
}
NS_HANDLER
NSLog(@"Error in UKKQueue watcherThread: %@",localException);
NS_ENDHANDLER
[pool release];
}
// Close our kqueue's file descriptor:
if( close( theFD ) == -1 )
NSLog(@"release: Couldn't close main kqueue (%d)", errno);
//NSLog(@"exiting kqueue watcher thread.");
}
// -----------------------------------------------------------------------------
// postNotification:forFile:
// This is the main bottleneck for posting notifications. If you don't want
// the notifications to go through NSWorkspace, override this method and
// send them elsewhere.
//
// REVISIONS:
// 2004-02-27 UK Changed this to send new notification, and the old one
// only to objects that respond to it. The old category on
// NSObject could cause problems with the proxy itself.
// 2004-10-31 UK Helloween fun: Make this use a mainThreadProxy and
// allow sending the notification even if we have a
// delegate.
// 2004-03-13 UK Documented.
// -----------------------------------------------------------------------------
-(void) postNotification: (NSString*)nm forFile: (NSString*)fp
{
if( delegateProxy )
{
#if UKKQUEUE_BACKWARDS_COMPATIBLE
if( ![delegateProxy respondsToSelector: @selector(watcher:receivedNotification:forPath:)] )
[delegateProxy kqueue: self receivedNotification: nm forFile: fp];
else
#endif
[delegateProxy watcher: self receivedNotification: nm forPath: fp];
}
if( !delegateProxy || alwaysNotify )
{
#if UKKQUEUE_SEND_STUPID_NOTIFICATIONS
[[[NSWorkspace sharedWorkspace] notificationCenter] postNotificationName: nm object: fp];
#else
[[[NSWorkspace sharedWorkspace] notificationCenter] postNotificationName: nm object: self
userInfo: [NSDictionary dictionaryWithObjectsAndKeys: fp, @"path", nil]];
#endif
}
}
-(id) delegate
{
return delegate;
}
-(void) setDelegate: (id)newDelegate
{
id oldProxy = delegateProxy;
delegate = newDelegate;
delegateProxy = [delegate copyMainThreadProxy];
[oldProxy release];
}
// -----------------------------------------------------------------------------
// Flag to send a notification even if we have a delegate:
// -----------------------------------------------------------------------------
-(BOOL) alwaysNotify
{
return alwaysNotify;
}
-(void) setAlwaysNotify: (BOOL)n
{
alwaysNotify = n;
}
// -----------------------------------------------------------------------------
// description:
// This method can be used to help in debugging. It provides the value
// used by NSLog & co. when you request to print this object using the
// %@ format specifier.
//
// REVISIONS:
// 2004-11-12 UK Created.
// -----------------------------------------------------------------------------
-(NSString*) description
{
return [NSString stringWithFormat: @"%@ { watchedPaths = %@, alwaysNotify = %@ }", NSStringFromClass([self class]), watchedPaths, (alwaysNotify? @"YES" : @"NO") ];
}
@end