Explorar o código

UI: Add Safe Mode

derrod %!s(int64=2) %!d(string=hai) anos
pai
achega
396cfdb416

+ 4 - 0
UI/CMakeLists.txt

@@ -108,6 +108,10 @@ foreach(graphics_library IN ITEMS opengl metal d3d11)
   endif()
 endforeach()
 
+get_property(obs_module_list GLOBAL PROPERTY OBS_MODULES_ENABLED)
+list(JOIN obs_module_list "|" SAFE_MODULES)
+target_compile_definitions(obs-studio PRIVATE "SAFE_MODULES=\"${SAFE_MODULES}\"")
+
 # cmake-format: off
 set_target_properties_obs(obs-studio PROPERTIES FOLDER frontend OUTPUT_NAME "$<IF:$<PLATFORM_ID:Windows>,obs64,obs>")
 # cmake-format: on

+ 4 - 0
UI/cmake/legacy.cmake

@@ -486,6 +486,10 @@ source_group(
 unset(_SOURCES)
 unset(_UI)
 
+get_property(OBS_MODULE_LIST GLOBAL PROPERTY OBS_MODULE_LIST)
+list(JOIN OBS_MODULE_LIST "|" SAFE_MODULES)
+target_compile_definitions(obs PRIVATE "SAFE_MODULES=\"${SAFE_MODULES}\"")
+
 define_graphic_modules(obs)
 setup_obs_app(obs)
 setup_target_resources(obs obs-studio)

+ 11 - 0
UI/data/locale/en-US.ini

@@ -125,6 +125,15 @@ 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?"
+
 ChromeOS.Title="Unsupported Platform"
 ChromeOS.Text="OBS appears to be running inside a ChromeOS container. This platform is unsupported."
 
@@ -834,6 +843,8 @@ Basic.MainMenu.Help.Logs.UploadLastLog="Upload &Previous Log File"
 Basic.MainMenu.Help.Logs.ViewCurrentLog="&View Current Log"
 Basic.MainMenu.Help.CheckForUpdates="Check For Updates"
 Basic.MainMenu.Help.Repair="Check File Integrity"
+Basic.MainMenu.Help.RestartSafeMode="Restart in Safe Mode"
+Basic.MainMenu.Help.RestartNormal="Restart in Normal Mode"
 Basic.MainMenu.Help.CrashLogs="Crash &Reports"
 Basic.MainMenu.Help.CrashLogs.ShowLogs="&Show Crash Reports"
 Basic.MainMenu.Help.CrashLogs.UploadLastLog="Upload &Previous Crash Report"

+ 8 - 1
UI/forms/OBSBasic.ui

@@ -532,9 +532,11 @@
     <addaction name="separator"/>
     <addaction name="actionRepair"/>
     <addaction name="actionCheckForUpdates"/>
+    <addaction name="actionRestartSafe"/>
+    <addaction name="actionShowMacPermissions"/>
+    <addaction name="separator"/>
     <addaction name="actionShowWhatsNew"/>
     <addaction name="actionShowAbout"/>
-    <addaction name="actionShowMacPermissions"/>
     <addaction name="separator"/>
    </widget>
    <widget class="QMenu" name="menuBasic_MainMenu_Edit">
@@ -2004,6 +2006,11 @@
     <string>Basic.MainMenu.Help.Repair</string>
    </property>
   </action>
+  <action name="actionRestartSafe">
+   <property name="text">
+    <string>Basic.MainMenu.Help.RestartSafeMode</string>
+   </property>
+  </action>
   <action name="actionInteract">
    <property name="text">
     <string>Interact</string>

+ 97 - 3
UI/obs-app.cpp

@@ -87,6 +87,10 @@ static string lastCrashLogFile;
 
 bool portable_mode = false;
 bool steam = false;
+bool safe_mode = false;
+bool disable_3p_plugins = false;
+bool unclean_shutdown = false;
+bool disable_shutdown_check = false;
 static bool multi = false;
 static bool log_verbose = false;
 static bool unfiltered_log = false;
@@ -105,6 +109,7 @@ string opt_starting_profile;
 string opt_starting_scene;
 
 bool restart = false;
+bool restart_safe = false;
 
 QPointer<OBSLogViewer> obsLogViewer;
 
@@ -1715,6 +1720,12 @@ bool OBSApp::OBSInit()
 	     QT_VERSION_STR);
 	blog(LOG_INFO, "Portable mode: %s", portable_mode ? "true" : "false");
 
