blob: a1b70948cc1c4abc98fa537da7ce2e2bc7e2ced3 [file] [log] [blame]
//
// GTMFileSystemKQueue.m
//
// Copyright 2008 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy
// of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
//
#import "GTMFileSystemKQueue.h"
#import <unistd.h>
#import "GTMDefines.h"
#import "GTMDebugSelectorValidation.h"
#import "GTMTypeCasting.h"
#pragma clang diagnostic push
// Ignore all of the deprecation warnings for GTMFileSystemKQueue
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
// File descriptor for the kqueue that will hold all of our file system events.
static int gFileSystemKQueueFileDescriptor = 0;
// A wrapper around the kqueue file descriptor so we can put it into a
// runloop.
static CFSocketRef gRunLoopSocket = NULL;
@interface GTMFileSystemKQueue (PrivateMethods)
- (void)notify:(GTMFileSystemKQueueEvents)eventFFlags;
- (void)addFileDescriptorMonitor:(int)fd;
- (int)registerWithKQueue;
- (void)unregisterWithKQueue;
@end
@implementation GTMFileSystemKQueue
-(id)init {
// Folks shouldn't call init directly, so they get what they deserve.
_GTMDevLog(@"Don't call init, use "
@"initWithPath:forEvents:acrossReplace:target:action:");
return [self initWithPath:nil forEvents:0 acrossReplace:NO
target:nil action:nil];
}
- (id)initWithPath:(NSString *)path
forEvents:(GTMFileSystemKQueueEvents)events
acrossReplace:(BOOL)acrossReplace
target:(id)target
action:(SEL)action {
if ((self = [super init])) {
fd_ = -1;
path_ = [path copy];
events_ = events;
acrossReplace_ = acrossReplace;
target_ = target; // Don't retain since target will most likely retain us.
action_ = action;
if ([path_ length] == 0 || !events_ || !target_ || !action_) {
[self release];
return nil;
}
// Make sure it imples what we expect
GTMAssertSelectorNilOrImplementedWithArguments(target_,
action_,
@encode(GTMFileSystemKQueue*),
@encode(GTMFileSystemKQueueEvents),
NULL);
fd_ = [self registerWithKQueue];
if (fd_ < 0) {
[self release];
return nil;
}
}
return self;
}
- (void)dealloc {
[self unregisterWithKQueue];
[path_ release];
[super dealloc];
}
- (NSString *)path {
return path_;
}
#pragma clang diagnostic push
// Ignore all of the deprecation warnings for GTMFileSystemKQueue
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
// Cribbed from Advanced Mac OS X Programming.
static void SocketCallBack(CFSocketRef socketref, CFSocketCallBackType type,
CFDataRef address, const void *data, void *info) {
// We're using CFRunLoop calls here. Even when used on the main thread, they
// don't trigger the draining of the main application's autorelease pool that
// NSRunLoop provides. If we're used in a UI-less app, this means that
// autoreleased objects would never go away, so we provide our own pool here.
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// We want to read as many events as possible so loop on the kevent call
// till the kqueue is empty.
int events = -1;
do {
// We wouldn't be here if CFSocket didn't think there was data on
// |gFileSystemKQueueFileDescriptor|. However, since this callback is tied
// to the runloop, if [... unregisterWithKQueue] was called before a runloop
// spin we may now be looking at an empty queue (remember,
// |gFileSystemKQueueFileDescriptor| isn't a normal descriptor).
// Try to consume one event with an immediate timeout.
struct kevent event;
const struct timespec noWait = { 0, 0 };
events = kevent(gFileSystemKQueueFileDescriptor, NULL, 0, &event, 1, &noWait);
if (events == 1) {
GTMFileSystemKQueue *fskq = GTM_STATIC_CAST(GTMFileSystemKQueue,
event.udata);
[fskq notify:event.fflags];
} else if (events == -1) {
_GTMDevLog(@"could not pick up kqueue event. Errno %d", errno); // COV_NF_LINE
} else {
// |events| is zero, either we've drained the kqueue or CFSocket was
// notified and then the events went away before we had a chance to see
// them.
}
} while (events > 0);
[pool drain];
}
#pragma clang diagnostic pop
// Cribbed from Advanced Mac OS X Programming
- (void)addFileDescriptorMonitor:(int)fd {
_GTMDevAssert(gRunLoopSocket == NULL, @"socket should be NULL at this point");
CFSocketContext context = { 0, NULL, NULL, NULL, NULL };
gRunLoopSocket = CFSocketCreateWithNative(kCFAllocatorDefault,
fd,
kCFSocketReadCallBack,
SocketCallBack,
&context);
if (gRunLoopSocket == NULL) {
_GTMDevLog(@"could not CFSocketCreateWithNative"); // COV_NF_LINE
goto bailout; // COV_NF_LINE
}
CFRunLoopSourceRef rls;
rls = CFSocketCreateRunLoopSource(NULL, gRunLoopSocket, 0);
if (rls == NULL) {
_GTMDevLog(@"could not create a run loop source"); // COV_NF_LINE
goto bailout; // COV_NF_LINE
}
CFRunLoopAddSource(CFRunLoopGetCurrent(), rls,
kCFRunLoopDefaultMode);
CFRelease(rls);
bailout:
return;
}
// Returns the FD we got in registering
- (int)registerWithKQueue {
// Make sure we have our kqueue.
if (gFileSystemKQueueFileDescriptor == 0) {
gFileSystemKQueueFileDescriptor = kqueue();
if (gFileSystemKQueueFileDescriptor == -1) {
// COV_NF_START
_GTMDevLog(@"could not make filesystem kqueue. Errno %d", errno);
return -1;
// COV_NF_END
}
// Add the kqueue file descriptor to the runloop.
[self addFileDescriptorMonitor:gFileSystemKQueueFileDescriptor];
}
int newFD = open([path_ fileSystemRepresentation], O_EVTONLY, 0);
if (newFD >= 0) {
// Add a new event for the file.
struct kevent filter;
EV_SET(&filter, newFD, EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR,
events_, 0, self);
const struct timespec noWait = { 0, 0 };
if (kevent(gFileSystemKQueueFileDescriptor, &filter, 1, NULL, 0, &noWait) == -1) {
// COV_NF_START
_GTMDevLog(@"could not add event for path %@. Errno %d", path_, errno);
close(newFD);
newFD = -1;
// COV_NF_END
}
}
return newFD;
}
- (void)unregisterWithKQueue {
// Short-circuit cases where we didn't actually register a kqueue event.
if (fd_ < 0) return;
struct kevent filter;
EV_SET(&filter, fd_, EVFILT_VNODE, EV_DELETE, 0, 0, self);
const struct timespec noWait = { 0, 0 };
if (kevent(gFileSystemKQueueFileDescriptor, &filter, 1, NULL, 0, &noWait) != 0) {
_GTMDevLog(@"could not remove event for path %@. Errno %d", path_, errno); // COV_NF_LINE
}
// Now close the file down
close(fd_);
fd_ = -1;
}
- (void)notify:(GTMFileSystemKQueueEvents)eventFFlags {
// Some notifications get a little bit of overhead first
if (eventFFlags & NOTE_REVOKE) {
// COV_NF_START - no good way to do this in a unittest
// Assume revoke means unmount and give up
[self unregisterWithKQueue];
// COV_NF_END
}
if (eventFFlags & NOTE_DELETE) {
[self unregisterWithKQueue];
if (acrossReplace_) {
fd_ = [self registerWithKQueue];
}
}
if (eventFFlags & NOTE_RENAME) {
// If we're doing it across replace, we move to the new fd for the new file
// that might have come onto the path. if we aren't doing accross replace,
// nothing to do, just stay on the file.
if (acrossReplace_) {
int newFD = [self registerWithKQueue];
if (newFD >= 0) {
[self unregisterWithKQueue];
fd_ = newFD;
}
}
}
// Now, fire the selector
NSMethodSignature *methodSig = [target_ methodSignatureForSelector:action_];
_GTMDevAssert(methodSig != nil, @"failed to get the signature?");
NSInvocation *invocation
= [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setTarget:target_];
[invocation setSelector:action_];
[invocation setArgument:&self atIndex:2];
[invocation setArgument:&eventFFlags atIndex:3];
[invocation invoke];
}
@end
#pragma clang diagnostic pop