/********************************************************************** Tenacity ModuleManager.cpp Dominic Mazzoni James Crook *******************************************************************//*! \file ModuleManager.cpp \brief Based on LoadLadspa, this code loads pluggable Audacity extension modules. It also has the code to invoke a function returning a replacement window, i.e. an alternative to the usual interface, for Audacity. *//*******************************************************************/ #include "ModuleManager.h" #include "tenacity/ModuleInterface.h" #include #include #include #include #include "FileNames.h" #include "MemoryX.h" #include "tenacity/PluginInterface.h" #ifdef EXPERIMENTAL_MODULE_PREFS #include "Prefs.h" #include "ModuleSettings.h" #endif #include "widgets/MultiDialog.h" #include "widgets/AudacityMessageBox.h" #define initFnName "ExtensionModuleInit" #define versionFnName "GetVersionString" //typedef wxString (*tVersionFn)(); typedef wxChar * (*tVersionFn)(); Module::Module(const FilePath & name) : mName{ name } { mLib = std::make_unique(); mDispatch = NULL; } Module::~Module() { } void Module::ShowLoadFailureError(const wxString &Error) { auto ShortName = wxFileName(mName).GetName(); AudacityMessageBox(XO("Unable to load the \"%s\" module.\n\nError: %s").Format(ShortName, Error), XO("Module Unsuitable")); wxLogMessage(wxT("Unable to load the module \"%s\". Error: %s"), mName, Error); } bool Module::Load(wxString &deferredErrorMessage) { deferredErrorMessage.clear(); // Will this ever happen??? if (mLib->IsLoaded()) { if (mDispatch) { return true; } // Any messages should have already been generated the first time it was loaded. return false; } auto ShortName = wxFileName(mName).GetName(); if (!mLib->Load(mName, wxDL_NOW | wxDL_QUIET | wxDL_GLOBAL)) { // For this failure path, only, there is a possiblity of retrial // after some other dependency of this module is loaded. So the // error is not immediately reported. deferredErrorMessage = wxString(wxSysErrorMsg()); return false; } // Check version string matches. (For now, they must match exactly) tVersionFn versionFn = (tVersionFn)(mLib->GetSymbol(wxT(versionFnName))); if (versionFn == NULL){ AudacityMessageBox( XO("The module \"%s\" does not provide a version string.\n\nIt will not be loaded.") .Format( ShortName), XO("Module Unsuitable")); wxLogMessage(wxT("The module \"%s\" does not provide a version string. It will not be loaded."), mName); mLib->Unload(); return false; } wxString moduleVersion = versionFn(); if( moduleVersion != AUDACITY_VERSION_STRING) { AudacityMessageBox( XO("The module \"%s\" is matched with Audacity version \"%s\".\n\nIt will not be loaded.") .Format(ShortName, moduleVersion), XO("Module Unsuitable")); wxLogMessage(wxT("The module \"%s\" is matched with Audacity version \"%s\". It will not be loaded."), mName, moduleVersion); mLib->Unload(); return false; } mDispatch = (fnModuleDispatch) mLib->GetSymbol(wxT(ModuleDispatchName)); if (!mDispatch) { // Module does not provide a dispatch function. return true; } // However if we do have it and it does not work, // then the module is bad. bool res = ((mDispatch(ModuleInitialize))!=0); if (res) { return true; } mDispatch = NULL; AudacityMessageBox( XO("The module \"%s\" failed to initialize.\n\nIt will not be loaded.").Format(ShortName), XO("Module Unsuitable")); wxLogMessage(wxT("The module \"%s\" failed to initialize.\nIt will not be loaded."), mName); mLib->Unload(); return false; } // This isn't yet used? void Module::Unload() { if (mLib->IsLoaded()) { if (mDispatch) mDispatch(ModuleTerminate); } mLib->Unload(); } int Module::Dispatch(ModuleDispatchTypes type) { if (mLib->IsLoaded()) if( mDispatch != NULL ) return mDispatch(type); return 0; } void * Module::GetSymbol(const wxString &name) { return mLib->GetSymbol(name); } // ============================================================================ // // ModuleManager // // ============================================================================ // The one and only ModuleManager std::unique_ptr ModuleManager::mInstance{}; // Provide builtin modules a means to identify themselves using BuiltinModuleList = std::vector; namespace { BuiltinModuleList &builtinModuleList() { static BuiltinModuleList theList; return theList; } } void RegisterProvider(ModuleMain moduleMain) { auto &list = builtinModuleList(); if ( moduleMain ) list.push_back(moduleMain); return; } void UnregisterProvider(ModuleMain moduleMain) { auto &list = builtinModuleList(); auto end = list.end(), iter = std::find(list.begin(), end, moduleMain); if (iter != end) list.erase(iter); } // ---------------------------------------------------------------------------- // Creation/Destruction // ---------------------------------------------------------------------------- ModuleManager::ModuleManager() { } ModuleManager::~ModuleManager() { mDynModules.clear(); builtinModuleList().clear(); } // static void ModuleManager::FindModules(FilePaths &files) { const auto &audacityPathList = FileNames::AudacityPathList(); FilePaths pathList; wxString pathVar; // Code from LoadLadspa that might be useful in load modules. pathVar = wxGetenv(wxT("AUDACITY_MODULES_PATH")); if (!pathVar.empty()) FileNames::AddMultiPathsToPathList(pathVar, pathList); for (const auto &path : audacityPathList) { wxString prefix = path + wxFILE_SEP_PATH; FileNames::AddUniquePathToPathList(prefix + wxT("modules"), pathList); if (files.size()) { break; } } #if defined(__WXMSW__) FileNames::FindFilesInPathList(wxT("*.dll"), pathList, files); #else FileNames::FindFilesInPathList(wxT("*.so"), pathList, files); #endif } void ModuleManager::TryLoadModules( const FilePaths &files, FilePaths &decided, DelayedErrors &errors) { FilePaths checked; wxString saveOldCWD = ::wxGetCwd(); auto cleanup = finally([&]{ ::wxSetWorkingDirectory(saveOldCWD); }); for (const auto &file : files) { // As a courtesy to some modules that might be bridges to // open other modules, we set the current working // directory to be the module's directory. auto prefix = ::wxPathOnly(file); ::wxSetWorkingDirectory(prefix); // Only process the first module encountered in the // defined search sequence. wxString ShortName = wxFileName( file ).GetName(); if( checked.Index( ShortName, false ) != wxNOT_FOUND ) continue; checked.Add( ShortName ); // Skip if a previous pass through this function decided it already if( decided.Index( ShortName, false ) != wxNOT_FOUND ) continue; #ifdef EXPERIMENTAL_MODULE_PREFS int iModuleStatus = ModuleSettings::GetModuleStatus( file ); if( iModuleStatus == kModuleDisabled ) continue; if( iModuleStatus == kModuleFailed ) continue; // New module? You have to go and explicitly enable it. if( iModuleStatus == kModuleNew ){ // To ensure it is noted in config file and so // appears on modules page. ModuleSettings::SetModuleStatus( file, kModuleNew); continue; } if( iModuleStatus == kModuleAsk ) #endif // JKC: I don't like prompting for the plug-ins individually // I think it would be better to show the module prefs page, // and let the user decide for each one. { auto msg = XO("Module \"%s\" found.").Format( ShortName ); msg += XO("\n\nOnly use modules from trusted sources"); const TranslatableStrings buttons{ XO("Yes"), XO("No"), }; // could add a button here for 'yes and remember that', and put it into the cfg file. Needs more thought. int action; action = ShowMultiDialog(msg, XO("Audacity Module Loader"), buttons, "", XO("Try and load this module?"), false); #ifdef EXPERIMENTAL_MODULE_PREFS // If we're not prompting always, accept the answer permanently if( iModuleStatus == kModuleNew ){ iModuleStatus = (action==1)?kModuleDisabled : kModuleEnabled; ModuleSettings::SetModuleStatus( file, iModuleStatus ); } #endif if(action == 1){ // "No" decided.Add( ShortName ); continue; } } #ifdef EXPERIMENTAL_MODULE_PREFS // Before attempting to load, we set the state to bad. // That way, if we crash, we won't try again. ModuleSettings::SetModuleStatus( file, kModuleFailed ); #endif wxString Error; auto umodule = std::make_unique(file); if (umodule->Load(Error)) // it will get rejected if there are version problems { decided.Add( ShortName ); auto module = umodule.get(); if (!module->HasDispatch()) { auto ShortName = wxFileName(file).GetName(); AudacityMessageBox( XO("The module \"%s\" does not provide any of the required functions.\n\nIt will not be loaded.").Format(ShortName), XO("Module Unsuitable")); wxLogMessage(wxT("The module \"%s\" does not provide any of the required functions. It will not be loaded."), file); module->Unload(); } else { Get().mModules.push_back(std::move(umodule)); #ifdef EXPERIMENTAL_MODULE_PREFS // Loaded successfully, restore the status. ModuleSettings::SetModuleStatus(file, iModuleStatus); #endif } } else if (!Error.empty()) { // Module is not yet decided in this pass. // Maybe it depends on another which has not yet been loaded. // But don't take the kModuleAsk path again in a later pass. ModuleSettings::SetModuleStatus( file, kModuleEnabled ); errors.emplace_back( std::move( umodule ), Error ); } } } // static void ModuleManager::Initialize() { FilePaths files; FindModules(files); FilePaths decided; DelayedErrors errors; size_t numDecided = 0; // Multiple passes give modules multiple chances to load in case they // depend on some other module not yet loaded do { numDecided = decided.size(); errors.clear(); TryLoadModules(files, decided, errors); } while ( errors.size() && numDecided < decided.size() ); // Only now show accumulated errors of modules that failed to load for ( const auto &pair : errors ) { auto &pModule = pair.first; pModule->ShowLoadFailureError(pair.second); ModuleSettings::SetModuleStatus( pModule->GetName(), kModuleFailed ); } } // static int ModuleManager::Dispatch(ModuleDispatchTypes type) { for (const auto &module: mModules) { module->Dispatch(type); } return 0; } // ============================================================================ // // Return reference to singleton // // (Thread-safe...no active threading during construction or after destruction) // ============================================================================ ModuleManager & ModuleManager::Get() { if (!mInstance) { mInstance.reset(safenew ModuleManager); } return *mInstance; } wxString ModuleManager::GetPluginTypeString() { return L"Module"; } PluginID ModuleManager::GetID(ModuleInterface *module) { return wxString::Format(wxT("%s_%s_%s_%s_%s"), GetPluginTypeString(), wxEmptyString, module->GetVendor().Internal(), module->GetSymbol().Internal(), module->GetPath()); } bool ModuleManager::DiscoverProviders() { InitializeBuiltins(); // The commented out code loads modules whether or not they are enabled. // none of our modules is a 'provider' of effects, so this code commented out. #if 0 FilePaths provList; FilePaths pathList; // Code from LoadLadspa that might be useful in load modules. wxString pathVar = wxString::FromUTF8(getenv("AUDACITY_MODULES_PATH")); if (!pathVar.empty()) { FileNames::AddMultiPathsToPathList(pathVar, pathList); } else { FileNames::AddUniquePathToPathList(FileNames::ModulesDir(), pathList); } #if defined(__WXMSW__) FileNames::FindFilesInPathList(wxT("*.dll"), pathList, provList); #elif defined(__WXMAC__) FileNames::FindFilesInPathList(wxT("*.dylib"), pathList, provList); #else FileNames::FindFilesInPathList(wxT("*.so"), pathList, provList); #endif for ( const auto &path : provList ) LoadModule(path); #endif return true; } void ModuleManager::InitializeBuiltins() { for (auto moduleMain : builtinModuleList()) { ModuleInterfaceHandle module { moduleMain(), ModuleInterfaceDeleter{} }; if (module && module->Initialize()) { // Register the provider ModuleInterface *pInterface = module.get(); auto id = GetID(pInterface); // Need to remember it mDynModules[id] = std::move(module); } else { // Don't leak! Destructor of module does that. } } } void ModuleInterfaceDeleter::operator() (ModuleInterface *pInterface) const { if (pInterface) { pInterface->Terminate(); std::unique_ptr < ModuleInterface > { pInterface }; // DELETE it } } bool ModuleManager::RegisterEffectPlugin(const PluginID & providerID, const PluginPath & path, TranslatableString &errMsg) { errMsg = {}; if (mDynModules.find(providerID) == mDynModules.end()) { return false; } auto nFound = mDynModules[providerID]->DiscoverPluginsAtPath(path, errMsg, PluginManagerInterface::DefaultRegistrationCallback); return nFound > 0; } ModuleInterface *ModuleManager::CreateProviderInstance(const PluginID & providerID, const PluginPath & path) { if (path.empty() && mDynModules.find(providerID) != mDynModules.end()) { return mDynModules[providerID].get(); } return nullptr; } std::unique_ptr ModuleManager::CreateInstance( const PluginID & providerID, const PluginPath & path) { if (auto iter = mDynModules.find(providerID); iter == mDynModules.end()) return nullptr; else return iter->second->CreateInstance(path); } bool ModuleManager::IsProviderValid(const PluginID & WXUNUSED(providerID), const PluginPath & path) { // Builtin modules do not have a path if (path.empty()) { return true; } wxFileName lib(path); if (lib.FileExists() || lib.DirExists()) { return true; } return false; } bool ModuleManager::IsPluginValid(const PluginID & providerID, const PluginPath & path, bool bFast) { if (mDynModules.find(providerID) == mDynModules.end()) { return false; } return mDynModules[providerID]->IsPluginValid(path, bFast); }