Browse Source

frontend: Adjust application shutdown logic

Improves app shutdown in a few ways, including separating out different
pieces of the OBSBasic close handler into their own functions.

Removes the crash handler sentinel earlier when the main window is closed,
preventing unclean shutdown warnings when a plugin causes issues. While not
ideal, the dialog is not useful when we cannot specify which plugin caused the
problem.

Increases shutdown priority of the main application so that when OBS interrupts
the session ending, CEF is not closed at the same time. This fixes a crash.

Additional safeguards and event handling to try to ensure a smoother shutdown.
Warchamp7 2 months ago
parent
commit
14572498dc

+ 62 - 11
frontend/OBSApp.cpp

@@ -42,9 +42,8 @@
 #include <QFile>
 #endif
 
-#ifdef _WIN32
 #include <QSessionManager>
-#else
+#ifndef _WIN32
 #include <QSocketNotifier>
 #endif
 
@@ -78,6 +77,7 @@ extern string opt_starting_profile;
 
 #ifndef _WIN32
 int OBSApp::sigintFd[2];
+int OBSApp::sigtermFd[2];
 #endif
 
 // GPU hint exports for AMD/NVIDIA laptops
@@ -868,6 +868,8 @@ OBSApp::OBSApp(int &argc, char **argv, profiler_name_store_t *store)
 	  profilerNameStore(store),
 	  appLaunchUUID_(QUuid::createUuid())
 {
+	installNativeEventFilter(new OBS::NativeEventFilter);
+
 	/* fix float handling */
 #if defined(Q_OS_UNIX)
 	if (!setlocale(LC_NUMERIC, "C"))
@@ -879,9 +881,14 @@ OBSApp::OBSApp(int &argc, char **argv, profiler_name_store_t *store)
 	socketpair(AF_UNIX, SOCK_STREAM, 0, sigintFd);
 	snInt = new QSocketNotifier(sigintFd[1], QSocketNotifier::Read, this);
 	connect(snInt, &QSocketNotifier::activated, this, &OBSApp::ProcessSigInt);
-#else
-	connect(qApp, &QGuiApplication::commitDataRequest, this, &OBSApp::commitData);
+
+	/* Handle SIGTERM */
+	socketpair(AF_UNIX, SOCK_STREAM, 0, sigtermFd);
+	snTerm = new QSocketNotifier(sigtermFd[1], QSocketNotifier::Read, this);
+	connect(snTerm, &QSocketNotifier::activated, this, &OBSApp::ProcessSigTerm);
 #endif
+	connect(qApp, &QGuiApplication::commitDataRequest, this, &OBSApp::commitData, Qt::DirectConnection);
+
 	if (multi) {
 		crashHandler_ = std::make_unique<OBS::CrashHandler>();
 	} else {
@@ -1229,7 +1236,20 @@ bool OBSApp::OBSInit()
 	mainWindow = new OBSBasic();
 
 	mainWindow->setAttribute(Qt::WA_DeleteOnClose, true);
-	connect(mainWindow, &OBSBasic::destroyed, this, &OBSApp::quit);
+
+	connect(QApplication::instance(), &QApplication::aboutToQuit, this, [this]() {
+		crashHandler_->applicationShutdownHandler();
+
+		/* Ensure OBSMainWindow gets closed */
+		if (mainWindow) {
+			mainWindow->close();
+			delete mainWindow;
+		}
+
+		if (libobs_initialized) {
+			applicationShutdown();
+		}
+	});
 
 	mainWindow->OBSInit();
 
@@ -1748,6 +1768,14 @@ void OBSApp::SigIntSignalHandler(int s)
 	char a = 1;
 	send(sigintFd[0], &a, sizeof(a), 0);
 }
+
+void OBSApp::SigTermSignalHandler(int s)
+{
+	UNUSED_PARAMETER(s);
+
+	char a = 1;
+	send(sigtermFd[0], &a, sizeof(a), 0);
+}
 #endif
 
 void OBSApp::ProcessSigInt(void)
@@ -1759,20 +1787,39 @@ void OBSApp::ProcessSigInt(void)
 	recv(sigintFd[1], &tmp, sizeof(tmp), 0);
 
 	OBSBasic *main = OBSBasic::Get();
-	if (main)
+	if (main) {
+		main->saveAll();
 		main->close();
+	}
+#endif
+}
+
+void OBSApp::ProcessSigTerm(void)
+{
+#ifndef _WIN32
+	char tmp;
+	recv(sigtermFd[1], &tmp, sizeof(tmp), 0);
+
+	OBSBasic *main = OBSBasic::Get();
+	if (main) {
+		main->saveAll();
+	}
+
+	quit();
 #endif
 }
 
