aboutsummaryrefslogtreecommitdiff
path: root/Foundation/GTMFileSystemKQueue.m
blob: a1b70948cc1c4abc98fa537da7ce2e2bc7e2ced3 (plain)
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
//
//  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