diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 10de47680..93253d6dc 100644 --- a/UI/CMakeLists.txt +++ b/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 "$,obs64,obs>") # cmake-format: on diff --git a/UI/cmake/legacy.cmake b/UI/cmake/legacy.cmake index ae08191b0..77c0609ea 100644 --- a/UI/cmake/legacy.cmake +++ b/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) diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 4b2f4de90..de77d5f27 100644 --- a/UI/data/locale/en-US.ini +++ b/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" diff --git a/UI/forms/OBSBasic.ui b/UI/forms/OBSBasic.ui index 30d3dd69f..b51c2ed0c 100644 --- a/UI/forms/OBSBasic.ui +++ b/UI/forms/OBSBasic.ui @@ -532,9 +532,11 @@ + + + - @@ -2004,6 +2006,11 @@ Basic.MainMenu.Help.Repair + + + Basic.MainMenu.Help.RestartSafeMode + + Interact diff --git a/UI/obs-app.cpp b/UI/obs-app.cpp index 4e5ffdf2e..77aabe005 100644 --- a/UI/obs-app.cpp +++ b/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; @@ -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; diff --git a/UI/obs-app.hpp b/UI/obs-app.hpp index b11c7895b..17f39b45f 100644 --- a/UI/obs-app.hpp +++ b/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); diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 9807cfd83..80ff75d65 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -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 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"); diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 470196e39..346f5f107 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -231,6 +231,8 @@ private: QList> 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();