/*
PluginFramework
by John Ryland
Copyright (c) 2023
*/
////////////////////////////////////////////////////////////////////////////////////
// Plugin Manager
#include "PluginManager.h"
#include <thread>
#include "FileWatch.hpp"
#include "Library.h"
#include "Utilities.h"
#include <string>
#include <regex>
#include <iostream>
#include <filesystem>
#include <unordered_set>
namespace details {
#ifdef _NDEBUG
static const char* buildTypeSuffix = "";
#else
static const char* buildTypeSuffix = "_d";
#endif
#ifdef _WIN32
static const char* pluginPrefix = "";
static const char* pluginExtension = ".dll";
#elif __APPLE__
static const char* pluginPrefix = "lib";
static const char* pluginExtension = ".dylib";
#else // UNIX / Linux
static const char* pluginPrefix = "lib";
static const char* pluginExtension = ".so";
#endif
std::string PluginNameFromFileName(std::string fileName)
{
static const std::string prefix = pluginPrefix;
static const std::string suffix = std::string(buildTypeSuffix) + pluginExtension;
std::string baseName = Utilities::base_name(fileName);
std::string filePrefix = baseName.substr(0, prefix.length());
std::string fileSuffix = baseName.substr(std::max<ssize_t>(baseName.length() - suffix.length(), 0));
// Only load plugins of matching build type and file pattern of operating system
if (filePrefix == prefix && fileSuffix == suffix)
return baseName.substr(prefix.length(), baseName.length() - (prefix.length() + suffix.length()));
return "";
}
}
namespace PluginFramework {
PluginManager::PluginManager()
: mNeedReload(false)
, mPendingAdd(false)
{
}
PluginManager::~PluginManager()
{
}
void PluginManager::Initialize()
{
SetPluginDirectory("bin/plugins/");
LoadPlugins();
RegisterPlugins();
}
void PluginManager::Shutdown()
{
}
void PluginManager::Update()
{
if (mNeedReload)
{
mNeedReload = false; // This needs to be before Loaded/Reload
mPendingAdd = false;
printf("plugin reload needed\n");
Reload();
}
}
void PluginManager::SetPluginDirectory(const char* pluginDirectoryName)
{
mPluginDirectoryName = pluginDirectoryName;
/*
mFileWatcher = std::make_unique<FileWatcher>(pluginDirectoryName, std::regex(".*"), [this](const std::string& file, const filewatch::Event eventType)
{
std::string pluginName = details::PluginNameFromFileName(file);
if (!pluginName.empty() && eventType == filewatch::Event::added)
{
mPendingAdd = true;
}
if (!pluginName.empty() && eventType == filewatch::Event::modified && mPendingAdd != true)
{
printf("=======================================================================================\n");
printf("WARNING: don't overwrite plugins to update them.\n");
printf("Please remove and re-add plugins to hot reload them (plugin: %s)\n", pluginName.c_str());
printf("eg: rm %s ; cp .build/pluginlib %s\n", file.c_str(), file.c_str());
printf("Sometimes this warning happens if remove and copy too quickly, and can be ignored, but otherwise it will crash\n");
printf("=======================================================================================\n");
}
mNeedReload = true;
});
*/
}
void PluginManager::Reload()
{
// Reload plugins
LoadPlugins();
RegisterPlugins();
}
void PluginManager::UnloadPlugin(Plugin& plugin)
{
//std::cout << "unloading plugin " << plugin.Name() << " from file " << plugin.FileName() << std::endl;
}
void PluginManager::LoadPlugins()
{
// Get the set of plugin file names now currently on disk
std::unordered_map<std::string, std::string> pluginFileNames;
/*
for (const auto& entry : std::filesystem::directory_iterator(mPluginDirectoryName))
{
std::string fileName = entry.path().u8string();
std::string pluginName = details::PluginNameFromFileName(fileName);
if (!pluginName.empty())
pluginFileNames[pluginName] = fileName;
}
*/
std::unordered_set<std::string> currentPlugins;
Utilities::GetKeys(currentPlugins, pluginFileNames);
std::unordered_set<std::string> loadedPlugins;
Utilities::GetKeys(loadedPlugins, mLoadedPlugins);
std::unordered_set<std::string> newPlugins;
Utilities::Difference(newPlugins, currentPlugins, loadedPlugins);
std::unordered_set<std::string> removedPlugins;
Utilities::Difference(removedPlugins, loadedPlugins, currentPlugins);
// Attempt to unload plugins that the library file has been removed
for (const auto &pluginName : removedPlugins)
{
//std::cout << "unloading plugin " << pluginName << std::endl;
mLoadedPlugins.at(pluginName).HotEject();
mLoadedPlugins.erase(pluginName);
}
// Attempt to load plugins that are not currently loaded
size_t loadedCount = mLoadedPlugins.size();
bool haveUnloadablePlugins = false;
for (const auto &pluginName : newPlugins)
{
std::string fileName = pluginFileNames[pluginName];
//std::cout << "loading plugin " << pluginName << " from file " << fileName << std::endl;
Plugin plugin(pluginName, fileName.c_str());
if (plugin.Loaded())
mLoadedPlugins.emplace(std::make_pair(pluginName, std::move(plugin)));
else
haveUnloadablePlugins = true;
}
// Iteratively retry as some plugins may depend on other plugins being loaded
// Idea is to keep iterating the unloaded plugins until no new plugins are loaded
// If a plugin was loaded while iterating, then need to iterate again to see if the
// newly loaded plugin then allows others to load. Circular dependancies between
// plugins are not supported.
// Idea here is that it is doing this over a number of frames/updates.
if (loadedCount != mLoadedPlugins.size() && haveUnloadablePlugins)
mNeedReload = true;
}
void PluginManager::RegisterPlugins()
{
for (auto& plugin : mLoadedPlugins)
plugin.second.Register(*this);
}
} // PluginFramework namespace