diff options
author | 2009-02-26 01:34:12 +0000 | |
---|---|---|
committer | 2009-02-26 01:34:12 +0000 | |
commit | 93796b6367645ea20b01649c4e397a95370ed727 (patch) | |
tree | a58e2947a8b326ed9d3a8a229ab9519999347d1d /Foundation | |
parent | 74ad2857a75567b273951be9cbe998133fbca26a (diff) |
adding DO helpers
Diffstat (limited to 'Foundation')
-rw-r--r-- | Foundation/GTMAbstractDOListener.h | 222 | ||||
-rw-r--r-- | Foundation/GTMAbstractDOListener.m | 438 | ||||
-rw-r--r-- | Foundation/GTMAbstractDOListenerTest.m | 61 | ||||
-rw-r--r-- | Foundation/GTMTransientRootProxy.h | 113 | ||||
-rw-r--r-- | Foundation/GTMTransientRootProxy.m | 222 | ||||
-rw-r--r-- | Foundation/GTMTransientRootProxyTest.m | 215 | ||||
-rw-r--r-- | Foundation/GTMTransientRootSocketProxy.h | 42 | ||||
-rw-r--r-- | Foundation/GTMTransientRootSocketProxy.m | 75 | ||||
-rw-r--r-- | Foundation/GTMTransientRootSocketProxyTest.m | 194 |
9 files changed, 1582 insertions, 0 deletions
diff --git a/Foundation/GTMAbstractDOListener.h b/Foundation/GTMAbstractDOListener.h new file mode 100644 index 0000000..e865727 --- /dev/null +++ b/Foundation/GTMAbstractDOListener.h @@ -0,0 +1,222 @@ +// +// GTMAbstractDOListener.h +// +// 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 <Foundation/Foundation.h> + +#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 +@class GTMReceivePortDelegate; +#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 + +// Abstract base class for DO "listeners". +// A class that needs to vend itself over DO should subclass this abstract +// class. This class takes care of certain things like creating a new thread +// to handle requests, setting request/reply timeouts, and ensuring the vended +// object only gets requests that comply with the specified protocol. +// +// Subclassers will want to use the +// GTM_ABSTRACTDOLISTENER_SUBCLASS_THREADMAIN_IMPL macro for easier debugging +// of stack traces. Please read it's description below. +// +@interface GTMAbstractDOListener : NSObject { + @protected + NSString *registeredName_; + __weak Protocol *protocol_; + NSConnection *connection_; + BOOL isRunningInNewThread_; + BOOL shouldShutdown_; + NSTimeInterval requestTimeout_; + NSTimeInterval replyTimeout_; + NSPort *port_; + +#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 + GTMReceivePortDelegate *receivePortDelegate_; // Strong (only used on Tiger) +#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 +} + +// Returns a set of all live instances of GTMAbstractDOListener subclasses. +// If no listeners have been created, this will return an empty array--not nil. +// +// TODO: Remove this method +// ++ (NSArray *)allListeners; + +// Initializer. This actually calls +// initWithRegisteredName:protocol:port with [NSMachPort port] as the port. +// +// Args: +// name - the name that the object will register under +// proto - the protocol that this object (self) should conform to +// +- (id)initWithRegisteredName:(NSString *)name protocol:(Protocol *)proto; + +// The designated initializer. +// +// Args: +// name - the name used to register the port. While not necessarily required +// for an NSSocketPort this class still requires it. +// proto - the protocol that this object (self) should conform to +// port - the port to be used when creating the NSConnection. If a NSMachPort +// is being used then initWithRegisteredName:protocol is recommended. +// Otherwise the port must be allocted by the caller. +// +- (id)initWithRegisteredName:(NSString *)name + protocol:(Protocol *)proto + port:(NSPort *)port; + +// Returns the name that this server will register with the +// mach port name sever. This is the name of the port that this class +// will "listen" on when -runInNewThread is called. +// +// Returns: +// The registered name as a string +// +- (NSString *)registeredName; + +// Sets the registered name to use when listening over DO. This only makes +// sense to be called before -runInNewThread has been called, because +// -runInNewThread will listen on this "registered name", so setting it +// afterwards would do nothing. +// +// Args: +// name - the name to register under. May not be nil. +// +- (void)setRegisteredName:(NSString *)name; + +// Get/set the request timeout interval. If set to a value less than 0, +// the default DO connection timeout will be used (maximum possible value). +// +- (NSTimeInterval)requestTimeout; +- (void)setRequestTimeout:(NSTimeInterval)timeout; + +// Get/set the reply timeout interval. If set to a value less than 0, +// the default DO connection timeout will be used (maximum possible value). +// +- (NSTimeInterval)replyTimeout; +- (void)setReplyTimeout:(NSTimeInterval)timeout; + +// Returns the listeners associated NSConnection. May be nil if no connection +// has been setup yet. +// +- (NSConnection *)connection; + +// Starts the DO system listening using the current thread and current runloop. +// It only makes sense to call this method -OR- -runInNewThread, but not both. +// Returns YES if it was able to startup the DO listener, NO otherwise. +// +- (BOOL)runInCurrentThread; + +// Starts the DO system listening, and creates a new thread to handle the DO +// connections. It only makes sense to call this method -OR- +// -runInCurrentThread, but not both. +// if |errObject| is non nil, it will be used along with |selector| and +// |argument| to signal that the startup of the listener in the new thread +// failed. The actual selector will be invoked back on the main thread so +// it does not have to be thread safe. +// The most basic way to call this method is as follows: +// [listener runInNewThreadWithErrorTarget:nil +// selector:NULL +// withObjectArgument:nil]; +// +// Note: Using the example above you will not know if the listener failed to +// startup due to some error. +// +- (void)runInNewThreadWithErrorTarget:(id)errObject + selector:(SEL)selector + withObjectArgument:(id)argument; + +// Shuts down the connection. If it was running in a new thread, that thread +// should exit (within about 10 seconds). This call does not block. +// +// NOTE: This method is called in -dealloc, so if -runInNewThread had previously +// been called, -dealloc will return *before* the thread actually exits. This +// can be a problem as "self" may be gone before the thread exits. This is a +// bug and needs to be fixed. Currently, to be safe, only call -shutdown if +// -runInCurrentThread had previously been called. +// +- (void)shutdown; + +@end + + +// Methods that subclasses may implement to vary the behavior of this abstract +// class. +// +@interface GTMAbstractDOListener (GTMAbstractDOListenerSubclassMethods) + +// Called by the -runIn* methods. In the case where a new thread is being used, +// this method is called on the new thread. The default implementation of this +// method just returns YES, but subclasses can override it to do subclass +// specific initialization. If this method returns NO, the -runIn* method that +// called it will fail with an error. +// +// Returns: +// YES if the -runIn* method should continue successfully, NO if the it should +// fail. +// +- (BOOL)doRunInitialization; + +// Called as the "main" for the thread spun off by GTMAbstractDOListener. +// Not really for use by subclassers, except to use the +// GTMABSTRACTDOLISTENER_SUBCLASS_THREADMAIN_IMPL macro defined below. +// +// This method runs forever in a new thread. This method actually starts the +// DO connection listening. +// +- (void)threadMain:(NSInvocation *)failureCallback; + +@end + +// GTMAbstractDOListeners used to be hard to debug because crashes in their +// stacks looked like this: +// +// #0 0x90009cd7 in mach_msg_trap () +// #1 0x90009c38 in mach_msg () +// #2 0x9082d2b3 in CFRunLoopRunSpecific () +// #3 0x9082cace in CFRunLoopRunInMode () +// #4 0x9282ad3a in -[NSRunLoop runMode:beforeDate:] () +// #5 0x928788e4 in -[NSRunLoop runUntilDate:] () +// #6 0x00052696 in -[GTMAbstractDOListener(GTMAbstractDOListenerSubclassMethods) threadMain:] ... +// #7 0x927f52e0 in forkThreadForFunction () +// #8 0x90024227 in _pthread_body () +// +// and there was no good way to figure out what thread had the problem because +// they all originated from +// -[GTMAbstractDOListener(GTMAbstractDOListenerSubclassMethods) threadMain:] +// +// If you add GTMABSTRACTDOLISTENER_SUBCLASS_THREADMAIN_IMPL to the impl of your +// subclass you will get a stack that looks like this: +// #0 0x90009cd7 in mach_msg_trap () +// #1 0x90009c38 in mach_msg () +// #2 0x9082d2b3 in CFRunLoopRunSpecific () +// #3 0x9082cace in CFRunLoopRunInMode () +// #4 0x9282ad3a in -[NSRunLoop runMode:beforeDate:] () +// #5 0x928788e4 in -[NSRunLoop runUntilDate:] () +// #6 0x00052696 in -[GTMAbstractDOListener(GTMAbstractDOListenerSubclassMethods) threadMain:] ... +// #7 0x0004b35c in -[GDStatsListener threadMain:] +// #8 0x927f52e0 in forkThreadForFunction () #9 0x90024227 in _pthread_body () +// +// so we can see that this was the GDStatsListener thread that failed. +// It will look something like this +// @implemetation MySubclassOfGTMAbstractDOListenerSubclassMethods +// GTM_ABSTRACTDOLISTENER_SUBCLASS_THREADMAIN_IMPL +// .... +// @end + +#define GTM_ABSTRACTDOLISTENER_SUBCLASS_THREADMAIN_IMPL \ + - (void)threadMain:(NSInvocation *)failureCallback { \ + [super threadMain:failureCallback]; \ + } diff --git a/Foundation/GTMAbstractDOListener.m b/Foundation/GTMAbstractDOListener.m new file mode 100644 index 0000000..772960b --- /dev/null +++ b/Foundation/GTMAbstractDOListener.m @@ -0,0 +1,438 @@ +// +// 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 { + __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; + + _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; +} + +- (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]; + + 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]; + } + + [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:10]; + [[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 diff --git a/Foundation/GTMAbstractDOListenerTest.m b/Foundation/GTMAbstractDOListenerTest.m new file mode 100644 index 0000000..6076724 --- /dev/null +++ b/Foundation/GTMAbstractDOListenerTest.m @@ -0,0 +1,61 @@ +// +// GTMAbstractDOListenerTest.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 "GTMSenTestCase.h" +#import "GTMAbstractDOListener.h" + +// Needed for GTMIsGarbageCollectionEnabled +#import "GTMGarbageCollection.h" + +@interface GTMAbstractDOListenerTest : GTMTestCase +@end + +// TODO: we need to add more tests for this class. Examples: send messages and +// send messages that are in the protocol. + +@implementation GTMAbstractDOListenerTest + +- (void)testAbstractDOListenerRelease { + NSUInteger listenerCount = [[GTMAbstractDOListener allListeners] count]; + GTMAbstractDOListener *listener = + [[GTMAbstractDOListener alloc] initWithRegisteredName:@"FOO" + protocol:@protocol(NSObject) + port:[NSPort port]]; + STAssertNotNil(listener, nil); + + // We throw an autorelease pool here because allStores does a couple of + // autoreleased retains on us which would screws up our retain count + // numbers. + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + STAssertEquals([[GTMAbstractDOListener allListeners] count], + listenerCount + 1, nil); + [pool drain]; + + if (!GTMIsGarbageCollectionEnabled()) { + // Not much point with GC on. + STAssertEquals([listener retainCount], (NSUInteger)1, nil); + } + + [listener release]; + if (!GTMIsGarbageCollectionEnabled()) { + STAssertEquals([[GTMAbstractDOListener allListeners] count], listenerCount, + nil); + } +} + +@end diff --git a/Foundation/GTMTransientRootProxy.h b/Foundation/GTMTransientRootProxy.h new file mode 100644 index 0000000..b5aa78a --- /dev/null +++ b/Foundation/GTMTransientRootProxy.h @@ -0,0 +1,113 @@ +// +// GTMTransientRootProxy.h +// +// 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 <Foundation/Foundation.h> + +// Handle (re-)connecting to a transient root proxy object via DO. +// +// This class is designed to handle connecting and reconnecting to a Distributed +// Objects root proxy (NSDistantObject* instance). It is a replacement for using +// the NSDistantObject returned from NSConnection, directly. When the DO +// connection is up, messages sent to this class are forwarded to the real +// object (the NSDistantObject); when the DO connection is down, messages sent +// to this class are silently swallowed. You can use the -isConnected method on +// this class to see if the DO connection is up or down. +// +// This class may be useful when you need a DO connection, but the +// server you're connected to may be going up and down. For example, the +// web browser plugins in Google Desktop may need to connect to the Google +// Desktop local webserver, but we'd want the browser plugins to be able to +// gracefully handle the local Google Desktop webserver starting and stopping. +// +// === Example Usage === +// +// Old code: +// +// NSDistantObject<MyProto> *o = +// [NSConnection rootProxyForConnectionWithRegisteredName:@"server" +// host:nil]; +// [o setProtocolForProxy:@protocol(MyProto)]; +// [o someMethodInMyProto]; +// // ... write a bunch of code to handle error conditions +// +// New code: +// +// GTMTransientRootProxy<MyProto> *o = +// [GTMTransientRootProxy rootProxyWithRegisteredName:@"server" +// host:nil +// protocol:@protocol(MyProto) +// requestTimeout:5.0 +// replyTimeout:5.0]; +// [o someMethodInMyProto]; +// +// The 'Old code' requires you to handle all the error conditions that may +// arise when using DO (such as the server crashing, or network going down), +// handle properly tearing down the broken connection, and trying to reconnect +// when the server finally comes back online. The 'New code' handles all of +// those details for you. +// +// Also, when creating a GMTransientRootProxy object, you must tell it the +// @protocol that will be used for communication - this is not optional. And +// in order to quiet compiler warnings, you'll also want to staticly type +// the pointer with the protocol as well. +// +@interface GTMTransientRootProxy : NSProxy { + @protected + __weak Protocol *protocol_; + NSDistantObject *realProxy_; + + NSString *registeredName_; + NSString *host_; + + NSTimeInterval requestTimeout_; + NSTimeInterval replyTimeout_; +} + +// Returns an autoreleased instance ++ (id)rootProxyWithRegisteredName:(NSString *)name + host:(NSString *)host + protocol:(Protocol *)protocol + requestTimeout:(NSTimeInterval)requestTimeout + replyTimeout:(NSTimeInterval)replyTimeout; + +// This function will return a GTMTransientRootProxy that is using Mach ports +// for the connection. The |name| and |host| arguments will be used to lookup +// the correct information to create the Mach port connection. +// +- (id)initWithRegisteredName:(NSString *)name + host:(NSString *)host + protocol:(Protocol *)protocol + requestTimeout:(NSTimeInterval)requestTimeout + replyTimeout:(NSTimeInterval)replyTimeout; + +// Returns YES if the DO connection is up and working, NO otherwise. +// +- (BOOL)isConnected; + +@end + +// Subclass of GTMTransientRootProxy that catches and ignores ALL exceptions. +// This class overrides GTMTransientRootProxy's -forwardInvocation: +// method, and wraps it in a try/catch block, and ignores all exceptions. +// +@interface GTMRootProxyCatchAll : GTMTransientRootProxy + +// Overridden, and ignores all thrown exceptions. +- (void)forwardInvocation:(NSInvocation *)invocation; + +@end diff --git a/Foundation/GTMTransientRootProxy.m b/Foundation/GTMTransientRootProxy.m new file mode 100644 index 0000000..4da4eec --- /dev/null +++ b/Foundation/GTMTransientRootProxy.m @@ -0,0 +1,222 @@ +// +// GTMTransientRootProxy.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 "GTMTransientRootProxy.h" +#import "GTMObjC2Runtime.h" + +// Private methods on NSMethodSignature that we need to call. This method has +// been available since 10.0, but Apple didn't add it to the headers until 10.5 +#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 +@interface NSMethodSignature (UndeclaredMethods) ++ (NSMethodSignature *)signatureWithObjCTypes:(const char *)fp8; +@end +#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 + +@interface GTMTransientRootProxy (PrivateMethods) +// Returns an NSConnection for NSMacPorts. This method is broken out to allow +// subclasses to override it to generate different types of NSConnections. +- (NSConnection *)makeConnection; + +// Returns the "real" proxy (stored in the realProxy_ ivar) associated with this +// instance. If realProxy_ is nil, then an attempt is made to make a connection +// to create the realProxy_. +// +- (NSDistantObject *)realProxy; + +// "Releases" the realProxy_ ivar, and removes |self| as an observer from +// the NSNotificationCenter. +// +- (void)releaseRealProxy; +@end + +@implementation GTMTransientRootProxy + ++ (id)rootProxyWithRegisteredName:(NSString *)name + host:(NSString *)host + protocol:(Protocol *)protocol + requestTimeout:(NSTimeInterval)requestTimeout + replyTimeout:(NSTimeInterval)replyTimeout { + return [[[self alloc] initWithRegisteredName:name + host:host + protocol:protocol + requestTimeout:requestTimeout + replyTimeout:replyTimeout] autorelease]; +} + +- (id)initWithRegisteredName:(NSString *)name + host:(NSString *)host + protocol:(Protocol *)protocol + requestTimeout:(NSTimeInterval)requestTimeout + replyTimeout:(NSTimeInterval)replyTimeout { + if (!name || !protocol) { + [self release]; + return nil; + } + + requestTimeout_ = requestTimeout; + replyTimeout_ = replyTimeout; + + registeredName_ = [name copy]; + host_ = [host copy]; + + protocol_ = protocol; // Protocols can't be retained + + return self; +} + +- (id)init { + return [self initWithRegisteredName:nil + host:nil + protocol:nil + requestTimeout:0.0 + replyTimeout:0.0]; +} + +- (void)dealloc { + [self releaseRealProxy]; + [super dealloc]; +} + +- (BOOL)isConnected { + BOOL result = NO; + @synchronized (self) { + result = [[[self realProxy] connectionForProxy] isValid]; + } + return result; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { + struct objc_method_description mdesc; + mdesc = protocol_getMethodDescription(protocol_, selector, YES, YES); + NSMethodSignature *returnValue = nil; + if (mdesc.types == NULL) { + _GTMDevLog(@"Unable to get the protocol method description. Returning " + @"nil."); + } else { + returnValue = [NSMethodSignature signatureWithObjCTypes:mdesc.types]; + } + return returnValue; +} + +- (void)forwardInvocation:(NSInvocation *)invocation { + @try { + NSDistantObject *target = [self realProxy]; + [invocation invokeWithTarget:target]; + + // We need to catch NSException* here rather than "id" because we need to + // treat |ex| as an NSException when using the -name method. Also, we're + // only looking to catch a few types of exception here, all of which are + // NSException types; the rest we just rethrow. + } @catch (NSException *ex) { + NSString *exName = [ex name]; + // If we catch an exception who's name matches any of the following types, + // it's because the DO connection probably went down. So, we'll just + // release our realProxy_, and attempt to reconnect on the next call. + if ([exName isEqualToString:NSPortTimeoutException] + || [exName isEqualToString:NSInvalidSendPortException] + || [exName isEqualToString:NSInvalidReceivePortException] + || [exName isEqualToString:NSFailedAuthenticationException] + || [exName isEqualToString:NSPortSendException] + || [exName isEqualToString:NSPortReceiveException]) { + [self releaseRealProxy]; + } else { + // If the exception was any other type (commonly + // NSInvalidArgumentException) then we'll just re-throw it to the caller. + @throw; + } + } +} + +@end + +@implementation GTMTransientRootProxy (PrivateMethods) + +- (NSConnection *)makeConnection { + return [NSConnection connectionWithRegisteredName:registeredName_ host:host_]; +} + +- (NSDistantObject *)realProxy { + NSDistantObject *returnProxy = nil; + + @synchronized (self) { + // No change so no notification + if (realProxy_) return realProxy_; + + NSConnection *conn = [self makeConnection]; + + @try { + // Try to get the root proxy for this connection's vended object. + realProxy_ = [conn rootProxy]; + } @catch (id ex) { + // We may fail here if we can't get the root proxy in the amount of time + // specified by the timeout above. This may happen, for example, if the + // server process is stopped (via SIGSTOP). We'll just ignore this, and + // try again at the next message. + return nil; + } + if (!realProxy_) { + // Again, no change in connection status + return nil; + } + [realProxy_ retain]; + [realProxy_ setProtocolForProxy:protocol_]; + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc addObserver:self + selector:@selector(connectionDidDie:) + name:NSConnectionDidDieNotification + object:conn]; + // Retain/autorelease so it lives at least the duration of this synchronize + returnProxy = [[realProxy_ retain] autorelease]; + } // @synchronized (self) + + return returnProxy; +} + +- (void)connectionDidDie:(NSNotification *)notification { + [self releaseRealProxy]; +} + +- (void)releaseRealProxy { + BOOL connectionChanged = NO; + @synchronized (self) { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + // Only trigger if we had a proxy before + if (realProxy_) { + connectionChanged = YES; + } + [realProxy_ release]; + realProxy_ = nil; + } +} + +@end + +@implementation GTMRootProxyCatchAll + +- (void)forwardInvocation:(NSInvocation *)invocation { + @try { + [super forwardInvocation:invocation]; + } + @catch (id ex) { + // Log for developers, but basically ignore it. + _GTMDevLog(@"Proxy for invoking %@ has caught and is ignoring exception: %@", + NSStringFromSelector([invocation selector]), ex); + } +} + +@end diff --git a/Foundation/GTMTransientRootProxyTest.m b/Foundation/GTMTransientRootProxyTest.m new file mode 100644 index 0000000..ac18432 --- /dev/null +++ b/Foundation/GTMTransientRootProxyTest.m @@ -0,0 +1,215 @@ +// +// GMTransientRootProxyTest.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 "GTMSenTestCase.h" +#import "GTMTransientRootProxy.h" +#import "GTMUnitTestDevLog.h" + +#define kDefaultTimeout 5.0 +#define kServerShuttingDownNotification @"serverShuttingDown" + +// === Start off declaring some auxillary data structures === +static NSString *const kTestServerName = @"test"; + +// The @protocol that we'll use for testing with. +@protocol DOTestProtocol +- (oneway void)doOneWayVoid; +- (bycopy NSString *)doReturnStringBycopy; +- (void)throwException; +@end + +// The "server" we'll use to test the DO connection. This server will implement +// our test protocol, and it will run in a separate thread from the main +// unit testing thread, so the DO requests can be serviced. +@interface DOTestServer : NSObject <DOTestProtocol> { + @private + BOOL quit_; +} +- (void)runThread:(id)ignore; +- (void)shutdownServer; +@end + +@implementation DOTestServer + +- (BOOL)shouldServerQuit { + BOOL returnValue = NO; + @synchronized(self) { + returnValue = quit_; + } + return returnValue; +} + +- (void)runThread:(id)ignore { + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + quit_ = NO; + + NSConnection *conn = [NSConnection defaultConnection]; + [conn setRootObject:self]; + if (![conn registerName:kTestServerName]) { + _GTMDevLog(@"Failed to register DO root object with name '%@'", + kTestServerName); + // We hit an error, we are shutting down. + quit_ = YES; + } + + while (![self shouldServerQuit]) { + NSDate* runUntil = [NSDate dateWithTimeIntervalSinceNow:0.5]; + [[NSRunLoop currentRunLoop] runUntilDate:runUntil]; + } + + [conn invalidate]; + [conn release]; + [nc postNotificationName:kServerShuttingDownNotification object:nil]; + [pool drain]; +} + +- (oneway void)doOneWayVoid { + // Do nothing +} +- (bycopy NSString *)doReturnStringBycopy { + return @"TestString"; +} + +- (void)shutdownServer { + @synchronized(self) { + quit_ = YES; + } +} + +- (void)throwException { + [NSException raise:@"testingException" format:@"for the unittest"]; +} + +@end + +// === Done with auxillary data structures, now for the main test class === + +@interface GTMTransientRootProxyTest : GTMTestCase { + @private + DOTestServer *server_; + BOOL serverOffline_; +} +@end + +@implementation GTMTransientRootProxyTest + +- (void)serverIsShuttingDown:(NSNotification *)note { + @synchronized(self) { + serverOffline_ = YES; + } +} + +- (BOOL)serverStatus { + BOOL returnValue = NO; + @synchronized(self) { + returnValue = serverOffline_; + } + return returnValue; +} + +- (void)testTransientRootProxy { + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + // Register for server notifications + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc addObserver:self + selector:@selector(serverIsShuttingDown:) + name:kServerShuttingDownNotification + object:nil]; + serverOffline_ = NO; + + // Setup our server. + server_ = [[[DOTestServer alloc] init] autorelease]; + [NSThread detachNewThreadSelector:@selector(runThread:) + toTarget:server_ + withObject:nil]; + // Sleep for 1 second to give the new thread time to set stuff up + [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]]; + + GTMTransientRootProxy<DOTestProtocol> *proxy = + [GTMTransientRootProxy rootProxyWithRegisteredName:kTestServerName + host:nil + protocol:@protocol(DOTestProtocol) + requestTimeout:kDefaultTimeout + replyTimeout:kDefaultTimeout]; + + STAssertEqualObjects([proxy doReturnStringBycopy], + @"TestString", @"proxy should have returned " + @"'TestString'"); + + // Redo the *exact* same test to make sure we can have multiple instances + // in the same app. + proxy = + [GTMTransientRootProxy rootProxyWithRegisteredName:kTestServerName + host:nil + protocol:@protocol(DOTestProtocol) + requestTimeout:kDefaultTimeout + replyTimeout:kDefaultTimeout]; + STAssertEqualObjects([proxy doReturnStringBycopy], + @"TestString", @"proxy should have returned " + @"'TestString'"); + + // Test the GTMRootProxyCatchAll within this test so we don't have to rebuild + // the server again. + + GTMRootProxyCatchAll<DOTestProtocol> *catchProxy = + [GTMRootProxyCatchAll rootProxyWithRegisteredName:kTestServerName + host:nil + protocol:@protocol(DOTestProtocol) + requestTimeout:kDefaultTimeout + replyTimeout:kDefaultTimeout]; + + [GTMUnitTestDevLog expectString:@"Proxy for invoking throwException has " + @"caught and is ignoring exception: [NOTE: this exception originated in " + @"the server.]\nfor the unittest"]; + id e = nil; + @try { + // Has the server throw an exception + [catchProxy throwException]; + } @catch (id ex) { + e = ex; + } + STAssertNil(e, @"The GTMRootProxyCatchAll did not catch the exception: %@.", e); + + proxy = + [GTMTransientRootProxy rootProxyWithRegisteredName:@"FAKE_SERVER" + host:nil + protocol:@protocol(DOTestProtocol) + requestTimeout:kDefaultTimeout + replyTimeout:kDefaultTimeout]; + STAssertNotNil(proxy, @"proxy shouldn't be nil, even when registered w/ a " + @"fake server"); + STAssertFalse([proxy isConnected], @"the proxy shouldn't be connected due to " + @"the fake server"); + + [server_ shutdownServer]; + + // Wait for the server to shutdown so we clean up nicely. + // The max amount of time we will wait until we abort this test. + NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:30.0]; + while (![self serverStatus] && + ([[[NSDate date] laterDate:timeout] isEqualToDate:timeout])) { + NSDate *runUntil = [NSDate dateWithTimeIntervalSinceNow:2.0]; + [[NSRunLoop currentRunLoop] runUntilDate:runUntil]; + } + + [pool drain]; +} + +@end diff --git a/Foundation/GTMTransientRootSocketProxy.h b/Foundation/GTMTransientRootSocketProxy.h new file mode 100644 index 0000000..ddf08fe --- /dev/null +++ b/Foundation/GTMTransientRootSocketProxy.h @@ -0,0 +1,42 @@ +// +// GTMTransientRootSocketProxy.h +// +// 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 <Foundation/Foundation.h> +#import "GTMTransientRootProxy.h" + +@interface GTMTransientRootSocketProxy : GTMTransientRootProxy { + @private + NSSocketPort *port_; +} + +// Returns an autoreleased instance ++ (id)rootProxyWithSocketPort:(NSSocketPort *)port + protocol:(Protocol *)protocol + requestTimeout:(NSTimeInterval)requestTimeout + replyTimeout:(NSTimeInterval)replyTimeout; + +// This function will return a GTMTransientRootProxy that is using NSSocketPorts +// for the connection. The |port| argument must be allocated and configured by +// the caller. +// +- (id)initWithSocketPort:(NSSocketPort *)port + protocol:(Protocol *)protocol + requestTimeout:(NSTimeInterval)requestTimeout + replyTimeout:(NSTimeInterval)replyTimeout; + +@end diff --git a/Foundation/GTMTransientRootSocketProxy.m b/Foundation/GTMTransientRootSocketProxy.m new file mode 100644 index 0000000..a41938e --- /dev/null +++ b/Foundation/GTMTransientRootSocketProxy.m @@ -0,0 +1,75 @@ +// +// GTMTransientRootSocketProxy.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 "GTMTransientRootSocketProxy.h" +#import "GTMObjC2Runtime.h" + +@interface GTMTransientRootSocketProxy (ProtectedMethods) +// Returns an NSConnection for NSSocketPorts. This method overrides the one in +// the GTMTransientRootProxy which allows us to create a connection with a +// NSSocketPort. +// +- (NSConnection *)makeConnection; +@end + + + +@implementation GTMTransientRootSocketProxy + ++ (id)rootProxyWithSocketPort:(NSSocketPort *)port + protocol:(Protocol *)protocol + requestTimeout:(NSTimeInterval)requestTimeout + replyTimeout:(NSTimeInterval)replyTimeout { + return [[[self alloc] initWithSocketPort:port + protocol:protocol + requestTimeout:requestTimeout + replyTimeout:replyTimeout] autorelease]; +} + +- (id)initWithSocketPort:(NSSocketPort *)port + protocol:(Protocol *)protocol + requestTimeout:(NSTimeInterval)requestTimeout + replyTimeout:(NSTimeInterval)replyTimeout { + if (!port || !protocol) { + [self release]; + return nil; + } + + requestTimeout_ = requestTimeout; + replyTimeout_ = replyTimeout; + + port_ = [port retain]; + + protocol_ = protocol; // Protocols can't be retained + return self; +} + +- (void)dealloc { + [port_ release]; + [super dealloc]; +} + +@end + +@implementation GTMTransientRootSocketProxy (ProtectedMethods) + +- (NSConnection *)makeConnection { + return [NSConnection connectionWithReceivePort:nil sendPort:port_]; +} + +@end diff --git a/Foundation/GTMTransientRootSocketProxyTest.m b/Foundation/GTMTransientRootSocketProxyTest.m new file mode 100644 index 0000000..e14c9c5 --- /dev/null +++ b/Foundation/GTMTransientRootSocketProxyTest.m @@ -0,0 +1,194 @@ +// +// GTMTransientRootSocketProxyTest.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 "GTMSenTestCase.h" +#import "GTMTransientRootSocketProxy.h" + +// Needed to get the socket port. +#import <netinet/in.h> +#import <arpa/inet.h> + +#define kDefaultTimeout 5.0 +#define kServerShuttingDownNotification @"serverShuttingDown" + +// === Start off declaring some auxillary data structures === + +// The @protocol that we'll use for testing with. +@protocol DOSocketTestProtocol +- (oneway void)doOneWayVoid; +- (bycopy NSString *)doReturnStringBycopy; +@end + +// The "server" we'll use to test the DO connection. This server will implement +// our test protocol, and it will run in a separate thread from the main +// unit testing thread, so the DO requests can be serviced. +@interface DOSocketTestServer : NSObject <DOSocketTestProtocol> { +@private + BOOL quit_; + unsigned short listeningPort_; +} +- (void)runThread:(id)ignore; +- (unsigned short)listeningPort; +- (void)shutdownServer; +@end + +@implementation DOSocketTestServer + +- (BOOL)shouldServerQuit { + BOOL returnValue = NO; + @synchronized(self) { + returnValue = quit_; + } + return returnValue; +} + +- (void)runThread:(id)ignore { + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + quit_ = NO; + + NSSocketPort *serverPort = [[NSSocketPort alloc] init]; + + // We will need the port so we can hand if off to the client + // The structure will get us this information. + struct sockaddr_in addrIn = + *(struct sockaddr_in *)[[serverPort address] bytes]; + listeningPort_ = htons(addrIn.sin_port); + + NSConnection *conn = [NSConnection connectionWithReceivePort:serverPort + sendPort:nil]; + // Port is retained by the NSConnection + [serverPort release]; + [conn setRootObject:self]; + + while (![self shouldServerQuit]) { + NSDate* runUntil = [NSDate dateWithTimeIntervalSinceNow:0.5]; + [[NSRunLoop currentRunLoop] runUntilDate:runUntil]; + } + + [conn invalidate]; + [conn release]; + [nc postNotificationName:kServerShuttingDownNotification object:nil]; + [pool drain]; +} + +- (unsigned short)listeningPort { + return listeningPort_; +} + +- (oneway void)doOneWayVoid { + // Do nothing +} +- (bycopy NSString *)doReturnStringBycopy { + return @"TestString"; +} + +- (void)shutdownServer { + @synchronized(self) { + quit_ = YES; + } +} + +@end + +// === Done with auxillary data structures, now for the main test class === + +@interface GTMTransientRootSocketProxyTest : GTMTestCase { + DOSocketTestServer *server_; + BOOL serverOffline_; +} + +@end + +@implementation GTMTransientRootSocketProxyTest + +- (void)serverIsShuttingDown:(NSNotification *)note { + @synchronized(self) { + serverOffline_ = YES; + } +} + +- (BOOL)serverStatus { + BOOL returnValue = NO; + @synchronized(self) { + returnValue = serverOffline_; + } + return returnValue; +} + +- (void)testTransientRootSocketProxy { + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + // Register for server notifications + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc addObserver:self + selector:@selector(serverIsShuttingDown:) + name:kServerShuttingDownNotification + object:nil]; + serverOffline_ = NO; + + // Setup our server. + server_ = [[[DOSocketTestServer alloc] init] autorelease]; + [NSThread detachNewThreadSelector:@selector(runThread:) + toTarget:server_ + withObject:nil]; + // Sleep for 1 second to give the new thread time to set stuff up + [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]]; + + // Create our NSSocketPort + NSSocketPort *receivePort = + [[NSSocketPort alloc] initRemoteWithTCPPort:[server_ listeningPort] + host:@"localhost"]; + + GTMTransientRootSocketProxy<DOSocketTestProtocol> *proxy = + [GTMTransientRootSocketProxy rootProxyWithSocketPort:receivePort + protocol:@protocol(DOSocketTestProtocol) + requestTimeout:kDefaultTimeout + replyTimeout:kDefaultTimeout]; + + STAssertEqualObjects([proxy doReturnStringBycopy], + @"TestString", @"proxy should have returned " + @"'TestString'"); + + // Redo the *exact* same test to make sure we can have multiple instances + // in the same app. + proxy = + [GTMTransientRootSocketProxy rootProxyWithSocketPort:receivePort + protocol:@protocol(DOSocketTestProtocol) + requestTimeout:kDefaultTimeout + replyTimeout:kDefaultTimeout]; + + STAssertEqualObjects([proxy doReturnStringBycopy], + @"TestString", @"proxy should have returned " + @"'TestString'"); + + [server_ shutdownServer]; + + // Wait for the server to shutdown so we clean up nicely. The max amount of + // time we will wait until we abort this test. + NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:30.0]; + while (![self serverStatus] && + ([[[NSDate date] laterDate:timeout] isEqualToDate:timeout])) { + NSDate *runUntil = [NSDate dateWithTimeIntervalSinceNow:2.0]; + [[NSRunLoop currentRunLoop] runUntilDate:runUntil]; + } + + [pool drain]; +} + +@end |