Skip to content

Instantly share code, notes, and snippets.

@nielsbot
Last active April 2, 2022 08:07
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save nielsbot/5155671 to your computer and use it in GitHub Desktop.
Save nielsbot/5155671 to your computer and use it in GitHub Desktop.
A wrapper around kqueue to monitor changes to files. Works on iOS.
#import "FileChangeObserver.h"
#undef Assert
#define Assert(COND) { if (!(COND)) { raise( SIGINT ) ; } }
@interface FileChangeObserver ()
@property ( nonatomic, readonly ) int kqueue ;
@property ( nonatomic ) enum FileChangeNotificationType typeMask ;
@end
@implementation FileChangeObserver
@synthesize kqueue = _kqueue ;
+(instancetype)observerForURL:(NSURL*)url types:(enum FileChangeNotificationType)types delegate:(id<FileChangeObserverDelegate>)delegate
{
if ( !url ) { return nil ; }
FileChangeObserver * result = [ [ [ self class ] alloc ] init ] ;
result.url = url ;
result.delegate = delegate ;
result.typeMask = types ;
[ result startObserving ] ;
return result ;
}
-(void)dealloc
{
printf("%s\n", __PRETTY_FUNCTION__);
[ self stopObserving ] ;
}
//
// kqueue_main
//
static void (^kqueue_main)(FileChangeObserver *) = ^(__unsafe_unretained FileChangeObserver * self) {
int fd = open( [ [ self.url path ] fileSystemRepresentation ], O_EVTONLY ) ;
Assert( fd >= 0 );
int q = self.kqueue ;
{
struct kevent event = {
.ident = fd,
.filter = EVFILT_VNODE,
.flags = EV_ADD | EV_CLEAR,
.fflags = self.typeMask,
} ;
int error = kevent( q, &event, 1, NULL, 0, NULL);
Assert(error == 0);
}
struct kevent event = {0} ;
for(;;)
{
int nEvents = kevent( q, NULL, 0, &event, 1, NULL ) ;
if ( nEvents != 1 ) { break ; }
[[ NSThread mainThread ] perform:^{
[ self.delegate fileChanged:self typeMask:(enum FileChangeNotificationType)event.fflags ] ;
}];
}
} ;
-(void)stopObserving
{
@synchronized( self )
{
close( self.kqueue ) ;
}
}
-(void)invalidate
{
[ self stopObserving ] ;
}
-(void)startObserving
{
@synchronized( self )
{
printf("%s\n", __PRETTY_FUNCTION__);
static dispatch_queue_t __q ;
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
__q = dispatch_queue_create( "file observer queue", DISPATCH_QUEUE_CONCURRENT ) ;
});
}
// this is __unsafe_unretained to avoid retaining 'self'.
// using __weak isn't a strong enough to prevent ARC from retaining 'self'.
// This allows the observer to be torn down simply be releasing it,
// instead of requiring the client to invoke -invalidate.
__unsafe_unretained id unsafe_self = self ;
dispatch_async( __q, ^{
kqueue_main( unsafe_self ) ;
}) ;
}
}
-(int)kqueue
{
if ( !_kqueue ) { _kqueue = kqueue() ; }
return _kqueue ;
}
@end
#import <Foundation/Foundation.h>
enum FileChangeNotificationType
{
kFileChangeType_Delete = NOTE_DELETE
, kFileChangeType_Write = NOTE_WRITE
, kFileChangeType_DirectoryContentsChanged = kFileChangeType_Write
//#define NOTE_WRITE 0x00000002 /* data contents changed */
//#define NOTE_EXTEND 0x00000004 /* size increased */
//#define NOTE_ATTRIB 0x00000008 /* attributes changed */
//#define NOTE_LINK 0x00000010 /* link count changed */
//#define NOTE_RENAME 0x00000020 /* vnode was renamed */
//#define NOTE_REVOKE 0x00000040 /* vnode access was revoked */
//#define NOTE_NONE 0x00000080 /* No specific vnode event: to test for EVFILT_READ activation*/
} ;
@class FileChangeObserver ;
@protocol FileChangeObserverDelegate<NSObject>
@required
-(void)fileChanged:(FileChangeObserver*)observer typeMask:(enum FileChangeNotificationType)type ;
@end
@interface FileChangeObserver : NSObject
@property ( nonatomic, copy ) NSURL * url ;
@property ( nonatomic, weak ) id<FileChangeObserverDelegate> delegate ;
+(instancetype)observerForURL:(NSURL*)url types:(enum FileChangeNotificationType)types delegate:(id<FileChangeObserverDelegate>)delegate ;
-(void)invalidate ;
@end
// category for NSString to print kqueue event flags
@implementation NSString (FileChangeObserver)
+(NSString*)stringWithKEventFFlags:(int)flags
{
NSMutableArray * array = [ NSMutableArray array ] ;
struct
{
__unsafe_unretained NSString * name;
} bitNames[] = {
{ @"NOTE_DELETE" }
, { @"NOTE_WRITE" }
, { @"NOTE_EXTEND" }
, { @"NOTE_ATTRIB" }
, { @"NOTE_LINK" }
, { @"NOTE_RENAME" }
, { @"NOTE_REVOKE" }
, { @"NOTE_NONE" }
} ;
for( int index = 0, count = COUNTOF( bitNames ); index < count; ++index )
{
if ( ( flags & ( 1 << index )) != 0 )
{
[ array addObject:bitNames[ index ].name ] ;
}
}
NSString * result = [ array componentsJoinedByString:@" " ] ;
return result ;
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment