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