From 11a8445aa415ebe66bde60738dd29dddc4a4cc97 Mon Sep 17 00:00:00 2001 From: FiniteSingularity Date: Wed, 17 Jun 2026 14:25:52 -0500 Subject: [PATCH] frontend: Update app module loading behavior With this change module loading in the application is managed by the plugin manager and allows the application to explicitly load core modules independantly from 3rd party (plugin) modules. The implementation is split into separate files for different platforms to allow for cleaner code handling the different possible locations and path schemes used, including all possible legacy locations and plugin bundle formats. This change introduces new locations and directory formats for plugins: * WINDOWS: Portable plugins need to be placed in a directory called "plugins" adjacent to the "bin" directory of an OBS Studio installation. * WINDOWS: Plugins need to be packaged into their own directory "bundles", with the associated DLL in the root of the bundle and the associated "data" directory next to it. * WINDOWS: The "obs-plugins" directory adjacent to the "bin" directory is deprecated and considered a legacy location for all plugins. * MACOS: No changes in locations or formats. * LINUX: Plugins distributed via system package managers now need to use the "obs-modules/plugins" directory in the system library directory for binaries, and the "obs/obs-modules/plugins" directory in the system data directory for resources. * LINUX: The "obs-plugins" directories in the system library and system data directories are deprecated. * LINUX: Plugins distributed outside of system packages need to use the same directory format as plugins for Windows (see above). * LINUX: A "portable" plugin location like on Windows is not supported. * LINUX: Proper XDG directory locations are scanned for plugins, using XDG_CONFIG_HOME for the legacy plugin format and XDG_DATA_HOME for the new plugin structure. * LINUX: Changes Flatpak plugin loading to use the new plugin structure (while still supporting the legacy structure), and moves Flatpak specific plugin loading to its own source files. * ALL PLATFORMS: A single environment variable "OBS_PLUGINS_PATH" can now be used to provide a highest-priority location from which to load plugins. This directory needs to contain plugin bundles per the new format. The one exception is Flatpak which does not support loading plugins from an env provided location. * ALL PLATFORMS: A custom legacy plugin locations can still be provided via the "OBS_LEGACY_PLUGINS_PATH" and "OBS_LEGACY_PLUGINS_DATA_PATH" environment variables. These are only compatible with plugins using the legacy format and are considered deprecated. The one exception is Flatpak which does not support loading plugins from an env provided location. Co-authored-by: PatTheMav --- build-aux/com.obsproject.Studio.json | 3 +- frontend/CMakeLists.txt | 4 - frontend/OBSApp.cpp | 58 ++++- frontend/OBSApp.hpp | 3 +- frontend/cmake/feature-plugin-manager.cmake | 24 ++ frontend/data/locale/en-US.ini | 6 + .../plugin-manager/LoaderPaths_BSD.hpp.in | 28 +++ .../plugin-manager/LoaderPaths_Flatpak.hpp.in | 30 +++ .../plugin-manager/LoaderPaths_Linux.hpp.in | 30 +++ frontend/plugin-manager/PluginManager.cpp | 81 ++++++- frontend/plugin-manager/PluginManager.hpp | 37 ++- .../plugin-manager/PluginManagerWindow.cpp | 53 ++++- .../plugin-manager/PluginManagerWindow.hpp | 7 +- .../plugin-manager/PluginModuleLoader.hpp | 66 ++++++ .../plugin-manager/PluginModuleLoader_BSD.cpp | 220 +++++++++++++++++ .../PluginModuleLoader_Flatpak.cpp | 142 +++++++++++ .../PluginModuleLoader_Linux.cpp | 224 ++++++++++++++++++ .../PluginModuleLoader_MacOS.mm | 196 +++++++++++++++ .../PluginModuleLoader_Windows.cpp | 196 +++++++++++++++ frontend/widgets/OBSBasic.cpp | 184 +------------- libobs/obs-internal.h | 4 +- libobs/obs-module.c | 129 ++++++---- libobs/obs.c | 7 +- libobs/obs.h | 18 +- 24 files changed, 1476 insertions(+), 274 deletions(-) create mode 100644 frontend/plugin-manager/LoaderPaths_BSD.hpp.in create mode 100644 frontend/plugin-manager/LoaderPaths_Flatpak.hpp.in create mode 100644 frontend/plugin-manager/LoaderPaths_Linux.hpp.in create mode 100644 frontend/plugin-manager/PluginModuleLoader.hpp create mode 100644 frontend/plugin-manager/PluginModuleLoader_BSD.cpp create mode 100644 frontend/plugin-manager/PluginModuleLoader_Flatpak.cpp create mode 100644 frontend/plugin-manager/PluginModuleLoader_Linux.cpp create mode 100644 frontend/plugin-manager/PluginModuleLoader_MacOS.mm create mode 100644 frontend/plugin-manager/PluginModuleLoader_Windows.cpp 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);