From 30e32870a052fda1a23099e46864242e52d94a17 Mon Sep 17 00:00:00 2001 From: Timur Pocheptsov Date: Wed, 4 Dec 2019 14:58:27 +0100 Subject: [PATCH] QCocoaEventDispatcher: make 'interrupt' work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit even if we are currently inside processEvents (apparently called manually and not from QEventLoop::exec()). A carefully crafted application (see, for example, the linked QTBUG or even updated auto-test) can trigger itself into failing to exit the current (potentially nested) event loop. We can harden our Cocoa event dispatcher to detect such condition and properly propagate 'interrupt' to where it'll do its job, indeed, interrupting the real event loop (aka [NSApp run]). This mainly means we have to undo what bool blocker would erroneously do. Also, long live (as people love to say these days) to another tricky (somewhat) auto-test (surely, it's not flaky!). Fixes: QTBUG-79477 Change-Id: I794f0cda23e24d36be67f2bb63d52b74be057c31 Reviewed-by: Volker Hilsheimer Reviewed-by: Qt CI Bot Reviewed-by: Tor Arne Vestbø --- .../platforms/cocoa/qcocoaeventdispatcher.h | 1 + .../platforms/cocoa/qcocoaeventdispatcher.mm | 22 ++++++++- .../qeventdispatcher/tst_qeventdispatcher.cpp | 45 +++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h index 69587a24be..b8d2532b8e 100644 --- a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h +++ b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h @@ -186,6 +186,7 @@ public: QAtomicInt serialNumber; int lastSerial; bool interrupt; + bool propagateInterrupt = false; static void postedEventsSourceCallback(void *info); static void waitingObserverCallback(CFRunLoopObserverRef observer, diff --git a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm index 110c82bf1f..94b9e62eab 100644 --- a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm +++ b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm @@ -84,13 +84,12 @@ #include "private/qthread_p.h" #include "private/qguiapplication_p.h" #include +#include #include QT_BEGIN_NAMESPACE -QT_USE_NAMESPACE - static inline CFRunLoopRef mainRunLoop() { return CFRunLoopGetMain(); @@ -348,6 +347,16 @@ static inline void qt_mac_waitForMoreEvents(NSString *runLoopMode = NSDefaultRun bool QCocoaEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags) { Q_D(QCocoaEventDispatcher); + + // In rare rather corner cases a user's application messes with + // QEventLoop::exec()/exit() and QCoreApplication::processEvents(), + // we have to undo what bool blocker normally does. + d->propagateInterrupt = false; + const auto boolBlockerUndo = qScopeGuard([d](){ + if (d->propagateInterrupt) + d->interrupt = true; + d->propagateInterrupt = false; + }); QBoolBlocker interruptBlocker(d->interrupt, false); bool interruptLater = false; @@ -496,7 +505,16 @@ bool QCocoaEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags) if ((d->processEventsFlags & QEventLoop::EventLoopExec) == 0) { // When called "manually", always process posted events and timers + bool oldInterrupt = d->interrupt; d->processPostedEvents(); + if (!oldInterrupt && d->interrupt && !d->currentModalSession()) { + // We had direct processEvent call, coming not from QEventLoop::exec(). + // One of the posted events triggered an application to interrupt the loop. + // But bool blocker will reset d->interrupt to false, so the real event + // loop will never notice it was interrupted. Now we'll have to fix it by + // enforcing the value of d->interrupt. + d->propagateInterrupt = true; + } retVal = d->processTimers() || retVal; } diff --git a/tests/auto/corelib/kernel/qeventdispatcher/tst_qeventdispatcher.cpp b/tests/auto/corelib/kernel/qeventdispatcher/tst_qeventdispatcher.cpp index 49c10c6a24..365508024b 100644 --- a/tests/auto/corelib/kernel/qeventdispatcher/tst_qeventdispatcher.cpp +++ b/tests/auto/corelib/kernel/qeventdispatcher/tst_qeventdispatcher.cpp @@ -67,6 +67,7 @@ private slots: void sendPostedEvents_data(); void sendPostedEvents(); void processEventsOnlySendsQueuedEvents(); + void eventLoopExit(); }; bool tst_QEventDispatcher::event(QEvent *e) @@ -314,5 +315,49 @@ void tst_QEventDispatcher::processEventsOnlySendsQueuedEvents() QCOMPARE(object.eventsReceived, 4); } +void tst_QEventDispatcher::eventLoopExit() +{ + // This test was inspired by QTBUG-79477. A particular + // implementation detail in QCocoaEventDispatcher allowed + // QEventLoop::exit() to fail to really exit the event loop. + // Thus this test is a part of the dispatcher auto-test. + + // Imitates QApplication::exec(): + QEventLoop mainLoop; + // The test itself is a lambda: + QTimer::singleShot(0, [&mainLoop]() { + // Two more single shots, both will be posted as events + // (zero timeout) and supposed to be processes by the + // mainLoop: + + QTimer::singleShot(0, [&mainLoop]() { + // wakeUp triggers QCocoaEventDispatcher into incrementing + // its 'serialNumber': + mainLoop.wakeUp(); + // QCocoaEventDispatcher::processEvents() will process + // posted events and execute the second lambda defined below: + QCoreApplication::processEvents(); + }); + + QTimer::singleShot(0, [&mainLoop]() { + // With QCocoaEventDispatcher this is executed while in the + // processEvents (see above) and would fail to actually + // interrupt the loop. + mainLoop.exit(); + }); + }); + + bool timeoutObserved = false; + QTimer::singleShot(500, [&timeoutObserved, &mainLoop]() { + // In case the QEventLoop::exit above failed, we have to bail out + // early, not wasting time: + mainLoop.exit(); + timeoutObserved = true; + }); + + mainLoop.exec(); + QVERIFY(!timeoutObserved); +} + QTEST_MAIN(tst_QEventDispatcher) #include "tst_qeventdispatcher.moc"