diff --git a/bin/jni/jni_interface.i b/bin/jni/jni_interface.i index a497c0bb8d618056a1520c57b0779bec7d7c33c8..509339ffecad18e18067d382c0063a2a8c0fc28a 100644 --- a/bin/jni/jni_interface.i +++ b/bin/jni/jni_interface.i @@ -208,6 +208,7 @@ namespace std { %include "datatransfer.i" %include "presencemanager.i" %include "videomanager.i" +%include "plugin_manager_interface.i" #include "dring/callmanager_interface.h" diff --git a/bin/jni/plugin_manager_interface.i b/bin/jni/plugin_manager_interface.i new file mode 100644 index 0000000000000000000000000000000000000000..dac0bcbe8858ed4a576f718de4a44e908d29080a --- /dev/null +++ b/bin/jni/plugin_manager_interface.i @@ -0,0 +1,20 @@ +%header %{ + +#include "dring/dring.h" +#include "dring/plugin_manager_interface.h" +%} + +namespace DRing { +bool loadPlugin(const std::string& path); +bool unloadPlugin(const std::string& path); +void togglePlugin(const std::string& path, bool toggle); +std::map<std::string,std::string> getPluginDetails(const std::string& path); +std::vector<std::map<std::string,std::string>> getPluginPreferences(const std::string& path); +bool setPluginPreference(const std::string& path, const std::string& key, const std::string& value); +std::map<std::string,std::string> getPluginPreferencesValues(const std::string& path); +bool resetPluginPreferencesValues(const std::string& path); +std::vector<std::string> listAvailablePlugins(); +std::vector<std::string> listLoadedPlugins(); +int installPlugin(const std::string& jplPath, bool force); +int uninstallPlugin(const std::string& pluginRootPath); +} diff --git a/src/Makefile.am b/src/Makefile.am index f9f7e3995c2c44298e7f788299a92139ea6d3244..7d6e601f79bd5a1cd7798242611e36d28f851f28 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -161,7 +161,15 @@ libring_la_SOURCES = \ generic_io.h \ scheduled_executor.h \ scheduled_executor.cpp \ - transport/peer_channel.h + transport/peer_channel.h \ + plugin/pluginloader.h \ + plugin/pluginloaderdl.cpp \ + plugin/pluginmanager.h \ + plugin/pluginmanager.cpp \ + plugin/mediahandler.h \ + plugin/callservicemanager.h \ + plugin/jamipluginmanager.h \ + plugin/jamipluginmanager.cpp if HAVE_WIN32 libring_la_SOURCES += \ diff --git a/src/client/Makefile.am b/src/client/Makefile.am index 9d9ab36ff1be8211a9f258b614fe196bf0d4fb92..1dad320f59f38356e881a3e3f2c05cf05b9f0802 100644 --- a/src/client/Makefile.am +++ b/src/client/Makefile.am @@ -17,6 +17,7 @@ libclient_la_SOURCES = \ callmanager.cpp \ configurationmanager.cpp \ datatransfer.cpp \ + plugin_manager_interface.cpp \ $(PRESENCE_SRC) \ $(VIDEO_SRC) diff --git a/src/client/plugin_manager_interface.cpp b/src/client/plugin_manager_interface.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f7e9ef6684901cf0ea297e3424d1ddd59add32d1 --- /dev/null +++ b/src/client/plugin_manager_interface.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2004-2020 Savoir-faire Linux Inc. + * + * 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 3 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "plugin_manager_interface.h" +#include "manager.h" +#include "plugin/jamipluginmanager.h" +#include "logger.h" +#include <iostream> + +namespace DRing { +// +bool +loadPlugin(const std::string& path){ + return jami::Manager::instance().getJamiPluginManager().loadPlugin(path); +} + +bool +unloadPlugin(const std::string& path){ + return jami::Manager::instance().getJamiPluginManager().unloadPlugin(path); +} + +void +togglePlugin(const std::string& path, bool toggle){ + jami::Manager::instance().getJamiPluginManager().togglePlugin(path,toggle); +} + +std::map<std::string,std::string> +getPluginDetails(const std::string& path){ + return jami::Manager::instance().getJamiPluginManager().getPluginDetails(path); +} + +std::vector<std::map<std::string,std::string>> +getPluginPreferences(const std::string& path){ + return jami::Manager::instance().getJamiPluginManager().getPluginPreferences(path); +} + +bool +setPluginPreference(const std::string& path, const std::string& key, const std::string& value) { + return jami::Manager::instance().getJamiPluginManager().setPluginPreference(path, key, value); +} + +std::map<std::string,std::string> +getPluginPreferencesValues(const std::string& path){ + return jami::Manager::instance().getJamiPluginManager().getPluginPreferencesValuesMap(path); +} +bool +resetPluginPreferencesValues(const std::string& path){ + return jami::Manager::instance().getJamiPluginManager().resetPluginPreferencesValuesMap(path); +} + +std::vector<std::string> +listAvailablePlugins() { + return jami::Manager::instance().getJamiPluginManager().listAvailablePlugins(); +} + +std::vector<std::string> +listLoadedPlugins() { + return jami::Manager::instance().getJamiPluginManager().listLoadedPlugins(); +} + +int installPlugin(const std::string& jplPath, bool force) { + return jami::Manager::instance().getJamiPluginManager().installPlugin(jplPath, force); +} + +int uninstallPlugin(const std::string& pluginRootPath) { + return jami::Manager::instance().getJamiPluginManager().uninstallPlugin(pluginRootPath); +} +} diff --git a/src/dring/plugin_manager_interface.h b/src/dring/plugin_manager_interface.h new file mode 100644 index 0000000000000000000000000000000000000000..6639bc24775cc5a27f2fe4c2dff4c6d0330a6816 --- /dev/null +++ b/src/dring/plugin_manager_interface.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2004-2020 Savoir-faire Linux Inc. + * + * 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 3 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include "def.h" + +#include <string> +#include <vector> +#include <map> + +namespace DRing { +DRING_PUBLIC bool loadPlugin(const std::string& path); +DRING_PUBLIC bool unloadPlugin(const std::string& path); +DRING_PUBLIC void togglePlugin(const std::string& path, bool toggle); +DRING_PUBLIC std::map<std::string,std::string> getPluginDetails(const std::string& path); +DRING_PUBLIC std::vector<std::map<std::string,std::string>> getPluginPreferences(const std::string& path); +DRING_PUBLIC bool setPluginPreference(const std::string& path, const std::string& key, const std::string& value); +DRING_PUBLIC std::map<std::string,std::string> getPluginPreferencesValues(const std::string& path); +DRING_PUBLIC bool resetPluginPreferencesValues(const std::string& path); +DRING_PUBLIC std::vector<std::string> listAvailablePlugins(); +DRING_PUBLIC std::vector<std::string> listLoadedPlugins(); +DRING_PUBLIC int installPlugin(const std::string& jplPath, bool force); +DRING_PUBLIC int uninstallPlugin(const std::string& pluginRootPath); +} + diff --git a/src/fileutils.h b/src/fileutils.h index 447204d5a8476c93e119b3c052789d17077714e6..57320be430867f84ecf9e802268e85ac34328beb 100644 --- a/src/fileutils.h +++ b/src/fileutils.h @@ -46,12 +46,14 @@ #ifndef _WIN32 #include <sys/stat.h> // mode_t -#define DIR_SEPARATOR_STR "/" // Directory separator char -#define DIR_SEPARATOR_CH '/' // Directory separator string +#define DIR_SEPARATOR_STR "/" // Directory separator string +#define DIR_SEPARATOR_CH '/' // Directory separator char +#define DIR_SEPARATOR_STR_ESC "\\/" // Escaped directory separator string #else #define mode_t unsigned -#define DIR_SEPARATOR_STR "\\" // Directory separator char -#define DIR_SEPARATOR_CH '\\' // Directory separator string +#define DIR_SEPARATOR_STR "\\" // Directory separator string +#define DIR_SEPARATOR_CH '\\' // Directory separator char +#define DIR_SEPARATOR_STR_ESC "\\\\" // Escaped directory separator string #endif namespace jami { namespace fileutils { diff --git a/src/manager.cpp b/src/manager.cpp index a4b271196162e8a297509abc912659f22fe1401a..b1bcd148f3f88a55c7ca0d9dd7123a8de61452c4 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -63,6 +63,7 @@ using random_device = dht::crypto::random_device; #include "audio/sound/tonelist.h" #include "audio/sound/dtmf.h" #include "audio/ringbufferpool.h" +#include "plugin/jamipluginmanager.h" #ifdef ENABLE_VIDEO #include "client/videomanager.h" @@ -415,6 +416,8 @@ struct Manager::ManagerPimpl #endif std::unique_ptr<SIPVoIPLink> sipLink_; + /* Jami Plugin Manager */ + JamiPluginManager jami_plugin_manager; }; Manager::ManagerPimpl::ManagerPimpl(Manager& base) @@ -3099,6 +3102,11 @@ Manager::sipVoIPLink() const } +JamiPluginManager& Manager::getJamiPluginManager() const +{ + return pimpl_->jami_plugin_manager; +} + std::map<std::string, std::string> Manager::getNearbyPeers(const std::string& accountID) { diff --git a/src/manager.h b/src/manager.h index c50b7015937a811f1663dce8af6bb3a3b4a718c0..5db538e8e829c4f291aaea219cff012730b50a4c 100644 --- a/src/manager.h +++ b/src/manager.h @@ -61,6 +61,7 @@ class IceTransportFactory; class DataTransferFacade; class JamiAccount; class SIPVoIPLink; +class JamiPluginManager; static constexpr uint64_t DRING_ID_MAX_VAL = 9007199254740992; @@ -904,6 +905,7 @@ class DRING_TESTABLE Manager { std::vector<DRing::Message> getLastMessages(const std::string& accountID, const uint64_t& base_timestamp); SIPVoIPLink& sipVoIPLink() const; + JamiPluginManager& getJamiPluginManager() const; private: Manager(); diff --git a/src/plugin/jamiplugin.h b/src/plugin/jamiplugin.h new file mode 100644 index 0000000000000000000000000000000000000000..563bbda2aa358180724369e2239ab728f217b229 --- /dev/null +++ b/src/plugin/jamiplugin.h @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2004-2019 Savoir-faire Linux Inc. + * + * Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> + * + * 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 3 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA. + */ + +#pragma once + +#include <inttypes.h> + +#ifdef __cplusplus +#define EXTERNAL_C_LINKAGE extern "C" +#define C_INTERFACE_START EXTERNAL_C_LINKAGE { +#define C_INTERFACE_END } +#else +#define C_LINKAGE +#define C_INTERFACE_START +#define C_INTERFACE_END +#endif + +#define JAMI_PLUGIN_ABI_VERSION 1 /* 0 doesn't exist, considered as error */ +#define JAMI_PLUGIN_API_VERSION 1 /* 0 doesn't exist, considered as error */ + +C_INTERFACE_START; + +typedef struct JAMI_PluginVersion { + /* plugin is not loadable if this number differs from one + * stored in the plugin loader */ + uint32_t abi; + + /* a difference on api number may be acceptable, see the loader code */ + uint32_t api; +} JAMI_PluginVersion; + +struct JAMI_PluginAPI; + +/* JAMI_PluginCreateFunc parameters */ +typedef struct JAMI_PluginObjectParams { + const JAMI_PluginAPI *pluginApi; /* this API */ + const char *type; +} JAMI_PluginObjectParams; + +typedef void *(*JAMI_PluginCreateFunc)(JAMI_PluginObjectParams *params, + void *closure); + +typedef void (*JAMI_PluginDestroyFunc)(void *object, void *closure); + +/* JAMI_PluginAPI.registerObjectFactory data */ +typedef struct JAMI_PluginObjectFactory { + JAMI_PluginVersion version; + void *closure; /* closure for create */ + JAMI_PluginCreateFunc create; + JAMI_PluginDestroyFunc destroy; +} JAMI_PluginObjectFactory; + +/* Plugins exposed API prototype */ +typedef int32_t (*JAMI_PluginFunc)(const JAMI_PluginAPI *api, const char *name, + void *data); + +/* JAMI_PluginInitFunc parameters. + * This structure is filled by the Plugin manager. + * For backware compatibility, never c + */ +typedef struct JAMI_PluginAPI { + JAMI_PluginVersion version; /* structure version, always the first data */ + void *context; /* opaque structure used by next functions */ + + /* API usable by plugin implementors */ + JAMI_PluginFunc registerObjectFactory; + JAMI_PluginFunc invokeService; + JAMI_PluginFunc manageComponent; +} JAMI_PluginAPI; + +typedef void (*JAMI_PluginExitFunc)(void); + +typedef JAMI_PluginExitFunc (*JAMI_PluginInitFunc)(const JAMI_PluginAPI *api); + +C_INTERFACE_END; + +#define JAMI_DYN_INIT_FUNC_NAME "JAMI_dynPluginInit" +#define JAMI_PLUGIN_INIT_STATIC(fname, pname) JAMI_PLUGIN_INIT(fname, pname) +#define JAMI_PLUGIN_INIT_DYNAMIC(pname) \ + JAMI_PLUGIN_INIT(JAMI_dynPluginInit, pname) + +/* Define here platform dependent way to export a declaration x to the dynamic + * loading system. + */ + +/* Default case (like POSIX/.so) */ + +#define JAMI_PLUGIN_INIT(fname, pname) \ + (EXTERNAL_C_LINKAGE JAMI_PluginExitFunc fname(const JAMI_PluginAPI *pname)) +#define JAMI_PLUGIN_EXIT(fname) (EXTERNAL_C_LINKAGE void fname(void)) + diff --git a/src/plugin/jamipluginmanager.cpp b/src/plugin/jamipluginmanager.cpp new file mode 100644 index 0000000000000000000000000000000000000000..fb23e308f22b6b95ce679c406696d60f8c7d6a21 --- /dev/null +++ b/src/plugin/jamipluginmanager.cpp @@ -0,0 +1,452 @@ +/* + * Copyright (C) 2004-2020 Savoir-faire Linux Inc. + * + * 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 3 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "jamipluginmanager.h" +#include "logger.h" + +#include <sstream> +#include <fstream> +#include <regex> +#include <stdexcept> + +//Manager +#include "manager.h" + +extern "C" { +#include <archive.h> +} + +#include <json/json.h> +#include <msgpack.hpp> + +#if defined(__arm__) + #if defined(__ARM_ARCH_7A__) + #define ABI "armeabi-v7a" + #else + #define ABI "armeabi" + #endif +#elif defined(__i386__) + #define ABI "x86" +#elif defined(__x86_64__) + #define ABI "x86_64" +#elif defined(__mips64) /* mips64el-* toolchain defines __mips__ too */ + #define ABI "mips64" +#elif defined(__mips__) + #define ABI "mips" +#elif defined(__aarch64__) + #define ABI "arm64-v8a" +#else + #define ABI "unknown" +#endif + +#define PLUGIN_ALREADY_INSTALLED 100 /* Plugin already installed with the same version */ +#define PLUGIN_OLD_VERSION 200 /* Plugin already installed with a newer version */ + +namespace jami { + +std::map<std::string, std::string> checkManifestJsonContentValidity(const Json::Value& root) { + std::string name = root.get("name", "").asString(); + std::string description = root.get("description", "").asString(); + std::string version = root.get("version", "").asString(); + if(!name.empty() || !version.empty()){ + return { + {"name", name}, + {"description", description}, + {"version", version}, + }; + } else { + throw std::runtime_error("plugin manifest file: bad format"); + } +} + +std::map<std::string, std::string> checkManifestValidity(std::istream& stream) { + Json::Value root; + Json::CharReaderBuilder rbuilder; + rbuilder["collectComments"] = false; + std::string errs; + + bool ok = Json::parseFromStream(rbuilder, stream, &root, &errs); + + if(ok) { + return checkManifestJsonContentValidity(root); + } else{ + throw std::runtime_error("failed to parse the plugin manifest file"); + } +} + +std::map<std::string, std::string> checkManifestValidity(const std::vector<uint8_t>& vec) { + Json::Value root; + std::unique_ptr<Json::CharReader> json_Reader(Json::CharReaderBuilder{}.newCharReader()); + std::string errs; + + bool ok = json_Reader->parse(reinterpret_cast<const char *>(vec.data()), + reinterpret_cast<const char *>(vec.data()+vec.size()), + &root,&errs); + + if(ok) { + return checkManifestJsonContentValidity(root); + } else{ + throw std::runtime_error("failed to parse the plugin manifest file"); + } +} + +static const std::regex DATA_REGEX("^data" DIR_SEPARATOR_STR_ESC ".+"); +static const std::regex SO_REGEX("([a-z0-9]+(?:[_-]?[a-z0-9]+)*)" DIR_SEPARATOR_STR_ESC "([a-z0-9_]+\\.(so|dll))"); + +std::pair<bool,const std::string> +uncompressJplFunction(const std::string& relativeFileName) +{ + std::smatch match; + if (relativeFileName == "manifest.json" || std::regex_match(relativeFileName, DATA_REGEX)){ + return std::make_pair(true, relativeFileName); + } else if (regex_search(relativeFileName, match, SO_REGEX) == true) { + if (match.str(1) == ABI) { + return std::make_pair(true, match.str(2)); + } + } + return std::make_pair(false, std::string{""}); +} + +std::string convertArrayToString(const Json::Value& jsonArray) +{ + std::string stringArray = "["; + + for(int i=0; i< static_cast<int>(jsonArray.size()) - 1; i++) { + if(jsonArray[i].isString()) { + stringArray+=jsonArray[i].asString()+","; + } else if(jsonArray[i].isArray()) { + stringArray+=convertArrayToString(jsonArray[i])+","; + } + } + + int lastIndex = static_cast<int>(jsonArray.size()) - 1; + if(jsonArray[lastIndex].isString()) { + stringArray+=jsonArray[lastIndex].asString(); + } + + stringArray+="]"; + + return stringArray; +} + +std::map<std::string, std::string> parsePreferenceConfig(const Json::Value &jsonPreference, const std::string &type) +{ + std::map<std::string, std::string> preferenceMap; + const auto& members = jsonPreference.getMemberNames(); + // Insert other fields + for(const auto& member : members) { + const Json::Value& value = jsonPreference[member]; + if(value.isString()) { + preferenceMap.emplace(member, jsonPreference[member].asString()); + } else if (value.isArray()) { + preferenceMap.emplace(member, convertArrayToString(jsonPreference[member])); + } + } + return preferenceMap; +} + +std::map<std::string, std::string> JamiPluginManager::getPluginDetails(const std::string &rootPath) +{ + auto detailsIt = pluginDetailsMap_.find(rootPath); + if (detailsIt != pluginDetailsMap_.end()) { + return detailsIt->second; + } + + std::map<std::string, std::string> details = parseManifestFile(manifestPath(rootPath)); + details["iconPath"] = rootPath + DIR_SEPARATOR_CH + "data" + DIR_SEPARATOR_CH + "icon.png"; + details["soPath"] = rootPath + DIR_SEPARATOR_CH + "lib" + details["name"] + ".so"; + detailsIt = pluginDetailsMap_.emplace(rootPath, std::move(details)).first; + return detailsIt->second; +} + +std::vector<std::string> JamiPluginManager::listAvailablePlugins() +{ + std::string pluginsPath = fileutils::get_data_dir() + DIR_SEPARATOR_CH + "plugins"; + std::vector<std::string> pluginsPaths = fileutils::readDirectory(pluginsPath); + std::for_each(pluginsPaths.begin(), pluginsPaths.end(), + [&pluginsPath](std::string& x){ x = pluginsPath + DIR_SEPARATOR_CH + x;}); + auto predicate = [this](std::string path){ return !checkPluginValidity(path);}; + auto returnIterator = std::remove_if(pluginsPaths.begin(),pluginsPaths.end(),predicate); + pluginsPaths.erase(returnIterator,std::end(pluginsPaths)); + return pluginsPaths; +} + +int JamiPluginManager::installPlugin(const std::string &jplPath, bool force) +{ + int r{0}; + if(fileutils::isFile(jplPath)) { + try{ + auto manifestMap = readPluginManifestFromArchive(jplPath); + std::string name = manifestMap["name"]; + std::string version = manifestMap["version"]; + const std::string destinationDir{fileutils::get_data_dir() + + DIR_SEPARATOR_CH + "plugins" + + DIR_SEPARATOR_CH + name}; + // Find if there is an existing version of this plugin + const auto alreadyInstalledManifestMap = parseManifestFile(manifestPath(destinationDir)); + + if (!alreadyInstalledManifestMap.empty()) { + if (force) { + r = uninstallPlugin(destinationDir); + if(r == 0) { + archiver::uncompressArchive(jplPath, destinationDir, uncompressJplFunction); + } + } else { + std::string installedVersion = alreadyInstalledManifestMap.at("version"); + if (version > installedVersion) { + r = uninstallPlugin(destinationDir); + if(r == 0) { + archiver::uncompressArchive(jplPath, destinationDir, uncompressJplFunction); + } + } else if (version == installedVersion){ + // An error code of 100 to know that this version is the same as the one installed + r = PLUGIN_ALREADY_INSTALLED; + } else { + // An error code of 100 to know that this version is older than the one installed + r = PLUGIN_OLD_VERSION; + } + } + } else { + archiver::uncompressArchive(jplPath, destinationDir, uncompressJplFunction); + } + } catch(const std::exception& e) { + JAMI_ERR() << e.what(); + } + } + return r; +} + +int JamiPluginManager::uninstallPlugin(const std::string &rootPath) +{ + if(checkPluginValidity(rootPath)) { + return fileutils::removeAll(rootPath); + } else { + return -1; + } +} + +bool JamiPluginManager::loadPlugin(const std::string &rootPath) +{ + try { + return pm_.load(getPluginDetails(rootPath).at("soPath")); + } catch(const std::exception& e) { + JAMI_ERR() << e.what(); + return false; + } +} + +bool JamiPluginManager::unloadPlugin(const std::string &rootPath) +{ + try { + return pm_.unload(getPluginDetails(rootPath).at("soPath")); + } catch(const std::exception& e) { + JAMI_ERR() << e.what(); + return false; + } +} + +void JamiPluginManager::togglePlugin(const std::string &rootPath, bool toggle) +{ + try { + std::string soPath = getPluginDetails(rootPath).at("soPath"); + // remove the previous plugin object if it was registered + pm_.destroyPluginComponents(soPath); + // If toggle, register a new instance of the plugin + // function + if(toggle){ + pm_.callPluginInitFunction(soPath); + } + } catch (const std::exception& e) { + JAMI_ERR() << e.what(); + } +} + +std::vector<std::string> JamiPluginManager::listLoadedPlugins() const +{ + std::vector<std::string> loadedSoPlugins = pm_.listLoadedPlugins(); + std::vector<std::string> loadedPlugins{}; + loadedPlugins.reserve(loadedSoPlugins.size()); + std::transform(loadedSoPlugins.begin(), loadedSoPlugins.end(), std::back_inserter(loadedPlugins), + [this](const std::string& soPath) { + return getRootPathFromSoPath(soPath); + }); + return loadedPlugins; +} + +std::vector<std::map<std::string, std::string> > JamiPluginManager::getPluginPreferences(const std::string &rootPath) +{ + const std::string preferenceFilePath = getPreferencesConfigFilePath(rootPath); + std::ifstream file(preferenceFilePath); + Json::Value root; + Json::CharReaderBuilder rbuilder; + rbuilder["collectComments"] = false; + std::string errs; + std::set<std::string> keys; + std::vector<std::map<std::string, std::string>> preferences; + if(file) { + bool ok = Json::parseFromStream(rbuilder, file, &root, &errs); + if(ok && root.isArray()) { + for(int i=0; i< static_cast<int>(root.size()); i++) { + const Json::Value jsonPreference = root[i]; + std::string category = jsonPreference.get("category", "NoCategory").asString(); + std::string type = jsonPreference.get("type", "None").asString(); + std::string key = jsonPreference.get("key", "None").asString(); + if(type != "None" && key != "None") { + if(keys.find(key) == keys.end()) { + const auto& preferenceAttributes = parsePreferenceConfig(jsonPreference, type); + // If the parsing of the attributes was successful, commit the map and the key + if(!preferenceAttributes.empty()) { + preferences.push_back(std::move(preferenceAttributes)); + keys.insert(key); + } + } + } + } + } else { + JAMI_ERR() << "PluginPreferencesParser:: Failed to parse preferences.json for plugin: " + << preferenceFilePath; + } + } + + return preferences; +} + +bool JamiPluginManager::setPluginPreference(const std::string &rootPath, const std::string &key, const std::string &value) +{ + bool returnValue = true; + std::map<std::string, std::string> pluginPreferencesMap = getPluginPreferencesValuesMap(rootPath); + // Using [] instead of insert to get insert or update effect + pluginPreferencesMap[key] = value; + + { + const std::string preferencesValuesFilePath = pluginPreferencesValuesFilePath(rootPath); + std::ofstream fs(preferencesValuesFilePath, std::ios::binary); + if(!fs.good()) { + return false; + } + try { + std::lock_guard<std::mutex> guard(fileutils::getFileLock(preferencesValuesFilePath)); + msgpack::pack(fs, pluginPreferencesMap); + } catch (const std::exception& e) { + returnValue = false; + JAMI_ERR() << e.what(); + } + } + + return returnValue; +} + +std::map<std::string, std::string> JamiPluginManager::getPluginPreferencesValuesMap(const std::string &rootPath) +{ + const std::string preferencesValuesFilePath = pluginPreferencesValuesFilePath(rootPath); + std::ifstream file(preferencesValuesFilePath, std::ios::binary); + std::map<std::string, std::string> rmap; + // If file is accessible + if(file.good()) { + std::lock_guard<std::mutex> guard(fileutils::getFileLock(preferencesValuesFilePath)); + // Get file size + std::string str; + file.seekg(0, std::ios::end); + size_t fileSize = static_cast<size_t>(file.tellg()); + // If not empty + if(fileSize > 0) { + // Read whole file content and put it in the string str + str.reserve(static_cast<size_t>(file.tellg())); + file.seekg(0, std::ios::beg); + str.assign((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()); + file.close(); + try { + // Unpack the string + msgpack::object_handle oh = msgpack::unpack(str.data(), str.size()); + // Deserialized object is valid during the msgpack::object_handle instance is alive. + msgpack::object deserialized = oh.get(); + deserialized.convert(rmap); + } catch (const std::exception& e) { + JAMI_ERR() << e.what(); + } + } + } + + return rmap; +} + +bool JamiPluginManager::resetPluginPreferencesValuesMap(const std::string &rootPath) +{ + bool returnValue = true; + std::map<std::string, std::string> pluginPreferencesMap{}; + + { + const std::string preferencesValuesFilePath = pluginPreferencesValuesFilePath(rootPath); + std::ofstream fs(preferencesValuesFilePath, std::ios::binary); + if(!fs.good()) { + return false; + } + try { + std::lock_guard<std::mutex> guard(fileutils::getFileLock(preferencesValuesFilePath)); + msgpack::pack(fs, pluginPreferencesMap); + } catch (const std::exception& e) { + returnValue = false; + JAMI_ERR() << e.what(); + } + } + + return returnValue; +} + +std::map<std::string, std::string> JamiPluginManager::readPluginManifestFromArchive(const std::string &jplPath) +{ + try { + return checkManifestValidity(archiver::readFileFromArchive(jplPath,"manifest.json")); + } catch (const std::exception& e) { + JAMI_ERR() << e.what(); + } +} + +std::map<std::string, std::string> JamiPluginManager::parseManifestFile(const std::string &manifestFilePath) +{ + std::ifstream file(manifestFilePath); + if(file) { + try { + return checkManifestValidity(file); + } catch (const std::exception& e) { + JAMI_ERR() << e.what(); + } + } + + return {}; +} + +void JamiPluginManager::registerServices() +{ + // Register pluginPreferences + pm_.registerService("getPluginPreferences", [this](const DLPlugin* plugin, void* data) { + auto ppp = static_cast<std::map<std::string, std::string>*>(data); + *ppp = getPluginPreferencesValuesMap( + getRootPathFromSoPath(plugin->getPath())); + return 0; + }); + + pm_.registerService("getPluginDataPath", [this](const DLPlugin* plugin, void* data) { + auto dataPath_ = static_cast<std::string*>(data); + dataPath_->assign(dataPath(plugin->getPath())); + return 0; + }); +} + +} + diff --git a/src/plugin/jamipluginmanager.h b/src/plugin/jamipluginmanager.h new file mode 100644 index 0000000000000000000000000000000000000000..34b951216cae70d85c58baaa7b28f401787872ce --- /dev/null +++ b/src/plugin/jamipluginmanager.h @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2004-2020 Savoir-faire Linux Inc. + * + * 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 3 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once +#include "noncopyable.h" +#include "fileutils.h" +#include "archiver.h" +#include "pluginmanager.h" + +#include <vector> +#include <map> +#include <string> +#include <algorithm> + +namespace jami { +class JamiPluginManager +{ +public: + JamiPluginManager() { + registerServices(); + } + // TODO : improve getPluginDetails + /** + * @brief getPluginDetails + * Parses a manifest file and returns : + * The tuple (name, description, version, icon path, so path) + * The icon should ideally be 192x192 pixels or better 512x512 pixels + * In order to match with android specifications + * https://developer.android.com/google-play/resources/icon-design-specifications + * Saves the result in a map + * @param plugin rootPath (folder of the plugin) + * @return map where the keyset is {"name", "description", "iconPath"} + */ + std::map<std::string, std::string> getPluginDetails(const std::string& rootPath); + + /** + * @brief listAvailablePlugins + * Lists available plugins with valid manifest files + * @return list of plugin directory names + */ + std::vector<std::string> listAvailablePlugins(); + + /** + * @brief installPlugin + * Checks if the plugin has a valid manifest, installs the plugin if not previously installed + * or if installing a newer version of it + * If force is true, we force install the plugin + * @param jplPath + * @param force + * @return + 0 if success + * 100 if already installed with similar version + * 200 if already installed with newer version + * libarchive error codes otherwise + */ + int installPlugin(const std::string& jplPath, bool force); + + /** + * @brief uninstallPlugin + * Checks if the plugin has a valid manifest then removes plugin folder + * @param rootPath + * @return 0 if success + */ + int uninstallPlugin(const std::string& rootPath); + + /** + * @brief loadPlugin + * @param rootPath of the plugin folder + * @return true is success + */ + bool loadPlugin(const std::string& rootPath); + + /** + * @brief unloadPlugin + * @param rootPath of the plugin folder + * @return true is success + */ + bool unloadPlugin(const std::string& rootPath); + + /** + * @brief togglePlugin + * @param rootPath of the plugin folder + * @param toggle: if true, register a new instance of the plugin + * else, remove the existing instance + * N.B: before adding a new instance, remove any existing one + */ + void togglePlugin(const std::string& rootPath, bool toggle); + + /** + * @brief listLoadedPlugins + * @return vector of rootpaths of the loaded plugins + */ + std::vector<std::string> listLoadedPlugins() const; + + std::vector<std::map<std::string,std::string>> getPluginPreferences(const std::string& rootPath); + + bool setPluginPreference(const std::string& rootPath, + const std::string& key, + const std::string& value); + + std::map<std::string,std::string> + getPluginPreferencesValuesMap(const std::string& rootPath); + + bool resetPluginPreferencesValuesMap(const std::string& rootPath); + +private: + + NON_COPYABLE(JamiPluginManager); + + /** + * @brief checkPluginValidity + * Checks if the plugin has a manifest file with a name and a version + * @return true if valid + */ + bool checkPluginValidity(const std::string& rootPath) { + return !parseManifestFile(manifestPath(rootPath)).empty(); + } + + /** + * @brief readPluginManifestFromArchive + * Reads the manifest file content without uncompressing the whole archive + * Maps the manifest data to a map(string, string) + * @param jplPath + * @return manifest map + */ + std::map<std::string, std::string> readPluginManifestFromArchive(const std::string &jplPath); + + /** + * @brief parseManifestFile, parses the manifest file of an installed plugin + * @param manifestFilePath + * @return manifest map + */ + std::map<std::string, std::string> parseManifestFile(const std::string &manifestFilePath); + + std::string manifestPath(const std::string& rootPath) { + return rootPath + DIR_SEPARATOR_CH + "manifest.json"; + } + + std::string getRootPathFromSoPath(const std::string& soPath) const { + return soPath.substr(0,soPath.find_last_of(DIR_SEPARATOR_CH)); + } + + std::string manifestPath(const std::string& rootPath) const { + return rootPath + DIR_SEPARATOR_CH + "manifest.json"; + } + + std::string dataPath(const std::string& pluginSoPath) const { + return getRootPathFromSoPath(pluginSoPath) + DIR_SEPARATOR_CH + "data"; + } + + /** + * @brief getPreferencesConfigFilePath + * Returns the plugin preferences config file path from the plugin root path + * This is entirely defined by how the plugin files are structured + * @param plugin rootPath + * @return path of the preferences config + */ + std::string getPreferencesConfigFilePath(const std::string& rootPath) const { + return rootPath + DIR_SEPARATOR_CH + "data" + DIR_SEPARATOR_CH + "preferences.json"; + } + + /** + * @brief pluginPreferencesValuesFilePath + * Returns the plugin preferences values file path from the plugin root path + * This is entirely defined by how the plugin files are structured + * @param plugin rootPath + * @return path of the preferences values + */ + std::string pluginPreferencesValuesFilePath(const std::string& rootPath) const { + return rootPath + DIR_SEPARATOR_CH + "preferences.msgpack"; + } + + void registerServices(); + +private: + PluginManager pm_; + std::map<std::string, std::map<std::string, std::string>> pluginDetailsMap_; +}; +} + + diff --git a/src/plugin/pluginloader.h b/src/plugin/pluginloader.h new file mode 100644 index 0000000000000000000000000000000000000000..a8b00e30ac378b34ed9251bfdefde6269578f47c --- /dev/null +++ b/src/plugin/pluginloader.h @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2004-2020 Savoir-faire Linux Inc. + * + * Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> + * + * 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 3 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA. + */ +#pragma once + +#include "jamiplugin.h" +#include <dlfcn.h> +#include <string> +#include <memory> + +namespace jami { + +class Plugin { +public: + virtual ~Plugin() = default; + + static Plugin *load(const std::string &path, std::string &error); + virtual void *getSymbol(const char *name) const = 0; + virtual JAMI_PluginInitFunc getInitFunction() const { + return reinterpret_cast<JAMI_PluginInitFunc>( + getSymbol(JAMI_DYN_INIT_FUNC_NAME)); + } + +protected: + Plugin() = default; +}; + +class DLPlugin : public Plugin { +public: + DLPlugin(void *handle, const std::string& path) : handle_(handle, ::dlclose), path_{path} { + api_.context = this; + } + + virtual ~DLPlugin() { unload();} + bool unload() { + if(!handle_){ + return false; + } + return ::dlclose(handle_.release()); + } + + + void *getSymbol(const char *name) const { + if (!handle_) + return nullptr; + + return ::dlsym(handle_.get(), name); + } + + const std::string& getPath() const { + return path_; + } + +public: + void* apiContext_; + JAMI_PluginAPI api_; + +private: + std::unique_ptr<void, int (*)(void *)> handle_; + const std::string path_; +}; + +} // namespace jami diff --git a/src/plugin/pluginloaderdl.cpp b/src/plugin/pluginloaderdl.cpp new file mode 100644 index 0000000000000000000000000000000000000000..12df5714bafe1a1552fe0b55637d02d7b4f1a2e3 --- /dev/null +++ b/src/plugin/pluginloaderdl.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2004-2018 Savoir-faire Linux Inc. + * + * Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> + * + * 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 3 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA. + */ + +#include "pluginloader.h" + +namespace jami { + +Plugin *Plugin::load(const std::string &path, std::string &error) { + if (path.empty()) { + error = "Empty path"; + return nullptr; + } + + // Clear any existing error + ::dlerror(); + + void *handle = ::dlopen(path.c_str(), RTLD_NOW); + if (!handle) { + error += "Failed to load \"" + path + '"'; + + std::string dlError = ::dlerror(); + if (dlError.size()) + error += " (" + dlError + ")"; + return nullptr; + } + + return new DLPlugin(handle, path); +} + +} // namespace jami diff --git a/src/plugin/pluginmanager.cpp b/src/plugin/pluginmanager.cpp new file mode 100644 index 0000000000000000000000000000000000000000..631271271dca65ea3f1319c4b0de42ad52393e00 --- /dev/null +++ b/src/plugin/pluginmanager.cpp @@ -0,0 +1,355 @@ +/* + * Copyright (C) 2004-2020 Savoir-faire Linux Inc. + * + * Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> + * + * 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 3 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA. + */ + +#include "pluginmanager.h" +#include "logger.h" + +#include <utility> + +namespace jami { + +PluginManager::PluginManager() { + pluginApi_.context = reinterpret_cast<void *>(this); +} + +PluginManager::~PluginManager() { + for (auto func : exitFuncVec_) { + try { + (*func)(); + } catch (...) { + JAMI_ERR() << "Exception caught during plugin exit"; + } + } + + dynPluginMap_.clear(); + exactMatchMap_.clear(); + wildCardVec_.clear(); + exitFuncVec_.clear(); +} + +bool PluginManager::load(const std::string &path) { + // TODO: Resolve symbolic links and make path absolute + + // Don't load the same dynamic library twice + if (dynPluginMap_.find(path) != dynPluginMap_.end()) { + JAMI_WARN() << "Plugin: already loaded"; + return true; + } + + std::string error; + std::unique_ptr<Plugin> plugin(Plugin::load(path, error)); + if (!plugin) { + JAMI_ERR() << "Plugin: " << error; + return false; + } + + const auto &init_func = plugin->getInitFunction(); + if (!init_func) { + JAMI_ERR() << "Plugin: no init symbol" << error; + return false; + } + + if (!registerPlugin(plugin)) + return false; + + dynPluginMap_[path] = std::move(plugin); + return true; +} + +bool PluginManager::unload(const std::string& path) { + bool returnValue{false}; + destroyPluginComponents(path); + PluginMap::iterator it = dynPluginMap_.find(path); + if ( it != dynPluginMap_.end()) { + dynPluginMap_.erase(it); + returnValue = true; + } + + return returnValue; +} + +std::vector<std::string> PluginManager::listLoadedPlugins() const +{ + std::vector<std::string> res{}; + for(const auto& pair : dynPluginMap_) { + res.push_back(pair.first); + } + return res; +} + +void PluginManager::destroyPluginComponents(const std::string &path) +{ + auto itComponents = pluginComponentsMap_.find(path); + if(itComponents != pluginComponentsMap_.end()) { + for(const auto& pair : itComponents->second) { + auto clcm = componentsLifeCycleManagers_.find(pair.first); + if(clcm != componentsLifeCycleManagers_.end()) { + clcm->second.destroyComponent(pair.second); + } + } + } +} + + +bool PluginManager::callPluginInitFunction(const std::string &path){ + bool returnValue{false}; + PluginMap::iterator it = dynPluginMap_.find(path); + if ( it != dynPluginMap_.end()) { + // Plugin found + // Since the Plugin was found it is of type DLPlugin with a valid init symbol + std::shared_ptr<DLPlugin> plugin = std::static_pointer_cast<DLPlugin>(it->second); + const auto &initFunc = plugin->getInitFunction(); + JAMI_PluginExitFunc exitFunc = nullptr; + + try { + // Call Plugin Init function + exitFunc = initFunc(&plugin->api_); + } catch (const std::runtime_error &e) { + JAMI_ERR() << e.what(); + return false; + } + + if (!exitFunc) { + JAMI_ERR() << "Plugin: init failed"; + returnValue = false; + } else { + returnValue = true; + } + } + + return returnValue; +} + +bool PluginManager::registerPlugin(std::unique_ptr<Plugin>& plugin) { + // Here we know that Plugin is of type DLPlugin with a valid init symbol + const auto &initFunc = plugin->getInitFunction(); + JAMI_PluginExitFunc exitFunc = nullptr; + + DLPlugin* pluginPtr = static_cast<DLPlugin*>(plugin.get()); + + pluginPtr->apiContext_ = this; + pluginPtr->api_.version = {JAMI_PLUGIN_ABI_VERSION, JAMI_PLUGIN_API_VERSION}; + pluginPtr->api_.registerObjectFactory = registerObjectFactory_; + /** + * Implements JAMI_PluginAPI.invokeService(). + * Must be C accessible. + */ + pluginPtr->api_.invokeService = [](const JAMI_PluginAPI *api, + const char *name, void *data) { + auto plugin = static_cast<DLPlugin*>(api->context); + auto manager = reinterpret_cast<PluginManager *>(plugin->apiContext_); + if (!manager) { + JAMI_ERR() << "invokeService called with null plugin API"; + return -1; + } + + return manager->invokeService(plugin, name, data); + }; + + /** + * Implements JAMI_PluginAPI.invokeService(). + * Must be C accessible. + */ + pluginPtr->api_.manageComponent = [](const JAMI_PluginAPI* api, const char* name, void *data){ + auto plugin = static_cast<DLPlugin*>(api->context); + auto manager = reinterpret_cast<PluginManager *>(plugin->apiContext_); + if (!manager) { + JAMI_ERR() << "createComponent called with null plugin API"; + return -1; + } else if(!plugin){ + JAMI_ERR() << "createComponent called with null context"; + return -1; + } + + return manager->manageComponent(plugin, name, data); + }; + + try { + exitFunc = initFunc(&pluginPtr->api_); + } catch (const std::runtime_error &e) { + JAMI_ERR() << e.what(); + } + + if (!exitFunc) { + tempExactMatchMap_.clear(); + tempWildCardVec_.clear(); + JAMI_ERR() << "Plugin: init failed"; + return false; + } + + exitFuncVec_.push_back(exitFunc); + exactMatchMap_.insert(tempExactMatchMap_.begin(), tempExactMatchMap_.end()); + wildCardVec_.insert(wildCardVec_.end(), tempWildCardVec_.begin(), + tempWildCardVec_.end()); + return true; +} + +bool PluginManager::registerService(const std::string &name, + ServiceFunction &&func) { + services_[name] = std::forward<ServiceFunction>(func); + return true; +} + +void PluginManager::unRegisterService(const std::string &name) { + services_.erase(name); +} + +int32_t PluginManager::invokeService(const DLPlugin* plugin, const std::string &name, void *data) { + const auto &iterFunc = services_.find(name); + if (iterFunc == services_.cend()) { + JAMI_ERR() << "Services not found: " << name; + return -1; + } + + const auto &func = iterFunc->second; + + try { + return func(plugin, data); + } catch (const std::runtime_error &e) { + JAMI_ERR() << e.what(); + return -1; + } +} + +int32_t PluginManager::manageComponent(const DLPlugin* plugin, const std::string& name, void *data) { + const auto& iter = componentsLifeCycleManagers_.find(name); + if(iter == componentsLifeCycleManagers_.end()) { + JAMI_ERR() << "Component lifecycle manager not found: " << name; + return -1; + } + + const auto& componentLifecycleManager = iter->second; + + try { + int32_t r = componentLifecycleManager.takeComponentOwnership(data); + if(r == 0) { + pluginComponentsMap_[plugin->getPath()].emplace_back(name,data); + } + return r; + } catch(const std::runtime_error &e) { + JAMI_ERR() << e.what(); + return -1; + } +} + +/* WARNING: exposed to plugins through JAMI_PluginAPI */ +bool PluginManager::registerObjectFactory( + const char *type, const JAMI_PluginObjectFactory &factoryData) { + if (!type) + return false; + + if (!factoryData.create || !factoryData.destroy) + return false; + + // Strict compatibility on ABI + if (factoryData.version.abi != pluginApi_.version.abi) + return false; + + // Backward compatibility on API + if (factoryData.version.api < pluginApi_.version.api) + return false; + + const std::string key(type); + auto deleter = [factoryData](void *o) { + factoryData.destroy(o, factoryData.closure); + }; + ObjectFactory factory = {factoryData, deleter}; + + // wildcard registration? + if (key == "*") { + wildCardVec_.push_back(factory); + return true; + } + + // fails on duplicate for exactMatch map + if (exactMatchMap_.find(key) != exactMatchMap_.end()) + return false; + + exactMatchMap_[key] = factory; + return true; +} + +bool PluginManager::registerComponentManager(const std::string &name, + ComponentFunction &&takeOwnership, + ComponentFunction &&destroyComponent) +{ + componentsLifeCycleManagers_[name] = {std::forward<ComponentFunction>(takeOwnership), + std::forward<ComponentFunction>(destroyComponent)}; + return true; +} + +std::unique_ptr<void, PluginManager::ObjectDeleter> +PluginManager::createObject(const std::string &type) { + if (type == "*") + return {nullptr, nullptr}; + + JAMI_PluginObjectParams op = { + /*.pluginApi = */ &pluginApi_, + /*.type = */ type.c_str(), + }; + + // Try to find an exact match + const auto &factoryIter = exactMatchMap_.find(type); + if (factoryIter != exactMatchMap_.end()) { + const auto &factory = factoryIter->second; + auto object = factory.data.create(&op, factory.data.closure); + if (object) + return {object, factory.deleter}; + } + + // Try to find a wildcard match + for (const auto &factory : wildCardVec_) { + auto object = factory.data.create(&op, factory.data.closure); + if (object) { + // promote registration to exactMatch_ + // (but keep also wildcard registration for other object types) + int32_t res = registerObjectFactory(op.type, factory.data); + if (res < 0) { + JAMI_ERR() << "failed to register object " << op.type; + return {nullptr, nullptr}; + } + + return {object, factory.deleter}; + } + } + + return {nullptr, nullptr}; +} + +/* WARNING: exposed to plugins through JAMI_PluginAPI */ +int32_t PluginManager::registerObjectFactory_(const JAMI_PluginAPI *api, + const char *type, void *data) { + auto manager = reinterpret_cast<PluginManager *>(api->context); + if (!manager) { + JAMI_ERR() << "registerObjectFactory called with null plugin API"; + return -1; + } + + if (!data) { + JAMI_ERR() << "registerObjectFactory called with null factory data"; + return -1; + } + + const auto factory = reinterpret_cast<JAMI_PluginObjectFactory *>(data); + return manager->registerObjectFactory(type, *factory) ? 0 : -1; +} + +} // namespace jami diff --git a/src/plugin/pluginmanager.h b/src/plugin/pluginmanager.h new file mode 100644 index 0000000000000000000000000000000000000000..9d47925ba82acf2a0488e91c631f166634689bbe --- /dev/null +++ b/src/plugin/pluginmanager.h @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2004-2020 Savoir-faire Linux Inc. + * + * Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> + * + * 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 3 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA. + */ +#pragma once + +#include "noncopyable.h" +#include "jamiplugin.h" +#include "pluginloader.h" + +#include <functional> +#include <map> +#include <memory> +#include <mutex> +#include <string> +#include <vector> + +#include <inttypes.h> + +namespace jami { + +class Plugin; + +class PluginManager { +public: + using ObjectDeleter = std::function<void(void *)>; + using ServiceFunction = std::function<int32_t(const DLPlugin*, void *)>; + using ComponentFunction = std::function<int32_t(void*)>; + //A vector to a pair<componentType, componentPtr> + using ComponentTypePtrVector = std::vector<std::pair<std::string, void*>>; +private: + struct ObjectFactory { + JAMI_PluginObjectFactory data; + ObjectDeleter deleter; + }; + + struct ComponentLifeCycleManager{ + ComponentFunction takeComponentOwnership; + ComponentFunction destroyComponent; + }; + + using PluginMap = std::map<std::string, std::shared_ptr<Plugin>>; + using PluginComponentsMap = std::map<std::string, ComponentTypePtrVector>; + using ExitFuncVec = std::vector<JAMI_PluginExitFunc>; + using ObjectFactoryVec = std::vector<ObjectFactory>; + using ObjectFactoryMap = std::map<std::string, ObjectFactory>; + +public: + PluginManager(); + ~PluginManager(); + + /** + * Load a dynamic plugin by filename. + * + * @param path fully qualified pathname on a loadable plugin binary + * @return true if success + */ + bool load(const std::string &path); + + /** + * @brief unloads the plugin with pathname path + * @param path + * @return true if success + */ + bool unload(const std::string& path); + + /** + * @brief listLoadedPlugins + * @return vector of strings of so files of the loaded plugins + */ + std::vector<std::string> listLoadedPlugins() const; + + /** + * @brief destroyPluginComponents + * @param path + */ + void destroyPluginComponents(const std::string& path); + + /** + * @brief callPluginInitFunction + * @param path: plugin path used as an id in the plugin map + * @return true if succes + */ + bool callPluginInitFunction(const std::string& path); + /** + * Register a plugin. + * + * @param initFunc plugin init function + * @return true if success + */ + bool registerPlugin(std::unique_ptr<Plugin>& plugin); + + /** + * Register a new service for plugin. + * + * @param name The service name + * @param func The function called by Ring_PluginAPI.invokeService + * @return true if success + */ + bool registerService(const std::string &name, ServiceFunction &&func); + + void unRegisterService(const std::string &name); + + /** + * Register a new public objects factory. + * + * @param type unique identifier of the object + * @param params object factory details + * @return true if success + * + * Note: type can be the string "*" meaning that the factory + * will be called if no exact match factories are found for a given type. + */ + bool registerObjectFactory(const char *type, + const JAMI_PluginObjectFactory &factory); + /** + * @brief registerComponentManager + * Registers a component manager that will have two functions, one to take + * ownership of the component and the other one to destroy it + * @param name : name of the component manager + * @param takeOwnership function that takes ownership on created objet in memory + * @param destroyComponent desotry the component + * @return true if success + */ + bool registerComponentManager(const std::string& name, ComponentFunction&& takeOwnership, + ComponentFunction&& destroyComponent); + + /** + * Create a new plugin's exported object. + * + * @param type unique identifier of the object to create. + * @return unique pointer on created object. + */ + std::unique_ptr<void, ObjectDeleter> createObject(const std::string &type); + +private: + NON_COPYABLE(PluginManager); + + /** + * Implements JAMI_PluginAPI.registerObjectFactory(). + * Must be C accessible. + */ + static int32_t registerObjectFactory_(const JAMI_PluginAPI *api, + const char *type, void *data); + int32_t invokeService(const DLPlugin* plugin, const std::string &name, void *data); + + int32_t manageComponent(const DLPlugin* plugin, const std::string& name, void *data); + + std::mutex mutex_{}; + JAMI_PluginAPI pluginApi_ = { + {JAMI_PLUGIN_ABI_VERSION, JAMI_PLUGIN_API_VERSION}, + nullptr, // set by PluginManager constructor + registerObjectFactory_, + nullptr, + nullptr + }; + PluginMap dynPluginMap_{}; // Only dynamic loaded plugins + ExitFuncVec exitFuncVec_{}; + ObjectFactoryMap exactMatchMap_{}; + ObjectFactoryVec wildCardVec_{}; + + // Storage used during plugin initialisation. + // Will be copied into previous ones only if the initialisation success. + ObjectFactoryMap tempExactMatchMap_{}; + ObjectFactoryVec tempWildCardVec_{}; + + // registered services + std::map<std::string, ServiceFunction> services_{}; + // registered component lifecycle managers + std::map<std::string, ComponentLifeCycleManager> componentsLifeCycleManagers_ {}; + + // references to plugins components, used for cleanup + PluginComponentsMap pluginComponentsMap_ {}; +}; + +} // namespace jami