-#ifdef _WIN32
 void OBSApp::commitData(QSessionManager &manager)
 {
-	if (auto main = App()->GetMainWindow()) {
-		QMetaObject::invokeMethod(main, "close", Qt::QueuedConnection);
-		manager.cancel();
+	OBSBasic *main = OBSBasic::Get();
+	if (main) {
+		main->saveAll();
+
+		if (manager.allowsInteraction() && main->shouldPromptForClose()) {
+			manager.cancel();
+		}
 	}
 }
-#endif
 
 void OBSApp::applicationShutdown() noexcept
 {
@@ -1784,6 +1831,10 @@ void OBSApp::applicationShutdown() noexcept
 	delete snInt;
 	close(sigintFd[0]);
 	close(sigintFd[1]);
+
+	delete snTerm;
+	close(sigtermFd[0]);
+	close(sigtermFd[1]);
 #endif
 
 #ifdef __APPLE__

+ 10 - 3
frontend/OBSApp.hpp

@@ -18,6 +18,7 @@
 #pragma once
 
 #include <utility/OBSTheme.hpp>
+#include <utility/NativeEventFilter.hpp>
 #include <widgets/OBSMainWindow.hpp>
 
 #include <obs-frontend-api.h>
@@ -25,6 +26,7 @@
 #include <util/profiler.hpp>
 #include <util/util.hpp>
 
+#include <QAbstractNativeEventFilter>
 #include <QApplication>
 #include <QPalette>
 #include <QPointer>
@@ -61,6 +63,8 @@ struct UpdateBranch {
 class OBSApp : public QApplication {
 	Q_OBJECT
 
+	friend class OBS::NativeEventFilter;
+
 private:
 	QUuid appLaunchUUID_;
 	std::unique_ptr<OBS::CrashHandler> crashHandler_;
@@ -117,12 +121,13 @@ private:
 #ifndef _WIN32
 	static int sigintFd[2];
 	QSocketNotifier *snInt = nullptr;
-#else
-private slots:
-	void commitData(QSessionManager &manager);
+
+	static int sigtermFd[2];
+	QSocketNotifier *snTerm = nullptr;
 #endif
 
 private slots:
+	void commitData(QSessionManager &manager);
 	void addLogLine(int logLevel, const QString &message);
 	void themeFileChanged(const QString &);
 	void applicationShutdown() noexcept;
@@ -212,6 +217,7 @@ public:
 	inline void PopUITranslation() { translatorHooks.pop_front(); }
 #ifndef _WIN32
 	static void SigIntSignalHandler(int);
+	static void SigTermSignalHandler(int);
 #endif
 
 	void loadAppModules(struct obs_module_failure_info &mfi);
@@ -222,6 +228,7 @@ public:
 public slots:
 	void Exec(VoidFunc func);
 	void ProcessSigInt();
+	void ProcessSigTerm();
 
 signals:
 	void logLineAdded(int logLevel, const QString &message);

+ 5 - 1
frontend/cmake/os-freebsd.cmake

@@ -1,6 +1,10 @@
 target_sources(
   obs-studio
-  PRIVATE utility/platform-x11.cpp utility/system-info-posix.cpp utility/CrashHandler_FreeBSD.cpp
+  PRIVATE
+    utility/NativeEventFilter.cpp
+    utility/platform-x11.cpp
+    utility/system-info-posix.cpp
+    utility/CrashHandler_FreeBSD.cpp
 )
 target_compile_definitions(obs-studio PRIVATE OBS_INSTALL_PREFIX="${OBS_INSTALL_PREFIX}")
 target_link_libraries(obs-studio PRIVATE Qt::GuiPrivate Qt::DBus procstat)

+ 8 - 1
frontend/cmake/os-linux.cmake

@@ -1,4 +1,11 @@
-target_sources(obs-studio PRIVATE utility/platform-x11.cpp utility/system-info-posix.cpp utility/CrashHandler_Linux.cpp)
+target_sources(
+  obs-studio
+  PRIVATE
+    utility/NativeEventFilter.cpp
+    utility/platform-x11.cpp
+    utility/system-info-posix.cpp
+    utility/CrashHandler_Linux.cpp
+)
 target_compile_definitions(
   obs-studio
   PRIVATE OBS_INSTALL_PREFIX="${OBS_INSTALL_PREFIX}" $<$<BOOL:${ENABLE_PORTABLE_CONFIG}>:ENABLE_PORTABLE_CONFIG>

+ 1 - 0
frontend/cmake/os-macos.cmake

@@ -7,6 +7,7 @@ target_sources(
     dialogs/OBSPermissions.hpp
     forms/OBSPermissions.ui
     utility/CrashHandler_MacOS.mm
+    utility/NativeEventFilter.cpp
     utility/platform-osx.mm
     utility/system-info-macos.mm
 )

+ 1 - 0
frontend/cmake/os-windows.cmake

@@ -29,6 +29,7 @@ target_sources(
     utility/crypto-helpers.hpp
     utility/models/branches.hpp
     utility/models/whatsnew.hpp
+    utility/NativeEventFilter_Windows.cpp
     utility/platform-windows.cpp
     utility/system-info-windows.cpp
     utility/update-helpers.cpp

+ 1 - 0
frontend/cmake/ui-utility.cmake

@@ -33,6 +33,7 @@ target_sources(
     utility/MultitrackVideoError.hpp
     utility/MultitrackVideoOutput.cpp
     utility/MultitrackVideoOutput.hpp
+    utility/NativeEventFilter.hpp
     utility/obf.c
     utility/obf.h
     utility/OBSCanvas.cpp

+ 1 - 1
frontend/docks/OBSDock.cpp

@@ -28,7 +28,7 @@ void OBSDock::closeEvent(QCloseEvent *event)
 	};
 
 	bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutClosingDocks");
-	if (!OBSBasic::Get()->Closing() && !warned) {
+	if (!OBSBasic::Get()->isClosing() && !warned) {
 		QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox));
 	}
 

+ 22 - 5
frontend/obs-main.cpp

@@ -858,13 +858,22 @@ int main(int argc, char *argv[])
 #ifndef _WIN32
 	signal(SIGPIPE, SIG_IGN);
 
-	struct sigaction sig_handler;
+	struct sigaction sigint_handler;
 
-	sig_handler.sa_handler = OBSApp::SigIntSignalHandler;
-	sigemptyset(&sig_handler.sa_mask);
-	sig_handler.sa_flags = 0;
+	sigint_handler.sa_handler = OBSApp::SigIntSignalHandler;
+	sigemptyset(&sigint_handler.sa_mask);
+	sigint_handler.sa_flags = 0;
 
-	sigaction(SIGINT, &sig_handler, NULL);
+	sigaction(SIGINT, &sigint_handler, NULL);
+
+	struct sigaction sigterm_handler;
+
+	sigterm_handler.sa_handler = OBSApp::SigTermSignalHandler;
+	sigemptyset(&sigterm_handler.sa_mask);
+	sigterm_handler.sa_flags = 0;
+
+	sigaction(SIGTERM, &sigterm_handler, NULL);
+	sigaction(SIGHUP, &sigterm_handler, NULL);
 
 	/* Block SIGPIPE in all threads, this can happen if a thread calls write on
 	a closed pipe. */
@@ -890,6 +899,14 @@ int main(int argc, char *argv[])
 	load_debug_privilege();
 	base_set_crash_handler(main_crash_handler, nullptr);
 
+	/* Shutdown priority value is a range from 0 - 4FF with higher values getting first priority.
+	 * 000 - 0FF and 400 - 4FF are reserved system ranges.
+	 * Processes start at shutdown level 0x280 by default.
+	 * We set the main OBS application to a higher priority to ensure it tries to close before
+	 * any subprocesses such as CEF.
+	 */
+	SetProcessShutdownParameters(0x300, SHUTDOWN_NORETRY);
+
 	const HMODULE hRtwq = LoadLibrary(L"RTWorkQ.dll");
 	if (hRtwq) {
 		typedef HRESULT(STDAPICALLTYPE * PFN_RtwqStartup)();

+ 27 - 0
frontend/utility/NativeEventFilter.cpp

@@ -0,0 +1,27 @@
+/******************************************************************************
+    Copyright (C) 2025 by Taylor Giampaolo <[email protected]>
+
+    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, see <http://www.gnu.org/licenses/>.
+******************************************************************************/
+
+#include "NativeEventFilter.hpp"
+
+namespace OBS {
+
+bool NativeEventFilter::nativeEventFilter(const QByteArray &, void *, qintptr *)
+{
+	// Stub file for operating systems that do not need nativeEventFilter
+	return false;
+}
+} // namespace OBS

+ 29 - 0
frontend/utility/NativeEventFilter.hpp

@@ -0,0 +1,29 @@
+/******************************************************************************
+    Copyright (C) 2025 by Taylor Giampaolo <[email protected]>
+
+    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, see <http://www.gnu.org/licenses/>.
+******************************************************************************/
+
+#pragma once
+
+#include <QAbstractNativeEventFilter>
+
+namespace OBS {
+
+class NativeEventFilter : public QAbstractNativeEventFilter {
+
+public:
+	bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result);
+};
+} // namespace OBS

+ 70 - 0
frontend/utility/NativeEventFilter_Windows.cpp

@@ -0,0 +1,70 @@
+/******************************************************************************
+    Copyright (C) 2025 by Taylor Giampaolo <[email protected]>
+
+    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, see <http://www.gnu.org/licenses/>.
+******************************************************************************/
+
+#include "NativeEventFilter.hpp"
+
+#include <widgets/OBSBasic.hpp>
+
+#include <sstream>
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+
+namespace OBS {
+
+bool NativeEventFilter::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result)
+{
+	if (eventType == "windows_generic_MSG") {
+		MSG *msg = static_cast<MSG *>(message);
+
+		OBSBasic *main = OBSBasic::Get();
+		if (!main) {
+			return false;
+		}
+
+		switch (msg->message) {
+		case WM_QUERYENDSESSION:
+			main->saveAll();
+			if (msg->lParam == ENDSESSION_CRITICAL) {
+				break;
+			}
+
+			if (main->shouldPromptForClose()) {
+				*result = FALSE;
+				return true;
+			}
+
+			return false;
+		case WM_ENDSESSION:
+			if (msg->wParam == TRUE) {
+				// Session is ending, start closing the main window now with no checks or prompts.
+				main->closeWindow();
+			} else {
+				/* Session is no longer ending. If OBS is still open, odds are it is what held
+				 * up the session end due to its higher than default priority. We call the
+				 * close method to trigger the confirmation window flow. We do this after the fact
+				 * to avoid blocking the main window event loop prior to this message.
+				 * Otherwise, OBS is already gone and invoking this does nothing. */
+				main->close();
+			}
+
+			return true;
+		}
+	}
+
+	return false;
+}
+} // namespace OBS

+ 185 - 110
frontend/widgets/OBSBasic.cpp

@@ -558,6 +558,7 @@ OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new
 	connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); });
 
 	connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); });
