aboutsummaryrefslogtreecommitdiff
path: root/Foundation
diff options
context:
space:
mode:
authorGravatar thomasvl@gmail.com <thomasvl@gmail.com@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2009-02-26 01:34:12 +0000
committerGravatar thomasvl@gmail.com <thomasvl@gmail.com@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2009-02-26 01:34:12 +0000
commit93796b6367645ea20b01649c4e397a95370ed727 (patch)
treea58e2947a8b326ed9d3a8a229ab9519999347d1d /Foundation
parent74ad2857a75567b273951be9cbe998133fbca26a (diff)
adding DO helpers
Diffstat (limited to 'Foundation')
-rw-r--r--Foundation/GTMAbstractDOListener.h222
-rw-r--r--Foundation/GTMAbstractDOListener.m438
-rw-r--r--Foundation/GTMAbstractDOListenerTest.m61
-rw-r--r--Foundation/GTMTransientRootProxy.h113
-rw-r--r--Foundation/GTMTransientRootProxy.m222
-rw-r--r--Foundation/GTMTransientRootProxyTest.m215
-rw-r--r--Foundation/GTMTransientRootSocketProxy.h42
-rw-r--r--Foundation/GTMTransientRootSocketProxy.m75
-rw-r--r--Foundation/GTMTransientRootSocketProxyTest.m194
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