Write BSProtocolThreadInvoker to replace ThreadSafeDelegate.
authorRobert Sesek <rsesek@bluestatic.org>
Wed, 3 Jul 2013 23:14:09 +0000 (19:14 -0400)
committerRobert Sesek <rsesek@bluestatic.org>
Wed, 3 Jul 2013 23:14:09 +0000 (19:14 -0400)
This class protects against reentrancy, which happens when AppKit uses WebKit
on the main thread for NSAttributedString.

MacGDBp.xcodeproj/project.pbxproj
Source/BSProtocolThreadInvoker.h [moved from Source/ThreadSafeDeleage.h with 67% similarity]
Source/BSProtocolThreadInvoker.m [new file with mode: 0644]
Source/MessageQueue.h
Source/MessageQueue.m
Source/ThreadSafeDeleage.m [deleted file]

index de7fa27a18cde6a8f9fee014ffb233b9adda4a70..330a3ca2611c842f2e5fd89d84aafacc47c266d0 100644 (file)
@@ -24,7 +24,7 @@
                1E416FF90D36F821009A53A2 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1E416FF60D36F821009A53A2 /* MainMenu.xib */; };
                1E42F1D70F53317B008412DB /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = 1E42F1D60F53317B008412DB /* dsa_pub.pem */; };
                1E4C7AF90DA401C7000A9DC7 /* BreakpointManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E4C7AF80DA401C7000A9DC7 /* BreakpointManager.m */; };
-               1E5C32AA177296DF00F4377B /* ThreadSafeDeleage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E5C32A9177296DF00F4377B /* ThreadSafeDeleage.m */; };
+               1E5C32AA177296DF00F4377B /* BSProtocolThreadInvoker.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E5C32A9177296DF00F4377B /* BSProtocolThreadInvoker.m */; };
                1E67E6FD0F3C052000E68F1B /* PreferencesPathsArrayController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E67E6FC0F3C052000E68F1B /* PreferencesPathsArrayController.m */; };
                1E6B5947116106FE001189D2 /* LoggingController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E6B5946116106FE001189D2 /* LoggingController.m */; };
                1E6B594C11610993001189D2 /* Log.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1E6B594A11610993001189D2 /* Log.xib */; };
@@ -98,8 +98,8 @@
                1E42F1D60F53317B008412DB /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = dsa_pub.pem; sourceTree = "<group>"; };
                1E4C7AF70DA401C7000A9DC7 /* BreakpointManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BreakpointManager.h; path = Source/BreakpointManager.h; sourceTree = "<group>"; };
                1E4C7AF80DA401C7000A9DC7 /* BreakpointManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BreakpointManager.m; path = Source/BreakpointManager.m; sourceTree = "<group>"; };
-               1E5C32A8177296DF00F4377B /* ThreadSafeDeleage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ThreadSafeDeleage.h; path = Source/ThreadSafeDeleage.h; sourceTree = "<group>"; };
-               1E5C32A9177296DF00F4377B /* ThreadSafeDeleage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ThreadSafeDeleage.m; path = Source/ThreadSafeDeleage.m; sourceTree = "<group>"; };
+               1E5C32A8177296DF00F4377B /* BSProtocolThreadInvoker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BSProtocolThreadInvoker.h; path = Source/BSProtocolThreadInvoker.h; sourceTree = "<group>"; };
+               1E5C32A9177296DF00F4377B /* BSProtocolThreadInvoker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BSProtocolThreadInvoker.m; path = Source/BSProtocolThreadInvoker.m; sourceTree = "<group>"; };
                1E67E6FB0F3C052000E68F1B /* PreferencesPathsArrayController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PreferencesPathsArrayController.h; path = Source/PreferencesPathsArrayController.h; sourceTree = "<group>"; };
                1E67E6FC0F3C052000E68F1B /* PreferencesPathsArrayController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PreferencesPathsArrayController.m; path = Source/PreferencesPathsArrayController.m; sourceTree = "<group>"; };
                1E6B5945116106FE001189D2 /* LoggingController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = LoggingController.h; path = Source/LoggingController.h; sourceTree = "<group>"; };
                                1EEBE841176FEA80003622C3 /* MessageQueue.m */,
                                1EEBE843176FFE04003622C3 /* ProtocolClient.h */,
                                1EEBE844176FFE04003622C3 /* ProtocolClient.m */,
