From f37f77e85f4e62a73bb6e600e3dd8ec1629f15b7 Mon Sep 17 00:00:00 2001 From: Gancho Tenev Date: Fri, 1 Mar 2019 14:33:09 -0800 Subject: [PATCH] Plugin reload Reloading plugin allows new versions of a plugin code to be loaded and executed and old versions to be unloaded without restarting the traffic server process. More info in doc/developer-guide/plugins/reloading-plugins.en.rst --- .../api/functions/TSVConnCreate.en.rst | 32 + .../design-documents/index.en.rst | 28 + .../design-documents/reloading-plugins.en.rst | 178 +++++ doc/developer-guide/index.en.rst | 3 +- include/ts/InkAPIPrivateIOCore.h | 3 +- include/tscore/ts_file.h | 67 +- proxy/ReverseProxy.cc | 4 +- proxy/http/HttpTransact.cc | 4 +- proxy/http/HttpTransact.h | 5 +- proxy/http/remap/Makefile.am | 87 +++ proxy/http/remap/PluginDso.cc | 266 +++++++ proxy/http/remap/PluginDso.h | 104 +++ proxy/http/remap/PluginFactory.cc | 264 +++++++ proxy/http/remap/PluginFactory.h | 119 ++++ proxy/http/remap/RemapConfig.cc | 201 ++---- proxy/http/remap/RemapPluginInfo.cc | 256 ++++++- proxy/http/remap/RemapPluginInfo.h | 62 +- proxy/http/remap/RemapPlugins.cc | 18 +- proxy/http/remap/RemapPlugins.h | 7 +- proxy/http/remap/UrlMapping.cc | 50 +- proxy/http/remap/UrlMapping.h | 15 +- proxy/http/remap/UrlRewrite.cc | 4 + proxy/http/remap/UrlRewrite.h | 4 + proxy/http/remap/unit-tests/plugin_misc_cb.cc | 106 +++ .../plugin_missing_deleteinstance.cc | 57 ++ .../unit-tests/plugin_missing_doremap.cc | 45 ++ .../remap/unit-tests/plugin_missing_init.cc | 45 ++ .../unit-tests/plugin_missing_newinstance.cc | 56 ++ .../remap/unit-tests/plugin_required_cb.cc | 51 ++ .../remap/unit-tests/plugin_testing_calls.cc | 130 ++++ .../remap/unit-tests/plugin_testing_common.cc | 39 ++ .../remap/unit-tests/plugin_testing_common.h | 95 +++ proxy/http/remap/unit-tests/test_PluginDso.cc | 395 +++++++++++ .../remap/unit-tests/test_PluginFactory.cc | 657 ++++++++++++++++++ .../http/remap/unit-tests/test_RemapPlugin.cc | 433 ++++++++++++ src/traffic_server/InkAPI.cc | 45 +- src/tscore/ts_file.cc | 210 ++++++ src/tscore/unit_tests/test_ts_file.cc | 193 +++++ 38 files changed, 4058 insertions(+), 280 deletions(-) create mode 100644 doc/developer-guide/api/functions/TSVConnCreate.en.rst create mode 100644 doc/developer-guide/design-documents/index.en.rst create mode 100644 doc/developer-guide/design-documents/reloading-plugins.en.rst create mode 100644 proxy/http/remap/PluginDso.cc create mode 100644 proxy/http/remap/PluginDso.h create mode 100644 proxy/http/remap/PluginFactory.cc create mode 100644 proxy/http/remap/PluginFactory.h create mode 100644 proxy/http/remap/unit-tests/plugin_misc_cb.cc create mode 100644 proxy/http/remap/unit-tests/plugin_missing_deleteinstance.cc create mode 100644 proxy/http/remap/unit-tests/plugin_missing_doremap.cc create mode 100644 proxy/http/remap/unit-tests/plugin_missing_init.cc create mode 100644 proxy/http/remap/unit-tests/plugin_missing_newinstance.cc create mode 100644 proxy/http/remap/unit-tests/plugin_required_cb.cc create mode 100644 proxy/http/remap/unit-tests/plugin_testing_calls.cc create mode 100644 proxy/http/remap/unit-tests/plugin_testing_common.cc create mode 100644 proxy/http/remap/unit-tests/plugin_testing_common.h create mode 100644 proxy/http/remap/unit-tests/test_PluginDso.cc create mode 100644 proxy/http/remap/unit-tests/test_PluginFactory.cc create mode 100644 proxy/http/remap/unit-tests/test_RemapPlugin.cc diff --git a/doc/developer-guide/api/functions/TSVConnCreate.en.rst b/doc/developer-guide/api/functions/TSVConnCreate.en.rst new file mode 100644 index 00000000000..1e0cd50865b --- /dev/null +++ b/doc/developer-guide/api/functions/TSVConnCreate.en.rst @@ -0,0 +1,32 @@ +.. 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. + +.. include:: ../../../common.defs + +.. default-domain:: c + +TSVConnCreate +************* + +Synopsis +======== + +`#include ` + +.. function:: TSCont TSVConnCreate(TSEventFunc funcp, TSMutex mutexp) + +Description +=========== diff --git a/doc/developer-guide/design-documents/index.en.rst b/doc/developer-guide/design-documents/index.en.rst new file mode 100644 index 00000000000..5d18ac06e37 --- /dev/null +++ b/doc/developer-guide/design-documents/index.en.rst @@ -0,0 +1,28 @@ +.. 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. + +.. include:: ../../common.defs + +.. _developer-design-documents: + +Design Documents +**************** + +.. toctree:: + :maxdepth: 1 + + reloading-plugins.en diff --git a/doc/developer-guide/design-documents/reloading-plugins.en.rst b/doc/developer-guide/design-documents/reloading-plugins.en.rst new file mode 100644 index 00000000000..c3cf61de5cc --- /dev/null +++ b/doc/developer-guide/design-documents/reloading-plugins.en.rst @@ -0,0 +1,178 @@ +.. 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. + +.. include:: ../../common.defs + +.. _developer-plugins-reloading-plugins: + +Reloading Plugins +***************** + +Reloading plugin allows new versions of a plugin code to be loaded and executed and old versions to be unloaded without +restarting the traffic server process. + +Plugins are Dynamic Shared Objects (DSO), new versions of the plugins are currently loaded by using a traffic server +configuration reload, i.e.:: + + traffic_ctl config reload + +Although this feature should be transparent to there plugin developers, the following are some design considerations +and implementation details. + + +Design Considerations +===================== + +1. The mechanism of the plugin reload should be transparent to the plugin developers, plugin developers should be + concerned only with properly initializing and cleaning up after the plugin and its instances. + +2. With the current traffic server implementation new version plugin (re)load is only triggered by a configuration + (re)load hence naturally the configuration should be always coupled with the set of plugins it loaded. + +3. Due to its asynchronouse nature traffic server should allow running different newer and older versions of the same plugin at the same time. + +4. Old plugin versions should be unloaded after the traffic server process no longer needs them after reload. + +5. Running different versions of the configuration and plugin versions at the same time requires maintaining + a "current active set" to be used by new transactions, new continuations, etc. and also multiple "previous inactive" sets as well. + +6. The result of the plugin reloading should be consistent across operating systems, file systems, dynamic loader + implementations. + + +Currently only loading of "remap" plugins (`remap.config`) is supported but (re)loading of "global" plugins +(`plugin.config`) could use same ideas and reuse some of the classes below. + + +Consistent (re)loading behavior +------------------------------- + +The following are some of the problems noticed during the initial experimentation: + + a. There is an internal reference counting of the DSOs implemented inside the dynamic loader. + If an older version of the plugin DSO is still loaded then loading of a newer version of the DSO by using + the same filename does not load the new version. + + b. If the filename used by the dynamic loader reference counting contains symbolic links the results are not + consistent across different operating/file systems and dynamic loader implementations. + +The following possible solutions were considered: + + a. maintaining different plugin filenames for each version - this would put unnecessary burden on the + configuration management tools + + b. experiments with Linux specific `dlmopen `_ yielded + good results but it was not available on all supported platforms + + +A less efficient but more reliable solution was chosen - DSO files are temporarily copied to and consequently +loaded from a runtime location and the copies is kept until plugin is unloaded. + +Each configuration / plugin reload would use a different runtime location, ``ATSUuid`` is used to create unique +runtime directories. + + +Reference counting against DSOs +------------------------------- + +During the initial analysis a common sense solution was considered - to add instrumentation around handling +of registered hooks in order to unload plugins safely. This would be more involved and not sufficient since hooks +are not the only mechanism that relies on the plugin DSO being loaded. This design / implementation proposes +a different approach. + +Plugin code can be called from HTTP state machine (1) while handling HTTP transactions or (2) while calling +event handling functions of continuations created by the plugin code. +The plugin reload mechanism should guarantee that all necessary plugin DSOs are still loaded when those calls +are performed. + +Those continuations are created by :c:func:`TSContCreate` and :c:func:`TSVConnCreate` and +could be used for registering hooks (i.e. registered by :c:func:`TSHttpHookAdd`) or for +scheduling events in the future (i.e. :c:func:`TSContScheduleOnPool`). + + +Registering hooks always requires creating continuations from inside the plugin code and a separate +instrumentation around handling of hooks is not necessary. + +There is an existing reference counting around ``UrlRewrite`` which makes sure it stays loaded until the HTTP state +machine (the last HTTP transaction) stops using it. By making all plugins loaded by a single configuration reload +a part of ``UrlRewrite`` (see `PluginFactory`_ below), we could guarantee the plugins are always loaded while +in use by the HTTP transactions. + + +Plugin context +-------------- + +Reference counting and managing different configuration and plugin sets require the continuation creation and +destruction to know in which plugin context they are running. + +Traffic server API change was considered for ``TSCreateCont``, ``TSVConnCreate`` and ``TSDestroyCont`` but +it was decided to keep things hidden from the plugin developer by using thread local plugin context which +would be set/switched accordingly before executing the plugin code. + +The continuations created by the plugin will have a context member added to them which will be used by +the reference counting and when continuations are destroyed or handle events. + + +TSHttpArgs +---------- + +Traffic Server sessions and transactions provide a fixed array of void pointers that can be used by plugins +to store information. To avoid collisions between plugins a plugin should first *reserve* an index in the array. + +Since :c:func:`TSHttpTxnArgIndexReserve` and :c:func:`TSHttpSsnArgIndexReserve` are meant to be called during plugin +initialization we could end up "leaking" indices during plugins reload. +Hence it is necessary to make sure only one index is allocated per "plugin identifying name", current +:c:func:`TSHttpTxnArgIndexNameLookup` and :c:func:`TSHttpTxnArgIndexNameLookup` implementation assumes 1-1 +index-to-name relationship as well. + + +PluginFactory +------------- + +`PluginFactory` - creates and holds all plugin instances corresponding to a single configuration (re)load. + +#. Instantiates and initializes 'remap' plugins, eventually signals plugin unload/destruction, makes sure each plugin + version is loaded only once per configuration (re)load by maintaining a list of DSOs already loaded. + +#. Initializes, keeps track of all resulting plugin instances and eventually signals each instance destruction. + +#. Handles multiple plugin search paths. + +#. Sets a common runtime path for all plugins loaded in a single configuration (re)load to guarantee + `consistent (re)loading behavior`_. + + + +RemapPluginInfo +--------------- + +`RemapPluginInfo` - a class representing a 'remap' plugin, derived from `PluginDso`, and handling 'remap' plugin specific +initialization and destruction and also sets up the right plugin context when its methods are called. + + + +PluginDso +--------- + +`PluginDso` - a class performing the actual DSO loading and unloading and all related initialization and +cleanup plus related error handling. Its functionality is modularized into a separate class in hopes to +be reused by 'global' plugins in the future. + + +To make sure plugins are still loaded while their code is still in use there is reference counting done around ``PluginDso`` +which inherits ``RefCountObj`` and implements ``aqcuire()`` and ``release()`` methods which are called by ``TSCreateCont``, +``TSVConnCreate`` and ``TSDestroyCont``. diff --git a/doc/developer-guide/index.en.rst b/doc/developer-guide/index.en.rst index 5deda01f4b6..d21e840dcf0 100644 --- a/doc/developer-guide/index.en.rst +++ b/doc/developer-guide/index.en.rst @@ -55,4 +55,5 @@ duplicate bugs is encouraged, but not required. host-resolution-proposal.en client-session-architecture.en core-architecture/index.en - layout/index.en \ No newline at end of file + design-documents/index.en + layout/index.en diff --git a/include/ts/InkAPIPrivateIOCore.h b/include/ts/InkAPIPrivateIOCore.h index c19dbc44949..ad75f367fcb 100644 --- a/include/ts/InkAPIPrivateIOCore.h +++ b/include/ts/InkAPIPrivateIOCore.h @@ -43,7 +43,7 @@ class INKContInternal : public DummyVConnection INKContInternal(); INKContInternal(TSEventFunc funcp, TSMutex mutexp); - void init(TSEventFunc funcp, TSMutex mutexp); + void init(TSEventFunc funcp, TSMutex mutexp, void *context = 0); virtual void destroy(); void handle_event_count(int event); @@ -60,6 +60,7 @@ class INKContInternal : public DummyVConnection int m_closed; int m_deletable; int m_deleted; + void *m_context; // INKqa07670: Nokia memory leak bug fix INKContInternalMagic_t m_free_magic; }; diff --git a/include/tscore/ts_file.h b/include/tscore/ts_file.h index b5ec194a1f4..e9bfd8b8010 100644 --- a/include/tscore/ts_file.h +++ b/include/tscore/ts_file.h @@ -104,6 +104,12 @@ namespace file /// Get a copy of the path. std::string string() const; + /// Get relative path + self_type relative_path(); + + /// Get parent path + path parent_path(); + protected: std::string _path; ///< File path. }; @@ -120,6 +126,7 @@ namespace file friend self_type status(const path &, std::error_code &) noexcept; friend int file_type(const self_type &); + friend time_t modification_time(const file_status &fs); friend uintmax_t file_size(const self_type &); friend bool is_regular_file(const file_status &); friend bool is_dir(const file_status &); @@ -141,6 +148,9 @@ namespace file /// Return the file type value. int file_type(const file_status &fs); + /// Return modification time + time_t modification_time(const file_status &fs); + /// Check if the path is to a regular file. bool is_regular_file(const file_status &fs); @@ -159,7 +169,28 @@ namespace file /// Check if file is readable. bool is_readable(const path &s); - /** Load the file at @a p into a @c std::string. + // Get directory location suitable for temporary files + path temp_directory_path(); + + // Returns current path. + path current_path(); + + // Returns return the canonicalized absolute pathname + path canonical(const path &p, std::error_code &ec); + + // Checks if the file/directory exists + bool exists(const path &p); + + // Create directories + bool create_directories(const path &p, std::error_code &ec, mode_t mode = 0775) noexcept; + + // Copy files ("from" cannot be directory). @todo make it more generic + bool copy(const path &from, const path &to, std::error_code &ec); + + // Removes files and directories recursively + bool remove(const path &path, std::error_code &ec); + + /** Load the file at @a p into a @c std::string * * @param p Path to file * @return The contents of the file. @@ -222,6 +253,28 @@ namespace file return *this /= std::string_view(that._path); } + inline path + path::relative_path() + { + return (this->is_absolute()) ? path(_path.substr(sizeof(preferred_separator))) : *this; + } + + inline path + path::parent_path() + { + if (this->is_absolute() && _path.substr(sizeof(preferred_separator)).empty()) { + // No relative path + return *this; + } + + const size_t last_slash_idx = _path.find_last_of(preferred_separator); + if (std::string::npos != last_slash_idx) { + return path(_path.substr(0, last_slash_idx)); + } else { + return path(); + } + } + /** Combine two strings as file paths. @return A @c path with the combined path. @@ -250,6 +303,18 @@ namespace file return path(std::move(lhs)) /= rhs; } + inline bool + operator==(const path &lhs, const path &rhs) + { + return lhs.string() == rhs.string(); + } + + inline bool + operator!=(const path &lhs, const path &rhs) + { + return lhs.string() != rhs.string(); + } + /* ------------------------------------------------------------------- */ } // namespace file } // namespace ts diff --git a/proxy/ReverseProxy.cc b/proxy/ReverseProxy.cc index 2ab3d036065..a4751a3d1d1 100644 --- a/proxy/ReverseProxy.cc +++ b/proxy/ReverseProxy.cc @@ -43,7 +43,8 @@ // Global Ptrs static Ptr reconfig_mutex; -UrlRewrite *rewrite_table = nullptr; +UrlRewrite *rewrite_table = nullptr; +thread_local PluginThreadContext *pluginThreadContext = nullptr; // Tokens for the Callback function #define FILE_CHANGED 0 @@ -150,6 +151,7 @@ reloadUrlRewrite() ink_assert(oldTable != nullptr); // Release the old one + oldTable->pluginFactory.indicateReload(); oldTable->release(); Debug("url_rewrite", "%s", msg); diff --git a/proxy/http/HttpTransact.cc b/proxy/http/HttpTransact.cc index 2116c632466..5da87713faf 100644 --- a/proxy/http/HttpTransact.cc +++ b/proxy/http/HttpTransact.cc @@ -3484,8 +3484,8 @@ HttpTransact::handle_response_from_server(State *s) // plugin call s->server_info.state = s->current.state; - if (s->fp_tsremap_os_response) { - s->fp_tsremap_os_response(s->remap_plugin_instance, reinterpret_cast(s->state_machine), s->current.state); + if (s->os_response_plugin_inst) { + s->os_response_plugin_inst->osResponse(reinterpret_cast(s->state_machine), s->current.state); } switch (s->current.state) { diff --git a/proxy/http/HttpTransact.h b/proxy/http/HttpTransact.h index 4889da92a2c..a1f5b47df2a 100644 --- a/proxy/http/HttpTransact.h +++ b/proxy/http/HttpTransact.h @@ -766,10 +766,9 @@ class HttpTransact CacheAuth_t www_auth_content = CACHE_AUTH_NONE; // INK API/Remap API plugin interface - void *remap_plugin_instance = nullptr; void *user_args[TS_HTTP_MAX_USER_ARG]; - RemapPluginInfo::OS_Response_F *fp_tsremap_os_response = nullptr; - HTTPStatus http_return_code = HTTP_STATUS_NONE; + RemapPluginInst *os_response_plugin_inst = nullptr; + HTTPStatus http_return_code = HTTP_STATUS_NONE; int api_txn_active_timeout_value = -1; int api_txn_connect_timeout_value = -1; diff --git a/proxy/http/remap/Makefile.am b/proxy/http/remap/Makefile.am index ddee9dcc87b..3a101955ab1 100644 --- a/proxy/http/remap/Makefile.am +++ b/proxy/http/remap/Makefile.am @@ -22,6 +22,7 @@ AM_CPPFLAGS += \ $(iocore_include_dirs) \ -I$(abs_top_srcdir)/include \ -I$(abs_top_srcdir)/lib \ + -I$(abs_top_srcdir)/lib/records \ -I$(abs_top_srcdir)/proxy \ -I$(abs_top_srcdir)/mgmt \ -I$(abs_top_srcdir)/mgmt/utils \ @@ -39,6 +40,9 @@ libhttp_remap_a_SOURCES = \ RemapConfig.h \ RemapPluginInfo.cc \ RemapPluginInfo.h \ + PluginDso.cc \ + PluginFactory.cc \ + PluginFactory.h \ RemapPlugins.cc \ RemapPlugins.h \ RemapProcessor.cc \ @@ -52,3 +56,86 @@ libhttp_remap_a_SOURCES = \ clang-tidy-local: $(libhttp_remap_a_SOURCES) $(CXX_Clang_Tidy) + +TESTS = $(check_PROGRAMS) +check_PROGRAMS = test_PluginDso test_PluginFactory test_RemapPluginInfo + +test_PluginDso_CPPFLAGS = $(AM_CPPFLAGS) -I$(abs_top_srcdir)/tests/include -DPLUGIN_DSO_TESTS +EXTRA_test_PluginDso_DEPENDENCIES = unit-tests/plugin_v1.la +test_PluginDso_LDADD = $(OPENSSL_LIBS) +test_PluginDso_LDFLAGS = $(AM_LDFLAGS) -L$(top_builddir)/src/tscore/.libs -ltscore +test_PluginDso_SOURCES = \ + unit-tests/test_PluginDso.cc \ + unit-tests/plugin_testing_common.cc \ + PluginDso.cc + +test_PluginFactory_CPPFLAGS = $(AM_CPPFLAGS) -I$(abs_top_srcdir)/tests/include -DPLUGIN_DSO_TESTS +EXTRA_test_PluginFactory_DEPENDENCIES = unit-tests/plugin_v1.la +test_PluginFactory_LDADD = $(OPENSSL_LIBS) +test_PluginFactory_LDFLAGS = $(AM_LDFLAGS) -L$(top_builddir)/src/tscore/.libs -ltscore +test_PluginFactory_SOURCES = \ + unit-tests/test_PluginFactory.cc \ + unit-tests/plugin_testing_common.cc \ + PluginFactory.cc \ + PluginDso.cc \ + RemapPluginInfo.cc + +test_RemapPluginInfo_CPPFLAGS = $(AM_CPPFLAGS) -I$(abs_top_srcdir)/tests/include -DPLUGIN_DSO_TESTS + EXTRA_test_RemapPluginInfo_DEPENDENCIES = \ + unit-tests/plugin_missing_init.la \ + unit-tests/plugin_missing_doremap.la \ + unit-tests/plugin_missing_deleteinstance.la \ + unit-tests/plugin_required_cb.la \ + unit-tests/plugin_missing_newinstance.la \ + unit-tests/plugin_testing_calls.la +test_RemapPluginInfo_LDADD = \ + $(OPENSSL_LIBS) +test_RemapPluginInfo_LDFLAGS = $(AM_LDFLAGS) -L$(top_builddir)/src/tscore/.libs -ltscore +test_RemapPluginInfo_SOURCES = \ + unit-tests/plugin_testing_common.cc \ + unit-tests/plugin_testing_calls.cc \ + unit-tests/test_RemapPlugin.cc \ + PluginDso.cc \ + RemapPluginInfo.cc + + +DSO_LDFLAGS = \ + -module \ + -shared \ + -avoid-version \ + -export-symbols-regex '^(TSRemapInit|TSRemapDone|TSRemapDoRemap|TSRemapNewInstance|TSRemapDeleteInstance|TSRemapOSResponse|TSRemapConfigReload|TSPluginInit|pluginDsoVersionTest|getPluginDebugObjectTest)$$' + +# Build plugins for unit testing the plugin (re)load. +pkglib_LTLIBRARIES = \ + unit-tests/plugin_v1.la \ + unit-tests/plugin_v2.la \ + unit-tests/plugin_required_cb.la \ + unit-tests/plugin_missing_deleteinstance.la \ + unit-tests/plugin_missing_doremap.la \ + unit-tests/plugin_missing_init.la \ + unit-tests/plugin_missing_newinstance.la \ + unit-tests/plugin_testing_calls.la +unit_tests_plugin_v1_la_SOURCES = unit-tests/plugin_misc_cb.cc +unit_tests_plugin_v1_la_LDFLAGS = $(DSO_LDFLAGS) +unit_tests_plugin_v1_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS) +unit_tests_plugin_v2_la_SOURCES = unit-tests/plugin_misc_cb.cc +unit_tests_plugin_v2_la_LDFLAGS = $(DSO_LDFLAGS) +unit_tests_plugin_v2_la_CPPFLAGS = -DPLUGINDSOVER=2 $(AM_CPPFLAGS) +unit_tests_plugin_required_cb_la_SOURCES = unit-tests/plugin_required_cb.cc +unit_tests_plugin_required_cb_la_LDFLAGS = $(DSO_LDFLAGS) +unit_tests_plugin_required_cb_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS) +unit_tests_plugin_missing_deleteinstance_la_SOURCES = unit-tests/plugin_missing_deleteinstance.cc +unit_tests_plugin_missing_deleteinstance_la_LDFLAGS = $(DSO_LDFLAGS) +unit_tests_plugin_missing_deleteinstance_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS) +unit_tests_plugin_missing_doremap_la_SOURCES = unit-tests/plugin_missing_doremap.cc +unit_tests_plugin_missing_doremap_la_LDFLAGS = $(DSO_LDFLAGS) +unit_tests_plugin_missing_doremap_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS) +unit_tests_plugin_missing_init_la_SOURCES = unit-tests/plugin_missing_init.cc +unit_tests_plugin_missing_init_la_LDFLAGS = $(DSO_LDFLAGS) +unit_tests_plugin_missing_init_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS) +unit_tests_plugin_missing_newinstance_la_SOURCES = unit-tests/plugin_missing_newinstance.cc +unit_tests_plugin_missing_newinstance_la_LDFLAGS = $(DSO_LDFLAGS) +unit_tests_plugin_missing_newinstance_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS) +unit_tests_plugin_testing_calls_la_SOURCES = unit-tests/plugin_testing_calls.cc unit-tests/plugin_testing_common.cc +unit_tests_plugin_testing_calls_la_LDFLAGS = $(DSO_LDFLAGS) +unit_tests_plugin_testing_calls_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS) diff --git a/proxy/http/remap/PluginDso.cc b/proxy/http/remap/PluginDso.cc new file mode 100644 index 00000000000..24490fc044d --- /dev/null +++ b/proxy/http/remap/PluginDso.cc @@ -0,0 +1,266 @@ +/** @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 "PluginDso.h" +#ifdef PLUGIN_DSO_TESTS +#include "unit-tests/plugin_testing_common.h" +#else +#include "tscore/Diags.h" +#endif + +PluginDso::PluginDso(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath) + : _configPath(configPath), _effectivePath(effectivePath), _runtimePath(runtimePath) +{ +} + +PluginDso::~PluginDso() +{ + std::string error; + (void)unload(error); +} + +bool +PluginDso::load(std::string &error) +{ + /* Clear all errors */ + error.clear(); + _errorCode.clear(); + bool result = true; + + if (isLoaded()) { + error.append("plugin already loaded"); + return false; + } + + Debug(_tag, "plugin '%s' started loading DSO", _configPath.c_str()); + + /* Find plugin DSO looking through the search dirs */ + if (_effectivePath.empty()) { + error.append("empty effective path"); + result = false; + } else { + Debug(_tag, "plugin '%s' effective path: %s", _configPath.c_str(), _effectivePath.c_str()); + + /* Copy the installed plugin DSO to a runtime directory */ + std::error_code ec; + if (!copy(_effectivePath, _runtimePath, ec)) { + std::string temp_error; + temp_error.append("failed to create a copy: ").append(strerror(ec.value())); + error.assign(temp_error); + result = false; + } else { + Debug(_tag, "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; + fs::file_status fs = fs::status(_effectivePath, ec); + _mtime = fs::modification_time(fs); + Debug(_tag, "plugin '%s' mоdification time %ld", _configPath.c_str(), _mtime); + + /* Now attemt to load the plugin DSO */ + if ((_dlh = dlopen(_runtimePath.c_str(), RTLD_NOW)) == nullptr) { +#if defined(freebsd) || defined(openbsd) + char *err = (char *)dlerror(); +#else + char *err = dlerror(); +#endif + error.append(err ? err : "Unknown dlopen() error"); + _dlh = nullptr; /* mark that the constructor failed. */ + + clean(error); + result = false; + + Error("plugin '%s' failed to load: %s", _configPath.c_str(), error.c_str()); + } + } + + /* Remove the runtime DSO copy even if we succeed loading to avoid leftovers after crashes */ + if (_preventiveCleaning) { + clean(error); + } + } + Debug(_tag, "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) +{ + /* clean errors */ + error.clear(); + bool result = false; + + if (isLoaded()) { + result = (0 == dlclose(_dlh)); + _dlh = nullptr; + if (true == result) { + clean(error); + } else { + error.append("failed to unload plugin"); + } + } else { + error.append("no plugin loaded"); + result = false; + } + + return result; +} + +/** + * @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(); + error.clear(); + + 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. + */ + +time_t +PluginDso::modTime() const +{ + return _mtime; +} + +/** + * @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 (false == remove(_runtimePath, _errorCode)) { + error.append("failed to remove runtime copy: ").append(_errorCode.message()); + } +} + +void +PluginDso::acquire() +{ + this->refcount_inc(); + Debug(_tag, "plugin DSO acquire (ref-count:%d, dso-addr:%p)", this->refcount(), this); +} + +void +PluginDso::release() +{ + Debug(_tag, "plugin DSO release (ref-count:%d, dso-addr:%p)", this->refcount() - 1, this); + if (0 == this->refcount_dec()) { + Debug(_tag, "unloading plugin DSO '%s' (dso-addr:%p)", _configPath.c_str(), this); + _list.erase(this); + delete this; + } +} + +void +PluginDso::incInstanceCount() +{ + _instanceCount.refcount_inc(); + Debug(_tag, "instance count (inst-count:%d, dso-addr:%p)", _instanceCount.refcount(), this); +} + +void +PluginDso::decInstanceCount() +{ + _instanceCount.refcount_dec(); + Debug(_tag, "instance count (inst-count:%d, dso-addr:%p)", _instanceCount.refcount(), this); +} + +int +PluginDso::instanceCount() +{ + return _instanceCount.refcount(); +} + +PluginDso::PluginList PluginDso::_list; diff --git a/proxy/http/remap/PluginDso.h b/proxy/http/remap/PluginDso.h new file mode 100644 index 00000000000..4554c6b8756 --- /dev/null +++ b/proxy/http/remap/PluginDso.h @@ -0,0 +1,104 @@ +/** @file + + Header file for 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. + + */ + +#pragma once + +#include +#include +#include + +#include "tscore/ts_file.h" +namespace fs = ts::file; + +#include "tscore/Ptr.h" +#include "tscpp/util/IntrusiveDList.h" + +class PluginThreadContext : public RefCountObj +{ +public: + virtual void acquire() = 0; + virtual void release() = 0; + static constexpr const char *const _tag = "plugin_context"; /** @brief log tag used by this class */ +}; + +class PluginDso : public PluginThreadContext +{ + friend class PluginFactory; + +public: + PluginDso(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath); + virtual ~PluginDso(); + + /* DSO Load, unload, get symbols from DSO */ + virtual bool load(std::string &error); + virtual bool unload(std::string &error); + bool isLoaded(); + bool getSymbol(const char *symbol, void *&address, std::string &error) const; + + /* Accessors for effective and runtime paths */ + const fs::path &effectivePath() const; + const fs::path &runtimePath() const; + time_t modTime() const; + + /* List used by the plugin factory */ + using self_type = PluginDso; ///< Self reference type. + self_type *_next = nullptr; + self_type *_prev = nullptr; + using Linkage = ts::IntrusiveLinkage; + using PluginList = ts::IntrusiveDList; + + /* Methods to be called when processing a list of plugins, to overloaded by the remap or the global plugins correspondingly */ + virtual void indicateReload() = 0; + virtual bool init(std::string &error) = 0; + virtual void done() = 0; + + void acquire(); + void release(); + + void incInstanceCount(); + void decInstanceCount(); + int instanceCount(); + +protected: + void clean(std::string &error); + + fs::path _configPath; /** @brief the name specified in the config file */ + fs::path _effectivePath; /** @brief the plugin installation path which was used to load DSO */ + fs::path _runtimePath; /** @brief the plugin runtime path where the plugin was copied to be loaded */ + + void *_dlh = nullptr; /** @brief dlopen handler used internally in this class, used as flag for loaded vs unloaded (nullptr) */ + std::error_code _errorCode; /** @brief used in filesystem calls */ + + static constexpr const char *const _tag = "plugin_dso"; /** @brief log tag used by this class */ + time_t _mtime = 0; /* @brief modification time of the DSO's file, used for checking */ + bool _preventiveCleaning = true; + + static PluginList _list; /** @brief a global list of plugins, usually maintained by a plugin factory or plugin instance itself */ + RefCountObj _instanceCount; /** @brief used for properly calling "done" and "indicate config reload" methods by the factory */ +}; diff --git a/proxy/http/remap/PluginFactory.cc b/proxy/http/remap/PluginFactory.cc new file mode 100644 index 00000000000..c6b1c8a7f31 --- /dev/null +++ b/proxy/http/remap/PluginFactory.cc @@ -0,0 +1,264 @@ +/** @file + + Functionality allowing to load all plugins from a single config reload. + + @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. + + */ + +#include "RemapPluginInfo.h" +#include "PluginFactory.h" +#ifdef PLUGIN_DSO_TESTS +#include "unit-tests/plugin_testing_common.h" +#else +#include "tscore/Diags.h" +#endif + +#include /* std::swap */ + +RemapPluginInst::RemapPluginInst(RemapPluginInfo &plugin) : _plugin(plugin) +{ + _plugin.acquire(); + _plugin.incInstanceCount(); +} + +RemapPluginInst::~RemapPluginInst() +{ + _plugin.decInstanceCount(); + _plugin.release(); +} + +bool +RemapPluginInst::init(int argc, char **argv, std::string &error) +{ + bool result = false; + result = _plugin.initInstance(argc, argv, &_instance, error); + + return result; +} + +void +RemapPluginInst::done() +{ + _plugin.doneInstance(_instance); +} + +TSRemapStatus +RemapPluginInst::doRemap(TSHttpTxn rh, TSRemapRequestInfo *rri) +{ + return _plugin.doRemap(_instance, rh, rri); +} + +void +RemapPluginInst::osResponse(TSHttpTxn rh, int os_response_type) +{ + _plugin.osResponse(_instance, rh, os_response_type); +} + +PluginFactory::PluginFactory() +{ + _uuid = new ATSUuid(); + if (nullptr != _uuid) { + _uuid->initialize(TS_UUID_V4); + if (!_uuid->valid()) { + /* Destroy and mark failure */ + delete _uuid; + _uuid = nullptr; + } + } + + Debug(_tag, "created plugin factory %s", getUuid()); +} + +PluginFactory::~PluginFactory() +{ + _instList.apply([](RemapPluginInst *pluginInst) -> void { delete pluginInst; }); + _instList.clear(); + + fs::remove(_runtimeDir, _ec); + + Debug(_tag, "destroyed plugin factory %s", getUuid()); + delete _uuid; +} + +PluginFactory & +PluginFactory::addSearchDir(const fs::path &searchDir) +{ + _searchDirs.push_back(searchDir); + Debug(_tag, "added plugin search dir %s", searchDir.c_str()); + return *this; +} + +PluginFactory & +PluginFactory::setRuntimeDir(const fs::path &runtimeDir) +{ + _runtimeDir = runtimeDir / fs::path(getUuid()); + Debug(_tag, "set plugin runtime dir %s", runtimeDir.c_str()); + return *this; +} + +const char * +PluginFactory::getUuid() +{ + return _uuid ? _uuid->getString() : "uknown"; +} + +/** + * @brief Loads, initializes and return a valid Remap Plugin instance. + * + * @param configPath plugin path as specified in the plugin + * @param argc number of parameters passed to the plugin during instance initialization + * @param argv parameters passed to the plugin during instance initialization + * @param context Plugin context is used from continuations to guarantee correct reference counting against the plugin. + * @param error human readable message if something goes wrong, empty otherwise + * @return pointer to a plugin instance, nullptr if failure + */ +RemapPluginInst * +PluginFactory::getRemapPlugin(const fs::path &configPath, int argc, char **argv, std::string &error) +{ + /* Discover the effective path by looking into the search dirs */ + fs::path effectivePath = getEffectivePath(configPath); + if (effectivePath.empty()) { + error.assign("failed to find plugin '").append(configPath.string()).append("'"); + return nullptr; + } + + /* Only one plugin with this effective path can be loaded by a plugin factory */ + RemapPluginInfo *plugin = dynamic_cast(findByEffectivePath(effectivePath)); + RemapPluginInst *inst = nullptr; + + if (nullptr == plugin) { + /* The plugin requested have not been loaded yet. */ + Debug(_tag, "plugin '%s' has not been loaded yet, loading as remap plugin", configPath.c_str()); + + fs::path runtimePath; + runtimePath /= _runtimeDir; + runtimePath /= effectivePath.relative_path(); + + fs::path parent = runtimePath.parent_path(); + if (!fs::create_directories(parent, _ec)) { + error.assign("failed to create plugin runtime dir"); + return nullptr; + } + + plugin = new RemapPluginInfo(configPath, effectivePath, runtimePath); + if (nullptr != plugin) { + if (plugin->load(error)) { + _list.append(plugin); + + if (plugin->init(error)) { + inst = new RemapPluginInst(*plugin); + inst->init(argc, argv, error); + _instList.append(inst); + } + + if (_preventiveCleaning) { + clean(error); + } + } else { + return nullptr; + } + } + } else { + Debug(_tag, "plugin '%s' has already been loaded", configPath.c_str()); + inst = new RemapPluginInst(*plugin); + inst->init(argc, argv, error); + _instList.append(inst); + } + + return inst; +} + +/** + * @brief full path to the first plugin found in the search path which will be used to be copied to runtime location and loaded. + * + * @param configPath path specified in the config file, it can be relative path. + * @return full path to the plugin. + */ +fs::path +PluginFactory::getEffectivePath(const fs::path &configPath) +{ + if (configPath.is_absolute()) { + if (fs::exists(configPath)) { + return fs::canonical(configPath.string(), _ec); + } else { + return fs::path(); + } + } + + fs::path path; + + for (auto dir : _searchDirs) { + fs::path candidatePath = dir / configPath; + if (fs::exists(candidatePath)) { + path = fs::canonical(candidatePath, _ec); + break; + } + } + + return path; +} + +/** + * @brief Find a plugin by path from our linked plugin list by using plugin effective (canonical) path + * + * @param path effective (caninical) path + * @return plugin found or nullptr if not found + */ +PluginDso * +PluginFactory::findByEffectivePath(const fs::path &path) +{ + struct stat sb; + time_t mtime = 0; + if (0 == stat(path.c_str(), &sb)) { + mtime = sb.st_mtime; + } + auto spot = std::find_if(_list.begin(), _list.end(), [&](PluginDso const &plugin) -> bool { + return (0 == path.string().compare(plugin.effectivePath().string()) && (mtime == plugin.modTime())); + }); + return spot == _list.end() ? nullptr : static_cast(spot); +} + +/** + * @brief Tell all plugins (that so wish) that remap.config is being reloaded + * + * This method would be useful only in case configs are reloaded independently from + * factory/plugins instantiation and initialization. + */ +void +PluginFactory::indicateReload() +{ + Debug(_tag, "indicated config reload to factory '%s'", getUuid()); + + _instList.apply([](RemapPluginInst &pluginInst) -> void { pluginInst.done(); }); + + _list.apply([](PluginDso &plugin) -> void { + if (1 == plugin.instanceCount()) { + plugin.done(); + } else { + plugin.indicateReload(); + } + }); +} + +void +PluginFactory::clean(std::string &error) +{ + fs::remove(_runtimeDir, _ec); +} diff --git a/proxy/http/remap/PluginFactory.h b/proxy/http/remap/PluginFactory.h new file mode 100644 index 00000000000..1cb0661903e --- /dev/null +++ b/proxy/http/remap/PluginFactory.h @@ -0,0 +1,119 @@ +/** @file + + Functionality allowing to load all plugins from a single config reload (header). + + @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. + */ + +#pragma once + +#include + +#include "tscore/Ptr.h" +#include "PluginDso.h" +#include "RemapPluginInfo.h" + +#include "tscore/Ptr.h" +#include "tscpp/util/IntrusiveDList.h" + +#include "tscore/ink_uuid.h" +#include "ts/apidefs.h" + +/** + * @brief Bundles plugin info + plugin instance data to be used always together. + */ +class RemapPluginInst +{ +public: + RemapPluginInst() = delete; + RemapPluginInst(RemapPluginInst &) = delete; + RemapPluginInst(RemapPluginInfo &plugin); + ~RemapPluginInst(); + + /* Used by the PluginFactory */ + bool init(int argc, char **argv, std::string &error); + void done(); + + /* Used by the traffic server core while processing requests */ + TSRemapStatus doRemap(TSHttpTxn rh, TSRemapRequestInfo *rri); + void osResponse(TSHttpTxn rh, int os_response_type); + + /* List used by the plugin factory */ + using self_type = RemapPluginInst; ///< Self reference type. + self_type *_next = nullptr; + self_type *_prev = nullptr; + using Linkage = ts::IntrusiveLinkage; + + /* Plugin instance = the plugin info + the data returned by the init callback */ + RemapPluginInfo &_plugin; + void *_instance = nullptr; +}; + +/** + * @brief loads plugins, instantiates and keep track of plugin instances created by this factory. + * + * - Handles looking through search directories to determine final plugin canonical file name to be used (called here effective + * path). + * - Makes sure we load each DSO only once per effective path. + * - Keeps track of all loaded remap plugins and their instances. + * - Maitains the notion of plugin runtime paths and makes sure every factory instance uses different runtime paths for its plugins. + * - Makes sure plugin DSOs are loaded for the lifetime of the PluginFactory. + * + * Each plugin factory instance corresponds to a config reload, each new config file set is meant to use a new factory instance. + * A notion of runtime directory is maintained to make sure the DSO library files are not erased or modified while the library are + * loaded in memory and make sure if the library file is overriden with a new DSO file that the new overriding plugin's + * functionality will be loaded with the next factory, it also handles some problems noticed on different OSes in handling + * filesystem links and different dl library implementations. + * + * @note This is meant to unify the way global and remap plugins are (re)loaded (global plugin support is not implemented yet). + */ +class PluginFactory +{ + using PluginInstList = ts::IntrusiveDList; + PluginDso::PluginList &_list = PluginDso::_list; + +public: + PluginFactory(); + virtual ~PluginFactory(); + + PluginFactory &setRuntimeDir(const fs::path &runtimeDir); + PluginFactory &addSearchDir(const fs::path &searchDir); + + RemapPluginInst *getRemapPlugin(const fs::path &configPath, int argc, char **argv, std::string &error); + + virtual const char *getUuid(); + void clean(std::string &error); + + void indicateReload(); + +protected: + PluginDso *findByEffectivePath(const fs::path &path); + fs::path getEffectivePath(const fs::path &configPath); + + std::vector _searchDirs; /** @brief ordered list of search paths where we look for plugins */ + fs::path _runtimeDir; /** @brief the path where we would create a temporary copies of the plugins to load */ + + PluginInstList _instList; + + ATSUuid *_uuid = nullptr; + std::error_code _ec; + bool _preventiveCleaning = true; + + static constexpr const char *const _tag = "plugin_factory"; /** @brief log tag used by this class */ +}; diff --git a/proxy/http/remap/RemapConfig.cc b/proxy/http/remap/RemapConfig.cc index 8d5948f7796..bf33265cc4a 100644 --- a/proxy/http/remap/RemapConfig.cc +++ b/proxy/http/remap/RemapConfig.cc @@ -32,6 +32,7 @@ #include "tscore/ink_file.h" #include "tscore/Tokenizer.h" #include "IPAllow.h" +#include "PluginFactory.h" #define modulePrefix "[ReverseProxy]" @@ -708,23 +709,29 @@ remap_check_option(const char **argv, int argc, unsigned long findmode, int *_re return ret_flags; } -int +/** + * @brief loads a remap plugin + * + * @pparam mp url mapping + * @pparam errbuf error buffer + * @pparam errbufsize size of the error buffer + * @pparam jump_to_argc + * @pparam plugin_found_at + * @return success - true, failure - false + */ +bool remap_load_plugin(const char **argv, int argc, url_mapping *mp, char *errbuf, int errbufsize, int jump_to_argc, - int *plugin_found_at) + int *plugin_found_at, UrlRewrite *rewrite) { - TSRemapInterface ri; - struct stat stat_buf; - RemapPluginInfo *pi; - char *c, *err, tmpbuf[2048], default_path[PATH_NAME_MAX]; + char *c, *err; const char *new_argv[1024]; - char *parv[1024]; - int idx = 0; - + char *pargv[1024]; + int idx = 0; + int parc = 0; *plugin_found_at = 0; - memset(parv, 0, sizeof(parv)); + memset(pargv, 0, sizeof(pargv)); memset(new_argv, 0, sizeof(new_argv)); - tmpbuf[0] = 0; ink_assert((unsigned)argc < countof(new_argv)); @@ -737,135 +744,40 @@ remap_load_plugin(const char **argv, int argc, url_mapping *mp, char *errbuf, in } argv = &new_argv[0]; if (!remap_check_option(argv, argc, REMAP_OPTFLG_PLUGIN, &idx)) { - return -1; + return false; } } else { if (unlikely(!mp || (remap_check_option(argv, argc, REMAP_OPTFLG_PLUGIN, &idx) & REMAP_OPTFLG_PLUGIN) == 0)) { snprintf(errbuf, errbufsize, "Can't find remap plugin keyword or \"url_mapping\" is nullptr"); - return -1; /* incorrect input data - almost impossible case */ + return false; /* incorrect input data - almost impossible case */ } } if (unlikely((c = (char *)strchr(argv[idx], (int)'=')) == nullptr || !(*(++c)))) { snprintf(errbuf, errbufsize, "Can't find remap plugin file name in \"@%s\"", argv[idx]); - return -2; /* incorrect input data */ - } - - if (stat(c, &stat_buf) != 0) { - ats_scoped_str plugin_default_path(RecConfigReadPluginDir()); - - // Try with the plugin path instead - if (strlen(c) + strlen(plugin_default_path) > (PATH_NAME_MAX - 1)) { - Debug("remap_plugin", "way too large a path specified for remap plugin"); - return -3; - } - - snprintf(default_path, PATH_NAME_MAX, "%s/%s", static_cast(plugin_default_path), c); - Debug("remap_plugin", "attempting to stat default plugin path: %s", default_path); - - if (stat(default_path, &stat_buf) == 0) { - Debug("remap_plugin", "stat successful on %s using that", default_path); - c = &default_path[0]; - } else { - snprintf(errbuf, errbufsize, "Can't find remap plugin file \"%s\"", c); - return -3; - } + return false; /* incorrect input data */ } Debug("remap_plugin", "using path %s for plugin", c); - if ((pi = RemapPluginInfo::find_by_path(c)) == nullptr) { - pi = new RemapPluginInfo(ts::file::path(c)); - RemapPluginInfo::add_to_list(pi); - Debug("remap_plugin", "New remap plugin info created for \"%s\"", c); - - { - uint32_t elevate_access = 0; - REC_ReadConfigInteger(elevate_access, "proxy.config.plugin.load_elevated"); - ElevateAccess access(elevate_access ? ElevateAccess::FILE_PRIVILEGE : 0); - - if ((pi->dl_handle = dlopen(c, RTLD_NOW)) == nullptr) { -#if defined(freebsd) || defined(openbsd) - err = (char *)dlerror(); -#else - err = dlerror(); -#endif - snprintf(errbuf, errbufsize, "Can't load plugin \"%s\" - %s", c, err ? err : "Unknown dlopen() error"); - return -4; - } - pi->init_cb = reinterpret_cast(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_INIT)); - pi->config_reload_cb = reinterpret_cast(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_CONFIG_RELOAD)); - pi->done_cb = reinterpret_cast(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_DONE)); - pi->new_instance_cb = - reinterpret_cast(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_NEW_INSTANCE)); - pi->delete_instance_cb = - reinterpret_cast(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_DELETE_INSTANCE)); - pi->do_remap_cb = reinterpret_cast(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_DO_REMAP)); - pi->os_response_cb = reinterpret_cast(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_OS_RESPONSE)); - - int retcode = 0; - if (!pi->init_cb) { - snprintf(errbuf, errbufsize, R"(Can't find "%s" function in remap plugin "%s")", TSREMAP_FUNCNAME_INIT, c); - retcode = -10; - } else if (!pi->new_instance_cb && pi->delete_instance_cb) { - snprintf(errbuf, errbufsize, - R"(Can't find "%s" function in remap plugin "%s" which is required if "%s" function exists)", - TSREMAP_FUNCNAME_NEW_INSTANCE, c, TSREMAP_FUNCNAME_DELETE_INSTANCE); - retcode = -11; - } else if (!pi->do_remap_cb) { - snprintf(errbuf, errbufsize, R"(Can't find "%s" function in remap plugin "%s")", TSREMAP_FUNCNAME_DO_REMAP, c); - retcode = -12; - } else if (pi->new_instance_cb && !pi->delete_instance_cb) { - snprintf(errbuf, errbufsize, - R"(Can't find "%s" function in remap plugin "%s" which is required if "%s" function exists)", - TSREMAP_FUNCNAME_DELETE_INSTANCE, c, TSREMAP_FUNCNAME_NEW_INSTANCE); - retcode = -13; - } - if (retcode) { - if (errbuf && errbufsize > 0) { - Debug("remap_plugin", "%s", errbuf); - } - dlclose(pi->dl_handle); - pi->dl_handle = nullptr; - return retcode; - } - memset(&ri, 0, sizeof(ri)); - ri.size = sizeof(ri); - ri.tsremap_version = TSREMAP_VERSION; - - if (pi->init_cb(&ri, tmpbuf, sizeof(tmpbuf) - 1) != TS_SUCCESS) { - snprintf(errbuf, errbufsize, "Failed to initialize plugin \"%s\": %s", pi->path.c_str(), - tmpbuf[0] ? tmpbuf : "Unknown plugin error"); - return -5; - } - } // done elevating access - Debug("remap_plugin", "Remap plugin \"%s\" - initialization completed", c); - } - - if (!pi->dl_handle) { - snprintf(errbuf, errbufsize, "Can't load plugin \"%s\"", c); - return -6; - } - + /* Prepare remap plugin parameters from the config */ if ((err = mp->fromURL.string_get(nullptr)) == nullptr) { snprintf(errbuf, errbufsize, "Can't load fromURL from URL class"); - return -7; + return false; } - - int parc = 0; - parv[parc++] = ats_strdup(err); + pargv[parc++] = ats_strdup(err); ats_free(err); if ((err = mp->toURL.string_get(nullptr)) == nullptr) { snprintf(errbuf, errbufsize, "Can't load toURL from URL class"); - return -7; + return false; } - parv[parc++] = ats_strdup(err); + pargv[parc++] = ats_strdup(err); ats_free(err); bool plugin_encountered = false; // how many plugin parameters we have for this remapping - for (idx = 0; idx < argc && parc < (int)(countof(parv) - 1); idx++) { + for (idx = 0; idx < argc && parc < static_cast(countof(pargv) - 1); idx++) { if (plugin_encountered && !strncasecmp("plugin=", argv[idx], 7) && argv[idx][7]) { *plugin_found_at = idx; break; // if there is another plugin, lets deal with that later @@ -876,7 +788,7 @@ remap_load_plugin(const char **argv, int argc, url_mapping *mp, char *errbuf, in } if (!strncasecmp("pparam=", argv[idx], 7) && argv[idx][7]) { - parv[parc++] = const_cast(&(argv[idx][7])); + pargv[parc++] = const_cast(&(argv[idx][7])); } } @@ -885,43 +797,32 @@ remap_load_plugin(const char **argv, int argc, url_mapping *mp, char *errbuf, in Debug("url_rewrite", "Argument %d: %s", k, argv[k]); } - Debug("url_rewrite", "Viewing parsed plugin parameters for %s: [%d]", pi->path.c_str(), *plugin_found_at); + Debug("url_rewrite", "Viewing parsed plugin parameters for %s: [%d]", c, *plugin_found_at); for (int k = 0; k < parc; k++) { - Debug("url_rewrite", "Argument %d: %s", k, parv[k]); + Debug("url_rewrite", "Argument %d: %s", k, pargv[k]); } - Debug("remap_plugin", "creating new plugin instance"); - - void *ih = nullptr; - TSReturnCode res = TS_SUCCESS; - if (pi->new_instance_cb) { -#if (!defined(kfreebsd) && defined(freebsd)) || defined(darwin) - optreset = 1; -#endif -#if defined(__GLIBC__) - optind = 0; -#else - optind = 1; -#endif - opterr = 0; - optarg = nullptr; - - res = pi->new_instance_cb(parc, parv, &ih, tmpbuf, sizeof(tmpbuf) - 1); - } - - Debug("remap_plugin", "done creating new plugin instance"); + RemapPluginInst *pi = nullptr; + std::string error; + { + uint32_t elevate_access = 0; + REC_ReadConfigInteger(elevate_access, "proxy.config.plugin.load_elevated"); + ElevateAccess access(elevate_access ? ElevateAccess::FILE_PRIVILEGE : 0); - ats_free(parv[0]); // fromURL - ats_free(parv[1]); // toURL + pi = rewrite->pluginFactory.getRemapPlugin(ts::file::path(const_cast(c)), parc, pargv, error); + } // done elevating access - if (res != TS_SUCCESS) { - snprintf(errbuf, errbufsize, "Failed to create instance for plugin \"%s\": %s", c, tmpbuf[0] ? tmpbuf : "Unknown plugin error"); - return -8; + bool result = true; + if (nullptr == pi) { + snprintf(errbuf, errbufsize, "%s", error.c_str()); + } else { + mp->add_plugin_instance(pi); } - mp->add_plugin(pi, ih); + ats_free(pargv[0]); // fromURL + ats_free(pargv[1]); // toURL - return 0; + return result; } /** will process the regex mapping configuration and create objects in output argument reg_map. It assumes existing data in reg_map is @@ -1368,8 +1269,8 @@ remap_parse_config_bti(const char *path, BUILD_TABLE_INFO *bti) int jump_to_argc = 0; // this loads the first plugin - if (remap_load_plugin((const char **)bti->argv, bti->argc, new_mapping, errStrBuf, sizeof(errStrBuf), 0, - &plugin_found_at)) { + if (!remap_load_plugin((const char **)bti->argv, bti->argc, new_mapping, errStrBuf, sizeof(errStrBuf), 0, &plugin_found_at, + bti->rewrite)) { Debug("remap_plugin", "Remap plugin load error - %s", errStrBuf[0] ? errStrBuf : "Unknown error"); errStr = errStrBuf; goto MAP_ERROR; @@ -1377,8 +1278,8 @@ remap_parse_config_bti(const char *path, BUILD_TABLE_INFO *bti) // this loads any subsequent plugins (if present) while (plugin_found_at) { jump_to_argc += plugin_found_at; - if (remap_load_plugin((const char **)bti->argv, bti->argc, new_mapping, errStrBuf, sizeof(errStrBuf), jump_to_argc, - &plugin_found_at)) { + if (!remap_load_plugin((const char **)bti->argv, bti->argc, new_mapping, errStrBuf, sizeof(errStrBuf), jump_to_argc, + &plugin_found_at, bti->rewrite)) { Debug("remap_plugin", "Remap plugin load error - %s", errStrBuf[0] ? errStrBuf : "Unknown error"); errStr = errStrBuf; goto MAP_ERROR; @@ -1421,7 +1322,7 @@ remap_parse_config(const char *path, UrlRewrite *rewrite) // If this happens to be a config reload, the list of loaded remap plugins is non-empty, and we // can signal all these plugins that a reload has begun. - RemapPluginInfo::indicate_reload(); + rewrite->pluginFactory.indicateReload(); bti.rewrite = rewrite; return remap_parse_config_bti(path, &bti); } diff --git a/proxy/http/remap/RemapPluginInfo.cc b/proxy/http/remap/RemapPluginInfo.cc index db6dfe59498..8a1c00ed134 100644 --- a/proxy/http/remap/RemapPluginInfo.cc +++ b/proxy/http/remap/RemapPluginInfo.cc @@ -19,62 +19,252 @@ 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. + */ +#include + #include "RemapPluginInfo.h" #include "tscore/ink_string.h" #include "tscore/ink_memory.h" +#include "tscore/ink_apidefs.h" + +#include "RemapPluginInfo.h" +#ifdef PLUGIN_DSO_TESTS +#include "unit-tests/plugin_testing_common.h" +#else +#include "tscore/Diags.h" +#endif + +/** + * @brief helper function that returns the function address from the plugin DSO + * + * There can be valid defined DSO symbols that are NULL + * but when it comes to functions we can assume that + * if not defined we can return nullptr and a valid address if the are defined. + * @param symbol function symbol name + * @param error error messages in case of symbol is not found + * @return function address or nullptr if not found. + */ +template +T * +RemapPluginInfo::getFunctionSymbol(const char *symbol) +{ + std::string error; /* ignore the error, return nullptr if symbol not defined */ + void *address = nullptr; + getSymbol(symbol, address, error); + return reinterpret_cast(address); +} + +std::string +RemapPluginInfo::missingRequiredSymbolError(const std::string &pluginName, const char *required, const char *requiring) +{ + std::string error; + error.assign("plugin ").append(pluginName).append(" missing required function ").append(required); + if (requiring) { + error.append(" if ").append(requiring).append(" is defined"); + } + return error; +} + +RemapPluginInfo::RemapPluginInfo(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath) + : PluginDso(configPath, effectivePath, runtimePath) +{ +} + +bool +RemapPluginInfo::load(std::string &error) +{ + error.clear(); + + if (!PluginDso::load(error)) { + return false; + } + + init_cb = getFunctionSymbol(TSREMAP_FUNCNAME_INIT); + config_reload_cb = getFunctionSymbol(TSREMAP_FUNCNAME_CONFIG_RELOAD); + done_cb = getFunctionSymbol(TSREMAP_FUNCNAME_DONE); + new_instance_cb = getFunctionSymbol(TSREMAP_FUNCNAME_NEW_INSTANCE); + delete_instance_cb = getFunctionSymbol(TSREMAP_FUNCNAME_DELETE_INSTANCE); + do_remap_cb = getFunctionSymbol(TSREMAP_FUNCNAME_DO_REMAP); + os_response_cb = getFunctionSymbol(TSREMAP_FUNCNAME_OS_RESPONSE); + + /* Validate if the callback TSREMAP functions are specified correctly in the plugin. */ + bool valid = true; + if (!init_cb) { + error = missingRequiredSymbolError(_configPath.string(), TSREMAP_FUNCNAME_INIT); + valid = false; + } else if (!do_remap_cb) { + error = missingRequiredSymbolError(_configPath.string(), TSREMAP_FUNCNAME_DO_REMAP); + valid = false; + } else if (!new_instance_cb && delete_instance_cb) { + error = missingRequiredSymbolError(_configPath.string(), TSREMAP_FUNCNAME_NEW_INSTANCE, TSREMAP_FUNCNAME_DELETE_INSTANCE); + valid = false; + } else if (new_instance_cb && !delete_instance_cb) { + error = missingRequiredSymbolError(_configPath.string(), TSREMAP_FUNCNAME_DELETE_INSTANCE, TSREMAP_FUNCNAME_NEW_INSTANCE); + valid = false; + } + + if (valid) { + Debug(_tag, "plugin '%s' callbacks validated", _configPath.c_str()); + } else { + Error("plugin '%s' callbacks validation failed: %s", _configPath.c_str(), error.c_str()); + } + return valid; +} + +/* Initialize plugin (required). */ +bool +RemapPluginInfo::init(std::string &error) +{ + TSRemapInterface ri; + bool result = true; -RemapPluginInfo::List RemapPluginInfo::g_list; + Debug(_tag, "started initializing plugin '%s'", _configPath.c_str()); -RemapPluginInfo::RemapPluginInfo(ts::file::path &&library_path) : path(std::move(library_path)) {} + /* A buffer to get the error from the plugin instance init function, be defensive here. */ + char tmpbuf[2048]; + ink_zero(tmpbuf); -RemapPluginInfo::~RemapPluginInfo() + ink_zero(ri); + ri.size = sizeof(ri); + ri.tsremap_version = TSREMAP_VERSION; + + setPluginContext(); + + if (init_cb && init_cb(&ri, tmpbuf, sizeof(tmpbuf) - 1) != TS_SUCCESS) { + error.assign("failed to initialize plugin ") + .append(_configPath.string()) + .append(": ") + .append(tmpbuf[0] ? tmpbuf : "Unknown plugin error"); + result = false; + } + + resetPluginContext(); + + Debug(_tag, "finished initializing plugin '%s'", _configPath.c_str()); + + return result; +} + +/* Called when plugin is unloaded (optional). */ +void +RemapPluginInfo::done() { - if (dl_handle) { - dlclose(dl_handle); + if (done_cb) { + done_cb(); } } -// -// Find a plugin by path from our linked list -// -RemapPluginInfo * -RemapPluginInfo::find_by_path(std::string_view library_path) +bool +RemapPluginInfo::initInstance(int argc, char **argv, void **ih, std::string &error) { - auto spot = std::find_if(g_list.begin(), g_list.end(), - [&](self_type const &info) -> bool { return 0 == library_path.compare(info.path.view()); }); - return spot == g_list.end() ? nullptr : static_cast(spot); + TSReturnCode res = TS_SUCCESS; + bool result = true; + + Debug(_tag, "started initializing instance of plugin '%s'", _configPath.c_str()); + + /* A buffer to get the error from the plugin instance init function, be defensive here. */ + char tmpbuf[2048]; + ink_zero(tmpbuf); + + if (new_instance_cb) { +#if defined(freebsd) || defined(darwin) + optreset = 1; +#endif +#if defined(__GLIBC__) + optind = 0; +#else + optind = 1; +#endif + opterr = 0; + optarg = nullptr; + + setPluginContext(); + + res = new_instance_cb(argc, argv, ih, tmpbuf, sizeof(tmpbuf) - 1); + + resetPluginContext(); + + if (TS_SUCCESS != res) { + error.assign("failed to create instance for plugin ") + .append(_configPath.string()) + .append(": ") + .append(tmpbuf[0] ? tmpbuf : "Unknown plugin error"); + result = false; + } + } + + Debug(_tag, "finished initializing instance of plugin '%s'", _configPath.c_str()); + + return result; } -// -// Add a plugin to the linked list -// void -RemapPluginInfo::add_to_list(RemapPluginInfo *pi) +RemapPluginInfo::doneInstance(void *ih) { - g_list.append(pi); + setPluginContext(); + + if (delete_instance_cb) { + delete_instance_cb(ih); + } + + resetPluginContext(); +} + +TSRemapStatus +RemapPluginInfo::doRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri) +{ + TSRemapStatus result = TSREMAP_NO_REMAP; + + setPluginContext(); + + if (do_remap_cb) { + result = do_remap_cb(ih, rh, rri); + } + + resetPluginContext(); + + return result; } -// -// Remove and delete all plugins from a list. -// void -RemapPluginInfo::delete_list() +RemapPluginInfo::osResponse(void *ih, TSHttpTxn rh, int os_response_type) { - g_list.apply([](self_type *info) -> void { delete info; }); - g_list.clear(); + setPluginContext(); + + if (os_response_cb) { + os_response_cb(ih, rh, os_response_type); + } + + resetPluginContext(); } -// -// Tell all plugins (that so wish) that remap.config is being reloaded -// +RemapPluginInfo::~RemapPluginInfo() {} + void -RemapPluginInfo::indicate_reload() +RemapPluginInfo::indicateReload() { - g_list.apply([](self_type *info) -> void { - if (info->config_reload_cb) { - info->config_reload_cb(); - } - }); + setPluginContext(); + + if (config_reload_cb) { + config_reload_cb(); + } + + resetPluginContext(); +} + +inline void +RemapPluginInfo::setPluginContext() +{ + _tempContext = pluginThreadContext; + pluginThreadContext = this; + Debug(_tag, "change plugin context from dso-addr:%p to dso-addr:%p", pluginThreadContext, _tempContext); +} + +inline void +RemapPluginInfo::resetPluginContext() +{ + Debug(_tag, "change plugin context from dso-addr:%p to dso-addr:%p (restore)", this, pluginThreadContext); + pluginThreadContext = _tempContext; } diff --git a/proxy/http/remap/RemapPluginInfo.h b/proxy/http/remap/RemapPluginInfo.h index 7802b5c252d..cc5941db64a 100644 --- a/proxy/http/remap/RemapPluginInfo.h +++ b/proxy/http/remap/RemapPluginInfo.h @@ -19,14 +19,22 @@ 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. + */ #pragma once + +#include +#include + #include "tscore/ink_platform.h" -#include "tscpp/util/IntrusiveDList.h" -#include "tscore/ts_file.h" #include "ts/apidefs.h" #include "ts/remap.h" +#include "PluginDso.h" + +class url_mapping; + +extern thread_local PluginThreadContext *pluginThreadContext; static constexpr const char *const TSREMAP_FUNCNAME_INIT = "TSRemapInit"; static constexpr const char *const TSREMAP_FUNCNAME_CONFIG_RELOAD = "TSRemapConfigReload"; @@ -36,14 +44,13 @@ static constexpr const char *const TSREMAP_FUNCNAME_DELETE_INSTANCE = "TSRemapDe static constexpr const char *const TSREMAP_FUNCNAME_DO_REMAP = "TSRemapDoRemap"; static constexpr const char *const TSREMAP_FUNCNAME_OS_RESPONSE = "TSRemapOSResponse"; -/** Information for a remap plugin. - * This stores the name of the library file and the callback entry points. +/** + * Holds information for a remap plugin, remap specific callback entry points for plugin init/done and instance init/done, do_remap, + * origin server response, */ -class RemapPluginInfo +class RemapPluginInfo : public PluginDso { public: - using self_type = RemapPluginInfo; ///< Self reference type. - /// Initialization function, called on library load. using Init_F = TSReturnCode(TSRemapInterface *api_info, char *errbuf, int errbuf_size); /// Reload function, called to inform the plugin of a configuration reload. @@ -59,12 +66,6 @@ class RemapPluginInfo /// I have no idea what this is for. using OS_Response_F = void(void *ih, TSHttpTxn rh, int os_response_type); - self_type *_next = nullptr; - self_type *_prev = nullptr; - using Linkage = ts::IntrusiveLinkage; - using List = ts::IntrusiveDList; - - ts::file::path path; void *dl_handle = nullptr; /* "handle" for the dynamic library */ Init_F *init_cb = nullptr; Reload_F *config_reload_cb = nullptr; @@ -74,16 +75,37 @@ class RemapPluginInfo Do_Remap_F *do_remap_cb = nullptr; OS_Response_F *os_response_cb = nullptr; - explicit RemapPluginInfo(ts::file::path &&library_path); + RemapPluginInfo(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath); ~RemapPluginInfo(); - static self_type *find_by_path(std::string_view library_path); - static void add_to_list(self_type *pi); - static void delete_list(); - static void indicate_reload(); + /* Overload to add / execute remap plugin specific tasks during the plugin loading */ + virtual bool load(std::string &error); + + /* Used by the factory to invoke callbacks during plugin load, init and unload */ + virtual bool init(std::string &error); + virtual void done(void); + + /* Used by the facility that handles remap plugin instances to invoke callbacks per plugin instance */ + bool initInstance(int argc, char **argv, void **ih, std::string &error); + void doneInstance(void *ih); + + /* Used by the other parts of the traffic server core while handling requests */ + TSRemapStatus doRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri); + void osResponse(void *ih, TSHttpTxn rh, int os_response_type); + + /* Used by traffic server core to indicate configuration reload */ + virtual void indicateReload(); + +protected: + /* Utility to be used only with unit testing */ + std::string missingRequiredSymbolError(const std::string &pluginName, const char *required, const char *requiring = nullptr); + template T *getFunctionSymbol(const char *symbol); + void setPluginContext(); + void resetPluginContext(); + + static constexpr const char *const _tag = "plugin_remap"; /** @brief log tag used by this class */ - /// Singleton list of remap plugin info instances. - static List g_list; + PluginThreadContext *_tempContext = nullptr; }; /** diff --git a/proxy/http/remap/RemapPlugins.cc b/proxy/http/remap/RemapPlugins.cc index 6a664450fb8..3e69c437966 100644 --- a/proxy/http/remap/RemapPlugins.cc +++ b/proxy/http/remap/RemapPlugins.cc @@ -19,6 +19,7 @@ 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. + */ #include "RemapPlugins.h" @@ -26,16 +27,14 @@ ClassAllocator pluginAllocator("RemapPluginsAlloc"); TSRemapStatus -RemapPlugins::run_plugin(RemapPluginInfo *plugin) +RemapPlugins::run_plugin(RemapPluginInst *plugin) { ink_assert(_s); TSRemapStatus plugin_retcode; TSRemapRequestInfo rri; - url_mapping *map = _s->url_map.getMapping(); - URL *map_from = _s->url_map.getFromURL(); - URL *map_to = _s->url_map.getToURL(); - void *ih = map->get_instance(_cur); + URL *map_from = _s->url_map.getFromURL(); + URL *map_to = _s->url_map.getToURL(); // This is the equivalent of TSHttpTxnClientReqGet(), which every remap plugin would // have to call. @@ -51,11 +50,10 @@ RemapPlugins::run_plugin(RemapPluginInfo *plugin) // Prepare State for the future if (_cur == 0) { - _s->fp_tsremap_os_response = plugin->os_response_cb; - _s->remap_plugin_instance = ih; + _s->os_response_plugin_inst = plugin; } - plugin_retcode = plugin->do_remap_cb(ih, reinterpret_cast(_s->state_machine), &rri); + plugin_retcode = plugin->doRemap(reinterpret_cast(_s->state_machine), &rri); // TODO: Deal with negative return codes here if (plugin_retcode < 0) { plugin_retcode = TSREMAP_NO_REMAP; @@ -82,7 +80,7 @@ bool RemapPlugins::run_single_remap() { url_mapping *map = _s->url_map.getMapping(); - RemapPluginInfo *plugin = map->get_plugin(_cur); // get the nth plugin in our list of plugins + RemapPluginInst *plugin = map->get_plugin_instance(_cur); // get the nth plugin in our list of plugins TSRemapStatus plugin_retcode = TSREMAP_NO_REMAP; bool zret = true; // default - last iteration. Debug("url_rewrite", "running single remap rule id %d for the %d%s time", map->map_id, _cur, @@ -109,7 +107,7 @@ RemapPlugins::run_single_remap() if (TSREMAP_NO_REMAP_STOP == plugin_retcode || TSREMAP_DID_REMAP_STOP == plugin_retcode) { Debug("url_rewrite", "breaking remap plugin chain since last plugin said we should stop after %d rewrites", _rewritten); - } else if (_cur >= map->plugin_count()) { + } else if (_cur >= map->plugin_instance_count()) { Debug("url_rewrite", "completed all remap plugins for rule id %d, changed by %d plugins", map->map_id, _rewritten); } else { Debug("url_rewrite", "completed single remap, attempting another via immediate callback"); diff --git a/proxy/http/remap/RemapPlugins.h b/proxy/http/remap/RemapPlugins.h index 421b55ca51e..1bec1b98f2c 100644 --- a/proxy/http/remap/RemapPlugins.h +++ b/proxy/http/remap/RemapPlugins.h @@ -19,11 +19,8 @@ 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. - */ -/** - * Remap plugins class - **/ + */ #pragma once @@ -68,7 +65,7 @@ struct RemapPlugins : public Continuation { int run_remap(int event, Event *e); bool run_single_remap(); - TSRemapStatus run_plugin(RemapPluginInfo *plugin); + TSRemapStatus run_plugin(RemapPluginInst *plugin); Action action; diff --git a/proxy/http/remap/UrlMapping.cc b/proxy/http/remap/UrlMapping.cc index 782b3e2ac56..61a375dea9a 100644 --- a/proxy/http/remap/UrlMapping.cc +++ b/proxy/http/remap/UrlMapping.cc @@ -19,6 +19,7 @@ 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. + */ #include "tscore/ink_defs.h" @@ -30,50 +31,25 @@ * **/ bool -url_mapping::add_plugin(RemapPluginInfo *i, void *ih) +url_mapping::add_plugin_instance(RemapPluginInst *i) { - _plugin_list.push_back(i); - _instance_data.push_back(ih); - + _plugin_inst_list.push_back(i); return true; } /** * **/ -RemapPluginInfo * -url_mapping::get_plugin(std::size_t index) const +RemapPluginInst * +url_mapping::get_plugin_instance(std::size_t index) const { - Debug("url_rewrite", "get_plugin says we have %zu plugins and asking for plugin %zu", plugin_count(), index); - if (index < _plugin_list.size()) { - return _plugin_list[index]; + Debug("url_rewrite", "get_plugin says we have %zu plugins and asking for plugin %zu", _plugin_inst_list.size(), index); + if (index < _plugin_inst_list.size()) { + return _plugin_inst_list[index]; } return nullptr; } -void * -url_mapping::get_instance(std::size_t index) const -{ - if (index < _instance_data.size()) { - return _instance_data[index]; - } - return nullptr; -} - -/** - * - **/ -void -url_mapping::delete_instance(unsigned int index) -{ - void *ih = get_instance(index); - RemapPluginInfo *p = get_plugin(index); - - if (ih && p && p->delete_instance_cb) { - p->delete_instance_cb(ih); - } -} - /** * **/ @@ -96,13 +72,6 @@ url_mapping::~url_mapping() delete rc; } - // Delete all instance data, this gets ugly because to delete the instance data, we also - // must know which plugin this is associated with. Hence, looping with index instead of a - // normal iterator. ToDo: Maybe we can combine them into another container. - for (std::size_t i = 0; i < plugin_count(); ++i) { - delete_instance(i); - } - // Delete filters while ((afr = filter) != nullptr) { filter = afr->next; @@ -122,7 +91,8 @@ url_mapping::Print() fromURL.string_get_buf(from_url_buf, (int)sizeof(from_url_buf)); toURL.string_get_buf(to_url_buf, (int)sizeof(to_url_buf)); printf("\t %s %s=> %s %s <%s> [plugins %s enabled; running with %zu plugins]\n", from_url_buf, unique ? "(unique)" : "", - to_url_buf, homePageRedirect ? "(R)" : "", tag ? tag : "", plugin_count() > 0 ? "are" : "not", plugin_count()); + to_url_buf, homePageRedirect ? "(R)" : "", tag ? tag : "", _plugin_inst_list.size() > 0 ? "are" : "not", + _plugin_inst_list.size()); } /** diff --git a/proxy/http/remap/UrlMapping.h b/proxy/http/remap/UrlMapping.h index 1b55e52b72f..c6de7683bde 100644 --- a/proxy/http/remap/UrlMapping.h +++ b/proxy/http/remap/UrlMapping.h @@ -19,6 +19,7 @@ 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. + */ #pragma once @@ -29,6 +30,7 @@ #include "AclFiltering.h" #include "URL.h" #include "RemapPluginInfo.h" +#include "PluginFactory.h" #include "tscore/Regex.h" #include "tscore/List.h" @@ -79,17 +81,15 @@ class url_mapping public: ~url_mapping(); - bool add_plugin(RemapPluginInfo *i, void *ih); - RemapPluginInfo *get_plugin(std::size_t) const; - void *get_instance(std::size_t) const; + bool add_plugin_instance(RemapPluginInst *i); + RemapPluginInst *get_plugin_instance(std::size_t) const; std::size_t - plugin_count() const + plugin_instance_count() const { - return _plugin_list.size(); + return _plugin_inst_list.size(); } - void delete_instance(unsigned int index); void Print(); int from_path_len = 0; @@ -122,8 +122,7 @@ class url_mapping }; private: - std::vector _plugin_list; - std::vector _instance_data; + std::vector _plugin_inst_list; int _rank = 0; }; diff --git a/proxy/http/remap/UrlRewrite.cc b/proxy/http/remap/UrlRewrite.cc index 6f2e83d1b95..91aa3e8ec5d 100644 --- a/proxy/http/remap/UrlRewrite.cc +++ b/proxy/http/remap/UrlRewrite.cc @@ -19,6 +19,7 @@ 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. + */ #include "UrlRewrite.h" @@ -79,6 +80,9 @@ UrlRewrite::load() REC_ReadConfigInteger(reverse_proxy, "proxy.config.reverse_proxy.enabled"); + /* Initialize the plugin factory */ + pluginFactory.setRuntimeDir(RecConfigReadRuntimeDir()).addSearchDir(RecConfigReadPluginDir()); + if (0 == this->BuildTable(config_file_path)) { _valid = true; if (is_debug_tag_set("url_rewrite")) { diff --git a/proxy/http/remap/UrlRewrite.h b/proxy/http/remap/UrlRewrite.h index 89661caecae..84386eed709 100644 --- a/proxy/http/remap/UrlRewrite.h +++ b/proxy/http/remap/UrlRewrite.h @@ -19,6 +19,7 @@ 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. + */ #pragma once @@ -28,6 +29,7 @@ #include "UrlMappingPathIndex.h" #include "HttpTransact.h" #include "tscore/Regex.h" +#include "PluginFactory.h" #include @@ -208,6 +210,8 @@ class UrlRewrite : public RefCountObj int num_rules_redirect_temporary = 0; int num_rules_forward_with_recv_port = 0; + PluginFactory pluginFactory; + private: bool _valid = false; diff --git a/proxy/http/remap/unit-tests/plugin_misc_cb.cc b/proxy/http/remap/unit-tests/plugin_misc_cb.cc new file mode 100644 index 00000000000..f0792fc463d --- /dev/null +++ b/proxy/http/remap/unit-tests/plugin_misc_cb.cc @@ -0,0 +1,106 @@ +/** @file + + A test plugin for testing Plugin's 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 "plugin_testing_common.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +#include "ts/ts.h" +#include "ts/remap.h" + +PluginDebugObject debugObject; + +TSReturnCode +TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size) +{ + debugObject.contextInit = pluginThreadContext; + return TS_SUCCESS; +} + +void +TSRemapDone(void) +{ +} + +TSRemapStatus +TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri) +{ + return TSREMAP_NO_REMAP; +} + +TSReturnCode +TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int errbuf_size) +{ + debugObject.contextInitInstance = pluginThreadContext; + + return TS_SUCCESS; +} + +void +TSRemapDeleteInstance(void *) +{ +} + +void +TSRemapOSResponse(void *ih, TSHttpTxn rh, int os_response_type) +{ +} + +void +TSPluginInit(int argc, const char *argv[]) +{ +} + +void +TSRemapConfigReload(void) +{ +} + +/* This is meant for test with plugins of different versions */ +int +pluginDsoVersionTest() +{ +#ifdef PLUGINDSOVER + return PLUGINDSOVER; +#else + return -1; +#endif +} + +void * +getPluginDebugObjectTest() +{ + return (void *)&debugObject; +} + +#ifdef __cplusplus +} +#endif /* __cplusplus */ diff --git a/proxy/http/remap/unit-tests/plugin_missing_deleteinstance.cc b/proxy/http/remap/unit-tests/plugin_missing_deleteinstance.cc new file mode 100644 index 00000000000..03ded2d1913 --- /dev/null +++ b/proxy/http/remap/unit-tests/plugin_missing_deleteinstance.cc @@ -0,0 +1,57 @@ +/** @file + + A test plugin for testing Plugin's 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. + + */ + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +#include "ts/ts.h" +#include "ts/remap.h" + +TSReturnCode +TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size) +{ + return TS_SUCCESS; +} + +TSRemapStatus +TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri) +{ + return TSREMAP_NO_REMAP; +} + +TSReturnCode +TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int errbuf_size) +{ + return TS_SUCCESS; +} + +#ifdef __cplusplus +} +#endif /* __cplusplus */ diff --git a/proxy/http/remap/unit-tests/plugin_missing_doremap.cc b/proxy/http/remap/unit-tests/plugin_missing_doremap.cc new file mode 100644 index 00000000000..f727f6dee58 --- /dev/null +++ b/proxy/http/remap/unit-tests/plugin_missing_doremap.cc @@ -0,0 +1,45 @@ +/** @file + + A test plugin for testing Plugin's 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. + + */ + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +#include "ts/ts.h" +#include "ts/remap.h" + +TSReturnCode +TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size) +{ + return TS_SUCCESS; +} + +#ifdef __cplusplus +} +#endif /* __cplusplus */ diff --git a/proxy/http/remap/unit-tests/plugin_missing_init.cc b/proxy/http/remap/unit-tests/plugin_missing_init.cc new file mode 100644 index 00000000000..265bfa5f916 --- /dev/null +++ b/proxy/http/remap/unit-tests/plugin_missing_init.cc @@ -0,0 +1,45 @@ +/** @file + + A test plugin for testing Plugin's 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. + + */ + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +#include "ts/ts.h" +#include "ts/remap.h" + +TSRemapStatus +TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri) +{ + return TSREMAP_NO_REMAP; +} + +#ifdef __cplusplus +} +#endif /* __cplusplus */ diff --git a/proxy/http/remap/unit-tests/plugin_missing_newinstance.cc b/proxy/http/remap/unit-tests/plugin_missing_newinstance.cc new file mode 100644 index 00000000000..bed55d6dae2 --- /dev/null +++ b/proxy/http/remap/unit-tests/plugin_missing_newinstance.cc @@ -0,0 +1,56 @@ +/** @file + + A test plugin for testing Plugin's 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. + + */ + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +#include "ts/ts.h" +#include "ts/remap.h" + +TSReturnCode +TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size) +{ + return TS_SUCCESS; +} + +TSRemapStatus +TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri) +{ + return TSREMAP_NO_REMAP; +} + +void +TSRemapDeleteInstance(void *) +{ +} + +#ifdef __cplusplus +} +#endif /* __cplusplus */ diff --git a/proxy/http/remap/unit-tests/plugin_required_cb.cc b/proxy/http/remap/unit-tests/plugin_required_cb.cc new file mode 100644 index 00000000000..65b8026f75c --- /dev/null +++ b/proxy/http/remap/unit-tests/plugin_required_cb.cc @@ -0,0 +1,51 @@ +/** @file + + A test plugin for testing Plugin's 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. + + */ + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +#include "ts/ts.h" +#include "ts/remap.h" + +TSReturnCode +TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size) +{ + return TS_SUCCESS; +} + +TSRemapStatus +TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri) +{ + return TSREMAP_NO_REMAP; +} + +#ifdef __cplusplus +} +#endif /* __cplusplus */ diff --git a/proxy/http/remap/unit-tests/plugin_testing_calls.cc b/proxy/http/remap/unit-tests/plugin_testing_calls.cc new file mode 100644 index 00000000000..89c8df2c749 --- /dev/null +++ b/proxy/http/remap/unit-tests/plugin_testing_calls.cc @@ -0,0 +1,130 @@ +/** @file + + A test plugin for testing Plugin's 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 "ts/ts.h" +#include "ts/remap.h" +#include "plugin_testing_common.h" +#include + +#include "../RemapPluginInfo.h" + +PluginDebugObject debugObject; + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +TSReturnCode +handleInitRun(char *errbuf, int errbuf_size, int &counter) +{ + TSReturnCode result = TS_SUCCESS; + + if (debugObject.fail) { + result = TS_ERROR; + snprintf(errbuf, errbuf_size, "%s", "Init failed"); + } + + counter++; + + return result; +} + +TSReturnCode +TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size) +{ + TSReturnCode result = handleInitRun(errbuf, errbuf_size, debugObject.initCalled); + return result; +} + +TSReturnCode +TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int errbuf_size) +{ + TSReturnCode result = handleInitRun(errbuf, errbuf_size, debugObject.initInstanceCalled); + + if (TS_SUCCESS == result) { + *ih = debugObject.input_ih; + } + + debugObject.argc = argc; + debugObject.argv = argv; + + return result; +} + +void +TSRemapDone(void) +{ + debugObject.doneCalled++; +} + +void +TSRemapDeleteInstance(void *ih) +{ + debugObject.deleteInstanceCalled++; + debugObject.ih = ih; +} + +TSRemapStatus +TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri) +{ + debugObject.doRemapCalled++; + return TSREMAP_NO_REMAP; +} + +void +TSRemapOSResponse(void *ih, TSHttpTxn rh, int os_response_type) +{ +} + +void +TSRemapConfigReload(void) +{ + debugObject.reloadConfigCalled++; +} + +/* The folowing functions are meant for unit testing */ +int +pluginDsoVersionTest() +{ +#ifdef PLUGINDSOVER + return PLUGINDSOVER; +#else + return -1; +#endif +} + +void * +getPluginDebugObjectTest() +{ + return (void *)&debugObject; +} + +#ifdef __cplusplus +} +#endif /* __cplusplus */ diff --git a/proxy/http/remap/unit-tests/plugin_testing_common.cc b/proxy/http/remap/unit-tests/plugin_testing_common.cc new file mode 100644 index 00000000000..d5d08d33ca8 --- /dev/null +++ b/proxy/http/remap/unit-tests/plugin_testing_common.cc @@ -0,0 +1,39 @@ +/** @file + + A test plugin common testing functionality + + @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 "plugin_testing_common.h" + +void +PrintToStdErr(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); +} diff --git a/proxy/http/remap/unit-tests/plugin_testing_common.h b/proxy/http/remap/unit-tests/plugin_testing_common.h new file mode 100644 index 00000000000..12346ea5984 --- /dev/null +++ b/proxy/http/remap/unit-tests/plugin_testing_common.h @@ -0,0 +1,95 @@ +/** @file + + A test plugin header for testing Plugin's 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. + + */ + +#pragma once + +#include +#include +#include + +#include +#include + +#include "../PluginFactory.h" + +extern thread_local PluginThreadContext *pluginThreadContext; + +class PluginDebugObject +{ +public: + PluginDebugObject() { clear(); } + + void + clear() + { + contextInit = nullptr; + contextInitInstance = nullptr; + doRemapCalled = 0; + initCalled = 0; + doneCalled = 0; + initInstanceCalled = 0; + deleteInstanceCalled = 0; + reloadConfigCalled = 0; + ih = nullptr; + argc = 0; + argv = nullptr; + } + + /* Input fields used to set the test behavior of the plugin call-backs */ + bool fail = false; /* tell the plugin call-back to fail for testing purposuses */ + void *input_ih; /* the value to be returned by the plugin instance init function */ + + /* Output fields showing what happend during the test */ + const PluginThreadContext *contextInit = nullptr; /* plugin initialization context */ + const PluginThreadContext *contextInitInstance = nullptr; /* plugin instance initialization context */ + int doRemapCalled = 0; /* mark if remap was called */ + int initCalled = 0; /* mark if plugin init was called */ + int doneCalled = 0; /* mark if done was called */ + int initInstanceCalled = 0; /* mark if instance init was called */ + int deleteInstanceCalled = 0; /* mark if delete instance was called */ + int reloadConfigCalled = 0; /* mark if reload config was called */ + void *ih = nullptr; /* instance handler */ + int argc = 0; /* number of plugin instance parameters received by the plugin */ + char **argv = nullptr; /* plugin instance parameters received by the plugin */ +}; + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +typedef void *GetPluginDebugObjectFunction(void); +GetPluginDebugObjectFunction getPluginDebugObjectTest; + +#define Debug(category, fmt, ...) PrintToStdErr("(%s) %s:%d:%s() " fmt "\n", category, __FILE__, __LINE__, __func__, ##__VA_ARGS__) +#define Error(fmt, ...) PrintToStdErr("%s:%d:%s() " fmt "\n", __FILE__, __LINE__, __func__, ##__VA_ARGS__) +void PrintToStdErr(const char *fmt, ...); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ diff --git a/proxy/http/remap/unit-tests/test_PluginDso.cc b/proxy/http/remap/unit-tests/test_PluginDso.cc new file mode 100644 index 00000000000..c31e1d62b9d --- /dev/null +++ b/proxy/http/remap/unit-tests/test_PluginDso.cc @@ -0,0 +1,395 @@ +/** @file + + Unit tests for 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. + + */ + +#define CATCH_CONFIG_MAIN /* include main function */ +#include /* catch unit-test framework */ +#include /* ofstream */ + +#include "plugin_testing_common.h" +#include "../PluginDso.h" + +class PluginContext; +thread_local PluginThreadContext *pluginThreadContext; + +std::error_code ec; + +/* A temp sandbox to play with our toys used for all fun with this test-bench */ +static fs::path tmpDir = fs::canonical(fs::temp_directory_path(), ec); + +/* The following are dirs that are used commonly in the unit-tests */ +static fs::path sandboxDir = tmpDir / fs::path("sandbox"); +static fs::path runtimeDir = sandboxDir / fs::path("runtime"); +static fs::path searchDir = sandboxDir / fs::path("search"); +static fs::path pluginBuildDir = fs::current_path() / fs::path("unit-tests/.libs"); + +/* The following are paths used in all scenarios in the unit tests */ +static fs::path configPath = fs::path("plugin_v1.so"); +static fs::path pluginBuildPath = pluginBuildDir / configPath; +static fs::path effectivePath = searchDir / configPath; +static fs::path runtimePath = runtimeDir / configPath; + +void +clean() +{ + fs::remove(sandboxDir, ec); +} + +/* Mock used only to make PluginDso concrete enough to be tested */ +class PluginDsoUnitTest : public PluginDso +{ +public: + PluginDsoUnitTest(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath) + : PluginDso(configPath, effectivePath, runtimePath) + { + /* don't remove runtime DSO copy preventively so we can check if it was created properly */ + _preventiveCleaning = false; + } + + virtual void + indicateReload() + { + } + virtual bool + init(std::string &error) + { + return true; + } + virtual void + done() + { + } +}; + +/* + * The following scenario tests loading and unloading of plugins + */ +SCENARIO("loading plugins", "[plugin][core]") +{ + clean(); + std::string error; + + GIVEN("a valid plugin") + { + /* Setup the test fixture - search, runtime dirs and install a plugin with some defined callback functions */ + CHECK(fs::create_directories(searchDir, ec)); + CHECK(fs::create_directories(runtimeDir, ec)); + fs::copy(pluginBuildPath, searchDir, ec); + + /* Instantiate and initialize a plugin DSO instance. Make sure effective path exists, used to load */ + CHECK(fs::exists(effectivePath)); + PluginDsoUnitTest plugin(configPath, effectivePath, runtimePath); + + WHEN("loading a valid plugin") + { + bool result = plugin.load(error); + + THEN("expect it to successfully load") + { + CHECK(true == result); + CHECK(error.empty()); + CHECK(effectivePath == plugin.effectivePath()); + CHECK(runtimePath == plugin.runtimePath()); + CHECK(fs::exists(runtimePath)); + } + CHECK(fs::remove(sandboxDir, ec)); + } + + WHEN("loading a valid plugin") + { + bool result = plugin.load(error); + + THEN("expect saving the right DSO file modification time") + { + CHECK(true == result); + CHECK(error.empty()); + std::error_code ec; + fs::file_status fs = fs::status(effectivePath, ec); + CHECK(plugin.modTime() == fs::modification_time(fs)); + } + CHECK(fs::remove(sandboxDir, ec)); + } + + WHEN("loading a valid plugin but missing runtime dir") + { + CHECK(fs::remove(runtimeDir, ec)); + CHECK_FALSE(fs::exists(runtimePath)); + bool result = plugin.load(error); + + THEN("expect it to fail") + { + CHECK_FALSE(true == result); + CHECK("failed to create a copy: No such file or directory" == error); + } + CHECK(fs::remove(sandboxDir, ec)); + } + + WHEN("loading a valid plugin twice in a row") + { + /* First attempt OK */ + bool result = plugin.load(error); + CHECK(true == result); + CHECK(error.empty()); + + /* Second attempt */ + result = plugin.load(error); + + THEN("expect it to fail the second attempt") + { + CHECK_FALSE(true == result); + CHECK("plugin already loaded" == error); + } + CHECK(fs::remove(sandboxDir, ec)); + } + + WHEN("explicitly unloading a valid but not loaded plugin") + { + /* Make sure it is not loaded, runtime DSO not present */ + CHECK_FALSE(fs::exists(runtimePath)); + + /* Unload w/o loading beforehand */ + bool result = plugin.unload(error); + + THEN("expect the unload to fail") + { + CHECK(false == result); + CHECK_FALSE(error.empty()); + CHECK_FALSE(fs::exists(runtimePath)); + } + CHECK(fs::remove(sandboxDir, ec)); + } + + WHEN("unloading a valid plugin twice in a row") + { + /* First attempt OK */ + bool result = plugin.load(error); + CHECK(true == result); + CHECK(error.empty()); + result = plugin.unload(error); + CHECK(true == result); + CHECK("" == error); + + /* Second attempt */ + result = plugin.unload(error); + + THEN("expect it to fail the second attempt") + { + CHECK_FALSE(true == result); + CHECK("no plugin loaded" == error); + } + CHECK(fs::remove(sandboxDir, ec)); + } + + WHEN("explicitly unloading a valid and loaded plugin") + { + /* Make sure it is not loaded, runtime DSO not present */ + CHECK_FALSE(fs::exists(runtimePath)); + + /* Load and make sure it is loaded */ + CHECK(plugin.load(error)); + /* Effective and runtime path set */ + CHECK(effectivePath == plugin.effectivePath()); + CHECK(runtimePath == plugin.runtimePath()); + /* Runtime DSO should be present */ + CHECK(fs::exists(runtimePath)); + + /* Unload */ + bool result = plugin.unload(error); + + THEN("expect it to successfully unload") + { + CHECK(true == result); + CHECK(error.empty()); + /* Effective and runtime path still set */ + CHECK(effectivePath == plugin.effectivePath()); + CHECK(runtimePath == plugin.runtimePath()); + /* Runtime DSO should not be found anymore */ + CHECK_FALSE(fs::exists(runtimePath)); + } + CHECK(fs::remove(sandboxDir, ec)); + } + + WHEN("implicitly unloading a valid and loaded plugin") + { + { + PluginDsoUnitTest localPlugin(configPath, effectivePath, runtimePath); + + /* Load and make sure it is loaded */ + CHECK(localPlugin.load(error)); + /* Effective and runtime path set */ + CHECK(effectivePath == localPlugin.effectivePath()); + CHECK(runtimePath == localPlugin.runtimePath()); + /* Runtime DSO should be present */ + CHECK(fs::exists(runtimePath)); + + /* Unload by going out of scope */ + } + + THEN("expect it to successfully unload and clean after itself") + { + /* Runtime path should be removed after unloading */ + CHECK_FALSE(fs::exists(runtimePath)); + } + CHECK(fs::remove(sandboxDir, ec)); + } + } + + GIVEN("a plugin instance initialized with an empty effective path") + { + std::string error; + PluginDsoUnitTest plugin(configPath, /* effectivePath */ fs::path(), runtimePath); + + WHEN("loading the plugin") + { + bool result = plugin.load(error); + + THEN("expect the load to fail") + { + CHECK_FALSE(true == result); + CHECK("empty effective path" == error); + CHECK(plugin.effectivePath().empty()); + CHECK(0 == plugin.modTime()); + CHECK(runtimePath == plugin.runtimePath()); + CHECK_FALSE(fs::exists(runtimePath)); + } + } + } + + GIVEN("an invalid plugin") + { + /* Create the directory structure and install plugins */ + CHECK(fs::create_directories(searchDir, ec)); + CHECK(fs::create_directories(runtimeDir, ec)); + /* Create an invalid plugin and make sure the effective path to it exists */ + std::ofstream file(effectivePath.string()); + file << "Invalid plugin DSO content"; + file.close(); + CHECK(fs::exists(effectivePath)); + + /* Instantiate and initialize a plugin DSO instance. */ + std::string error; + PluginDsoUnitTest plugin(configPath, effectivePath, runtimePath); + + WHEN("loading an invalid plugin") + { + bool result = plugin.load(error); + + THEN("expect it to fail to load") + { + /* After calling load() the following should be set correctly */ + CHECK(effectivePath == plugin.effectivePath()); + CHECK(runtimePath == plugin.runtimePath()); + + /* But the load should fail and an error should be returned */ + CHECK(false == result); + CHECK_FALSE(error.empty()); + + /* Runtime DSO should not exist since the load failed. */ + CHECK_FALSE(fs::exists(runtimePath)); + } + CHECK(fs::remove(sandboxDir, ec)); + } + } +} + +/* + * The following scenario tests finding symbols inside the DSO. + */ +SCENARIO("looking for symbols inside a plugin DSO", "[plugin][core]") +{ + clean(); + std::string error; + + /* Setup the test fixture - search, runtime dirs and install a plugin with some defined callback functions */ + CHECK(fs::create_directories(searchDir, ec)); + CHECK(fs::create_directories(runtimeDir, ec)); + fs::copy(pluginBuildDir / configPath, searchDir, ec); + + /* Initialize a plugin DSO instance */ + PluginDsoUnitTest plugin(configPath, effectivePath, runtimePath); + + /* Now test away. */ + GIVEN("plugin loaded successfully") + { + CHECK(plugin.load(error)); + + WHEN("looking for an existing symbol") + { + THEN("expect to find it") + { + void *s = nullptr; + CHECK(plugin.getSymbol("TSRemapInit", s, error)); + CHECK(nullptr != s); + CHECK(error.empty()); + } + CHECK(fs::remove(sandboxDir, ec)); + } + + WHEN("looking for non-existing symbol") + { + THEN("expect not to find it and get an error") + { + void *s = nullptr; + CHECK_FALSE(plugin.getSymbol("NONEXISTING_SYMBOL", s, error)); + CHECK(nullptr == s); + CHECK_FALSE(error.empty()); + } + CHECK(fs::remove(sandboxDir, ec)); + } + + WHEN("looking for multiple existing symbols") + { + THEN("expect to find them all") + { + std::vector list{"TSRemapInit", "TSRemapDone", "TSRemapDoRemap", "TSRemapNewInstance", + "TSRemapDeleteInstance", "TSRemapOSResponse", "TSPluginInit", "pluginDsoVersionTest"}; + for (auto symbol : list) { + void *s = nullptr; + CHECK(plugin.getSymbol(symbol, s, error)); + CHECK(nullptr != s); + CHECK(error.empty()); + } + } + CHECK(fs::remove(sandboxDir, ec)); + } + + /* The following version function is used only for unit-testing of the plugin factory functionality */ + WHEN("using a symbol to call the corresponding version function") + { + THEN("expect to return the version number") + { + void *s = nullptr; + CHECK(plugin.getSymbol("pluginDsoVersionTest", s, error)); + int (*version)() = reinterpret_cast(s); + int ver = version ? version() : -1; + CHECK(1 == ver); + } + CHECK(fs::remove(sandboxDir, ec)); + } + } +} diff --git a/proxy/http/remap/unit-tests/test_PluginFactory.cc b/proxy/http/remap/unit-tests/test_PluginFactory.cc new file mode 100644 index 00000000000..c75040e8342 --- /dev/null +++ b/proxy/http/remap/unit-tests/test_PluginFactory.cc @@ -0,0 +1,657 @@ +/** @file + + Unit tests for 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. + + */ + +#define CATCH_CONFIG_MAIN /* include main function */ +#include /* catch unit-test framework */ +#include /* ofstream */ +#include + +#include "plugin_testing_common.h" +#include "../PluginFactory.h" +#include "../PluginDso.h" + +thread_local PluginThreadContext *pluginThreadContext; + +std::error_code ec; +static void *INSTANCE_HANDLER = (void *)789; + +/* Mock of PluginFactory just to get consisten UUID to be able to test consistently */ +static fs::path tempComponent = fs::path("c71e2bab-90dc-4770-9535-c9304c3de38e"); +class PluginFactoryUnitTest : public PluginFactory +{ +public: + PluginFactoryUnitTest(const fs::path &tempComponent) + { + _tempComponent = tempComponent; + _preventiveCleaning = false; + } + +protected: + const char * + getUuid() + { + return _tempComponent.c_str(); + } + + fs::path _tempComponent; +}; + +PluginDebugObject * +getDebugObject(const PluginDso &plugin) +{ + std::string error; /* ignore the error, return nullptr if symbol not defined */ + void *address = nullptr; + plugin.getSymbol("getPluginDebugObjectTest", address, error); + GetPluginDebugObjectFunction *getObject = reinterpret_cast(address); + if (getObject) { + PluginDebugObject *object = reinterpret_cast(getObject()); + return object; + } else { + return nullptr; + } +} + +/* A temp sandbox to play with our toys used for all fun with this test-bench */ +static fs::path tmpDir = fs::canonical(fs::temp_directory_path(), ec); + +/* The following are paths that are used commonly in the unit-tests */ +static fs::path sandboxDir = tmpDir / "sandbox"; +static fs::path runtimeRootDir = sandboxDir / "runtime"; +static fs::path runtimeDir = runtimeRootDir / tempComponent; +static fs::path searchDir = sandboxDir / "search"; +static fs::path pluginBuildDir = fs::current_path() / "unit-tests/.libs"; + +void +clean() +{ + fs::remove(sandboxDir, ec); +} + +static void +setupConfigPathTest(const fs::path &configPath, const fs::path &pluginBuildPath, const fs::path &uuid, fs::path &effectivePath, + fs::path &runtimePath, time_t mtime = 0, bool append = false) +{ + std::string error; + if (!append) { + clean(); + } + + effectivePath = configPath.is_absolute() ? configPath : searchDir / configPath; + runtimePath = runtimeRootDir / uuid / effectivePath.relative_path(); + + /* Create the directory structure and install plugins */ + fs::create_directories(effectivePath.parent_path(), ec); + fs::copy(pluginBuildPath, effectivePath, ec); + if (0 != mtime) { + struct stat sb; + struct utimbuf new_times; + stat(effectivePath.c_str(), &sb); + new_times.actime = sb.st_atime; /* keep atime unchanged */ + new_times.modtime = mtime; /* set mtime to current time */ + utime(effectivePath.c_str(), &new_times); + } + + CHECK(fs::exists(effectivePath)); +} + +static PluginFactoryUnitTest * +getFactory(const fs::path &uuid) +{ + /* Instantiate and initialize a plugin factory. */ + PluginFactoryUnitTest *factory = new PluginFactoryUnitTest(uuid); + factory->setRuntimeDir(runtimeRootDir); + factory->addSearchDir(searchDir); + return factory; +} + +static void +teardownConfigPathTest(PluginFactoryUnitTest *factory) +{ + delete factory; + clean(); +} + +static void +validateSuccessfulConfigPathTest(const RemapPluginInst *pluginInst, const std::string &error, const fs::path &effectivePath, + const fs::path &runtimePath) +{ + CHECK(nullptr != pluginInst); + CHECK("" == error); + CHECK(effectivePath == pluginInst->_plugin.effectivePath()); + CHECK(runtimePath == pluginInst->_plugin.runtimePath()); +} + +SCENARIO("loading plugins", "[plugin][core]") +{ + fs::path effectivePath; + fs::path runtimePath; + std::string error; + + GIVEN("an existing plugin") + { + fs::path pluginName = fs::path("plugin_v1.so"); + fs::path buildPath = pluginBuildDir / pluginName; + + WHEN("config using plugin file name only") + { + fs::path configPath = pluginName; + CHECK(configPath.is_relative()); /* make sure this is relative path - this is what we are testing */ + + setupConfigPathTest(configPath, buildPath, tempComponent, effectivePath, runtimePath); + PluginFactoryUnitTest *factory = getFactory(tempComponent); + RemapPluginInst *plugin = factory->getRemapPlugin(configPath, 0, nullptr, error); + + THEN("expect it to successfully load") { validateSuccessfulConfigPathTest(plugin, error, effectivePath, runtimePath); } + + teardownConfigPathTest(factory); + } + + WHEN("config is using plugin relative filename") + { + fs::path configPath = fs::path("subdir") / pluginName; + CHECK(configPath.is_relative()); /* make sure this is relative path - this is what we are testing */ + + setupConfigPathTest(configPath, buildPath, tempComponent, effectivePath, runtimePath); + PluginFactoryUnitTest *factory = getFactory(tempComponent); + RemapPluginInst *plugin = factory->getRemapPlugin(configPath, 0, nullptr, error); + + THEN("expect it to successfully load") { validateSuccessfulConfigPathTest(plugin, error, effectivePath, runtimePath); } + + teardownConfigPathTest(factory); + } + + WHEN("config is using plugin absolute path") + { + fs::path configPath = searchDir / "subdir" / pluginName; + CHECK(configPath.is_absolute()); /* make sure this is absolute path - this is what we are testing */ + + setupConfigPathTest(configPath, buildPath, tempComponent, effectivePath, runtimePath); + PluginFactoryUnitTest *factory = getFactory(tempComponent); + RemapPluginInst *plugin = factory->getRemapPlugin(configPath, 0, nullptr, error); + + THEN("expect it to successfully load") { validateSuccessfulConfigPathTest(plugin, error, effectivePath, runtimePath); } + + teardownConfigPathTest(factory); + } + + WHEN("config using nonexisting relative plugin file name") + { + fs::path relativeExistingPath = pluginName; + CHECK(relativeExistingPath.is_relative()); + fs::path relativeNonexistingPath("subdir"); + relativeNonexistingPath /= fs::path("nonexisting_plugin.so"); + CHECK(relativeNonexistingPath.is_relative()); + + setupConfigPathTest(relativeExistingPath, buildPath, tempComponent, effectivePath, runtimePath); + PluginFactoryUnitTest *factory = getFactory(tempComponent); + RemapPluginInst *plugin = factory->getRemapPlugin(relativeNonexistingPath, 0, nullptr, error); + + THEN("expect it to fail with appropriate error message") + { + std::string expectedError; + expectedError.append("failed to find plugin '").append(relativeNonexistingPath.string()).append("'"); + CHECK(nullptr == plugin); + CHECK(expectedError == error); + } + + teardownConfigPathTest(factory); + } + + WHEN("config using nonexisting absolute plugin file name") + { + fs::path relativeExistingPath = pluginName; + CHECK(relativeExistingPath.is_relative()); + fs::path absoluteNonexistingPath = searchDir / "subdir" / "nonexisting_plugin.so"; + CHECK(absoluteNonexistingPath.is_absolute()); + + setupConfigPathTest(relativeExistingPath, buildPath, tempComponent, effectivePath, runtimePath); + PluginFactoryUnitTest *factory = getFactory(tempComponent); + RemapPluginInst *plugin = factory->getRemapPlugin(absoluteNonexistingPath, 0, nullptr, error); + + THEN("expect it to fail with appropriate error message") + { + std::string expectedError; + expectedError.append("failed to find plugin '").append(absoluteNonexistingPath.string()).append("'"); + CHECK(nullptr == plugin); + CHECK(expectedError == error); + } + + teardownConfigPathTest(factory); + } + } +} + +SCENARIO("multiple search dirs + multiple or no plugins installed", "[plugin][core]") +{ + GIVEN("multiple search dirs specified for the plugin search") + { + /* Create the directory structure and install plugins */ + fs::path configPath = fs::path("plugin_v1.so"); + fs::path pluginName = fs::path("plugin_v1.so"); + fs::path searchDir1 = sandboxDir / "search1"; + fs::path searchDir2 = sandboxDir / "search2"; + fs::path searchDir3 = sandboxDir / "search3"; + std::vector searchDirs = {searchDir1, searchDir2, searchDir3}; + fs::path effectivePath1 = searchDir1 / configPath; + fs::path effectivePath2 = searchDir2 / configPath; + fs::path effectivePath3 = searchDir3 / configPath; + fs::path runtimePath1 = runtimeDir / effectivePath1.relative_path(); + fs::path runtimePath2 = runtimeDir / effectivePath2.relative_path(); + fs::path runtimePath3 = runtimeDir / effectivePath3.relative_path(); + fs::path pluginBuildPath = fs::current_path() / fs::path("unit-tests/.libs") / pluginName; + + std::string error; + + for (auto searchDir : searchDirs) { + CHECK(fs::create_directories(searchDir, ec)); + fs::copy(pluginBuildPath, searchDir, ec); + } + CHECK(fs::create_directories(runtimeDir, ec)); + + /* Instantiate and initialize a plugin DSO instance. */ + PluginFactoryUnitTest factory(tempComponent); + factory.setRuntimeDir(runtimeRootDir); + for (auto searchDir : searchDirs) { + factory.addSearchDir(searchDir); + } + + CHECK(fs::exists(effectivePath1)); + CHECK(fs::exists(effectivePath2)); + CHECK(fs::exists(effectivePath3)); + + WHEN("loading an existing plugin using its absolute path but the plugin is not located in any of the search dirs") + { + /* Prepare "unregistered" directory containing a valid plugin but not registered with the factory as a search directory */ + fs::path unregisteredDir = sandboxDir / searchDir / "unregistered"; + CHECK(fs::create_directories(unregisteredDir, ec)); + fs::copy(pluginBuildPath, unregisteredDir, ec); + fs::path abEffectivePath = unregisteredDir / pluginName; + fs::path absRuntimePath = runtimeDir / abEffectivePath.relative_path(); + CHECK(abEffectivePath.is_absolute()); + CHECK(fs::exists(abEffectivePath)); + + /* Now use an absolute path containing the unregistered search directory */ + RemapPluginInst *pluginInst = factory.getRemapPlugin(abEffectivePath, 0, nullptr, error); + + THEN("Expect it to successfully load") + { + CHECK(nullptr != pluginInst); + CHECK(error.empty()); + CHECK(abEffectivePath == pluginInst->_plugin.effectivePath()); + CHECK(absRuntimePath == pluginInst->_plugin.runtimePath()); + } + CHECK(fs::remove(sandboxDir, ec)); + } + + WHEN("a valid plugin is found in the first search path") + { + RemapPluginInst *pluginInst = factory.getRemapPlugin(configPath, 0, nullptr, error); + + THEN("Expect it to successfully load the one found in the first search dir and copy it in the runtime dir") + { + CHECK(nullptr != pluginInst); + CHECK(error.empty()); + CHECK(effectivePath1 == pluginInst->_plugin.effectivePath()); + CHECK(runtimePath1 == pluginInst->_plugin.runtimePath()); + } + CHECK(fs::remove(sandboxDir, ec)); + } + + WHEN("the first search dir is missing the plugin but the second search has it") + { + CHECK(fs::remove(effectivePath1, ec)); + RemapPluginInst *pluginInst = factory.getRemapPlugin(configPath, 0, nullptr, error); + + THEN("Expect it to successfully load the one found in the second search dir") + { + CHECK(nullptr != pluginInst); + CHECK(error.empty()); + CHECK(effectivePath2 == pluginInst->_plugin.effectivePath()); + CHECK(runtimePath2 == pluginInst->_plugin.runtimePath()); + } + CHECK(fs::remove(sandboxDir, ec)); + } + + WHEN("the first and second search dir are missing the plugin but the third search has it") + { + CHECK(fs::remove(effectivePath1, ec)); + CHECK(fs::remove(effectivePath2, ec)); + RemapPluginInst *pluginInst = factory.getRemapPlugin(configPath, 0, nullptr, error); + + THEN("Expect it to successfully load the one found in the third search dir") + { + CHECK(nullptr != pluginInst); + CHECK(error.empty()); + CHECK(effectivePath3 == pluginInst->_plugin.effectivePath()); + CHECK(runtimePath3 == pluginInst->_plugin.runtimePath()); + } + CHECK(fs::remove(sandboxDir, ec)); + } + + WHEN("none of the search dirs contains a valid plugin") + { + CHECK(fs::remove(effectivePath1, ec)); + CHECK(fs::remove(effectivePath2, ec)); + CHECK(fs::remove(effectivePath3, ec)); + + THEN("expect the plugin load to fail.") + { + RemapPluginInst *pluginInst = factory.getRemapPlugin(configPath, 0, nullptr, error); + CHECK(nullptr == pluginInst); + CHECK(std::string("failed to find plugin '").append(configPath.string()).append("'") == error); + CHECK_FALSE(fs::exists(runtimePath1)); + CHECK_FALSE(fs::exists(runtimePath2)); + CHECK_FALSE(fs::exists(runtimePath3)); + } + CHECK(fs::remove(sandboxDir, ec)); + } + } +} + +static int +getPluginVersion(const PluginDso &plugin) +{ + std::string error; + void *s = nullptr; + CHECK(plugin.getSymbol("pluginDsoVersionTest", s, error)); + int (*version)() = reinterpret_cast(s); + return version ? version() : -1; +} + +SCENARIO("loading multiple version of the same plugin at the same time", "[plugin][core]") +{ + static fs::path uuid_t1 = fs::path("c71e2bab-90dc-4770-9535-c9304c3de381"); /* UUID at moment t1 */ + static fs::path uuid_t2 = fs::path("c71e2bab-90dc-4770-9535-e7304c3ee732"); /* UUID at moment t2 */ + + fs::path effectivePath_v1; /* expected effective path for DSO v1 */ + fs::path effectivePath_v2; /* expected effective path for DSO v2 */ + fs::path runtimePath_v1; /* expected runtime path for DSO v1 */ + fs::path runtimePath_v2; /* expected runtime path for DSO v2 */ + void *tsRemapInitSym_v1_t1 = nullptr; /* callback address from DSO v1 at moment t1 */ + void *tsRemapInitSym_v1_t2 = nullptr; /* callback address from DSO v1 at moment t2 */ + void *tsRemapInitSym_v2_t2 = nullptr; /* callback address from DSO v2 at moment t2 */ + + std::string error; + std::string error1; + std::string error2; + + fs::path configName = fs::path("plugin.so"); /* use same config name for all following tests */ + fs::path buildPath_v1 = pluginBuildDir / fs::path("plugin_v1.so"); /* DSO v1 */ + fs::path buildPath_v2 = pluginBuildDir / fs::path("plugin_v2.so"); /* DSO v1 */ + + GIVEN("two different versions v1 and v2 of same plugin") + { + WHEN("(1) loading v1, (2) overwriting with v2 and then (3) reloading by using the same plugin name, " + "(*) v1 and v2 DSOs modification time are different (changed)") + { + /* Simulate installing plugin plugin_v1.so (ver 1) as plugin.so and loading it at some point of time t1 */ + setupConfigPathTest(configName, buildPath_v1, uuid_t1, effectivePath_v1, runtimePath_v1, 1556825556); + PluginFactoryUnitTest *factory1 = getFactory(uuid_t1); + RemapPluginInst *plugin_v1 = factory1->getRemapPlugin(configName, 0, nullptr, error1); + plugin_v1->_plugin.getSymbol("TSRemapInit", tsRemapInitSym_v1_t1, error); + + /* Simulate installing plugin plugin_v2.so (v1) as plugin.so and loading it at some point of time t2 */ + /* Note that during the installation plugin_v2.so (v2) is "barberically" overriding the existing plugin.so which was v1 */ + setupConfigPathTest(configName, buildPath_v2, uuid_t2, effectivePath_v2, runtimePath_v2, 1556825557); + PluginFactoryUnitTest *factory2 = getFactory(uuid_t2); + RemapPluginInst *plugin_v2 = factory2->getRemapPlugin(configName, 0, nullptr, error2); + + /* Make sure plugin.so was overriden */ + CHECK(effectivePath_v1 == effectivePath_v2); + + /* Although effective path is the same runtime paths should be different */ + CHECK(runtimePath_v1 != runtimePath_v2); + + THEN("expect both to be successfully loaded and used simultaneously") + { + /* Both loadings should succeed */ + validateSuccessfulConfigPathTest(plugin_v1, error1, effectivePath_v1, runtimePath_v1); + validateSuccessfulConfigPathTest(plugin_v2, error2, effectivePath_v2, runtimePath_v2); + + /* Make sure what we installed and loaded first was v1 and after the plugin reload we run v2 */ + CHECK(1 == getPluginVersion(plugin_v1->_plugin)); + CHECK(2 == getPluginVersion(plugin_v2->_plugin)); + + /* Make sure the symbols we get from the 2 loaded plugins don't yield the same callback function pointer */ + plugin_v1->_plugin.getSymbol("TSRemapInit", tsRemapInitSym_v1_t2, error); + plugin_v2->_plugin.getSymbol("TSRemapInit", tsRemapInitSym_v2_t2, error); + CHECK(nullptr != tsRemapInitSym_v1_t2); + CHECK(nullptr != tsRemapInitSym_v2_t2); + CHECK(tsRemapInitSym_v1_t2 != tsRemapInitSym_v2_t2); + + /* Make sure v1 callback functions addresses did not change for v1 after v2 was loaded */ + CHECK(tsRemapInitSym_v1_t1 == tsRemapInitSym_v1_t2); + } + + teardownConfigPathTest(factory1); + teardownConfigPathTest(factory2); + } + } + + GIVEN("two different versions v1 and v2 of same plugin") + { + WHEN("(1) loading v1, (2) overwriting with v2 and then (3) reloading by using the same plugin name, " + "(*) v1 and v2 DSOs modification time are same (did NOT change)") + { + /* Simulate installing plugin plugin_v1.so (ver 1) as plugin.so and loading it at some point of time t1 */ + setupConfigPathTest(configName, buildPath_v1, uuid_t1, effectivePath_v1, runtimePath_v1, 1556825556); + PluginFactoryUnitTest *factory1 = getFactory(uuid_t1); + RemapPluginInst *plugin_v1 = factory1->getRemapPlugin(configName, 0, nullptr, error1); + + /* Simulate installing plugin plugin_v2.so (v1) as plugin.so and loading it at some point of time t2 */ + /* Note that during the installation plugin_v2.so (v2) is "barberically" overriding the existing plugin.so + which was v1, since the modification time is exactly the same the new v2 plugin would not be loaded and + we should get the same PluginDso address and same effective and runtime paths */ + setupConfigPathTest(configName, buildPath_v2, uuid_t2, effectivePath_v2, runtimePath_v2, 1556825556); + PluginFactoryUnitTest *factory2 = getFactory(uuid_t2); + RemapPluginInst *plugin_v2 = factory2->getRemapPlugin(configName, 0, nullptr, error2); + + /* Make sure plugin.so was overriden */ + CHECK(effectivePath_v1 == effectivePath_v2); + + THEN("expect only v1 plugin to be loaded since the timestamp has not changed") + { + /* Both getRemapPlugin() calls should succeed but only v1 plugin DSO should be used */ + validateSuccessfulConfigPathTest(plugin_v1, error1, effectivePath_v1, runtimePath_v1); + validateSuccessfulConfigPathTest(plugin_v2, error2, effectivePath_v2, runtimePath_v1); + + /* Make sure we ended up with the same DSO object and runtime paths should be same - no new plugin was loaded */ + CHECK(&(plugin_v1->_plugin) == &(plugin_v2->_plugin)); + CHECK(plugin_v1->_plugin.runtimePath() == plugin_v2->_plugin.runtimePath()); + + /* Make sure v2 DSO was NOT loaded both instances should return same v1 version */ + CHECK(1 == getPluginVersion(plugin_v1->_plugin)); + CHECK(1 == getPluginVersion(plugin_v2->_plugin)); + + /* Make sure the symbols we get from the 2 loaded plugins yield the same callback function pointer */ + plugin_v1->_plugin.getSymbol("TSRemapInit", tsRemapInitSym_v1_t2, error); + plugin_v2->_plugin.getSymbol("TSRemapInit", tsRemapInitSym_v2_t2, error); + CHECK(nullptr != tsRemapInitSym_v1_t2); + CHECK(nullptr != tsRemapInitSym_v2_t2); + CHECK(tsRemapInitSym_v1_t2 == tsRemapInitSym_v2_t2); + } + + teardownConfigPathTest(factory1); + teardownConfigPathTest(factory2); + } + } + + /* Since factories share the list of loaded plugins to avoid unnecessary loading of unchanged plugins + * lets check if destroying a factory impacts plugins loaded from another factory */ + GIVEN("configurations with and without plugins") + { + WHEN("loading a configuration without plugins and then reloading configuration with a plugin") + { + /* Simulate configuration without plugins - an unused factory */ + PluginFactoryUnitTest *factory1 = getFactory(uuid_t1); + + /* Now provision and load a plugin using a second factory */ + setupConfigPathTest(configName, buildPath_v2, uuid_t2, effectivePath_v2, runtimePath_v2, 1556825556); + PluginFactoryUnitTest *factory2 = getFactory(uuid_t2); + RemapPluginInst *plugin_v2 = factory2->getRemapPlugin(configName, 0, nullptr, error2); + + THEN("the plugin from the second factory to work") + { + validateSuccessfulConfigPathTest(plugin_v2, error2, effectivePath_v2, runtimePath_v2); + + /* Now delete the first factory and call a plugin from the second factory */ + delete factory1; + CHECK(TSREMAP_NO_REMAP == plugin_v2->_plugin.doRemap(INSTANCE_HANDLER, nullptr, nullptr)); + } + + teardownConfigPathTest(factory2); + } + } +} + +SCENARIO("notifying plugins of config reload", "[plugin][core]") +{ + /* use 2 copies of the same plugin to test */ + fs::path configName1 = fs::path("plugin_testing_calls_1.so"); + fs::path configName2 = fs::path("plugin_testing_calls_2.so"); + fs::path buildPath = pluginBuildDir / fs::path("plugin_testing_calls.so"); + + static fs::path uuid_t1 = fs::path("c71e2bab-90dc-4770-9535-c9304c3de381"); /* UUID at moment t1 */ + static fs::path uuid_t2 = fs::path("c71e2bab-90dc-4770-9535-e7304c3ee732"); /* UUID at moment t2 */ + + fs::path effectivePath1; + fs::path effectivePath2; + fs::path runtimePath1; + fs::path runtimePath2; + + std::string error; + + GIVEN("simple configuration with 1 plugin and 1 factory") + { + WHEN("indicating config reload") + { + /* Simulate configuration without plugins - an unused factory */ + setupConfigPathTest(configName1, buildPath, uuid_t1, effectivePath1, runtimePath1, 1556825556); + PluginFactoryUnitTest *factory1 = getFactory(uuid_t1); + RemapPluginInst *plugin1 = factory1->getRemapPlugin(configName1, 0, nullptr, error); + + /* check if loaded successfully */ + validateSuccessfulConfigPathTest(plugin1, error, effectivePath1, runtimePath1); + + /* Prapare the debug object */ + PluginDebugObject *debugObject = getDebugObject(plugin1->_plugin); + debugObject->clear(); + + THEN("expect 'done' methods to be called for plugin and the instance but not the 'reload config' methods") + { + /* Simulate reloading the config */ + factory1->indicateReload(); + + /* was "done" method called? */ + CHECK(1 == debugObject->doneCalled); + CHECK(1 == debugObject->deleteInstanceCalled); + CHECK(0 == debugObject->reloadConfigCalled); + } + + teardownConfigPathTest(factory1); + } + } + + GIVEN("configuration with 2 plugins loaded by 1 factory") + { + WHEN("indicating config reload") + { + /* Simulate configuration without plugins - an unused factory */ + setupConfigPathTest(configName1, buildPath, uuid_t1, effectivePath1, runtimePath1, 1556825556); + setupConfigPathTest(configName2, buildPath, uuid_t1, effectivePath2, runtimePath2, 1556825556, /* append */ true); + PluginFactoryUnitTest *factory1 = getFactory(uuid_t1); + RemapPluginInst *plugin1 = factory1->getRemapPlugin(configName1, 0, nullptr, error); + RemapPluginInst *plugin2 = factory1->getRemapPlugin(configName2, 0, nullptr, error); + + /* check if loaded successfully */ + validateSuccessfulConfigPathTest(plugin1, error, effectivePath1, runtimePath1); + validateSuccessfulConfigPathTest(plugin2, error, effectivePath2, runtimePath2); + + /* Prapare the debug objects */ + PluginDebugObject *debugObject1 = getDebugObject(plugin1->_plugin); + PluginDebugObject *debugObject2 = getDebugObject(plugin2->_plugin); + debugObject1->clear(); + debugObject2->clear(); + + THEN("expect 'done' methods to be called but not the 'reload config' methods") + { + /* Simulate reloading the config */ + factory1->indicateReload(); + + /* Was "done" method called? */ + CHECK(1 == debugObject1->doneCalled); + CHECK(1 == debugObject1->deleteInstanceCalled); + CHECK(0 == debugObject1->reloadConfigCalled); + CHECK(1 == debugObject2->doneCalled); + CHECK(1 == debugObject2->deleteInstanceCalled); + CHECK(0 == debugObject2->reloadConfigCalled); + } + + teardownConfigPathTest(factory1); + } + } + + GIVEN("configuration with 1 plugin loaded by 2 separate factories") + { + WHEN("indicating config reload") + { + /* Simulate configuration without plugins - an unused factory */ + setupConfigPathTest(configName1, buildPath, uuid_t1, effectivePath1, runtimePath1, 1556825556); + PluginFactoryUnitTest *factory1 = getFactory(uuid_t1); + PluginFactoryUnitTest *factory2 = getFactory(uuid_t2); + RemapPluginInst *plugin1 = factory1->getRemapPlugin(configName1, 0, nullptr, error); + RemapPluginInst *plugin2 = factory2->getRemapPlugin(configName1, 0, nullptr, error); + + /* Prapare the debug objects */ + PluginDebugObject *debugObject1 = getDebugObject(plugin1->_plugin); + PluginDebugObject *debugObject2 = getDebugObject(plugin2->_plugin); + + THEN("expect instance 'done' to be always called, but plugin 'done' called only after destroying one factory") + { + debugObject2->clear(); + factory2->indicateReload(); + CHECK(0 == debugObject2->doneCalled); + CHECK(1 == debugObject2->deleteInstanceCalled); + CHECK(1 == debugObject2->reloadConfigCalled); + + delete factory2; + + debugObject1->clear(); + factory1->indicateReload(); + CHECK(1 == debugObject1->doneCalled); + CHECK(1 == debugObject1->deleteInstanceCalled); + CHECK(0 == debugObject1->reloadConfigCalled); + + delete factory1; + } + + clean(); + } + } +} diff --git a/proxy/http/remap/unit-tests/test_RemapPlugin.cc b/proxy/http/remap/unit-tests/test_RemapPlugin.cc new file mode 100644 index 00000000000..0eedcaf5c84 --- /dev/null +++ b/proxy/http/remap/unit-tests/test_RemapPlugin.cc @@ -0,0 +1,433 @@ +/** @file + + Unit tests for a class that deals with remap plugins + + @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. + + */ + +#define CATCH_CONFIG_MAIN /* include main function */ +#include /* catch unit-test framework */ +#include /* ofstream */ +#include + +#include "plugin_testing_common.h" +#include "../RemapPluginInfo.h" + +thread_local PluginThreadContext *pluginThreadContext; + +static void *INSTANCE_HANDLER = (void *)789; +std::error_code ec; + +/* Some plugin context pointers used for unit testing */ +// static const PluginThreadContext *PLUGIN_INIT_CONTEXT_CUR = (PluginThreadContext *)1; +// static const PluginThreadContext *PLUGIN_INIT_CONTEXT_NEW_V1 = (PluginThreadContext *)2; +// static const PluginThreadContext *PLUGIN_INIT_CONTEXT_NEW_V2 = (PluginThreadContext *)3; + +/* A temp sandbox to play with our toys used for all fun with this test-bench */ +static fs::path tmpDir = fs::canonical(fs::temp_directory_path(), ec); + +/* The following are paths that are used commonly in the unit-tests */ +static fs::path sandboxDir = tmpDir / "sandbox"; +static fs::path runtimeDir = sandboxDir / "runtime"; +static fs::path searchDir = sandboxDir / "search"; +static fs::path pluginBuildDir = fs::current_path() / "unit-tests/.libs"; + +void +clean() +{ + fs::remove(sandboxDir, ec); +} + +/* Mock used only to make unit testing convenient to check if callbacks are really called and check errors */ +class RemapPluginUnitTest : public RemapPluginInfo +{ +public: + RemapPluginUnitTest(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath) + : RemapPluginInfo(configPath, effectivePath, runtimePath) + { + } + std::string + getError(const char *required, const char *requiring = nullptr) + { + return missingRequiredSymbolError(_configPath.string(), required, requiring); + } + + PluginDebugObject * + getDebugObject() + { + std::string error; /* ignore the error, return nullptr if symbol not defined */ + void *address = nullptr; + getSymbol("getPluginDebugObjectTest", address, error); + GetPluginDebugObjectFunction *getObject = reinterpret_cast(address); + if (getObject) { + PluginDebugObject *object = reinterpret_cast(getObject()); + return object; + } else { + return nullptr; + } + } +}; + +RemapPluginUnitTest * +setupSandBox(const fs::path configPath) +{ + std::string error; + clean(); + + /* Create the directory structure and install plugins */ + CHECK(fs::create_directories(searchDir, ec)); + fs::copy(pluginBuildDir / configPath, searchDir, ec); + CHECK(fs::create_directories(runtimeDir, ec)); + + fs::path effectivePath = searchDir / configPath; + fs::path runtimePath = runtimeDir / configPath; + fs::path pluginBuildPath = pluginBuildDir / configPath; + + /* Instantiate and initialize a plugin DSO instance. */ + RemapPluginUnitTest *plugin = new RemapPluginUnitTest(configPath, effectivePath, runtimePath); + + return plugin; +} + +bool +loadPlugin(RemapPluginUnitTest *plugin, std::string &error, PluginDebugObject *&debugObject) +{ + bool result = plugin->load(error); + debugObject = plugin->getDebugObject(); + return result; +} + +void +cleanupSandBox(RemapPluginInfo *plugin) +{ + delete plugin; + clean(); +} + +SCENARIO("loading remap plugins", "[plugin][core]") +{ + std::string error; + PluginDebugObject *debugObject = nullptr; + + GIVEN("a plugin which has only minimum required call back functions") + { + fs::path pluginConfigPath = fs::path("plugin_required_cb.so"); + RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath); + + WHEN("loading") + { + bool result = loadPlugin(plugin, error, debugObject); + + THEN("expect it to successfully load") + { + CHECK(true == result); + CHECK(error.empty()); + } + cleanupSandBox(plugin); + } + } + + GIVEN("a plugin which is missing the plugin TSREMAP_FUNCNAME_INIT function") + { + fs::path pluginConfigPath = fs::path("plugin_missing_init.so"); + RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath); + + WHEN("loading") + { + bool result = loadPlugin(plugin, error, debugObject); + + THEN("expect it to successfully load") + { + CHECK_FALSE(result); + CHECK(error == plugin->getError(TSREMAP_FUNCNAME_INIT)); + } + cleanupSandBox(plugin); + } + } + + GIVEN("a plugin which is missing the TSREMAP_FUNCNAME_DO_REMAP function") + { + fs::path pluginConfigPath = fs::path("plugin_missing_doremap.so"); + RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath); + + WHEN("loading") + { + bool result = loadPlugin(plugin, error, debugObject); + + THEN("expect it to fail") + { + CHECK_FALSE(result); + CHECK(error == plugin->getError(TSREMAP_FUNCNAME_DO_REMAP)); + } + cleanupSandBox(plugin); + } + } + + GIVEN("a plugin which has TSREMAP_FUNCNAME_NEW_INSTANCE but is missing the TSREMAP_FUNCNAME_DELETE_INSTANCE function") + { + fs::path pluginConfigPath = fs::path("plugin_missing_deleteinstance.so"); + RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath); + + WHEN("loading") + { + bool result = loadPlugin(plugin, error, debugObject); + + THEN("expect it to fail") + { + CHECK_FALSE(result); + CHECK(error == plugin->getError(TSREMAP_FUNCNAME_DELETE_INSTANCE, TSREMAP_FUNCNAME_NEW_INSTANCE)); + } + cleanupSandBox(plugin); + } + } + + GIVEN("a plugin which has TSREMAP_FUNCNAME_DELETE_INSTANCE but is missing the TSREMAP_FUNCNAME_NEW_INSTANCE function") + { + fs::path pluginConfigPath = fs::path("plugin_missing_newinstance.so"); + RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath); + + WHEN("loading") + { + bool result = loadPlugin(plugin, error, debugObject); + + THEN("expect it to fail") + { + CHECK_FALSE(result); + CHECK(error == plugin->getError(TSREMAP_FUNCNAME_NEW_INSTANCE, TSREMAP_FUNCNAME_DELETE_INSTANCE)); + } + cleanupSandBox(plugin); + } + } +} + +void +prepCallTest(bool toFail, PluginDebugObject *debugObject) +{ + debugObject->clear(); + debugObject->fail = toFail; // Tell the mock init to succeed or succeed. +} + +void +checkCallTest(bool shouldHaveFailed, bool result, const std::string &error, std::string &expectedError, int &called) +{ + CHECK(1 == called); // Init was called. + if (shouldHaveFailed) { + CHECK(false == result); + CHECK(error == expectedError); // Appropriate error was returned. + } else { + CHECK(true == result); // Init succesfull - returned TS_SUCCESS. + CHECK(error.empty()); // No error was returned. + } +} + +SCENARIO("invoking plugin init", "[plugin][core]") +{ + std::string error; + PluginDebugObject *debugObject = nullptr; + + GIVEN("plugin init function") + { + fs::path pluginConfigPath = fs::path("plugin_testing_calls.so"); + RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath); + + bool result = loadPlugin(plugin, error, debugObject); + CHECK(true == result); + + WHEN("init succeeds") + { + prepCallTest(/* toFail */ false, debugObject); + + result = plugin->init(error); + + THEN("expect init to be called, success code and no error to be returned") + { + std::string expectedError; + + checkCallTest(/* shouldHaveFailed */ false, result, error, expectedError, debugObject->initCalled); + } + cleanupSandBox(plugin); + } + + WHEN("init fails") + { + prepCallTest(/* toFail */ true, debugObject); + + result = plugin->init(error); + + THEN("expect init to be called, failure code and an error to be returned") + { + std::string expectedError; + expectedError.assign("failed to initialize plugin ").append(pluginConfigPath.string()).append(": Init failed"); + + checkCallTest(/* shouldHaveFailed */ true, result, error, expectedError, debugObject->initCalled); + } + cleanupSandBox(plugin); + } + } +} + +SCENARIO("invoking plugin instance init", "[plugin][core]") +{ + std::string error; + PluginDebugObject *debugObject = nullptr; + void *ih = nullptr; // Instance handler pointer. + + /* a sample test set of parameters */ + static const char *args[] = {"arg1", "arg2", "arg3"}; + static char **ARGV = const_cast(args); + static char ARGC = sizeof ARGV; + + GIVEN("an instance init function") + { + fs::path pluginConfigPath = fs::path("plugin_testing_calls.so"); + RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath); + + bool result = loadPlugin(plugin, error, debugObject); + CHECK(true == result); + + WHEN("instance init succeeds") + { + prepCallTest(/* toFail */ false, debugObject); + debugObject->input_ih = INSTANCE_HANDLER; /* this is what the plugin instance init will return */ + + result = plugin->initInstance(ARGC, ARGV, &ih, error); + + THEN("expect init to be called successfully with no error and expected instance handler") + { + std::string expectedError; + + checkCallTest(/* shouldHaveFailed */ false, result, error, expectedError, debugObject->initInstanceCalled); + + /* Verify expected handler */ + CHECK(INSTANCE_HANDLER == ih); + /* Plugin received the parameters that we passed */ + CHECK(ARGC == debugObject->argc); + CHECK(ARGV == debugObject->argv); + for (int i = 0; i < 3; i++) { + CHECK(0 == strcmp(ARGV[i], debugObject->argv[i])); + } + } + cleanupSandBox(plugin); + } + + WHEN("instance init fails") + { + prepCallTest(/* toFail */ true, debugObject); + + result = plugin->initInstance(ARGC, ARGV, &ih, error); + + THEN("expect init to be called but failed with expected error and no instance handler") + { + std::string expectedError; + expectedError.assign("failed to create instance for plugin ").append(pluginConfigPath.string()).append(": Init failed"); + + checkCallTest(/* shouldHaveFailed */ true, result, error, expectedError, debugObject->initInstanceCalled); + + /* Ideally instance handler should not be touched in case of failure */ + CHECK(nullptr == ih); + /* Plugin received the parameters that we passed */ + CHECK(ARGC == debugObject->argc); + CHECK(ARGV == debugObject->argv); + for (int i = 0; i < 3; i++) { + CHECK(0 == strcmp(ARGV[i], debugObject->argv[i])); + } + } + cleanupSandBox(plugin); + } + } +} + +SCENARIO("unloading the plugin", "[plugin][core]") +{ + std::string error; + PluginDebugObject *debugObject = nullptr; + + GIVEN("a 'done' function") + { + fs::path pluginConfigPath = fs::path("plugin_testing_calls.so"); + RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath); + + bool result = loadPlugin(plugin, error, debugObject); + CHECK(true == result); + + WHEN("'done' is called") + { + debugObject->clear(); + + plugin->done(); + + THEN("expect it to run") { CHECK(1 == debugObject->doneCalled); } + cleanupSandBox(plugin); + } + } + + GIVEN("a 'delete_instance' function") + { + fs::path pluginConfigPath = fs::path("plugin_testing_calls.so"); + RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath); + + bool result = loadPlugin(plugin, error, debugObject); + CHECK(true == result); + + WHEN("'delete_instance' is called") + { + debugObject->clear(); + + plugin->doneInstance(INSTANCE_HANDLER); + + THEN("expect it to run and receive the right instance handler") + { + CHECK(1 == debugObject->deleteInstanceCalled); + CHECK(INSTANCE_HANDLER == debugObject->ih); + } + cleanupSandBox(plugin); + } + } +} + +SCENARIO("config reload", "[plugin][core]") +{ + std::string error; + PluginDebugObject *debugObject = nullptr; + + GIVEN("a 'config reload' callback function") + { + fs::path pluginConfigPath = fs::path("plugin_testing_calls.so"); + RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath); + + bool result = loadPlugin(plugin, error, debugObject); + CHECK(true == result); + + WHEN("'config reload' is called") + { + debugObject->clear(); + + plugin->indicateReload(); + + THEN("expect it to run") { CHECK(1 == debugObject->reloadConfigCalled); } + cleanupSandBox(plugin); + } + } +} diff --git a/src/traffic_server/InkAPI.cc b/src/traffic_server/InkAPI.cc index 316282b519e..c0392a4ab36 100644 --- a/src/traffic_server/InkAPI.cc +++ b/src/traffic_server/InkAPI.cc @@ -1000,6 +1000,7 @@ INKContInternal::INKContInternal() m_closed(1), m_deletable(0), m_deleted(0), + m_context(0), m_free_magic(INKCONT_INTERN_MAGIC_ALIVE) { } @@ -1012,18 +1013,20 @@ INKContInternal::INKContInternal(TSEventFunc funcp, TSMutex mutexp) m_closed(1), m_deletable(0), m_deleted(0), + m_context(0), m_free_magic(INKCONT_INTERN_MAGIC_ALIVE) { SET_HANDLER(&INKContInternal::handle_event); } void -INKContInternal::init(TSEventFunc funcp, TSMutex mutexp) +INKContInternal::init(TSEventFunc funcp, TSMutex mutexp, void *context) { SET_HANDLER(&INKContInternal::handle_event); mutex = (ProxyMutex *)mutexp; m_event_func = funcp; + m_context = context; } void @@ -1094,7 +1097,11 @@ INKContInternal::handle_event(int event, void *edata) Debug("plugin", "INKCont Deletable but not deleted %d", m_event_count); } } else { - int retval = m_event_func((TSCont)this, (TSEvent)event, edata); + /* set the plugin context */ + auto *previousContext = pluginThreadContext; + pluginThreadContext = reinterpret_cast(m_context); + int retval = m_event_func((TSCont)this, (TSEvent)event, edata); + pluginThreadContext = previousContext; if (edata && event == EVENT_INTERVAL) { Event *e = reinterpret_cast(edata); if (e->period != 0) { @@ -4369,6 +4376,8 @@ TSMgmtSourceGet(const char *var_name, TSMgmtSource *source) // //////////////////////////////////////////////////////////////////// +extern thread_local PluginThreadContext *pluginThreadContext; + TSCont TSContCreate(TSEventFunc funcp, TSMutex mutexp) { @@ -4377,9 +4386,13 @@ TSContCreate(TSEventFunc funcp, TSMutex mutexp) sdk_assert(sdk_sanity_check_mutex(mutexp) == TS_SUCCESS); } + if (pluginThreadContext) { + pluginThreadContext->acquire(); + } + INKContInternal *i = INKContAllocator.alloc(); - i->init(funcp, mutexp); + i->init(funcp, mutexp, pluginThreadContext); return (TSCont)i; } @@ -4390,6 +4403,10 @@ TSContDestroy(TSCont contp) INKContInternal *i = (INKContInternal *)contp; + if (i->m_context) { + reinterpret_cast(i->m_context)->release(); + } + i->destroy(); } @@ -5951,6 +5968,8 @@ TSHttpTxnReenable(TSHttpTxn txnp, TSEvent event) } } +TSReturnCode TSHttpArgIndexNameLookup(UserArg::Type type, const char *name, int *arg_idx, const char **description); + TSReturnCode TSHttpArgIndexReserve(UserArg::Type type, const char *name, const char *description, int *ptr_idx) { @@ -5958,7 +5977,19 @@ TSHttpArgIndexReserve(UserArg::Type type, const char *name, const char *descript sdk_assert(sdk_sanity_check_null_ptr(name) == TS_SUCCESS); sdk_assert(0 <= type && type < UserArg::Type::COUNT); - int idx = UserArgIdx[type]++; + int idx; + + /* Since this function is meant to be called during plugin initialization we could end up "leaking" indices during plugins reload. + * Make sure we allocate 1 index per name, also current TSHttpArgIndexNameLookup() implementation assumes 1-1 relationship as + * well. */ + const char *desc; + if (TS_SUCCESS == TSHttpArgIndexNameLookup(type, name, &idx, &desc)) { + // Found existing index. + *ptr_idx = idx; + return TS_SUCCESS; + } + + idx = UserArgIdx[type]++; int limit = (type == UserArg::Type::VCONN) ? TS_VCONN_MAX_USER_ARG : TS_HTTP_MAX_USER_ARG; if (idx < limit) { @@ -6609,11 +6640,15 @@ TSVConnCreate(TSEventFunc event_funcp, TSMutex mutexp) // TODO: probably don't need this if memory allocations fails properly sdk_assert(sdk_sanity_check_mutex(mutexp) == TS_SUCCESS); + if (pluginThreadContext) { + pluginThreadContext->acquire(); + } + INKVConnInternal *i = INKVConnAllocator.alloc(); sdk_assert(sdk_sanity_check_null_ptr((void *)i) == TS_SUCCESS); - i->init(event_funcp, mutexp); + i->init(event_funcp, mutexp, pluginThreadContext); return reinterpret_cast(i); } diff --git a/src/tscore/ts_file.cc b/src/tscore/ts_file.cc index 3467176c2e2..d50fae319d4 100644 --- a/src/tscore/ts_file.cc +++ b/src/tscore/ts_file.cc @@ -20,6 +20,8 @@ #include "tscore/ts_file.h" #include +#include +#include namespace ts { @@ -56,12 +58,220 @@ namespace file return zret; } + path + temp_directory_path() + { + /* ISO/IEC 9945 (POSIX): The path supplied by the first environment variable found in the list TMPDIR, TMP, TEMP, TEMPDIR. + * If none of these are found, "/tmp" */ + char const *folder = nullptr; + if ((nullptr == (folder = getenv("TMPDIR"))) && (nullptr == (folder = getenv("TMP"))) && + (nullptr == (folder = getenv("TEMPDIR")))) { + folder = "/tmp"; + } + return path(folder); + } + + path + current_path() + { + char cwd[PATH_MAX]; + if (::getcwd(cwd, sizeof(cwd)) != NULL) { + return path(cwd); + } + return path(); + } + + path + canonical(const path &p, std::error_code &ec) + { + if (p.empty()) { + ec = std::error_code(EINVAL, std::system_category()); + return path(); + } + + char buf[PATH_MAX + 1]; + char *res = ::realpath(p.c_str(), buf); + if (res) { + ec = std::error_code(); + return path(res); + } + + ec = std::error_code(errno, std::system_category()); + return path(); + } + + bool + exists(const path &p) + { + std::error_code ec; + status(p, ec); + return !(ec && ENOENT == ec.value()); + } + + static bool + do_mkdir(const path &p, std::error_code &ec, mode_t mode) + { + struct stat st; + if (stat(p.c_str(), &st) != 0) { + if (mkdir(p.c_str(), mode) != 0 && errno != EEXIST) { + ec = std::error_code(errno, std::system_category()); + return false; + } + } else if (!S_ISDIR(st.st_mode)) { + ec = std::error_code(ENOTDIR, std::system_category()); + return false; + } + return true; + } + + bool + create_directories(const path &p, std::error_code &ec, mode_t mode) noexcept + { + if (p.empty()) { + ec = std::error_code(EINVAL, std::system_category()); + return false; + } + + bool result = false; + ec = std::error_code(); + + size_t pos = 0; + std::string token; + while ((pos = p.string().find_first_of(p.preferred_separator, pos)) != std::string::npos) { + token = p.string().substr(0, pos); + if (!token.empty()) { + result = do_mkdir(path(token), ec, mode); + } + pos = pos + sizeof(p.preferred_separator); + } + + if (result) { + result = do_mkdir(p, ec, mode); + } + return result; + } + + bool + copy(const path &from, const path &to, std::error_code &ec) + { + static int BUF_SIZE = 65536; + FILE *src, *dst; + size_t in, out; + char buf[BUF_SIZE]; + int bufsize = BUF_SIZE; + + if (from.empty() || to.empty()) { + ec = std::error_code(EINVAL, std::system_category()); + return false; + } + + ec = std::error_code(); + + std::error_code err; + path final_to; + file_status s = status(to, err); + if (!(err && ENOENT == err.value()) && is_dir(s)) { + const size_t last_slash_idx = from.string().find_last_of(from.preferred_separator); + std::string filename = from.string().substr(last_slash_idx + 1); + final_to = to / filename; + } else { + final_to = to; + } + + if (nullptr == (src = fopen(from.c_str(), "r"))) { + ec = std::error_code(errno, std::system_category()); + return false; + } + if (nullptr == (dst = fopen(final_to.c_str(), "w"))) { + ec = std::error_code(errno, std::system_category()); + return false; + } + + while (1) { + in = fread(buf, 1, bufsize, src); + if (0 == in) + break; + out = fwrite(buf, 1, in, dst); + if (0 == out) + break; + } + + fclose(src); + fclose(dst); + + return true; + } + + static bool + remove_path(const path &p, std::error_code &ec) + { + DIR *dir; + struct dirent *entry; + bool res = true; + std::error_code err; + + file_status s = status(p, err); + if (err && ENOENT == err.value()) { + // file/dir does not exist + return false; + } else if (is_regular_file(s)) { + // regular file, try to remove it! + if (unlink(p.c_str()) != 0) { + ec = std::error_code(errno, std::system_category()); + res = false; + } + return res; + } else if (!is_dir(s)) { + // not a directory + ec = std::error_code(ENOTDIR, std::system_category()); + return false; + } + + // recursively remove nested files and directories + if (nullptr == (dir = opendir(p.c_str()))) { + ec = std::error_code(errno, std::system_category()); + return false; + } + + while (nullptr != (entry = readdir(dir))) { + if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) { + continue; + } + + remove_path(p / entry->d_name, ec); + } + + if (0 != rmdir(p.c_str())) { + ec = std::error_code(errno, std::system_category()); + } + + closedir(dir); + return true; + } + + bool + remove(const path &p, std::error_code &ec) + { + if (p.empty()) { + ec = std::error_code(EINVAL, std::system_category()); + return false; + } + + ec = std::error_code(); + return remove_path(p, ec); + } // namespace file + int file_type(const file_status &fs) { return fs._stat.st_mode & S_IFMT; } + time_t + modification_time(const file_status &fs) + { + return fs._stat.st_mtime; + } uintmax_t file_size(const file_status &fs) { diff --git a/src/tscore/unit_tests/test_ts_file.cc b/src/tscore/unit_tests/test_ts_file.cc index 58366def626..603ded33653 100644 --- a/src/tscore/unit_tests/test_ts_file.cc +++ b/src/tscore/unit_tests/test_ts_file.cc @@ -22,6 +22,7 @@ */ #include +#include /* ofstream */ #include "tscore/ts_file.h" #include "../../../tests/include/catch.hpp" @@ -67,3 +68,195 @@ TEST_CASE("ts_file_io", "[libts][ts_file_io]") REQUIRE(ec.value() == 2); REQUIRE(ts::file::is_readable(file) == false); } + +TEST_CASE("ts_file::path::parent_path", "[libts][fs_file]") +{ + CHECK(ts::file::path("/").parent_path() == path("/")); + CHECK(ts::file::path("/absolute/path/file.txt").parent_path() == ts::file::path("/absolute/path")); + CHECK(ts::file::path("/absolute/path/.").parent_path() == ts::file::path("/absolute/path")); + + CHECK(ts::file::path("relative/path/file.txt").parent_path() == ts::file::path("relative/path")); + CHECK(ts::file::path("relative/path/.").parent_path() == ts::file::path("relative/path")); + CHECK(ts::file::path(".").parent_path() == ts::file::path("")); +} + +static std::string +setenvvar(const std::string &name, const std::string &value) +{ + std::string saved; + if (nullptr != getenv(name.c_str())) { + saved.assign(value); + } + + if (!value.empty()) { + setenv(name.c_str(), value.c_str(), 1); + } else { + unsetenv(name.c_str()); + } + + return saved; +} + +TEST_CASE("ts_file::path::temp_directory_path", "[libts][fs_file]") +{ + // Clean all temp dir env variables. + std::string s1 = setenvvar("TMPDIR", std::string()); + std::string s2 = setenvvar("TEMPDIR", std::string()); + std::string s3 = setenvvar("TMP", std::string()); + std::string s; + + // If nothing defined return "/tmp" + CHECK(ts::file::temp_directory_path() == ts::file::path("/tmp")); + + // TMPDIR defined. + s = setenvvar("TMPDIR", "/temp_dirname1"); + CHECK(ts::file::temp_directory_path() == ts::file::path("/temp_dirname1")); + setenvvar("TMPDIR", s); + + // TEMPDIR + s = setenvvar("TEMPDIR", "/temp_dirname"); + CHECK(ts::file::temp_directory_path() == ts::file::path("/temp_dirname")); + // TMP defined, it should take precedence over TEMPDIR. + s = setenvvar("TMP", "/temp_dirname1"); + CHECK(ts::file::temp_directory_path() == ts::file::path("/temp_dirname1")); + // TMPDIR defined, it should take precedence over TMP. + s = setenvvar("TMPDIR", "/temp_dirname2"); + CHECK(ts::file::temp_directory_path() == ts::file::path("/temp_dirname2")); + setenvvar("TMPDIR", s); + setenvvar("TMP", s); + setenvvar("TEMPDIR", s); + + // Restore all temp dir env variables to their previous state. + setenvvar("TMPDIR", s1); + setenvvar("TEMPDIR", s2); + setenvvar("TMP", s3); +} + +TEST_CASE("ts_file::path::create_directories", "[libts][fs_file]") +{ + std::error_code ec; + path tempdir = ts::file::temp_directory_path(); + + CHECK_FALSE(ts::file::create_directories(path(), ec)); + CHECK(ec.value() == EINVAL); + + path testdir1 = tempdir / "dir1"; + CHECK(ts::file::create_directories(testdir1, ec)); + CHECK(ts::file::exists(testdir1)); + + path testdir2 = testdir1 / "dir2"; + CHECK(ts::file::create_directories(testdir1, ec)); + CHECK(ts::file::exists(testdir1)); + + // Cleanup + CHECK(ts::file::remove(testdir1, ec)); + CHECK_FALSE(ts::file::exists(testdir1)); +} + +TEST_CASE("ts_file::path::remove", "[libts][fs_file]") +{ + std::error_code ec; + path tempdir = ts::file::temp_directory_path(); + + CHECK_FALSE(ts::file::remove(path(), ec)); + CHECK(ec.value() == EINVAL); + + path testdir1 = tempdir / "dir1"; + path testdir2 = testdir1 / "dir2"; + path file1 = testdir2 / "test.txt"; + + // Simple creation and removal of a directory /tmp/dir1 + CHECK(ts::file::create_directories(testdir1, ec)); + CHECK(ts::file::exists(testdir1)); + CHECK(ts::file::remove(testdir1, ec)); + CHECK_FALSE(ts::file::exists(testdir1)); + + // Create /tmp/dir1/dir2 and remove /tmp/dir1/dir2 => /tmp/dir1 should exist + CHECK(ts::file::create_directories(testdir2, ec)); + CHECK(ts::file::remove(testdir2, ec)); + CHECK(ts::file::exists(testdir1)); + + // Create a file, remove it, test if exists and then attempting to remove it again should fail. + CHECK(ts::file::create_directories(testdir2, ec)); + std::ofstream file(file1.string()); + file << "Simple test file"; + file.close(); + CHECK(ts::file::exists(file1)); + CHECK(ts::file::remove(file1, ec)); + CHECK_FALSE(ts::file::exists(file1)); + CHECK_FALSE(ts::file::remove(file1, ec)); + + // Clean up. + CHECK(ts::file::remove(testdir1, ec)); + CHECK_FALSE(ts::file::exists(testdir1)); +} + +TEST_CASE("ts_file::path::canonical", "[libts][fs_file]") +{ + std::error_code ec; + path tempdir = ts::file::canonical(ts::file::temp_directory_path(), ec); + path testdir1 = tempdir / "dir1"; + path testdir2 = testdir1 / "dir2"; + path testdir3 = testdir2 / "dir3"; + path unorthodox = testdir3 / path("..") / path("..") / "dir2"; + + // Invalid empty path. + CHECK(path() == ts::file::canonical(path(), ec)); + CHECK(ec.value() == EINVAL); + + // Fail if directory does not exist + CHECK(path() == ts::file::canonical(unorthodox, ec)); + CHECK(ec.value() == ENOENT); + + // Create the dir3 and test again + CHECK(create_directories(testdir3, ec)); + CHECK(ts::file::exists(testdir3)); + CHECK(ts::file::exists(testdir2)); + CHECK(ts::file::exists(testdir1)); + CHECK(ts::file::exists(unorthodox)); + CHECK(ts::file::canonical(unorthodox, ec) == testdir2); + CHECK(ec.value() == 0); + + // Cleanup + CHECK(ts::file::remove(testdir1, ec)); + CHECK_FALSE(ts::file::exists(testdir1)); +} + +TEST_CASE("ts_file::path::copy", "[libts][fs_file]") +{ + std::error_code ec; + path tempdir = ts::file::temp_directory_path(); + path testdir1 = tempdir / "dir1"; + path testdir2 = testdir1 / "dir2"; + path file1 = testdir2 / "test1.txt"; + path file2 = testdir2 / "test2.txt"; + + // Invalid empty path, both to and from parameters. + CHECK_FALSE(ts::file::copy(path(), path(), ec)); + CHECK(ec.value() == EINVAL); + + CHECK(ts::file::create_directories(testdir2, ec)); + std::ofstream file(file1.string()); + file << "Simple test file"; + file.close(); + CHECK(ts::file::exists(file1)); + + // Invalid empty path, now from parameter is ok but to is empty + CHECK_FALSE(ts::file::copy(file1, path(), ec)); + CHECK(ec.value() == EINVAL); + + // successfull copy: "to" is directory + CHECK(ts::file::copy(file1, testdir2, ec)); + CHECK(ec.value() == 0); + + // successful copy: "to" is file + CHECK(ts::file::copy(file1, file2, ec)); + CHECK(ec.value() == 0); + + // Compare the content + CHECK(ts::file::load(file1, ec) == ts::file::load(file2, ec)); + + // Cleanup + CHECK(ts::file::remove(testdir1, ec)); + CHECK_FALSE(ts::file::exists(testdir1)); +} \ No newline at end of file