Jelajahi Sumber

frontend: Update crash handling and log upload functionality

Updates include:

* Use of CrashHandler to provide automatic uploads of the most recent
crash log if an unclean shutdown was detected and it has not been
uploaded yet.
* Detection and handling of unclean shutdowns is delegated entirely to
the CrashHandler class
* Use of OBSLogReply has been replaced with the LogUploadDialog, which
asks for confirmation before new uploads of log files (confirmation is
skipped for files with available upload URLs already - only available
for crash logs with this change)

Architectural changes:
* OBSApp is the layer responsible for application launch and shutdown
states, as well as crash logs and application logs
* The actual handling is delegated to purpose-made classes which OBSApp
owns instances of
* OBSBasic in turn refers to OBSApp for all this functionality, and can
subscribe/connect to appropriate events exposed by OBSApp to this
purpose
* Implementation details (like the existence of the CrashHandler class)
are not exposed to OBSBasic or the LogUploadDialog

The amount of changes for normal log file upload have been purposefully
limited. A proper refactoring of the application log file handling will
move this code out of OBSBasic as well.
PatTheMav 6 bulan lalu
induk
melakukan
de997b1e2f

+ 154 - 30
frontend/OBSApp.cpp

@@ -18,6 +18,8 @@
 #include "OBSApp.hpp"
 
 #include <components/Multiview.hpp>
+#include <dialogs/LogUploadDialog.hpp>
+#include <utility/CrashHandler.hpp>
 #include <utility/OBSEventFilter.hpp>
 #if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER)
 #include <utility/models/branches.hpp>
@@ -32,6 +34,8 @@
 #endif
 #include <qt-wrappers.hpp>
 
+#include <QCheckBox>
+#include <QDesktopServices>
 #if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER)
 #include <QFile>
 #endif
@@ -42,6 +46,8 @@
 #include <QSocketNotifier>
 #endif
 
+#include <chrono>
+
 #ifdef _WIN32
 #include <sstream>
 #define WIN32_LEAN_AND_MEAN
@@ -61,6 +67,7 @@ string lastCrashLogFile;
 
 extern bool portable_mode;
 extern bool safe_mode;
+extern bool multi;
 extern bool disable_3p_plugins;
 extern bool opt_disable_updater;
 extern bool opt_disable_missing_files_check;
@@ -79,6 +86,66 @@ extern "C" __declspec(dllexport) DWORD NvOptimusEnablement = 1;
 extern "C" __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
 #endif
 
