Cocoa: allow timer activation to recurse
authorBradley T. Hughes <bradley.hughes@nokia.com>
Wed, 11 Apr 2012 11:08:40 +0000 (13:08 +0200)
committerQt by Nokia <qt-info@nokia.com>
Tue, 17 Apr 2012 08:15:44 +0000 (10:15 +0200)
CFRunLoopTimer's callback will never recurse. Since we are using one
CFRunLoopTimer to drive all Qt timers, we need to work around this by
sending all timers via a dedicated CFRunLoopSource (since these
callbacks can recurse). We also need to block this new timer source
along with the posted event source when calling processEvents()
"manually" to prevent livelock deep in CFRunLoop.

Change-Id: I375e46b6cfa0c76db678a1085314d42d8996d062
Reviewed-by: Morten Johan Sørvig <morten.sorvig@nokia.com>
src/plugins/platforms/cocoa/qcocoaeventdispatcher.h
src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm

index a4a1286..f286b63 100644 (file)
@@ -116,7 +116,6 @@ public:
     explicit QCocoaEventDispatcher(QObject *parent = 0);
     ~QCocoaEventDispatcher();
 
-
     bool processEvents(QEventLoop::ProcessEventsFlags flags);
     bool hasPendingEvents();
 
@@ -154,9 +153,11 @@ public:
     // timer handling
     QTimerInfoList timerInfoList;
     CFRunLoopTimerRef runLoopTimerRef;
+    CFRunLoopSourceRef activateTimersSourceRef;
     void maybeStartCFRunLoopTimer();
     void maybeStopCFRunLoopTimer();
-    static void activateTimer(CFRunLoopTimerRef, void *info);
+    static void runLoopTimerCallback(CFRunLoopTimerRef, void *info);
+    static void activateTimersSourceCallback(void *info);
 
     // Set 'blockSendPostedEvents' to true if you _really_ need
     // to make sure that qt events are not posted while calling
@@ -188,8 +189,7 @@ public:
     int lastSerial;
     bool interrupt;
 
-    static Boolean postedEventSourceEqualCallback(const void *info1, const void *info2);
-    static void postedEventsSourcePerformCallback(void *info);
+    static void postedEventsSourceCallback(void *info);
     static void waitingObserverCallback(CFRunLoopObserverRef observer,
                                         CFRunLoopActivity activity, void *info);
     static void firstLoopEntry(CFRunLoopObserverRef ref, CFRunLoopActivity activity, void *info);
index 77cccac..f2927d3 100644 (file)
@@ -104,12 +104,28 @@ static inline CFRunLoopRef mainRunLoop()
     return CFRunLoopGetMain();
 }
 
+static Boolean runLoopSourceEqualCallback(const void *info1, const void *info2)
+{
+    return info1 == info2;
+}
+
 /*****************************************************************************
   Timers stuff
  *****************************************************************************/
 
 /* timer call back */
-void QCocoaEventDispatcherPrivate::activateTimer(CFRunLoopTimerRef, void *info)
+void QCocoaEventDispatcherPrivate::runLoopTimerCallback(CFRunLoopTimerRef, void *info)
+{
+    QCocoaEventDispatcherPrivate *d = static_cast<QCocoaEventDispatcherPrivate *>(info);
+    if ((d->processEventsFlags & QEventLoop::EventLoopExec) == 0) {
+        // processEvents() was called "manually," ignore this source for now
+        d->maybeCancelWaitForMoreEvents();
+        return;
+    }
+    CFRunLoopSourceSignal(d->activateTimersSourceRef);
+}
+
+void QCocoaEventDispatcherPrivate::activateTimersSourceCallback(void *info)
 {
     QCocoaEventDispatcherPrivate *d = static_cast<QCocoaEventDispatcherPrivate *>(info);
     (void) d->timerInfoList.activateTimers();
@@ -145,7 +161,7 @@ void QCocoaEventDispatcherPrivate::maybeStartCFRunLoopTimer()
         CFRunLoopTimerContext info = { 0, this, 0, 0, 0 };
         // create the timer with a large interval, as recommended by the CFRunLoopTimerSetNextFireDate()
         // documentation, since we will adjust the timer's time-to-fire as needed to keep Qt timers working
-        runLoopTimerRef = CFRunLoopTimerCreate(0, ttf, oneyear, 0, 0, QCocoaEventDispatcherPrivate::activateTimer, &info);
+        runLoopTimerRef = CFRunLoopTimerCreate(0, ttf, oneyear, 0, 0, QCocoaEventDispatcherPrivate::runLoopTimerCallback, &info);
         Q_ASSERT(runLoopTimerRef != 0);
 
         CFRunLoopAddTimer(mainRunLoop(), runLoopTimerRef, kCFRunLoopCommonModes);
@@ -513,10 +529,10 @@ bool QCocoaEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags)
 
     emit awake();
 
-    bool excludeUserEvents = flags & QEventLoop::ExcludeUserInputEvents;
-    bool retVal = false;
     uint oldflags = d->processEventsFlags;
     d->processEventsFlags = flags;
+    bool excludeUserEvents = d->processEventsFlags & QEventLoop::ExcludeUserInputEvents;
+    bool retVal = false;
     forever {
         if (d->interrupt)
             break;
@@ -544,8 +560,9 @@ bool QCocoaEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags)
         // Finally, if we are to exclude user input events, we cannot call [NSApp run]
         // as we then loose control over which events gets dispatched:
         const bool canExec_3rdParty = d->nsAppRunCalledByQt || ![NSApp isRunning];