+	if (safe_mode) {
+		blog(LOG_WARNING, "Safe Mode enabled.");
+	} else if (disable_3p_plugins) {
+		blog(LOG_WARNING, "Third-party plugins disabled.");
+	}
+
 	setQuitOnLastWindowClosed(false);
 
 	mainWindow = new OBSBasic();
@@ -2429,6 +2440,10 @@ static int run_program(fstream &logFile, int argc, char *argv[])
 			blog(LOG_WARNING, "================================");
 			blog(LOG_WARNING, "User is now running multiple "
 					  "instances of OBS!");
+			/* Clear unclean_shutdown flag as multiple instances
+			 * running from the same config will lead to a
+			 * false-positive detection.*/
+			unclean_shutdown = false;
 		}
 
 		/* --------------------------------------- */
@@ -2453,6 +2468,34 @@ 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.");
+			}
+		}
+
 		qInstallMessageHandler([](QtMsgType type,
 					  const QMessageLogContext &,
 					  const QString &message) {
@@ -2540,9 +2583,18 @@ static int run_program(fstream &logFile, int argc, char *argv[])
 		OBSErrorBox(nullptr, "%s", error);
 	}
 
-	if (restart)
-		QProcess::startDetached(qApp->arguments()[0],
-					qApp->arguments());
+	if (restart || restart_safe) {
+		auto args = qApp->arguments();
+		auto executable = args[0];
+
+		if (restart_safe) {
+			args.append("--safe-mode");
+		} else {
+			args.removeAll("--safe-mode");
+		}
+
+		QProcess::startDetached(executable, args);
+	}
 
 	return ret;
 }
@@ -3194,6 +3246,32 @@ static void upgrade_settings(void)
 	os_closedir(dir);
 }
 
+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 = GetConfigPathPtr("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)
+{
+	BPtr sentinelPath = GetConfigPathPtr("obs-studio/safe_mode");
+	os_unlink(sentinelPath);
+}
+
 #ifndef _WIN32
 void OBSApp::SigIntSignalHandler(int s)
 {
@@ -3281,6 +3359,17 @@ int main(int argc, char *argv[])
 		} else if (arg_is(argv[i], "--verbose", nullptr)) {
 			log_verbose = true;
 
+		} else if (arg_is(argv[i], "--safe-mode", nullptr)) {
+			safe_mode = true;
+
+		} 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;
 
@@ -3347,6 +3436,9 @@ int main(int argc, char *argv[])
 				"--portable, -p: Use portable mode.\n"
 #endif
 				"--multi, -m: Don't warn when launching multiple instances.\n\n"
+				"--safe-mode: Run in Safe Mode (disables third-party plugins, scripting, and websockets).\n"
+				"--only-bundled-plugins: Only load included (first-party) plugins\n"
+				"--disable-shutdown-check: Disable unclean shutdown detection.\n"
 				"--verbose: Make log more verbose.\n"
 				"--always-on-top: Start in 'always on top' mode.\n\n"
 				"--unfiltered_log: Make log unfiltered.\n\n"
@@ -3393,6 +3485,7 @@ int main(int argc, char *argv[])
 	}
 #endif
 
+	check_safe_mode_sentinel();
 	upgrade_settings();
 
 	fstream logFile;
@@ -3412,6 +3505,7 @@ 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);
 	return ret;

+ 3 - 0
UI/obs-app.hpp

