Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 568 lines (458 sloc) 17.563 kb
dc5be1c Initial check-in.
uli authored
1 /* =============================================================================
2 FILE: UKKQueue.m
3 PROJECT: Filie
4
5 COPYRIGHT: (c) 2003 M. Uli Kusterer, all rights reserved.
6
7 AUTHORS: M. Uli Kusterer - UK
8
9 LICENSES: MIT License
10
11 REVISIONS:
12 2008-11-05 UK General cleanup, prettier thread handling.
13 2006-03-13 UK Clarified license, streamlined UKFileWatcher stuff,
14 Changed notifications to be useful and turned off by
15 default some deprecated stuff.
16 2004-12-28 UK Several threading fixes.
17 2003-12-21 UK Created.
18 ========================================================================== */
19
20 // -----------------------------------------------------------------------------
21 // Headers:
22 // -----------------------------------------------------------------------------
23
24 #import "UKKQueue.h"
25 #import "UKMainThreadProxy.h"
26 #import <unistd.h>
27 #import <fcntl.h>
28 #include <sys/stat.h>
29
30
31 // -----------------------------------------------------------------------------
32 // Macros:
33 // -----------------------------------------------------------------------------
34
35 #define DEBUG_LOG_THREAD_LIFETIME 1
36 #define DEBUG_DETAILED_MESSAGES 1
37
38
39 // -----------------------------------------------------------------------------
40 // Private stuff:
41 // -----------------------------------------------------------------------------
42
43 @interface UKKQueue (UKPrivateMethods)
44
45 -(void) watcherThread: (id)sender;
46 -(void) postNotification: (NSString*)nm forFile: (NSString*)fp; // Message-posting bottleneck.
47
48 @end
49
50
51 // -----------------------------------------------------------------------------
52 // Globals:
53 // -----------------------------------------------------------------------------
54
55 static UKKQueue * gUKKQueueSharedQueueSingleton = nil;
56 static id gUKKQueueSharedNotificationCenterProxy = nil; // Object to which we send notifications so they get put in the main thread.
57
58
59 @implementation UKKQueue
60
61 // -----------------------------------------------------------------------------
62 // sharedQueue:
63 // Returns a singleton queue object. In many apps (especially those that
64 // subscribe to the notifications) there will only be one kqueue instance,
65 // and in that case you can use this.
66 //
67 // For all other cases, feel free to create additional instances to use
68 // independently.
69 //
70 // REVISIONS:
71 // 2008-11-07 UK Made this always notify, in case some nit sets a
72 // delegate on the singleton.
73 // 2006-03-13 UK Renamed from sharedQueue.
74 // 2005-07-02 UK Created.
75 // -----------------------------------------------------------------------------
76
77 +(id) sharedFileWatcher
78 {
79 @synchronized( self )
80 {
81 if( !gUKKQueueSharedQueueSingleton )
82 {
83 gUKKQueueSharedQueueSingleton = [[UKKQueue alloc] init]; // This is a singleton, and thus an intentional "leak".
84 [gUKKQueueSharedQueueSingleton setAlwaysNotify: YES];
85 }
86 }
87
88 return gUKKQueueSharedQueueSingleton;
89 }
90
91
92 // -----------------------------------------------------------------------------
93 // * CONSTRUCTOR:
94 // Creates a new KQueue and starts that thread we use for our
95 // notifications.
96 //
97 // REVISIONS:
98 // 2008-11-07 UK Adapted to new threading model.
99 // 2004-11-12 UK Doesn't pass self as parameter to watcherThread anymore,
100 // because detachNewThreadSelector retains target and args,
101 // which would cause us to never be released.
102 // 2004-03-13 UK Documented.
103 // -----------------------------------------------------------------------------
104
105 -(id) init
106 {
107 self = [super init];
108 if( self )
109 {
110 if( !gUKKQueueSharedNotificationCenterProxy )
111 {
112 gUKKQueueSharedNotificationCenterProxy = [[[NSWorkspace sharedWorkspace] notificationCenter] copyMainThreadProxy]; // Singleton, 'intentional leak'.
113 [gUKKQueueSharedNotificationCenterProxy setWaitForCompletion: NO]; // Better performance and avoid deadlocks.
114 }
115
116 queueFD = kqueue();
117 if( queueFD == -1 )
118 {
119 [self release];
120 return nil;
121 }
122
123 watchedPaths = [[NSMutableArray alloc] init];
124 watchedFDs = [[NSMutableArray alloc] init];
125
126 threadHasTerminated = YES;
127 }
128
129 return self;
130 }
131
132
133 // -----------------------------------------------------------------------------
134 // * DESTRUCTOR:
135 // Releases the kqueue again.
136 //
137 // REVISIONS:
138 // 2008-11-07 UK Adapted to new threading model.
139 // 2004-03-13 UK Documented.
140 // -----------------------------------------------------------------------------
141
142 -(void) dealloc
143 {
144 delegate = nil;
145 [delegateProxy release];
146
147 // Close all our file descriptors so the files can be deleted:
148 [self removeAllPaths];
149
150 [watchedPaths release];
151 watchedPaths = nil;
152 [watchedFDs release];
153 watchedFDs = nil;
154
155 [super dealloc];
156
157 //NSLog(@"kqueue released.");
158 }
159
160
161 -(void) finalize
162 {
163 // Close all our file descriptors so the files can be deleted:
164 [self removeAllPaths];
165
166 [super finalize];
167 }
168
169
170 -(void) invalidate
171 {
172 @synchronized( self )
173 {
174 keepThreadRunning = NO;
175 }
176 }
177
178
179 // -----------------------------------------------------------------------------
180 // removeAllPaths:
181 // Stop listening for changes to all paths. This removes all
182 // notifications.
183 //
184 // REVISIONS:
185 // 2008-11-07 UK Renamed from unsubscribeAll, for consistency.
186 // 2004-12-28 UK Added as suggested by bbum.
187 // -----------------------------------------------------------------------------
188
189 -(void) unsubscribeAll
190 {
191 [self removeAllPaths];
192 }
193
194 -(void) removeAllPaths
195 {
196 @synchronized( self )
197 {
198 NSEnumerator * fdEnumerator = [watchedFDs objectEnumerator];
199 NSNumber * anFD;
200
201 while( (anFD = [fdEnumerator nextObject]) != nil )
202 close( [anFD intValue] );
203
204 [watchedFDs removeAllObjects];
205 [watchedPaths removeAllObjects];
206
207 [self invalidate];
208 }
209 }
210
211
212 // -----------------------------------------------------------------------------
213 // queueFD:
214 // Returns a Unix file descriptor for the KQueue this uses. The descriptor
215 // is owned by this object. Do not close it!
216 //
217 // REVISIONS:
218 // 2004-03-13 UK Documented.
219 // -----------------------------------------------------------------------------
220
221 -(int) queueFD
222 {
223 return queueFD;
224 }
225
226
227 // -----------------------------------------------------------------------------
228 // addPathToQueue:
229 // Tell this queue to listen for all interesting notifications sent for
230 // the object at the specified path. If you want more control, use the
231 // addPathToQueue:notifyingAbout: variant instead.
232 //
233 // REVISIONS:
234 // 2004-03-13 UK Documented.
235 // -----------------------------------------------------------------------------
236
237 -(void) addPath: (NSString*)path
238 {
239 [self addPath: path notifyingAbout: UKKQueueNotifyAboutRename
240 | UKKQueueNotifyAboutWrite
241 | UKKQueueNotifyAboutDelete
242 | UKKQueueNotifyAboutAttributeChange
243 | UKKQueueNotifyAboutSizeIncrease
244 | UKKQueueNotifyAboutLinkCountChanged
245 | UKKQueueNotifyAboutAccessRevocation ];
246 }
247
248
249 // -----------------------------------------------------------------------------
250 // addPath:notfyingAbout:
251 // Tell this queue to listen for the specified notifications sent for
252 // the object at the specified path.
253 //
254 // NOTE: This keeps track of each time you call it. If you subscribe twice,
255 // you are expected to unsubscribe twice. This is necessary for the
256 // singleton to be reasonably safe to use, because otherwise two
257 // objects could subscribe for the same path, and when one of them
258 // unsubscribes, it would unsubscribe both.
259 //
260 // REVISIONS:
261 // 2008-11-07 UK Renamed from addPathToQueue:notifyingAbout:
262 // 2005-06-29 UK Files are now opened using O_EVTONLY instead of O_RDONLY
263 // which allows ejecting or deleting watched files/folders.
264 // Thanks to Phil Hargett for finding this flag in the docs.
265 // 2004-03-13 UK Documented.
266 // -----------------------------------------------------------------------------
267
268 -(void) addPathToQueue: (NSString*)path notifyingAbout: (u_int)fflags
269 {
270 [self addPath: path notifyingAbout: fflags];
271 }
272
273 -(void) addPath: (NSString*)path notifyingAbout: (u_int)fflags
274 {
275 struct timespec nullts = { 0, 0 };
276 struct kevent ev;
277 int fd = open( [path fileSystemRepresentation], O_EVTONLY, 0 );
278
279 if( fd >= 0 )
280 {
281 EV_SET( &ev, fd, EVFILT_VNODE,
282 EV_ADD | EV_ENABLE | EV_CLEAR,
283 fflags, 0, (void*)path );
284
285 @synchronized( self )
286 {
287 [watchedPaths addObject: path];
288 [watchedFDs addObject: [NSNumber numberWithInt: fd]];
289 kevent( queueFD, &ev, 1, NULL, 0, &nullts );
290
291 // Start new thread that fetches and processes our events:
292 if( !keepThreadRunning )
293 {
294 while( !threadHasTerminated )
295 usleep(10);
296
297 keepThreadRunning = YES;
298 threadHasTerminated = NO;
299 [NSThread detachNewThreadSelector:@selector(watcherThread:) toTarget:self withObject:nil];
300 }
301 }
302 }
303 }
304
305
306 // -----------------------------------------------------------------------------
307 // removePath:
308 // Stop listening for changes to the specified path. Use this to balance
309 // both addPath:notfyingAbout: as well as addPath:.
310 //
311 // REVISIONS:
312 // 2004-03-13 UK Documented.
313 // -----------------------------------------------------------------------------
314
315 -(void) removePath: (NSString*)path
316 {
317 int index = 0;
318 int fd = -1;
319
320 @synchronized( self )
321 {
322 index = [watchedPaths indexOfObject: path];
323
324 if( index == NSNotFound )
325 return;
326
327 fd = [[watchedFDs objectAtIndex: index] intValue];
328
329 [watchedFDs removeObjectAtIndex: index];
330 [watchedPaths removeObjectAtIndex: index];
331
332 if( [watchedPaths count] == 0 )
333 [self invalidate];
334 }
335
336 if( close( fd ) == -1 )
337 NSLog(@"removePathFromQueue: Couldn't close file descriptor (%d)", errno);
338 }
339
340
341 /*-(NSString*) filePathForFileDescriptor:(const int)fd oldPath: (NSString*)oldPath
342 {
343 struct stat fileStatus;
344 struct stat currentFileStatus;
345
346 // Get file status
347 if( fstat(fd, &fileStatus) == -1 )
348 return nil;
349
350 NSString* basePath = [oldPath stringByDeletingLastPathComponent];
351 NSEnumerator *dirEnumerator;
352 dirEnumerator = [[NSFileManager defaultManager] enumeratorAtPath: basePath];
353
354 NSString *path;
355 while( (path = [dirEnumerator nextObject]) )
356 {
357 NSString *fullPath = [basePath stringByAppendingPathComponent:path];
358 if( stat([fullPath fileSystemRepresentation], &currentFileStatus) == 0 )
359 {
360 if ((currentFileStatus.st_dev == fileStatus.st_dev) &&
361 (currentFileStatus.st_ino == fileStatus.st_ino))
362 {
363 // Found file
364 return fullPath;
365 }
366 }
367 }
368
369 // Didn't find file
370 return nil;
371 }*/
372
373
374 -(id) delegate
375 {
376 return delegate;
377 }
378
379 -(void) setDelegate: (id)newDelegate
380 {
381 id oldProxy = delegateProxy;
382 delegate = newDelegate;
383 delegateProxy = [delegate copyMainThreadProxy];
384 [delegateProxy setWaitForCompletion: NO]; // Better performance and avoid deadlocks.
385 [oldProxy release];
386 }
387
388 // -----------------------------------------------------------------------------
389 // Flag to send a notification even if we have a delegate:
390 // -----------------------------------------------------------------------------
391
392 -(BOOL) alwaysNotify
393 {
394 return alwaysNotify;
395 }
396
397
398 -(void) setAlwaysNotify: (BOOL)n
399 {
400 alwaysNotify = n;
401 }
402
403
404 // -----------------------------------------------------------------------------
405 // description:
406 // This method can be used to help in debugging. It provides the value
407 // used by NSLog & co. when you request to print this object using the
408 // %@ format specifier.
409 //
410 // REVISIONS:
411 // 2008-11-05 UK Made this indentation-aware.
412 // 2004-11-12 UK Created.
413 // -----------------------------------------------------------------------------
414
415 -(NSString*) descriptionWithLocale: (id)locale indent: (NSUInteger)level
416 {
417 NSMutableString* mutStr = [NSMutableString string];
418 int x = 0;
419
420 for( x = 0; x < level; x++ )
421 [mutStr appendString: @" "];
422 [mutStr appendString: NSStringFromClass([self class])];
423 for( x = 0; x < level; x++ )
424 [mutStr appendString: @" "];
425 [mutStr appendString: @"{"];
426 for( x = 0; x < level; x++ )
427 [mutStr appendString: @" "];
428 [mutStr appendFormat: @"watchedPaths = %@", [watchedPaths descriptionWithLocale: locale indent: level +1]];
429 for( x = 0; x < level; x++ )
430 [mutStr appendString: @" "];
431 [mutStr appendFormat: @"alwaysNotify = %@", (alwaysNotify? @"YES" : @"NO")];
432 for( x = 0; x < level; x++ )
433 [mutStr appendString: @" "];
434 [mutStr appendString: @"}"];
435
436 return mutStr;
437 }
438
439 @end
440
441 @implementation UKKQueue (UKPrivateMethods)
442
443 // -----------------------------------------------------------------------------
444 // watcherThread:
445 // This method is called by our NSThread to loop and poll for any file
446 // changes that our kqueue wants to tell us about. This sends separate
447 // notifications for the different kinds of changes that can happen.
448 // All messages are sent via the postNotification:forFile: main bottleneck.
449 //
450 // This also calls sharedWorkspace's noteFileSystemChanged.
451 //
452 // To terminate this method (and its thread), set keepThreadRunning to NO.
453 //
454 // REVISIONS:
455 // 2008-11-07 UK Adapted to new threading model.
456 // 2005-08-27 UK Changed to use keepThreadRunning instead of kqueueFD
457 // being -1 as termination criterion, and to close the
458 // queue in this thread so the main thread isn't blocked.
459 // 2004-11-12 UK Fixed docs to include termination criterion, added
460 // timeout to make sure the bugger gets disposed.
461 // 2004-03-13 UK Documented.
462 // -----------------------------------------------------------------------------
463
464 -(void) watcherThread: (id)sender
465 {
466 int n;
467 struct kevent ev;
468 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.
469 int theFD = queueFD; // So we don't have to risk accessing iVars when the thread is terminated.
470
471 #if DEBUG_LOG_THREAD_LIFETIME
472 NSLog(@"watcherThread started.");
473 #endif
474
475 threadHasTerminated = NO;
476 while( keepThreadRunning )
477 {
478 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
479
480 NS_DURING
481 n = kevent( queueFD, NULL, 0, &ev, 1, &timeout );
482 if( n > 0 )
483 {
484 NSLog( @"KEVENT returned %d", n );
485 if( ev.filter == EVFILT_VNODE )
486 {
487 NSLog( @"KEVENT filter is EVFILT_VNODE" );
488 if( ev.fflags )
489 {
490 NSLog( @"KEVENT flags are set" );
491 NSString* fpath = [[(NSString *)ev.udata retain] autorelease]; // In case one of the notified folks removes the path.
492 [[NSWorkspace sharedWorkspace] noteFileSystemChanged: fpath];
493
494 if( (ev.fflags & NOTE_RENAME) == NOTE_RENAME )
495 [self postNotification: UKFileWatcherRenameNotification forFile: fpath];
496 if( (ev.fflags & NOTE_WRITE) == NOTE_WRITE )
497 [self postNotification: UKFileWatcherWriteNotification forFile: fpath];
498 if( (ev.fflags & NOTE_DELETE) == NOTE_DELETE )
499 [self postNotification: UKFileWatcherDeleteNotification forFile: fpath];
500 if( (ev.fflags & NOTE_ATTRIB) == NOTE_ATTRIB )
501 [self postNotification: UKFileWatcherAttributeChangeNotification forFile: fpath];
502 if( (ev.fflags & NOTE_EXTEND) == NOTE_EXTEND )
503 [self postNotification: UKFileWatcherSizeIncreaseNotification forFile: fpath];
504 if( (ev.fflags & NOTE_LINK) == NOTE_LINK )
505 [self postNotification: UKFileWatcherLinkCountChangeNotification forFile: fpath];
506 if( (ev.fflags & NOTE_REVOKE) == NOTE_REVOKE )
507 [self postNotification: UKFileWatcherAccessRevocationNotification forFile: fpath];
508 }
509 }
510 }
511 NS_HANDLER
512 NSLog(@"Error in UKKQueue watcherThread: %@",localException);
513 NS_ENDHANDLER
514
515 [pool release];
516 }
517
518 // Close our kqueue's file descriptor:
519 if( close( theFD ) == -1 )
520 NSLog(@"watcherThread: Couldn't close main kqueue (%d)", errno);
521
522 threadHasTerminated = YES;
523
524 #if DEBUG_LOG_THREAD_LIFETIME
525 NSLog(@"watcherThread finished.");
526 #endif
527 }
528
529
530 // -----------------------------------------------------------------------------
531 // postNotification:forFile:
532 // This is the main bottleneck for posting notifications. If you don't want
533 // the notifications to go through NSWorkspace, override this method and
534 // send them elsewhere.
535 //
536 // REVISIONS:
537 // 2008-11-07 UK Got rid of old notifications.
538 // 2004-02-27 UK Changed this to send new notification, and the old one
539 // only to objects that respond to it. The old category on
540 // NSObject could cause problems with the proxy itself.
541 // 2004-10-31 UK Helloween fun: Make this use a mainThreadProxy and
542 // allow sending the notification even if we have a
543 // delegate.
544 // 2004-03-13 UK Documented.
545 // -----------------------------------------------------------------------------
546
547 -(void) postNotification: (NSString*)nm forFile: (NSString*)fp
548 {
549 #if DEBUG_DETAILED_MESSAGES
550 NSLog( @"%@: %@", nm, fp );
551 #endif
552
553 if( delegate && delegateProxy )
554 {
555 [delegateProxy watcher: self receivedNotification: nm forPath: fp];
556 }
557
558 if( !delegateProxy || alwaysNotify )
559 {
560 [gUKKQueueSharedNotificationCenterProxy postNotificationName: nm object: self
561 userInfo: [NSDictionary dictionaryWithObjectsAndKeys: fp, @"path", nil]]; // The proxy sends the notification on the main thread.
562 }
563 }
564
565 @end
566
567
Something went wrong with that request. Please try again.