+	connect(App(), &OBSApp::aboutToQuit, this, &OBSBasic::closeWindow);
 
 	QActionGroup *actionGroup = new QActionGroup(this);
 	actionGroup->addAction(ui->actionSceneListMode);
@@ -1372,8 +1373,8 @@ void OBSBasic::OnFirstLoad()
 
 OBSBasic::~OBSBasic()
 {
-	if (!handledShutdown) {
-		applicationShutdown();
+	if (!isClosing()) {
+		closeWindow();
 	}
 }
 
@@ -1457,19 +1458,6 @@ void OBSBasic::applicationShutdown() noexcept
 	 * expect or want it to. */
 	QApplication::sendPostedEvents(nullptr);
 
-	config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER);
-	config_save_safe(App()->GetAppConfig(), "tmp", nullptr);
-
-	config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled);
-	config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked());
-	config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode);
-	config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode);
-	config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode);
-	config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode());
-	config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked());
-	config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked());
-	config_save_safe(App()->GetUserConfig(), "tmp", nullptr);
-
 #ifdef BROWSER_AVAILABLE
 	DestroyPanelCookieManager();
 	delete cef;
@@ -1658,127 +1646,61 @@ bool OBSBasic::ResetAudio()
 	return obs_reset_audio2(&ai);
 }
 