-                               1E5C32A8177296DF00F4377B /* ThreadSafeDeleage.h */,
-                               1E5C32A9177296DF00F4377B /* ThreadSafeDeleage.m */,
+                               1E5C32A8177296DF00F4377B /* BSProtocolThreadInvoker.h */,
+                               1E5C32A9177296DF00F4377B /* BSProtocolThreadInvoker.m */,
                        );
                        name = Protocol;
                        sourceTree = "<group>";
                                1E109019136DD92D002E34E0 /* StripLineBreaksValueTransformer.m in Sources */,
                                1EEBE842176FEA80003622C3 /* MessageQueue.m in Sources */,
                                1EEBE845176FFE04003622C3 /* ProtocolClient.m in Sources */,
-                               1E5C32AA177296DF00F4377B /* ThreadSafeDeleage.m in Sources */,
+                               1E5C32AA177296DF00F4377B /* BSProtocolThreadInvoker.m in Sources */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
similarity index 67%
rename from Source/ThreadSafeDeleage.h
rename to Source/BSProtocolThreadInvoker.h
index f80dfa017ecb233aaaa76200ed26105c6867b57d..da6543ac0c84c7cbb5e24bdfb387c05d9fc0930e 100644 (file)
 
 #import <Foundation/Foundation.h>
 
-@interface ThreadSafeDeleage : NSObject
+// BSProtocolThreadInvoker will forward all messages that are part of |protocol|
+// to its target |object| on the specified |thread| in the optional modes. This
+// allows a client to hold this as a delegate and write thread-safe code with
+// minimal work. The class also protects against the target |object| reentering
+// itself; if it or something it calls runs a nested run loop, the messages
+// will be queued until it would no longer reenter the object.
+@interface BSProtocolThreadInvoker : NSObject
 
+// The target object to which messages will be sent.
 @property(readonly, atomic) NSObject* object;
 
 - (id)initWithObject:(NSObject*)object
