-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathSSYPathObserver.h
514 lines (422 loc) · 17.2 KB
/
SSYPathObserver.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
#import <Cocoa/Cocoa.h>
// We need to import the following system header in this header, so that
// classes including this headers know the definitions of the NOTE_XXX
// constants which are used to define our SSYPathObserverChangeFlagsXxxx
// constants.
#import <sys/event.h>
/*!
@brief Error domain for the SSYPathObserver class
*/
extern NSString* const SSYPathObserverErrorDomain ;
/*!
@brief Notification which will be passed on the designated
notifee thread when a watched change is observed on a watched filesystem item.
The combination of notifications you will receive due to any
given filesystem change is not predictable. For example,
I have found that when I change the data in an ASCII text
file from "Hello1" to "Hello2", I may receive two notifications
with these flags:
(SSYPathObserverChangeFlagsAttributes+SSYPathObserverChangeFlagsBigger)
(SSYPathObserverChangeFlagsData+SSYPathObserverChangeFlagsBigger)
or three notifications with these flags:
(SSYPathObserverChangeFlagsBigger)
(SSYPathObserverChangeFlagsData+SSYPathObserverChangeFlagsBigger)
(SSYPathObserverChangeFlagsAttributes)
Note that, in this case the SSYPathObserverChangeFlagsBigger flag was
received twice. (The above was observed running in Mac OS 10.6, 32-bits,
compiled with 10.5 SDK.)
*/
extern NSString* const SSYPathObserverChangeNotification ;
/*!
@brief Name of the watcher thread which is created when an
SSYPathObserver is initialized and should exit when its
ssyPathObserver is deallocated.
@details This has no use that we know of, other than for quality
assurance testing and debugging, but that's important!
*/
extern NSString* const SSYPathObserverWatcherThread ;
/*!
@brief Keys in the user info dictionary of an
SSYPathObserverChangeNotification
*/
/*!
@brief The path of the filesystem item which changed
*/
extern NSString* const SSYPathObserverPathKey ;
/*!
@brief The BSD file descriptor which has been opened by
SSYPathObserver for the watched file.
@details If you require the new path to the watched file
after it has been moved, you can get it by giving the value
of this key to -[NSFileManager(SSYFileDescriptor)pathForFileDescriptor:::].
We don't do that because it's a fairly expensive process.
*/
extern NSString* const SSYPathObserverFileDescriptorKey ;
/*!
@brief Flags which indicate which properties of the changed
filesystem item changed.
@details Bitwise OR of SSYPathObserverChangeFlags values
*/
extern NSString* const SSYPathObserverChangeFlagsKey ;
/*!
@brief User info which was passed in to addPath:watchFlags:notifyThread:userInfo:error_p:
*/
extern NSString* const SSYPathObserverUserInfoKey ;
/*!
@brief File's Vnode was removed. Note that this is "removed"
in the unix sense. Moving a file to the Trash does not
count here.
@details For debugging only, the value of this constant, compiled
with the macOS 10.6 SDK, 32-bit i386, is: 1
*/
#define SSYPathObserverChangeFlagsDelete NOTE_DELETE
/*!
@brief For regular files, the contents of file's data fork changed; for
directories, the contents of the directory has changed, meaning that one of
the files it contains has been added, deleted, renamed or had its data
modified
@details For debugging only, the value of this constant, compiled
with the macOS 10.6 SDK, 32-bit i386, is: 2
*/
#define SSYPathObserverChangeFlagsData NOTE_WRITE
/*!
@brief Size of file was increased
@details For debugging only, the value of this constant, compiled
with the macOS 10.6 SDK, 32-bit i386, is: 4
*/
#define SSYPathObserverChangeFlagsBigger NOTE_EXTEND
/*!
@brief File attributes were changed
@details For debugging only, the value of this constant, compiled
with the macOS 10.6 SDK, 32-bit i386, is: 8
*/
#define SSYPathObserverChangeFlagsAttributes NOTE_ATTRIB
/*!
@brief The Link Count of the file was changed
@details For debugging only, the value of this constant, compiled
with the macOS 10.6 SDK, 32-bit i386, is: 16
*/
#define SSYPathObserverChangeFlagsLinkCount NOTE_LINK
/*!
@brief The file was renamed, which in Macintosh parlance
includes *moving* a file, including *moving* to the Trash.
@details For debugging only, the value of this constant, compiled
with the macOS 10.6 SDK, 32-bit i386, is: 32
*/
#define SSYPathObserverChangeFlagsRename NOTE_RENAME
/*!
@brief Access (permissions) to the file was revoked
@details For debugging only, the value of this constant, compiled
with the macOS 10.6 SDK, 32-bit i386, is: 64
*/
#define SSYPathObserverChangeFlagsAccessGone NOTE_REVOKE
/*!
@brief A Cocoa wrapper around the kqueue path-watching
notification system which will watch a set of filesystem items.
@details
* Background
Although File system events (FSEvents) became available
for watching directories in Mac OS 10.5, according to Apple's
"File System Events Programming Guide" > Appendix A,
"File system events are intended to provide notification of changes
with directory-level granularity. For most purposes, this is sufficient.
In some cases, however, you may need to receive notifications with
finer granularity. For example, you might need to monitor only changes
made to a single file. For that purpose, the kernel queue (kqueue)
notification system is more appropriate."
This class uses the kqueue notification system. However, you can use this
class to watch directories. To do so, pass in a path ending in a slash ("/")
and flags SSYPathObserverChangeFlagsData. You will get a notification
whenever any file in the directory is added, removed, or modified.
IMPORTANT: You cannot use a kqueue, and therefore cannot use this class,
to observe a file/folder which does not exist yet. If you try,
-addPath::::: will an error.
* Interface
An instance of this class is typically configured to watch one or
more filesystem items, identified by their paths. When any of these
items changes and triggers a kqueue event, the instance will issue
a notification via NSNotificationCenter.
* Availability
This class requires macOS 10.5 or later.
* Acknowledgements
Thanks to Uli Kusterer for publishing UKKQueue,
http://www.zathras.de/angelweb/sourcecode.htm#UKKQueue
Learned alot from that but thought it was time for an update.
Later, I found that Uli may have updated his earlier work. Check
out http://www.github.com/uliwitness/UliKit before using this class.
Special thanks to Terry Lambert for the macOS 10.5 workaround.
In *Advanced macOS Programming* by Dalrymple & Hillegass,
Chapter 15 has information on kqueues.
* Todo
64-bit. Seems there are 64-bit kqueue functions. Do I need them?
*/
@interface SSYPathObserver : NSObject {
uint32_t m_kqueueFileDescriptor ;
CFSocketRef runLoopSocket ;
NSMutableSet* m_pathWatches ;
BOOL m_isWatching ;
BOOL m_isAlive ;
}
/*!
@brief Adds a given path, which must currently exist in the filesystem,
to the receiver's watched paths
@details If the given path does not currently exist in the filesystem, this
method will return NO and return error 812002 in error_p.
@param path The path to be watched. May be nil.
@param watchFlags Bitwise OR of one or more SSYPathObserverChangeFlags
constants you wish to receive notifications for. If 0, will
default to the three most popular flags:
* SSYPathObserverChangeFlagsData
* SSYPathObserverChangeFlagsRename
* SSYPathObserverChangeFlagsDelete
@param userInfo An object which will be retained and passed
in the SSYPathObserverChangeNotification in the notification's
userInfo dictionary for key SSYPathObserverUserInfoKey.
May be nil if you have no userInfo to pass.
@param notifyThread Thread on which the SSYPathObserverChangeNotification
notification to be sent when a watched change occurs. If nil, will be
passed on the main thread. You might want [NSThread currentThread]?
@param error_p Pointer which will, upon return, if this method
returns NO and said pointer is not NULL, point to an NSError
describing said error.
@result YES if the given path was added, or is nil.
NO only if the given non-nil path could not be added.
*/
- (BOOL)addPath:(NSString*)path
watchFlags:(uint32_t)watchFlags
notifyThread:(NSThread*)notifyThread
userInfo:(id)userInfo
error_p:(NSError**)error_p ;
/*!
@brief Removes a given path from the receiver's watched paths
@param path The path to be removed
@param error_p Pointer which will, upon return, if this method
returns NO and said pointer is not NULL, point to an NSError
describing said error.
@result YES if the item was removed, does not exist, or if
the given path is nil. NO only if the item exists but could
not be removed.
*/
- (BOOL)removePath:(NSString*)path
error_p:(NSError**)error_p ;
/*!
@brief Setting this to NO will disable all of the receiver's path watches
until it is set back to YES.
@details The default value is YES.
*/
@property (assign) BOOL isWatching;
/*!
@brief Returns a set of strings, each one representing a path that is
currently being watched
@details If .isWatching is NO, or if no paths are currently being watched,
returns an empty set.
*/
- (NSSet*)currentlyWatchedPaths;
@end
/*************************************************************************/
/****************** TEST CODE FOR SSYPathObserver ************************/
/*************************************************************************/
#if 0
#import <Cocoa/Cocoa.h>
#import "SSYPathObserver.h"
/*
Note that this demo runs in three threads:
* Main thread, in which main() sits and listens for notifications
of filesystem changes from SSYPathObserver.
* Demo Stimulator thread, detached from main thread to make changes to
files. This thread is a demo artifact.
* Watcher thread, detached in -[SSYPathObserver init]. This thread camps on
the kqueue system's kevent() function. That is to say: One of these threads
is created internally by each SSYPathObserver instance.
*/
/*
If you would like to watch files appear and disappear during this
demo, change the following from 0 to 1000000 or so
*/
#define SLEEP_MICROSECONDS 100000
NSString* const SSYPathObserverDemoIsDoneNotification = @"DaDemoDone" ;
@interface Notifee : NSObject {
BOOL m_demoIsDone ;
}
@property BOOL demoIsDone ;
@end
@implementation Notifee
@synthesize demoIsDone = m_demoIsDone ;
- (void)getNote:(NSNotification*)note {
NSString* msg = [NSString stringWithFormat:
@"Received %@",
[note name]] ;
if ([[note name] isEqualToString:SSYPathObserverChangeNotification]) {
msg = [msg stringByAppendingFormat:
@"\n Path: %@\n"
" Change Flags: %p\n"
" UserInfo: %@\n",
[[note userInfo] objectForKey:SSYPathObserverPathKey],
[[[note userInfo] objectForKey:SSYPathObserverChangeFlagsKey] integerValue],
[[note userInfo] objectForKey:SSYPathObserverUserInfoKey]] ;
}
else if ([[note name] isEqualToString:NSThreadWillExitNotification]) {
msg = [msg stringByAppendingFormat:
@" for thread named %@\n",
[note object]] ;
}
else if ([[note name] isEqualToString:SSYPathObserverDemoIsDoneNotification]) {
[self setDemoIsDone:YES] ;
}
NSLog(@"%@", msg) ;
}
@end
@interface DemoFileStimulator : NSObject
+ (void)demoStimulateWithPathObserver:(SSYPathObserver*)pathObserver ;
@end
@implementation DemoFileStimulator
+ (void)demoStimulateWithPathObserver:(SSYPathObserver*)pathObserver {
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init] ;
BOOL ok ;
NSError* error = nil ;
NSInteger i ;
NSData* data ;
NSLog(@"Watch the files being created, renamed and removed on your Desktop") ;
// Create paths
NSString* desktop = [NSHomeDirectory() stringByAppendingPathComponent:@"Desktop"] ;
#define N_PATHS 3
NSString* paths[N_PATHS] ;
paths[0] = [desktop stringByAppendingPathComponent:@"000Junk1.txt"] ;
paths[1] = [desktop stringByAppendingPathComponent:@"000Junk2.txt"] ;
paths[2] = [desktop stringByAppendingPathComponent:@"000Junk3.txt"] ;
NSString* appendage = @"a" ;
// Remove any files due to previous crashes of this test, and then
// try to add the nonexistent paths to our path watcher
i = 0 ;
for (i=0; i<N_PATHS; i++) {
NSString* path = paths[i] ;
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
[[NSFileManager defaultManager] removeItemAtPath:path
error:NULL] ;
}
NSString* laterPath = [path stringByAppendingString:appendage] ;
if ([[NSFileManager defaultManager] fileExistsAtPath:laterPath]) {
[[NSFileManager defaultManager] removeItemAtPath:laterPath
error:NULL] ;
}
error = nil ;
ok = [pathObserver addPath:path
watchFlags:0 // Default to watch for all types of changes
notifyThread:nil
userInfo:@"First round"
error_p:&error] ;
NSLog(@"Attempted to set watch for nonexisting path: %@\n(This should have made an error)\nok=%ld, error:\n%@", path, (long)ok, [error description]) ;
}
NSLog(@"Creating files...") ;
data = [@"hello1" dataUsingEncoding:NSUTF8StringEncoding] ;
for (i=0; i<N_PATHS; i++) {
NSString* path = paths[i] ;
[data writeToFile:path
atomically:NO] ;
}
NSLog(@"Adding the now-existing files to our path watcher. This time it should work.") ;
i = 0 ;
for (i=0; i<N_PATHS; i++) {
NSString* path = paths[i] ;
ok = [pathObserver addPath:path
watchFlags:0 // Default to watch for all types of changes
notifyThread:nil
userInfo:@"Second round"
error_p:&error] ;
error = nil ;
NSLog(@" Setting kqueue for existing path: %@\nok=%ld, error:\n%@", path, (long)ok, error) ;
}
NSLog(@"Modifying files") ;
data = [@"hello2" dataUsingEncoding:NSUTF8StringEncoding] ;
for (i=0; i<N_PATHS; i++) {
NSString* path = paths[i] ;
[data writeToFile:path
atomically:NO] ;
usleep(SLEEP_MICROSECONDS) ;
}
NSLog(@"Suspending path watcher. Should be no notifications.") ;
[pathObserver setIsWatching:NO] ;
NSLog(@"Modifying files again.") ;
data = [@"hello3" dataUsingEncoding:NSUTF8StringEncoding] ;
for (i=0; i<N_PATHS; i++) {
NSString* path = paths[i] ;
[data writeToFile:path
atomically:NO] ;
usleep(SLEEP_MICROSECONDS) ;
}
NSLog(@"Unsuspending path watcher. Should get notifications again.") ;
[pathObserver setIsWatching:YES] ;
NSLog(@"Renaming files, appending '%@'", appendage) ;
for (i=0; i<N_PATHS; i++) {
NSString* oldPath = paths[i] ;
NSString* newPath = [oldPath stringByAppendingString:appendage] ;
[[NSFileManager defaultManager] moveItemAtPath:oldPath
toPath:newPath
error:NULL] ;
usleep(SLEEP_MICROSECONDS) ;
}
NSLog(@"Removing files") ;
for (i=0; i<N_PATHS; i++) {
NSString* path = paths[i] ;
path = [path stringByAppendingString:appendage] ;
[[NSFileManager defaultManager] removeItemAtPath:path
error:NULL] ;
usleep(SLEEP_MICROSECONDS) ;
}
NSLog(@"Demo Stimulation is done.") ;
NSNotification* note = [NSNotification notificationWithName:SSYPathObserverDemoIsDoneNotification
object:nil
userInfo:nil] ;
[[NSNotificationCenter defaultCenter] performSelectorOnMainThread:@selector(postNotification:)
withObject:note
waitUntilDone:NO] ;
[pool release] ;
}
@end
int main(int argc, char *argv[]) {
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init] ;
// Create a pathObserver. (We'll add paths etc. in the other thread)
SSYPathObserver* pathObserver = [[SSYPathObserver alloc] init] ;
// Create a notifee to receive notifications
Notifee* notifee = [[Notifee alloc] init] ;
// Create a run loop and tell our Notifee class to observe notifications
// and also thread exit notification
[[NSNotificationCenter defaultCenter] addObserver:notifee
selector:@selector(getNote:)
name:SSYPathObserverChangeNotification
object:pathObserver] ;
[[NSNotificationCenter defaultCenter] addObserver:notifee
selector:@selector(getNote:)
name:SSYPathObserverDemoIsDoneNotification
object:nil] ;
[[NSNotificationCenter defaultCenter] addObserver:notifee
selector:@selector(getNote:)
name:NSThreadWillExitNotification
object:nil] ;
// The demo stimulator is run on a secondary thread so that the
// main thread can enter a run loop and listen for
// notifications.
[NSThread detachNewThreadSelector:@selector(demoStimulateWithPathObserver:)
toTarget:[DemoFileStimulator class]
withObject:pathObserver] ;
// Enter a run loop to listen for notifications until the demo
// program is done
while (![notifee demoIsDone] && [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]]) {
}
[pathObserver release] ;
// For purposes of this demo, we insert a couple sleeps here. The reasons are:
// 1. As explained in comments for -[SSYPathObserver dealloc],
// -[SSYPathObserver dealloc] is invoked by the watcher thread.
// 2. To make sure that the NSThreadWillExitNotification is
// received.
// 3. To make sure there is no crash or exception triggered by
// the deallocation or thread exitting
usleep(500000) ;
[[NSNotificationCenter defaultCenter] removeObserver:notifee] ;
[notifee release] ;
[pool release] ;
usleep(500000) ;
return 0 ;
}
#endif