| /** @file |
| |
| A class that deals with plugin Dynamic Shared Objects (DSO) |
| |
| @section license License |
| |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| |
| @section details Details |
| |
| Implements code necessary for Reverse Proxy which mostly consists of |
| general purpose hostname substitution in URLs. |
| |
| */ |
| |
| #include "proxy/http/remap/PluginDso.h" |
| #include "iocore/eventsystem/Freer.h" |
| #ifdef PLUGIN_DSO_TESTS |
| #include "unit-tests/plugin_testing_common.h" |
| #else |
| #include "tscore/Diags.h" |
| #define PluginDbg Dbg |
| #define PluginError Error |
| #endif |
| |
| #include <cstdlib> |
| #include <utility> |
| |
| namespace |
| { |
| |
| // concat_error joins msg to the given error string, separating it from |
| // an existing error with a colon. |
| void |
| concat_error(std::string &error, const std::string &msg) |
| { |
| if (error.empty()) { |
| error.assign(msg); |
| } else { |
| error.append(": ").append(msg); |
| } |
| } |
| |
| } // namespace |
| |
| PluginDso::PluginDso(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath) |
| : _configPath(configPath), _effectivePath(effectivePath), _runtimePath(runtimePath) |
| { |
| PluginDbg(_dbg_ctl(), "PluginDso (%p) created _configPath: [%s] _effectivePath: [%s] _runtimePath: [%s]", this, |
| _configPath.c_str(), _effectivePath.c_str(), _runtimePath.c_str()); |
| } |
| |
| PluginDso::~PluginDso() |
| { |
| std::string error; |
| |
| (void)unload(error); |
| } |
| |
| bool |
| PluginDso::load(std::string &error, const fs::path &compilerPath) |
| { |
| /* Clear all errors */ |
| error.clear(); |
| bool result = true; |
| |
| if (isLoaded()) { |
| concat_error(error, "plugin already loaded"); |
| return false; |
| } |
| |
| PluginDbg(_dbg_ctl(), "plugin '%s' started loading DSO", _configPath.c_str()); |
| |
| /* Find plugin DSO looking through the search dirs */ |
| if (_effectivePath.empty()) { |
| concat_error(error, "empty effective path"); |
| result = false; |
| } else { |
| PluginDbg(_dbg_ctl(), "plugin '%s' effective path: %s", _configPath.c_str(), _effectivePath.c_str()); |
| |
| std::error_code ec; |
| |
| if (!_effectivePath.string().ends_with(".so")) { |
| if (!isDynamicReloadEnabled()) { |
| concat_error(error, "Dynamic reload must be enabled for Cript files"); |
| result = false; |
| } else { |
| std::string command = compilerPath.string() + " " + _effectivePath.string() + " " + _runtimePath.string(); |
| |
| if (std::system(command.c_str()) != 0) { |
| concat_error(error, "Compile script failed"); |
| result = false; |
| } |
| } |
| } else if (isDynamicReloadEnabled() && !fs::copy(_effectivePath, _runtimePath, ec)) { |
| concat_error(error, "failed to create a copy"); |
| concat_error(error, ec.message()); |
| result = false; |
| } |
| |
| if (result) { |
| PluginDbg(_dbg_ctl(), "plugin '%s' runtime path: %s", _configPath.c_str(), _runtimePath.c_str()); |
| |
| /* Save the time for later checking if DSO got modified in consecutive DSO reloads */ |
| std::error_code ec; |
| _mtime = fs::last_write_time(_effectivePath, ec); |
| PluginDbg(_dbg_ctl(), "plugin '%s' modification time %ld", _configPath.c_str(), ts_clock::to_time_t(_mtime)); |
| |
| /* Now attempt to load the plugin DSO */ |
| #if defined(darwin) |
| if (!dlopen_preflight(_runtimePath.c_str()) || (_dlh = dlopen(_runtimePath.c_str(), RTLD_NOW | RTLD_LOCAL)) == nullptr) { |
| #else |
| if ((_dlh = dlopen(_runtimePath.c_str(), RTLD_NOW | RTLD_LOCAL)) == nullptr) { |
| #endif |
| const char *err = dlerror(); |
| concat_error(error, err ? err : "unknown dlopen() error"); |
| _dlh = nullptr; /* mark that the constructor failed. */ |
| |
| clean(error); |
| result = false; |
| |
| PluginError("plugin '%s' failed to load: %s", _configPath.c_str(), error.c_str()); |
| } |
| } |
| } |
| PluginDbg(_dbg_ctl(), "plugin '%s' finished loading DSO", _configPath.c_str()); |
| |
| return result; |
| } |
| |
| /** |
| * @brief unload plugin DSO |
| * |
| * @param error - error messages in case of failure. |
| * @return true - success, false - failure during unload. |
| */ |
| bool |
| PluginDso::unload(std::string &error) |
| { |
| if (!isLoaded()) { |
| concat_error(error, "no plugin loaded"); |
| return false; |
| } |
| |
| bool unloaded = (0 == dlclose(_dlh)); |
| |
| _dlh = nullptr; |
| |
| if (!unloaded) { |
| const char *err = dlerror(); |
| concat_error(error, "failed to unload plugin"); |
| concat_error(error, err ? err : "unknown dlopen() error"); |
| return false; |
| } |
| |
| clean(error); |
| return true; |
| } |
| |
| /** |
| * @brief returns the address of a symbol in the plugin DSO |
| * |
| * @param symbol symbol name |
| * @param address reference to the address to be returned to the caller |
| * @param error error messages in case of symbol is not found |
| * @return true if success, false could not find the symbol (symbol can be nullptr itself) |
| */ |
| bool |
| PluginDso::getSymbol(const char *symbol, void *&address, std::string &error) const |
| { |
| /* Clear the errors */ |
| dlerror(); |
| |
| address = dlsym(_dlh, symbol); |
| char *err = dlerror(); |
| |
| if (nullptr == address && nullptr != err) { |
| /* symbol really cannot be found */ |
| error.assign(err); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * @brief shows if the DSO corresponding to this effective path has already been loaded. |
| * @return true - loaded, false - not loaded |
| */ |
| bool |
| PluginDso::isLoaded() |
| { |
| return nullptr != _dlh; |
| } |
| |
| /** |
| * @brief full path to the first plugin found in the search path which will be used to be loaded. |
| * |
| * @return full path to the plugin DSO. |
| */ |
| const fs::path & |
| PluginDso::effectivePath() const |
| { |
| return _effectivePath; |
| } |
| |
| /** |
| * @brief full path to the runtime location of the plugin DSO actually loaded. |
| * |
| * @return full path to the runtime plugin DSO. |
| */ |
| |
| const fs::path & |
| PluginDso::runtimePath() const |
| { |
| return _runtimePath; |
| } |
| |
| /** |
| * @brief DSO modification time at the moment of DSO load. |
| * |
| * @return modification time. |
| */ |
| |
| swoc::file::file_time_type |
| PluginDso::modTime() const |
| { |
| return _mtime; |
| } |
| |
| /** |
| * @brief file handle returned by dlopen syscall |
| * |
| * @return dlopen filehandle |
| */ |
| |
| void * |
| PluginDso::dlOpenHandle() const |
| { |
| return _dlh; |
| } |
| |
| /** |
| * @brief clean files created by the plugin instance and handle errors |
| * |
| * @param error a human readable error message if something goes wrong |
| * @ return void |
| */ |
| void |
| PluginDso::clean(std::string &error) |
| { |
| if (!isDynamicReloadEnabled()) { |
| return; |
| } |
| |
| std::error_code err; |
| remove_all(_runtimePath, err); |
| if (err) { |
| concat_error(error, "failed to remove runtime copy"); |
| concat_error(error, err.message()); |
| } |
| } |
| |
| void |
| PluginDso::acquire() |
| { |
| this->refcount_inc(); |
| PluginDbg(_dbg_ctl(), "plugin DSO acquire (ref-count:%d, dso-addr:%p)", this->refcount(), this); |
| } |
| |
| void |
| PluginDso::release() |
| { |
| PluginDbg(_dbg_ctl(), "plugin DSO release (ref-count:%d, dso-addr:%p)", this->refcount() - 1, this); |
| if (0 == this->refcount_dec()) { |
| PluginDbg(_dbg_ctl(), "unloading plugin DSO '%s' (dso-addr:%p)", _configPath.c_str(), this); |
| _plugins->remove(this); |
| } |
| } |
| |
| void |
| PluginDso::incInstanceCount() |
| { |
| ++_instanceCount; |
| PluginDbg(_dbg_ctl(), "instance count (inst-count:%d, dso-addr:%p)", _instanceCount.load(), this); |
| } |
| |
| void |
| PluginDso::decInstanceCount() |
| { |
| --_instanceCount; |
| PluginDbg(_dbg_ctl(), "instance count (inst-count:%d, dso-addr:%p)", _instanceCount.load(), this); |
| } |
| |
| int |
| PluginDso::instanceCount() const |
| { |
| return _instanceCount; |
| } |
| |
| bool |
| PluginDso::isDynamicReloadEnabled() const |
| { |
| return (_runtimePath != _effectivePath); |
| } |
| |
| void |
| PluginDso::LoadedPlugins::add(PluginDso *plugin) |
| { |
| SCOPED_MUTEX_LOCK(lock, _mutex, this_ethread()); |
| |
| _list.append(plugin); |
| } |
| |
| void |
| PluginDso::LoadedPlugins::remove(PluginDso *plugin) |
| { |
| SCOPED_MUTEX_LOCK(lock, _mutex, this_ethread()); |
| |
| _list.erase(plugin); |
| this_ethread()->schedule_imm(new DeleterContinuation<PluginDso>(plugin)); |
| } |
| |
| /* check if need to reload the plugin DSO |
| * if dynamic reload not enabled: check if plugin Dso with same effective path already loaded |
| * if dynamic reload enabled: check if plugin Dso with same effective path and same time stamp already loaded |
| * return pointer to already loaded plugin if found, else return null |
| */ |
| PluginDso * |
| PluginDso::LoadedPlugins::findByEffectivePath(const fs::path &path, bool dynamicReloadEnabled) |
| { |
| std::error_code ec; |
| auto fs = fs::status(path, ec); |
| ts_clock::time_point mtime; |
| if (!ec) { |
| mtime = fs::last_write_time(fs); |
| } |
| |
| SCOPED_MUTEX_LOCK(lock, _mutex, this_ethread()); |
| |
| auto spot = std::find_if(_list.begin(), _list.end(), [&](PluginDso const &plugin) -> bool { |
| return ((!dynamicReloadEnabled || (mtime == plugin.modTime())) && |
| (0 == path.string().compare(plugin.effectivePath().string()))); |
| }); |
| return spot == _list.end() ? nullptr : static_cast<PluginDso *>(spot); |
| } |
| |
| void |
| PluginDso::LoadedPlugins::indicatePreReload(const char *factoryId) |
| { |
| SCOPED_MUTEX_LOCK(lock, _mutex, this_ethread()); |
| |
| PluginDbg(_dbg_ctl(), "indicated config is going to be reloaded by factory '%s' to %zu plugin%s", factoryId, _list.count(), |
| _list.count() != 1 ? "s" : ""); |
| |
| _list.apply([](PluginDso &plugin) -> void { plugin.indicatePreReload(); }); |
| } |
| |
| void |
| PluginDso::LoadedPlugins::indicatePostReload(bool reloadSuccessful, const std::unordered_map<PluginDso *, int> &pluginUsed, |
| const char *factoryId) |
| { |
| SCOPED_MUTEX_LOCK(lock, _mutex, this_ethread()); |
| |
| PluginDbg(_dbg_ctl(), "indicated config is done reloading by factory '%s' to %zu plugin%s", factoryId, _list.count(), |
| _list.count() != 1 ? "s" : ""); |
| |
| for (auto &plugin : _list) { |
| TSRemapReloadStatus status = TSREMAP_CONFIG_RELOAD_FAILURE; |
| if (reloadSuccessful) { |
| /* reload succeeded but was the plugin instantiated by this factory? */ |
| status = (pluginUsed.end() == pluginUsed.find(&plugin) ? TSREMAP_CONFIG_RELOAD_SUCCESS_PLUGIN_UNUSED : |
| TSREMAP_CONFIG_RELOAD_SUCCESS_PLUGIN_USED); |
| } |
| plugin.indicatePostReload(status); |
| } |
| } |
| |
| bool |
| PluginDso::LoadedPlugins::addPluginPathToDsoOptOutTable(std::string_view pluginPath) |
| { |
| std::error_code ec; |
| auto effectivePath = fs::canonical(fs::path{pluginPath}, ec); |
| |
| if (ec) { |
| PluginError("Error getting the canonical path: %s", ec.message().c_str()); |
| return false; |
| } |
| |
| { |
| SCOPED_MUTEX_LOCK(lock, _mutex, this_ethread()); |
| _optoutDsoReloadPlugins.push_front(DisableDSOReloadPluginInfo{std::move(effectivePath)}); |
| } |
| |
| return true; |
| } |
| |
| void |
| PluginDso::LoadedPlugins::removePluginPathFromDsoOptOutTable(std::string_view pluginPath) |
| { |
| std::error_code ec; |
| auto effectivePath = fs::canonical(fs::path{pluginPath}, ec); |
| |
| if (ec) { |
| PluginError("Error getting the canonical path: %s", ec.message().c_str()); |
| return; |
| } |
| |
| { |
| SCOPED_MUTEX_LOCK(lock, _mutex, this_ethread()); |
| _optoutDsoReloadPlugins.remove_if([&effectivePath](auto const &info) { return info.dsoEffectivePath == effectivePath; }); |
| } |
| } |
| |
| bool |
| PluginDso::LoadedPlugins::isPluginInDsoOptOutTable(const fs::path &effectivePath) |
| { |
| SCOPED_MUTEX_LOCK(lock, _mutex, this_ethread()); |
| const auto found = std::find_if(std::begin(this->_optoutDsoReloadPlugins), std::end(this->_optoutDsoReloadPlugins), |
| [&effectivePath](const auto &info) { return info.dsoEffectivePath == effectivePath; }); |
| |
| // if is found, then the plugin opt out. |
| return (found != std::end(this->_optoutDsoReloadPlugins)); |
| } |
| |
| Ptr<PluginDso::LoadedPlugins> PluginDso::_plugins; |