diff --git a/Source/BSProtocolThreadInvoker.m b/Source/BSProtocolThreadInvoker.m
new file mode 100644 (file)
index 0000000..24c04e4
--- /dev/null
@@ -0,0 +1,200 @@
+/*
+ * MacGDBp
+ * Copyright (c) 2013, Blue Static <http://www.bluestatic.org>
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
+ * General Public License as published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
+ * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program; if not,
+ * write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+#import "BSProtocolThreadInvoker.h"
+
+@interface BSProtocolThreadInvoker (Private)
+// Installs a run loop observer on the target thread that is used to start
+// dispatching messages again after falling out of a nested run loop.
+// *MUST* be called on the target thread.
+- (void)addRunLoopObserver;
+
+// Removes the observer added with |-addRunLoopObserver|. Can be called on any
+// thread since CFRunLoop is threadsafe.
+- (void)removeRunLoopObserver;
+
+// Enqueues the |invocation| for execution, and dequeues the first if not called
+// reentrantly.
+// *MUST* be called on the target thread.
+- (void)dispatchInvocation:(NSInvocation*)invocation;
+
+// Callback for the run loop whenever it begins a new pass. This will schedule
+// work if any was previously deferred due to reentrancy protection.
+- (void)observedRunLoopEnter;
+@end
+
+@implementation BSProtocolThreadInvoker {
+  // The fully qualified target of the invocation.
+  NSObject* _object;
+  Protocol* _protocol;
+  NSThread* _thread;
+  CFRunLoopRef _runLoop;
+  NSArray* _modes;
+
+  // If executing an invocation from |-dispatchInvocation:|. Protects against
+  // reentering the target.
+  BOOL _isDispatching;
+
+  // The queue of work to be executed. Enqueues in  |-dispatchInvocation:|.
+  NSMutableArray* _invocations;
+
+  CFRunLoopObserverRef _observer;
+}
+
+@synthesize object = _object;
+
+- (id)initWithObject:(NSObject*)object
+            protocol:(Protocol*)protocol
+              thread:(NSThread*)thread
+{
+  return [self initWithObject:object
+                     protocol:protocol
+                       thread:thread
+                        modes:@[ NSRunLoopCommonModes ]];
+}
+
+- (id)initWithObject:(NSObject*)object
+            protocol:(Protocol*)protocol
+              thread:(NSThread*)thread
+               modes:(NSArray*)runLoopModes
+{
+  if ((self = [super init])) {
+    _object = object;
+    _protocol = protocol;
+    _thread = thread;
+    _modes = [runLoopModes retain];
+    _invocations = [[NSMutableArray alloc] init];
+
+    [self performSelector:@selector(addRunLoopObserver)
+                 onThread:_thread
+               withObject:nil
+            waitUntilDone:NO
+                    modes:_modes];
+  }
+  return self;
+}
+
+- (void)dealloc
+{
+  [self removeRunLoopObserver];
+  [_modes release];
+  [_invocations release];
+  [super dealloc];
+}
+
+- (BOOL)conformsToProtocol:(Protocol*)protocol
+{
+  return [_protocol isEqual:protocol];
+}
+
+- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector
+{
+  if (!_object)
+    return [_protocol methodSignatureForSelector:aSelector];
+  return [_object methodSignatureForSelector:aSelector];
+}
+
+- (BOOL)respondsToSelector:(SEL)aSelector
+{
+  if (!_object)
+    return [_protocol respondsToSelector:aSelector];
+  return [_object respondsToSelector:aSelector];
+}
+
+- (void)forwardInvocation:(NSInvocation*)invocation
+{
+  if ([_object respondsToSelector:[invocation selector]]) {
+    [invocation retainArguments];
+    [self performSelector:@selector(dispatchInvocation:)
+                 onThread:_thread
+               withObject:invocation
+            waitUntilDone:NO
+                    modes:_modes];
+  }
+}
+
+// Private /////////////////////////////////////////////////////////////////////
+
+- (void)addRunLoopObserver
+{
+  assert([NSThread currentThread] == _thread);
+  _runLoop = CFRunLoopGetCurrent();
+
+  BSProtocolThreadInvoker* __block weakSelf = self;
+  _observer = CFRunLoopObserverCreateWithHandler(
+      kCFAllocatorDefault,
+      kCFRunLoopEntry,
+      TRUE,  // Repeats.
+      0,  // Order.
+      ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
+          [weakSelf observedRunLoopEnter];
+      });
+  for (NSString* mode in _modes)
+    CFRunLoopAddObserver(_runLoop, _observer, (CFStringRef)mode);
+}
+
+- (void)removeRunLoopObserver
+{
+  for (NSString* mode in _modes)
+    CFRunLoopRemoveObserver(_runLoop, _observer, (CFStringRef)mode);
+  CFRelease(_observer);
+}
+
+- (void)dispatchInvocation:(NSInvocation*)invocation
+{
+  // |invocation| will be nil if dispatch was requested after entering a new
+  // pass of the run loop, to process deferred work.
+  if (invocation)
+    [_invocations addObject:invocation];
+
+  // Protect the target object from reentering itself. This work will be
+  // rescheduled when another run loop starts (including falling out of a
+  // nested loop and starting a new pass through a lower loop).
+  if (_isDispatching)
+    return;
+
+  _isDispatching = YES;
+
+  // Dequeue only one item. If multiple items are present, the next pass through
+  // the run loop will schedule another dispatch via |-observedRunLoopEnter|.
+  invocation = [_invocations objectAtIndex:0];
+  [invocation invokeWithTarget:_object];
+  [_invocations removeObjectAtIndex:0];
+
+  _isDispatching = NO;
+}
+
+- (void)observedRunLoopEnter
+{
+  // Don't do anything if there's nothing to do.
+  if ([_invocations count] == 0)
+    return;
+
+  // If this nested run loop is still executing from within
+  // |-dispatchInvocation:|, continue to wait for a the nested loop to exit.
+  if (_isDispatching)
+    return;
+
+  // A run loop has started running outside of |-dispatchInvocation:|, so
+  // schedule work to be done again.
+  [self performSelector:@selector(dispatchInvocation:)
+               onThread:_thread
+             withObject:nil
+          waitUntilDone:NO
+                  modes:_modes];
+}
+
+@end
index 2e622889dc7a46b83fea7b7df24b2cb54bbbd389..16961d75fa9c0206ce1dc2880ab04370bbe877fc 100644 (file)
@@ -16,7 +16,7 @@
 
 #import <Foundation/Foundation.h>
 
-#import "ThreadSafeDeleage.h"
+#import "BSProtocolThreadInvoker.h"
 
 @protocol MessageQueueDelegate;
 
@@ -40,7 +40,7 @@
   NSMutableArray* _queue;
 
   // The delegate for this class.
-  ThreadSafeDeleage<MessageQueueDelegate>* _delegate;
+  BSProtocolThreadInvoker<MessageQueueDelegate>* _delegate;
 
   // The socket that listens for new incoming connections.
   CFSocketRef _socket;
index 8ada0007bf2c02721664ac08a435cfc7c6000d5d..0a657e3a5cd7b34e6956a612c4d90ea878c1ba47 100644 (file)
@@ -88,11 +88,10 @@ static void MessageQueueWriteEvent(CFWriteStreamRef stream,
   if ((self = [super init])) {
     _port = port;
     _queue = [[NSMutableArray alloc] init];
-    _delegate = (ThreadSafeDeleage<MessageQueueDelegate>*)
-        [[ThreadSafeDeleage alloc] initWithObject:delegate
+    _delegate = (BSProtocolThreadInvoker<MessageQueueDelegate>*)
+        [[BSProtocolThreadInvoker alloc] initWithObject:delegate
                                          protocol:@protocol(MessageQueueDelegate)
-                                           thread:[NSThread currentThread]
-                                            modes:@[ NSDefaultRunLoopMode ]];
+                                           thread:[NSThread currentThread]];
   }
   return self;
 }
diff --git a/Source/ThreadSafeDeleage.m b/Source/ThreadSafeDeleage.m
deleted file mode 100644 (file)
index 4c0493f..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * MacGDBp
- * Copyright (c) 2013, Blue Static <http://www.bluestatic.org>
- *
- * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
- * General Public License as published by the Free Software Foundation; either version 2 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
- * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with this program; if not,
- * write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
- */
-
-#import "ThreadSafeDeleage.h"
-
-@implementation ThreadSafeDeleage {
-  NSObject* _object;
-  Protocol* _protocol;
-  NSThread* _thread;
-  NSArray* _modes;
-}
-
-@synthesize object = _object;
-
-- (id)initWithObject:(NSObject*)object
-            protocol:(Protocol*)protocol
-              thread:(NSThread*)thread {
-  return [self initWithObject:object
-                     protocol:protocol
-                       thread:thread
-                        modes:@[ NSRunLoopCommonModes ]];
-}
-
-- (id)initWithObject:(NSObject*)object
-            protocol:(Protocol*)protocol
-              thread:(NSThread*)thread
-               modes:(NSArray*)runLoopModes {
-  if ((self = [super init])) {
-    _object = object;
-    _protocol = protocol;
-    _thread = thread;
-    _modes = [runLoopModes retain];
-  }
-  return self;
-}
-
-- (void)dealloc {
-  [_modes release];
-  [super dealloc];
-}
-
-- (BOOL)conformsToProtocol:(Protocol*)protocol {
-  return [_protocol isEqual:protocol];
-}
-
-- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector {
-  if (!_object)
-    return [_protocol methodSignatureForSelector:aSelector];
-  return [_object methodSignatureForSelector:aSelector];
-}
-
-- (BOOL)respondsToSelector:(SEL)aSelector {
-  if (!_object)
-    return [_protocol respondsToSelector:aSelector];
-  return [_object respondsToSelector:aSelector];
-}
-
-- (void)forwardInvocation:(NSInvocation*)invocation {
-  if ([_object respondsToSelector:[invocation selector]]) {
-    [invocation retainArguments];
-    [self performSelector:@selector(dispatchInvocation:)
-                 onThread:_thread
-               withObject:invocation
-            waitUntilDone:NO
-                    modes:_modes];
-  }
-}
-
-- (void)dispatchInvocation:(NSInvocation*)invocation {
-  [invocation invokeWithTarget:_object];
-}
-
-@end