diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a7c377 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +### OSX ### +.DS_Store +.AppleDouble +.LSOverride + +# Icon must ends with two \r. +Icon + + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + + +### Xcode ### +build +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcworkspace/contents.xcworkspacedata + diff --git a/README.md b/README.md index bc556a4..1b31765 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,11 @@ VDKQueue -======= +======== A modern, faster, better version of UKKQueue. - -about +About ----- VDKQueue is an Objective-C wrapper around kernel queues (kQueues). @@ -15,19 +14,17 @@ It allows you to watch a file or folder for changes and be notified when they oc VDKQueue is a modern, streamlined and much faster version of UKKQueue, which was originally written in 2003 by Uli Kusterer. Objective-C has come a long way in the past nine years and UKKQueue was long in the tooth. VDKQueue is better in several ways: - -- The number of method calls is vastly reduced. - -- Grand Central Dispatch is used in place of Uli's "threadProxy" notifications (much faster) - -- Memory footprint is roughly halved, since VDKQueue creates less overhead - -- Fewer locks are taken, especially in loops (faster) - -- The code is *much* cleaner and simpler! - -- There is only one .h and one .m file to include. - +- The number of method calls is vastly reduced. +- Grand Central Dispatch is used in place of Uli's "threadProxy" notifications (much faster) +- Memory footprint is roughly halved, since VDKQueue creates less overhead +- Fewer locks are taken, especially in loops (faster) +- The code is *much* cleaner and simpler! +- There is only one .h and one .m file to include. + VDKQueue also fixes long-standing bugs in UKKQueue. For example: OS X limits the number of open file descriptors each process may have to about 3,000. If UKKQueue fails to open a new file descriptor because it has hit this limit, it will crash. VDKQueue will not. - - - -performance + +Performance ----------- Adding 1,945 file paths to a UKKQueue instance took, on average, 80ms. @@ -37,22 +34,12 @@ VDKQueue processes and pushes out notifications about file changes roughly 50-70 All tests conducted on a 2008 MacBook Pro 2.5Ghz with 4GB of RAM running OS 10.7.3 using Xcode and Instruments (time profiler). - - - -requirements +Requirements ------------ -VDKQueue requires Mac OS X 10.6+ because it uses Grand Central Dispatch. - -VDKQueue does not support garbage collection. If you use garbage collection, you are lazy. Shape up. +VDKQueue requires Mac OS X 10.6+ because it uses Grand Central Dispatch. VDKQueue uses ARC. -VDKQueue does not currently use ARC, although it should be straightforward to convert if you wish. (Don't be the guy that can't manually manage memory, though.) - - - - -license +License ------- Created by Bryan D K Jones on 28 March 2012 @@ -63,7 +50,5 @@ Based heavily on UKKQueue, which was created and copyrighted by Uli Kusterer on This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. - 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. - 3. This notice may not be removed or altered from any source distribution. diff --git a/VDKQueue.h b/VDKQueue.h index 9856a34..cbf87cd 100644 --- a/VDKQueue.h +++ b/VDKQueue.h @@ -1,42 +1,42 @@ -// VDKQueue.h -// Created by Bryan D K Jones on 28 March 2012 -// Copyright 2013 Bryan D K Jones +// VDKQueue.h +// Created by Bryan D K Jones on 28 March 2012 +// Copyright 2013 Bryan D K Jones // // Based heavily on UKKQueue, which was created and copyrighted by Uli Kusterer on 21 Dec 2003. // -// This software is provided 'as-is', without any express or implied -// warranty. In no event will the authors be held liable for any damages -// arising from the use of this software. -// Permission is granted to anyone to use this software for any purpose, -// including commercial applications, and to alter it and redistribute it -// freely, subject to the following restrictions: -// 1. The origin of this software must not be misrepresented; you must not -// claim that you wrote the original software. If you use this software -// in a product, an acknowledgment in the product documentation would be -// appreciated but is not required. -// 2. Altered source versions must be plainly marked as such, and must not be -// misrepresented as being the original software. -// 3. This notice may not be removed or altered from any source -// distribution. +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source +// distribution. // // BASED ON UKKQUEUE: // // This is an updated, modernized and streamlined version of the excellent UKKQueue class, which was authored by Uli Kusterer. -// UKKQueue was written back in 2003 and there have been many, many improvements to Objective-C since then. VDKQueue uses the +// UKKQueue was written back in 2003 and there have been many, many improvements to Objective-C since then. VDKQueue uses the // core of Uli's original class, but makes it faster and more efficient. Method calls are reduced. Grand Central Dispatch is used in place // of Uli's "threadProxy" objects. The memory footprint is roughly halved, as I don't create the overhead that UKKQueue does. // // VDKQueue is also simplified. The option to use it as a singleton is removed. You simply alloc/init an instance and add paths you want to -// watch. Your objects can be alerted to changes either by notifications or by a delegate method (or both). See below. +// watch. Your objects can be alerted to changes either by notifications or by a delegate method (or both). See below. // // It also fixes several bugs. For one, it won't crash if it can't create a file descriptor to a file you ask it to watch. (By default, an OS X process can only // have about 3,000 file descriptors open at once. If you hit that limit, UKKQueue will crash. VDKQueue will not.) // // -// DEPENDENCIES: -// +// DEPENDENCIES: +// // VDKQueue requires OS 10.6+ because it relies on Grand Central Dispatch. // @@ -57,94 +57,101 @@ // // Other frameworks out there try to work around this issue by immediately attempting to re-open the file descriptor to the path. This is not bulletproof and may fail; // it all depends on the timing of disk I/O. Bottom line: you could not rely on it and might miss future changes to the file path you're supposedly watching. That's why -// VDKQueue does not take this approach, but favors the "manual" method of "stop-watching-then-rewatch". +// VDKQueue does not take this approach, but favors the "manual" method of "stop-watching-then-rewatch". // - - #import -#include + #include +/** + * Logical OR these values into the flags that you pass in the @c -addPath:notifyingAbout: method + * to specify the types of notifications you're interested in. + * Pass VDKQueueEventAll to receive all of them. + */ +typedef NS_OPTIONS(unsigned, VDKQueueEvent) { + /// Item was renamed. + VDKQueueEventRename = NOTE_RENAME, -// -// Logical OR these values into the u_int that you pass in the -addPath:notifyingAbout: method -// to specify the types of notifications you're interested in. Pass the default value to receive all of them. -// -#define VDKQueueNotifyAboutRename NOTE_RENAME // Item was renamed. -#define VDKQueueNotifyAboutWrite NOTE_WRITE // Item contents changed (also folder contents changed). -#define VDKQueueNotifyAboutDelete NOTE_DELETE // item was removed. -#define VDKQueueNotifyAboutAttributeChange NOTE_ATTRIB // Item attributes changed. -#define VDKQueueNotifyAboutSizeIncrease NOTE_EXTEND // Item size increased. -#define VDKQueueNotifyAboutLinkCountChanged NOTE_LINK // Item's link count changed. -#define VDKQueueNotifyAboutAccessRevocation NOTE_REVOKE // Access to item was revoked. + /// Item contents changed (also folder contents changed). + VDKQueueEventWrite = NOTE_WRITE, + + /// Item was removed. + VDKQueueEventDelete = NOTE_DELETE, + + /// Item attributes changed. + VDKQueueEventAttributeChange = NOTE_ATTRIB, -#define VDKQueueNotifyDefault (VDKQueueNotifyAboutRename | VDKQueueNotifyAboutWrite \ - | VDKQueueNotifyAboutDelete | VDKQueueNotifyAboutAttributeChange \ - | VDKQueueNotifyAboutSizeIncrease | VDKQueueNotifyAboutLinkCountChanged \ - | VDKQueueNotifyAboutAccessRevocation) + /// Item size increased. + VDKQueueEventSizeIncrease = NOTE_EXTEND, + + /// Item's link count changed. + VDKQueueEventLinkCountChanged = NOTE_LINK, + + /// Access to item was revoked. + VDKQueueEventAccessRevocation = NOTE_REVOKE, + + /// All events. + VDKQueueEventAll = VDKQueueEventRename + | VDKQueueEventWrite + | VDKQueueEventDelete + | VDKQueueEventAttributeChange + | VDKQueueEventSizeIncrease + | VDKQueueEventLinkCountChanged + | VDKQueueEventAccessRevocation +}; // // Notifications that this class sends to the NSWORKSPACE notification center. // Object = the instance of VDKQueue that was watching for changes // userInfo.path = the file path where the change was observed // -extern NSString * VDKQueueRenameNotification; -extern NSString * VDKQueueWriteNotification; -extern NSString * VDKQueueDeleteNotification; -extern NSString * VDKQueueAttributeChangeNotification; -extern NSString * VDKQueueSizeIncreaseNotification; -extern NSString * VDKQueueLinkCountChangeNotification; -extern NSString * VDKQueueAccessRevocationNotification; - - +extern NSString *const VDKQueueRenameNotification; +extern NSString *const VDKQueueWriteNotification; +extern NSString *const VDKQueueDeleteNotification; +extern NSString *const VDKQueueAttributeChangeNotification; +extern NSString *const VDKQueueSizeIncreaseNotification; +extern NSString *const VDKQueueLinkCountChangeNotification; +extern NSString *const VDKQueueAccessRevocationNotification; // // Or, instead of subscribing to notifications, you can specify a delegate and implement this method to respond to kQueue events. // Note the required statement! For speed, this class does not check to make sure the delegate implements this method. (When I say "required" I mean it!) // + @class VDKQueue; + @protocol VDKQueueDelegate -@required --(void) VDKQueue:(VDKQueue *)queue receivedNotification:(NSString*)noteName forPath:(NSString*)fpath; +- (void)queue:(VDKQueue *)queue didReceiveNotification:(NSString *)notificationName forPath:(NSString *)fpath; @end - - - - @interface VDKQueue : NSObject -{ - id _delegate; - BOOL _alwaysPostNotifications; // By default, notifications are posted only if there is no delegate set. Set this value to YES to have notes posted even when there is a delegate. - -@private - int _coreQueueFD; // The actual kqueue ID (Unix file descriptor). - NSMutableDictionary *_watchedPathEntries; // List of VDKQueuePathEntries. Keys are NSStrings of the path that each VDKQueuePathEntry is for. - BOOL _keepWatcherThreadRunning; // Set to NO to cancel the thread that watches _coreQueueFD for kQueue events -} - - -// -// Note: there is no need to ask whether a path is already being watched. Just add it or remove it and this class -// will take action only if appropriate. (Add only if we're not already watching it, remove only if we are.) -// -// Warning: You must pass full, root-relative paths. Do not pass tilde-abbreviated paths or file URLs. -// -- (void) addPath:(NSString *)aPath; -- (void) addPath:(NSString *)aPath notifyingAbout:(u_int)flags; // See note above for values to pass in "flags" - -- (void) removePath:(NSString *)aPath; -- (void) removeAllPaths; - - -- (NSUInteger) numberOfWatchedPaths; // Returns the number of paths that this VDKQueue instance is actively watching. +@property (nonatomic, weak) id delegate; +@property (nonatomic, retain) dispatch_queue_t queue; +/** + * By default, notifications are posted only if there is no delegate set. + * Set this value to @c YES to have notes posted even when there is a delegate. + */ +@property (nonatomic) BOOL alwaysPostNotifications; +@property (nonatomic) NSTimeInterval sleepInterval; + +/** + * Note: there is no need to ask whether a path is already being watched. Just add it or remove it and this class + * will take action only if appropriate. (Add only if we're not already watching it, remove only if we are.) + * + * Warning: You must pass full, root-relative paths. Do not pass tilde-abbreviated paths or file URLs. + */ +- (void)addPath:(NSString *)aPath; +/// See note above for values to pass in "flags" +- (void)addPath:(NSString *)aPath notifyingAbout:(VDKQueueEvent)flags; + +- (void)removePath:(NSString *)aPath; +- (void)removeAllPaths; + +/// Returns the number of paths that this VDKQueue instance is actively watching. +- (NSUInteger)numberOfWatchedPaths; - -@property (assign) id delegate; -@property (assign) BOOL alwaysPostNotifications; - -@end \ No newline at end of file +@end diff --git a/VDKQueue.m b/VDKQueue.m index f1bc83b..7460405 100644 --- a/VDKQueue.m +++ b/VDKQueue.m @@ -1,385 +1,312 @@ -// VDKQueue.m -// Created by Bryan D K Jones on 28 March 2012 -// Copyright 2013 Bryan D K Jones +// VDKQueue.m +// Created by Bryan D K Jones on 28 March 2012 +// Copyright 2013 Bryan D K Jones // // Based heavily on UKKQueue, which was created and copyrighted by Uli Kusterer on 21 Dec 2003. // -// This software is provided 'as-is', without any express or implied -// warranty. In no event will the authors be held liable for any damages -// arising from the use of this software. -// Permission is granted to anyone to use this software for any purpose, -// including commercial applications, and to alter it and redistribute it -// freely, subject to the following restrictions: -// 1. The origin of this software must not be misrepresented; you must not -// claim that you wrote the original software. If you use this software -// in a product, an acknowledgment in the product documentation would be -// appreciated but is not required. -// 2. Altered source versions must be plainly marked as such, and must not be -// misrepresented as being the original software. -// 3. This notice may not be removed or altered from any source -// distribution. +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source +// distribution. #import "VDKQueue.h" -#import -#import -#include +NSString *const VDKQueueRenameNotification = @"VDKQueueFileRenamedNotification"; +NSString *const VDKQueueWriteNotification = @"VDKQueueFileWrittenToNotification"; +NSString *const VDKQueueDeleteNotification = @"VDKQueueFileDeletedNotification"; +NSString *const VDKQueueAttributeChangeNotification = @"VDKQueueFileAttributesChangedNotification"; +NSString *const VDKQueueSizeIncreaseNotification = @"VDKQueueFileSizeIncreasedNotification"; +NSString *const VDKQueueLinkCountChangeNotification = @"VDKQueueLinkCountChangedNotification"; +NSString *const VDKQueueAccessRevocationNotification = @"VDKQueueAccessWasRevokedNotification"; +#pragma mark - VDKQueuePathEntry -NSString * VDKQueueRenameNotification = @"VDKQueueFileRenamedNotification"; -NSString * VDKQueueWriteNotification = @"VDKQueueFileWrittenToNotification"; -NSString * VDKQueueDeleteNotification = @"VDKQueueFileDeletedNotification"; -NSString * VDKQueueAttributeChangeNotification = @"VDKQueueFileAttributesChangedNotification"; -NSString * VDKQueueSizeIncreaseNotification = @"VDKQueueFileSizeIncreasedNotification"; -NSString * VDKQueueLinkCountChangeNotification = @"VDKQueueLinkCountChangedNotification"; -NSString * VDKQueueAccessRevocationNotification = @"VDKQueueAccessWasRevokedNotification"; +// This is a simple model class used to hold info about each path we watch. - - -#pragma mark - -#pragma mark VDKQueuePathEntry -#pragma mark - -#pragma ------------------------------------------------------------------------------------------------------------------------------------------------------------ - -// This is a simple model class used to hold info about each path we watch. @interface VDKQueuePathEntry : NSObject -{ - NSString* _path; - int _watchedFD; - u_int _subscriptionFlags; -} - -- (id) initWithPath:(NSString*)inPath andSubscriptionFlags:(u_int)flags; @property (atomic, copy) NSString *path; -@property (atomic, assign) int watchedFD; -@property (atomic, assign) u_int subscriptionFlags; +@property (atomic) int watchedFD; +@property (atomic) VDKQueueEvent subscriptionFlags; + +- (instancetype)initWithPath:(NSString *)inPath subscriptionFlags:(VDKQueueEvent)flags; @end @implementation VDKQueuePathEntry -@synthesize path = _path, watchedFD = _watchedFD, subscriptionFlags = _subscriptionFlags; - -- (id) initWithPath:(NSString*)inPath andSubscriptionFlags:(u_int)flags; +- (instancetype)initWithPath:(NSString *)inPath subscriptionFlags:(VDKQueueEvent)flags { self = [super init]; - if (self) - { - _path = [inPath copy]; - _watchedFD = open([_path fileSystemRepresentation], O_EVTONLY, 0); - if (_watchedFD < 0) - { - [self autorelease]; - return nil; - } - _subscriptionFlags = flags; - } - return self; + if (self) + { + _path = [inPath copy]; + _watchedFD = open([_path fileSystemRepresentation], O_EVTONLY, 0); + if (_watchedFD < 0) + return nil; + + _subscriptionFlags = flags; + } + return self; } --(void) dealloc +- (void)dealloc { - [_path release]; - _path = nil; - - if (_watchedFD >= 0) close(_watchedFD); - _watchedFD = -1; - - [super dealloc]; + if (_watchedFD >= 0) + close(_watchedFD); } @end +#pragma mark - VDKQueue +@interface VDKQueue () { + /// The actual kqueue ID (Unix file descriptor). + int _coreQueueFD; + /// List of VDKQueuePathEntries. Keys are NSStrings of the path that each VDKQueuePathEntry is for. + NSMutableDictionary *_watchedPathEntries; + /// Set to NO to cancel the thread that watches _coreQueueFD for kQueue events + BOOL _keepWatcherThreadRunning; +} - - - - - - - - -#pragma mark - -#pragma mark VDKQueue -#pragma mark - -#pragma ------------------------------------------------------------------------------------------------------------------------------------------------------------ - -@interface VDKQueue () -- (void) watcherThread:(id)sender; @end - - @implementation VDKQueue -@synthesize delegate = _delegate, alwaysPostNotifications = _alwaysPostNotifications; - - -#pragma mark - -#pragma mark INIT/DEALLOC +#pragma mark - INIT/DEALLOC -- (id) init +- (instancetype)init { - self = [super init]; - if (self) - { - _coreQueueFD = kqueue(); - if (_coreQueueFD == -1) - { - [self autorelease]; - return nil; - } - + self = [super init]; + if (self) + { + _coreQueueFD = kqueue(); + if (_coreQueueFD == -1) + return nil; + _alwaysPostNotifications = NO; - _watchedPathEntries = [[NSMutableDictionary alloc] init]; - } - return self; -} +#if TARGET_OS_IPHONE + _sleepInterval = 60; +#else + _sleepInterval = 0; +#endif + _watchedPathEntries = [[NSMutableDictionary alloc] init]; + _queue = dispatch_get_main_queue(); + } + return self; +} -- (void) dealloc +- (void)dealloc { // Shut down the thread that's scanning for kQueue events _keepWatcherThreadRunning = NO; - + // Do this to close all the open file descriptors for files we're watching [self removeAllPaths]; - - [_watchedPathEntries release]; - _watchedPathEntries = nil; - - [super dealloc]; } +#pragma mark - PRIVATE METHODS +- (VDKQueuePathEntry *)addPathToQueue:(NSString *)path notifyingAbout:(VDKQueueEvent)flags +{ + @synchronized(self) + { + // Are we already watching this path? + VDKQueuePathEntry *pathEntry = _watchedPathEntries[path]; + if (pathEntry) + { + // All flags already set? + if (([pathEntry subscriptionFlags] & flags) == flags) + return pathEntry; + flags |= [pathEntry subscriptionFlags]; + } -#pragma mark - -#pragma mark PRIVATE METHODS + struct timespec nullts = { 0, 0 }; + struct kevent ev; + + if (!pathEntry) + pathEntry = [[VDKQueuePathEntry alloc] initWithPath:path subscriptionFlags:flags]; -- (VDKQueuePathEntry *) addPathToQueue:(NSString *)path notifyingAbout:(u_int)flags -{ - @synchronized(self) - { - // Are we already watching this path? - VDKQueuePathEntry *pathEntry = [_watchedPathEntries objectForKey:path]; - if (pathEntry) - { - // All flags already set? - if(([pathEntry subscriptionFlags] & flags) == flags) - { - return [[pathEntry retain] autorelease]; - } - - flags |= [pathEntry subscriptionFlags]; - } - - struct timespec nullts = { 0, 0 }; - struct kevent ev; - - if (!pathEntry) { - pathEntry = [[[VDKQueuePathEntry alloc] initWithPath:path andSubscriptionFlags:flags] autorelease]; - } - - if (pathEntry) - { - EV_SET(&ev, [pathEntry watchedFD], EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR, flags, 0, pathEntry); - - [pathEntry setSubscriptionFlags:flags]; - - [_watchedPathEntries setObject:pathEntry forKey:path]; + EV_SET(&ev, [pathEntry watchedFD], EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR, flags, 0, (__bridge void *)pathEntry); + + [pathEntry setSubscriptionFlags:flags]; + + _watchedPathEntries[path] = pathEntry; kevent(_coreQueueFD, &ev, 1, NULL, 0, &nullts); - - // Start the thread that fetches and processes our events if it's not already running. - if(!_keepWatcherThreadRunning) - { - _keepWatcherThreadRunning = YES; - [NSThread detachNewThreadSelector:@selector(watcherThread:) toTarget:self withObject:nil]; - } + + // Start the thread that fetches and processes our events if it's not already running. + if (!_keepWatcherThreadRunning) + { + _keepWatcherThreadRunning = YES; + [NSThread detachNewThreadSelector:@selector(watcherThread:) toTarget:self withObject:nil]; + } } - - return [[pathEntry retain] autorelease]; + + return pathEntry; } - + return nil; } - -// -// WARNING: This thread has no active autorelease pool, so if you make changes, you must manually manage -// memory without relying on autorelease. Otherwise, you will leak! -// -- (void) watcherThread:(id)sender +- (void)watcherThread:(id)sender { - int n; - struct kevent ev; + int n; + struct kevent ev; struct timespec timeout = { 1, 0 }; // 1 second timeout. Should be longer, but we need this thread to exit when a kqueue is dealloced, so 1 second timeout is quite a while to wait. - int theFD = _coreQueueFD; // So we don't have to risk accessing iVars when the thread is terminated. - + int theFD = _coreQueueFD; // So we don't have to risk accessing iVars when the thread is terminated. + NSMutableArray *notesToPost = [[NSMutableArray alloc] initWithCapacity:5]; - + #if DEBUG_LOG_THREAD_LIFETIME - NSLog(@"watcherThread started."); + NSLog(@"watcherThread started."); #endif - - while(_keepWatcherThreadRunning) + + [[NSThread currentThread] setName:@"VDKQueue Watcher Thread"]; + + while (_keepWatcherThreadRunning) { - @try + @try { n = kevent(theFD, NULL, 0, &ev, 1, &timeout); - if (n > 0) - { - //NSLog( @"KEVENT returned %d", n ); - if (ev.filter == EVFILT_VNODE) + if (n <= 0) + continue; + + if (ev.filter != EVFILT_VNODE) + continue; + + if (!ev.fflags) + continue; + + // + // Note: VDKQueue gets tested by thousands of CodeKit users who each watch several thousand files at once. + // I was receiving about 3 EXC_BAD_ACCESS (SIGSEGV) crash reports a month that listed the 'path' objc_msgSend + // as the culprit. That suggests the KEVENT is being sent back to us with a udata value that is NOT what we assigned + // to the queue, though I don't know why and I don't know why it's intermittent. Regardless, I've added an extra + // check here to try to eliminate this (infrequent) problem. In theory, a KEVENT that does not have a VDKQueuePathEntry + // object attached as the udata parameter is not an event we registered for, so we should not be "missing" any events. In theory. + // + id pe = (__bridge id)ev.udata; + if (!pe || ![pe respondsToSelector:@selector(path)]) + continue; + + NSString *fpath = ((VDKQueuePathEntry *)pe).path; + if (!fpath) + continue; + + @autoreleasepool { + // Clear any old notifications + [notesToPost removeAllObjects]; + + // Figure out which notifications we need to issue + if ((ev.fflags & NOTE_RENAME) == NOTE_RENAME) + { + [notesToPost addObject:VDKQueueRenameNotification]; + } + if ((ev.fflags & NOTE_WRITE) == NOTE_WRITE) + { + [notesToPost addObject:VDKQueueWriteNotification]; + } + if ((ev.fflags & NOTE_DELETE) == NOTE_DELETE) + { + [notesToPost addObject:VDKQueueDeleteNotification]; + } + if ((ev.fflags & NOTE_ATTRIB) == NOTE_ATTRIB) { - //NSLog( @"KEVENT filter is EVFILT_VNODE" ); - if (ev.fflags) + [notesToPost addObject:VDKQueueAttributeChangeNotification]; + } + if ((ev.fflags & NOTE_EXTEND) == NOTE_EXTEND) + { + [notesToPost addObject:VDKQueueSizeIncreaseNotification]; + } + if ((ev.fflags & NOTE_LINK) == NOTE_LINK) + { + [notesToPost addObject:VDKQueueLinkCountChangeNotification]; + } + if ((ev.fflags & NOTE_REVOKE) == NOTE_REVOKE) + { + [notesToPost addObject:VDKQueueAccessRevocationNotification]; + } + + NSArray *notes = [[NSArray alloc] initWithArray:notesToPost]; // notesToPost will be changed in the next loop iteration, which will likely occur before the block below runs. + + // Post the notifications (or call the delegate method) on the specified queue. + dispatch_async(_queue, ^{ + for (NSString *note in notes) { - //NSLog( @"KEVENT flags are set" ); - - // - // Note: VDKQueue gets tested by thousands of CodeKit users who each watch several thousand files at once. - // I was receiving about 3 EXC_BAD_ACCESS (SIGSEGV) crash reports a month that listed the 'path' objc_msgSend - // as the culprit. That suggests the KEVENT is being sent back to us with a udata value that is NOT what we assigned - // to the queue, though I don't know why and I don't know why it's intermittent. Regardless, I've added an extra - // check here to try to eliminate this (infrequent) problem. In theory, a KEVENT that does not have a VDKQueuePathEntry - // object attached as the udata parameter is not an event we registered for, so we should not be "missing" any events. In theory. - // - id pe = ev.udata; - if (pe && [pe respondsToSelector:@selector(path)]) - { - NSString *fpath = [((VDKQueuePathEntry *)pe).path retain]; // Need to retain so it does not disappear while the block at the bottom is waiting to run on the main thread. Released in that block. - if (!fpath) continue; - - [[NSWorkspace sharedWorkspace] noteFileSystemChanged:fpath]; - - // Clear any old notifications - [notesToPost removeAllObjects]; - - // Figure out which notifications we need to issue - if ((ev.fflags & NOTE_RENAME) == NOTE_RENAME) - { - [notesToPost addObject:VDKQueueRenameNotification]; - } - if ((ev.fflags & NOTE_WRITE) == NOTE_WRITE) - { - [notesToPost addObject:VDKQueueWriteNotification]; - } - if ((ev.fflags & NOTE_DELETE) == NOTE_DELETE) - { - [notesToPost addObject:VDKQueueDeleteNotification]; - } - if ((ev.fflags & NOTE_ATTRIB) == NOTE_ATTRIB) - { - [notesToPost addObject:VDKQueueAttributeChangeNotification]; - } - if ((ev.fflags & NOTE_EXTEND) == NOTE_EXTEND) - { - [notesToPost addObject:VDKQueueSizeIncreaseNotification]; - } - if ((ev.fflags & NOTE_LINK) == NOTE_LINK) - { - [notesToPost addObject:VDKQueueLinkCountChangeNotification]; - } - if ((ev.fflags & NOTE_REVOKE) == NOTE_REVOKE) - { - [notesToPost addObject:VDKQueueAccessRevocationNotification]; - } - - - NSArray *notes = [[NSArray alloc] initWithArray:notesToPost]; // notesToPost will be changed in the next loop iteration, which will likely occur before the block below runs. - - - // Post the notifications (or call the delegate method) on the main thread. - dispatch_async(dispatch_get_main_queue(), - ^{ - for (NSString *note in notes) - { - [_delegate VDKQueue:self receivedNotification:note forPath:fpath]; - - if (!_delegate || _alwaysPostNotifications) - { - NSDictionary *userInfoDict = [[NSDictionary alloc] initWithObjectsAndKeys:fpath, @"path", nil]; - [[[NSWorkspace sharedWorkspace] notificationCenter] postNotificationName:note object:self userInfo:userInfoDict]; - [userInfoDict release]; - } - } - - [fpath release]; - [notes release]; - }); - } + [_delegate queue:self didReceiveNotification:note forPath:fpath]; + + if (!_delegate || _alwaysPostNotifications) + [[NSNotificationCenter defaultCenter] postNotificationName:note object:self userInfo:@{@"path": fpath}]; } - } + }); } } - - @catch (NSException *localException) + @catch (NSException *localException) { NSLog(@"Error in VDKQueue watcherThread: %@", localException); } + +#if TARGET_OS_IPHONE + [NSThread sleepForTimeInterval:_sleepInterval]; // To save power on iOS +#endif } - - // Close our kqueue's file descriptor - if(close(theFD) == -1) { - NSLog(@"VDKQueue watcherThread: Couldn't close main kqueue (%d)", errno); + + // Close our kqueue's file descriptor + if (close(theFD) == -1) { + NSLog(@"VDKQueue watcherThread: Couldn't close main kqueue (%d)", errno); } - - [notesToPost release]; - + #if DEBUG_LOG_THREAD_LIFETIME - NSLog(@"watcherThread finished."); + NSLog(@"watcherThread finished."); #endif - } +#pragma mark - PUBLIC METHODS - - - - -#pragma mark - -#pragma mark PUBLIC METHODS -#pragma ----------------------------------------------------------------------------------------------------------------------------------------------------- - - -- (void) addPath:(NSString *)aPath +- (void)addPath:(NSString *)aPath { - if (!aPath) return; - [aPath retain]; - + if (!aPath) + return; + @synchronized(self) { - VDKQueuePathEntry *entry = [_watchedPathEntries objectForKey:aPath]; - + VDKQueuePathEntry *entry = _watchedPathEntries[aPath]; + // Only add this path if we don't already have it. if (!entry) { - entry = [self addPathToQueue:aPath notifyingAbout:VDKQueueNotifyDefault]; + entry = [self addPathToQueue:aPath notifyingAbout:VDKQueueEventAll]; if (!entry) { NSLog(@"VDKQueue tried to add the path %@ to watchedPathEntries, but the VDKQueuePathEntry was nil. \nIt's possible that the host process has hit its max open file descriptors limit.", aPath); } } } - - [aPath release]; } - -- (void) addPath:(NSString *)aPath notifyingAbout:(u_int)flags +- (void)addPath:(NSString *)aPath notifyingAbout:(VDKQueueEvent)flags { - if (!aPath) return; - [aPath retain]; - + if (!aPath) + return; + @synchronized(self) { - VDKQueuePathEntry *entry = [_watchedPathEntries objectForKey:aPath]; - + VDKQueuePathEntry *entry = _watchedPathEntries[aPath]; + // Only add this path if we don't already have it. if (!entry) { @@ -389,31 +316,25 @@ - (void) addPath:(NSString *)aPath notifyingAbout:(u_int)flags } } } - - [aPath release]; } - -- (void) removePath:(NSString *)aPath +- (void)removePath:(NSString *)aPath { - if (!aPath) return; - [aPath retain]; - + if (!aPath) + return; + @synchronized(self) - { - VDKQueuePathEntry *entry = [_watchedPathEntries objectForKey:aPath]; - + { + VDKQueuePathEntry *entry = _watchedPathEntries[aPath]; + // Remove it only if we're watching it. if (entry) { [_watchedPathEntries removeObjectForKey:aPath]; } - } - - [aPath release]; + } } - -- (void) removeAllPaths +- (void)removeAllPaths { @synchronized(self) { @@ -421,21 +342,16 @@ - (void) removeAllPaths } } - -- (NSUInteger) numberOfWatchedPaths +- (NSUInteger)numberOfWatchedPaths { NSUInteger count; - + @synchronized(self) { count = [_watchedPathEntries count]; } - + return count; } - - - @end -