-        const bool canExec_Qt = !excludeUserEvents &&
-                (flags & QEventLoop::DialogExec || flags & QEventLoop::EventLoopExec) ;
+        const bool canExec_Qt = (!excludeUserEvents
+                                 && ((d->processEventsFlags & QEventLoop::DialogExec)
+                                     || (d->processEventsFlags & QEventLoop::EventLoopExec)));
 
         if (canExec_Qt && canExec_3rdParty) {
             // We can use exec-mode, meaning that we can stay in a tight loop until
@@ -632,9 +649,11 @@ bool QCocoaEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags)
                 }
             } while (!d->interrupt && event != nil);
 
-            if ((flags & QEventLoop::WaitForMoreEvents) == 0) {
-                // when called "manually", always send posted events
+            if ((d->processEventsFlags & QEventLoop::EventLoopExec) == 0) {
+                // when called "manually", always send posted events and timers
                 d->processPostedEvents();
+                retVal = d->timerInfoList.activateTimers() > 0 || retVal;
+                d->maybeStartCFRunLoopTimer();
             }
 
             // be sure to return true if the posted event source fired
@@ -651,12 +670,12 @@ bool QCocoaEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags)
         bool canWait = (d->threadData->canWait
                 && !retVal
                 && !d->interrupt
-                && (flags & QEventLoop::WaitForMoreEvents));
+                && (d->processEventsFlags & QEventLoop::WaitForMoreEvents));
         if (canWait) {
             // INVARIANT: We haven't processed any events yet. And we're told
             // to stay inside this function until at least one event is processed.
             qt_mac_waitForMoreEvents();
-            flags &= ~QEventLoop::WaitForMoreEvents;
+            d->processEventsFlags &= ~QEventLoop::WaitForMoreEvents;
         } else {
             // Done with event processing for now.
             // Leave the function:
@@ -904,15 +923,28 @@ QCocoaEventDispatcher::QCocoaEventDispatcher(QObject *parent)
     : QAbstractEventDispatcher(*new QCocoaEventDispatcherPrivate, parent)
 {
     Q_D(QCocoaEventDispatcher);
+
+    // keep our sources running when modal loops are running
+    CFRunLoopAddCommonMode(mainRunLoop(), (CFStringRef) NSModalPanelRunLoopMode);
+
     CFRunLoopSourceContext context;
     bzero(&context, sizeof(CFRunLoopSourceContext));
     context.info = d;
-    context.equal = QCocoaEventDispatcherPrivate::postedEventSourceEqualCallback;
-    context.perform = QCocoaEventDispatcherPrivate::postedEventsSourcePerformCallback;
-    d->postedEventsSource = CFRunLoopSourceCreate(0, 0, &context);
+    context.equal = runLoopSourceEqualCallback;
+
+    // source used to activate timers
+    context.perform = QCocoaEventDispatcherPrivate::activateTimersSourceCallback;
+    d->activateTimersSourceRef = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
+    Q_ASSERT(d->activateTimersSourceRef);
+    CFRunLoopAddSource(mainRunLoop(), d->activateTimersSourceRef, kCFRunLoopCommonModes);
+
+    // source used to send posted events
+    context.perform = QCocoaEventDispatcherPrivate::postedEventsSourceCallback;
+    d->postedEventsSource = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
     Q_ASSERT(d->postedEventsSource);
     CFRunLoopAddSource(mainRunLoop(), d->postedEventsSource, kCFRunLoopCommonModes);
 
+    // observer to emit aboutToBlock() and awake()
     CFRunLoopObserverContext observerContext;
     bzero(&observerContext, sizeof(CFRunLoopObserverContext));
     observerContext.info = this;
@@ -948,11 +980,6 @@ void QCocoaEventDispatcherPrivate::waitingObserverCallback(CFRunLoopObserverRef,
         emit static_cast<QCocoaEventDispatcher*>(info)->awake();
 }
 
-Boolean QCocoaEventDispatcherPrivate::postedEventSourceEqualCallback(const void *info1, const void *info2)
-{
-    return info1 == info2;
-}
-
 void QCocoaEventDispatcherPrivate::processPostedEvents()
 {
     if (blockSendPostedEvents) {
@@ -1017,9 +1044,14 @@ void QCocoaEventDispatcherPrivate::firstLoopEntry(CFRunLoopObserverRef ref,
     static_cast<QCocoaEventDispatcherPrivate *>(info)->processPostedEvents();
 }
 
-void QCocoaEventDispatcherPrivate::postedEventsSourcePerformCallback(void *info)
+void QCocoaEventDispatcherPrivate::postedEventsSourceCallback(void *info)
 {
     QCocoaEventDispatcherPrivate *d = static_cast<QCocoaEventDispatcherPrivate *>(info);
+    if ((d->processEventsFlags & QEventLoop::EventLoopExec) == 0) {
+        // processEvents() was called "manually," ignore this source for now
+        d->maybeCancelWaitForMoreEvents();
+        return;
+    }
     d->processPostedEvents();
     d->maybeCancelWaitForMoreEvents();
 }
@@ -1069,6 +1101,8 @@ QCocoaEventDispatcher::~QCocoaEventDispatcher()
 
     qDeleteAll(d->timerInfoList);
     d->maybeStopCFRunLoopTimer();
+    CFRunLoopRemoveSource(mainRunLoop(), d->activateTimersSourceRef, kCFRunLoopCommonModes);
+    CFRelease(d->activateTimersSourceRef);
 
     // Remove CFSockets from the runloop.
     for (MacSocketHash::ConstIterator it = d->macSockets.constBegin(); it != d->macSockets.constEnd(); ++it) {