Skip to content

Conversation

@PatTheMav
Copy link
Member

@PatTheMav PatTheMav commented Jan 23, 2026

Description

Fixes a crash in application shutdown with a YouTube service active by explicitly removing the dock before the main CEF browser instance used by the application is destroyed.

Motivation and Context

Currently all docks in the application are destroyed through the explicit destruction of their owner objects. This applies to most service-based docks that are owned by an Auth object, which itself is explicitly destroyed by having its pointer reset during shutdown, and user-created "extra" browser docks, which are explicitly destroyed via ClearExtraBrowserDocks.

The global YouTube app dock which is created without an established service connection (but only when YouTube is selected as an output destination) is not handled by any code during shutdown.

Fixes #12920.

Investigation of the Crash

This will result in a crash via the following process:

  1. OBSBasic::closeWindow - runs in one way or another (either triggered directly by a QAction or indirectly by the platform's terminate signal
  2. QWidget::deleteLater - called on the OBSBasic instance, scheduling a DeferredDelete on the active event loop
  3. OBSBasic::~OBSBasic - triggered by the actual delete call in the event handler
  4. std::default_delete<QList<std::shared_ptr<QDockWidget>>> - triggered by the destruction of the owning OBSBasic instance
  5. QDockWidget::~QDockWidget - triggered by the destruction of the owning QList
  6. YouTubeAppDock::~YouTubeAppDock - triggered by the destructor of QDockWidget
  7. QCefWidgetInternal::~QCefWidgetInternal - triggered by the destructor of YouTubeAppDock
  8. QCefWidgetInternal::closeBrowser - triggered by the class's destructor
  9. QEventLoop::exec - runs a nested event loop, blocking continuation of the closeBrowser function to wait 1s for CEF to signal successful shutdown
  10. [NSApp terminate:nil] - triggered by a QCocoaIntegration::Quit signal that originates from OBSBasic::closeWindow via QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection). This event is now handled by the nested event loop.
  11. QWindowSystemInterface::handleApplicationTermination() - triggered by the terminate AppKit call and emits the aboutToQuit signal
  12. OBSBasic::~OBSBasic - triggered by the anonymous lambda connected to the aboutToQuit signal and its explicit delete of the OBSBasic instance
  13. 💥💥💥

The nested destructor call leads to destructor calls of child objects that have been deallocated already, thus the app crashes.

This issue is also another example of the inherent dangers of using exec in Qt (something the project relies on far too much):

  • Two events are scheduled to run on the event loop in close succession (and in this order) by OBSBasic::closeWindow:
    1. OBSBasic::deleteLater
    2. OBSApp::Quit
  • It seems that events are considered "consumed" while their associated event handlers run
  • The nested event loop spun up inside QCefWidgetInternal::closeBrowser will continue consuming all scheduled events in the loop, which will either immediately or very quickly be the Quit event.
  • Thus the nested event loop runs all events that were scheduled after the deleteLater event, even though its event handler hasn't finished yet

Mitigation of the Crash

After an initial attempt to fix the crash by ensuring a specific order of events did not work when the application shutdown was initiated by AppKit's terminate event, the second commit was completely rewritten.

