UKKQueue
Language: Objective-C, Author: Uli Kusterer
License: MIT/X11
A wrapper class around the kqueue file change notification mechanism.
Simply create a UKKQueue, add a few paths to it and listen to the change notifications via NSWorkspace's notification center or delegate messages.
UKKQueue source preview
/* ============================================================================= 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
UKKQueue header preview
/* ============================================================================= FILE: UKKQueue.h 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. 2003-12-21 UK Created. ========================================================================== */ // ----------------------------------------------------------------------------- // Headers: // ----------------------------------------------------------------------------- #import <Foundation/Foundation.h> #include <sys/types.h> #include <sys/event.h> #import "UKFileWatcher.h" // ----------------------------------------------------------------------------- // Constants: // ----------------------------------------------------------------------------- // Backwards compatibility constants. Don't rely on code commented out with these constants, because it may be deleted in a future version. #ifndef UKKQUEUE_BACKWARDS_COMPATIBLE #define UKKQUEUE_BACKWARDS_COMPATIBLE 0 // 1 to send old-style kqueue:receivedNotification:forFile: messages to objects that accept them. #endif #ifndef UKKQUEUE_SEND_STUPID_NOTIFICATIONS #define UKKQUEUE_SEND_STUPID_NOTIFICATIONS 0 // 1 to send old-style notifications that have the path as the object and no userInfo dictionary. #endif #ifndef UKKQUEUE_OLD_SINGLETON_ACCESSOR_NAME #define UKKQUEUE_OLD_SINGLETON_ACCESSOR_NAME 0 // 1 to allow use of sharedQueue instead of sharedFileWatcher. #endif #ifndef UKKQUEUE_OLD_NOTIFICATION_NAMES #define UKKQUEUE_OLD_NOTIFICATION_NAMES 0 // 1 to allow use of old KQueue-style notification names instead of the new more generic ones in UKFileWatcher. #endif // Flags for notifyingAbout: #define UKKQueueNotifyAboutRename NOTE_RENAME // Item was renamed. #define UKKQueueNotifyAboutWrite NOTE_WRITE // Item contents changed (also folder contents changed). #define UKKQueueNotifyAboutDelete NOTE_DELETE // item was removed. #define UKKQueueNotifyAboutAttributeChange NOTE_ATTRIB // Item attributes changed. #define UKKQueueNotifyAboutSizeIncrease NOTE_EXTEND // Item size increased. #define UKKQueueNotifyAboutLinkCountChanged NOTE_LINK // Item's link count changed. #define UKKQueueNotifyAboutAccessRevocation NOTE_REVOKE // Access to item was revoked. // Notifications this sends: // (see UKFileWatcher) // Old names: *deprecated* #if UKKQUEUE_OLD_NOTIFICATION_NAMES #define UKKQueueFileRenamedNotification UKFileWatcherRenameNotification #define UKKQueueFileWrittenToNotification UKFileWatcherWriteNotification #define UKKQueueFileDeletedNotification UKFileWatcherDeleteNotification #define UKKQueueFileAttributesChangedNotification UKFileWatcherAttributeChangeNotification #define UKKQueueFileSizeIncreasedNotification UKFileWatcherSizeIncreaseNotification #define UKKQueueFileLinkCountChangedNotification UKFileWatcherLinkCountChangeNotification #define UKKQueueFileAccessRevocationNotification UKFileWatcherAccessRevocationNotification #endif // ----------------------------------------------------------------------------- // UKKQueue: // ----------------------------------------------------------------------------- @interface UKKQueue : NSObject <UKFileWatcher> { int queueFD; // The actual queue ID (Unix file descriptor). NSMutableArray* watchedPaths; // List of NSStrings containing the paths we're watching. NSMutableArray* watchedFDs; // List of NSNumbers containing the file descriptors we're watching. id delegate; // Gets messages about changes instead of notification center, if specified. id delegateProxy; // Proxy object to which we send messages so they reach delegate on the main thread. BOOL alwaysNotify; // Send notifications even if we have a delegate? Defaults to NO. BOOL keepThreadRunning; // Termination criterion of our thread. } +(id) sharedFileWatcher; // Returns a singleton, a shared kqueue object Handy if you're subscribing to the notifications. Use this, or just create separate objects using alloc/init. Whatever floats your boat. -(int) queueFD; // I know you unix geeks want this... // High-level file watching: (use UKFileWatcher protocol methods instead, where possible!) -(void) addPathToQueue: (NSString*)path; -(void) addPathToQueue: (NSString*)path notifyingAbout: (u_int)fflags; -(void) removePathFromQueue: (NSString*)path; -(id) delegate; -(void) setDelegate: (id)newDelegate; -(BOOL) alwaysNotify; -(void) setAlwaysNotify: (BOOL)n; #if UKKQUEUE_OLD_SINGLETON_ACCESSOR_NAME +(UKKQueue*) sharedQueue; #endif // private: -(void) watcherThread: (id)sender; -(void) postNotification: (NSString*)nm forFile: (NSString*)fp; // Message-posting bottleneck. @end // ----------------------------------------------------------------------------- // Methods delegates need to provide: // * DEPRECATED * use UKFileWatcher delegate methods instead! // ----------------------------------------------------------------------------- @interface NSObject (UKKQueueDelegate) -(void) kqueue: (UKKQueue*)kq receivedNotification: (NSString*)nm forFile: (NSString*)fpath; @endDownload Archive
Compatible with:
- Mac OS X 10.4 PPC
- Mac OS X 10.4 Intel
- Mac OS X 10.5 PPC
- Mac OS X 10.5 Intel
Comments
Comment feed