-void OBSBasic::closeEvent(QCloseEvent *event)
+void OBSBasic::close()
 {
-	/* Wait for multitrack video stream to start/finish processing in the background */
-	if (setupStreamingGuard.valid() &&
-	    setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) {
-		QTimer::singleShot(1000, this, &OBSBasic::close);
-		event->ignore();
+	if (isClosePromptOpen() || isClosing()) {
 		return;
 	}
 
-	/* Do not close window if inside of a temporary event loop because we
-	 * could be inside of an Auth::LoadUI call.  Keep trying once per
-	 * second until we've exit any known sub-loops. */
-	if (os_atomic_load_long(&insideEventLoop) != 0) {
-		QTimer::singleShot(1000, this, &OBSBasic::close);
-		event->ignore();
-		return;
-	}
+	OBSMainWindow::close();
+}
 
-#ifdef YOUTUBE_ENABLED
-	/* Also don't close the window if the youtube stream check is active */
-	if (youtubeStreamCheckThread) {
-		QTimer::singleShot(1000, this, &OBSBasic::close);
-		event->ignore();
+void OBSBasic::closeEvent(QCloseEvent *event)
+{
+	if (isClosePromptOpen() || isClosing()) {
 		return;
 	}
-#endif
 
-	if (isVisible())
+	if (isVisible()) {
 		config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry",
 				  saveGeometry().toBase64().constData());
+	}
 
-	bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit");
-
-	if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) {
-		SetShowing(true);
-
-		QMessageBox::StandardButton button =
-			OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text"));
+	if (!isReadyToClose()) {
+		event->ignore();
 
-		if (button == QMessageBox::No) {
-			event->ignore();
-			restart = false;
-			return;
-		}
+		QTimer::singleShot(1000, this, &OBSBasic::close);
+		return;
 	}
 
 	if (remux && !remux->close()) {
 		event->ignore();
 		restart = false;
-		return;
-	}
 
-	QWidget::closeEvent(event);
-	if (!event->isAccepted())
 		return;
-
-	blog(LOG_INFO, SHUTDOWN_SEPARATOR);
-
-	closing = true;
-
-	/* While closing, a resize event to OBSQTDisplay could be triggered.
-	 * The graphics thread on macOS dispatches a lambda function to be
-	 * executed asynchronously in the main thread. However, the display is
-	 * sometimes deleted before the lambda function is actually executed.
-	 * To avoid such a case, destroy displays earlier than others such as
-	 * deleting browser docks. */
-	ui->preview->DestroyDisplay();
-	if (program)
-		program->DestroyDisplay();
-
-	if (outputHandler->VirtualCamActive())
-		outputHandler->StopVirtualCam();
-
-	if (introCheckThread)
-		introCheckThread->wait();
-	if (whatsNewInitThread)
-		whatsNewInitThread->wait();
-	if (updateCheckThread)
-		updateCheckThread->wait();
-	if (logUploadThread)
-		logUploadThread->wait();
-	if (devicePropertiesThread && devicePropertiesThread->isRunning()) {
-		devicePropertiesThread->wait();
-		devicePropertiesThread.reset();
 	}
 
-	QApplication::sendPostedEvents(nullptr);
-
-	signalHandlers.clear();
-
-	Auth::Save();
-	SaveProjectNow();
-	auth.reset();
-
-	delete extraBrowsers;
-
-	config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData());
-
-#ifdef BROWSER_AVAILABLE
-	if (cef)
-		SaveExtraBrowserDocks();
-
-	ClearExtraBrowserDocks();
-#endif
-
-	OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN);
+	if (shouldPromptForClose()) {
+		event->ignore();
+		restart = false;
 
-	disableSaving++;
+		if (!isClosePromptOpen()) {
+			bool shouldClose = promptToClose();
 
-	/* Clear all scene data (dialogs, widgets, widget sub-items, scenes,
-	 * sources, etc) so that all references are released before shutdown */
-	ClearSceneData();
+			if (shouldClose) {
+				closeWindow();
+			}
+		}
 
-	OnEvent(OBS_FRONTEND_EVENT_EXIT);
+		return;
+	}
 
