From 27f08548131e385f3c0c1fd4721e5979d512effe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Arne=20Vestb=C3=B8?= Date: Mon, 14 Nov 2022 14:39:12 +0100 Subject: [PATCH] macOS: Ensure NSApp initialization without relying on event processing When processing events without QEventLoop::EventLoopExec we can not rely on [NSApp run] for running the native run loop. This can happen e.g. when running a dialog, or when processing events manually via processEvents(). Unfortunately, AppKit relies on at least one call to [NSApp run] for doing critical initialization of NSApplication, so when we end up spinning the native runloop via other means, we still need to ensure we pass through [NSApp run] at least once. Doing so in a way that relies on our own runloop sources firing and interrupting the NSApp run is fragile though. For example, the logic added in bffbfc5b3368abaa5f95b7e4f8342e9202ab065a to avoid live locks in CFRunLoop by skipping out event runloop source when processEvents() is called manually will also result in the [NSApp run] never being stopped. To fix this we use a dedicated runloop block instead, where we explicitly call [NSApp stop:] when first entering the runloop. In addition we ensure that none of our sources will process events, so that we don't end up recursing into a runModal or similar call until after we've initialized NSApp. Change-Id: I137df46d5fa4105cdc39fe74b6c12275cfd46127 Reviewed-by: Timur Pocheptsov --- .../platforms/cocoa/qcocoaeventdispatcher.h | 4 ++ .../platforms/cocoa/qcocoaeventdispatcher.mm | 70 +++++++++++++++---- 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h index 7a291022fe..b4db7775da 100644 --- a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h +++ b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h @@ -55,11 +55,14 @@ #include #include #include +#include #include QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcEventDispatcher); + typedef struct _NSModalSession *NSModalSession; typedef struct _QCocoaModalSessionInfo { QPointer window; @@ -126,6 +129,7 @@ public: QStack cocoaModalSessionStack; bool currentExecIsNSAppRun; bool nsAppRunCalledByQt; + bool initializingNSApplication = false; bool cleanupModalSessionsNeeded; uint processEventsCalled; NSModalSession currentModalSessionCached; diff --git a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm index da77a8432f..1ea3344ee8 100644 --- a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm +++ b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm @@ -58,6 +58,8 @@ QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher"); + static inline CFRunLoopRef mainRunLoop() { return CFRunLoopGetMain(); @@ -87,6 +89,13 @@ void QCocoaEventDispatcherPrivate::runLoopTimerCallback(CFRunLoopTimerRef, void void QCocoaEventDispatcherPrivate::activateTimersSourceCallback(void *info) { QCocoaEventDispatcherPrivate *d = static_cast(info); + if (d->initializingNSApplication) { + qCDebug(lcEventDispatcher) << "Deferring" << __FUNCTION__ << "due to NSApp initialization"; + // We don't want to process any sources during explicit NSApplication + // initialization, so defer the source until the actual event processing. + CFRunLoopSourceSignal(d->activateTimersSourceRef); + return; + } d->processTimers(); d->maybeCancelWaitForMoreEvents(); } @@ -555,22 +564,41 @@ void QCocoaEventDispatcher::wakeUp() void QCocoaEventDispatcherPrivate::ensureNSAppInitialized() { - // Some elements in Cocoa require NSApplication to be running before - // they get fully initialized, in particular the menu bar. This - // function is intended for cases where a dialog is told to execute before - // QGuiApplication::exec is called, or the application spins the events loop - // manually rather than calling QGuiApplication:exec. - // The function makes sure that NSApplication starts running, but stops - // it again as soon as the send posted events callback is called. That way - // we let Cocoa finish the initialization it seems to need. We'll only - // apply this trick at most once for any application, and we avoid doing it - // for the common case where main just starts QGuiApplication::exec. + // Some elements in Cocoa require NSApplication to be initialized before + // use, for example the menu bar. Under normal circumstances this happens + // as part of [NSApp run], as a result of a call to QGuiApplication:exec(), + // but in the cases where a dialog is asked to execute before that happens, + // or the application spins the event loop manually via processEvents(), + // we need to explicitly ensure NSApplication initialization. + + // We can unfortunately not do this via NSApplicationLoad(), as the function + // bails out early if there's already an NSApplication instance, which is + // the case if any code has called [NSApplication sharedApplication], + // or its short form 'NSApp'. + + // Instead we do an actual [NSApp run], but stop the application as soon + // as possible, ensuring that AppKit will do the required initialization, + // including calling [NSApplication finishLaunching]. + + // We only apply this trick at most once for any application, and we avoid + // doing it for the common case where main just starts QGuiApplication::exec. if (nsAppRunCalledByQt || [NSApp isRunning]) return; + + qCDebug(lcEventDispatcher) << "Ensuring NSApplication is initialized"; nsAppRunCalledByQt = true; - QBoolBlocker block1(interrupt, true); - QBoolBlocker block2(currentExecIsNSAppRun, true); + + // Stopping the application will still process runloop sources before + // actually stopping, so we need to explicitly guard our sources from + // doing anything, deferring their actions until later. + QBoolBlocker initializationGuard(initializingNSApplication, true); + + CFRunLoopPerformBlock(mainRunLoop(), kCFRunLoopCommonModes, ^{ + qCDebug(lcEventDispatcher) << "NSApplication has been initialized; Stopping NSApp"; + [NSApp stop:NSApp]; + }); [NSApp run]; + qCDebug(lcEventDispatcher) << "Finished ensuring NSApplication is initialized"; } void QCocoaEventDispatcherPrivate::temporarilyStopAllModalSessions() @@ -875,12 +903,30 @@ void QCocoaEventDispatcherPrivate::firstLoopEntry(CFRunLoopObserverRef ref, { Q_UNUSED(ref); Q_UNUSED(activity); + + QCocoaEventDispatcherPrivate *d = static_cast(info); + if (d->initializingNSApplication) { + qCDebug(lcEventDispatcher) << "Deferring" << __FUNCTION__ << "due to NSApp initialization"; + // We don't want to process any sources during explicit NSApplication + // initialization, so defer the source until the actual event processing. + CFRunLoopSourceSignal(d->postedEventsSource); + return; + } + static_cast(info)->processPostedEvents(); } void QCocoaEventDispatcherPrivate::postedEventsSourceCallback(void *info) { QCocoaEventDispatcherPrivate *d = static_cast(info); + if (d->initializingNSApplication) { + qCDebug(lcEventDispatcher) << "Deferring" << __FUNCTION__ << "due to NSApp initialization"; + // We don't want to process any sources during explicit NSApplication + // initialization, so defer the source until the actual event processing. + CFRunLoopSourceSignal(d->postedEventsSource); + return; + } + if (d->processEventsCalled && (d->processEventsFlags & QEventLoop::EventLoopExec) == 0) { // processEvents() was called "manually," ignore this source for now d->maybeCancelWaitForMoreEvents();