@@ -267,6 +267,8 @@ static inline int GetProfilePath(char *path, size_t size, const char *file)
 
 extern bool portable_mode;
 extern bool steam;
+extern bool safe_mode;
+extern bool disable_3p_plugins;
 
 extern bool opt_start_streaming;
 extern bool opt_start_recording;
@@ -278,6 +280,7 @@ extern bool opt_allow_opengl;
 extern bool opt_always_on_top;
 extern std::string opt_starting_scene;
 extern bool restart;
+extern bool restart_safe;
 
 #ifdef _WIN32
 extern "C" void install_dll_blocklist_hook(void);

+ 77 - 8
UI/window-basic-main.cpp

@@ -21,6 +21,7 @@
 #include <cstddef>
 #include <ctime>
 #include <functional>
+#include <unordered_set>
 #include <obs-data.h>
 #include <obs.h>
 #include <obs.hpp>
@@ -232,6 +233,30 @@ static void AddExtraModulePaths()
 #endif
 }
 
+/* First-party modules considered to be potentially unsafe to load in Safe Mode
+ * due to them allowing external code (e.g. scripts) to modify OBS's state. */
+static const unordered_set<string> unsafe_modules = {
+	"frontend-tools", // Scripting
+	"obs-websocket",  // Allows outside modifications
+};
+
+static void SetSafeModuleNames()
+{
+#ifndef SAFE_MODULES
+	return;
+#else
+	string module;
+	stringstream modules(SAFE_MODULES);
+
+	while (getline(modules, module, '|')) {
+		/* When only disallowing third-party plugins, still add
+		 * "unsafe" bundled modules to the safe list. */
+		if (disable_3p_plugins || !unsafe_modules.count(module))
+			obs_add_safe_module(module.c_str());
+	}
+#endif
+}
+
 extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main);
 
 void assignDockToggle(QDockWidget *dock, QAction *action)
@@ -801,9 +826,18 @@ void OBSBasic::Save(const char *file)
 	}
 
 	if (api) {
-		OBSDataAutoRelease moduleObj = obs_data_create();
-		api->on_save(moduleObj);
-		obs_data_set_obj(saveData, "modules", moduleObj);
+		if (safeModeModuleData) {
+			/* If we're in Safe Mode and have retained unloaded
+			 * plugin data, update the existing data object instead
+			 * of creating a new one. */
+			api->on_save(safeModeModuleData);
+			obs_data_set_obj(saveData, "modules",
+					 safeModeModuleData);
+		} else {
+			OBSDataAutoRelease moduleObj = obs_data_create();
+			api->on_save(moduleObj);
+			obs_data_set_obj(saveData, "modules", moduleObj);
+		}
 	}
 
 	if (!obs_data_save_json_safe(saveData, file, "tmp", "bak"))
@@ -1084,6 +1118,12 @@ void OBSBasic::LoadData(obs_data_t *data, const char *file)
 	if (api)
 		api->on_preload(modulesObj);
 
+	if (safe_mode || disable_3p_plugins) {
+		/* Keep a reference to "modules" data so plugins that are not
+		 * loaded do not have their collection specific data lost. */
+		safeModeModuleData = obs_data_get_obj(data, "modules");
+	}
+
 	OBSDataArrayAutoRelease sceneOrder =
 		obs_data_get_array(data, "scene_order");
 	OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources");
@@ -1259,14 +1299,14 @@ retryScene:
 	if (!opt_starting_scene.empty())
 		opt_starting_scene.clear();
 
