root / trunk / UKKQueue.m

Revision 5, 14.6 kB (checked in by stephen_booth, 3 years ago)

Added file watching support

Line 
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                2006-03-13      UK      Clarified license, streamlined UKFileWatcher stuff,
13                                                Changed notifications to be useful and turned off by
14                                                default some deprecated stuff.
15        2004-12-28  UK  Several threading fixes.
16                2003-12-21      UK      Created.
17   ========================================================================== */
18
19// -----------------------------------------------------------------------------
20//  Headers:
21// -----------------------------------------------------------------------------
22
23#import "UKKQueue.h"
24#import "UKMainThreadProxy.h"
25#import <unistd.h>
26#import <fcntl.h>
27
28
29// -----------------------------------------------------------------------------
30//  Macros:
31// -----------------------------------------------------------------------------
32
33// @synchronized isn't available prior to 10.3, so we use a typedef so
34//  this class is thread-safe on Panther but still compiles on older OSs.
35
36#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_3
37#define AT_SYNCHRONIZED(n)      @synchronized(n)
38#else
39#define AT_SYNCHRONIZED(n)
40#endif
41
42
43// -----------------------------------------------------------------------------
44//  Globals:
45// -----------------------------------------------------------------------------
46
47static UKKQueue * gUKKQueueSharedQueueSingleton = nil;
48
49
50@implementation UKKQueue
51
52// Deprecated:
53#if UKKQUEUE_OLD_SINGLETON_ACCESSOR_NAME
54+(UKKQueue*) sharedQueue
55{
56        return [self sharedFileWatcher];
57}
58#endif
59
60// -----------------------------------------------------------------------------
61//  sharedQueue:
62//              Returns a singleton queue object. In many apps (especially those that
63//      subscribe to the notifications) there will only be one kqueue instance,
64//      and in that case you can use this.
65//
66//      For all other cases, feel free to create additional instances to use
67//      independently.
68//
69//      REVISIONS:
70//              2006-03-13      UK      Renamed from sharedQueue.
71//      2005-07-02  UK  Created.
72// -----------------------------------------------------------------------------
73
74+(id) sharedFileWatcher
75{
76    AT_SYNCHRONIZED( self )
77    {
78        if( !gUKKQueueSharedQueueSingleton )
79            gUKKQueueSharedQueueSingleton = [[UKKQueue alloc] init];    // This is a singleton, and thus an intentional "leak".
80    }
81   
82    return gUKKQueueSharedQueueSingleton;
83}
84
85
86// -----------------------------------------------------------------------------
87//      * CONSTRUCTOR:
88//              Creates a new KQueue and starts that thread we use for our
89//              notifications.
90//
91//      REVISIONS:
92//      2004-11-12  UK  Doesn't pass self as parameter to watcherThread anymore,
93//                      because detachNewThreadSelector retains target and args,
94//                      which would cause us to never be released.
95//              2004-03-13      UK      Documented.
96// -----------------------------------------------------------------------------
97
98-(id)   init
99{
100        self = [super init];
101        if( self )
102        {
103                queueFD = kqueue();
104                if( queueFD == -1 )
105                {
106                        [self release];
107                        return nil;
108                }
109               
110                watchedPaths = [[NSMutableArray alloc] init];
111                watchedFDs = [[NSMutableArray alloc] init];
112               
113                // Start new thread that fetches and processes our events:
114                keepThreadRunning = YES;
115                [NSThread detachNewThreadSelector:@selector(watcherThread:) toTarget:self withObject:nil];
116        }
117       
118        return self;
119}
120
121
122// -----------------------------------------------------------------------------
123//      release:
124//              Since NSThread retains its target, we need this method to terminate the
125//      thread when we reach a retain-count of two. The thread is terminated by
126//      setting keepThreadRunning to NO.
127//
128//      REVISIONS:
129//              2004-11-12      UK      Created.
130// -----------------------------------------------------------------------------
131
132-(oneway void) release
133{
134    AT_SYNCHRONIZED(self)
135    {
136        //NSLog(@"%@ (%d)", self, [self retainCount]);
137        if( [self retainCount] == 2 && keepThreadRunning )
138            keepThreadRunning = NO;
139    }
140   
141    [super release];
142}
143   
144// -----------------------------------------------------------------------------
145//      * DESTRUCTOR:
146//              Releases the kqueue again.
147//
148//      REVISIONS:
149//              2004-03-13      UK      Documented.
150// -----------------------------------------------------------------------------
151
152-(void) dealloc
153{
154        delegate = nil;
155        [delegateProxy release];
156       
157        if( keepThreadRunning )
158                keepThreadRunning = NO;
159       
160        // Close all our file descriptors so the files can be deleted:
161        NSEnumerator*   enny = [watchedFDs objectEnumerator];
162        NSNumber*               fdNum;
163        while( (fdNum = [enny nextObject]) )
164        {
165        if( close( [fdNum intValue] ) == -1 )
166            NSLog(@"dealloc: Couldn't close file descriptor (%d)", errno);
167    }
168       
169        [watchedPaths release];
170        watchedPaths = nil;
171        [watchedFDs release];
172        watchedFDs = nil;
173       
174        [super dealloc];
175   
176    //NSLog(@"kqueue released.");
177}
178
179
180// -----------------------------------------------------------------------------
181//      queueFD:
182//              Returns a Unix file descriptor for the KQueue this uses. The descriptor
183//              is owned by this object. Do not close it!
184//
185//      REVISIONS:
186//              2004-03-13      UK      Documented.
187// -----------------------------------------------------------------------------
188
189-(int)  queueFD
190{
191        return queueFD;
192}
193
194
195// -----------------------------------------------------------------------------
196//      addPathToQueue:
197//              Tell this queue to listen for all interesting notifications sent for
198//              the object at the specified path. If you want more control, use the
199//              addPathToQueue:notifyingAbout: variant instead.
200//
201//      REVISIONS:
202//              2004-03-13      UK      Documented.
203// -----------------------------------------------------------------------------
204
205-(void) addPathToQueue: (NSString*)path
206{
207        [self addPath: path];
208}
209
210
211-(void) addPath: (NSString*)path
212{
213        [self addPathToQueue: path notifyingAbout: UKKQueueNotifyAboutRename
214                                                                                                | UKKQueueNotifyAboutWrite
215                                                                                                | UKKQueueNotifyAboutDelete
216                                                                                                | UKKQueueNotifyAboutAttributeChange];
217}
218
219
220// -----------------------------------------------------------------------------
221//      addPathToQueue:notfyingAbout:
222//              Tell this queue to listen for the specified notifications sent for
223//              the object at the specified path.
224//
225//      REVISIONS:
226//      2005-06-29  UK  Files are now opened using O_EVTONLY instead of O_RDONLY
227//                      which allows ejecting or deleting watched files/folders.
228//                      Thanks to Phil Hargett for finding this flag in the docs.
229//              2004-03-13      UK      Documented.
230// -----------------------------------------------------------------------------
231
232-(void) addPathToQueue: (NSString*)path notifyingAbout: (u_int)fflags
233{
234        struct timespec         nullts = { 0, 0 };
235        struct kevent           ev;
236        int                                     fd = open( [path fileSystemRepresentation], O_EVTONLY, 0 );
237       
238    if( fd >= 0 )
239    {
240        EV_SET( &ev, fd, EVFILT_VNODE,
241                                EV_ADD | EV_ENABLE | EV_CLEAR,
242                                fflags, 0, (void*)path );
243               
244        AT_SYNCHRONIZED( self )
245        {
246            [watchedPaths addObject: path];
247            [watchedFDs addObject: [NSNumber numberWithInt: fd]];
248            kevent( queueFD, &ev, 1, NULL, 0, &nullts );
249        }
250    }
251}
252
253
254-(void) removePath: (NSString*)path
255{
256    [self removePathFromQueue: path];
257}
258
259
260// -----------------------------------------------------------------------------
261//      removePathFromQueue:
262//              Stop listening for changes to the specified path. This removes all
263//              notifications. Use this to balance both addPathToQueue:notfyingAbout:
264//              as well as addPathToQueue:.
265//
266//      REVISIONS:
267//              2004-03-13      UK      Documented.
268// -----------------------------------------------------------------------------
269
270-(void) removePathFromQueue: (NSString*)path
271{
272    int         index = 0;
273    int         fd = -1;
274   
275    AT_SYNCHRONIZED( self )
276    {
277        index = [watchedPaths indexOfObject: path];
278       
279        if( index == NSNotFound )
280            return;
281       
282        fd = [[watchedFDs objectAtIndex: index] intValue];
283       
284        [watchedFDs removeObjectAtIndex: index];
285        [watchedPaths removeObjectAtIndex: index];
286    }
287       
288        if( close( fd ) == -1 )
289        NSLog(@"removePathFromQueue: Couldn't close file descriptor (%d)", errno);
290}
291
292
293// -----------------------------------------------------------------------------
294//      removeAllPathsFromQueue:
295//              Stop listening for changes to all paths. This removes all
296//              notifications.
297//
298//  REVISIONS:
299//      2004-12-28  UK  Added as suggested by bbum.
300// -----------------------------------------------------------------------------
301
302-(void) removeAllPathsFromQueue;
303{
304    AT_SYNCHRONIZED( self )
305    {
306        NSEnumerator *  fdEnumerator = [watchedFDs objectEnumerator];
307        NSNumber     *  anFD;
308       
309        while( (anFD = [fdEnumerator nextObject]) != nil )
310            close( [anFD intValue] );
311
312        [watchedFDs removeAllObjects];
313        [watchedPaths removeAllObjects];
314    }
315}
316
317
318// -----------------------------------------------------------------------------
319//      watcherThread:
320//              This method is called by our NSThread to loop and poll for any file
321//              changes that our kqueue wants to tell us about. This sends separate
322//              notifications for the different kinds of changes that can happen.
323//              All messages are sent via the postNotification:forFile: main bottleneck.
324//
325//              This also calls sharedWorkspace's noteFileSystemChanged.
326//
327//      To terminate this method (and its thread), set keepThreadRunning to NO.
328//
329//      REVISIONS:
330//              2005-08-27      UK      Changed to use keepThreadRunning instead of kqueueFD
331//                                              being -1 as termination criterion, and to close the
332//                                              queue in this thread so the main thread isn't blocked.
333//              2004-11-12      UK      Fixed docs to include termination criterion, added
334//                      timeout to make sure the bugger gets disposed.
335//              2004-03-13      UK      Documented.
336// -----------------------------------------------------------------------------
337
338-(void)         watcherThread: (id)sender
339{
340        int                                     n;
341    struct kevent               ev;
342    struct timespec     timeout = { 5, 0 }; // 5 seconds timeout.
343        int                                     theFD = queueFD;        // So we don't have to risk accessing iVars when the thread is terminated.
344   
345    while( keepThreadRunning )
346    {
347                NSAutoreleasePool*  pool = [[NSAutoreleasePool alloc] init];
348               
349                NS_DURING
350                        n = kevent( queueFD, NULL, 0, &ev, 1, &timeout );
351                        if( n > 0 )
352                        {
353                                if( ev.filter == EVFILT_VNODE )
354                                {
355                                        if( ev.fflags )
356                                        {
357                                                NSString*               fpath = [[(NSString *)ev.udata retain] autorelease];    // In case one of the notified folks removes the path.
358                                                //NSLog(@"UKKQueue: Detected file change: %@", fpath);
359                                                [[NSWorkspace sharedWorkspace] noteFileSystemChanged: fpath];
360                                               
361                                                //NSLog(@"ev.flags = %u",ev.fflags);    // DEBUG ONLY!
362                                               
363                                                if( (ev.fflags & NOTE_RENAME) == NOTE_RENAME )
364                                                        [self postNotification: UKFileWatcherRenameNotification forFile: fpath];
365                                                if( (ev.fflags & NOTE_WRITE) == NOTE_WRITE )
366                                                        [self postNotification: UKFileWatcherWriteNotification forFile: fpath];
367                                                if( (ev.fflags & NOTE_DELETE) == NOTE_DELETE )
368                                                        [self postNotification: UKFileWatcherDeleteNotification forFile: fpath];
369                                                if( (ev.fflags & NOTE_ATTRIB) == NOTE_ATTRIB )
370                                                        [self postNotification: UKFileWatcherAttributeChangeNotification forFile: fpath];
371                                                if( (ev.fflags & NOTE_EXTEND) == NOTE_EXTEND )
372                                                        [self postNotification: UKFileWatcherSizeIncreaseNotification forFile: fpath];
373                                                if( (ev.fflags & NOTE_LINK) == NOTE_LINK )
374                                                        [self postNotification: UKFileWatcherLinkCountChangeNotification forFile: fpath];
375                                                if( (ev.fflags & NOTE_REVOKE) == NOTE_REVOKE )
376                                                        [self postNotification: UKFileWatcherAccessRevocationNotification forFile: fpath];
377                                        }
378                                }
379                        }
380                NS_HANDLER
381                        NSLog(@"Error in UKKQueue watcherThread: %@",localException);
382                NS_ENDHANDLER
383               
384                [pool release];
385    }
386   
387        // Close our kqueue's file descriptor:
388        if( close( theFD ) == -1 )
389                NSLog(@"release: Couldn't close main kqueue (%d)", errno);
390       
391    //NSLog(@"exiting kqueue watcher thread.");
392}
393
394
395// -----------------------------------------------------------------------------
396//      postNotification:forFile:
397//              This is the main bottleneck for posting notifications. If you don't want
398//              the notifications to go through NSWorkspace, override this method and
399//              send them elsewhere.
400//
401//      REVISIONS:
402//      2004-02-27  UK  Changed this to send new notification, and the old one
403//                      only to objects that respond to it. The old category on
404//                      NSObject could cause problems with the proxy itself.
405//              2004-10-31      UK      Helloween fun: Make this use a mainThreadProxy and
406//                                              allow sending the notification even if we have a
407//                                              delegate.
408//              2004-03-13      UK      Documented.
409// -----------------------------------------------------------------------------
410
411-(void) postNotification: (NSString*)nm forFile: (NSString*)fp
412{
413        if( delegateProxy )
414    {
415        #if UKKQUEUE_BACKWARDS_COMPATIBLE
416        if( ![delegateProxy respondsToSelector: @selector(watcher:receivedNotification:forPath:)] )
417            [delegateProxy kqueue: self receivedNotification: nm forFile: fp];
418        else
419        #endif
420            [delegateProxy watcher: self receivedNotification: nm forPath: fp];
421    }
422       
423        if( !delegateProxy || alwaysNotify )
424        {
425                #if UKKQUEUE_SEND_STUPID_NOTIFICATIONS
426                [[[NSWorkspace sharedWorkspace] notificationCenter] postNotificationName: nm object: fp];
427                #else
428                [[[NSWorkspace sharedWorkspace] notificationCenter] postNotificationName: nm object: self
429                                                                                                                                userInfo: [NSDictionary dictionaryWithObjectsAndKeys: fp, @"path", nil]];
430                #endif
431        }
432}
433
434-(id)   delegate
435{
436    return delegate;
437}
438
439-(void) setDelegate: (id)newDelegate
440{
441        id      oldProxy = delegateProxy;
442        delegate = newDelegate;
443        delegateProxy = [delegate copyMainThreadProxy];
444        [oldProxy release];
445}
446
447// -----------------------------------------------------------------------------
448//      Flag to send a notification even if we have a delegate:
449// -----------------------------------------------------------------------------
450
451-(BOOL) alwaysNotify
452{
453        return alwaysNotify;
454}
455
456
457-(void) setAlwaysNotify: (BOOL)n
458{
459        alwaysNotify = n;
460}
461
462
463// -----------------------------------------------------------------------------
464//      description:
465//              This method can be used to help in debugging. It provides the value
466//      used by NSLog & co. when you request to print this object using the
467//      %@ format specifier.
468//
469//      REVISIONS:
470//              2004-11-12      UK      Created.
471// -----------------------------------------------------------------------------
472
473-(NSString*)    description
474{
475        return [NSString stringWithFormat: @"%@ { watchedPaths = %@, alwaysNotify = %@ }", NSStringFromClass([self class]), watchedPaths, (alwaysNotify? @"YES" : @"NO") ];
476}
477
478@end
479
480
Note: See TracBrowser for help on using the browser.