diff --git a/build-aux/com.obsproject.Studio.json b/build-aux/com.obsproject.Studio.json index 2c0fac395..f6a4165c3 100644 --- a/build-aux/com.obsproject.Studio.json +++ b/build-aux/com.obsproject.Studio.json @@ -25,7 +25,7 @@ "versions": "stable;test", "subdirectories": true, "add-ld-path": "lib", - "merge-dirs": "lib/obs-plugins;share/obs/obs-plugins", + "merge-dirs": "lib/obs-modules/plugins;share/obs/obs-modules/plugins;lib/obs-plugins;share/obs/obs-plugins", "no-autodownload": true, "autodelete": true }, @@ -132,6 +132,7 @@ "builddir": true, "config-opts": [ "-DCMAKE_BUILD_TYPE=RelWithDebInfo", + "-DENABLE_FLATPAK=ON", "-DENABLE_WAYLAND=ON", "-DENABLE_BROWSER=ON", "-DCEF_ROOT_DIR=/app/cef", diff --git a/frontend/CMakeLists.txt b/frontend/CMakeLists.txt index 98daf7bb0..eda3a3a77 100644 --- a/frontend/CMakeLists.txt +++ b/frontend/CMakeLists.txt @@ -99,10 +99,6 @@ 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}\"") - get_target_property(target_sources obs-studio SOURCES) set(target_cpp_sources ${target_sources}) set(target_hpp_sources ${target_sources}) diff --git a/frontend/OBSApp.cpp b/frontend/OBSApp.cpp index 9b85f5f52..4e0d8fb4c 100644 --- a/frontend/OBSApp.cpp +++ b/frontend/OBSApp.cpp @@ -90,6 +90,35 @@ typedef struct UncleanLaunchAction { bool sendCrashReport = false; } UncleanLaunchAction; +enum class PluginFailureAction { Continue, OpenPluginManager }; + +PluginFailureAction handlePluginFailure() +{ + QMessageBox pluginWarning; + + pluginWarning.setIcon(QMessageBox::Warning); + + pluginWarning.setWindowTitle(QTStr("PluginFailure.Dialog.Title")); + pluginWarning.setText(QTStr("PluginFailure.Labels.Text")); + + QPushButton *continueButton = + pluginWarning.addButton(QTStr("PluginFailure.Dialog.Continue"), QMessageBox::RejectRole); + QPushButton *handleButton = + pluginWarning.addButton(QTStr("PluginFailure.Dialog.Open"), QMessageBox::AcceptRole); + + pluginWarning.setDefaultButton(continueButton); + + pluginWarning.exec(); + + bool openPluginManager = pluginWarning.clickedButton() == handleButton; + + if (openPluginManager) { + return PluginFailureAction::OpenPluginManager; + } else { + return PluginFailureAction::Continue; + } +} + UncleanLaunchAction handleUncleanShutdown(bool enableCrashUpload) { UncleanLaunchAction launchAction; @@ -2062,16 +2091,27 @@ void OBSApp::addLogLine(int logLevel, const QString &message) emit logLineAdded(logLevel, message); } -void OBSApp::loadAppModules(struct obs_module_failure_info &mfi) +void OBSApp::loadAppModules() { - pluginManager_->preLoad(); - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - pluginManager_->postLoad(); + using PluginMode = OBS::PluginManager::Mode; + PluginMode mode = (disable_3p_plugins || safe_mode) ? PluginMode::CoreOnly : PluginMode::Full; + pluginManager_->setPluginMode(mode); + + pluginManager_->loadAllPlugins(portable_mode); +} + +void OBSApp::handlePluginLoadState() +{ + using PluginState = OBS::PluginManager::State; + PluginState loadState = pluginManager_->loadState(); + + if (loadState != PluginState::Success) { + PluginFailureAction action = handlePluginFailure(); + + if (action == PluginFailureAction::OpenPluginManager) { + pluginManagerOpenDialog(); + } + } } void OBSApp::pluginManagerOpenDialog() diff --git a/frontend/OBSApp.hpp b/frontend/OBSApp.hpp index 657b194d3..23e245d86 100644 --- a/frontend/OBSApp.hpp +++ b/frontend/OBSApp.hpp @@ -235,12 +235,13 @@ public: static void sigQuitSignalHandler(int); #endif - void loadAppModules(struct obs_module_failure_info &mfi); + void loadAppModules(); ThumbnailManager *thumbnails() const { return thumbnailManager; } // Plugin Manager Accessors void pluginManagerOpenDialog(); + void handlePluginLoadState(); public slots: void addLogLine(int logLevel, const QString &message); diff --git a/frontend/cmake/feature-plugin-manager.cmake b/frontend/cmake/feature-plugin-manager.cmake index faf9cf2a0..5be57d9ac 100644 --- a/frontend/cmake/feature-plugin-manager.cmake +++ b/frontend/cmake/feature-plugin-manager.cmake @@ -1,5 +1,13 @@ find_package(nlohmann_json 3.11 REQUIRED) +set(OBS_PLATFORM_INSTALL_PATH "${CMAKE_INSTALL_PREFIX}/") +set(OBS_PLATFORM_LIBRARY_PATH "${CMAKE_INSTALL_LIBDIR}/") +set(OBS_PLATFORM_DATA_PATH "${CMAKE_INSTALL_DATAROOTDIR}/") +set(OBS_PLATFORM_CORE_MODULE_PATH "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/obs-modules/core") +set(OBS_PLATFORM_CORE_DATA_PATH "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/obs/obs-modules/core") +set(OBS_PLATFORM_PLUGIN_MODULE_PATH "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/obs-modules/plugins") +set(OBS_PLATFORM_PLUGIN_DATA_PATH "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/obs/obs-modules/plugins") + target_sources( obs-studio PRIVATE @@ -7,6 +15,22 @@ target_sources( plugin-manager/PluginManager.hpp plugin-manager/PluginManagerWindow.cpp plugin-manager/PluginManagerWindow.hpp + plugin-manager/PluginModuleLoader.hpp ) +if(OS_WINDOWS) + target_sources(obs-studio PRIVATE plugin-manager/PluginModuleLoader_Windows.cpp) +elseif(OS_MACOS) + target_sources(obs-studio PRIVATE plugin-manager/PluginModuleLoader_MacOS.mm) +elseif(OS_FLATPAK) + configure_file(plugin-manager/LoaderPaths_Flatpak.hpp.in LoaderPaths_Flatpak.hpp @ONLY) + target_sources(obs-studio PRIVATE plugin-manager/PluginModuleLoader_Flatpak.cpp LoaderPaths_Flatpak.hpp) +elseif(OS_LINUX) + configure_file(plugin-manager/LoaderPaths_Linux.hpp.in LoaderPaths_Linux.hpp @ONLY) + target_sources(obs-studio PRIVATE plugin-manager/PluginModuleLoader_Linux.cpp LoaderPaths_Linux.hpp) +elseif(OS_FREEBSD OR OS_OPENBSD) + configure_file(plugin-manager/LoaderPaths_BSD.hpp.in LoaderPaths_BSD.hpp @ONLY) + target_sources(obs-studio PRIVATE plugin-manager/PluginModuleLoader_BSD.cpp LoaderPaths_BSD.hpp) +endif() + target_link_libraries(obs-studio PRIVATE nlohmann_json::nlohmann_json) diff --git a/frontend/data/locale/en-US.ini b/frontend/data/locale/en-US.ini index ef69b12d5..821c29200 100644 --- a/frontend/data/locale/en-US.ini +++ b/frontend/data/locale/en-US.ini @@ -141,6 +141,12 @@ 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" +# Plugin Manager Load Failure +PluginFailure.Dialog.Title="OBS Studio Plugin Loading Failure" +PluginFailure.Labels.Text="OBS Studio was not able to load all third-party plugins.\n\nOpen Plugin Manager to check the state of third-party plugins?" +PluginFailure.Dialog.Continue="Continue" +PluginFailure.Dialog.Open="Open Plugin Manager" + # 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?" diff --git a/frontend/plugin-manager/LoaderPaths_BSD.hpp.in b/frontend/plugin-manager/LoaderPaths_BSD.hpp.in new file mode 100644 index 000000000..5caf9240d --- /dev/null +++ b/frontend/plugin-manager/LoaderPaths_BSD.hpp.in @@ -0,0 +1,28 @@ +/****************************************************************************** + Copyright (C) 2026 by FiniteSingularity + + 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 . +******************************************************************************/ + +#pragma once + +#include + +namespace OBS::Constants { +inline constexpr std::string_view kPlatformInstallPath{"@OBS_PLATFORM_INSTALL_PATH@"}; +inline constexpr std::string_view kPlatformLibraryPath{"@OBS_PLATFORM_LIBRARY_PATH@"}; +inline constexpr std::string_view kPlatformDataPath{"@OBS_PLATFORM_DATA_PATH@"}; +inline constexpr std::string_view kPlatformPluginModulePath{"@OBS_PLATFORM_PLUGIN_MODULE_PATH@"}; +inline constexpr std::string_view kPlatformPluginDataPath{"@OBS_PLATFORM_PLUGIN_DATA_PATH@"}; +} // namespace OBS::Constants diff --git a/frontend/plugin-manager/LoaderPaths_Flatpak.hpp.in b/frontend/plugin-manager/LoaderPaths_Flatpak.hpp.in new file mode 100644 index 000000000..0b49c3bb6 --- /dev/null +++ b/frontend/plugin-manager/LoaderPaths_Flatpak.hpp.in @@ -0,0 +1,30 @@ +/****************************************************************************** + Copyright (C) 2026 by FiniteSingularity + + 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 . +******************************************************************************/ + +#pragma once + +#include + +namespace OBS::Constants { +inline constexpr std::string_view kPlatformInstallPath{"@OBS_PLATFORM_INSTALL_PATH@"}; +inline constexpr std::string_view kPlatformLibraryPath{"@OBS_PLATFORM_LIBRARY_PATH@"}; +inline constexpr std::string_view kPlatformDataPath{"@OBS_PLATFORM_DATA_PATH@"}; +inline constexpr std::string_view kPlatformPluginModulePath{"@OBS_PLATFORM_PLUGIN_MODULE_PATH@"}; +inline constexpr std::string_view kPlatformPluginDataPath{"@OBS_PLATFORM_PLUGIN_DATA_PATH@"}; +inline constexpr std::string_view kXDGConfigHomeVariable{"XDG_CONFIG_HOME"}; +inline constexpr std::string_view kXDGDataHomeVariable{"XDG_DATA_HOME"}; +} // namespace OBS::Constants diff --git a/frontend/plugin-manager/LoaderPaths_Linux.hpp.in b/frontend/plugin-manager/LoaderPaths_Linux.hpp.in new file mode 100644 index 000000000..0b49c3bb6 --- /dev/null +++ b/frontend/plugin-manager/LoaderPaths_Linux.hpp.in @@ -0,0 +1,30 @@ +/****************************************************************************** + Copyright (C) 2026 by FiniteSingularity + + 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 . +******************************************************************************/ + +#pragma once + +#include + +namespace OBS::Constants { +inline constexpr std::string_view kPlatformInstallPath{"@OBS_PLATFORM_INSTALL_PATH@"}; +inline constexpr std::string_view kPlatformLibraryPath{"@OBS_PLATFORM_LIBRARY_PATH@"}; +inline constexpr std::string_view kPlatformDataPath{"@OBS_PLATFORM_DATA_PATH@"}; +inline constexpr std::string_view kPlatformPluginModulePath{"@OBS_PLATFORM_PLUGIN_MODULE_PATH@"}; +inline constexpr std::string_view kPlatformPluginDataPath{"@OBS_PLATFORM_PLUGIN_DATA_PATH@"}; +inline constexpr std::string_view kXDGConfigHomeVariable{"XDG_CONFIG_HOME"}; +inline constexpr std::string_view kXDGDataHomeVariable{"XDG_DATA_HOME"}; +} // namespace OBS::Constants diff --git a/frontend/plugin-manager/PluginManager.cpp b/frontend/plugin-manager/PluginManager.cpp index 5cd351d45..b2fc421c3 100644 --- a/frontend/plugin-manager/PluginManager.cpp +++ b/frontend/plugin-manager/PluginManager.cpp @@ -39,7 +39,7 @@ void addModuleToPluginManagerImpl(void *param, obs_module_t *newModule) std::string moduleName = obs_get_module_file_name(newModule); moduleName = moduleName.substr(0, moduleName.rfind(".")); - if (!obs_get_module_allow_disable(moduleName.c_str())) { + if (obs_is_core_module(newModule)) { return; } @@ -67,7 +67,7 @@ constexpr std::string_view OBSPluginManagerModulesFile = "modules.json"; void PluginManager::preLoad() { - loadModules_(); + loadModuleConfiguration_(); disableModules_(); } @@ -82,6 +82,32 @@ void PluginManager::postLoad() linkUnloadedModules_(); } +void PluginManager::loadAllPlugins(bool usePortableMode) +{ + preLoad(); + bool coreModuleLoadSuccess = obs_load_core_modules(); + + if (!coreModuleLoadSuccess) { + loadState_ = State::Failure; + //TODO: Replace with structured exception type - https://github.com/obsproject/obs-studio/issues/13394 + throw "Failed to load core OBS modules. OBS cannot run without these modules. Please try reinstalling OBS."; + } + + if (loadMode_ == Mode::Full) { + blog(LOG_INFO, "---------------------------------"); + State pluginState = loadPlugins(usePortableMode); + State legacyPluginState = loadLegacyPlugins(usePortableMode); + + loadState_ = (pluginState && legacyPluginState) ? State::Success : State::PartialFailure; + } + + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + postLoad(); +} + std::filesystem::path PluginManager::getConfigFilePath_() { std::filesystem::path path = App()->userPluginManagerSettingsLocation / @@ -90,7 +116,7 @@ std::filesystem::path PluginManager::getConfigFilePath_() return path; } -void PluginManager::loadModules_() +void PluginManager::loadModuleConfiguration_() { auto modulesFile = getConfigFilePath_(); if (std::filesystem::exists(modulesFile)) { @@ -282,7 +308,12 @@ void PluginManager::disableModules_() void PluginManager::open() { auto main = OBSBasic::Get(); - PluginManagerWindow pluginManagerWindow(modules_, main); + PluginManagerWindow pluginManagerWindow(modules_, failedModules_, main); + + if (loadState_ == State::PartialFailure) { + pluginManagerWindow.setPage(PluginManagerWindow::Page::Failure); + } + auto result = pluginManagerWindow.exec(); if (result == QDialog::Accepted) { modules_ = pluginManagerWindow.result(); @@ -310,3 +341,45 @@ void PluginManager::open() } }; // namespace OBS + +bool operator&&(const OBS::PluginManager::State &lhs, const OBS::PluginManager::State &rhs) +{ + using State = OBS::PluginManager::State; + + if (lhs == State::Success && rhs == State::Success) { + return true; + } + + return false; +} + +bool operator&&(const OBS::PluginManager::State &lhs, bool rhs) +{ + return (lhs == OBS::PluginManager::State::Success && rhs); +} + +bool operator&&(bool lhs, const OBS::PluginManager::State &rhs) +{ + return (lhs && rhs == OBS::PluginManager::State::Success); +} + +bool operator||(const OBS::PluginManager::State &lhs, const OBS::PluginManager::State &rhs) +{ + using State = OBS::PluginManager::State; + + if (lhs == State::Success || rhs == State::Success) { + return true; + } + + return false; +} + +bool operator||(const OBS::PluginManager::State &lhs, bool rhs) +{ + return (lhs == OBS::PluginManager::State::Success || rhs); +} + +bool operator||(bool lhs, const OBS::PluginManager::State &rhs) +{ + return (lhs || rhs == OBS::PluginManager::State::Success); +} diff --git a/frontend/plugin-manager/PluginManager.hpp b/frontend/plugin-manager/PluginManager.hpp index 2f6d5808a..90af0367e 100644 --- a/frontend/plugin-manager/PluginManager.hpp +++ b/frontend/plugin-manager/PluginManager.hpp @@ -43,22 +43,41 @@ struct ModuleInfo { }; class PluginManager { +public: + using ModuleList = std::vector; + enum class Mode { CoreOnly, Full }; + enum class State { Failure, PartialFailure, Success }; + private: + Mode loadMode_{Mode::Full}; + State loadState_{State::Failure}; + std::vector modules_ = {}; - std::vector disabledSources_ = {}; - std::vector disabledOutputs_ = {}; - std::vector disabledServices_ = {}; - std::vector disabledEncoders_ = {}; + ModuleList failedModules_ = {}; + ModuleList disabledSources_ = {}; + ModuleList disabledOutputs_ = {}; + ModuleList disabledServices_ = {}; + ModuleList disabledEncoders_ = {}; std::filesystem::path getConfigFilePath_(); - void loadModules_(); + void loadModuleConfiguration_(); void saveModules_(); void disableModules_(); void addModuleTypes_(); void linkUnloadedModules_(); + State loadPlugins(bool usePortableMode); + State loadLegacyPlugins(bool usePortableMode); + public: + Mode pluginMode() { return loadMode_; } + void setPluginMode(Mode mode) { loadMode_ = mode; } + + State loadState() { return loadState_; } + + void disablePlugins(); void preLoad(); void postLoad(); + void loadAllPlugins(bool usePortableMode); void open(); friend void addModuleToPluginManagerImpl(void *param, obs_module_t *newModule); @@ -68,6 +87,14 @@ void addModuleToPluginManagerImpl(void *param, obs_module_t *newModule); }; // namespace OBS +bool operator&&(const OBS::PluginManager::State &lhs, const OBS::PluginManager::State &rhs); +bool operator&&(const OBS::PluginManager::State &lhs, bool rhs); +bool operator&&(bool lhs, const OBS::PluginManager::State &rhs); + +bool operator||(const OBS::PluginManager::State &lhs, const OBS::PluginManager::State &rhs); +bool operator||(const OBS::PluginManager::State &lhs, bool rhs); +bool operator||(bool lhs, const OBS::PluginManager::State &rhs); + // Anonymous namespace function to add module to plugin manager // via libobs's module enumeration. namespace { diff --git a/frontend/plugin-manager/PluginManagerWindow.cpp b/frontend/plugin-manager/PluginManagerWindow.cpp index b29cb12c8..75411690c 100644 --- a/frontend/plugin-manager/PluginManagerWindow.cpp +++ b/frontend/plugin-manager/PluginManagerWindow.cpp @@ -33,7 +33,9 @@ extern bool safe_mode; namespace OBS { -PluginManagerWindow::PluginManagerWindow(std::vector const &modules, QWidget *parent) +PluginManagerWindow::PluginManagerWindow(std::vector const &modules, + std::vector const &failedModules, QWidget *parent) + : QDialog(parent), modules_(modules), ui(new Ui::PluginManagerWindow) @@ -61,6 +63,9 @@ PluginManagerWindow::PluginManagerWindow(std::vector const &modules, QListWidgetItem *installed = new QListWidgetItem(QTStr("PluginManager.Section.Manage")); ui->sectionList->addItem(installed); + QListWidgetItem *failed = new QListWidgetItem("Failed"); + ui->sectionList->addItem(failed); + QListWidgetItem *updates = new QListWidgetItem(QTStr("PluginManager.Section.Updates")); updates->setFlags(updates->flags() & ~Qt::ItemIsEnabled); updates->setFlags(updates->flags() & ~Qt::ItemIsSelectable); @@ -87,22 +92,33 @@ PluginManagerWindow::PluginManagerWindow(std::vector const &modules, int row = 0; int missingIndex = -1; for (auto &metadata : modules_) { - std::string id = metadata.module_name; + std::string_view id{metadata.module_name}; // Check if the module is missing: - bool missing = !obs_get_module(id.c_str()) && !obs_get_disabled_module(id.c_str()); + obs_module_t *moduleData = obs_get_module(id.data()); + + if (!moduleData) { + moduleData = obs_get_disabled_module(id.data()); + } + + bool isMissingModule = !moduleData; + bool isLegacyModule = !isMissingModule && obs_is_legacy_module(moduleData); QString name = !metadata.display_name.empty() ? metadata.display_name.c_str() : metadata.module_name.c_str(); - if (missing && missingIndex == -1) { + if (isMissingModule && missingIndex == -1) { missingIndex = row; } + if (isLegacyModule) { + name += " LEGACY"; + } + auto item = new QCheckBox(name); item->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); item->setChecked(metadata.enabled); - if (!metadata.enabledAtLaunch || missing) { + if (!metadata.enabledAtLaunch || isMissingModule) { item->setProperty("class", "text-muted"); } @@ -116,6 +132,19 @@ PluginManagerWindow::PluginManagerWindow(std::vector const &modules, row++; } + QLabel *item = new QLabel("FAILED ITEMS"); + item->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); + ui->modulesList->layout()->addWidget(item); + + for (const std::string &moduleName : failedModules) { + QString name = QString::fromStdString(moduleName); + + QLabel *item = new QLabel(name); + item->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); + item->setProperty("class", "text-muted"); + ui->modulesList->layout()->addWidget(item); + } + QVBoxLayout *layout = qobject_cast(ui->modulesList->layout()); if (safe_mode) { QLabel *safeModeLabel = new QLabel(ui->modulesList); @@ -175,4 +204,18 @@ bool PluginManagerWindow::isEnabledPluginsChanged() return result; } +void PluginManagerWindow::setPage(Page page) +{ + switch (page) { + case Page::Installed: + ui->sectionList->setCurrentRow(1); + break; + case Page::Failure: + ui->sectionList->setCurrentRow(2); + break; + default: + break; + } +} + }; // namespace OBS diff --git a/frontend/plugin-manager/PluginManagerWindow.hpp b/frontend/plugin-manager/PluginManagerWindow.hpp index 4c851067b..54e19e4a9 100644 --- a/frontend/plugin-manager/PluginManagerWindow.hpp +++ b/frontend/plugin-manager/PluginManagerWindow.hpp @@ -30,9 +30,14 @@ class PluginManagerWindow : public QDialog { std::unique_ptr ui; public: - explicit PluginManagerWindow(std::vector const &modules, QWidget *parent = nullptr); + enum class Page { Installed, Failure }; + + explicit PluginManagerWindow(std::vector const &modules, + std::vector const &failedModules, QWidget *parent = nullptr); inline std::vector const result() { return modules_; } + void setPage(Page page); + private: std::vector modules_; diff --git a/frontend/plugin-manager/PluginModuleLoader.hpp b/frontend/plugin-manager/PluginModuleLoader.hpp new file mode 100644 index 000000000..386591294 --- /dev/null +++ b/frontend/plugin-manager/PluginModuleLoader.hpp @@ -0,0 +1,66 @@ +/****************************************************************************** + Copyright (C) 2026 by FiniteSingularity + + 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 . +******************************************************************************/ + +#pragma once + +#include + +#include +#include +#include +#include + +using FailureInfo = obs_module_failure_info; +using ModuleLoadInfo = obs_runtime_module_info; +using ModuleList = std::vector; + +inline constexpr std::string_view kPathVariable{"OBS_PLUGINS_PATH"}; +inline constexpr std::string_view kLegacyBinaryPathVariable{"OBS_LEGACY_PLUGINS_PATH"}; +inline constexpr std::string_view kLegacyDataPathVariable{"OBS_LEGACY_PLUGINS_DATA_PATH"}; + +inline std::string getEnvironmentVariable(std::string_view variableName) +{ + std::unique_ptr variablePointer{}; + variablePointer.reset(getenv(variableName.data())); + + if (!variablePointer) { + return {}; + } + + std::string result{variablePointer.release()}; + + return result; +} + +inline int loadPluginsByInfo(const ModuleLoadInfo &info, ModuleList &failedModules) +{ + FailureInfo result = {0}; + + obs_load_plugins(const_cast(std::addressof(info)), std::addressof(result)); + + for (size_t i = 0; i < result.count; ++i) { + const char *failedPluginName = result.failed_modules[i]; + + if (failedPluginName && *failedPluginName) { + failedModules.emplace_back(failedPluginName); + } + } + + obs_module_failure_info_free(std::addressof(result)); + + return static_cast(result.count); +} diff --git a/frontend/plugin-manager/PluginModuleLoader_BSD.cpp b/frontend/plugin-manager/PluginModuleLoader_BSD.cpp new file mode 100644 index 000000000..4061478d8 --- /dev/null +++ b/frontend/plugin-manager/PluginModuleLoader_BSD.cpp @@ -0,0 +1,220 @@ +/****************************************************************************** + Copyright (C) 2026 by FiniteSingularity + + 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 . +******************************************************************************/ + +#include "PluginModuleLoader.hpp" +#include "PluginManager.hpp" +#include + +#include +#include +#include + +#include +#include +#include +#include + +// BSD Third-Party Plugin Locations +// +// * User Path: +// * Binary: //.so +// * Data: //data +// +// * System Root Path: /usr +// * Binary: //obs-modules/plugins/.so +// * Data: /share/obs/obs-modules/plugins/ +// +// * User Root Path: /home//.config +// * Binary: /obs-studio/plugins//.so +// * Data: /obs-studio/plugins//data +// +// * Legacy User Path: + +// * Binary: /.so +// * Data: / +// +// * Legacy System Root Path: /usr +// * Binary: //obs-plugins/.so +// * Data: /share/obs/obs-plugins/ +// +// * Legacy User Root Path: /home//.config +// * Binary: /obs-studio/plugins//bin/64bit/.so +// * Data: /obs-studio/plugins//data +// +// * Legacy Fallback Path: +// * Binary: /../../obs-plugins/64bit/.so +// * Data: /share/obs/obs-plugins/ +// + +using State = OBS::PluginManager::State; +using ModuleType = obs_runtime_module_type; + +namespace Constants = OBS::Constants; + +constexpr std::string_view kModulePathSuffix{"/%module%/"}; +constexpr std::string_view kModuleDataPathSuffix{"/%module%/data/"}; +constexpr std::string_view kConfigBinaryPath{"/obs-studio/plugins/%module%/"}; +constexpr std::string_view kConfigDataPath{"/obs-studio/plugins/%module%/data/"}; + +constexpr std::string_view kLegacyConfigBinaryPath{"/obs-studio/plugins/%module%/bin/64bit"}; +constexpr std::string_view kLegacyConfigDataPath{"/obs-studio/plugins/%module%/data"}; + +constexpr bool hasSystemPluginPath = !Constants::kPlatformPluginModulePath.empty() && + !Constants::kPlatformPluginDataPath.empty(); + +namespace { +State pluginLoadHelper(const ModuleLoadInfo &info, ModuleList &failedModules) +{ + int failedModuleCount = loadPluginsByInfo(info, failedModules); + + State result = (failedModuleCount > 0) ? State::PartialFailure : State::Success; + + return result; +} +} // namespace + +namespace OBS { +State PluginManager::loadPlugins(bool usePortableMode) +{ + State userPluginState = State::Failure; + std::string userPluginPath = getEnvironmentVariable(kPathVariable); + + if (!userPluginPath.empty()) { + std::string binaryPath{userPluginPath}; + std::string dataPath{userPluginPath}; + binaryPath.append(kModulePathSuffix); + dataPath.append(kModuleDataPathSuffix); + + ModuleLoadInfo info = {.path_info = {.binary = binaryPath.c_str(), .data = dataPath.c_str()}, + .type = OBS_MODULE_TYPE_PLUGIN, + .name = nullptr}; + + userPluginState = pluginLoadHelper(info, failedModules_); + } else { + userPluginState = State::Success; + } + + State systemPluginState = State::Failure; + + if constexpr (hasSystemPluginPath) { + std::string binaryPath{Constants::kPlatformPluginModulePath}; + std::string dataPath{Constants::kPlatformPluginDataPath}; + + dataPath.append(kModulePathSuffix); + + ModuleLoadInfo info = {.path_info = {.binary = binaryPath.c_str(), .data = dataPath.c_str()}, + .type = OBS_MODULE_TYPE_PLUGIN, + .name = nullptr}; + + systemPluginState = pluginLoadHelper(info, failedModules_); + } else { + systemPluginState = State::Success; + } + + State configPluginState = State::Failure; + + if (!usePortableMode) { + std::string userConfigPath{}; + userConfigPath.resize(PATH_MAX); + int bufferSize = os_get_config_path(userConfigPath.data(), userConfigPath.size(), nullptr); + + if (bufferSize > 0) { + userConfigPath.resize(bufferSize); + std::string binaryPath{userConfigPath}; + std::string dataPath{userConfigPath}; + + binaryPath.append(kConfigBinaryPath); + dataPath.append(kConfigDataPath); + + ModuleLoadInfo info = {.path_info = {.binary = binaryPath.c_str(), .data = dataPath.c_str()}, + .type = OBS_MODULE_TYPE_PLUGIN, + .name = nullptr}; + configPluginState = pluginLoadHelper(info, failedModules_); + } + } else { + configPluginState = State::Success; + } + + return (userPluginState && systemPluginState && configPluginState) ? State::Success : State::PartialFailure; +} + +State PluginManager::loadLegacyPlugins(bool usePortableMode) +{ + State userPluginState = State::Failure; + + std::string userPluginPath = getEnvironmentVariable(kLegacyBinaryPathVariable); + std::string userDataPath = getEnvironmentVariable(kLegacyDataPathVariable); + + if (!userPluginPath.empty() && !userDataPath.empty()) { + std::string binaryPath{userPluginPath}; + std::string dataPath{userDataPath}; + + dataPath.append(kModulePathSuffix); + + ModuleLoadInfo info = {.path_info = {.binary = binaryPath.c_str(), .data = dataPath.c_str()}, + .type = OBS_MODULE_TYPE_LEGACY_PLUGIN, + .name = nullptr}; + + userPluginState = pluginLoadHelper(info, failedModules_); + } else { + userPluginState = State::Success; + } + + State systemPluginState = State::Failure; + + { + std::string binaryPath{Constants::kPlatformInstallPath}; + std::string dataPath{Constants::kPlatformInstallPath}; + binaryPath.append(Constants::kPlatformLibraryPath); + binaryPath.append("obs-plugins/"); + dataPath.append(Constants::kPlatformDataPath); + dataPath.append("obs/obs-plugins/%module%"); + + ModuleLoadInfo info = {.path_info = {.binary = binaryPath.c_str(), .data = dataPath.c_str()}, + .type = OBS_MODULE_TYPE_LEGACY_PLUGIN, + .name = nullptr}; + + systemPluginState = pluginLoadHelper(info, failedModules_); + } + + State configPluginState = State::Failure; + + if (!usePortableMode) { + std::string userConfigPath{}; + userConfigPath.resize(PATH_MAX); + int bufferSize = os_get_config_path(userConfigPath.data(), userConfigPath.size(), nullptr); + + if (bufferSize > 0) { + userConfigPath.resize(bufferSize); + std::string binaryPath{userConfigPath}; + std::string dataPath{userConfigPath}; + + binaryPath.append(kLegacyConfigBinaryPath); + dataPath.append(kLegacyConfigDataPath); + + ModuleLoadInfo info = {.path_info = {.binary = binaryPath.c_str(), .data = dataPath.c_str()}, + .type = OBS_MODULE_TYPE_LEGACY_PLUGIN, + .name = nullptr}; + + configPluginState = pluginLoadHelper(info, failedModules_); + } else { + configPluginState = State::Success; + } + } + + return (userPluginState && systemPluginState && configPluginState) ? State::Success : State::PartialFailure; +} +} // namespace OBS diff --git a/frontend/plugin-manager/PluginModuleLoader_Flatpak.cpp b/frontend/plugin-manager/PluginModuleLoader_Flatpak.cpp new file mode 100644 index 000000000..096d92b3e --- /dev/null +++ b/frontend/plugin-manager/PluginModuleLoader_Flatpak.cpp @@ -0,0 +1,142 @@ +/****************************************************************************** + Copyright (C) 2026 by FiniteSingularity + + 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 . +******************************************************************************/ + +#include "PluginModuleLoader.hpp" +#include "PluginManager.hpp" +#include + +#include +#include +#include + +#include +#include +#include +#include + +// Flatpak Third-Party Plugin Locations +// +// * Flatpak Extension Point Path: /app/plugins +// * Binary: /app/plugins//obs-modules/plugins/.so +// * Data: /app/plugins//share/obs/obs-modules/plugins/ +// +// * Legacy Flatpak Root Path: /app/plugins +// * Binary: //obs-plugins/.so +// * Data: /share/obs/obs-plugins/ +// +// * XDG Data Home Path: +// * Binary: /obs-studio/plugins//.so +// * Data: /obs-studio/plugins//data +// +// * XDG Config Home Path: +// * Binary: /obs-studio/plugins//bin/64bit/.so +// * Data: /obs-studio/plugins//data + +using State = OBS::PluginManager::State; +using ModuleType = obs_runtime_module_type; + +namespace Constants = OBS::Constants; + +constexpr std::string_view kConfigBinaryPath{"/obs-studio/plugins/%module%/"}; +constexpr std::string_view kConfigDataPath{"/obs-studio/plugins/%module%/data/"}; +constexpr std::string_view kLegacyConfigBinaryPath{"/obs-studio/plugins/%module%/bin/64bit"}; +constexpr std::string_view kLegacyConfigDataPath{"/obs-studio/plugins/%module%/data"}; +constexpr std::string_view kFlatpakBasePath{"/app/plugins/"}; + +namespace { +State pluginLoadHelper(const ModuleLoadInfo &info, ModuleList &failedModules) +{ + int failedModuleCount = loadPluginsByInfo(info, failedModules); + + State result = (failedModuleCount > 0) ? State::PartialFailure : State::Success; + + return result; +} +} // namespace + +namespace OBS { +State PluginManager::loadPlugins(bool /*usePortableMode*/) +{ + State flatpakPluginState = State::Failure; + + { + std::string binaryPath{kFlatpakBasePath}; + std::string dataPath{kFlatpakBasePath}; + binaryPath.append(Constants::kPlatformLibraryPath); + binaryPath.append("obs-modules/plugins/"); + dataPath.append(Constants::kPlatformDataPath); + dataPath.append("obs-modules/plugins/%module%"); + ModuleLoadInfo info = {.path_info = {.binary = binaryPath.c_str(), .data = dataPath.c_str()}, + .type = OBS_MODULE_TYPE_PLUGIN, + .name = nullptr}; + + flatpakPluginState = pluginLoadHelper(info, failedModules_); + } + + State xdgDataHomeState = State::Failure; + std::string xdgDataHomePath = getEnvironmentVariable(Constants::kXDGDataHomeVariable); + if (!xdgDataHomePath.empty()) { + std::string binaryPath{xdgDataHomePath}; + std::string dataPath{xdgDataHomePath}; + binaryPath.append(kConfigBinaryPath); + dataPath.append(kConfigDataPath); + ModuleLoadInfo info = {.path_info = {.binary = binaryPath.c_str(), .data = dataPath.c_str()}, + .type = OBS_MODULE_TYPE_PLUGIN, + .name = nullptr}; + + xdgDataHomeState = pluginLoadHelper(info, failedModules_); + } else { + xdgDataHomeState = State::Success; + } + + return (flatpakPluginState && xdgDataHomeState) ? State::Success : State::PartialFailure; +} + +State PluginManager::loadLegacyPlugins(bool /*usePortableMode*/) +{ + State flatpakPluginState = State::Failure; + + { + std::string binaryPath{kFlatpakBasePath}; + std::string dataPath{kFlatpakBasePath}; + binaryPath.append(Constants::kPlatformLibraryPath); + binaryPath.append("obs-plugins/"); + dataPath.append(Constants::kPlatformDataPath); + dataPath.append("obs/obs-plugins/%module%"); + ModuleLoadInfo info = {.path_info = {.binary = binaryPath.c_str(), .data = dataPath.c_str()}, + .type = OBS_MODULE_TYPE_LEGACY_PLUGIN, + .name = nullptr}; + + flatpakPluginState = pluginLoadHelper(info, failedModules_); + } + + State xdgDataConfigState = State::Failure; + std::string xdgConfigHomePath = getEnvironmentVariable(Constants::kXDGConfigHomeVariable); + if (!xdgConfigHomePath.empty()) { + std::string binaryPath{xdgConfigHomePath}; + std::string dataPath{xdgConfigHomePath}; + binaryPath.append(kLegacyConfigBinaryPath); + dataPath.append(kLegacyConfigDataPath); + ModuleLoadInfo info = {.path_info = {.binary = binaryPath.c_str(), .data = dataPath.c_str()}, + .type = OBS_MODULE_TYPE_LEGACY_PLUGIN, + .name = nullptr}; + xdgDataConfigState = pluginLoadHelper(info, failedModules_); + } + + return (flatpakPluginState && xdgDataConfigState) ? State::Success : State::PartialFailure; +} +} // namespace OBS diff --git a/frontend/plugin-manager/PluginModuleLoader_Linux.cpp b/frontend/plugin-manager/PluginModuleLoader_Linux.cpp new file mode 100644 index 000000000..ee65e94c4 --- /dev/null +++ b/frontend/plugin-manager/PluginModuleLoader_Linux.cpp @@ -0,0 +1,224 @@ +/****************************************************************************** + Copyright (C) 2026 by FiniteSingularity + + 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 . +******************************************************************************/ + +#include "PluginModuleLoader.hpp" +#include "PluginManager.hpp" +#include + +#include +#include +#include + +#include +#include +#include +#include + +// Linux Third-Party Plugin Locations +// +// * User Path: +// * Binary: //.so +// * Data: //data +// +// * System Root Path: /usr +// * Binary: //obs-modules/plugins/.so +// * Data: /share/obs/obs-modules/plugins/ +// +// * User Root Path: $XDG_DATA_HOME +// * Binary: /obs-studio/plugins//.so +// * Data: /obs-studio/plugins//data +// +// * Legacy User Path: + +// * Binary: /.so +// * Data: / +// +// * Legacy System Root Path: /usr +// * Binary: //obs-plugins/.so +// * Data: /share/obs/obs-plugins/ +// +// * Legacy User Root Path: $XDG_CONFIG_HOME +// * Binary: /obs-studio/plugins//bin/64bit/.so +// * Data: /obs-studio/plugins//data +// +// * Legacy Fallback Path: +// * Binary: /../../obs-plugins/64bit/.so +// * Data: /share/obs/obs-plugins/ + +using State = OBS::PluginManager::State; +using ModuleType = obs_runtime_module_type; + +namespace Constants = OBS::Constants; + +constexpr std::string_view kModulePathSuffix{"/%module%/"}; +constexpr std::string_view kModuleDataPathSuffix{"/%module%/data/"}; +constexpr std::string_view kXdgBinaryPath{"/obs-studio/plugins/%module%/"}; +constexpr std::string_view kXdgDataPath{"/obs-studio/plugins/%module%/data/"}; + +constexpr std::string_view kLegacyXdgBinaryPath{"/obs-studio/plugins/%module%/bin/64bit"}; +constexpr std::string_view kLegacyXdgDataPath{"/obs-studio/plugins/%module%/data"}; + +constexpr bool hasSystemPluginPath = !Constants::kPlatformPluginModulePath.empty() && + !Constants::kPlatformPluginDataPath.empty(); + +namespace { +State pluginLoadHelper(const ModuleLoadInfo &info, ModuleList &failedModules) +{ + int failedModuleCount = loadPluginsByInfo(info, failedModules); + + State result = (failedModuleCount > 0) ? State::PartialFailure : State::Success; + + return result; +} +} // namespace + +namespace OBS { +State PluginManager::loadPlugins(bool usePortableMode) +{ + State userPluginState = State::Failure; + std::string userPluginPath = getEnvironmentVariable(kPathVariable); + + if (!userPluginPath.empty()) { + std::string binaryPath{userPluginPath}; + std::string dataPath{userPluginPath}; + binaryPath.append(kModulePathSuffix); + dataPath.append(kModuleDataPathSuffix); + + ModuleLoadInfo info = {.path_info = {.binary = binaryPath.c_str(), .data = dataPath.c_str()}, + .type = OBS_MODULE_TYPE_PLUGIN, + .name = nullptr}; + + userPluginState = pluginLoadHelper(info, failedModules_); + } else { + userPluginState = State::Success; + } + + State systemPluginState = State::Failure; + + if constexpr (hasSystemPluginPath) { + std::string binaryPath{Constants::kPlatformPluginModulePath}; + std::string dataPath{Constants::kPlatformPluginDataPath}; + + dataPath.append(kModulePathSuffix); + + ModuleLoadInfo info = {.path_info = {.binary = binaryPath.c_str(), .data = dataPath.c_str()}, + .type = OBS_MODULE_TYPE_PLUGIN, + .name = nullptr}; + + systemPluginState = pluginLoadHelper(info, failedModules_); + } else { + systemPluginState = State::Success; + } + + State xdgPluginState = State::Failure; + + if (!usePortableMode) { + std::string xdgDataHomePath = getEnvironmentVariable(Constants::kXDGDataHomeVariable); + if (xdgDataHomePath.empty()) { + std::string homePath = getEnvironmentVariable("HOME"); + if (!homePath.empty()) { + xdgDataHomePath = homePath + "/.local/share"; + } + } + if (!xdgDataHomePath.empty()) { + std::string binaryPath{xdgDataHomePath}; + std::string dataPath{xdgDataHomePath}; + binaryPath.append(kXdgBinaryPath); + dataPath.append(kXdgDataPath); + ModuleLoadInfo info = {.path_info = {.binary = binaryPath.c_str(), .data = dataPath.c_str()}, + .type = OBS_MODULE_TYPE_PLUGIN, + .name = nullptr}; + + xdgPluginState = pluginLoadHelper(info, failedModules_); + } else { + xdgPluginState = State::Success; + } + } else { + xdgPluginState = State::Success; + } + + return (userPluginState && systemPluginState && xdgPluginState) ? State::Success : State::PartialFailure; +} + +State PluginManager::loadLegacyPlugins(bool usePortableMode) +{ + State userPluginState = State::Failure; + + std::string userPluginPath = getEnvironmentVariable(kLegacyBinaryPathVariable); + std::string userDataPath = getEnvironmentVariable(kLegacyDataPathVariable); + + if (!userPluginPath.empty() && !userDataPath.empty()) { + std::string binaryPath{userPluginPath}; + std::string dataPath{userDataPath}; + + dataPath.append(kModulePathSuffix); + + ModuleLoadInfo info = {.path_info = {.binary = binaryPath.c_str(), .data = dataPath.c_str()}, + .type = OBS_MODULE_TYPE_LEGACY_PLUGIN, + .name = nullptr}; + + userPluginState = pluginLoadHelper(info, failedModules_); + } else { + userPluginState = State::Success; + } + + State systemPluginState = State::Failure; + + { + std::string binaryPath{Constants::kPlatformInstallPath}; + std::string dataPath{Constants::kPlatformInstallPath}; + binaryPath.append(Constants::kPlatformLibraryPath); + binaryPath.append("obs-plugins/"); + dataPath.append(Constants::kPlatformDataPath); + dataPath.append("obs/obs-plugins/%module%"); + + ModuleLoadInfo info = {.path_info = {.binary = binaryPath.c_str(), .data = dataPath.c_str()}, + .type = OBS_MODULE_TYPE_LEGACY_PLUGIN, + .name = nullptr}; + + systemPluginState = pluginLoadHelper(info, failedModules_); + } + + State xdgPluginState = State::Failure; + + if (!usePortableMode) { + std::string xdgConfigHomePath = getEnvironmentVariable(Constants::kXDGConfigHomeVariable); + if (xdgConfigHomePath.empty()) { + std::string homePath = getEnvironmentVariable("HOME"); + if (!homePath.empty()) { + xdgConfigHomePath = homePath + "/.config"; + } + } + if (!xdgConfigHomePath.empty()) { + std::string binaryPath{xdgConfigHomePath}; + std::string dataPath{xdgConfigHomePath}; + binaryPath.append(kLegacyXdgBinaryPath); + dataPath.append(kLegacyXdgDataPath); + ModuleLoadInfo info = {.path_info = {.binary = binaryPath.c_str(), .data = dataPath.c_str()}, + .type = OBS_MODULE_TYPE_LEGACY_PLUGIN, + .name = nullptr}; + + xdgPluginState = pluginLoadHelper(info, failedModules_); + } else { + xdgPluginState = State::Success; + } + } else { + xdgPluginState = State::Success; + } + + return (userPluginState && systemPluginState && xdgPluginState) ? State::Success : State::PartialFailure; +} +} // namespace OBS diff --git a/frontend/plugin-manager/PluginModuleLoader_MacOS.mm b/frontend/plugin-manager/PluginModuleLoader_MacOS.mm new file mode 100644 index 000000000..2a6ead8f3 --- /dev/null +++ b/frontend/plugin-manager/PluginModuleLoader_MacOS.mm @@ -0,0 +1,196 @@ +/****************************************************************************** + Copyright (C) 2026 by FiniteSingularity + + 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 . +******************************************************************************/ + +#include "PluginModuleLoader.hpp" +#include "PluginManager.hpp" + +#include +#include +#include + +#include +#include +#include + +// macOS Third-Party Plugin Locations +// +// * User Path: +// * Binary: /.plugin/Contents/MacOS +// * Data: /.plugin/Contents/Resources +// +// * Root Path: /Library/Application Support +// * Binary: /obs-studio/plugins/.plugin/Contents/MacOS/ +// * Data: /obs-studio/plugins/.plugin/Contents/Resources +// +// * Legacy User Path: + +// * Binary: /.plugin/Contents/MacOS/ +// * Data: /.plugin/Contents/Resources +// +// * Legacy System Root Path: /Library/Application Support +// * Legacy binary: /obs-studio/plugins//bin/.so +// * Legacy data: /obs-studio/plugins//data +// +// * Legacy User Root Path: /Library/Application Support +// * Legacy binary: /obs-studio/plugins//bin/.so +// * Legacy data: /obs-studio/plugins//data +// + +using State = OBS::PluginManager::State; +using ModuleType = obs_runtime_module_type; + +constexpr std::string_view kPluginPathSuffix {"obs-studio/plugins/%module%.plugin"}; +constexpr std::string_view kUserPluginPathSuffix {"/%module%.plugin"}; +constexpr std::string_view kLegacyPluginPathSuffix {"obs-studio/plugins/%module%"}; + +#ifdef __aarch64__ +constexpr bool kIsAppleSilicon = true; +#else +constexpr bool kIsAppleSilicon = false; +#endif + +namespace { + State loadPluginsFromPath(const std::string &pathString, ModuleType type, ModuleList &failedModules) + { + std::string binaryPath {pathString}; + std::string dataPath {pathString}; + + switch (type) { + case OBS_MODULE_TYPE_LEGACY_PLUGIN: { + binaryPath.append("/bin"); + dataPath.append("/data"); + break; + } + default: { + binaryPath.append("/Contents/MacOS"); + dataPath.append("/Contents/Resources"); + break; + } + } + + ModuleLoadInfo info = { + .path_info = {.binary = binaryPath.c_str(), .data = dataPath.c_str()}, + .type = type, + .name = nullptr + }; + + int failedModuleCount = loadPluginsByInfo(info, failedModules); + + State result = (failedModuleCount > 0) ? State::PartialFailure : State::Success; + + return result; + } + + State loadPluginsFromLegacyPaths(const std::string &binaryPathString, const std::string &dataPathString, + ModuleList &failedModules) + { + std::string binaryPath {binaryPathString}; + std::string dataPath {dataPathString}; + + binaryPath.append("/%module%.plugin/Contents/MacOS"); + dataPath.append("/%module%.plugin/Contents/Resources"); + + ModuleLoadInfo info = { + .path_info = {.binary = binaryPath.c_str(), .data = dataPath.c_str()}, + .type = OBS_MODULE_TYPE_LEGACY_PLUGIN, + .name = nullptr + }; + + int failedModuleCount = loadPluginsByInfo(info, failedModules); + + State result = (failedModuleCount > 0) ? State::PartialFailure : State::Success; + + return result; + } + +} // namespace + +namespace OBS { + State PluginManager::loadPlugins(bool usePortableMode __unused) + { + State userPluginState = State::Failure; + std::string userPluginPath = getEnvironmentVariable(kPathVariable); + + if (!userPluginPath.empty()) { + userPluginPath.append(kUserPluginPathSuffix); + + userPluginState = loadPluginsFromPath(userPluginPath, OBS_MODULE_TYPE_PLUGIN, failedModules_); + } else { + userPluginState = State::Success; + } + + State libraryPluginState = State::Failure; + + std::string appLibraryPath {}; + appLibraryPath.resize(PATH_MAX); + int bufferSize = os_get_config_path(appLibraryPath.data(), appLibraryPath.size(), kPluginPathSuffix.data()); + if (bufferSize > 0) { + appLibraryPath.resize(bufferSize); + + libraryPluginState = loadPluginsFromPath(appLibraryPath, OBS_MODULE_TYPE_PLUGIN, failedModules_); + } + + return (userPluginState && libraryPluginState) ? State::Success : State::PartialFailure; + } + + State PluginManager::loadLegacyPlugins(bool usePortableMode __unused) + { + State userPluginState = State::Failure; + + std::string userPluginPath = getEnvironmentVariable(kLegacyBinaryPathVariable); + std::string userDataPath = getEnvironmentVariable(kLegacyDataPathVariable); + + if (!userPluginPath.empty() && !userDataPath.empty()) { + userPluginState = loadPluginsFromLegacyPaths(userPluginPath, userDataPath, failedModules_); + } else { + userPluginState = State::Success; + } + + State legacyPluginState = State::Failure; + + if constexpr (!kIsAppleSilicon) { + State systemLibraryState = State::Failure; + + std::string systemLibraryPath {}; + systemLibraryPath.resize(PATH_MAX); + int bufferSize = os_get_program_data_path(systemLibraryPath.data(), systemLibraryPath.size(), + kLegacyPluginPathSuffix.data()); + + if (bufferSize > 0) { + systemLibraryPath.resize(bufferSize); + systemLibraryState = + loadPluginsFromPath(systemLibraryPath, OBS_MODULE_TYPE_LEGACY_PLUGIN, failedModules_); + } + + State appLibrayState = State::Failure; + std::string appLibraryPath {}; + appLibraryPath.resize(PATH_MAX); + bufferSize = + os_get_config_path(appLibraryPath.data(), appLibraryPath.size(), kLegacyPluginPathSuffix.data()); + + if (bufferSize > 0) { + appLibraryPath.resize(bufferSize); + appLibrayState = loadPluginsFromPath(appLibraryPath, OBS_MODULE_TYPE_LEGACY_PLUGIN, failedModules_); + } + + legacyPluginState = (systemLibraryState && appLibrayState) ? State::Success : State::PartialFailure; + } else { + legacyPluginState = State::Success; + } + + return (userPluginState && legacyPluginState) ? State::Success : State::PartialFailure; + } +} // namespace OBS diff --git a/frontend/plugin-manager/PluginModuleLoader_Windows.cpp b/frontend/plugin-manager/PluginModuleLoader_Windows.cpp new file mode 100644 index 000000000..d0d43970a --- /dev/null +++ b/frontend/plugin-manager/PluginModuleLoader_Windows.cpp @@ -0,0 +1,196 @@ +/****************************************************************************** + Copyright (C) 2026 by FiniteSingularity + + 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 . +******************************************************************************/ + +#include "PluginModuleLoader.hpp" +#include "PluginManager.hpp" + +#include +#include +#include + +#include +#include +#include + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#undef WIN32_LEAN_AND_MEAN + +// Windows Third-Party Plugin Locations +// +// * User Path: +// * Binary: //.dll +// * Data: //data +// +// * Root Path: C:/ProgramData +// * Binary: /obs-studio/plugins//.dll +// * Data: /obs-studio/plugins//data +// +// * Portable Root Path: +// * Binary: /../../plugins//.dll +// * Data: /../../plugins//data +// +// * Legacy User Path: + +// * Binary: /.dll +// * Data: //data +// +// * Legacy Root Path: +// * Binary: /../../obs-plugins/.dll +// * Data: /../../data/obs-plugins/ +// +// * Legacy Portable Path: +// * Binary: /../../obs-plugins/.dll +// * Data: /../../data/obs-plugins/ +// +// * Legacy System Path: C:/ProgramData +// * Binary: /obs-studio/plugins//bin/.dll +// * Data: /obs-studio/plugins//data +// + +using State = OBS::PluginManager::State; +using ModuleType = obs_runtime_module_type; + +constexpr std::string_view kPluginPathSuffix{"obs-studio/plugins/%module%"}; +constexpr std::string_view kUserPluginPathSuffix{"/%module%"}; +constexpr std::string_view kPortablePluginPath{"../../plugins/%module%"}; + +constexpr std::string_view kLegacyPortableBinaryPath{"../../obs-plugins/64bit/"}; +constexpr std::string_view kLegacyPortableDataPath{"../../data/obs-plugins/%module%"}; + +namespace { +State loadPluginsFromPath(const std::string &pathString, ModuleType type, ModuleList &failedModules) +{ + std::string binaryPath{pathString}; + std::string dataPath{pathString}; + + switch (type) { + case OBS_MODULE_TYPE_LEGACY_PLUGIN: { + binaryPath.append("/bin/64bit"); + dataPath.append("/data"); + break; + } + default: { + dataPath.append("/data"); + break; + } + } + + ModuleLoadInfo info = {0}; + info.path_info.binary = binaryPath.c_str(); + info.path_info.data = dataPath.c_str(); + info.type = type; + info.name = nullptr; + + int failedModuleCount = loadPluginsByInfo(info, failedModules); + + State result = (failedModuleCount > 0) ? State::PartialFailure : State::Success; + + return result; +} +} // namespace + +namespace OBS { +State PluginManager::loadPlugins(bool usePortableMode) +{ + State userPluginState = State::Failure; + std::string userPluginPath = getEnvironmentVariable(kPathVariable); + + if (!userPluginPath.empty()) { + userPluginPath.append(kUserPluginPathSuffix); + + userPluginState = loadPluginsFromPath(userPluginPath, OBS_MODULE_TYPE_PLUGIN, failedModules_); + } else { + userPluginState = State::Success; + } + + State libraryPluginState = State::Failure; + std::string appLibraryPath{}; + + if (!usePortableMode) { + appLibraryPath.resize(MAX_PATH); + int bufferSize = os_get_program_data_path(appLibraryPath.data(), appLibraryPath.size(), + kPluginPathSuffix.data()); + if (bufferSize > 0) { + appLibraryPath.resize(bufferSize); + } + } else { + appLibraryPath = kPortablePluginPath; + } + + libraryPluginState = loadPluginsFromPath(appLibraryPath, OBS_MODULE_TYPE_PLUGIN, failedModules_); + + return (userPluginState && libraryPluginState) ? State::Success : State::PartialFailure; +} + +State PluginManager::loadLegacyPlugins(bool usePortableMode) +{ + State userPluginState = State::Failure; + + std::string userPluginPath = getEnvironmentVariable(kLegacyBinaryPathVariable); + std::string userDataPath = getEnvironmentVariable(kLegacyDataPathVariable); + + if (!userPluginPath.empty() && !userDataPath.empty()) { + userDataPath.append(kUserPluginPathSuffix); + + ModuleLoadInfo info = {0}; + info.path_info.binary = userPluginPath.c_str(); + info.path_info.data = userDataPath.c_str(); + info.type = OBS_MODULE_TYPE_LEGACY_PLUGIN; + info.name = nullptr; + + int failedModuleCount = loadPluginsByInfo(info, failedModules_); + + userPluginState = (failedModuleCount > 0) ? State::PartialFailure : State::Success; + } else { + userPluginState = State::Success; + } + + State portablePluginState = State::Failure; + + { + ModuleLoadInfo info = {0}; + info.path_info.binary = kLegacyPortableBinaryPath.data(); + info.path_info.data = kLegacyPortableDataPath.data(); + info.type = OBS_MODULE_TYPE_LEGACY_PLUGIN; + info.name = nullptr; + + int failedModuleCount = loadPluginsByInfo(info, failedModules_); + + portablePluginState = (failedModuleCount > 0) ? State::PartialFailure : State::Success; + } + + State libraryPluginState = State::Failure; + + if (!usePortableMode) { + std::string appLibraryPath{}; + appLibraryPath.resize(MAX_PATH); + int bufferSize = os_get_program_data_path(appLibraryPath.data(), appLibraryPath.size(), + kPluginPathSuffix.data()); + if (bufferSize > 0) { + appLibraryPath.resize(bufferSize); + libraryPluginState = + loadPluginsFromPath(appLibraryPath, OBS_MODULE_TYPE_LEGACY_PLUGIN, failedModules_); + } + } else { + libraryPluginState = State::Success; + } + + return (userPluginState && portablePluginState && libraryPluginState) ? State::Success : State::PartialFailure; +} +} // namespace OBS diff --git a/frontend/widgets/OBSBasic.cpp b/frontend/widgets/OBSBasic.cpp index f03ffa3c2..76b95bd13 100644 --- a/frontend/widgets/OBSBasic.cpp +++ b/frontend/widgets/OBSBasic.cpp @@ -66,7 +66,6 @@ #include #endif #include -#include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -113,146 +112,6 @@ namespace { std::once_flag saveOnceFlag; } -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) { - plugins_path = s; - } - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) { - plugins_data_path = s; - } - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - -#if !defined(__APPLE__) -#if defined(_WIN32) - char *thirdPartyPluginPath = os_get_executable_path_ptr("../../obs-plugins/64bit/"); - char *thirdPartyDataPath = os_get_executable_path_ptr("../../data/obs-plugins/%module%"); - - if (thirdPartyPluginPath && *thirdPartyPluginPath && thirdPartyDataPath && *thirdPartyDataPath) { - obs_add_module_path(thirdPartyPluginPath, thirdPartyDataPath); - } - - bfree(thirdPartyPluginPath); - bfree(thirdPartyDataPath); -#else - constexpr std::string_view findToken{"obs-modules"}; - constexpr std::string_view replaceToken{"obs-plugins"}; - constexpr std::string_view thirdPartyDataPath{OBS_INSTALL_DATA_PATH "/obs-plugins/%module%"}; - - std::string thirdPartyPluginPath{OBS_INSTALL_PREFIX "/" OBS_PLUGIN_DESTINATION}; - size_t startPos = thirdPartyPluginPath.find(findToken); - if (startPos != std::string::npos) { - thirdPartyPluginPath.replace(startPos, findToken.length(), replaceToken); - obs_add_module_path(thirdPartyPluginPath.c_str(), thirdPartyDataPath.data()); - } -#endif -#endif - - if (portable_mode) { - return; - } - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) { - return; - } - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#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 -} - -static void SetCoreModuleNames() -{ -#ifndef SAFE_MODULES - throw "SAFE_MODULES not defined"; -#else - std::string safeModules = SAFE_MODULES; - if (safeModules.empty()) { - throw "SAFE_MODULES is empty"; - } - string module; - stringstream modules_(SAFE_MODULES); - - while (getline(modules_, module, '|')) { - obs_add_core_module(module.c_str()); - } -#endif -} - extern void setupDockAction(QDockWidget *dock); OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) @@ -1061,27 +920,15 @@ void OBSBasic::OBSInit() #if defined(_WIN32) && !defined(_DEBUG) LoadLibraryW(L"Qt6Network"); #endif - struct obs_module_failure_info mfi; - - // Safe Mode disables third-party plugins so we don't need to add each path outside the OBS bundle/installation. - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - // Core modules are not allowed to be disabled by the user via plugin manager. - SetCoreModuleNames(); - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after initializing global settings) to ensure legacy code gets valid path information. - */ + // Modules can access frontend information (i.e., profile and scene collection data) during their initialization, + // and some modules (e.g., obs-websockets) are known to use the filesystem location of the current profile in their + // own code. + // + // Thus, the profile and scene collection discovery needs to happen before any access to that information happens, + // but after initializing global settings, to ensure legacy code gets valid path information. RefreshSceneCollections(true); - App()->loadAppModules(mfi); - - BPtr failed_modules = mfi.failed_modules; + App()->loadAppModules(); #ifdef BROWSER_AVAILABLE cef = obs_browser_init_panel(); @@ -1399,22 +1246,7 @@ void OBSBasic::OBSInit() activateWindow(); } - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } + App()->handlePluginLoadState(); } void OBSBasic::OnFirstLoad() diff --git a/libobs/obs-internal.h b/libobs/obs-internal.h index 53a1d41ac..c326604a1 100644 --- a/libobs/obs-internal.h +++ b/libobs/obs-internal.h @@ -158,6 +158,7 @@ extern void free_module(struct obs_module *mod); struct obs_module_path { char *bin; char *data; + enum obs_runtime_module_type type; }; static inline void free_module_path(struct obs_module_path *omp) @@ -552,7 +553,6 @@ struct obs_core { DARRAY(struct obs_module_path) module_paths; DARRAY(char *) safe_modules; DARRAY(char *) disabled_modules; - DARRAY(char *) core_modules; obs_source_info_array_t source_types; obs_source_info_array_t input_types; @@ -580,6 +580,8 @@ struct obs_core { os_task_queue_t *destruction_task_thread; obs_task_handler_t ui_task_handler; + + bool core_modules_loaded; }; extern struct obs_core *obs; diff --git a/libobs/obs-module.c b/libobs/obs-module.c index cd3b7411f..a046de80e 100644 --- a/libobs/obs-module.c +++ b/libobs/obs-module.c @@ -426,15 +426,6 @@ void obs_add_safe_module(const char *name) da_push_back(obs->safe_modules, &item); } -void obs_add_core_module(const char *name) -{ - if (!obs || !name) - return; - - char *item = bstrdup(name); - da_push_back(obs->core_modules, &item); -} - void obs_add_disabled_module(const char *name) { if (!obs || !name) @@ -508,12 +499,32 @@ static void load_all_callback(void *param, const struct obs_module_info2 *info) return; } - if (is_disabled_module(info->name)) { + if (info->type != OBS_MODULE_TYPE_CORE && is_disabled_module(info->name)) { obs_create_disabled_module(&disabled_module, info->bin_path, info->data_path, OBS_MODULE_DISABLED); blog(LOG_WARNING, "Skipping module '%s', is disabled", info->name); return; } + /* This is a necessary check until legacy plugin loading is removed in a future update. + * It covers portable installs where the old core modules still live in `obs-plugins`. + */ + if (info->type == OBS_MODULE_TYPE_LEGACY_PLUGIN && is_core_module(info->name)) { + blog(LOG_WARNING, "Skipping module '%s', is a legacy core module", info->name); + return; + } + + obs_module_t *existing_module = obs_get_module(info->name); + if (info->type != OBS_MODULE_TYPE_CORE && existing_module != NULL) { + if (info->type == OBS_MODULE_TYPE_LEGACY_PLUGIN) { + blog(LOG_WARNING, "Skipping legacy plugin '%s' at '%s', already loaded from '%s'", info->name, + info->bin_path, existing_module->bin_path); + } else { + blog(LOG_WARNING, "Skipping plugin '%s' at '%s', already loaded from '%s'", info->name, + info->bin_path, existing_module->bin_path); + } + return; + } + int code = obs_open_module(&module, info->bin_path, info->data_path); switch (code) { case MODULE_MISSING_EXPORTS: @@ -540,6 +551,9 @@ static void load_all_callback(void *param, const struct obs_module_info2 *info) free_module(module); obs_create_disabled_module(&disabled_module, info->bin_path, info->data_path, OBS_MODULE_FAILED_TO_INITIALIZE); + disabled_module->module_type = info->type; + } else { + module->module_type = info->type; } UNUSED_PARAMETER(param); @@ -553,53 +567,45 @@ load_failure: } } -static const char *obs_load_all_modules_name = "obs_load_all_modules"; -#ifdef _WIN32 -static const char *reset_win32_symbol_paths_name = "reset_win32_symbol_paths"; -#endif - -void obs_load_all_modules(void) -{ - profile_start(obs_load_all_modules_name); - obs_find_modules2(load_all_callback, NULL); -#ifdef _WIN32 - profile_start(reset_win32_symbol_paths_name); - reset_win32_symbol_paths(); - profile_end(reset_win32_symbol_paths_name); -#endif - profile_end(obs_load_all_modules_name); -} - -static const char *obs_load_all_modules2_name = "obs_load_all_modules2"; +void obs_load_all_modules(void) {} void obs_load_all_modules2(struct obs_module_failure_info *mfi) { - struct fail_info fail_info = {0}; - memset(mfi, 0, sizeof(*mfi)); + UNUSED_PARAMETER(mfi); +} - profile_start(obs_load_all_modules2_name); - - struct fail_info core_fail_info = {0}; - load_core_modules(load_all_callback, &core_fail_info); - - if (core_fail_info.fail_count > 0) { - fail_info.fail_count += core_fail_info.fail_count; - - dstr_insert_dstr(&fail_info.fail_modules, 0, &core_fail_info.fail_modules); +bool obs_load_core_modules(void) +{ + if (obs->core_modules_loaded) { + return true; } - dstr_free(&core_fail_info.fail_modules); - obs_find_modules2(load_all_callback, &fail_info); -#ifdef _WIN32 - profile_start(reset_win32_symbol_paths_name); - reset_win32_symbol_paths(); - profile_end(reset_win32_symbol_paths_name); -#endif - profile_end(obs_load_all_modules2_name); + struct fail_info error = {0}; - mfi->count = fail_info.fail_count; - mfi->failed_modules = strlist_split(fail_info.fail_modules.array, ';', false); - dstr_free(&fail_info.fail_modules); + load_core_modules(load_all_callback, &error); + + obs->core_modules_loaded = error.fail_count == 0; + + dstr_free(&error.fail_modules); + + return obs->core_modules_loaded; +} + +static void find_modules_in_path(struct obs_module_path *omp, obs_find_module_callback2_t callback, void *param); + +void obs_load_plugins(struct obs_runtime_module_info *info, struct obs_module_failure_info *error) +{ + struct obs_module_path omp = {.bin = (char *)info->path_info.binary, + .data = (char *)info->path_info.data, + .type = info->type}; + + struct fail_info failure = {0}; + + find_modules_in_path(&omp, load_all_callback, &failure); + + error->count = failure.fail_count; + error->failed_modules = strlist_split(failure.fail_modules.array, ';', false); + dstr_free(&failure.fail_modules); } void obs_module_failure_info_free(struct obs_module_failure_info *mfi) @@ -610,8 +616,17 @@ void obs_module_failure_info_free(struct obs_module_failure_info *mfi) } } +#ifdef _WIN32 +static const char *reset_win32_symbol_paths_name = "reset_win32_symbol_paths"; +#endif + void obs_post_load_modules(void) { +#ifdef _WIN32 + profile_start(reset_win32_symbol_paths_name); + reset_win32_symbol_paths(); + profile_end(reset_win32_symbol_paths_name); +#endif for (obs_module_t *mod = obs->first_module; !!mod; mod = mod->next) if (mod->post_load) mod->post_load(); @@ -667,7 +682,8 @@ bool find_core_module(struct obs_runtime_module_info *info, obs_find_module_call if (os_file_exists(module_path.array)) { struct obs_module_info2 callback_info = {.bin_path = module_path.array, .data_path = parsed_data_directory, - .name = name}; + .name = name, + .type = info->type}; callback(data, &callback_info); found = true; @@ -686,6 +702,15 @@ bool obs_is_core_module(obs_module_t *module) return module->module_type == OBS_MODULE_TYPE_CORE; } +bool obs_is_legacy_module(obs_module_t *module) +{ + if (!module) { + return false; + } + + return module->module_type == OBS_MODULE_TYPE_LEGACY_PLUGIN; +} + static bool parse_binary_from_directory(struct dstr *parsed_bin_path, const char *bin_path, const char *file) { struct dstr directory = {0}; @@ -759,6 +784,7 @@ static void process_found_module(struct obs_module_path *omp, const char *path, info.bin_path = parsed_bin_path.array; info.data_path = parsed_data_dir; info.name = name.array; + info.type = omp->type; callback(param, &info); } @@ -775,7 +801,6 @@ static void find_modules_in_path(struct obs_module_path *omp, obs_find_module_ca os_glob_t *gi; dstr_copy(&search_path, omp->bin); - module_start = strstr(search_path.array, "%module%"); if (module_start) { dstr_resize(&search_path, module_start - search_path.array); diff --git a/libobs/obs.c b/libobs/obs.c index cfd51c4cc..082cbf74d 100644 --- a/libobs/obs.c +++ b/libobs/obs.c @@ -1266,6 +1266,8 @@ static bool obs_init(const char *locale, const char *module_config_path, profile obs_register_source(&scene_info); obs_register_source(&group_info); obs_register_source(&audio_line_info); + + obs->core_modules_loaded = false; return true; } @@ -1461,11 +1463,6 @@ void obs_shutdown(void) } da_free(obs->disabled_modules); - for (size_t i = 0; i < obs->core_modules.num; i++) { - bfree(obs->core_modules.array[i]); - } - da_free(obs->core_modules); - if (obs->name_store_owned) profiler_name_store_free(obs->name_store); diff --git a/libobs/obs.h b/libobs/obs.h index 83743c7d2..eea431f1c 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -586,16 +586,8 @@ EXPORT void obs_add_module_path(const char *bin, const char *data); */ EXPORT void obs_add_safe_module(const char *name); -/** - * Adds a module to the list of core modules (which cannot be disabled). - * If the list is empty, all modules are allowed. - * - * @param name Specifies the module's name (filename sans extension). - */ -EXPORT void obs_add_core_module(const char *name); - /** Automatically loads all modules from module paths (convenience function) */ -EXPORT void obs_load_all_modules(void); +OBS_DEPRECATED EXPORT void obs_load_all_modules(void); struct obs_module_failure_info { char **failed_modules; @@ -603,7 +595,7 @@ struct obs_module_failure_info { }; EXPORT void obs_module_failure_info_free(struct obs_module_failure_info *mfi); -EXPORT void obs_load_all_modules2(struct obs_module_failure_info *mfi); +OBS_DEPRECATED EXPORT void obs_load_all_modules2(struct obs_module_failure_info *mfi); /** Notifies modules that all modules have been loaded. This function should * be called after all modules have been loaded. */ @@ -623,6 +615,7 @@ struct obs_module_info2 { const char *bin_path; const char *data_path; const char *name; + enum obs_runtime_module_type type; }; typedef void (*obs_find_module_callback2_t)(void *param, const struct obs_module_info2 *info); @@ -633,9 +626,14 @@ EXPORT void obs_find_modules2(obs_find_module_callback2_t callback, void *param) /** Loads all registered core modules. */ EXPORT bool obs_load_core_modules(void); +/** Loads plugins at a given path. omp defines if modern or legacy plugins at path. */ +EXPORT void obs_load_plugins(struct obs_runtime_module_info *info, struct obs_module_failure_info *error); + /** Returns true if a module is a core module. */ EXPORT bool obs_is_core_module(obs_module_t *module); +EXPORT bool obs_is_legacy_module(obs_module_t *module); + /** Finds and loads a particular core module. * Returns false if module cant be found. */ bool find_core_module(struct obs_runtime_module_info *info, obs_find_module_callback2_t callback, void *data);