The following changes are made to the application code:

  • The custom "Quit" menu item is removed on macOS, thus Qt will use its own default implementation which will simply call [NSApp terminate] and thus creates the same sequence of events as if the app had been terminated by macOS (either because the OS shuts down/reboots or because it's been terminated via the Dock or Finder).
  • Parts of the updated shutdown logic have been excluded from macOS via preprocessor conditionals to prevent the emission of additional and superfluous "Quit", "Close", or "DeferredDelete" events.
  • The first time the main window is closed (and its cleanup code executed) a "Quit" event is emitted directly
  • The POSIX signal handlers have been updated to also simply emit a "Quit" event instead

In combination these changes reduce the number of different code paths taken during shutdown:

  • Closing the app via the menu item, menu item shortcut, initiated by AppKit (OS shutdown/reboot, or quit via Dock/Finder) will emit an AppKit "terminate" event for orderly shutdown
  • Closing the main window or sending a POSIX signal triggers the "terminate" event indirectly by emitting the "quit" event on the application instance

Either way a "close" event to the main window happens before the event loop is terminated and the application instance is torn down (either directly, or indirectly via Qt's "closeAllWindows" function in response to "terminate"). The order of events thus is always:

  1. Terminate event by AppKit (except when closing the main window)
  2. Closing of main window
  3. Termination of browser docks
  4. Deallocation of main window
  5. Termination of application
  6. Deallocation of application

How Has This Been Tested?

Successfully shut down the app with open YouTube app docks via:

  • Menu item
  • Menu item shortcut
  • Quitting the app via its dock icon
  • Closing the main window
  • Sending SIGABRT, SIGQUIT, SIGINT, and SIGTERM to the application

Types of changes

  • Bug fix (non-breaking change which fixes an issue)

Checklist:

  • My code has been run through clang-format.
  • I have read the contributing document.
  • My code is not on the master branch.
  • The code has been tested.
  • All commit messages are properly formatted and commits squashed where appropriate.
  • I have included updates to all appropriate documentation.

@PatTheMav PatTheMav requested review from RytoEX and Warchamp7 January 23, 2026 16:00
@PatTheMav PatTheMav force-pushed the macos-shutdown-fix branch 3 times, most recently from c1c63e3 to 822ddeb Compare January 26, 2026 17:04
When the main window is closed and with it the application state is torn
down, browser panels need to be explicitly removed before the the CEF
instance used by the application is shut down itself.

For service-based docks this happens as part of the "reset" of the
"auth" pointer (and thus its destructor), for user-created browser
panels this is achieved by the call to "ClearExtraBrowserDocks".

Because the Youtube app dock is a special browser panel that is created
conditionally, but potentially exists globally, it also has to be
closed this way (if it was created).

Otherwise CEF will force-close the underlying browser host instance as
part of its own shutdown and also deallocate the native window used
by the browser. When the QCefWidget then attempts to detach the
native window from the view hierarchy (to avoid this operation from
potentially closing the root window it is anchored to), it will either
attempt to access a CrFatZombie object (and crash) or access deallocated
memory (and also crash).
With recent changes to the application shutdown logic, events had to
follow a very strict order as certain elements of shutdown code
depend on other elements not being deallocated prematurely.

This turned the (correct) order of events on macOS upside down and
lead to crashes either when the app was quit from within or when
terminated by the OS.

The fix incorporates multiple elements:

* Removal of the custom "Quit" menu item on macOS to use the default
  implementation of Qt's platform plugin.
* Soft-revert (via preprocessor conditionals) parts of the updated
  shutdown logic to prevent emitting recursive shutdown events.
* Handle main window close event by simply emitting a "quit" event
  on the application instance.
* Update POSIX signal handlers to also simply emit a "quit" event.

In combination these changes reduce the number of different code paths
taken during shutdown:

* Closing the app via the menu item, menu item shortcut, or initiated
  by AppKit (OS shutdown/reboot, or quit via Dock/Finder) will emit an
  AppKit "terminate" event for orderly shutdown.
* Closing the main window or sending an appropriate POSIX signal
  triggers the "terminate" event indirectly by emitting the "quit"
  event on the application instance.

Either way a "close" event to the main window happens before the
event loop is terminated and the application instance is torn down
(either directly, or indirectly via Qt's "closeAllWindows" function in
response to "terminate"). The order of events thus is always:

0. Terminate event by AppKit (except when closing the main window)
1. Closing of main window
2. Termination of browser docks
3. Deallocation of main window
4. Termination of application
5. Deallocation of application

NOTE: All this only applies to macOS. The shutdown order and procedures
on Windows and Linux are unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OBS 32.0.4 Keeps Asking for Re-authentication of My YouTube Account on macOS

1 participant