aboutsummaryrefslogtreecommitdiff
path: root/Foundation/GTMAbstractDOListener.m
blob: 4ac69bd0717c8774bf87417314de15da4632f7b4 (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
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
//
//  GTMAbstractDOListener.m
//
//  Copyright 2006-2009 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 "GTMAbstractDOListener.h"
#import "GTMSystemVersion.h"
#import <mach/mach_init.h>

// Hack workaround suggested by DTS for the DO deadlock bug.  Basically, this
// class intercepts the delegate role for DO's receive port (which is an
// NSMachPort).  When -handlePortMessage: is called, it verifies that the send
// and receive ports are not nil, then forwards the message on to the original
// delegate.  If the ports are nil, then the resulting NSConnection would
// eventually cause us to deadlock.  In this case, it simply ignores the
// message.  This is only need on Tiger.
#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
@interface GTMReceivePortDelegate : NSObject {
  GTM_WEAK id delegate_;
}
- (id)initWithDelegate:(id)delegate;
- (void)handlePortMessage:(NSPortMessage *)message;
@end
#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4

@interface GTMAbstractDOListener (PrivateMethods)
- (BOOL)startListening;
- (void)stopListening;

// Returns a description of the port based on the type of port.
- (NSString *)portDescription;

#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
// Uses the GTMReceivePortDelegate hack (see comments above) if we're on Tiger.
- (void)hackaroundTigerDOWedgeBug:(NSConnection *)conn;
#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
@end

// Static global set that holds a pointer to all instances of
// GTMAbstractDOListener subclasses.
//
static NSMutableSet *gAllListeners = nil;

@implementation GTMAbstractDOListener

+ (void)initialize {
  if (self == [GTMAbstractDOListener class]) {
    // We create the set using CFSetCreateMutable because we don't
    // want to retain things in this set. If we retained things in the
    // set we would never be able to dealloc ourselves because we
    // add "self" to this set in it's init routine would cause an
    // extra retain to be added to it.
    gAllListeners = (NSMutableSet*)CFSetCreateMutable(NULL, 0, NULL);
  }
}

+ (NSArray *)allListeners {
  // We return an NSArray instead of an NSSet here because NSArrays look nicer
  // when displayed as %@
  NSArray *allListeners = nil;

  @synchronized (gAllListeners) {
    allListeners = [gAllListeners allObjects];
  }
  return allListeners;
}

- (id)init {
  return [self initWithRegisteredName:nil protocol:NULL];
}

- (id)initWithRegisteredName:(NSString *)name protocol:(Protocol *)proto {
  return [self initWithRegisteredName:name
                             protocol:proto
                                 port:[NSMachPort port]];
}

- (id)initWithRegisteredName:(NSString *)name
                    protocol:(Protocol *)proto
                        port:(NSPort *)port {
  self = [super init];
  if (!self) {
    return nil;
  }

  if ((!proto) || (!port) || (!name)) {
    if (!proto) {
      _GTMDevLog(@"Failed to create a listener, a protocol must be specified");
    }

    if (!port) {
      _GTMDevLog(@"Failed to create a listener, a port must be specified");
    }

    if (!name) {
      _GTMDevLog(@"Failed to create a listener, a name must be specified");
    }

    [self release];
    return nil;
  }

  registeredName_ = [name copy];
  protocol_ = proto;  // Can't retain protocols
  port_ = [port retain];

  requestTimeout_ = -1;
  replyTimeout_ = -1;

  heartRate_ = (NSTimeInterval)10.0;

  _GTMDevAssert(gAllListeners, @"gAllListeners is not nil");
  @synchronized (gAllListeners) {
    [gAllListeners addObject:self];
  }

  return self;
}

- (void)dealloc {
  _GTMDevAssert(gAllListeners, @"gAllListeners is not nil");
  @synchronized (gAllListeners) {
    [gAllListeners removeObject:self];
  }

#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
  [receivePortDelegate_ release];
#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
  
  [self shutdown];
  [port_ release];
  [registeredName_ release];
  [super dealloc];
}


#pragma mark Getters and Setters

- (NSString *)registeredName {
  return registeredName_;
}

- (void)setRegisteredName:(NSString *)name {
  if (!name) {
    return;
  }
  [registeredName_ autorelease];
  registeredName_ = [name copy];
}

- (NSTimeInterval)requestTimeout {
  return requestTimeout_;
}

- (void)setRequestTimeout:(NSTimeInterval)timeout {
  requestTimeout_ = timeout;
}

- (NSTimeInterval)replyTimeout {
  return replyTimeout_;
}

- (void)setReplyTimeout:(NSTimeInterval)timeout {
  replyTimeout_ = timeout;
}

- (void)setThreadHeartRate:(NSTimeInterval)heartRate {
  heartRate_ = heartRate;
}

- (NSTimeInterval)ThreadHeartRate {
  return heartRate_;
}

- (NSConnection *)connection {
  return connection_;
}

- (NSString *)description {
  return [NSString stringWithFormat:@"%@<%p> { name=\"%@\", %@ }",
            [self class], self, registeredName_, [self portDescription]];
}

#pragma mark "Run" methods

- (BOOL)runInCurrentThread {
  return [self startListening];
}

- (void)runInNewThreadWithErrorTarget:(id)errObject
                             selector:(SEL)selector
                   withObjectArgument:(id)argument {
  NSInvocation *invocation = nil;
  
  _GTMDevAssert(((errObject != nil && selector != NULL) ||
                 (!errObject && !selector)), @"errObject and selector must "
                @"both be nil or not nil");

  // create an invocation we can use if things fail
  if (errObject) {
    NSMethodSignature *signature =
    [errObject methodSignatureForSelector:selector];
    invocation = [NSInvocation invocationWithMethodSignature:signature];
    [invocation setSelector:selector];
    [invocation setTarget:errObject];

    // If the selector they passed in takes an arg (i.e., it has at least one 
    // colon in the selector name), then set the first user-specified arg to be
    // the |argument| they specified.  The first two args are self and _cmd.
    if ([signature numberOfArguments] > 2) {
      [invocation setArgument:&argument atIndex:2];
    }

    [invocation retainArguments];
  }

  shouldShutdown_ = NO;
  [NSThread detachNewThreadSelector:@selector(threadMain:)
                           toTarget:self
                         withObject:invocation];
}

- (void)shutdown {
  // If we're not running in a new thread (then we're running in the "current"
  // thread), tear down the NSConnection here.  If we are running in a new
  // thread we just set the shouldShutdown_ flag, and the thread will teardown
  // the NSConnection itself.
  if (!isRunningInNewThread_) {
    [self stopListening];
  } else {
    shouldShutdown_ = YES;
  }
}

@end

@implementation GTMAbstractDOListener (PrivateMethods)

- (BOOL)startListening {
  BOOL result = NO;

  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  
  _GTMDevAssert(!connection_, @"Connection_ should not be set. Was this "
                @"listener already started? %@", self);
  connection_ = [[NSConnection alloc] initWithReceivePort:port_ sendPort:nil];

  NSProtocolChecker *checker =
  [NSProtocolChecker protocolCheckerWithTarget:self
                                      protocol:protocol_];

  if (requestTimeout_ >= 0) {
    [connection_ setRequestTimeout:requestTimeout_];
  }

  if (replyTimeout_ >= 0) {
    [connection_ setReplyTimeout:replyTimeout_];
  }

  // Set the connection's root object to be the protocol checker so that only
  // methods listed in the protocol_ are available via DO.
  [connection_ setRootObject:checker];

  // Allow subclasses to be the connection delegate
  [connection_ setDelegate:self];

  // Because of radar 5493309 we need to do this. [NSConnection registeredName:]
  // returns NO when the connection is created using an NSSocketPort under
  // Leopard.
  //
  // The recommendation from Apple was to use the command:
  // [NSConnection registerName:withNameServer:].
  NSPortNameServer *server;
  if ([port_ isKindOfClass:[NSSocketPort class]]) {
    server = [NSSocketPortNameServer sharedInstance];
  } else {
    server = [NSPortNameServer systemDefaultPortNameServer];
  }

  BOOL registered = [connection_ registerName:registeredName_
                               withNameServer:server];

  if (registeredName_ && registered) {
#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
    [self hackaroundTigerDOWedgeBug:connection_];
#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4

    result = YES;

    _GTMDevLog(@"listening on %@ with name '%@'", [self portDescription],
               registeredName_);
  } else {
    _GTMDevLog(@"failed to register %@ with %@", connection_, registeredName_);
  }

  // we're good, so call the overrideable initializer
  if (result) {
    // Call the virtual "runIn*" initializer
    result = [self doRunInitialization];
  } else {
    [connection_ invalidate];
    [connection_ release];
    connection_ = nil;
  }

  [pool drain];

  return result;
}

- (void)stopListening {
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  [connection_ invalidate];
  [connection_ release];
  connection_ = nil;
  [pool drain];
}

- (NSString *)portDescription {
  NSString *portDescription;
  if ([port_ isKindOfClass:[NSMachPort class]]) {
    portDescription = [NSString stringWithFormat:@"mach_port=%#x",
                       [(NSMachPort *)port_ machPort]];
  } else {
    portDescription = [NSString stringWithFormat:@"port=%@",
                       [port_ description]];
  }
  return portDescription;
}

#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
- (void)hackaroundTigerDOWedgeBug:(NSConnection *)conn {
  if ([GTMSystemVersion isTiger]) {
    NSPort *receivePort = [conn receivePort];
    if ([receivePort isKindOfClass:[NSMachPort class]]) {
      id portDelegate = [receivePort delegate];
      receivePortDelegate_ =
        [[GTMReceivePortDelegate alloc] initWithDelegate:portDelegate];
      [receivePort setDelegate:receivePortDelegate_];
    }
  }
}
#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4

@end

@implementation GTMAbstractDOListener (GTMAbstractDOListenerSubclassMethods)

- (BOOL)doRunInitialization {
  return YES;
}

//
// -threadMain:
//

//
- (void)threadMain:(NSInvocation *)failureCallback {
  isRunningInNewThread_ = YES;

  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

  // register
  if ([self startListening]) {
    // spin
    for (;;) {  // Run forever

      // check if we were asked to shutdown
      if (shouldShutdown_) {
        break;
      }

      NSAutoreleasePool *localPool = [[NSAutoreleasePool alloc] init];
      // Wrap our runloop in case we get an exception from DO
      @try {
        NSDate *waitDate = [NSDate dateWithTimeIntervalSinceNow:heartRate_];
        [[NSRunLoop currentRunLoop] runUntilDate:waitDate];
      } @catch (id e) {
        _GTMDevLog(@"Listener '%@' caught exception: %@", registeredName_, e);
      }
      [localPool drain];
    }
  } else {
    // failed, if we had something to invoke, call it on the main thread
    if (failureCallback) {
      [failureCallback performSelectorOnMainThread:@selector(invoke)
                                        withObject:nil
                                     waitUntilDone:NO];
    }
  }

  [self stopListening];
  [pool drain];

  isRunningInNewThread_ = NO;
}

@end

#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
@implementation GTMReceivePortDelegate

- (id)initWithDelegate:(id)delegate {
  if ((self = [super init])) {
    delegate_ = delegate;  // delegates aren't retained
  }
  return self;
}

- (void)handlePortMessage:(NSPortMessage *)message {
  NSPort *receivePort = [message receivePort];
  NSPort *sendPort    = [message sendPort];

  // If we don't have a sensible send or receive port, just act like 
  // the message never arrived.  Otherwise, hand it off to the original 
  // delegate (which is the NSMachPort itself).
  if (receivePort == nil || sendPort == nil || [receivePort isEqual:sendPort]) {
    _GTMDevLog(@"Dropping port message destined for itself to avoid DO wedge.");
  } else {
    // Uncomment for super-duper verbose DO message forward logging
    // _GTMDevLog(@"--> Forwarding message %@ to delegate %@",
    //            message, delegate_);
    [delegate_ handlePortMessage:message];
  }

  // If processing the message caused us to drop no longer being the delegate, 
  // set us back.  Due to interactions between NSConnection and NSMachPort, 
  // it's possible for the NSMachPort's delegate to get set back to its 
  // original value.  If that happens, we set it back to the value we want.
  if ([delegate_ delegate] != self) {
    if ([delegate_ delegate] == delegate_) {
      _GTMDevLog(@"Restoring DO delegate to %@", self);
      [delegate_ setDelegate:self];
    } else {
      _GTMDevLog(@"GMReceivePortDelegate replaced with %@",
                 [delegate_ delegate]);
    }
  }
}
@end
#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4