diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 84e804a4d..a651c619e 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -502,6 +502,10 @@ MacPermissions.Continue="Continue" UpdateAvailable="New Update Available" UpdateAvailable.Text="Version %1.%2.%3 is now available. Click here to download" +# Source leak error message +SourceLeak.Title="Source Cleanup Error" +SourceLeak.Text="There was a problem while changing scene collections and some sources could not be unloaded. This issue is typically caused by plugins that are not releasing resources properly. Please ensure that any plugins you are using are up to date.\n\nOBS Studio will now exit to prevent any potential data corruption." + # audio device names Basic.DesktopDevice1="Desktop Audio" Basic.DesktopDevice2="Desktop Audio 2" diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index fc9104a08..86a6e3b0f 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -1065,9 +1065,18 @@ static inline void AddMissingFiles(void *data, obs_source_t *source) void OBSBasic::LoadData(obs_data_t *data, const char *file) { ClearSceneData(); - InitDefaultTransitions(); ClearContextBar(); + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), + QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { devicePropertiesThread->wait(); devicePropertiesThread.reset(); @@ -4880,10 +4889,45 @@ void OBSBasic::ClearSceneData() unsetCursor(); - disableSaving--; + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = std::accumulate( + orphan_sources.begin(), orphan_sources.end(), + string(""), [](string a, string b) { + return std::move(a) + "\n- " + b; + }); + + blog(LOG_ERROR, + "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, + "------------------------------------------------"); + } } void OBSBasic::closeEvent(QCloseEvent *event) @@ -4914,7 +4958,8 @@ void OBSBasic::closeEvent(QCloseEvent *event) bool confirmOnExit = config_get_bool(GetGlobalConfig(), "General", "ConfirmOnExit"); - if (confirmOnExit && outputHandler && outputHandler->Active()) { + if (confirmOnExit && outputHandler && outputHandler->Active() && + !clearingFailed) { SetShowing(true); QMessageBox::StandardButton button = OBSMessageBox::question( diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index cbce69832..55abea8fe 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -245,6 +245,8 @@ private: OBSWeakSourceAutoRelease copySourceTransition; bool closing = false; + bool clearingFailed = false; + QScopedPointer devicePropertiesThread; QScopedPointer whatsNewInitThread; QScopedPointer updateCheckThread;