-	if (opt_start_streaming) {
+	if (opt_start_streaming && !safe_mode) {
 		blog(LOG_INFO, "Starting stream due to command line parameter");
 		QMetaObject::invokeMethod(this, "StartStreaming",
 					  Qt::QueuedConnection);
 		opt_start_streaming = false;
 	}
 
-	if (opt_start_recording) {
+	if (opt_start_recording && !safe_mode) {
 		blog(LOG_INFO,
 		     "Starting recording due to command line parameter");
 		QMetaObject::invokeMethod(this, "StartRecording",
@@ -1274,13 +1314,13 @@ retryScene:
 		opt_start_recording = false;
 	}
 
-	if (opt_start_replaybuffer) {
+	if (opt_start_replaybuffer && !safe_mode) {
 		QMetaObject::invokeMethod(this, "StartReplayBuffer",
 					  Qt::QueuedConnection);
 		opt_start_replaybuffer = false;
 	}
 
-	if (opt_start_virtualcam) {
+	if (opt_start_virtualcam && !safe_mode) {
 		QMetaObject::invokeMethod(this, "StartVirtualCam",
 					  Qt::QueuedConnection);
 		opt_start_virtualcam = false;
@@ -1961,7 +2001,14 @@ void OBSBasic::OBSInit()
 #endif
 	struct obs_module_failure_info mfi;
 
-	AddExtraModulePaths();
+	/* Safe Mode disables third-party plugins so we don't need to add earch
+	 * paths outside the OBS bundle/installation. */
+	if (safe_mode || disable_3p_plugins) {
+		SetSafeModuleNames();
+	} else {
+		AddExtraModulePaths();
+	}
+
 	blog(LOG_INFO, "---------------------------------");
 	obs_load_all_modules2(&mfi);
 	blog(LOG_INFO, "---------------------------------");
@@ -2274,6 +2321,11 @@ void OBSBasic::OBSInit()
 	ui->actionShowWhatsNew = nullptr;
 #endif
 
+	if (safe_mode) {
+		ui->actionRestartSafe->setText(
+			QTStr("Basic.MainMenu.Help.RestartNormal"));
+	}
+
 	UpdatePreviewProgramIndicators();
 	OnFirstLoad();
 
@@ -4904,6 +4956,7 @@ void OBSBasic::ClearSceneData()
 		outputHandler->UpdateVirtualCamOutputSource();
 	}
 
+	safeModeModuleData = nullptr;
 	lastScene = nullptr;
 	swapScene = nullptr;
 	programScene = nullptr;
@@ -6563,6 +6616,20 @@ void OBSBasic::on_actionRepair_triggered()
 #endif
 }
 
+void OBSBasic::on_actionRestartSafe_triggered()
+{
+	QMessageBox::StandardButton button = OBSMessageBox::question(
+		this, QTStr("Restart"),
+		safe_mode ? QTStr("SafeMode.RestartNormal")
+			  : QTStr("SafeMode.Restart"));
+
+	if (button == QMessageBox::Yes) {
+		restart = safe_mode;
+		restart_safe = !safe_mode;
+		close();
+	}
+}
+
 void OBSBasic::logUploadFinished(const QString &text, const QString &error)
 {
 	ui->menuLogFiles->setEnabled(true);
@@ -9270,6 +9337,8 @@ void OBSBasic::UpdateTitleBar()
 		name << "Studio ";
 
 	name << App()->GetVersionString(false);
+	if (safe_mode)
+		name << " (SAFE MODE)";
 	if (App()->IsPortableMode())
 		name << " - " << Str("TitleBar.PortableMode");
 

+ 3 - 0
UI/window-basic-main.hpp

@@ -231,6 +231,8 @@ private:
 	QList<QPointer<QDockWidget>> oldExtraDocks;
 	QStringList oldExtraDockNames;
 
+	OBSDataAutoRelease safeModeModuleData;
+
 	bool loaded = false;
 	long disableSaving = 1;
 	bool projectChanged = false;
@@ -1044,6 +1046,7 @@ private slots:
 	void on_actionCheckForUpdates_triggered();
 	void on_actionRepair_triggered();
 	void on_actionShowWhatsNew_triggered();
+	void on_actionRestartSafe_triggered();
 
 	void on_actionShowCrashLogs_triggered();
 	void on_actionUploadLastCrashLog_triggered();