+namespace {
+
+typedef struct UncleanLaunchAction {
+	bool useSafeMode = false;
+	bool sendCrashReport = false;
+} UncleanLaunchAction;
+
+UncleanLaunchAction handleUncleanShutdown(bool enableCrashUpload)
+{
+	UncleanLaunchAction launchAction;
+
+	blog(LOG_WARNING, "Crash or unclean shutdown detected");
+
+	QMessageBox crashWarning;
+
+	crashWarning.setIcon(QMessageBox::Warning);
+#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
+	crashWarning.setOption(QMessageBox::Option::DontUseNativeDialog);
+#endif
+	crashWarning.setWindowTitle(QTStr("CrashHandling.Dialog.Title"));
+	crashWarning.setText(QTStr("CrashHandling.Labels.Text"));
+
+	if (enableCrashUpload) {
+		crashWarning.setInformativeText(QTStr("CrashHandling.Labels.PrivacyNotice"));
+
+		QCheckBox *sendCrashReportCheckbox = new QCheckBox(QTStr("CrashHandling.Checkbox.SendReport"));
+		crashWarning.setCheckBox(sendCrashReportCheckbox);
+	}
+
+	QPushButton *launchSafeButton =
+		crashWarning.addButton(QTStr("CrashHandling.Buttons.LaunchSafe"), QMessageBox::AcceptRole);
+	QPushButton *launchNormalButton =
+		crashWarning.addButton(QTStr("CrashHandling.Buttons.LaunchNormal"), QMessageBox::RejectRole);
+
+	crashWarning.setDefaultButton(launchNormalButton);
+
+	crashWarning.exec();
+
+	bool useSafeMode = crashWarning.clickedButton() == launchSafeButton;
+
+	if (useSafeMode) {
+		launchAction.useSafeMode = true;
+
+		blog(LOG_INFO, "[Safe Mode] Safe mode launch selected, loading third-party plugins is disabled");
+	} else {
+		blog(LOG_WARNING, "[Safe Mode] Normal launch selected, loading third-party plugins is enabled");
+	}
+
+	bool sendCrashReport = (enableCrashUpload) ? crashWarning.checkBox()->isChecked() : false;
+
+	if (sendCrashReport) {
+		launchAction.sendCrashReport = true;
+
+		blog(LOG_INFO, "User selected to send crash report");
+	}
+
+	return launchAction;
+}
+} // namespace
+
 QObject *CreateShortcutFilter()
 {
 	return new OBSEventFilter([](QObject *obj, QEvent *event) {
@@ -771,7 +838,8 @@ std::vector<UpdateBranch> OBSApp::GetBranches()
 
 OBSApp::OBSApp(int &argc, char **argv, profiler_name_store_t *store)
 	: QApplication(argc, argv),
-	  profilerNameStore(store)
+	  profilerNameStore(store),
+	  appLaunchUUID_(QUuid::createUuid())
 {
 	/* fix float handling */
 #if defined(Q_OS_UNIX)
@@ -787,6 +855,11 @@ OBSApp::OBSApp(int &argc, char **argv, profiler_name_store_t *store)
 #else
 	connect(qApp, &QGuiApplication::commitDataRequest, this, &OBSApp::commitData);
 #endif
+	if (multi) {
+		crashHandler_ = std::make_unique<OBS::CrashHandler>();
+	} else {
+		crashHandler_ = std::make_unique<OBS::CrashHandler>(appLaunchUUID_);
+	}
 
 	sleepInhibitor = os_inhibit_sleep_create("OBS Video/audio");
 
@@ -799,29 +872,10 @@ OBSApp::OBSApp(int &argc, char **argv, profiler_name_store_t *store)
 
 OBSApp::~OBSApp()
 {
-#ifdef _WIN32
-	bool disableAudioDucking = config_get_bool(appConfig, "Audio", "DisableAudioDucking");
-	if (disableAudioDucking)
-		DisableAudioDucking(false);
-#else
-	delete snInt;
-	close(sigintFd[0]);
-	close(sigintFd[1]);
-#endif
-
-#ifdef __APPLE__
-	bool vsyncDisabled = config_get_bool(appConfig, "Video", "DisableOSXVSync");
-	bool resetVSync = config_get_bool(appConfig, "Video", "ResetOSXVSyncOnExit");
-	if (vsyncDisabled && resetVSync)
-		EnableOSXVSync(true);
-#endif
-
-	os_inhibit_sleep_set_active(sleepInhibitor, false);
-	os_inhibit_sleep_destroy(sleepInhibitor);
-
-	if (libobs_initialized)
-		obs_shutdown();
-}
+	if (libobs_initialized) {
+		applicationShutdown();
+	}
+};
 
 static void move_basic_to_profiles(void)
 {
@@ -982,6 +1036,22 @@ void OBSApp::AppInit()
 		throw "Failed to create profile directories";
 }
 
+void OBSApp::checkForUncleanShutdown()
+{
+	bool hasUncleanShutdown = crashHandler_->hasUncleanShutdown();
+	bool hasNewCrashLog = crashHandler_->hasNewCrashLog();
+
+	if (hasUncleanShutdown) {
+		UncleanLaunchAction launchAction = handleUncleanShutdown(hasNewCrashLog);
+
+		safe_mode = launchAction.useSafeMode;
+
+		if (launchAction.sendCrashReport) {
+			crashHandler_->uploadLastCrashLog();
+		}
+	}
+}
+
 const char *OBSApp::GetRenderModule() const
 {
 	const char *renderer = config_get_string(appConfig, "Video", "Renderer");
@@ -1130,6 +1200,15 @@ bool OBSApp::OBSInit()
 	connect(this, &QGuiApplication::applicationStateChanged,
 		[this](Qt::ApplicationState state) { ResetHotkeyState(state == Qt::ApplicationActive); });
 	ResetHotkeyState(applicationState() == Qt::ApplicationActive);
+
+	connect(crashHandler_.get(), &OBS::CrashHandler::crashLogUploadFailed, this,
+		[this](const QString &errorMessage) {
+			emit this->logUploadFailed(OBS::LogFileType::CrashLog, errorMessage);
+		});
+
+	connect(crashHandler_.get(), &OBS::CrashHandler::crashLogUploadFinished, this,
+		[this](const QString &fileUrl) { emit this->logUploadFinished(OBS::LogFileType::CrashLog, fileUrl); });
+
 	return true;
 }
 
@@ -1208,30 +1287,47 @@ const char *OBSApp::GetCurrentLog() const
 	return currentLogFile.c_str();
 }
 
-const char *OBSApp::GetLastCrashLog() const
+void OBSApp::openCrashLogDirectory() const
 {
-	return lastCrashLogFile.c_str();
+	std::filesystem::path crashLogDirectory = crashHandler_->getCrashLogDirectory();
+
+	if (crashLogDirectory.empty()) {
+		return;
+	}
+
+	QString crashLogDirectoryString = QString::fromStdString(crashLogDirectory.u8string());
+
+	QUrl crashLogDirectoryURL = QUrl::fromLocalFile(crashLogDirectoryString);
+	QDesktopServices::openUrl(crashLogDirectoryURL);
 }
 
 void OBSApp::uploadLastAppLog() const
 {
-	return;
+	OBSBasic *basicWindow = static_cast<OBSBasic *>(GetMainWindow());
+
+	basicWindow->UploadLog("obs-studio/logs", GetLastLog(), OBS::LogFileType::LastAppLog);
 }
 
 void OBSApp::uploadCurrentAppLog() const
 {
-	return;
+	OBSBasic *basicWindow = static_cast<OBSBasic *>(GetMainWindow());
+
+	basicWindow->UploadLog("obs-studio/logs", GetCurrentLog(), OBS::LogFileType::CurrentAppLog);
 }
 
 void OBSApp::uploadLastCrashLog()
 {
-	return;
+	crashHandler_->uploadLastCrashLog();
 }
 
 OBS::LogFileState OBSApp::getLogFileState(OBS::LogFileType type) const
 {
 	switch (type) {
-	case OBS::LogFileType::CrashLog:
+	case OBS::LogFileType::CrashLog: {
+		bool hasNewCrashLog = crashHandler_->hasNewCrashLog();
+
+		return (hasNewCrashLog) ? OBS::LogFileState::New : OBS::LogFileState::Uploaded;
+	}
 	case OBS::LogFileType::CurrentAppLog:
 	case OBS::LogFileType::LastAppLog:
 		return OBS::LogFileState::New;
@@ -1633,3 +1729,31 @@ void OBSApp::commitData(QSessionManager &manager)
 	}
 }
 #endif
+
+void OBSApp::applicationShutdown() noexcept
+{
+#ifdef _WIN32
+	bool disableAudioDucking = config_get_bool(appConfig, "Audio", "DisableAudioDucking");
+	if (disableAudioDucking)
+		DisableAudioDucking(false);
+#else
+	delete snInt;
+	close(sigintFd[0]);
+	close(sigintFd[1]);
+#endif
+
+#ifdef __APPLE__
+	bool vsyncDisabled = config_get_bool(appConfig, "Video", "DisableOSXVSync");
+	bool resetVSync = config_get_bool(appConfig, "Video", "ResetOSXVSyncOnExit");
+	if (vsyncDisabled && resetVSync)
+		EnableOSXVSync(true);
+#endif
+
+	os_inhibit_sleep_set_active(sleepInhibitor, false);
+	os_inhibit_sleep_destroy(sleepInhibitor);
+
+	if (libobs_initialized) {
+		obs_shutdown();
+		libobs_initialized = false;
+	}
+}

+ 9 - 1
frontend/OBSApp.hpp

@@ -28,6 +28,7 @@
 #include <QApplication>
 #include <QPalette>
 #include <QPointer>
+#include <QUuid>
 
 #include <deque>
 #include <functional>
@@ -42,6 +43,8 @@ class QFileSystemWatcher;
 class QSocketNotifier;
 
 namespace OBS {
+class CrashHandler;
+
 enum class LogFileType { NoType, CurrentAppLog, LastAppLog, CrashLog };
 enum class LogFileState { NoState, New, Uploaded };
 } // namespace OBS
@@ -58,6 +61,9 @@ class OBSApp : public QApplication {
 	Q_OBJECT
 
 private:
+	QUuid appLaunchUUID_;
+	std::unique_ptr<OBS::CrashHandler> crashHandler_;
+
 	std::string locale;
 
 	ConfigFile appConfig;
@@ -114,12 +120,14 @@ private slots:
 
 private slots:
 	void themeFileChanged(const QString &);
+	void applicationShutdown() noexcept;
 
 public:
 	OBSApp(int &argc, char **argv, profiler_name_store_t *store);
 	~OBSApp();
 
 	void AppInit();
+	void checkForUncleanShutdown();
 	bool OBSInit();
 
 	void UpdateHotkeyFocusSetting(bool reset = true);
@@ -157,7 +165,7 @@ public:
 	const char *GetLastLog() const;
 	const char *GetCurrentLog() const;
 
-	const char *GetLastCrashLog() const;
+	void openCrashLogDirectory() const;
 	void uploadLastAppLog() const;
 	void uploadCurrentAppLog() const;
 	void uploadLastCrashLog();

+ 0 - 2
frontend/cmake/ui-dialogs.cmake

@@ -29,8 +29,6 @@ target_sources(
     dialogs/OBSBasicTransform.hpp
     dialogs/OBSBasicVCamConfig.cpp
     dialogs/OBSBasicVCamConfig.hpp
-    dialogs/OBSLogReply.cpp
-    dialogs/OBSLogReply.hpp
     dialogs/OBSLogViewer.cpp
     dialogs/OBSLogViewer.hpp
     dialogs/OBSMissingFiles.cpp

+ 2 - 4
frontend/cmake/ui-qt.cmake

@@ -30,7 +30,6 @@ target_sources(
     forms/AutoConfigTestPage.ui
     forms/AutoConfigVideoPage.ui
     forms/ColorSelect.ui
-    forms/obs.qrc
     forms/LogUploadDialog.ui
     forms/OBSAbout.ui
     forms/OBSAdvAudio.ui
@@ -44,10 +43,10 @@ target_sources(
     forms/OBSBasicVCamConfig.ui
     forms/OBSExtraBrowsers.ui
     forms/OBSImporter.ui
-    forms/OBSLogReply.ui
-    forms/OBSLogReply.ui
     forms/OBSMissingFiles.ui
     forms/OBSRemux.ui
+    forms/StatusBarWidget.ui
+    forms/obs.qrc
     forms/source-toolbar/browser-source-toolbar.ui
     forms/source-toolbar/color-source-toolbar.ui
     forms/source-toolbar/device-select-toolbar.ui
@@ -55,5 +54,4 @@ target_sources(
     forms/source-toolbar/image-source-toolbar.ui
     forms/source-toolbar/media-controls.ui
     forms/source-toolbar/text-source-toolbar.ui
-    forms/StatusBarWidget.ui
 )

+ 8 - 21
frontend/data/locale/en-US.ini

@@ -128,15 +128,6 @@ AlreadyRunning.Title="OBS is already running"
 AlreadyRunning.Text="OBS is already running! Unless you meant to do this, please shut down any existing instances of OBS before trying to run a new instance. If you have OBS set to minimize to the system tray, please check to see if it's still running there."
 AlreadyRunning.LaunchAnyway="Launch Anyway"
 
-# warning if auto Safe Mode has engaged
-AutoSafeMode.Title="Safe Mode"
-AutoSafeMode.Text="OBS did not shut down properly during your last session.\n\nWould you like to start in Safe Mode (third-party plugins, scripting, and WebSockets disabled)?"
-AutoSafeMode.LaunchSafe="Run in Safe Mode"
-AutoSafeMode.LaunchNormal="Run Normally"
-## Restart Option
-SafeMode.Restart="Do you want to restart OBS in Safe Mode (third-party plugins, scripting, and WebSockets disabled)?"
-SafeMode.RestartNormal="Do you want to restart OBS in Normal Mode?"
-
 # Prior Crash Detection
 CrashHandling.Dialog.Title="OBS Studio Crash Detected"
 CrashHandling.Labels.Text="OBS Studio did not properly shut down.\n\nRun in Safe Mode (third-party plugins, scripting, and WebSockets disabled)?"
@@ -147,6 +138,10 @@ CrashHandling.Buttons.LaunchNormal="Run in Normal Mode"
 CrashHandling.Errors.UploadJSONError="An error occurred while trying to upload the most recent crash log. Please try again later."
 CrashHandling.Errors.Title="Crash Log Upload Error"
 
+# Safe Mode Restart Option
+SafeMode.Restart="Do you want to restart OBS in Safe Mode (third-party plugins, scripting, and WebSockets disabled)?"
+SafeMode.RestartNormal="Do you want to restart OBS in Normal Mode?"
+
 ChromeOS.Title="Unsupported Platform"
 ChromeOS.Text="OBS appears to be running inside a ChromeOS container. This platform is unsupported."
 
@@ -460,24 +455,16 @@ Output.NoBroadcast.Text="You need to set up a broadcast before you can start str
 Output.BroadcastStartFailed="Failed to start broadcast"
 Output.BroadcastStopFailed="Failed to stop broadcast"
 
-# log upload dialog text and messages
-LogReturnDialog="Log Upload Successful"
-LogReturnDialog.Description="Your log file has been uploaded. You can now share the URL for debugging or support purposes."
-LogReturnDialog.Description.Crash="Your crash report has been uploaded. You can now share the URL for debugging purposes."
-LogReturnDialog.CopyURL="Copy URL"
-LogReturnDialog.AnalyzeURL="Analyze"
-LogReturnDialog.ErrorUploadingLog="Error uploading log file"
-
-# Log Upload Dialog for Application- and Crash-Logs
+# Log Upload Dialog for Application and Crash Logs
 LogUploadDialog.Title="OBS Studio Log File Upload"
 LogUploadDialog.Labels.PrivacyNotice="Please read the <a href='https://obsproject.com/privacy-policy'>Privacy Policy</a> and its section regarding file uploads before uploading any files."
-LogUploadDialog.Labels.Progress="Log upload in progress - please wait..."
+LogUploadDialog.Labels.Progress="Log upload in progress. Please wait..."
 LogUploadDialog.Labels.Description.AppLog="Your log file has been uploaded. You can now share the URL for debugging or support purposes."
 LogUploadDialog.Labels.Description.CrashLog="Your crash report has been uploaded. You can now share the URL for debugging purposes."
-LogUploadDialog.Buttons.ConfirmUpload="Continue Upload..."
+LogUploadDialog.Buttons.ConfirmUpload="Upload"
 LogUploadDialog.Buttons.CopyURL="Copy Log URL"
 LogUploadDialog.Buttons.AnalyzeURL="Analyze Log File"
-LogUploadDialog.Buttons.RetryButton="Retry Upload..."
+LogUploadDialog.Buttons.RetryButton="Retry"
 LogUploadDialog.Errors.Template="An error occurred while trying to upload the file:\n\n%1"
 LogUploadDialog.Errors.NoLogFile="No file to upload found or file was empty."
 

+ 0 - 56
frontend/dialogs/OBSLogReply.cpp

@@ -1,56 +0,0 @@
-/******************************************************************************
-    Copyright (C) 2023 by Lain Bailey <[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 "OBSLogReply.hpp"
-
-#include <OBSApp.hpp>
-
-#include <QClipboard>
-#include <QDesktopServices>
-#include <QUrlQuery>
-
-#include "moc_OBSLogReply.cpp"
-
-OBSLogReply::OBSLogReply(QWidget *parent, const QString &url, const bool crash)
-	: QDialog(parent),
-	  ui(new Ui::OBSLogReply)
-{
-	setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
-	ui->setupUi(this);
-	ui->urlEdit->setText(url);
-	if (crash) {
-		ui->analyzeURL->hide();
-		ui->description->setText(Str("LogReturnDialog.Description.Crash"));
-	}
-
-	installEventFilter(CreateShortcutFilter());
-}
-
-void OBSLogReply::on_copyURL_clicked()
-{
-	QClipboard *clipboard = QApplication::clipboard();
-	clipboard->setText(ui->urlEdit->text());
-}
-
-void OBSLogReply::on_analyzeURL_clicked()
-{
-	QUrlQuery param;
-	param.addQueryItem("log_url", QUrl::toPercentEncoding(ui->urlEdit->text()));
-	QUrl url("https://obsproject.com/tools/analyzer", QUrl::TolerantMode);
-	url.setQuery(param);
-	QDesktopServices::openUrl(url);
-}

+ 0 - 36
frontend/dialogs/OBSLogReply.hpp

@@ -1,36 +0,0 @@
-/******************************************************************************
-    Copyright (C) 2023 by Lain Bailey <[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 "ui_OBSLogReply.h"
-
-#include <QDialog>
-
-class OBSLogReply : public QDialog {
-	Q_OBJECT
-
-private:
-	std::unique_ptr<Ui::OBSLogReply> ui;
-
-public:
-	OBSLogReply(QWidget *parent, const QString &url, const bool crash);
-
-private slots:
-	void on_copyURL_clicked();
-	void on_analyzeURL_clicked();
-};

+ 0 - 102
frontend/forms/OBSLogReply.ui

@@ -1,102 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>OBSLogReply</class>
- <widget class="QDialog" name="OBSLogReply">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>600</width>
-    <height>96</height>
-   </rect>
-  </property>
-  <property name="windowTitle">
-   <string>LogReturnDialog</string>
-  </property>
-  <layout class="QVBoxLayout" name="verticalLayout">
-   <item>
-    <widget class="QLabel" name="description">
-     <property name="text">
-      <string>LogReturnDialog.Description</string>
-     </property>
-    </widget>
-   </item>
-   <item>
-    <layout class="QHBoxLayout" name="horizontalLayout">
-     <item>
-      <widget class="QLineEdit" name="urlEdit">
-       <property name="text">
-        <string notr="true"/>
-       </property>
-       <property name="readOnly">
-        <bool>true</bool>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <widget class="QPushButton" name="copyURL">
-       <property name="text">
-        <string>LogReturnDialog.CopyURL</string>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <widget class="QPushButton" name="analyzeURL">
-       <property name="text">
-        <string>LogReturnDialog.AnalyzeURL</string>
-       </property>
-      </widget>
-     </item>
-    </layout>
-   </item>
-   <item>
-    <widget class="QDialogButtonBox" name="buttonBox">
-     <property name="orientation">
-      <enum>Qt::Horizontal</enum>
-     </property>
-     <property name="standardButtons">
-      <set>QDialogButtonBox::Ok</set>
-     </property>
-     <property name="centerButtons">
-      <bool>true</bool>
-     </property>
-    </widget>
-   </item>
-  </layout>
- </widget>
- <resources/>
- <connections>
-  <connection>
-   <sender>buttonBox</sender>
-   <signal>accepted()</signal>
-   <receiver>OBSLogReply</receiver>
-   <slot>accept()</slot>
-   <hints>
-    <hint type="sourcelabel">
-     <x>248</x>
-     <y>254</y>
-    </hint>
-    <hint type="destinationlabel">
-     <x>157</x>
-     <y>274</y>
-    </hint>
-   </hints>
-  </connection>
-  <connection>
-   <sender>buttonBox</sender>
-   <signal>rejected()</signal>
-   <receiver>OBSLogReply</receiver>
-   <slot>reject()</slot>
-   <hints>
-    <hint type="sourcelabel">
-     <x>316</x>
-     <y>260</y>
-    </hint>
-    <hint type="destinationlabel">
-     <x>286</x>
-     <y>274</y>
-    </hint>
-   </hints>
-  </connection>
- </connections>
-</ui>

+ 2 - 64
frontend/obs-main.cpp

@@ -53,15 +53,13 @@ static log_handler_t def_log_handler;
 
 extern string currentLogFile;
 extern string lastLogFile;
-extern string lastCrashLogFile;
 
 bool portable_mode = false;
 bool steam = false;
 bool safe_mode = false;
 bool disable_3p_plugins = false;
 static bool unclean_shutdown = false;
-static bool disable_shutdown_check = false;
-static bool multi = false;
+bool multi = false;
 static bool log_verbose = false;
 static bool unfiltered_log = false;
 bool opt_start_streaming = false;
@@ -405,9 +403,6 @@ static void create_log_file(fstream &logFile)
 	stringstream dst;
 
 	get_last_log(false, "obs-studio/logs", lastLogFile);
-#ifdef _WIN32
-	get_last_log(true, "obs-studio/crashes", lastCrashLogFile);
-#endif
 
 	currentLogFile = GenerateTimeDateFilename("txt");
 	dst << "obs-studio/logs/" << currentLogFile.c_str();
@@ -625,26 +620,7 @@ static int run_program(fstream &logFile, int argc, char *argv[])
 		if (!created_log)
 			create_log_file(logFile);
 
-		if (unclean_shutdown) {
-			blog(LOG_WARNING, "[Safe Mode] Unclean shutdown detected!");
-		}
-
-		if (unclean_shutdown && !safe_mode) {
-			QMessageBox mb(QMessageBox::Warning, QTStr("AutoSafeMode.Title"), QTStr("AutoSafeMode.Text"));
-			QPushButton *launchSafeButton =
-				mb.addButton(QTStr("AutoSafeMode.LaunchSafe"), QMessageBox::AcceptRole);
-			QPushButton *launchNormalButton =
-				mb.addButton(QTStr("AutoSafeMode.LaunchNormal"), QMessageBox::RejectRole);
-			mb.setDefaultButton(launchNormalButton);
-			mb.exec();
-
-			safe_mode = mb.clickedButton() == launchSafeButton;
-			if (safe_mode) {
-				blog(LOG_INFO, "[Safe Mode] User has launched in Safe Mode.");
-			} else {
-				blog(LOG_WARNING, "[Safe Mode] User elected to launch normally.");
-			}
-		}
+		program.checkForUncleanShutdown();
 
 		qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &, const QString &message) {
 			switch (type) {
@@ -842,36 +818,6 @@ static inline bool arg_is(const char *arg, const char *long_form, const char *sh
 	return (long_form && strcmp(arg, long_form) == 0) || (short_form && strcmp(arg, short_form) == 0);
 }
 
-static void check_safe_mode_sentinel(void)
-{
-#ifndef NDEBUG
-	/* Safe Mode detection is disabled in Debug builds to keep developers
-	 * somewhat sane. */
-	return;
-#else
-	if (disable_shutdown_check)
-		return;
-
-	BPtr sentinelPath = GetAppConfigPathPtr("obs-studio/safe_mode");
-	if (os_file_exists(sentinelPath)) {
-		unclean_shutdown = true;
-		return;
-	}
-
-	os_quick_write_utf8_file(sentinelPath, nullptr, 0, false);
-#endif
-}
-
-static void delete_safe_mode_sentinel(void)
-{
-#ifndef NDEBUG
-	return;
-#else
-	BPtr sentinelPath = GetAppConfigPathPtr("obs-studio/safe_mode");
-	os_unlink(sentinelPath);
-#endif
-}
-
 #ifdef _WIN32
 static constexpr char vcRunErrorTitle[] = "Outdated Visual C++ Runtime";
 static constexpr char vcRunErrorMsg[] = "OBS Studio requires a newer version of the Microsoft Visual C++ "
@@ -965,7 +911,6 @@ int main(int argc, char *argv[])
 	for (int i = 1; i < argc; i++) {
 		if (arg_is(argv[i], "--multi", "-m")) {
 			multi = true;
-			disable_shutdown_check = true;
 
 #if ALLOW_PORTABLE_MODE
 		} else if (arg_is(argv[i], "--portable", "-p")) {
@@ -981,10 +926,6 @@ int main(int argc, char *argv[])
 		} else if (arg_is(argv[i], "--only-bundled-plugins", nullptr)) {
 			disable_3p_plugins = true;
 
-		} else if (arg_is(argv[i], "--disable-shutdown-check", nullptr)) {
-			/* This exists mostly to bypass the dialog during development. */
-			disable_shutdown_check = true;
-
 		} else if (arg_is(argv[i], "--always-on-top", nullptr)) {
 			opt_always_on_top = true;
 
@@ -1091,8 +1032,6 @@ int main(int argc, char *argv[])
 	}
 #endif
 
-	check_safe_mode_sentinel();
-
 	fstream logFile;
 
 	curl_global_init(CURL_GLOBAL_ALL);
@@ -1109,7 +1048,6 @@ int main(int argc, char *argv[])
 	log_blocked_dlls();
 #endif
 
-	delete_safe_mode_sentinel();
 	blog(LOG_INFO, "Number of memory leaks: %ld", bnum_allocs());
 	base_set_log_handler(nullptr, nullptr);
 

+ 13 - 4
frontend/widgets/OBSBasic.cpp

@@ -1233,16 +1233,16 @@ void OBSBasic::OBSInit()
 	ui->sources->UpdateIcons();
 
 #if !defined(_WIN32)
+	delete ui->actionRepair;
+	ui->actionRepair = nullptr;
+#if !defined(__APPLE__)
 	delete ui->actionShowCrashLogs;
 	delete ui->actionUploadLastCrashLog;
 	delete ui->menuCrashLogs;
-	delete ui->actionRepair;
+	delete ui->actionCheckForUpdates;
 	ui->actionShowCrashLogs = nullptr;
 	ui->actionUploadLastCrashLog = nullptr;
 	ui->menuCrashLogs = nullptr;
-	ui->actionRepair = nullptr;
-#if !defined(__APPLE__)
-	delete ui->actionCheckForUpdates;
 	ui->actionCheckForUpdates = nullptr;
 #endif
 #endif
@@ -1323,6 +1323,13 @@ void OBSBasic::OnFirstLoad()
 }
 
 OBSBasic::~OBSBasic()
+{
+	if (!handledShutdown) {
+		applicationShutdown();
+	}
+}
+
+void OBSBasic::applicationShutdown() noexcept
 {
 	/* clear out UI event queue */
 	QApplication::sendPostedEvents(nullptr);
@@ -1420,6 +1427,8 @@ OBSBasic::~OBSBasic()
 	delete cef;
 	cef = nullptr;
 #endif
+
+	handledShutdown = true;
 }
 
 static inline int AttemptToResetVideo(struct obs_video_info *ovi)

+ 13 - 4
frontend/widgets/OBSBasic.hpp

@@ -64,6 +64,7 @@ struct QuickTransition;
 namespace OBS {
 class SceneCollection;
 struct Rect;
+enum class LogFileType;
 } // namespace OBS
 
 #define DESKTOP_AUDIO_1 Str("DesktopAudioDevice1")
@@ -259,6 +260,7 @@ private:
 
 	bool loaded = false;
 	bool closing = false;
+	bool handledShutdown = false;
 
 	// TODO: Remove, orphaned variable
 	bool copyVisible = true;
@@ -300,6 +302,7 @@ private:
 public slots:
 	void UpdatePatronJson(const QString &text, const QString &error);
 	void UpdateEditMenu();
+	void applicationShutdown() noexcept;
 
 public:
 	/* `undo_s` needs to be declared after `ui` to prevent an uninitialized
@@ -579,7 +582,6 @@ private:
 
 	QList<QPoint> visDlgPositions;
 
-	void UploadLog(const char *subdir, const char *file, const bool crash);
 	void CloseDialogs();
 	void EnumDialogs();
 
@@ -631,9 +633,7 @@ private slots:
 
 	void on_resetUI_triggered();
 
-	void logUploadFinished(const QString &text, const QString &error);
-	void crashUploadFinished(const QString &text, const QString &error);
-	void openLogDialog(const QString &text, const bool crash);
+	void logUploadFinished(const QString &text, const QString &error, OBS::LogFileType uploadType);
 
 	void updateCheckFinished();
 
@@ -645,6 +645,15 @@ public:
 	void CreateEditTransformWindow(obs_sceneitem_t *item);
 	void CreatePropertiesWindow(obs_source_t *source);
 
+	void UploadLog(const char *subdir, const char *file, OBS::LogFileType uploadType);
+
+	/* -------------------------------------
+	 * MARK: - OBSBasic_MainMenu
+	 * -------------------------------------
+	 */
+private:
+	void setupMenuItemStateHandlers();
+
 	/* -------------------------------------
 	 * MARK: - OBSBasic_OutputHandler
 	 * -------------------------------------

+ 43 - 55
frontend/widgets/OBSBasic_MainControls.cpp

@@ -20,6 +20,7 @@
 #include "OBSBasic.hpp"
 #include "OBSBasicStats.hpp"
 
+#include <dialogs/LogUploadDialog.hpp>
 #include <dialogs/OBSAbout.hpp>
 #include <dialogs/OBSBasicAdvAudio.hpp>
 #include <dialogs/OBSBasicFilters.hpp>
@@ -27,7 +28,6 @@
 #include <dialogs/OBSBasicProperties.hpp>
 #include <dialogs/OBSBasicTransform.hpp>
 #include <dialogs/OBSLogViewer.hpp>
-#include <dialogs/OBSLogReply.hpp>
 #ifdef __APPLE__
 #include <dialogs/OBSPermissions.hpp>
 #endif
@@ -62,6 +62,8 @@ extern QCef *cef;
 extern QCefCookieManager *panel_cookies;
 
 using namespace std;
+using LogUploadDialog = OBS::LogUploadDialog;
+using LogUploadType = OBS::LogFileType;
 
 void OBSBasic::CreateInteractionWindow(obs_source_t *source)
 {
@@ -256,20 +258,17 @@ static BPtr<char> ReadLogFile(const char *subdir, const char *log)
 	return file;
 }
 
-void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash)
+void OBSBasic::UploadLog(const char *subdir, const char *file, const LogUploadType uploadType)
 {
 	BPtr<char> fileString{ReadLogFile(subdir, file)};
 
-	if (!fileString)
-		return;
-
-	if (!*fileString)
+	if (!fileString || !*fileString) {
+		OBSApp *app = App();
+		emit app->logUploadFailed(uploadType, QTStr("LogUploadDialog.Errors.NoLogFile"));
 		return;
+	}
 
 	ui->menuLogFiles->setEnabled(false);
-#if defined(_WIN32)
-	ui->menuCrashLogs->setEnabled(false);
-#endif
 
 	stringstream ss;
 	ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n"
@@ -282,11 +281,11 @@ void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash)
 	RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str());
 
 	logUploadThread.reset(thread);
-	if (crash) {
-		connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished);
-	} else {
-		connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished);
-	}
+
+	connect(thread, &RemoteTextThread::Result, this, [this, uploadType](const QString &text, const QString &error) {
+		logUploadFinished(text, error, uploadType);
+	});
+
 	logUploadThread->start();
 }
 
@@ -302,12 +301,24 @@ void OBSBasic::on_actionShowLogs_triggered()
 
 void OBSBasic::on_actionUploadCurrentLog_triggered()
 {
-	UploadLog("obs-studio/logs", App()->GetCurrentLog(), false);
+	ui->menuLogFiles->setEnabled(false);
+
+	LogUploadDialog uploadDialog{this, LogUploadType::CurrentAppLog};
+
+	uploadDialog.exec();
+
+	ui->menuLogFiles->setEnabled(true);
 }
 
 void OBSBasic::on_actionUploadLastLog_triggered()
 {
-	UploadLog("obs-studio/logs", App()->GetLastLog(), false);
+	ui->menuLogFiles->setEnabled(false);
+
+	LogUploadDialog uploadDialog{this, LogUploadType::LastAppLog};
+
+	uploadDialog.exec();
+
+	ui->menuLogFiles->setEnabled(true);
 }
 
 void OBSBasic::on_actionViewCurrentLog_triggered()
@@ -323,17 +334,18 @@ void OBSBasic::on_actionViewCurrentLog_triggered()
 
 void OBSBasic::on_actionShowCrashLogs_triggered()
 {
-	char logDir[512];
-	if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0)
-		return;
-
-	QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir));
-	QDesktopServices::openUrl(url);
+	App()->openCrashLogDirectory();
 }
 
 void OBSBasic::on_actionUploadLastCrashLog_triggered()
 {
-	UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true);
+	ui->menuCrashLogs->setEnabled(false);
+
+	LogUploadDialog uploadDialog{this, LogUploadType::CrashLog};
+
+	uploadDialog.exec();
+
+	ui->menuCrashLogs->setEnabled(true);
 }
 
 void OBSBasic::on_actionCheckForUpdates_triggered()
@@ -367,43 +379,19 @@ void OBSBasic::on_actionRestartSafe_triggered()
 	}
 }
 
-void OBSBasic::logUploadFinished(const QString &text, const QString &error)
+void OBSBasic::logUploadFinished(const QString &text, const QString &error, LogUploadType uploadType)
 {
-	ui->menuLogFiles->setEnabled(true);
-#if defined(_WIN32)
-	ui->menuCrashLogs->setEnabled(true);
-#endif
+	OBSApp *app = App();
 
 	if (text.isEmpty()) {
-		OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error);
-		return;
-	}
-	openLogDialog(text, false);
-}
-
-void OBSBasic::crashUploadFinished(const QString &text, const QString &error)
-{
-	ui->menuLogFiles->setEnabled(true);
-#if defined(_WIN32)
-	ui->menuCrashLogs->setEnabled(true);
-#endif
+		emit app->logUploadFailed(uploadType, error);
+	} else {
+		OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text));
+		string resURL = obs_data_get_string(returnData, "url");
+		QString logURL = resURL.c_str();
 
-	if (text.isEmpty()) {
-		OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error);
-		return;
+		emit app->logUploadFinished(uploadType, logURL);
 	}
-	openLogDialog(text, true);
-}
-
-void OBSBasic::openLogDialog(const QString &text, const bool crash)
-{
-
-	OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text));
-	string resURL = obs_data_get_string(returnData, "url");
-	QString logURL = resURL.c_str();
-
-	OBSLogReply logDialog(this, logURL, crash);
-	logDialog.exec();
 }
 
 void OBSBasic::on_actionHelpPortal_triggered()