-	// Destroys the frontend API so plugins can't continue calling it
-	obs_frontend_set_callbacks_internal(nullptr);
-	api = nullptr;
+	QWidget::closeEvent(event);
+	if (!event->isAccepted()) {
+		return;
+	}
 
-	QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection);
+	closeWindow();
 }
 
 bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *)
@@ -1899,6 +1821,159 @@ config_t *OBSBasic::Config() const
 	return activeConfiguration;
 }
 
+void OBSBasic::saveAll()
+{
+	if (isVisible()) {
+		config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry",
+				  saveGeometry().toBase64().constData());
+	}
+
+	Auth::Save();
+	SaveProjectNow();
+
+	config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData());
+
+#ifdef BROWSER_AVAILABLE
+	if (cef) {
+		SaveExtraBrowserDocks();
+	}
+#endif
+
+	config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER);
+	config_save_safe(App()->GetAppConfig(), "tmp", nullptr);
+
+	config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled);
+	config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked());
+	config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode);
+	config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode);
+	config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode);
+	config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode());
+	config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked());
+	config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked());
+	config_save_safe(App()->GetUserConfig(), "tmp", nullptr);
+}
+
+bool OBSBasic::isReadyToClose()
+{
+	/* Wait for multitrack video stream to start/finish processing in the background */
+	if (setupStreamingGuard.valid() &&
+	    setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) {
+		return false;
+	}
+
+	/* Do not close window if inside of a temporary event loop because we
+	 * could be inside of an Auth::LoadUI call.  Keep trying once per
+	 * second until we've exit any known sub-loops. */
+	if (os_atomic_load_long(&insideEventLoop) != 0) {
+		return false;
+	}
+
+#ifdef YOUTUBE_ENABLED
+	/* Also don't close the window if the youtube stream check is active */
+	if (youtubeStreamCheckThread) {
+		return false;
+	}
+#endif
+
+	return true;
+}
+
+bool OBSBasic::shouldPromptForClose()
+{
+	bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit");
+	if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) {
+		return true;
+	}
+
+	return false;
+}
+
+bool OBSBasic::promptToClose()
+{
+	isClosePromptOpen_ = true;
+
+	SetShowing(true);
+	QMessageBox::StandardButton button =
+		OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text"));
+
+	if (button == QMessageBox::No) {
+		isClosePromptOpen_ = false;
+		return false;
+	}
+
+	isClosePromptOpen_ = false;
+	return true;
+}
+
+void OBSBasic::closeWindow()
+{
+	if (isClosing()) {
+		return;
+	}
+
+	blog(LOG_INFO, SHUTDOWN_SEPARATOR);
+
+	isClosing_ = true;
+
+	/* While closing, a resize event to OBSQTDisplay could be triggered.
+	 * The graphics thread on macOS dispatches a lambda function to be
+	 * executed asynchronously in the main thread. However, the display is
+	 * sometimes deleted before the lambda function is actually executed.
+	 * To avoid such a case, destroy displays earlier than others such as
+	 * deleting browser docks. */
+	ui->preview->DestroyDisplay();
+	if (program)
+		program->DestroyDisplay();
+
+	if (outputHandler->VirtualCamActive())
+		outputHandler->StopVirtualCam();
+
+	if (introCheckThread)
+		introCheckThread->wait();
+	if (whatsNewInitThread)
+		whatsNewInitThread->wait();
+	if (updateCheckThread)
+		updateCheckThread->wait();
+	if (logUploadThread)
+		logUploadThread->wait();
+	if (devicePropertiesThread && devicePropertiesThread->isRunning()) {
+		devicePropertiesThread->wait();
+		devicePropertiesThread.reset();
+	}
+
+	QApplication::sendPostedEvents(nullptr);
+
+	signalHandlers.clear();
+	delete extraBrowsers;
+
+	saveAll();
+
+	auth.reset();
+
+#ifdef BROWSER_AVAILABLE
+	ClearExtraBrowserDocks();
+#endif
+
+	OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN);
+
+	disableSaving++;
+
+	/* Clear all scene data (dialogs, widgets, widget sub-items, scenes,
+	 * sources, etc) so that all references are released before shutdown */
+	ClearSceneData();
+
+	OnEvent(OBS_FRONTEND_EVENT_EXIT);
+
+	// Destroys the frontend API so plugins can't continue calling it
+	obs_frontend_set_callbacks_internal(nullptr);
+	api = nullptr;
+
+	applicationShutdown();
+	deleteLater();
+
+	QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection);
+}
+
 void OBSBasic::UpdateEditMenu()
 {
 	QModelIndexList items = GetAllSelectedSourceItems();

+ 11 - 2
frontend/widgets/OBSBasic.hpp

@@ -259,7 +259,8 @@ private:
 	std::vector<OBSSignal> signalHandlers;
 
 	bool loaded = false;
-	bool closing = false;
+	bool isClosing_ = false;
+	bool isClosePromptOpen_ = false;
 	bool handledShutdown = false;
 
 	// TODO: Remove, orphaned variable
@@ -300,6 +301,7 @@ private:
 	void LoadProject();
 
 public slots:
+	void close();
 	void UpdatePatronJson(const QString &text, const QString &error);
 	void UpdateEditMenu();
 	void applicationShutdown() noexcept;
@@ -325,9 +327,16 @@ public:
 
 	void SetDisplayAffinity(QWindow *window);
 
-	inline bool Closing() { return closing; }
+	void saveAll();
+	bool shouldPromptForClose();
+	inline bool isClosing() { return isClosing_; }
+	inline bool isClosePromptOpen() { return isClosePromptOpen_; }
+	void closeWindow();
 
 protected:
+	bool isReadyToClose();
+	bool promptToClose();
+
 	virtual void closeEvent(QCloseEvent *event) override;
 	virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override;
 	virtual void changeEvent(QEvent *event) override;

+ 4 - 2
frontend/widgets/OBSBasic_Updater.cpp

@@ -63,8 +63,9 @@ template<typename OBSRef> struct SignalContainer {
 void OBSBasic::ReceivedIntroJson(const QString &text)
 {
 #ifdef WHATSNEW_ENABLED
-	if (closing)
+	if (isClosing()) {
 		return;
+	}
 
 	WhatsNewList items;
 	try {
@@ -153,8 +154,9 @@ void OBSBasic::ReceivedIntroJson(const QString &text)
 void OBSBasic::ShowWhatsNew(const QString &url)
 {
 #ifdef BROWSER_AVAILABLE
-	if (closing)
+	if (isClosing()) {
 		return;
+	}
 
 	if (obsWhatsNew) {
 		obsWhatsNew->close();