diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index ce923d5c6..90f6de355 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -113,6 +113,10 @@ MoveSourceDown="Move Source(s) Down" SourceProperties="Open Source Properties" SourceFilters="Open Source Filters" +# warning for plugin load failures +PluginsFailedToLoad.Title="Plugin Load Error" +PluginsFailedToLoad.Text="The following OBS plugins failed to load:\n\n%1\nPlease update or remove these plugins." + # warning if program already open AlreadyRunning.Title="OBS is already running" AlreadyRunning.Text="OBS is already running! Unless you meant to do this, please shut down any existing instances of OBS before trying to run a new instance. If you have OBS set to minimize to the system tray, please check to see if it's still running there." diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index e66ca53e7..fb36a36d2 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -1778,15 +1778,18 @@ void OBSBasic::OBSInit() LoadLibraryW(L"Qt6Network"); #endif #endif + struct obs_module_failure_info mfi; AddExtraModulePaths(); blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules(); + obs_load_all_modules2(&mfi); blog(LOG_INFO, "---------------------------------"); obs_log_loaded_modules(); blog(LOG_INFO, "---------------------------------"); obs_post_load_modules(); + BPtr failed_modules = mfi.failed_modules; + #ifdef BROWSER_AVAILABLE cef = obs_browser_init_panel(); #endif @@ -2071,6 +2074,25 @@ void OBSBasic::OBSInit() OnFirstLoad(); 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); + } } void OBSBasic::OnFirstLoad() diff --git a/docs/sphinx/reference-modules.rst b/docs/sphinx/reference-modules.rst index 9dc19bfb5..80940ca4f 100644 --- a/docs/sphinx/reference-modules.rst +++ b/docs/sphinx/reference-modules.rst @@ -246,6 +246,36 @@ plugin modules. --------------------- +.. function:: void obs_load_all_modules2(struct obs_module_failure_info *mfi) + + Automatically loads all modules from module paths (convenience function). + Additionally gives you information about modules that fail to load. + + :param mfi: Provides module failure information. The *failed_modules* + member is a string list via a pointer to pointers of + strings of modules that failed to load. Can be freed + either with :c:func:`obs_module_failure_info_free()` or + by simply calling :c:func:`bfree()` on the + *failed_modules* member variable. + + Relevant data types used with this function: + +.. code:: cpp + + struct obs_module_failure_info { + char **failed_modules; + size_t count; + }; + +--------------------- + +.. function:: void obs_module_failure_info_free(struct obs_module_failure_info *mfi) + + Frees data allocated data used in the *mfi* parameter (calls + :c:func:`bfree()` on the *failed_modules* member variable). + +--------------------- + .. function:: void obs_post_load_modules(void) Notifies modules that all modules have been loaded. @@ -271,6 +301,26 @@ plugin modules. --------------------- +.. function:: void obs_find_modules2(obs_find_module_callback_t callback, void *param) + + Finds all modules within the search paths added by + :c:func:`obs_add_module_path()`. + + Relevant data types used with this function: + +.. code:: cpp + + struct obs_module_info2 { + const char *bin_path; + const char *data_path; + const char *name; + }; + + typedef void (*obs_find_module_callback2_t)(void *param, + const struct obs_module_info2 *info); + +--------------------- + .. function:: void obs_enum_modules(obs_enum_module_callback_t callback, void *param) Enumerates all loaded modules. diff --git a/libobs/CMakeLists.txt b/libobs/CMakeLists.txt index 8bfd833b4..a7390821d 100644 --- a/libobs/CMakeLists.txt +++ b/libobs/CMakeLists.txt @@ -11,6 +11,12 @@ find_package( OPTIONAL_COMPONENTS avcodec) find_package(ZLIB REQUIRED) +if(ENABLE_UI) + find_qt(COMPONENTS Core) +else() + set(_QT_VERSION 0) +endif() + add_library(libobs SHARED) add_library(OBS::libobs ALIAS libobs) diff --git a/libobs/obs-module.c b/libobs/obs-module.c index 14e15b539..cc5017c4e 100644 --- a/libobs/obs-module.c +++ b/libobs/obs-module.c @@ -274,27 +274,55 @@ void obs_add_module_path(const char *bin, const char *data) da_push_back(obs->module_paths, &omp); } -static void load_all_callback(void *param, const struct obs_module_info *info) +extern void get_plugin_info(const char *path, bool *is_obs_plugin, + bool *can_load); + +struct fail_info { + struct dstr fail_modules; + size_t fail_count; +}; + +static void load_all_callback(void *param, const struct obs_module_info2 *info) { + struct fail_info *fail_info = param; obs_module_t *module; - if (!os_is_obs_plugin(info->bin_path)) { + bool is_obs_plugin; + bool can_load_obs_plugin; + + get_plugin_info(info->bin_path, &is_obs_plugin, &can_load_obs_plugin); + + if (!is_obs_plugin) { blog(LOG_WARNING, "Skipping module '%s', not an OBS plugin", info->bin_path); return; } + if (!can_load_obs_plugin) { + blog(LOG_WARNING, "Skipping module '%s' due to possible " + "import conflicts"); + goto load_failure; + } + int code = obs_open_module(&module, info->bin_path, info->data_path); if (code != MODULE_SUCCESS) { blog(LOG_DEBUG, "Failed to load module file '%s': %d", info->bin_path, code); - return; + goto load_failure; } if (!obs_init_module(module)) free_module(module); UNUSED_PARAMETER(param); + return; + +load_failure: + if (fail_info) { + dstr_cat(&fail_info->fail_modules, info->name); + dstr_cat(&fail_info->fail_modules, ";"); + fail_info->fail_count++; + } } static const char *obs_load_all_modules_name = "obs_load_all_modules"; @@ -305,7 +333,7 @@ static const char *reset_win32_symbol_paths_name = "reset_win32_symbol_paths"; void obs_load_all_modules(void) { profile_start(obs_load_all_modules_name); - obs_find_modules(load_all_callback, NULL); + obs_find_modules2(load_all_callback, NULL); #ifdef _WIN32 profile_start(reset_win32_symbol_paths_name); reset_win32_symbol_paths(); @@ -314,6 +342,36 @@ void obs_load_all_modules(void) profile_end(obs_load_all_modules_name); } +static const char *obs_load_all_modules2_name = "obs_load_all_modules2"; + +void obs_load_all_modules2(struct obs_module_failure_info *mfi) +{ + struct fail_info fail_info = {0}; + memset(mfi, 0, sizeof(*mfi)); + + profile_start(obs_load_all_modules2_name); + 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); + + mfi->count = fail_info.fail_count; + mfi->failed_modules = + strlist_split(fail_info.fail_modules.array, ';', false); + dstr_free(&fail_info.fail_modules); +} + +void obs_module_failure_info_free(struct obs_module_failure_info *mfi) +{ + if (mfi->failed_modules) { + bfree(mfi->failed_modules); + mfi->failed_modules = NULL; + } +} + void obs_post_load_modules(void) { for (obs_module_t *mod = obs->first_module; !!mod; mod = mod->next) @@ -388,10 +446,10 @@ static bool parse_binary_from_directory(struct dstr *parsed_bin_path, static void process_found_module(struct obs_module_path *omp, const char *path, bool directory, - obs_find_module_callback_t callback, + obs_find_module_callback2_t callback, void *param) { - struct obs_module_info info; + struct obs_module_info2 info; struct dstr name = {0}; struct dstr parsed_bin_path = {0}; const char *file; @@ -421,6 +479,7 @@ static void process_found_module(struct obs_module_path *omp, const char *path, if (parsed_data_dir && bin_found) { info.bin_path = parsed_bin_path.array; info.data_path = parsed_data_dir; + info.name = name.array; callback(param, &info); } @@ -430,7 +489,7 @@ static void process_found_module(struct obs_module_path *omp, const char *path, } static void find_modules_in_path(struct obs_module_path *omp, - obs_find_module_callback_t callback, + obs_find_module_callback2_t callback, void *param) { struct dstr search_path = {0}; @@ -467,7 +526,7 @@ static void find_modules_in_path(struct obs_module_path *omp, dstr_free(&search_path); } -void obs_find_modules(obs_find_module_callback_t callback, void *param) +void obs_find_modules2(obs_find_module_callback2_t callback, void *param) { if (!obs) return; @@ -478,6 +537,12 @@ void obs_find_modules(obs_find_module_callback_t callback, void *param) } } +void obs_find_modules(obs_find_module_callback_t callback, void *param) +{ + /* the structure is ABI compatible so we can just cast the callback */ + obs_find_modules2((obs_find_module_callback2_t)callback, param); +} + void obs_enum_modules(obs_enum_module_callback_t callback, void *param) { struct obs_module *module; diff --git a/libobs/obs.h b/libobs/obs.h index 9df703527..82a043d2c 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -506,6 +506,7 @@ EXPORT const char *obs_get_module_binary_path(obs_module_t *module); /** Returns the module data path */ EXPORT const char *obs_get_module_data_path(obs_module_t *module); +#ifndef SWIG /** * Adds a module search path to be used with obs_find_modules. If the search * path strings contain %module%, that text will be replaced with the module @@ -519,11 +520,18 @@ EXPORT void obs_add_module_path(const char *bin, const char *data); /** Automatically loads all modules from module paths (convenience function) */ EXPORT void obs_load_all_modules(void); +struct obs_module_failure_info { + char **failed_modules; + size_t count; +}; + +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); + /** Notifies modules that all modules have been loaded. This function should * be called after all modules have been loaded. */ EXPORT void obs_post_load_modules(void); -#ifndef SWIG struct obs_module_info { const char *bin_path; const char *data_path; @@ -534,6 +542,19 @@ typedef void (*obs_find_module_callback_t)(void *param, /** Finds all modules within the search paths added by obs_add_module_path. */ EXPORT void obs_find_modules(obs_find_module_callback_t callback, void *param); + +struct obs_module_info2 { + const char *bin_path; + const char *data_path; + const char *name; +}; + +typedef void (*obs_find_module_callback2_t)( + void *param, const struct obs_module_info2 *info); + +/** Finds all modules within the search paths added by obs_add_module_path. */ +EXPORT void obs_find_modules2(obs_find_module_callback2_t callback, + void *param); #endif typedef void (*obs_enum_module_callback_t)(void *param, obs_module_t *module); diff --git a/libobs/obsconfig.h.in b/libobs/obsconfig.h.in index 0ebd0eb0c..4a664285a 100644 --- a/libobs/obsconfig.h.in +++ b/libobs/obsconfig.h.in @@ -14,6 +14,7 @@ #define OBS_DATA_PATH "@OBS_DATA_PATH@" #define OBS_INSTALL_PREFIX "@OBS_INSTALL_PREFIX@" #define OBS_PLUGIN_DESTINATION "@OBS_PLUGIN_DESTINATION@" +#define OBS_QT_VERSION @_QT_VERSION@ #cmakedefine LINUX_PORTABLE #cmakedefine GIO_FOUND diff --git a/libobs/util/platform-nix.c b/libobs/util/platform-nix.c index 75634d69d..3fa9b07d6 100644 --- a/libobs/util/platform-nix.c +++ b/libobs/util/platform-nix.c @@ -96,6 +96,13 @@ void os_dlclose(void *module) dlclose(module); } +void get_plugin_info(const char *path, bool *is_obs_plugin, bool *can_load) +{ + *is_obs_plugin = true; + *can_load = true; + UNUSED_PARAMETER(path); +} + bool os_is_obs_plugin(const char *path) { UNUSED_PARAMETER(path); diff --git a/libobs/util/platform-windows.c b/libobs/util/platform-windows.c index c2078e5cb..adec5375a 100644 --- a/libobs/util/platform-windows.c +++ b/libobs/util/platform-windows.c @@ -26,6 +26,7 @@ #include "platform.h" #include "darray.h" #include "dstr.h" +#include "obsconfig.h" #include "util_uint64.h" #include "windows/win-registry.h" #include "windows/win-version.h" @@ -136,7 +137,145 @@ void os_dlclose(void *module) FreeLibrary(module); } -bool os_is_obs_plugin(const char *path) +#if OBS_QT_VERSION == 6 +static bool has_qt5_import(VOID *base, PIMAGE_NT_HEADERS nt_headers) +{ + __try { + PIMAGE_DATA_DIRECTORY data_dir; + data_dir = + &nt_headers->OptionalHeader + .DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; + + if (data_dir->Size == 0) + return false; + + PIMAGE_SECTION_HEADER section, last_section; + section = IMAGE_FIRST_SECTION(nt_headers); + last_section = section; + + /* find the section that contains the export directory */ + int i; + for (i = 0; i < nt_headers->FileHeader.NumberOfSections; i++) { + if (section->VirtualAddress <= + data_dir->VirtualAddress) { + last_section = section; + section++; + continue; + } else { + break; + } + } + + /* double check in case we exited early */ + if (last_section->VirtualAddress > data_dir->VirtualAddress || + section->VirtualAddress <= data_dir->VirtualAddress) + return false; + + section = last_section; + + /* get a pointer to the import directory */ + PIMAGE_IMPORT_DESCRIPTOR import; + import = (PIMAGE_IMPORT_DESCRIPTOR)((byte *)base + + data_dir->VirtualAddress - + section->VirtualAddress + + section->PointerToRawData); + + while (import->Name != 0) { + char *name = (char *)((byte *)base + import->Name - + section->VirtualAddress + + section->PointerToRawData); + + /* qt5? bingo, reject this library */ + if (astrcmpi_n(name, "qt5", 3) == 0) { + return true; + } + + import++; + } + + } __except (EXCEPTION_EXECUTE_HANDLER) { + /* we failed somehow, for compatibility assume no qt5 import */ + return false; + } + + return false; +} +#endif + +static bool has_obs_export(VOID *base, PIMAGE_NT_HEADERS nt_headers) +{ + __try { + PIMAGE_DATA_DIRECTORY data_dir; + data_dir = + &nt_headers->OptionalHeader + .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; + + if (data_dir->Size == 0) + return false; + + PIMAGE_SECTION_HEADER section, last_section; + section = IMAGE_FIRST_SECTION(nt_headers); + last_section = section; + + /* find the section that contains the export directory */ + int i; + for (i = 0; i < nt_headers->FileHeader.NumberOfSections; i++) { + if (section->VirtualAddress <= + data_dir->VirtualAddress) { + last_section = section; + section++; + continue; + } else { + break; + } + } + + /* double check in case we exited early */ + if (last_section->VirtualAddress > data_dir->VirtualAddress || + section->VirtualAddress <= data_dir->VirtualAddress) + return false; + + section = last_section; + + /* get a pointer to the export directory */ + PIMAGE_EXPORT_DIRECTORY export; + export = (PIMAGE_EXPORT_DIRECTORY)((byte *)base + + data_dir->VirtualAddress - + section->VirtualAddress + + section->PointerToRawData); + + if (export->NumberOfNames == 0) + return false; + + /* get a pointer to the export directory names */ + DWORD *names_ptr; + names_ptr = (DWORD *)((byte *)base + export->AddressOfNames - + section->VirtualAddress + + section->PointerToRawData); + + /* iterate through each name and see if its an obs plugin */ + CHAR *name; + size_t j; + for (j = 0; j < export->NumberOfNames; j++) { + + name = (CHAR *)base + names_ptr[j] - + section->VirtualAddress + + section->PointerToRawData; + + if (!strcmp(name, "obs_module_load")) { + return true; + } + } + } __except (EXCEPTION_EXECUTE_HANDLER) { + /* we failed somehow, for compatibility let's assume it + * was a valid plugin and let the loader deal with it */ + return true; + } + + return false; +} + +void get_plugin_info(const char *path, bool *is_obs_plugin, bool *can_load) { struct dstr dll_name; wchar_t *wpath; @@ -147,12 +286,12 @@ bool os_is_obs_plugin(const char *path) PIMAGE_DOS_HEADER dos_header; PIMAGE_NT_HEADERS nt_headers; - PIMAGE_SECTION_HEADER section, last_section; - bool ret = false; + *is_obs_plugin = false; + *can_load = false; if (!path) - return false; + return; dstr_init_copy(&dll_name, path); dstr_replace(&dll_name, "\\", "/"); @@ -193,72 +332,21 @@ bool os_is_obs_plugin(const char *path) if (nt_headers->Signature != IMAGE_NT_SIGNATURE) goto cleanup; - PIMAGE_DATA_DIRECTORY data_dir; - data_dir = - &nt_headers->OptionalHeader - .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; + *is_obs_plugin = has_obs_export(base, nt_headers); - if (data_dir->Size == 0) - goto cleanup; - - section = IMAGE_FIRST_SECTION(nt_headers); - last_section = section; - - /* find the section that contains the export directory */ - int i; - for (i = 0; i < nt_headers->FileHeader.NumberOfSections; i++) { - if (section->VirtualAddress <= - data_dir->VirtualAddress) { - last_section = section; - section++; - continue; - } else { - break; - } - } - - /* double check in case we exited early */ - if (last_section->VirtualAddress > data_dir->VirtualAddress || - section->VirtualAddress <= data_dir->VirtualAddress) - goto cleanup; - - section = last_section; - - /* get a pointer to the export directory */ - PIMAGE_EXPORT_DIRECTORY export; - export = (PIMAGE_EXPORT_DIRECTORY)((byte *)base + - data_dir->VirtualAddress - - section->VirtualAddress + - section->PointerToRawData); - - if (export->NumberOfNames == 0) - goto cleanup; - - /* get a pointer to the export directory names */ - DWORD *names_ptr; - names_ptr = (DWORD *)((byte *)base + export->AddressOfNames - - section->VirtualAddress + - section->PointerToRawData); - - /* iterate through each name and see if its an obs plugin */ - CHAR *name; - size_t j; - for (j = 0; j < export->NumberOfNames; j++) { - - name = (CHAR *)base + names_ptr[j] - - section->VirtualAddress + - section->PointerToRawData; - - if (!strcmp(name, "obs_module_load")) { - ret = true; - goto cleanup; - } +#if OBS_QT_VERSION == 6 + if (*is_obs_plugin) { + *can_load = !has_qt5_import(base, nt_headers); } +#else + *can_load = true; +#endif } __except (EXCEPTION_EXECUTE_HANDLER) { /* we failed somehow, for compatibility let's assume it * was a valid plugin and let the loader deal with it */ - ret = true; + *is_obs_plugin = true; + *can_load = true; goto cleanup; } @@ -271,8 +359,16 @@ cleanup: if (hFile != INVALID_HANDLE_VALUE) CloseHandle(hFile); +} - return ret; +bool os_is_obs_plugin(const char *path) +{ + bool is_obs_plugin; + bool can_load; + + get_plugin_info(path, &is_obs_plugin, &can_load); + + return is_obs_plugin && can_load; } union time_data {