553 lines
15 KiB
C++
Executable File
553 lines
15 KiB
C++
Executable File
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
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 "audacity/ModuleInterface.h"
|
|
|
|
|
|
|
|
#include <wx/dynlib.h>
|
|
#include <wx/log.h>
|
|
#include <wx/string.h>
|
|
#include <wx/filename.h>
|
|
|
|
#include "FileNames.h"
|
|
#include "MemoryX.h"
|
|
|
|
#include "audacity/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<wxDynamicLibrary>();
|
|
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> ModuleManager::mInstance{};
|
|
|
|
// Provide builtin modules a means to identify themselves
|
|
using BuiltinModuleList = std::vector<ModuleMain>;
|
|
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<Module>(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<ComponentInterface> 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);
|
|
}
|