1// VDKQueue.m 2// Created by Bryan D K Jones on 28 March 2012 3// Copyright 2013 Bryan D K Jones 4// 5// Based heavily on UKKQueue, which was created and copyrighted by Uli Kusterer on 21 Dec 2003. 6// 7// This software is provided 'as-is', without any express or implied 8// warranty. In no event will the authors be held liable for any damages 9// arising from the use of this software. 10// Permission is granted to anyone to use this software for any purpose, 11// including commercial applications, and to alter it and redistribute it 12// freely, subject to the following restrictions: 13// 1. The origin of this software must not be misrepresented; you must not 14// claim that you wrote the original software. If you use this software 15// in a product, an acknowledgment in the product documentation would be 16// appreciated but is not required. 17// 2. Altered source versions must be plainly marked as such, and must not be 18// misrepresented as being the original software. 19// 3. This notice may not be removed or altered from any source 20// distribution. 21 22#import <AppKit/AppKit.h> 23 24#import "VDKQueue.h" 25 26#import <unistd.h> 27#import <fcntl.h> 28#include <sys/stat.h> 29 30 31 32NSString * VDKQueueRenameNotification = @"VDKQueueFileRenamedNotification"; 33NSString * VDKQueueWriteNotification = @"VDKQueueFileWrittenToNotification"; 34NSString * VDKQueueDeleteNotification = @"VDKQueueFileDeletedNotification"; 35NSString * VDKQueueAttributeChangeNotification = @"VDKQueueFileAttributesChangedNotification"; 36NSString * VDKQueueSizeIncreaseNotification = @"VDKQueueFileSizeIncreasedNotification"; 37NSString * VDKQueueLinkCountChangeNotification = @"VDKQueueLinkCountChangedNotification"; 38NSString * VDKQueueAccessRevocationNotification = @"VDKQueueAccessWasRevokedNotification"; 39 40 41 42#pragma mark - 43#pragma mark VDKQueuePathEntry 44#pragma mark - 45#pragma ------------------------------------------------------------------------------------------------------------------------------------------------------------ 46 47// This is a simple model class used to hold info about each path we watch. 48@interface VDKQueuePathEntry : NSObject 49{ 50 NSString* _path; 51 int _watchedFD; 52 u_int _subscriptionFlags; 53} 54 55- (id) initWithPath:(NSString*)inPath andSubscriptionFlags:(u_int)flags; 56 57@property (atomic, copy) NSString *path; 58@property (atomic, assign) int watchedFD; 59@property (atomic, assign) u_int subscriptionFlags; 60 61@end 62 63@implementation VDKQueuePathEntry 64@synthesize path = _path, watchedFD = _watchedFD, subscriptionFlags = _subscriptionFlags; 65 66 67- (id) initWithPath:(NSString*)inPath andSubscriptionFlags:(u_int)flags; 68{ 69 self = [super init]; 70 if (self) 71 { 72 _path = [inPath copy]; 73 _watchedFD = open([_path fileSystemRepresentation], O_EVTONLY, 0); 74 if (_watchedFD < 0) 75 { 76 return nil; 77 } 78 _subscriptionFlags = flags; 79 } 80 return self; 81} 82 83-(void) dealloc 84{ 85 86 if (_watchedFD >= 0) close(_watchedFD); 87 _watchedFD = -1; 88 89} 90 91@end 92 93 94 95 96 97 98 99 100 101 102 103#pragma mark - 104#pragma mark VDKQueue 105#pragma mark - 106#pragma ------------------------------------------------------------------------------------------------------------------------------------------------------------ 107 108@interface VDKQueue () 109- (void) watcherThread:(id)sender; 110@end 111 112 113 114@implementation VDKQueue 115@synthesize delegate = _delegate, alwaysPostNotifications = _alwaysPostNotifications; 116 117 118 119#pragma mark - 120#pragma mark INIT/DEALLOC 121 122- (id) init 123{ 124 self = [super init]; 125 if (self) 126 { 127 _coreQueueFD = kqueue(); 128 if (_coreQueueFD == -1) 129 { 130 return nil; 131 } 132 133 _alwaysPostNotifications = NO; 134 _watchedPathEntries = [[NSMutableDictionary alloc] init]; 135 } 136 return self; 137} 138 139 140- (void) dealloc 141{ 142 // Shut down the thread that's scanning for kQueue events 143 _keepWatcherThreadRunning = NO; 144 145 // Do this to close all the open file descriptors for files we're watching 146 [self removeAllPaths]; 147 148 _watchedPathEntries = nil; 149} 150 151 152 153 154 155#pragma mark - 156#pragma mark PRIVATE METHODS 157 158- (VDKQueuePathEntry *) addPathToQueue:(NSString *)path notifyingAbout:(u_int)flags 159{ 160 @synchronized(self) 161 { 162 // Are we already watching this path? 163 VDKQueuePathEntry *pathEntry = _watchedPathEntries[path]; 164 165 if (pathEntry) 166 { 167 // All flags already set? 168 if(([pathEntry subscriptionFlags] & flags) == flags) 169 { 170 return pathEntry; 171 } 172 173 flags |= [pathEntry subscriptionFlags]; 174 } 175 176 struct timespec nullts = { 0, 0 }; 177 struct kevent ev; 178 179 if (!pathEntry) 180 { 181 pathEntry = [[VDKQueuePathEntry alloc] initWithPath:path andSubscriptionFlags:flags]; 182 } 183 184 if (pathEntry) 185 { 186 EV_SET(&ev, [pathEntry watchedFD], EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR, flags, 0, (__bridge void *) pathEntry); 187 188 [pathEntry setSubscriptionFlags:flags]; 189 190 _watchedPathEntries[path] = pathEntry; 191 kevent(_coreQueueFD, &ev, 1, NULL, 0, &nullts); 192 193 // Start the thread that fetches and processes our events if it's not already running. 194 if(!_keepWatcherThreadRunning) 195 { 196 _keepWatcherThreadRunning = YES; 197 [NSThread detachNewThreadSelector:@selector(watcherThread:) toTarget:self withObject:nil]; 198 } 199 } 200 201 return pathEntry; 202 } 203 204 return nil; 205} 206 207 208- (void) watcherThread:(id)sender 209{ 210 int n; 211 struct kevent ev; 212 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. 213 int theFD = _coreQueueFD; // So we don't have to risk accessing iVars when the thread is terminated. 214 215 NSMutableArray *notesToPost = [[NSMutableArray alloc] initWithCapacity:5]; 216 217#if DEBUG_LOG_THREAD_LIFETIME 218 NSLog(@"watcherThread started."); 219#endif 220 221 while(_keepWatcherThreadRunning) 222 { 223 @try 224 { 225 n = kevent(theFD, NULL, 0, &ev, 1, &timeout); 226 if (n > 0) 227 { 228 //NSLog( @"KEVENT returned %d", n ); 229 if (ev.filter == EVFILT_VNODE) 230 { 231 //NSLog( @"KEVENT filter is EVFILT_VNODE" ); 232 if (ev.fflags) 233 { 234 //NSLog( @"KEVENT flags are set" ); 235 236 // 237 // Note: VDKQueue gets tested by thousands of CodeKit users who each watch several thousand files at once. 238 // I was receiving about 3 EXC_BAD_ACCESS (SIGSEGV) crash reports a month that listed the 'path' objc_msgSend 239 // as the culprit. That suggests the KEVENT is being sent back to us with a udata value that is NOT what we assigned 240 // to the queue, though I don't know why and I don't know why it's intermittent. Regardless, I've added an extra 241 // check here to try to eliminate this (infrequent) problem. In theory, a KEVENT that does not have a VDKQueuePathEntry 242 // object attached as the udata parameter is not an event we registered for, so we should not be "missing" any events. In theory. 243 // 244 id pe = (__bridge id)(ev.udata); 245 if (pe && [pe respondsToSelector:@selector(path)]) 246 { 247 NSString *fpath = ((VDKQueuePathEntry *)pe).path; 248 if (!fpath) continue; 249 250 [[NSWorkspace sharedWorkspace] noteFileSystemChanged:fpath]; 251 252 // Clear any old notifications 253 [notesToPost removeAllObjects]; 254 255 // Figure out which notifications we need to issue 256 if ((ev.fflags & NOTE_RENAME) == NOTE_RENAME) 257 { 258 [notesToPost addObject:VDKQueueRenameNotification]; 259 } 260 if ((ev.fflags & NOTE_WRITE) == NOTE_WRITE) 261 { 262 [notesToPost addObject:VDKQueueWriteNotification]; 263 } 264 if ((ev.fflags & NOTE_DELETE) == NOTE_DELETE) 265 { 266 [notesToPost addObject:VDKQueueDeleteNotification]; 267 } 268 if ((ev.fflags & NOTE_ATTRIB) == NOTE_ATTRIB) 269 { 270 [notesToPost addObject:VDKQueueAttributeChangeNotification]; 271 } 272 if ((ev.fflags & NOTE_EXTEND) == NOTE_EXTEND) 273 { 274 [notesToPost addObject:VDKQueueSizeIncreaseNotification]; 275 } 276 if ((ev.fflags & NOTE_LINK) == NOTE_LINK) 277 { 278 [notesToPost addObject:VDKQueueLinkCountChangeNotification]; 279 } 280 if ((ev.fflags & NOTE_REVOKE) == NOTE_REVOKE) 281 { 282 [notesToPost addObject:VDKQueueAccessRevocationNotification]; 283 } 284 285 286 NSArray *notes = [[NSArray alloc] initWithArray:notesToPost]; // notesToPost will be changed in the next loop iteration, which will likely occur before the block below runs. 287 288 289 // Post the notifications (or call the delegate method) on the main thread. 290 dispatch_async(dispatch_get_main_queue(), 291 ^{ 292 for (NSString *note in notes) 293 { 294 [_delegate VDKQueue:self receivedNotification:note forPath:fpath]; 295 296 if (!_delegate || _alwaysPostNotifications) 297 { 298 NSDictionary * userInfoDict = [[NSDictionary alloc] initWithObjects: @[fpath] forKeys: @[@"path"]]; 299 [[[NSWorkspace sharedWorkspace] notificationCenter] postNotificationName:note object:self userInfo:userInfoDict]; 300 } 301 } 302 303 }); 304 } 305 } 306 } 307 } 308 } 309 310 @catch (NSException *localException) 311 { 312 NSLog(@"Error in VDKQueue watcherThread: %@", localException); 313 } 314 } 315 316 // Close our kqueue's file descriptor 317 if(close(theFD) == -1) { 318 NSLog(@"VDKQueue watcherThread: Couldn't close main kqueue (%d)", errno); 319 } 320 321 322#if DEBUG_LOG_THREAD_LIFETIME 323 NSLog(@"watcherThread finished."); 324#endif 325 326} 327 328 329 330 331 332 333#pragma mark - 334#pragma mark PUBLIC METHODS 335#pragma ----------------------------------------------------------------------------------------------------------------------------------------------------- 336 337 338- (void) addPath:(NSString *)aPath 339{ 340 if (!aPath) return; 341 342 @synchronized(self) 343 { 344 VDKQueuePathEntry *entry = _watchedPathEntries[aPath]; 345 346 // Only add this path if we don't already have it. 347 if (!entry) 348 { 349 entry = [self addPathToQueue:aPath notifyingAbout:VDKQueueNotifyDefault]; 350 if (!entry) { 351 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); 352 } 353 } 354 } 355 356} 357 358 359- (void) addPath:(NSString *)aPath notifyingAbout:(u_int)flags 360{ 361 if (!aPath) return; 362 363 @synchronized(self) 364 { 365 VDKQueuePathEntry *entry = _watchedPathEntries[aPath]; 366 367 // Only add this path if we don't already have it. 368 if (!entry) 369 { 370 entry = [self addPathToQueue:aPath notifyingAbout:flags]; 371 if (!entry) { 372 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); 373 } 374 } 375 } 376 377} 378 379 380- (void) removePath:(NSString *)aPath 381{ 382 if (!aPath) return; 383 384 @synchronized(self) 385 { 386 VDKQueuePathEntry *entry = _watchedPathEntries[aPath]; 387 388 // Remove it only if we're watching it. 389 if (entry) { 390 [_watchedPathEntries removeObjectForKey:aPath]; 391 } 392 } 393 394} 395 396 397- (void) removeAllPaths 398{ 399 @synchronized(self) 400 { 401 [_watchedPathEntries removeAllObjects]; 402 } 403} 404 405 406- (NSUInteger) numberOfWatchedPaths 407{ 408 NSUInteger count; 409 410 @synchronized(self) 411 { 412 count = [_watchedPathEntries count]; 413 } 414 415 return count; 416} 417 418 419 420 421@end 422 423