From 73fc0a2d0975ef7991dc955bb900bc5198abf249 Mon Sep 17 00:00:00 2001 From: melektron Date: Thu, 2 Nov 2023 20:53:40 +0100 Subject: [PATCH 01/50] changed some library dependency systems and wrote a down a specification of the goals of the msglink protocol --- include/el/cxxversions.h | 4 +- include/el/hashable_path.hpp | 4 +- include/el/jsonutils.hpp | 8 ++-- include/el/msglink/README.md | 73 +++++++++++++++++++++++++++++++++++ include/el/msglink/server.hpp | 19 +++++++++ include/el/retcode.hpp | 2 +- include/el/struct_proxy.hpp | 4 +- include/el/strutil.hpp | 4 +- include/el/types.hpp | 4 +- include/el/universal.hpp | 4 +- 10 files changed, 109 insertions(+), 17 deletions(-) create mode 100644 include/el/msglink/README.md create mode 100644 include/el/msglink/server.hpp diff --git a/include/el/cxxversions.h b/include/el/cxxversions.h index ebc4e94..d42705c 100644 --- a/include/el/cxxversions.h +++ b/include/el/cxxversions.h @@ -1,6 +1,6 @@ /* -ELEKTRON © 2022 -Written by Matteo Reiter +ELEKTRON © 2022 - now +Written by melektron www.elektron.work 26.11.22, 18:25 All rights reserved. diff --git a/include/el/hashable_path.hpp b/include/el/hashable_path.hpp index 2e6ed58..05b1c81 100644 --- a/include/el/hashable_path.hpp +++ b/include/el/hashable_path.hpp @@ -1,6 +1,6 @@ /* -ELEKTRON © 2022 -Written by Matteo Reiter +ELEKTRON © 2022 - now +Written by melektron www.elektron.work 20.11.22, 21:43 All rights reserved. diff --git a/include/el/jsonutils.hpp b/include/el/jsonutils.hpp index fb766c9..0a585a0 100644 --- a/include/el/jsonutils.hpp +++ b/include/el/jsonutils.hpp @@ -1,6 +1,6 @@ /* -ELEKTRON © 2022 -Written by Matteo Reiter +ELEKTRON © 2022 - now +Written by melektron www.elektron.work 19.11.22, 23:57 All rights reserved. @@ -11,12 +11,12 @@ LICENSE file in the root directory of this source tree. utility functions for the nlohmann json library. This depends on the nlohmann::json library -which must be includable like this: "#include " +which must be includeable like this: "#include " */ #pragma once -#include +#include #include #include "cxxversions.h" diff --git a/include/el/msglink/README.md b/include/el/msglink/README.md new file mode 100644 index 0000000..594d172 --- /dev/null +++ b/include/el/msglink/README.md @@ -0,0 +1,73 @@ +# Message Link + +Message link (msglink) is a custom networking protocol based on websockets that can be used to easily exchange data, events and commands between multiple systems over the network. + + +## Principles + +msglink tries to conform to these core principals: + +- Simple API: It should be easy to start a msglink server and define client handling functionality without needing to decide between a thousand different transport protocols and other config +- Clean and user-friendly API definition: event definition and use should be as simply as possible leveraging language specific features to abstract away the implementation details. There should be as little repeated boiler-plate as possible. +- Platform independence: the protocol should be usable on any OS or system using multiple programming languages (protocol features should not depend on special language functions) +- Simple state management: The user program shall not be required to manage any state related to the communication such as channel subscriptions or any of that stuff. A user should be able to say "I want this and this and this data" to the library and it must make sure these requests are fulfilled even after reconnects and similar incidents. + + +## Features + +msglink is similar to Socket.IO, except it provides additional functionality extending the simple event system. + +- Strict types and data validation +- Type-Defined events +- Data subscriptions +- Remote Procedure calls (with return data) + + +## Strict types and data validation + +One of the most annoying and often repetitive coding tasks when it comes to network communication is serialization and deserialization (especially when communicating between different languages and platforms). At this point, JSON has become the defacto standard for most general purpose web APIs and many high-level communication protocols (at least where ever maximum performance and throughput is not the most important point). While JSON is a nice way to encode data both readable with relative ease by humans and computers, it is still a ton of work to encode and decode data in your application from/to the language-internal format. + +Most modern programming languages provide some sort of native or third party support for serializing and deserializing JSON, but the problem with JSON data received via the network is, that it is fully dynamic. You cannot be sure at the time of development what the JSON object will contain. So after parsing, it is required to manually go through the JSON object, checking that all it's fields match the type and restrictions required by your program. Then the data should ideally be extracted into some form of language-specific format like a struct in C/C++. + +Luckily there are libraries that can help us simplify this task. The Python library PyDantic provides a way to elegantly define a JSON property's the type, value restrictions and optional (de)serializer functions and enables simple parsing and automatic validation of incoming data. Since everything is represented by classes, static type checkers can see the datatype of properties and provide excellent editor support. In Swift, the Codable protocol is natively supported and provides similar functionality. In some languages like C++ this is not quite as simple to represent but we can still simplify the process. + +msgpack tries to implement and require these type definitions natively in it's implementation libraries, each in the style and with the features supported by the respective programming language. This way, every event has a clearly defined data structure passed along the event. Event listeners can access incoming data in the language-native format and rely on the fact that they receive what they expect. Event emitters on the other hand can pass data in the language-native format and will be forced to only emit valid data for a specific event. + + +## Type-defined events + +Traditionally, events have been identified by a simple string, it's name. There is nothing inherently wrong with this approach, but it introduces additional places to make mistakes. One may want to listen to the same event in multiple places of a program but might make a typo when identifying the event name or forget to update one listener after changing the name. + +Language features such as enums, constants or TS literal types will solve this issue. However, msglink aims to integrate this as a requirement in it's implementation. This goes hand-in-hand nicely with the previous point, strict types. Every event has to have a defined and validatable data type which also defines the name of the event it is associated with. After defining it once, this event type can be used everywhere in the program (details depend on language implementation) to refer to this specific event, there cannot be typos in the event name and it is impossible emit events with the wrong data structure. + + +## Data subscriptions + +Everything up to this point has just been library implementation specific improvements to the API of Socket.IO, but msglink also provides some additional features that are completely new to solve common problems in a repeatable way. + +Data subscriptions basically allow a communication party to "subscribe" to a certain data source in THE OTHER PARTY's ENVIRONMENT. At first, this may sound similar to event listeners, however there is a key difference. In the usual event system, party A doesn't know what events party B listens to. Whenever a communication party has something to offer, it simply emits an event and the other party decides if it is interested in it. + +Data subscriptions on the other hand, allow one party (we will call it the client although it doesn't matter which one is the client and which one is the server) to tell the other (the server) that it needs some data value (for example a list of all online users). The server will then send the client the requested information and also automatically send an update whenever the data changes in some way on the server. This is especially useful if the server itself needs to get these value updates from some other system and needs to subscribe/listen to them on demand (or if there are too many events to just send all of them all the time). + +These data sources do not need to be part of a static API definition that is hard-coded and known by both parties beforehand. For example, a client might want to know the activity status of user "xyz" and get automatic updates on it. But maybe user "xyz" doesn't even exist. The client can still request the wanted data source from the server, and if the server can provide it it will do so. If the server cannot provide the data source, it will respond with a not-available error or send the data as soon as it is available, depending on what is required. + +The same functionality can be implemented with simple messages, however since this is used so frequently, it gets repetitive and error-prone quite quickly. By implementing this feature in a library, the repetitive parts can be abstracted and we can even use language/framework specific features to make such "remote" data sources even more convenient to use (e.g. React State or Svelt Stores). + + +## Remote Procedure calls + +Another common use-case for messaging protocols is a remote procedure calls. A remote procedure call consists of one communication party sending some data to the other one and causing some code to run there. Once the other party is finished, it will return the result to the calling party. + +A remote procedure call can be implemented by emitting an event and running some action in the even listener. At the end of the listener, another event has to be emitted containing the result returned to the client. + +There are a few problems with manually implementing this using events. First of all, in order for the request and response events to be associated with each other, some sort of unique ID must be added by the caller that is then also returned in the response so the caller can associate the result with any particular call. Second, an application is likely to have many different procedures to be called, so the additional overhead of defining and emitting separate request and response events (with this ID management) for every procedure is quite tedious and error prone, as it involves re-implementing the same functionality multiple times. + +msglink avoids this by implementing the base functionality once and providing a language-specific and clean way to define procedures in one place with input data, result data and name. This is similar to [JSON-RPC](https://en.wikipedia.org/wiki/JSON-RPC) but provides the additional data validation and automatic parsing functionality described above. + + + +## Naming alternatives + +Note for future me: If msglink doesn't fit for some reason in the future, here are some alternative name ideas: + +- msgio (MessageIO) \ No newline at end of file diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp new file mode 100644 index 0000000..a8f9f94 --- /dev/null +++ b/include/el/msglink/server.hpp @@ -0,0 +1,19 @@ +/* +ELEKTRON © 2022 +Written by Matteo Reiter +www.elektron.work +26.11.22, 18:25 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Preprocessor definitions for detecting C++ verisons +and enabeling supported features. + +Currentyl this might not work for all compilers and it might not work at all. +In order to bypass all version checking, just +#define __EL_ENABLE_CXX11 +#define __EL_ENABLE_CXX17 +to enable library features for versions not detected using the __cplusplus definition. +*/ \ No newline at end of file diff --git a/include/el/retcode.hpp b/include/el/retcode.hpp index 7d17df9..d87ee4c 100644 --- a/include/el/retcode.hpp +++ b/include/el/retcode.hpp @@ -1,5 +1,5 @@ /* -ELEKTRON © 2022 +ELEKTRON © 2022 - now Written by melektron www.elektron.work 27.12.22, 13:44 diff --git a/include/el/struct_proxy.hpp b/include/el/struct_proxy.hpp index 427f330..8f62cc1 100644 --- a/include/el/struct_proxy.hpp +++ b/include/el/struct_proxy.hpp @@ -1,6 +1,6 @@ /* -ELEKTRON © 2023 -Written by Matteo Reiter +ELEKTRON © 2023 - now +Written by melektron www.elektron.work 10.06.23, 23:29 All rights reserved. diff --git a/include/el/strutil.hpp b/include/el/strutil.hpp index 3fa5986..b2f0b9d 100644 --- a/include/el/strutil.hpp +++ b/include/el/strutil.hpp @@ -1,6 +1,6 @@ /* -ELEKTRON © 2022 -Written by Matteo Reiter +ELEKTRON © 2022 - now +Written by melektron www.elektron.work 07.10.22, 21:28 All rights reserved. diff --git a/include/el/types.hpp b/include/el/types.hpp index 8cf57ea..63471e5 100644 --- a/include/el/types.hpp +++ b/include/el/types.hpp @@ -1,6 +1,6 @@ /* -ELEKTRON © 2022 -Written by Matteo Reiter +ELEKTRON © 2022 - now +Written by melektron www.elektron.work 07.10.22, 22:13 All rights reserved. diff --git a/include/el/universal.hpp b/include/el/universal.hpp index b3205f1..bb88873 100644 --- a/include/el/universal.hpp +++ b/include/el/universal.hpp @@ -1,6 +1,6 @@ /* -ELEKTRON © 2022 -Written by Matteo Reiter +ELEKTRON © 2022 - now +Written by melektron www.elektron.work 06.10.22, 21:50 All rights reserved. From b8a89b72111a33937be6ce79373a69ae09271343 Mon Sep 17 00:00:00 2001 From: melektron Date: Fri, 3 Nov 2023 00:53:04 +0100 Subject: [PATCH 02/50] worked on implementing basic websocket server (working) and also added exception and logging framework along the way (to be improved still) --- include/el/exceptions.hpp | 46 +++++++ include/el/logging.hpp | 171 +++++++++++++++++++++++++ include/el/msglink/errors.hpp | 41 ++++++ include/el/msglink/server.hpp | 228 ++++++++++++++++++++++++++++++++-- 4 files changed, 474 insertions(+), 12 deletions(-) create mode 100644 include/el/exceptions.hpp create mode 100644 include/el/logging.hpp create mode 100644 include/el/msglink/errors.hpp diff --git a/include/el/exceptions.hpp b/include/el/exceptions.hpp new file mode 100644 index 0000000..d510fa6 --- /dev/null +++ b/include/el/exceptions.hpp @@ -0,0 +1,46 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +02.11.23, 23:23 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +el-std base exceptions +*/ + +#pragma once + +#include +#include + + +namespace el +{ + /** + * @brief el-std base exception allowing custom messages + */ + class exception : std::exception + { + private: + std::string m_message; + + public: + exception(const char *_msg) + : m_message(_msg) + {} + + exception(const std::string &_msg) + : m_message(_msg) + {} + + virtual const char *what() const noexcept override + { + return m_message.c_str(); + } + }; + +} // namespace el + diff --git a/include/el/logging.hpp b/include/el/logging.hpp new file mode 100644 index 0000000..c6fe2f4 --- /dev/null +++ b/include/el/logging.hpp @@ -0,0 +1,171 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +02.11.23, 22:10 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Simple logging framework +*/ + +#pragma once + +#include +#include +#include +#include + +#ifdef __GNUC__ +#include +#endif + +#include + + +// color escape sequences +#define _EL_LOG_ANSI_COLOR_RED "\e[31m" +#define _EL_LOG_ANSI_COLOR_GREEN "\e[32m" +#define _EL_LOG_ANSI_COLOR_YELLOW "\e[33m" +#define _EL_LOG_ANSI_COLOR_BLUE "\e[34m" +#define _EL_LOG_ANSI_COLOR_MAGENTA "\e[35m" +#define _EL_LOG_ANSI_COLOR_CYAN "\e[36m" +#define _EL_LOG_ANSI_COLOR_RESET "\e[0m" + +#define _EL_LOG_PREFIX_BUFFER_SIZE 100 + +#define _EL_LOG_FILEW 15 +#define _EL_LOG_LINEW 4 + + +#define EL_DEFINE_LOGGER() auto logger_inst = el::logging::logger(__FILE__) +#define EL_LOGC(fmt, ...) logger_inst.critical(__LINE__, fmt, ## __VA_ARGS__) +#define EL_LOGE(fmt, ...) logger_inst.error(__LINE__, fmt, ## __VA_ARGS__) +#define EL_LOGW(fmt, ...) logger_inst.warning(__LINE__, fmt, ## __VA_ARGS__) +#define EL_LOGI(fmt, ...) logger_inst.info(__LINE__, fmt, ## __VA_ARGS__) +#define EL_LOGD(fmt, ...) logger_inst.debug(__LINE__, fmt, ## __VA_ARGS__) +#define EL_LOG_EXCEPTION(ex) EL_LOGE("Exception occured: %s", el::logging::format_exception(ex).c_str()) + +namespace el::logging +{ + class logger + { + private: + const std::string m_file_name; + + void generate_prefix(char *_output_buffer, int _line, const char *_level) + { + snprintf( + _output_buffer, + _EL_LOG_PREFIX_BUFFER_SIZE - 1, + "[%*.*s@%-*d] %s: ", + _EL_LOG_FILEW, + _EL_LOG_FILEW, + m_file_name.c_str(), + _EL_LOG_LINEW, + _line, + _level + ); + } + + public: + logger(std::string _file_name) + : m_file_name(_file_name) + {} + + template + void critical(int _line, const std::string _fmt, _Args... _args) + { + // format the message + const std::string message = strutil::format(_fmt, _args...); + + // generate prefix + char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; + generate_prefix(prefix_buffer, _line, "C"); + + // print in red + std::cout << _EL_LOG_ANSI_COLOR_RED << prefix_buffer << message << _EL_LOG_ANSI_COLOR_RESET << std::endl; + } + + template + void error(int _line, const std::string _fmt, _Args... _args) + { + // format the message + const std::string message = strutil::format(_fmt, _args...); + + // generate prefix + char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; + generate_prefix(prefix_buffer, _line, "E"); + + // print in red + std::cout << _EL_LOG_ANSI_COLOR_RED << prefix_buffer << message << _EL_LOG_ANSI_COLOR_RESET << std::endl; + } + + template + void warning(int _line, const std::string _fmt, _Args... _args) + { + // format the message + const std::string message = strutil::format(_fmt, _args...); + + // generate prefix + char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; + generate_prefix(prefix_buffer, _line, "W"); + + // print in red + std::cout << _EL_LOG_ANSI_COLOR_YELLOW << prefix_buffer << message << _EL_LOG_ANSI_COLOR_RESET << std::endl; + } + + template + void info(int _line, const std::string _fmt, _Args... _args) + { + // format the message + const std::string message = strutil::format(_fmt, _args...); + + // generate prefix + char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; + generate_prefix(prefix_buffer, _line, "W"); + + // print in red + std::cout << _EL_LOG_ANSI_COLOR_RESET << prefix_buffer << message << _EL_LOG_ANSI_COLOR_RESET << std::endl; + } + + template + void debug(int _line, const std::string _fmt, _Args... _args) + { + // format the message + const std::string message = strutil::format(_fmt, _args...); + + // generate prefix + char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; + generate_prefix(prefix_buffer, _line, "W"); + + // print in red + std::cout << _EL_LOG_ANSI_COLOR_GREEN << prefix_buffer << message << _EL_LOG_ANSI_COLOR_RESET << std::endl; + } + }; + + /** + * @brief turns the passed exception into a string + * in the format ": " + * + * Note the type name is demangled in GCC, the raw name (may be demangled + * depending on impl) is used for other compilers + * @param _e the exception to print + * @return std::string the printed exception + */ + std::string format_exception(const std::exception &_e) + { +#ifdef __GNUC__ + int status = 0; + char *ex_type_name = abi::__cxa_demangle(typeid(_e).name(), nullptr, nullptr, &status); + auto output = std::string(ex_type_name) + ": " + _e.what(); + free(ex_type_name); + return output; +#else + return std::string(typeid(_e).name()) + ": " + _e.what(); +#endif + } + +} // namespace el::log diff --git a/include/el/msglink/errors.hpp b/include/el/msglink/errors.hpp new file mode 100644 index 0000000..66a2b77 --- /dev/null +++ b/include/el/msglink/errors.hpp @@ -0,0 +1,41 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +02.11.23, 22:03 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +msglink exceptions +*/ + +#pragma once + +#include +#include + +namespace el::msglink +{ + class initialization_error : public el::exception + { + using el::exception::exception; + }; + + class launch_error : public el::exception + { + using el::exception::exception; + }; + + class serving_error : public el::exception + { + using el::exception::exception; + }; + + class termination_error: public el::exception + { + using el::exception::exception; + }; + +} // namespace el::msglink diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index a8f9f94..0b42e66 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -1,19 +1,223 @@ /* -ELEKTRON © 2022 -Written by Matteo Reiter +ELEKTRON © 2023 - now +Written by melektron www.elektron.work -26.11.22, 18:25 +02.11.23, 22:02 All rights reserved. This source code is licensed under the Apache-2.0 license found in the -LICENSE file in the root directory of this source tree. +LICENSE file in the root directory of this source tree. -Preprocessor definitions for detecting C++ verisons -and enabeling supported features. +msglink server class +*/ -Currentyl this might not work for all compilers and it might not work at all. -In order to bypass all version checking, just -#define __EL_ENABLE_CXX11 -#define __EL_ENABLE_CXX17 -to enable library features for versions not detected using the __cplusplus definition. -*/ \ No newline at end of file +#pragma once + +#define ASIO_STANDALONE + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + + +namespace el::msglink +{ + + // typedefs and namespaces to make code more readably + namespace wspp = websocketpp; + namespace pl = std::placeholders; + typedef wspp::server wsserver; + + class connection_handler + { + void on_open(wspp::connection_hdl hdl) noexcept; + void on_http(wspp::connection_hdl hdl) noexcept; + void on_message(wspp::connection_hdl hdl, wsserver::message_ptr msg) noexcept; + void on_fail(wspp::connection_hdl hdl) noexcept; + void on_close(wspp::connection_hdl hdl) noexcept; + }; + + class server + { + private: + // == Configuration + // port to serve on + int m_port; + + // == State + // the websocket server used for transport + wsserver m_socket_server; + + // enumeration managing current server state + enum server_state_t + { + UNINITIALIZED = 0, // newly instantiated, not initialized + INITIALIZED = 1, // initialize() successful + RUNNING = 2, // run() called, server still running + FAILED = 3, // run() exited with error + STOPPED = 4 // run() exited cleanly (through stop() or other natural way) + }; + server_state_t server_state = UNINITIALIZED; + + // set of connections to corresponding connection handler instance + std::map< + wspp::connection_hdl, + connection_handler, + std::owner_less + > open_connections; + + private: + + /** + * @brief websocket server callback functions called when + * new client connection is opened, a message is received, an error occurs + * or the connection is closed. + * + * @param hdl websocket connection handle (a generic argument that identifies the connection, always present) + * @param ... more arguments may be required for a specific function + */ + void on_open(wspp::connection_hdl hdl) noexcept + { + std::cout << __FUNCTION__ << std::endl; + } + void on_http(wspp::connection_hdl hdl) noexcept + { + std::cout << __FUNCTION__ << std::endl; + } + void on_message(wspp::connection_hdl hdl, wsserver::message_ptr msg) noexcept + { + std::cout << __FUNCTION__ << std::endl; + } + void on_fail(wspp::connection_hdl hdl) noexcept + { + std::cout << __FUNCTION__ << std::endl; + } + void on_close(wspp::connection_hdl hdl) noexcept + { + std::cout << __FUNCTION__ << std::endl; + } + + public: + server() + : m_port(8080) + {} + + server(int _port) + : m_port(_port) + {} + + // standard init/terminate methods to start stop global object server in main + void initialize() + { + if (server_state != UNINITIALIZED) + throw initialization_error("msglink server instance is single use, cannot re-initialize"); + + try + { + // we don't want any wspp log messages + m_socket_server.clear_access_channels(wspp::log::alevel::all); + m_socket_server.clear_error_channels(wspp::log::elevel::all); + + // initialize asio communication + m_socket_server.init_asio(); + + // register callback handlers + m_socket_server.set_open_handler(std::bind(&server::on_open, this, pl::_1)); + m_socket_server.set_http_handler(std::bind(&server::on_http, this, pl::_1)); + m_socket_server.set_message_handler(std::bind(&server::on_message, this, pl::_1, pl::_2)); + m_socket_server.set_fail_handler(std::bind(&server::on_fail, this, pl::_1)); + m_socket_server.set_close_handler(std::bind(&server::on_close, this, pl::_1)); + + // set reuse addr flag to allow faster restart times + m_socket_server.set_reuse_addr(true); + + } + catch (const std::exception &e) + { + throw initialization_error(el::logging::format_exception(e)); + } + + server_state = INITIALIZED; + } + + /** + * @brief runs the server I/O loop (blocking) + * + * @throws serving_error if error occurred while running the server + * @throws launch_error if couldn't run because of invalid state + */ + void run() + { + if (server_state == UNINITIALIZED) + throw launch_error("called server::run() before server::initialize()"); + else if (server_state != INITIALIZED) + throw launch_error("called server::run() multiple times (msglink server instance is single use, cannot run multiple times)"); + + try + { + // listening happens on port defined in settings + m_socket_server.listen(m_port); + + // start accepting + m_socket_server.start_accept(); + } + catch(const std::exception& e) + { + throw launch_error(el::logging::format_exception(e)); + } + + server_state = RUNNING; + + try + { + // run the io loop + m_socket_server.run(); + } + catch(const std::exception& e) + { + server_state = FAILED; + throw serving_error(el::logging::format_exception(e)); + } + + server_state = STOPPED; + } + + /** + * @brief stops the server if it is running and does nothing + * otherwise. + * + * @throws termination_error if stopping was attempted but failed + */ + void stop() + { + // do nothing if server is not running + if (server_state != RUNNING) return; + + try + { + // stop listening for new connections + m_socket_server.stop_listening(); + + // close all existing connections + for (const auto& [hdl, client] : open_connections) + { + m_socket_server.close(hdl, 0, "server stopped"); + } + } + catch(const std::exception& e) + { + throw termination_error(el::logging::format_exception(e)); + } + } + + }; + +} // namespace el \ No newline at end of file From 9f2413c348afff66cf43bfa284a5174e45da7afe Mon Sep 17 00:00:00 2001 From: melektron Date: Fri, 3 Nov 2023 16:34:00 +0100 Subject: [PATCH 03/50] updated strutil and specifically format and improved logging --- include/el/logging.hpp | 81 ++++++++++++++++++++++++++++-------------- include/el/strutil.hpp | 41 ++++++++++----------- 2 files changed, 73 insertions(+), 49 deletions(-) diff --git a/include/el/logging.hpp b/include/el/logging.hpp index c6fe2f4..0e58741 100644 --- a/include/el/logging.hpp +++ b/include/el/logging.hpp @@ -46,7 +46,9 @@ Simple logging framework #define EL_LOGW(fmt, ...) logger_inst.warning(__LINE__, fmt, ## __VA_ARGS__) #define EL_LOGI(fmt, ...) logger_inst.info(__LINE__, fmt, ## __VA_ARGS__) #define EL_LOGD(fmt, ...) logger_inst.debug(__LINE__, fmt, ## __VA_ARGS__) -#define EL_LOG_EXCEPTION(ex) EL_LOGE("Exception occured: %s", el::logging::format_exception(ex).c_str()) + +#define EL_LOG_EXCEPTION_MSG(msg, ex) EL_LOGE(msg ": %s", el::logging::format_exception(ex).c_str()) +#define EL_LOG_EXCEPTION(ex) EL_LOG_EXCEPTION_MSG("Exception occurred", ex) namespace el::logging { @@ -74,76 +76,103 @@ namespace el::logging logger(std::string _file_name) : m_file_name(_file_name) {} + + // Critical - template - void critical(int _line, const std::string _fmt, _Args... _args) + void critical(int _line, const std::string &_message) { - // format the message - const std::string message = strutil::format(_fmt, _args...); - // generate prefix char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; generate_prefix(prefix_buffer, _line, "C"); - // print in red - std::cout << _EL_LOG_ANSI_COLOR_RED << prefix_buffer << message << _EL_LOG_ANSI_COLOR_RESET << std::endl; + // print in color + std::cout << _EL_LOG_ANSI_COLOR_RED << prefix_buffer << _message << _EL_LOG_ANSI_COLOR_RESET << std::endl; } template - void error(int _line, const std::string _fmt, _Args... _args) + void critical(int _line, const std::string &_fmt, _Args... _args) { // format the message - const std::string message = strutil::format(_fmt, _args...); + critical(_line, strutil::format(_fmt, _args...)); + } + + // Error + void error(int _line, const std::string &_message) + { // generate prefix char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; generate_prefix(prefix_buffer, _line, "E"); - // print in red - std::cout << _EL_LOG_ANSI_COLOR_RED << prefix_buffer << message << _EL_LOG_ANSI_COLOR_RESET << std::endl; + // print in color + std::cout << _EL_LOG_ANSI_COLOR_RED << prefix_buffer << _message << _EL_LOG_ANSI_COLOR_RESET << std::endl; } template - void warning(int _line, const std::string _fmt, _Args... _args) + void error(int _line, const std::string &_fmt, _Args... _args) { // format the message - const std::string message = strutil::format(_fmt, _args...); + error(_line, strutil::format(_fmt, _args...)); + } + + // Warning + + void warning(int _line, const std::string &_message) + { // generate prefix char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; generate_prefix(prefix_buffer, _line, "W"); - // print in red - std::cout << _EL_LOG_ANSI_COLOR_YELLOW << prefix_buffer << message << _EL_LOG_ANSI_COLOR_RESET << std::endl; + // print in color + std::cout << _EL_LOG_ANSI_COLOR_YELLOW << prefix_buffer << _message << _EL_LOG_ANSI_COLOR_RESET << std::endl; } template - void info(int _line, const std::string _fmt, _Args... _args) + void warning(int _line, const std::string &_fmt, _Args... _args) { // format the message - const std::string message = strutil::format(_fmt, _args...); + warning(_line, strutil::format(_fmt, _args...)); + } + // Info + + void info(int _line, const std::string &_message) + { // generate prefix char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; - generate_prefix(prefix_buffer, _line, "W"); + generate_prefix(prefix_buffer, _line, "I"); - // print in red - std::cout << _EL_LOG_ANSI_COLOR_RESET << prefix_buffer << message << _EL_LOG_ANSI_COLOR_RESET << std::endl; + // print in color + std::cout << _EL_LOG_ANSI_COLOR_RESET << prefix_buffer << _message << _EL_LOG_ANSI_COLOR_RESET << std::endl; } template - void debug(int _line, const std::string _fmt, _Args... _args) + void info(int _line, const std::string &_fmt, _Args... _args) { // format the message - const std::string message = strutil::format(_fmt, _args...); + info(_line, strutil::format(_fmt, _args...)); + } + + // Debug + void debug(int _line, const std::string &_message) + { // generate prefix char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; - generate_prefix(prefix_buffer, _line, "W"); + generate_prefix(prefix_buffer, _line, "D"); + + // print in color + std::cout << _EL_LOG_ANSI_COLOR_GREEN << prefix_buffer << _message << _EL_LOG_ANSI_COLOR_RESET << std::endl; + } - // print in red - std::cout << _EL_LOG_ANSI_COLOR_GREEN << prefix_buffer << message << _EL_LOG_ANSI_COLOR_RESET << std::endl; + template + void debug(int _line, const std::string &_fmt, _Args... _args) + { + // format the message + debug(_line, strutil::format(_fmt, _args...)); } + }; /** diff --git a/include/el/strutil.hpp b/include/el/strutil.hpp index b2f0b9d..d5f938b 100644 --- a/include/el/strutil.hpp +++ b/include/el/strutil.hpp @@ -29,35 +29,36 @@ namespace el::strutil * NOTE: This method uses dynamic memory allocation ("new" operator) and std::unique_ptr. * Usually, the template arguments don't have to be provided but can be deducted from function * arguments. + * Inspiration: https://stackoverflow.com/a/26221725 * - * @tparam _ST string type of the format and return value. The type must be constructable - * from "const char *" (string copy) and must have a .c_str() method to convert to "char *"". * @tparam _Args varadic format argument types * @param _fmt Format string * @param _args Format arguments - * @return _ST newly created string of specified type + * @return newly created string of specified type */ - template - _ST format(const _ST& _fmt, _Args... _args) + template + std::string format(const std::string& _fmt, _Args... _args) { - size_t len = snprintf(nullptr, 0, _fmt.c_str(), _args...) + 1; // extra space for null byte + int len_or_error = std::snprintf(nullptr, 0, _fmt.c_str(), _args...) + 1; // extra space for null byte + + if( len_or_error <= 0 ) + throw std::runtime_error( "Error during formatting." ); + auto len = static_cast(len_or_error); + std::unique_ptr _cstr(new char[len]); - snprintf(_cstr.get(), len, _fmt.c_str(), _args...); - return _ST(_cstr.get()); + std::snprintf(_cstr.get(), len, _fmt.c_str(), _args...); + + return std::string(_cstr.get()); } /** * @brief creates a copy of a string with all lowercase letters. * The tolower() C function is used to convert the letters. - * Any string class compatible with the C++ std::string class in terms - * of iteration and uses "char" as the character type can be used. * - * @tparam _ST string type to be used (deducted, typically std::string) * @param instr the input string to convert - * @return _ST copy of the string in lowercase + * @return copy of the string in lowercase */ - template - _ST lowercase(_ST instr) + std::string lowercase(std::string instr) { std::for_each(instr.begin(), instr.end(), [](char &c) { c = ::tolower(c); }); @@ -68,15 +69,11 @@ namespace el::strutil /** * @brief creates a copy of a string with all lowercase letters. * The tolower() C function is used to convert the letters. - * Any string class compatible with the C++ std::string class in terms - * of iteration and uses "char" as the character type can be used. * - * @tparam _ST string type to be used (deducted, typically std::string) * @param instr the input string to convert - * @return _ST copy of the string in lowercase + * @return copy of the string in lowercase */ - template - _ST uppercase(_ST instr) + std::string uppercase(std::string instr) { std::for_each(instr.begin(), instr.end(), [](char &c) { c = ::toupper(c); }); @@ -88,13 +85,11 @@ namespace el::strutil * @brief Reads the entire content of a file and stores it in a string. * @exception This function can trough any exception that the string or ifstream can. * - * @tparam _ST string type, typically std::string (can be deducted) * @param _file The file stream to read from * @param _string The string to store the file contents in. This will overwrite the string. * @return The length of the file (= the number of characters copied to the string) */ - template - size_t read_file_into_string(std::ifstream &_file, _ST &_string) + size_t read_file_into_string(std::ifstream &_file, std::string &_string) { // get file length _file.seekg(0, std::ios::end); From ef012bf9fe0675d213ca7b111b9753794a31d139 Mon Sep 17 00:00:00 2001 From: melektron Date: Fri, 3 Nov 2023 23:50:00 +0100 Subject: [PATCH 04/50] added some message handling, experimented with pings and http --- include/el/exceptions.hpp | 4 +- include/el/msglink/README.md | 7 +- include/el/msglink/errors.hpp | 56 +++++- include/el/msglink/server.hpp | 320 +++++++++++++++++++++++++++------- include/el/msglink/wspp.hpp | 29 +++ 5 files changed, 343 insertions(+), 73 deletions(-) create mode 100644 include/el/msglink/wspp.hpp diff --git a/include/el/exceptions.hpp b/include/el/exceptions.hpp index d510fa6..d949df0 100644 --- a/include/el/exceptions.hpp +++ b/include/el/exceptions.hpp @@ -22,7 +22,7 @@ namespace el /** * @brief el-std base exception allowing custom messages */ - class exception : std::exception + class exception : public std::exception { private: std::string m_message; @@ -36,6 +36,8 @@ namespace el : m_message(_msg) {} + virtual ~exception() noexcept = default; + virtual const char *what() const noexcept override { return m_message.c_str(); diff --git a/include/el/msglink/README.md b/include/el/msglink/README.md index 594d172..473ee9e 100644 --- a/include/el/msglink/README.md +++ b/include/el/msglink/README.md @@ -70,4 +70,9 @@ msglink avoids this by implementing the base functionality once and providing a Note for future me: If msglink doesn't fit for some reason in the future, here are some alternative name ideas: -- msgio (MessageIO) \ No newline at end of file +- msgio (MessageIO) + + +## Link collection + +- wspp client reconnect: https://github.com/zaphoyd/websocketpp/issues/754#issue-353706390 \ No newline at end of file diff --git a/include/el/msglink/errors.hpp b/include/el/msglink/errors.hpp index 66a2b77..2281b98 100644 --- a/include/el/msglink/errors.hpp +++ b/include/el/msglink/errors.hpp @@ -18,24 +18,64 @@ msglink exceptions namespace el::msglink { - class initialization_error : public el::exception + namespace wspp = websocketpp; + + /** + * @brief base class for all errors related to msglink + */ + class msglink_error : public el::exception { using el::exception::exception; }; - - class launch_error : public el::exception + + /** + * @brief exception indicating that initialization + * could not be performed for some reason + */ + class initialization_error : public msglink_error { - using el::exception::exception; + using msglink_error::msglink_error; }; - class serving_error : public el::exception + /** + * @brief msglink error to represent wspp error. + * All wspp::exceptions will be caught and rethrown as + * socket_errors by msglink, so everything inherits from + * msglink_error + */ + class socket_error : public msglink_error { - using el::exception::exception; + public: + + wspp::lib::error_code m_code; + + socket_error(const wspp::exception &_e) + : msglink_error(_e.m_msg) + , m_code(_e.m_code) + {} + + wspp::lib::error_code code() const noexcept { + return m_code; + } + + }; + + /** + * @brief exception indicating that the server/client couldn't be + * started because of incorrect state (e.g. not initialized) + */ + class launch_error : public msglink_error + { + using msglink_error::msglink_error; }; - class termination_error: public el::exception + /** + * @brief tried to access invalid connection instance + * (wspp callback with unknown connection handle) + */ + class invalid_connection_error: public msglink_error { - using el::exception::exception; + using msglink_error::msglink_error; }; } // namespace el::msglink diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index 0b42e66..ad7631b 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -17,37 +17,89 @@ msglink server class #include #include +#include +#include #include #include -#include -#include +#include +#include +#include #include #include +#include #include +#define PRINT_CALL std::cout << __PRETTY_FUNCTION__ << std::endl + + namespace el::msglink { + using namespace std::chrono_literals; - // typedefs and namespaces to make code more readably - namespace wspp = websocketpp; - namespace pl = std::placeholders; - typedef wspp::server wsserver; + class server; + class connection_handler; class connection_handler { - void on_open(wspp::connection_hdl hdl) noexcept; - void on_http(wspp::connection_hdl hdl) noexcept; - void on_message(wspp::connection_hdl hdl, wsserver::message_ptr msg) noexcept; - void on_fail(wspp::connection_hdl hdl) noexcept; - void on_close(wspp::connection_hdl hdl) noexcept; + friend class server; + + private: // state + + // the server managing this client connection + wsserver &m_socket_server; + + // a handle to the connection handled by this client + wspp::connection_hdl m_connection; + + + public: + + connection_handler(wsserver &_socket_server, wspp::connection_hdl _connection) + : m_socket_server(_socket_server) + , m_connection(_connection) + { + PRINT_CALL; + } + + // connection handler is supposed to be instantiated in-place exactly once per + // connection in the connection map. It should never be moved or copied. + connection_handler(const connection_handler &) = delete; + connection_handler(connection_handler &&) = delete; + + virtual ~connection_handler() + { + PRINT_CALL; + } + + /** + * @brief called by server when message arrives for this connection + * + * @param _msg the message to handle + */ + void on_message(wsserver::message_ptr _msg) noexcept + { + std::cout << "message: " << _msg->get_payload() << std::endl; + m_socket_server.send(m_connection, _msg->get_payload(), _msg->get_opcode()); + } + + /** + * @brief initiates a websocket ping. + * This is called periodically by server thread. + */ + void initiate_ping() + { + m_socket_server.ping(m_connection, ""); // no message needed + } }; class server { + private: + // == Configuration // port to serve on int m_port; @@ -65,102 +117,243 @@ namespace el::msglink FAILED = 3, // run() exited with error STOPPED = 4 // run() exited cleanly (through stop() or other natural way) }; - server_state_t server_state = UNINITIALIZED; + std::atomic m_server_state { UNINITIALIZED }; // set of connections to corresponding connection handler instance std::map< wspp::connection_hdl, connection_handler, std::owner_less - > open_connections; + > m_open_connections; + + // connection processing thread. This thread is responsible for + // some housekeeping work such as keepalive for all the connections. + std::thread m_processing_thread; + + // mutex and condition variable guarded flag to tell the thread + // when to exit + bool m_threxit; + std::mutex m_threxit_mutex; + std::condition_variable m_threxit_cv; private: /** - * @brief websocket server callback functions called when + * @brief websocket server callback functions called when * new client connection is opened, a message is received, an error occurs * or the connection is closed. * * @param hdl websocket connection handle (a generic argument that identifies the connection, always present) * @param ... more arguments may be required for a specific function */ - void on_open(wspp::connection_hdl hdl) noexcept + void on_open(wspp::connection_hdl _hdl) { - std::cout << __FUNCTION__ << std::endl; + PRINT_CALL; + + if (m_server_state != RUNNING) + return; + + // create new handler instance and save it + m_open_connections.emplace( + std::piecewise_construct, // Needed for in-place construct https://en.cppreference.com/w/cpp/utility/piecewise_construct + std::forward_as_tuple(_hdl), + std::forward_as_tuple(m_socket_server, _hdl) + ); + } - void on_http(wspp::connection_hdl hdl) noexcept + void on_message(wspp::connection_hdl _hdl, wsserver::message_ptr _msg) { - std::cout << __FUNCTION__ << std::endl; + PRINT_CALL; + + if (m_server_state != RUNNING) + return; + + // forward message to client handler + try + { + m_open_connections.at(_hdl).on_message(_msg); + } + catch (const std::out_of_range &e) + { + throw invalid_connection_error("Received message from unknown/invalid connection."); + } + } - void on_message(wspp::connection_hdl hdl, wsserver::message_ptr msg) noexcept + void on_close(wspp::connection_hdl _hdl) { - std::cout << __FUNCTION__ << std::endl; + PRINT_CALL; + + if (m_server_state != RUNNING) + return; + + // remove closed connection from connection map + if (!m_open_connections.erase(_hdl)) + { + throw invalid_connection_error("Attempted to close an unknown/invalid connection which doesn't seem to exist."); + } } - void on_fail(wspp::connection_hdl hdl) noexcept + + void on_http(wspp::connection_hdl _hdl) { - std::cout << __FUNCTION__ << std::endl; + PRINT_CALL; + + if (m_server_state != RUNNING) + return; + + // get the connection + wsserver::connection_ptr con = m_socket_server.get_con_from_hdl(_hdl); + + // respond with upgrade required for now (which is also default behaviour) + con->set_body("Upgrade Required"); + con->set_status(wspp::http::status_code::upgrade_required); + } + + /** + * @brief fail handler gets called when connection + * failed to open or the server stops, not when an open connection "fails". + * It is not relevant for handling open connections + * + * @param _hdl + */ + void on_fail(wspp::connection_hdl _hdl) noexcept + { + PRINT_CALL; + + if (m_server_state != RUNNING) + return; + + wsserver::connection_ptr con = m_socket_server.get_con_from_hdl(_hdl); + auto error = con->get_ec(); + + std::cout << "msglink fail (irrelevant): " << error.message() << std::endl; } - void on_close(wspp::connection_hdl hdl) noexcept + + /** + * @brief function of the connection processing thread + */ + void processing_thread_fn() noexcept { - std::cout << __FUNCTION__ << std::endl; + try + { + for (;;) + { + std::unique_lock lock(m_threxit_mutex); + m_threxit_cv.wait_for(lock, 10s); + // mutext is now locked regardless of timeout or not + if (m_threxit) + break; + + // if server is not running there is nothing to do + if (m_server_state != RUNNING) + continue; + + // perform routine task + std::cout << "routine" << std::endl; + + for (auto &[connection_handle, handler] : m_open_connections) + { + handler.initiate_ping(); + } + + } + + std::cout << "thread exit" << std::endl; + } + catch (const std::exception &e) + { + // TODO: park exception here and re-raise it in run() for user handling + std::cout << "Exception in server thread: " << el::logging::format_exception(e) << std::endl; + } + } public: - server() - : m_port(8080) - {} server(int _port) : m_port(_port) - {} + { + PRINT_CALL; + + // start processing thread + m_processing_thread = std::thread(std::bind(&server::processing_thread_fn, this)); + } + + // never copy or move a server + server(const server &) = delete; + server(server &&) = delete; + + ~server() + { + PRINT_CALL; + + // order processing thread to exit + { + std::lock_guard lock(m_threxit_mutex); + m_threxit = true; + } + m_threxit_cv.notify_one(); + // wait for thread to exit + if (m_processing_thread.joinable()) + m_processing_thread.join(); + + } - // standard init/terminate methods to start stop global object server in main + /** + * @brief initializes the server setting up all transport settings + * and preparing the server to run. This MUST be called before run(). + * + * @throws msglink::initialization_error invalid state to initialize + * @throws msglink::socket_error error while configuring networking + * @throws other std exceptions possible + */ void initialize() { - if (server_state != UNINITIALIZED) + if (m_server_state != UNINITIALIZED) throw initialization_error("msglink server instance is single use, cannot re-initialize"); - + try { // we don't want any wspp log messages - m_socket_server.clear_access_channels(wspp::log::alevel::all); - m_socket_server.clear_error_channels(wspp::log::elevel::all); + m_socket_server.set_access_channels(wspp::log::alevel::all); + m_socket_server.set_error_channels(wspp::log::elevel::all); // initialize asio communication m_socket_server.init_asio(); - // register callback handlers + // register callback handlers (More handlers: https://docs.websocketpp.org/reference_8handlers.html) m_socket_server.set_open_handler(std::bind(&server::on_open, this, pl::_1)); - m_socket_server.set_http_handler(std::bind(&server::on_http, this, pl::_1)); m_socket_server.set_message_handler(std::bind(&server::on_message, this, pl::_1, pl::_2)); - m_socket_server.set_fail_handler(std::bind(&server::on_fail, this, pl::_1)); m_socket_server.set_close_handler(std::bind(&server::on_close, this, pl::_1)); + m_socket_server.set_http_handler(std::bind(&server::on_http, this, pl::_1)); + m_socket_server.set_fail_handler(std::bind(&server::on_fail, this, pl::_1)); // set reuse addr flag to allow faster restart times m_socket_server.set_reuse_addr(true); } - catch (const std::exception &e) + catch (const wspp::exception &e) { - throw initialization_error(el::logging::format_exception(e)); + throw socket_error(e); } - server_state = INITIALIZED; + m_server_state = INITIALIZED; } /** * @brief runs the server I/O loop (blocking) - * - * @throws serving_error if error occurred while running the server - * @throws launch_error if couldn't run because of invalid state + * + * @throws msglink::launch_error couldn't run server because of invalid state (e.g. not initialized) + * @throws msglink::socket_error network communication / websocket error occurred + * @throws other msglink::msglink_error? + * @throws other std exceptions possible */ void run() { - if (server_state == UNINITIALIZED) + if (m_server_state == UNINITIALIZED) throw launch_error("called server::run() before server::initialize()"); - else if (server_state != INITIALIZED) + else if (m_server_state != INITIALIZED) throw launch_error("called server::run() multiple times (msglink server instance is single use, cannot run multiple times)"); - + try { // listening happens on port defined in settings @@ -168,38 +361,38 @@ namespace el::msglink // start accepting m_socket_server.start_accept(); - } - catch(const std::exception& e) - { - throw launch_error(el::logging::format_exception(e)); - } - - server_state = RUNNING; - try - { // run the io loop + m_server_state = RUNNING; m_socket_server.run(); + m_server_state = STOPPED; + } + catch (const wspp::exception &e) + { + m_server_state = FAILED; + throw socket_error(e); } - catch(const std::exception& e) + catch (...) { - server_state = FAILED; - throw serving_error(el::logging::format_exception(e)); + m_server_state = FAILED; + throw; } - server_state = STOPPED; } /** * @brief stops the server if it is running and does nothing * otherwise. * - * @throws termination_error if stopping was attempted but failed + * This can be called from any thread, wspp is thread safe. + * + * @throws msglink::socket_error networking error occurred while stopping server + * @throws other msglink::msglink_error? */ void stop() { // do nothing if server is not running - if (server_state != RUNNING) return; + if (m_server_state != RUNNING) return; try { @@ -207,15 +400,16 @@ namespace el::msglink m_socket_server.stop_listening(); // close all existing connections - for (const auto& [hdl, client] : open_connections) + for (const auto &[hdl, client] : m_open_connections) { m_socket_server.close(hdl, 0, "server stopped"); } } - catch(const std::exception& e) + catch (const wspp::exception &e) { - throw termination_error(el::logging::format_exception(e)); + throw socket_error(e); } + } }; diff --git a/include/el/msglink/wspp.hpp b/include/el/msglink/wspp.hpp new file mode 100644 index 0000000..465b2c7 --- /dev/null +++ b/include/el/msglink/wspp.hpp @@ -0,0 +1,29 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +03.11.23, 14:13 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Includes and environment configuration for the websocket++ classes +needed by the msglink implementation +*/ + +#pragma once + +#include +#include +#include + + +namespace el::msglink +{ + // typedefs and namespaces to make code more readably + namespace wspp = websocketpp; + namespace pl = std::placeholders; + typedef wspp::server wsserver; + +} // namespace el::msglink \ No newline at end of file From 7cf0ca61c1427ded8a67b994595fb701e08effa3 Mon Sep 17 00:00:00 2001 From: melektron Date: Sat, 4 Nov 2023 00:40:19 +0100 Subject: [PATCH 05/50] got close on timeout working --- include/el/msglink/server.hpp | 43 +++++++++-------------------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index ad7631b..b56b2ad 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -23,7 +23,6 @@ msglink server class #include #include #include -#include #include #include @@ -139,12 +138,9 @@ namespace el::msglink private: /** - * @brief websocket server callback functions called when - * new client connection is opened, a message is received, an error occurs - * or the connection is closed. + * @brief new websocket connection opened (fully connected) * - * @param hdl websocket connection handle (a generic argument that identifies the connection, always present) - * @param ... more arguments may be required for a specific function + * @param hdl websocket connection handle */ void on_open(wspp::connection_hdl _hdl) { @@ -193,39 +189,23 @@ namespace el::msglink } } - void on_http(wspp::connection_hdl _hdl) - { - PRINT_CALL; - - if (m_server_state != RUNNING) - return; - - // get the connection - wsserver::connection_ptr con = m_socket_server.get_con_from_hdl(_hdl); - - // respond with upgrade required for now (which is also default behaviour) - con->set_body("Upgrade Required"); - con->set_status(wspp::http::status_code::upgrade_required); - } - /** - * @brief fail handler gets called when connection - * failed to open or the server stops, not when an open connection "fails". - * It is not relevant for handling open connections + * @brief called by wspp when a pong message times out. This is + * used by the keepalive system to detect connection loss * - * @param _hdl + * @param _hdl handle to connection where timeout occurred */ - void on_fail(wspp::connection_hdl _hdl) noexcept + void on_pong_timeout(wspp::connection_hdl _hdl) { PRINT_CALL; if (m_server_state != RUNNING) return; + // if we already timed out, terminate the connection with no handshake + // (this will still call close handler) wsserver::connection_ptr con = m_socket_server.get_con_from_hdl(_hdl); - auto error = con->get_ec(); - - std::cout << "msglink fail (irrelevant): " << error.message() << std::endl; + con->terminate(std::make_error_code(std::errc::timed_out)); } /** @@ -239,7 +219,7 @@ namespace el::msglink { std::unique_lock lock(m_threxit_mutex); m_threxit_cv.wait_for(lock, 10s); - // mutext is now locked regardless of timeout or not + // mutex is now locked regardless of timeout or not if (m_threxit) break; @@ -324,8 +304,7 @@ namespace el::msglink m_socket_server.set_open_handler(std::bind(&server::on_open, this, pl::_1)); m_socket_server.set_message_handler(std::bind(&server::on_message, this, pl::_1, pl::_2)); m_socket_server.set_close_handler(std::bind(&server::on_close, this, pl::_1)); - m_socket_server.set_http_handler(std::bind(&server::on_http, this, pl::_1)); - m_socket_server.set_fail_handler(std::bind(&server::on_fail, this, pl::_1)); + m_socket_server.set_pong_timeout_handler(std::bind(&server::on_pong_timeout, this, pl::_1)); // set reuse addr flag to allow faster restart times m_socket_server.set_reuse_addr(true); From 4aad12f49532848ef8639c26629f2b26631a95bf Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 5 Nov 2023 00:55:05 +0100 Subject: [PATCH 06/50] made ping loop an asio timer instead of thread --- include/el/msglink/errors.hpp | 55 +++++++--- include/el/msglink/server.hpp | 188 ++++++++++++++++++---------------- include/el/msglink/wspp.hpp | 36 ++++++- 3 files changed, 177 insertions(+), 102 deletions(-) diff --git a/include/el/msglink/errors.hpp b/include/el/msglink/errors.hpp index 2281b98..7223e8f 100644 --- a/include/el/msglink/errors.hpp +++ b/include/el/msglink/errors.hpp @@ -14,6 +14,7 @@ msglink exceptions #pragma once #include +#include #include namespace el::msglink @@ -36,9 +37,28 @@ namespace el::msglink { using msglink_error::msglink_error; }; + + /** + * @brief exception indicating that the server/client couldn't be + * started because of incorrect state (e.g. not initialized) + */ + class launch_error : public msglink_error + { + using msglink_error::msglink_error; + }; + + /** + * @brief tried to access invalid connection instance + * (wspp callback with unknown connection handle) + */ + class invalid_connection_error: public msglink_error + { + using msglink_error::msglink_error; + }; /** * @brief msglink error to represent wspp error. + * (which is basically just asio error) * All wspp::exceptions will be caught and rethrown as * socket_errors by msglink, so everything inherits from * msglink_error @@ -46,8 +66,8 @@ namespace el::msglink class socket_error : public msglink_error { public: - - wspp::lib::error_code m_code; + + wspp::lib::error_code m_code; // is just asio::error_code which should be std::error_code on modern system socket_error(const wspp::exception &_e) : msglink_error(_e.m_msg) @@ -59,23 +79,28 @@ namespace el::msglink } }; - - /** - * @brief exception indicating that the server/client couldn't be - * started because of incorrect state (e.g. not initialized) - */ - class launch_error : public msglink_error - { - using msglink_error::msglink_error; - }; /** - * @brief tried to access invalid connection instance - * (wspp callback with unknown connection handle) + * @brief exception used to indicate an unexpected + * error code occurred like e.g. in some asio-related operation. + * This uses std::error_code which is the same as + * asio::error_code and therefor also wspp::error_code in modern C++. + * Multiple error code enums might be used though. */ - class invalid_connection_error: public msglink_error + class unexpected_error : public msglink_error { - using msglink_error::msglink_error; + public: + + std::error_code m_code; // should be just std::error_code on modern systems + + unexpected_error(const std::error_code &_ec) + : msglink_error(_ec.message()) + , m_code(_ec) + {} + + std::error_code code() const noexcept { + return m_code; + } }; } // namespace el::msglink diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index b56b2ad..8a673cd 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -15,14 +15,16 @@ msglink server class #define ASIO_STANDALONE +#include +#include #include #include -#include -#include -#include -#include #include #include +#include +#include + +#include #include #include @@ -41,6 +43,15 @@ namespace el::msglink class server; class connection_handler; + /** + * @brief class that is instantiated for every connection. + * This is used inside the server class internally to perform + * actions that are specific to but needed for all open connections. + * + * Methods of this class are only allowed to be called from within + * the main asio loop, so from the handlers in the + * server class. + */ class connection_handler { friend class server; @@ -53,24 +64,100 @@ namespace el::msglink // a handle to the connection handled by this client wspp::connection_hdl m_connection; + // asio timer used to schedule keep-alive pings + std::shared_ptr m_ping_timer; + + private: // methods - public: + /** + * @brief upgrades the connection handle m_connection + * to a full connection ptr (shared ptr to con). + * Throws error if fails because invalid connection. + * This should never happen and is supposed to be + * caught in the main asio loop. + * + * Should never be called from outside server io loop. + * + * @return wsserver::connection_ptr the upgraded connection + */ + wsserver::connection_ptr get_connection() + { + return m_socket_server.get_con_from_hdl(m_connection); + } - connection_handler(wsserver &_socket_server, wspp::connection_hdl _connection) - : m_socket_server(_socket_server) - , m_connection(_connection) + /** + * @brief schedules a ping to be initiated + * after the configured ping interval. + * If a ping timer is active, it will be canceled. + * + */ + void schedule_ping() { - PRINT_CALL; + auto con = get_connection(); + + // cancel existing timer if one is set + if (m_ping_timer) + m_ping_timer->cancel(); + + m_ping_timer = con->set_timer(1000, std::bind(&connection_handler::handle_ping_timer, this, pl::_1)); + } + + /** + * @brief initiates a websocket ping. + * This is called periodically by timer. + */ + void handle_ping_timer(const std::error_code &_ec) + { + // if timer was canceled, do nothing. + if (_ec == wspp::transport::error::operation_aborted) // the set_timer method intercepts the handler and changes the code to a non-default asio one + return; + else if (_ec) + throw unexpected_error(_ec); + + std::cout << "ping timer" << std::endl; + + get_connection()->ping(""); // no message needed + + // start next ping for now + schedule_ping(); // TODO: move to pong handler } + public: + // connection handler is supposed to be instantiated in-place exactly once per // connection in the connection map. It should never be moved or copied. connection_handler(const connection_handler &) = delete; connection_handler(connection_handler &&) = delete; + /** + * @brief called during on_open when new connection + * is established. Used to initiate asynchronous + * actions. + * + * @param _socket_server + * @param _connection + */ + connection_handler(wsserver &_socket_server, wspp::connection_hdl _connection) + : m_socket_server(_socket_server) + , m_connection(_connection) + { + PRINT_CALL; + + // start the first ping + schedule_ping(); + } + + /** + * @brief called during on_close when connection is closed or terminated. + * Used to cancel any potential actions. + */ virtual ~connection_handler() { PRINT_CALL; + + // cancel ping timer if one is running + if (m_ping_timer) + m_ping_timer->cancel(); } /** @@ -81,17 +168,9 @@ namespace el::msglink void on_message(wsserver::message_ptr _msg) noexcept { std::cout << "message: " << _msg->get_payload() << std::endl; - m_socket_server.send(m_connection, _msg->get_payload(), _msg->get_opcode()); + get_connection()->send(_msg->get_payload(), _msg->get_opcode()); } - /** - * @brief initiates a websocket ping. - * This is called periodically by server thread. - */ - void initiate_ping() - { - m_socket_server.ping(m_connection, ""); // no message needed - } }; class server @@ -125,16 +204,6 @@ namespace el::msglink std::owner_less > m_open_connections; - // connection processing thread. This thread is responsible for - // some housekeeping work such as keepalive for all the connections. - std::thread m_processing_thread; - - // mutex and condition variable guarded flag to tell the thread - // when to exit - bool m_threxit; - std::mutex m_threxit_mutex; - std::condition_variable m_threxit_cv; - private: /** @@ -164,7 +233,7 @@ namespace el::msglink if (m_server_state != RUNNING) return; - // forward message to client handler + // forward message to connection handler try { m_open_connections.at(_hdl).on_message(_msg); @@ -208,54 +277,12 @@ namespace el::msglink con->terminate(std::make_error_code(std::errc::timed_out)); } - /** - * @brief function of the connection processing thread - */ - void processing_thread_fn() noexcept - { - try - { - for (;;) - { - std::unique_lock lock(m_threxit_mutex); - m_threxit_cv.wait_for(lock, 10s); - // mutex is now locked regardless of timeout or not - if (m_threxit) - break; - - // if server is not running there is nothing to do - if (m_server_state != RUNNING) - continue; - - // perform routine task - std::cout << "routine" << std::endl; - - for (auto &[connection_handle, handler] : m_open_connections) - { - handler.initiate_ping(); - } - - } - - std::cout << "thread exit" << std::endl; - } - catch (const std::exception &e) - { - // TODO: park exception here and re-raise it in run() for user handling - std::cout << "Exception in server thread: " << el::logging::format_exception(e) << std::endl; - } - - } - public: server(int _port) : m_port(_port) { PRINT_CALL; - - // start processing thread - m_processing_thread = std::thread(std::bind(&server::processing_thread_fn, this)); } // never copy or move a server @@ -265,17 +292,6 @@ namespace el::msglink ~server() { PRINT_CALL; - - // order processing thread to exit - { - std::lock_guard lock(m_threxit_mutex); - m_threxit = true; - } - m_threxit_cv.notify_one(); - // wait for thread to exit - if (m_processing_thread.joinable()) - m_processing_thread.join(); - } /** @@ -335,7 +351,7 @@ namespace el::msglink try { - // listening happens on port defined in settings + // listen on configured port m_socket_server.listen(m_port); // start accepting @@ -360,10 +376,10 @@ namespace el::msglink } /** - * @brief stops the server if it is running and does nothing - * otherwise. + * @brief stops the server if it is running, does nothing + * otherwise (if it's not running). * - * This can be called from any thread, wspp is thread safe. + * This can be called from any thread. (TODO: make sure using mutex) * * @throws msglink::socket_error networking error occurred while stopping server * @throws other msglink::msglink_error? diff --git a/include/el/msglink/wspp.hpp b/include/el/msglink/wspp.hpp index 465b2c7..eeaa77c 100644 --- a/include/el/msglink/wspp.hpp +++ b/include/el/msglink/wspp.hpp @@ -24,6 +24,40 @@ namespace el::msglink // typedefs and namespaces to make code more readably namespace wspp = websocketpp; namespace pl = std::placeholders; - typedef wspp::server wsserver; + + struct wspp_config : public wspp::config::asio + { + typedef wspp_config type; // apply standard names for what is this config type + typedef asio base; // standard name for what this config is based on (the default asio config) + + typedef base::concurrency_type concurrency_type; + + typedef base::request_type request_type; + typedef base::response_type response_type; + + typedef base::message_type message_type; + typedef base::con_msg_manager_type con_msg_manager_type; + typedef base::endpoint_msg_manager_type endpoint_msg_manager_type; + + typedef base::alog_type alog_type; + typedef base::elog_type elog_type; + + typedef base::rng_type rng_type; + + struct transport_config : public base::transport_config { + typedef type::concurrency_type concurrency_type; + typedef type::alog_type alog_type; + typedef type::elog_type elog_type; + typedef type::request_type request_type; + typedef type::response_type response_type; + typedef websocketpp::transport::asio::basic_socket::endpoint + socket_type; + }; + + typedef websocketpp::transport::asio::endpoint + transport_type; + }; + + typedef wspp::server wsserver; } // namespace el::msglink \ No newline at end of file From 0c4afd7e652ea58cf80422fa73e9fdc47f885a12 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 5 Nov 2023 18:15:21 +0100 Subject: [PATCH 07/50] properly imlemented ping/pong loop for keep-alive --- include/el/msglink/server.hpp | 103 ++++++++++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 12 deletions(-) diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index 8a673cd..b96e8c2 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -115,11 +115,12 @@ namespace el::msglink throw unexpected_error(_ec); std::cout << "ping timer" << std::endl; - + + // send a ping get_connection()->ping(""); // no message needed - // start next ping for now - schedule_ping(); // TODO: move to pong handler + // when a ping is received in the pong handler, a new ping will + // be scheduled. } public: @@ -171,6 +172,36 @@ namespace el::msglink get_connection()->send(_msg->get_payload(), _msg->get_opcode()); } + /** + * @brief called by server when a pong message arrives for this connection. + * This is used to test if the connection is still alive + * + * @param _payload the pong payload (not used) + */ + void on_pong_received(std::string &_payload) + { + // pong arrived in time, all good, connection alive + + // schedule a new ping to be sent a bit later. + schedule_ping(); + } + + /** + * @brief called by server when an expected pong message (given the sent + * ping messages) has not arrived in time. When this happens, + * the connection is considered dead and will be terminated. + * + * @param _expected_payload the expected payload from the ping message + */ + void on_pong_timeout(std::string &_expected_payload) + { + // terminate connection + get_connection()->terminate(std::make_error_code(std::errc::timed_out)); + + // WARNING: connection_handler instance is destroyed before the terminate() call returns. + // Don't use it here anymore! + } + }; class server @@ -208,7 +239,7 @@ namespace el::msglink /** * @brief new websocket connection opened (fully connected) - * + * This instantiates a connection handler. * @param hdl websocket connection handle */ void on_open(wspp::connection_hdl _hdl) @@ -226,6 +257,15 @@ namespace el::msglink ); } + + /** + * @brief message received from a connection. + * This forwards the call to the appropriate connection handler + * or throws if the connection is invalid. + * + * @param _hdl ws connection handle + * @param _msg message that was received + */ void on_message(wspp::connection_hdl _hdl, wsserver::message_ptr _msg) { PRINT_CALL; @@ -242,8 +282,16 @@ namespace el::msglink { throw invalid_connection_error("Received message from unknown/invalid connection."); } - } + + /** + * @brief websocket connection has been closed, + * Whether gracefully or dropped. This deletes + * the associated connection handler and therefore + * stops any tasks going on with that connection. + * + * @param _hdl ws connection handle that has been closed + */ void on_close(wspp::connection_hdl _hdl) { PRINT_CALL; @@ -257,24 +305,54 @@ namespace el::msglink throw invalid_connection_error("Attempted to close an unknown/invalid connection which doesn't seem to exist."); } } + + /** + * @brief called by wspp when a pong is received. + * This is forwarded to the connection handler. + * + * @param _hdl handle to associated ws connection + */ + void on_pong_received(wspp::connection_hdl _hdl, std::string _payload) + { + PRINT_CALL; + + if (m_server_state != RUNNING) + return; + + // forward message to connection handler + try + { + m_open_connections.at(_hdl).on_pong_received(_payload); + } + catch (const std::out_of_range &e) + { + throw invalid_connection_error("Received pong from unknown/invalid connection."); + } + } /** * @brief called by wspp when a pong message times out. This is - * used by the keepalive system to detect connection loss + * used by the keepalive system to detect connection loss. + * This call is forwarded to connection handler. * * @param _hdl handle to connection where timeout occurred */ - void on_pong_timeout(wspp::connection_hdl _hdl) + void on_pong_timeout(wspp::connection_hdl _hdl, std::string _expected_payload) { PRINT_CALL; if (m_server_state != RUNNING) return; - // if we already timed out, terminate the connection with no handshake - // (this will still call close handler) - wsserver::connection_ptr con = m_socket_server.get_con_from_hdl(_hdl); - con->terminate(std::make_error_code(std::errc::timed_out)); + // forward message to connection handler + try + { + m_open_connections.at(_hdl).on_pong_timeout(_expected_payload); + } + catch (const std::out_of_range &e) + { + throw invalid_connection_error("Pong timeout on unknown/invalid connection."); + } } public: @@ -320,7 +398,8 @@ namespace el::msglink m_socket_server.set_open_handler(std::bind(&server::on_open, this, pl::_1)); m_socket_server.set_message_handler(std::bind(&server::on_message, this, pl::_1, pl::_2)); m_socket_server.set_close_handler(std::bind(&server::on_close, this, pl::_1)); - m_socket_server.set_pong_timeout_handler(std::bind(&server::on_pong_timeout, this, pl::_1)); + m_socket_server.set_pong_handler(std::bind(&server::on_pong_received, this, pl::_1, pl::_2)); + m_socket_server.set_pong_timeout_handler(std::bind(&server::on_pong_timeout, this, pl::_1, pl::_2)); // set reuse addr flag to allow faster restart times m_socket_server.set_reuse_addr(true); From 5d5a3430d942afb1c5213700ba32980b040b5849 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 5 Nov 2023 22:31:16 +0100 Subject: [PATCH 08/50] started moving codable and link/event systems into library from experimentation repo --- include/el/codable.hpp | 190 +++++++++++++++++++++++++++++++++++ include/el/metaprog.hpp | 150 +++++++++++++++++++++++++++ include/el/msglink/README.md | 18 +++- include/el/msglink/link.hpp | 16 +++ 4 files changed, 373 insertions(+), 1 deletion(-) create mode 100644 include/el/codable.hpp create mode 100644 include/el/metaprog.hpp create mode 100644 include/el/msglink/link.hpp diff --git a/include/el/codable.hpp b/include/el/codable.hpp new file mode 100644 index 0000000..5dfae52 --- /dev/null +++ b/include/el/codable.hpp @@ -0,0 +1,190 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +05.11.23, 18:28 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Codable helper classes and macros for defining structures +and classes that can be encoded to and/or decoded from json. + +This functionality is based on Niels Lohmann's JSON for modern C++ library +and depends on it. It must be includable as follows: + +#include +*/ + +#pragma once + +#include + +#include + +#include + + +namespace el +{ + /** + * @brief Class interface for + * decodable structures and classes. + * + * This also provides the converter function from_json() + * which allows integration with the nlohmann::json library's + * builtin conversion system. This generates an operator allowing + * the assignment of a json object to any decodable object. + */ + class decodable + { + protected: + virtual ~decodable() = default; + + public: + /** + * @brief function which decodes the decodable + * object from json-encoded data + * + * @param _output the json instance to decode. + * This can be a number, object, list or whatever json type + * is used to represent this codable. Invalid type will throw + * a decoder exception. + */ + virtual void _el_codable_decode(const nlohmann::json &_input) = 0; + + /** + * @brief function to convert this decodable from json using + * the functionality provided by the nlohmann::json library + * + * @param _j_output json instance to decode + * @param _t_input decodable to decode from json + */ + friend void from_json(const nlohmann::json &_j_input, decodable &_t_output) + { + _t_output._el_codable_decode(_j_input); + }; + }; + + /** + * @brief Class interface for + * encodable structures and classes. + * + * This also provides the converter function to_json() + * which allows integration with the nlohmann::json library's + * builtin conversion system. This generates an operator allowing + * the assignment of any decodable object to a json object. + */ + class encodable + { + protected: + virtual ~encodable() = default; + + public: + + /** + * @brief function which encodes the encodable's + * members to json. + * + * @param _output the json instance to save the json encoded object to. + * This might be converted to number, object, list or whatever json type + * is used to represent this encodable. + */ + virtual void _el_codable_encode(nlohmann::json &_output) const = 0; + + /** + * @brief function to convert this encodable to json using + * the functionality provided by the nlohmann::json library + * + * @param _j_output json instance to encode to + * @param _t_input encodable to encode + */ + friend void to_json(nlohmann::json &_j_output, const encodable &_t_input) + { + _t_input._el_codable_encode(_j_output); + } + }; + + /** + * @brief Class interface for + * codable structures and classes. + * + * This combines encodable and decodable + * for objects that need to be both en- and decoded. + */ + class codable : public encodable, public decodable + { + protected: + virtual ~codable() = default; + }; + + +/** + * Automatic constructor generation + * + */ + +// (private) generates a constructor argument for a structure member (of a codable) +#define __EL_CODABLE_CONSTRUCTOR_ARG(member) decltype(member) & _ ## member, +// (private) generates a constructor initializer list entry for a member from the above defined argument +#define __EL_CODABLE_CONSTRUCTOR_INIT(member) member(_ ## member), + +// (public) generates a constructor for a given structure which initializes the given members +#define EL_CODABLE_GENERATE_CONSTRUCTORS(TypeName, ...) \ + private: \ + inline int __el_codable_ctorgen_dummy; \ + public: \ + TypeName() = default; \ + TypeName(EL_METAPROG_DO_FOR_EACH(__EL_CODABLE_CONSTRUCTOR_ARG, __VA_ARGS__) char __dummy = 0) \ + : EL_METAPROG_DO_FOR_EACH(__EL_CODABLE_CONSTRUCTOR_INIT, __VA_ARGS__) \ + __el_codable_ctorgen_dummy(__dummy) /* dummy is because comma left by macro */ \ + {} + +/** + * Encoding/Decoding code generation + * + */ + +// (private) generates code which uses a member's encoder function to add it to a json object +#define __EL_CODABLE_ENCODE_KEY(member) encode_ ## member (_output[#member]); +// (private) generates code which uses a member's decoder function to retrieve it's value from a json object +#define __EL_CODABLE_DECODE_KEY(member) decode_ ## member (_input.at(#member)); + +// (public) generates the declaration of the encoder method for a specific member +#define EL_ENCODER(member) void encode_ ## member (nlohmann::json &encoded_data) const +// (public) generates the declaration of the decoder method for a specific member +#define EL_DECODER(member) void decode_ ## member (const nlohmann::json &encoded_data) + +// (private) generates the default encoder/decoder functions for a class member +#define __EL_CODABLE_DEFINE_DEFAULT_CONVERTERS(member) \ + /* these dummy templates make this function less specialized than one without, \ + so the user can manually define their encoder which will take precedence over \ + this one */ \ + template \ + EL_ENCODER(member) \ + { \ + encoded_data = member; \ + } \ + template \ + EL_DECODER(member) \ + { \ + member = encoded_data; \ + } + +// (public) generates the methods necessary to make a structure codable. Only the provided +// members will be made encodable/decodable, the others will not be touched. +#define EL_DEFINE_CODABLE(Name, ...) \ + \ + EL_METAPROG_DO_FOR_EACH(__EL_CODABLE_DEFINE_DEFAULT_CONVERTERS, __VA_ARGS__) \ + \ + virtual void _el_codable_encode(nlohmann::json &_output) const override \ + { \ + EL_METAPROG_DO_FOR_EACH(__EL_CODABLE_ENCODE_KEY, __VA_ARGS__) \ + } \ + virtual void _el_codable_decode(const nlohmann::json &_input) override \ + { \ + EL_METAPROG_DO_FOR_EACH(__EL_CODABLE_DECODE_KEY, __VA_ARGS__) \ + } + +} // namespace el \ No newline at end of file diff --git a/include/el/metaprog.hpp b/include/el/metaprog.hpp new file mode 100644 index 0000000..1d7f089 --- /dev/null +++ b/include/el/metaprog.hpp @@ -0,0 +1,150 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +05.11.23, 18:34 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +A collection of macros allowing basic macro-based metaprogramming. +*/ + +#pragma once + + +// based on macros from nlohmann's JSON for modern C++ library + +#define EL_METAPROG_EXPAND( x ) x +#define EL_METAPROG_GET_MACRO(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, NAME,...) NAME +#define EL_METAPROG_PASTE(...) EL_METAPROG_EXPAND(EL_METAPROG_GET_MACRO(__VA_ARGS__, \ + EL_METAPROG_PASTE64, \ + EL_METAPROG_PASTE63, \ + EL_METAPROG_PASTE62, \ + EL_METAPROG_PASTE61, \ + EL_METAPROG_PASTE60, \ + EL_METAPROG_PASTE59, \ + EL_METAPROG_PASTE58, \ + EL_METAPROG_PASTE57, \ + EL_METAPROG_PASTE56, \ + EL_METAPROG_PASTE55, \ + EL_METAPROG_PASTE54, \ + EL_METAPROG_PASTE53, \ + EL_METAPROG_PASTE52, \ + EL_METAPROG_PASTE51, \ + EL_METAPROG_PASTE50, \ + EL_METAPROG_PASTE49, \ + EL_METAPROG_PASTE48, \ + EL_METAPROG_PASTE47, \ + EL_METAPROG_PASTE46, \ + EL_METAPROG_PASTE45, \ + EL_METAPROG_PASTE44, \ + EL_METAPROG_PASTE43, \ + EL_METAPROG_PASTE42, \ + EL_METAPROG_PASTE41, \ + EL_METAPROG_PASTE40, \ + EL_METAPROG_PASTE39, \ + EL_METAPROG_PASTE38, \ + EL_METAPROG_PASTE37, \ + EL_METAPROG_PASTE36, \ + EL_METAPROG_PASTE35, \ + EL_METAPROG_PASTE34, \ + EL_METAPROG_PASTE33, \ + EL_METAPROG_PASTE32, \ + EL_METAPROG_PASTE31, \ + EL_METAPROG_PASTE30, \ + EL_METAPROG_PASTE29, \ + EL_METAPROG_PASTE28, \ + EL_METAPROG_PASTE27, \ + EL_METAPROG_PASTE26, \ + EL_METAPROG_PASTE25, \ + EL_METAPROG_PASTE24, \ + EL_METAPROG_PASTE23, \ + EL_METAPROG_PASTE22, \ + EL_METAPROG_PASTE21, \ + EL_METAPROG_PASTE20, \ + EL_METAPROG_PASTE19, \ + EL_METAPROG_PASTE18, \ + EL_METAPROG_PASTE17, \ + EL_METAPROG_PASTE16, \ + EL_METAPROG_PASTE15, \ + EL_METAPROG_PASTE14, \ + EL_METAPROG_PASTE13, \ + EL_METAPROG_PASTE12, \ + EL_METAPROG_PASTE11, \ + EL_METAPROG_PASTE10, \ + EL_METAPROG_PASTE9, \ + EL_METAPROG_PASTE8, \ + EL_METAPROG_PASTE7, \ + EL_METAPROG_PASTE6, \ + EL_METAPROG_PASTE5, \ + EL_METAPROG_PASTE4, \ + EL_METAPROG_PASTE3, \ + EL_METAPROG_PASTE2, \ + EL_METAPROG_PASTE1)(__VA_ARGS__)) +#define EL_METAPROG_PASTE2(func, v1) func(v1) +#define EL_METAPROG_PASTE3(func, v1, v2) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE2(func, v2) +#define EL_METAPROG_PASTE4(func, v1, v2, v3) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE3(func, v2, v3) +#define EL_METAPROG_PASTE5(func, v1, v2, v3, v4) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE4(func, v2, v3, v4) +#define EL_METAPROG_PASTE6(func, v1, v2, v3, v4, v5) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE5(func, v2, v3, v4, v5) +#define EL_METAPROG_PASTE7(func, v1, v2, v3, v4, v5, v6) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE6(func, v2, v3, v4, v5, v6) +#define EL_METAPROG_PASTE8(func, v1, v2, v3, v4, v5, v6, v7) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE7(func, v2, v3, v4, v5, v6, v7) +#define EL_METAPROG_PASTE9(func, v1, v2, v3, v4, v5, v6, v7, v8) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE8(func, v2, v3, v4, v5, v6, v7, v8) +#define EL_METAPROG_PASTE10(func, v1, v2, v3, v4, v5, v6, v7, v8, v9) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE9(func, v2, v3, v4, v5, v6, v7, v8, v9) +#define EL_METAPROG_PASTE11(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE10(func, v2, v3, v4, v5, v6, v7, v8, v9, v10) +#define EL_METAPROG_PASTE12(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE11(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) +#define EL_METAPROG_PASTE13(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE12(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) +#define EL_METAPROG_PASTE14(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE13(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) +#define EL_METAPROG_PASTE15(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE14(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) +#define EL_METAPROG_PASTE16(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE15(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) +#define EL_METAPROG_PASTE17(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE16(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) +#define EL_METAPROG_PASTE18(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE17(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17) +#define EL_METAPROG_PASTE19(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE18(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18) +#define EL_METAPROG_PASTE20(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE19(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19) +#define EL_METAPROG_PASTE21(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE20(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) +#define EL_METAPROG_PASTE22(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE21(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21) +#define EL_METAPROG_PASTE23(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE22(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22) +#define EL_METAPROG_PASTE24(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE23(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23) +#define EL_METAPROG_PASTE25(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE24(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24) +#define EL_METAPROG_PASTE26(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE25(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25) +#define EL_METAPROG_PASTE27(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE26(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26) +#define EL_METAPROG_PASTE28(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE27(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27) +#define EL_METAPROG_PASTE29(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE28(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28) +#define EL_METAPROG_PASTE30(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE29(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29) +#define EL_METAPROG_PASTE31(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE30(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30) +#define EL_METAPROG_PASTE32(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE31(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31) +#define EL_METAPROG_PASTE33(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE32(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32) +#define EL_METAPROG_PASTE34(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE33(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33) +#define EL_METAPROG_PASTE35(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE34(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34) +#define EL_METAPROG_PASTE36(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE35(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35) +#define EL_METAPROG_PASTE37(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE36(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36) +#define EL_METAPROG_PASTE38(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE37(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37) +#define EL_METAPROG_PASTE39(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE38(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38) +#define EL_METAPROG_PASTE40(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE39(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39) +#define EL_METAPROG_PASTE41(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE40(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40) +#define EL_METAPROG_PASTE42(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE41(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41) +#define EL_METAPROG_PASTE43(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE42(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42) +#define EL_METAPROG_PASTE44(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE43(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43) +#define EL_METAPROG_PASTE45(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE44(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44) +#define EL_METAPROG_PASTE46(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE45(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45) +#define EL_METAPROG_PASTE47(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE46(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46) +#define EL_METAPROG_PASTE48(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE47(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47) +#define EL_METAPROG_PASTE49(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE48(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48) +#define EL_METAPROG_PASTE50(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE49(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49) +#define EL_METAPROG_PASTE51(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE50(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50) +#define EL_METAPROG_PASTE52(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE51(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51) +#define EL_METAPROG_PASTE53(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE52(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52) +#define EL_METAPROG_PASTE54(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE53(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53) +#define EL_METAPROG_PASTE55(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE54(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54) +#define EL_METAPROG_PASTE56(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE55(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55) +#define EL_METAPROG_PASTE57(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE56(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56) +#define EL_METAPROG_PASTE58(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE57(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57) +#define EL_METAPROG_PASTE59(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE58(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58) +#define EL_METAPROG_PASTE60(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE59(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59) +#define EL_METAPROG_PASTE61(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE60(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60) +#define EL_METAPROG_PASTE62(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE61(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61) +#define EL_METAPROG_PASTE63(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE62(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62) +#define EL_METAPROG_PASTE64(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE63(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63) + +#define EL_METAPROG_DO_FOR_EACH(func, ...) EL_METAPROG_EXPAND(EL_METAPROG_PASTE(func, __VA_ARGS__)) diff --git a/include/el/msglink/README.md b/include/el/msglink/README.md index 473ee9e..0bea49b 100644 --- a/include/el/msglink/README.md +++ b/include/el/msglink/README.md @@ -75,4 +75,20 @@ Note for future me: If msglink doesn't fit for some reason in the future, here a ## Link collection -- wspp client reconnect: https://github.com/zaphoyd/websocketpp/issues/754#issue-353706390 \ No newline at end of file +- wspp client reconnect: https://github.com/zaphoyd/websocketpp/issues/754#issue-353706390 + + +## TODOS + +```cpp + // TODO: next up is moving link and event to el .hpp files. Then add some more macros, a separate event class and more + // event define functions so a user can decide wether they want events to be just en/de codable and if they should + // be just outgoing/incoming or both. + // Also maybe add the possibility for an event handler as a method of the event class (maybe, not sure if so many options + // are a good idea). + // Then actually add this link support to the msglink server. + // Then implement a corresponding msglink client (reconnects, ...) + // Then implement state-management calls on a link (e.g. on_connect/on_disconnect, possibly a "currently not connected but attempting to reconnect, don't give up jet" state) + // Then add support for data subscriptions (they need more state management (e.g. requests ) in their own classes) + // Then add support for remote procedure calls. +``` \ No newline at end of file diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp new file mode 100644 index 0000000..597a505 --- /dev/null +++ b/include/el/msglink/link.hpp @@ -0,0 +1,16 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +05.11.23, 18:26 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +This file implements the link class which can be inherited by +the user to define the API of a link (Can be used on client and server side). +*/ + +#pragma once + From 0f48b2452ed7ce6eff5113888a50f9a69ec92988 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 12 Nov 2023 00:12:18 +0100 Subject: [PATCH 09/50] added simple event and link classes --- include/el/msglink/README.md | 4 +- include/el/msglink/event.hpp | 34 +++++++++++ include/el/msglink/link.hpp | 115 ++++++++++++++++++++++++++++++++++- 3 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 include/el/msglink/event.hpp diff --git a/include/el/msglink/README.md b/include/el/msglink/README.md index 0bea49b..5c6136c 100644 --- a/include/el/msglink/README.md +++ b/include/el/msglink/README.md @@ -81,8 +81,8 @@ Note for future me: If msglink doesn't fit for some reason in the future, here a ## TODOS ```cpp - // TODO: next up is moving link and event to el .hpp files. Then add some more macros, a separate event class and more - // event define functions so a user can decide wether they want events to be just en/de codable and if they should + // DONE: next up is moving link and event to el .hpp files. + // TODO: Then add some more macros, a separate event class and more event define functions so a user can decide wether they want events to be just en/de codable and if they should // be just outgoing/incoming or both. // Also maybe add the possibility for an event handler as a method of the event class (maybe, not sure if so many options // are a good idea). diff --git a/include/el/msglink/event.hpp b/include/el/msglink/event.hpp new file mode 100644 index 0000000..23a7183 --- /dev/null +++ b/include/el/msglink/event.hpp @@ -0,0 +1,34 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +11.11.23, 23:00 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +msglink event class used to define custom events +*/ + +#pragma once + +#include + + +namespace el::msglink +{ + +#define EL_MSGLINK_DEFINE_EVENT(TypeName, ...) \ + static inline const char *_event_name = #TypeName; \ + \ + EL_DEFINE_CODABLE(TypeName, __VA_ARGS__) + + /** + * @brief base class for all msglink event definition classes + * + */ + struct event : public el::codable + { + }; +} // namespace el::msglink diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index 597a505..9e8ad90 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -9,8 +9,121 @@ This source code is licensed under the Apache-2.0 license found in the LICENSE file in the root directory of this source tree. This file implements the link class which can be inherited by -the user to define the API of a link (Can be used on client and server side). +the user to define the API/protocol of a link +(Can be used on client and server side). */ #pragma once +#include +#include +#include + +#include + +#include +#include + + +namespace el::msglink +{ + /** + * @brief class that defines the custom communication "protocol" + * used to communicate with the other party. + * The link class defines which events, data subscriptions and functions + * can be transmitted and/or received from/to the other communication party. + * It is also responsible for defining the data structure and type associated + * with each of those interactions. + * + * Both clients and server have to define the link in order to be + * able to communicate. Ideally, the client and server links would match up + * meaning that for every e.g. event one party can send, the other party knows of + * and can receive this event. + * + * If a link receives any interaction from the other party that it either doesn't + * known of, cannot decode or otherwise interpret, an error is automatically + * reported to the sending party so a language-dependent error handling scheme such + * as an exception can catch it. + * + */ + class link + { + private: + + // type of the lambda used to wrap event handlers + using event_handler_wrapper_t = std::function; + + // map of event name to handler function + std::unordered_map< + std::string, + event_handler_wrapper_t + > handler_list; + + protected: + + /** + * @brief Method for registering a link-method event handler + * for an event. The event handler must be a method + * of the link it is registered on. This is a shortcut + * to avoid having to use std::bind to bind every handler + * to the instance. When an external handler is needed, this + * is the wrong overload. + * + * Method function pointer: + * https://isocpp.org/wiki/faq/pointers-to-members#typedef-for-ptr-to-memfn + * + * @tparam _ET the event class of the event to register + * (must inherit from el::msglink::event, can be deduced from method parameter) + * @tparam _LT the link class the handler is a method of (can also be deduced) + * @param _handler the handler method for the event + */ + template _ET, std::derived_from _LT> + void define_event(void (_LT::*_handler)(_ET &)) + { + //EL_LOGD("defined an event"); + + std::function handler = _handler; + + handler_list.emplace( + _ET::_event_name, + [this, handler](const nlohmann::json &_data) { + std::cout << "hievent " << _data << std::endl; + _ET new_event_inst; + new_event_inst = _data; + handler( + static_cast<_LT*>(this), + new_event_inst + ); + } + ); + } + + + public: + + /** + * @brief valid link definitions must implement this define method + * to define the protocol by calling the specialized define + * methods for events and other interactions. + * + */ + virtual void define() noexcept = 0; + + void on_message(const std::string &_msg_content) + { + try + { + nlohmann::json jmsg = nlohmann::json::parse(_msg_content); + std::string event_name = jmsg.at("event_name"); + nlohmann::json event_data = jmsg.at("event_data"); + handler_list.at(event_name)(event_data); + } + catch (const std::exception &e) + { + std::cout << "exception in link" << std::endl; + //EL_LOG_EXCEPTION_MSG("Exception occurred while processing event message", e); + } + } + }; + +} // namespace el::msglink From 96f0e85eac7ed556258fb4c29cf5686bd330c9d1 Mon Sep 17 00:00:00 2001 From: melektron Date: Wed, 15 Nov 2023 13:14:58 +0100 Subject: [PATCH 10/50] defined seperate classes for incoming and outgoing event --- include/el/codable.hpp | 48 ++++++++++++++++------ include/el/msglink/event.hpp | 77 ++++++++++++++++++++++++++++++++---- 2 files changed, 106 insertions(+), 19 deletions(-) diff --git a/include/el/codable.hpp b/include/el/codable.hpp index 5dfae52..eda9674 100644 --- a/include/el/codable.hpp +++ b/include/el/codable.hpp @@ -156,8 +156,8 @@ namespace el // (public) generates the declaration of the decoder method for a specific member #define EL_DECODER(member) void decode_ ## member (const nlohmann::json &encoded_data) -// (private) generates the default encoder/decoder functions for a class member -#define __EL_CODABLE_DEFINE_DEFAULT_CONVERTERS(member) \ +// (private) generates the default encoder method for a member +#define __EL_CODABLE_DEFINE_DEFAULT_ENCODER(member) \ /* these dummy templates make this function less specialized than one without, \ so the user can manually define their encoder which will take precedence over \ this one */ \ @@ -165,26 +165,50 @@ namespace el EL_ENCODER(member) \ { \ encoded_data = member; \ - } \ + } + +// (private) generates the default decoder method for a member +#define __EL_CODABLE_DEFINE_DEFAULT_DECODER(member) \ + /* these dummy templates make this function less specialized than one without, \ + so the user can manually define their encoder which will take precedence over \ + this one */ \ template \ EL_DECODER(member) \ { \ member = encoded_data; \ } -// (public) generates the methods necessary to make a structure codable. Only the provided -// members will be made encodable/decodable, the others will not be touched. -#define EL_DEFINE_CODABLE(Name, ...) \ +// (private) generates the default encoder/decoder methods for a class member +#define __EL_CODABLE_DEFINE_DEFAULT_CONVERTERS(member) \ + __EL_CODABLE_DEFINE_DEFAULT_ENCODER(member) \ + __EL_CODABLE_DEFINE_DEFAULT_DECODER(member) + +// (public) generates the methods necessary to make a structure encodable. +// Only the provided members will be made encodable, the others will not be touched. +#define EL_DEFINE_ENCODABLE(Name, ...) \ \ - EL_METAPROG_DO_FOR_EACH(__EL_CODABLE_DEFINE_DEFAULT_CONVERTERS, __VA_ARGS__) \ + EL_METAPROG_DO_FOR_EACH(__EL_CODABLE_DEFINE_DEFAULT_ENCODER, __VA_ARGS__) \ \ - virtual void _el_codable_encode(nlohmann::json &_output) const override \ + virtual void _el_codable_encode(nlohmann::json &_output) const override \ { \ - EL_METAPROG_DO_FOR_EACH(__EL_CODABLE_ENCODE_KEY, __VA_ARGS__) \ - } \ - virtual void _el_codable_decode(const nlohmann::json &_input) override \ + EL_METAPROG_DO_FOR_EACH(__EL_CODABLE_ENCODE_KEY, __VA_ARGS__) \ + } + +// (public) generates the methods necessary to make a structure decodable. +// Only the provided members will be made decodable, the others will not be touched. +#define EL_DEFINE_DECODABLE(Name, ...) \ + \ + EL_METAPROG_DO_FOR_EACH(__EL_CODABLE_DEFINE_DEFAULT_DECODER, __VA_ARGS__) \ + \ + virtual void _el_codable_decode(const nlohmann::json &_input) override \ { \ - EL_METAPROG_DO_FOR_EACH(__EL_CODABLE_DECODE_KEY, __VA_ARGS__) \ + EL_METAPROG_DO_FOR_EACH(__EL_CODABLE_DECODE_KEY, __VA_ARGS__) \ } +// (public) generates the methods necessary to make a structure codable (encodable and decodable). +// Only the provided members will be made encodable/decodable, the others will not be touched. +#define EL_DEFINE_CODABLE(Name, ...) \ + EL_DEFINE_ENCODABLE(Name, __VA_ARGS__) \ + EL_DEFINE_DECODABLE(Name, __VA_ARGS__) \ + } // namespace el \ No newline at end of file diff --git a/include/el/msglink/event.hpp b/include/el/msglink/event.hpp index 23a7183..9d05001 100644 --- a/include/el/msglink/event.hpp +++ b/include/el/msglink/event.hpp @@ -18,17 +18,80 @@ msglink event class used to define custom events namespace el::msglink { - -#define EL_MSGLINK_DEFINE_EVENT(TypeName, ...) \ + + + /** + * @brief base class for all incoming msglink event + * definition classes. To create an incoming event define a class inheriting from this one. + * Then use the EL_MSGLINK_DEFINE_INCOMING_EVENT macro to generate the required boilerplate. + * Incoming events are el::decodable[s], + * meaning they must be decodable from json. If a decoder for a member cannot + * be generated automatically or needs to be altered, the EL_DECODER macro can be + * used like with codables to manually define the decoder. + */ + struct incoming_event : public el::decodable + { + virtual ~incoming_event() = default; + + // dummy method which must be overriden to ensure the correct + // generate macro is used. + virtual void _el_msglink_is_incoming_dummy() const noexcept = 0; + }; + +// (public) generates the necessary boilerplate code for an incoming event class. +// The members listed in the arguments will be made decodable using el::decodable +// and are part of the event's data. +#define EL_MSGLINK_DEFINE_INCOMING_EVENT(TypeName, ...) \ static inline const char *_event_name = #TypeName; \ - \ - EL_DEFINE_CODABLE(TypeName, __VA_ARGS__) + virtual void _el_msglink_is_incoming_dummy() const noexcept override {} \ + EL_DEFINE_DECODABLE(TypeName, __VA_ARGS__) + + /** + * @brief base class for all outgoing msglink event + * definition classes. To create an outgoing event define a class inheriting from this one. + * Then use the EL_MSGLINK_DEFINE_OUTGOING_EVENT macro to generate the required boilerplate. + * Outgoing events are el::encodable[s], + * meaning they must be encodable to json. If a encoder for a member cannot + * be generated automatically or needs to be altered, the EL_ENCODER macro can be + * used like with codables to manually define the encoder. + */ + struct outgoing_event : public el::encodable + { + virtual ~outgoing_event() = default; + + // dummy method which must be overriden to ensure the correct + // generate macro is used. + virtual void _el_msglink_is_outgoing_dummy() const noexcept = 0; + }; + +// (public) generates the necessary boilerplate code for an outgoing event class. +// The members listed in the arguments will be made encodable using el::encodable +// and are part of the event's data. +#define EL_MSGLINK_DEFINE_OUTGOING_EVENT(TypeName, ...) \ + static inline const char *_event_name = #TypeName; \ + virtual void _el_msglink_is_outgoing_dummy() const noexcept override {} \ + EL_DEFINE_ENCODABLE(TypeName, __VA_ARGS__) /** - * @brief base class for all msglink event definition classes - * + * @brief base class for all bidirectional (incoming and outgoing) msglink event + * definition classes. It is simply a composite class of outgoing_event + * and incoming_event. This class must satisfy all the requirements of incoming and outgoing + * events. + * To create a bidirectional event define a class inheriting from this one. + * Then use the EL_MSGLINK_DEFINE_EVENT macro to generate the required boilerplate. */ - struct event : public el::codable + struct event : public incoming_event, public outgoing_event { + virtual ~event() = default; }; + +// (public) generates the necessary boilerplate code for an event class. +// The members listed in the arguments will be made codable using el::codable +// and are part of the event's data. +#define EL_MSGLINK_DEFINE_EVENT(TypeName, ...) \ + static inline const char *_event_name = #TypeName; \ + virtual void _el_msglink_is_incoming_dummy() const noexcept override {} \ + virtual void _el_msglink_is_outgoing_dummy() const noexcept override {} \ + EL_DEFINE_CODABLE(TypeName, __VA_ARGS__) + } // namespace el::msglink From e919ef8a9d3c70ea35bf33ce481f3f905ac90d37 Mon Sep 17 00:00:00 2001 From: melektron Date: Sat, 18 Nov 2023 22:04:20 +0100 Subject: [PATCH 11/50] worked a bit on defining requirements --- include/el/msglink/README.md | 89 +++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/include/el/msglink/README.md b/include/el/msglink/README.md index 5c6136c..8f945d3 100644 --- a/include/el/msglink/README.md +++ b/include/el/msglink/README.md @@ -29,7 +29,7 @@ One of the most annoying and often repetitive coding tasks when it comes to netw Most modern programming languages provide some sort of native or third party support for serializing and deserializing JSON, but the problem with JSON data received via the network is, that it is fully dynamic. You cannot be sure at the time of development what the JSON object will contain. So after parsing, it is required to manually go through the JSON object, checking that all it's fields match the type and restrictions required by your program. Then the data should ideally be extracted into some form of language-specific format like a struct in C/C++. -Luckily there are libraries that can help us simplify this task. The Python library PyDantic provides a way to elegantly define a JSON property's the type, value restrictions and optional (de)serializer functions and enables simple parsing and automatic validation of incoming data. Since everything is represented by classes, static type checkers can see the datatype of properties and provide excellent editor support. In Swift, the Codable protocol is natively supported and provides similar functionality. In some languages like C++ this is not quite as simple to represent but we can still simplify the process. +Luckily there are libraries that can help us simplify this task. The Python library PyDantic provides a way to elegantly define a JSON property's type, value restrictions and optional (de)serializer functions and enables simple parsing and automatic validation of incoming data. Since everything is represented by classes, static type checkers can see the datatype of properties and provide excellent editor support. In Swift, the Codable protocol is natively supported and provides similar functionality. In some languages like C++ this is not quite as simple to represent but we can still simplify the process. msgpack tries to implement and require these type definitions natively in it's implementation libraries, each in the style and with the features supported by the respective programming language. This way, every event has a clearly defined data structure passed along the event. Event listeners can access incoming data in the language-native format and rely on the fact that they receive what they expect. Event emitters on the other hand can pass data in the language-native format and will be forced to only emit valid data for a specific event. @@ -38,7 +38,7 @@ msgpack tries to implement and require these type definitions natively in it's i Traditionally, events have been identified by a simple string, it's name. There is nothing inherently wrong with this approach, but it introduces additional places to make mistakes. One may want to listen to the same event in multiple places of a program but might make a typo when identifying the event name or forget to update one listener after changing the name. -Language features such as enums, constants or TS literal types will solve this issue. However, msglink aims to integrate this as a requirement in it's implementation. This goes hand-in-hand nicely with the previous point, strict types. Every event has to have a defined and validatable data type which also defines the name of the event it is associated with. After defining it once, this event type can be used everywhere in the program (details depend on language implementation) to refer to this specific event, there cannot be typos in the event name and it is impossible emit events with the wrong data structure. +Language features such as enums, constants or TS literal types will solve this issue. msglink aims to integrate this as a requirement in it's implementation. This goes hand-in-hand nicely with the previous point, strict types. Every event has to have a defined and validatable data type which also defines the name of the event it is associated with. After defining it once, this event type can be used everywhere in the program (details depend on language implementation) to refer to this specific event, there cannot be typos in the event name and it is impossible emit events with the wrong data structure. ## Data subscriptions @@ -65,6 +65,91 @@ There are a few problems with manually implementing this using events. First of msglink avoids this by implementing the base functionality once and providing a language-specific and clean way to define procedures in one place with input data, result data and name. This is similar to [JSON-RPC](https://en.wikipedia.org/wiki/JSON-RPC) but provides the additional data validation and automatic parsing functionality described above. +## Decision criteria + +Which of the three options provided by msglink (events, data subscriptions, RPCs) to use depends on a few criteria: + +- **value or incident** focus: What's more important? The actual data value or the fact that it changed? + > If the focus is on the value of some datum, a data subscription should be used. It has different syntax to events allowing it to be easily used as a (possibly observable) remote-synced variable. + > + > If the focus is an incident which should cause some action to be performed by the other party, then an event is the way to go. Events also carry data, but are always associated with handler functions, so called listeners, which are called when an event is received. + > + > In short: The focus of events is to run some code when something happens while the focus of a data subscription is to have some data value that can be accessed at any time without worrying about updating it. +- **conditionality**: When and for how long is some data needed and when do events need to be transmitted? + > Both event listeners and data subscriptions offer a way to "enable" and "disable" them during the lifetime of the program. It is strongly encouraged for library implementations to use of language features such as scope and object lifetime to determine when events and data subscription updates are needed in a granular way. This can save on network bandwidth. + > + > When listening to an event, a handler function (listener) is registered which is then called whenever an event is received from the other party. Registering a listener may yield an object or handle representing it. This object can then be used to manage the lifetime of the listener. For example in C++, if an event is only needed inside a class instance, the listener object should be a member of that class and be unregistered whenever the class instance is destroyed (goes out of scope). + > + > When subscribing to some data using data subscriptions, a similar object/handle will be created. Again in C++, it might be used to directly access the data using the arrow operator and manage the lifetime of the data subscription like the event listener object. +- **uniqueness**: Is a piece of data/event unique or are there multiple multiple different ones with the same structure and meaning? + > Uniqueness means, that exactly one of something exists. + > + > In the msglink protocol, events (not event instances) are unique entities. Let's say, there is an event called "device_connect". In the entire application, there is only one such event with a clearly defined data structure associated with it. However, when emitting the event, the data value may be different every time. For example this event might have a "device_id" parameter which uniquely identifies the device that has joined. No matter what the device ID is, every listener for "device_connect" will receive the event. + > + > Sometimes you may only want a listener to be called when the device with a specific ID is connected. You cannot define a different event for each device ID, because it would be very tedious and you probably don't even known at compile time what device IDs exist. Instead this would require some sort of filter, comparing the actual data. This can be done inside the listener function, but that has a big disadvantage: Even though only events with a specific device ID are required, all "device_connect" events are still transmitted over the network. And then you probably need to do the same thing with the "device_disconnect" event. + > + > In such a situation, what you really want is a data subscription which can have subscription parameters. So you might define a data source called "device_connected" which may have a boolean property "is_connected" which can be true or false. Then you define the subscription parameter to have a property "device_id". When this data source is subscribed, a device ID has to be passed to that call. The providing party can then immediately respond saying that it either can or cannot provide the data for the given device ID. If it can, it will then only update the online value for that device ID and all others will not be transmitted over the network. +- **confirmation**: Does some event require any form of confirmation/response/result from the other party? + > When the goal is for one communication party to cause some sort of action by the other party, an event can be used. + > + > However often times the executing party needs send some result data or outcome of the action back to the emitter. In the past, it was necessary to define a separate request and response event and write code for every type of interaction to sync the two up, wait for the response and so on. This is very tedious and repetitive. + > + > With msglink, for such a case a procedure can be defined instead of an event. A procedure is basically two events combined, with the only difference being that the listener now returns another object which is sent back to the emitter. This can be integrated nicely with the async programming capability of many programming languages. + + + +# Implementation details + +As described in the beginning, msglink uses websockets as the underlying communication layer. The websocket protocol already has the concept of messages, which is very convenient. These messages are the underlying protocol data units (PDUs) used by msglink protocol. + +For now, msglink uses json to encode protocol data and user data in websocket messages. Later versions may introduce binary encoding if that turns out to be necessary for performance reasons. + +In the following section, the details of the communication protocol will be described. Every communication step will be accompanied with an example of how the corresponding websocket message will look like. We will use the following application as an example: + +A robot (client) needs to communicate with some base station (server) controlling it. The robot's job is to move around the map and perform actions guided by the server
+The robot needs the following information form the base station: + +- + + +We will be using a simple online combat game as an example application. In the combat game every player connects to a central game server using a compatible game client. Players can move around on the map and see each other. To prevent cheating, a client will only receive position information of players actually visible to it and not obstructed by any walls. Players can also attack other clients if they are in range. The server is responsible for deciding if a player is in range. + +Game client needs + +- a list of other players (identified by their name) visible to it.
+ (unique data object -> **```event```**) +- to be informed when a visible player (including it's own) is damaged so an animation and a sound can be played
+ -> event +- to know the position of other nearby clients on the map that it can see but not all clients
+ -> data subscriptions + +Game server: +- + + +As an example we are using a system managing multiple hardware devices connected to some computer. A webapp (client) displays information about connected devices and allows managing them by communicating with a server running on that computer. + +The client needs: +- a list of devices connected to the computer identified by their ID +- the power consumption of the device currently displayed in the UI +- the ability to disable a device because it needs to much power + +## Login procedure + +When a msglink client first connects to the msglink server both parties send an initial JSON encoded message to the other party containing the following information: + +- the msglink protocol version (determines whether certain features are supported) +- the user-defined link version (version of the user defined protocol) +- a list of events the party may listen for +- a list of data subscriptions the party may request +- a list of remote procedures the party may call + +> + +These lists are then used by the receiving party to determine weather it can fulfill the requirements of the other one. If this is not the case, the connection is immediately closed with a message + + +# Notes ## Naming alternatives From 4618f5f086196a1459d3efa973626e02a78eccb0 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 19 Nov 2023 01:00:34 +0100 Subject: [PATCH 12/50] finished the protocol definition for events (for now) and added some minor preparations in the code --- include/el/msglink/README.md | 203 +++++++++++++++++++++++++++++----- include/el/msglink/errors.hpp | 29 +++++ include/el/msglink/link.hpp | 34 ++++-- 3 files changed, 228 insertions(+), 38 deletions(-) diff --git a/include/el/msglink/README.md b/include/el/msglink/README.md index 8f945d3..0cc11ce 100644 --- a/include/el/msglink/README.md +++ b/include/el/msglink/README.md @@ -98,55 +98,200 @@ Which of the three options provided by msglink (events, data subscriptions, RPCs -# Implementation details +# Protocol details -As described in the beginning, msglink uses websockets as the underlying communication layer. The websocket protocol already has the concept of messages, which is very convenient. These messages are the underlying protocol data units (PDUs) used by msglink protocol. +In the following section, the details of the communication protocol will be described. Every communication step will be accompanied with an example of how the corresponding websocket message will look like. + +As an example we are using a system managing multiple hardware devices connected to some computer. A webapp (client) displays information about connected devices and allows managing them by communicating with a server running on that computer. + +The client needs: + +- to be informed when an error occurs
+ **Indicent without response -> event: "error_occurred"** +- a list of devices connected to the computer identified by their ID
+ **unique data -> data subscription (simple): "devices"** +- the power consumption of the device currently displayed in the UI
+ **non-unique data depending on parameter -> data subscription (with parameter): "power_consumption"** +- the ability for the user to disable a device, for example because it needs to much power
+ **Command with response -> RPC: "disable_device"** + +> In msglink there is (almost) no difference between the client and the server except for how the socket connection is established (and transaction IDs which are covered below). Therefore, any example described here could just as well work in the other direction. + + +## The basics + +As explained in the beginning, msglink uses websockets as the underlying communication layer. The websocket protocol already has the concept of messages, which is very convenient. Websockets guarantee that messages transmitted by one communication party are received as a whole and in order (no byte-stream fiddling needed). These messages are the underlying protocol data units (PDUs) used by msglink protocol. For now, msglink uses json to encode protocol data and user data in websocket messages. Later versions may introduce binary encoding if that turns out to be necessary for performance reasons. -In the following section, the details of the communication protocol will be described. Every communication step will be accompanied with an example of how the corresponding websocket message will look like. We will use the following application as an example: +### Working message -A robot (client) needs to communicate with some base station (server) controlling it. The robot's job is to move around the map and perform actions guided by the server
-The robot needs the following information form the base station: +Working messages are just the "normal" messages sent back and forth while the connection is open. -- +Every message has 2 base properties: +```json +{ + "type": "...", // string + "tid": 123, // int + ... +} +``` -We will be using a simple online combat game as an example application. In the combat game every player connects to a central game server using a compatible game client. Players can move around on the map and see each other. To prevent cheating, a client will only receive position information of players actually visible to it and not obstructed by any walls. Players can also attack other clients if they are in range. The server is responsible for deciding if a player is in range. +The **```type```** property defines the purpose of the message. There are the following message types: -Game client needs +- login +- login_ack +- evt_sub +- evt_sub_ack +- evt_sub_nak +- evt_unsub +- evt_emit +- data_sub +- data_sub_ack +- data_sub_nak +- data_unsub +- data_change +- rpc_call +- rpc_nak +- rpc_err +- rpc_result -- a list of other players (identified by their name) visible to it.
- (unique data object -> **```event```**) -- to be informed when a visible player (including it's own) is damaged so an animation and a sound can be played
- -> event -- to know the position of other nearby clients on the map that it can see but not all clients
- -> data subscriptions +The **```tid```** property is the transaction ID. The transaction ID is a signed integer number which (within a single session) uniquely identifies the transaction the message belongs to. -Game server: -- +> A transaction is a (from the perspective of the protocol implementation) complete interaction between the two communication parties. It could be an event, a data subscription or an RPC.
+This is a scheme used by many networking protocols and is required for the communication parties to know what messages belong together when a single transaction requires multiple back-and-forth messages like during an RPC. This is one of those tedious repetitive things that would otherwise need to be reimplemented for every command-response event pair if it was implemented manually using only events. +Every time a communication party starts a new interaction, it first generates a new transaction ID by using an internal ID counter. To prevent both parties from generating the same transaction ID at the same time, the **server always starts at transaction ID 1 and increments** it for each new transaction it starts (1, 2, 3, 4, ...) while the **client always starts a transaction ID -1 and decrements** from there (-1, -2, -3, -4, ...). Eventually, the two will meet in the middle when the integer overflows, which will take a very long time assuming 64 bit (or even 32 bit) integers. -As an example we are using a system managing multiple hardware devices connected to some computer. A webapp (client) displays information about connected devices and allows managing them by communicating with a server running on that computer. +The names of properties are intentionally kept as short as possible while still being readable pretty well by humans to reduce message size. + +Messages can have other properties specific to the message type. + + +### Closing message + +When closing the msglink and therefore websocket connection, custom close codes and reasons are used. The following table describes the possible codes and their meaning: + +| Code | Meaning | Notes | +|---|---|---| +| 1000 | Closed by user | Reason string is user defined. +| 3001 | msglink version incompatible | | +| 3002 | link version mismatch | | +| 3003 | Event requirement(s) unsatisfied | | +| 3004 | Data source requirement(s) unsatisfied | | +| 3005 | RPC requirement(s) unsatisfied | | -The client needs: -- a list of devices connected to the computer identified by their ID -- the power consumption of the device currently displayed in the UI -- the ability to disable a device because it needs to much power ## Login procedure -When a msglink client first connects to the msglink server both parties send an initial JSON encoded message to the other party containing the following information: +When a msglink client first connects to the msglink server both parties send an initial JSON encoded login message to the other party containing the following information: + +```json +{ + "type": "login", + "tid": 1, // 1 for server, -1 for client + "proto_version": 1, + "link_version": 1, + "events": ["error_occurred"], + "data_sources": ["devices", "power_consumption"], + "procedures": ["disable_device"] +} +``` + +- **```proto_verison```**: the msglink protocol version (determines whether certain features are supported) +- **```link_verison```**: the user-defined link version (version of the user defined protocol) +- **```events```**: a list of events the party may emit (it's outgoing events) +- **```data_sources```**: a list of data sources the party can provide (it's outgoing data sources) +- **```procedures```**: a list of remote procedures the party provides + +After receiving the message from the other party, both parties will check that the protocol versions of the other party are compatible and that the user defined link versions match. If that is not the case, the connection will be closed with code 3001 or 3002. + +The message also contains lists of all the functionality the party can provide to the other one. These lists are used by the receiving party to determine weather they fulfill all it's requirements. If any requirement fails, the connection is immediately closed with the corresponding code described below. This helps to detect simple coding mistakes early and reduce the amount of errors that will occur later during communication. + +- **events**: one party's incoming event list must be a subset of the other's outgoing event list. Fails with code 3003. Fail reasons: + - If one party may want to listen for an event the other party doesn't even know about and will never be able to emit +- **data sources**: one party's data subscription list must be a subset of the other's data source list. Fails with code 3004. Fail reasons: + - If one party may subscribe to a source the other doesn't know about and provide +- **remote procedure calls**: one party's called procedures list must be a subset of the other's callable procedure list. Fails with code 3005. Fail reasons: + - If one party may call a procedure the other doesn't know about and cannot handle + +Obviously these requirements are only checked approximately. The client doesn't know at that point whether the server ever will emit the "error_occurred" event or even if there will ever be a listener for it. The only thing it knows is that both the server and itself know that this event exists and know how to deal with it should that become necessary later. + +If no problems were found, each party sends a login acknowledgement message as a response to the other with the respective transaction ID (not a new one) to complete the login transaction: + +```json +{ + "type": "login_ack", + "tid": 1 // now 1 for client, -1 for server +} +``` + +Only after the login transaction has been successfully completed, is the party allowed to send further messages. + + +## Event messages + +If a communication party has a listener for a specific event, it needs to first subscribe to the event before it will receive it over the network. To do so, the event subscribe message is sent: + +```json +{ + "type": "evt_sub", + "tid": ..., // new transaction ID + "name": "..." +} +``` + +- **```name```**: name of the event to be subscribed to + +If the event is unknown by the other party, it will respond with a negative acknowledgement: + +```json +{ + "type": "evt_sub_nak", + "tid": ... +} +``` + +Otherwise, a positive acknowledgement will be sent: + +```json +{ + "type": "evt_sub_ack", + "tid": ... +} +``` + +This is the end of this transaction. + +After that, the emitting party will inform the listening one when this event type is emitted using the event emit message: + + +```json +{ + "type": "evt_emit", + "tid": ..., // new transaction ID for each emit + "name": "...", + "data": {...} +} +``` + +- **```name```**: name of the emitted event +- **```data```**: a json object containing the data associated with the event. This data will be validated according to the schema defined on the listening party and will cause a local error if it is invalid (error will not be sent to emitting party). Listeners are only called if the data was validated successfully. + +Once all listeners are disabled on the listening party, it can tell the emitting party that the event information is no longer required with the event unsubscribe message: -- the msglink protocol version (determines whether certain features are supported) -- the user-defined link version (version of the user defined protocol) -- a list of events the party may listen for -- a list of data subscriptions the party may request -- a list of remote procedures the party may call +```json +{ + "type": "evt_unsub", + "tid": ..., // new transaction ID + "name": "..." +} +``` -> +- **```name```**: name of the event to unsubscribe from -These lists are then used by the receiving party to determine weather it can fulfill the requirements of the other one. If this is not the case, the connection is immediately closed with a message +There are no acknowledgement messages for unsubscribe. Unsubscribe will guarantee that no more events with the given name are received. If the unsubscribed event wasn't subscribed before or doesn't even exist, a local error is thrown on the emitting party only. # Notes diff --git a/include/el/msglink/errors.hpp b/include/el/msglink/errors.hpp index 7223e8f..562bdb7 100644 --- a/include/el/msglink/errors.hpp +++ b/include/el/msglink/errors.hpp @@ -80,6 +80,35 @@ namespace el::msglink }; + /** + * @brief received malformed event which either couldn't be parsed + * as json or was otherwise structurally invalid, e.g. missing properties. + */ + class malformed_message_error : public msglink_error + { + using msglink_error::msglink_error; + }; + + /** + * @brief received unknown (invalid) incoming msglink event. + * This means, the event is either not defined or defined as + * outgoing only. + */ + class invalid_incoming_event : public msglink_error + { + using msglink_error::msglink_error; + }; + + /** + * @brief attempted to emit an unknown (invalid) outgoing msglink event. + * This means, the event is either not defined or defined as + * incoming only. + */ + class invalid_outgoing_event : public msglink_error + { + using msglink_error::msglink_error; + }; + /** * @brief exception used to indicate an unexpected * error code occurred like e.g. in some asio-related operation. diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index 9e8ad90..be32486 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -16,6 +16,7 @@ the user to define the API/protocol of a link #pragma once #include +#include #include #include @@ -23,6 +24,7 @@ the user to define the API/protocol of a link #include #include +#include namespace el::msglink @@ -53,17 +55,22 @@ namespace el::msglink // type of the lambda used to wrap event handlers using event_handler_wrapper_t = std::function; - // map of event name to handler function + // set of all outgoing events (including bidirectional ones) + std::unordered_set outgoing_events; + // set of all incoming events (including bidirectional ones) + std::unordered_set incoming_events; + + // map of incoming event name to handler function std::unordered_map< std::string, event_handler_wrapper_t - > handler_list; + > incoming_event_handler_map; protected: /** * @brief Method for registering a link-method event handler - * for an event. The event handler must be a method + * for a bidirectional event. The event handler must be a method * of the link it is registered on. This is a shortcut * to avoid having to use std::bind to bind every handler * to the instance. When an external handler is needed, this @@ -80,11 +87,15 @@ namespace el::msglink template _ET, std::derived_from _LT> void define_event(void (_LT::*_handler)(_ET &)) { - //EL_LOGD("defined an event"); + std::string event_name = _ET::_event_name; + + // save to incoming and outgoing event lists + incoming_events.insert(event_name); + outgoing_events.insert(event_name); std::function handler = _handler; - handler_list.emplace( + incoming_event_handler_map.emplace( _ET::_event_name, [this, handler](const nlohmann::json &_data) { std::cout << "hievent " << _data << std::endl; @@ -116,12 +127,17 @@ namespace el::msglink nlohmann::json jmsg = nlohmann::json::parse(_msg_content); std::string event_name = jmsg.at("event_name"); nlohmann::json event_data = jmsg.at("event_data"); - handler_list.at(event_name)(event_data); + + if (!incoming_events.contains(event_name)) + { + throw invalid_incoming_event(el::strutil::format("Incoming event '%s' is undefined or defined as outgoing only.", event_name.c_str())); + } + + incoming_event_handler_map.at(event_name)(event_data); } - catch (const std::exception &e) + catch (const nlohmann::json::exception &e) { - std::cout << "exception in link" << std::endl; - //EL_LOG_EXCEPTION_MSG("Exception occurred while processing event message", e); + throw malformed_message_error(el::strutil::format("Malformed event message: %s", _msg_content.c_str())); } } }; From 6a9602492d3791c856d4d69f4b95bdfcd96bd3d2 Mon Sep 17 00:00:00 2001 From: melektron Date: Mon, 20 Nov 2023 22:06:46 +0100 Subject: [PATCH 13/50] started implementing proper event protocol --- include/el/msglink/errors.hpp | 12 ++- include/el/msglink/link.hpp | 58 +++++++++++-- include/el/msglink/msgtype.hpp | 143 +++++++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 10 deletions(-) create mode 100644 include/el/msglink/msgtype.hpp diff --git a/include/el/msglink/errors.hpp b/include/el/msglink/errors.hpp index 562bdb7..035fe9d 100644 --- a/include/el/msglink/errors.hpp +++ b/include/el/msglink/errors.hpp @@ -94,7 +94,7 @@ namespace el::msglink * This means, the event is either not defined or defined as * outgoing only. */ - class invalid_incoming_event : public msglink_error + class invalid_incoming_event_error : public msglink_error { using msglink_error::msglink_error; }; @@ -104,7 +104,7 @@ namespace el::msglink * This means, the event is either not defined or defined as * incoming only. */ - class invalid_outgoing_event : public msglink_error + class invalid_outgoing_event_error : public msglink_error { using msglink_error::msglink_error; }; @@ -132,4 +132,12 @@ namespace el::msglink } }; + /** + * @brief tried to parse/serialize an invalid message type. + */ + class invalid_msg_type_error : public msglink_error + { + using msglink_error::msglink_error; + }; + } // namespace el::msglink diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index be32486..f5c7c42 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -52,6 +52,9 @@ namespace el::msglink { private: + // + bool authentication_done = false; + // type of the lambda used to wrap event handlers using event_handler_wrapper_t = std::function; @@ -66,6 +69,36 @@ namespace el::msglink event_handler_wrapper_t > incoming_event_handler_map; + private: // methods + + /** + * @brief handles incoming messages (already parsed) before login is complete + * to perform authentication. + * + * @param _jmsg parsed message + */ + void handle_message_pre_login( + const std::string &_msg_type, + const int transaction_id, + const nlohmann::json &_jmsg + ) { + + } + + /** + * @brief handles incoming messages (already parsed) after login is complete + * and parties are authenticated. + * + * @param _jmsg parsed message + */ + void handle_message_post_login( + const std::string &_msg_type, + const int transaction_id, + const nlohmann::json &_jmsg + ) { + + } + protected: /** @@ -107,6 +140,7 @@ namespace el::msglink ); } ); + } @@ -125,19 +159,27 @@ namespace el::msglink try { nlohmann::json jmsg = nlohmann::json::parse(_msg_content); - std::string event_name = jmsg.at("event_name"); - nlohmann::json event_data = jmsg.at("event_data"); - if (!incoming_events.contains(event_name)) - { - throw invalid_incoming_event(el::strutil::format("Incoming event '%s' is undefined or defined as outgoing only.", event_name.c_str())); - } + // read message type and transaction ID (always present) + std::string msg_type = jmsg.at("type"); + int transaction_id = jmsg.at("tid"); - incoming_event_handler_map.at(event_name)(event_data); + if (authentication_done) + handle_message_post_login( + msg_type, + transaction_id, + jmsg + ); + else + handle_message_pre_login( + msg_type, + transaction_id, + jmsg + ); } catch (const nlohmann::json::exception &e) { - throw malformed_message_error(el::strutil::format("Malformed event message: %s", _msg_content.c_str())); + throw malformed_message_error(el::strutil::format("Malformed link message: %s\n%s", _msg_content.c_str(), e.what())); } } }; diff --git a/include/el/msglink/msgtype.hpp b/include/el/msglink/msgtype.hpp new file mode 100644 index 0000000..a7b6300 --- /dev/null +++ b/include/el/msglink/msgtype.hpp @@ -0,0 +1,143 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +20.11.23, 18:35 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Defines all message types possible and conversions from/to string +*/ + +#pragma once + +#include + +#include "errors.hpp" + + +namespace el::msglink +{ + enum class msg_type_t + { + LOGIN, + LOGIN_ACK, + EVENT_SUB, + EVENT_SUB_ACK, + EVENT_SUB_NAK, + EVENT_UNSUB, + EVENT_EMIT, + DATA_SUB, + DATA_SUB_ACK, + DATA_SUB_NAK, + DATA_UNSUB, + DATA_CHANGE, + RPC_CALL, + RPC_NAK, + RPC_ERR, + RPC_RESULT, + }; + + const char *msg_type_to_string(const msg_type_t _msg_type) noexcept + { + switch (_msg_type) + { + using enum msg_type_t; + + case LOGIN: + return "login"; + break; + case LOGIN_ACK: + return "login_ack"; + break; + case EVENT_SUB: + return "event_sub"; + break; + case EVENT_SUB_ACK: + return "event_sub_ack"; + break; + case EVENT_SUB_NAK: + return "event_sub_nak"; + break; + case EVENT_UNSUB: + return "event_unsub"; + break; + case EVENT_EMIT: + return "event_emit"; + break; + case DATA_SUB: + return "data_sub"; + break; + case DATA_SUB_ACK: + return "data_sub_ack"; + break; + case DATA_SUB_NAK: + return "data_sub_nak"; + break; + case DATA_UNSUB: + return "data_unsub"; + break; + case DATA_CHANGE: + return "data_change"; + break; + case RPC_CALL: + return "rpc_call"; + break; + case RPC_NAK: + return "rpc_nak"; + break; + case RPC_ERR: + return "rpc_err"; + break; + case RPC_RESULT: + return "rpc_result"; + break; + + default: + throw invalid_msg_type_error("Invalid enum value: " + std::to_string((int)_msg_type)); + } + } + + msg_type_t msg_type_from_string(const std::string &_msg_type_name) noexcept + { + using enum msg_type_t; + + if (_msg_type_name == "login") + return LOGIN; + else if (_msg_type_name == "login_ack") + return LOGIN_ACK; + else if (_msg_type_name == "event_sub") + return EVENT_SUB; + else if (_msg_type_name == "event_sub_ack") + return EVENT_SUB_ACK; + else if (_msg_type_name == "event_sub_nak") + return EVENT_SUB_NAK; + else if (_msg_type_name == "event_unsub") + return EVENT_UNSUB; + else if (_msg_type_name == "event_emit") + return EVENT_EMIT; + else if (_msg_type_name == "data_sub") + return DATA_SUB; + else if (_msg_type_name == "data_sub_ack") + return DATA_SUB_ACK; + else if (_msg_type_name == "data_sub_nak") + return DATA_SUB_NAK; + else if (_msg_type_name == "data_unsub") + return DATA_UNSUB; + else if (_msg_type_name == "data_change") + return DATA_CHANGE; + else if (_msg_type_name == "rpc_call") + return RPC_CALL; + else if (_msg_type_name == "rpc_nak") + return RPC_NAK; + else if (_msg_type_name == "rpc_err") + return RPC_ERR; + else if (_msg_type_name == "rpc_result") + return RPC_RESULT; + else + throw invalid_msg_type_error("Invalid type name: " + _msg_type_name); + } +} // namespace el::msglink + From 9007c11fa9b2adde4ece578e79746d7a80795663 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 26 Nov 2023 19:00:34 +0100 Subject: [PATCH 14/50] added flags module --- include/el/flags.hpp | 79 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 include/el/flags.hpp diff --git a/include/el/flags.hpp b/include/el/flags.hpp new file mode 100644 index 0000000..a17a07f --- /dev/null +++ b/include/el/flags.hpp @@ -0,0 +1,79 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +26.11.23, 15:26 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +flag types with specific restrictions +*/ + +#pragma once + +#include "cxxversions.h" + + +namespace el +{ + using flag = bool; + + /** + * @brief set-only flag. + * + * This single use flag behaves like a normal boolean flag, + * except it can only be set but never cleared again. + * When default initialized, the flag is initialized to false. + * When copy/move initialized, the flag is initialized as expected. + */ + class soflag + { + protected: + flag internal_flag = false; + + public: + // allow default init + soflag() = default; + soflag(const soflag &) = default; + soflag(soflag &&) = default; + + // cannot copy assign as that would allow clearing + soflag &operator=(const soflag &) = delete; + + /** + * @brief sets the set only flag to the value of + * the provided flag if it is set. If the provided + * flag _x is cleared, nothing will happen. + * + * @param _x whether to set + * @return flag& value of the soflag after the operation (not _x) + */ + inline flag &operator=(flag _x) noexcept + { + if (_x) + internal_flag = _x; + + return internal_flag; + } + + /** + * @brief sets the flag. + * If already set, nothing happens. + */ + inline void set() noexcept + { + internal_flag = true; + } + + /** + * @return flag the current value of the flag (set/cleared) + */ + inline operator flag() const noexcept + { + return internal_flag; + } + }; + +} // namespace el From f9bcecdf3e97e1889a6af1dd233399c4570685ef Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 26 Nov 2023 21:46:42 +0100 Subject: [PATCH 15/50] started working on message parsing and authentication procedure --- include/el/codable.hpp | 4 +- include/el/cxxversions.h | 8 + include/el/msglink/README.md | 25 +- include/el/msglink/errors.hpp | 11 +- include/el/msglink/event.hpp | 2 +- include/el/msglink/internal/messages.hpp | 63 +++++ include/el/msglink/internal/msgtype.hpp | 161 ++++++++++++ include/el/msglink/internal/proto_version.hpp | 50 ++++ include/el/msglink/internal/types.hpp | 24 ++ include/el/msglink/{ => internal}/wspp.hpp | 0 include/el/msglink/link.hpp | 237 ++++++++++++++---- include/el/msglink/msgtype.hpp | 143 ----------- include/el/msglink/server.hpp | 8 +- include/el/strutil.hpp | 20 +- 14 files changed, 543 insertions(+), 213 deletions(-) create mode 100644 include/el/msglink/internal/messages.hpp create mode 100644 include/el/msglink/internal/msgtype.hpp create mode 100644 include/el/msglink/internal/proto_version.hpp create mode 100644 include/el/msglink/internal/types.hpp rename include/el/msglink/{ => internal}/wspp.hpp (100%) delete mode 100644 include/el/msglink/msgtype.hpp diff --git a/include/el/codable.hpp b/include/el/codable.hpp index eda9674..5a52f9a 100644 --- a/include/el/codable.hpp +++ b/include/el/codable.hpp @@ -161,7 +161,7 @@ namespace el /* these dummy templates make this function less specialized than one without, \ so the user can manually define their encoder which will take precedence over \ this one */ \ - template \ + template \ EL_ENCODER(member) \ { \ encoded_data = member; \ @@ -172,7 +172,7 @@ namespace el /* these dummy templates make this function less specialized than one without, \ so the user can manually define their encoder which will take precedence over \ this one */ \ - template \ + template \ EL_DECODER(member) \ { \ member = encoded_data; \ diff --git a/include/el/cxxversions.h b/include/el/cxxversions.h index d42705c..c9da055 100644 --- a/include/el/cxxversions.h +++ b/include/el/cxxversions.h @@ -42,4 +42,12 @@ to enable library features for versions not detected using the __cplusplus defin #define __EL_CXX17 #define __EL_ENABLE_CXX17 +#endif + +// check for C++ 20 compatibility +#if __cplusplus >= 202002L + +#define __EL_CXX20 +#define __EL_ENABLE_CXX20 + #endif \ No newline at end of file diff --git a/include/el/msglink/README.md b/include/el/msglink/README.md index 0cc11ce..feb51c8 100644 --- a/include/el/msglink/README.md +++ b/include/el/msglink/README.md @@ -140,8 +140,8 @@ Every message has 2 base properties: The **```type```** property defines the purpose of the message. There are the following message types: -- login -- login_ack +- auth +- auth_ack - evt_sub - evt_sub_ack - evt_sub_nak @@ -183,15 +183,15 @@ When closing the msglink and therefore websocket connection, custom close codes | 3005 | RPC requirement(s) unsatisfied | | -## Login procedure +## Authentication procedure -When a msglink client first connects to the msglink server both parties send an initial JSON encoded login message to the other party containing the following information: +When a msglink client first connects to the msglink server both parties send an initial JSON encoded authentication message to the other party containing the following information: ```json { - "type": "login", - "tid": 1, // 1 for server, -1 for client - "proto_version": 1, + "type": "auth", + "tid": 1, // should be 1 for server and -1 for client according to definition of tid generation above + "proto_version": [1, 2, 3], "link_version": 1, "events": ["error_occurred"], "data_sources": ["devices", "power_consumption"], @@ -207,6 +207,8 @@ When a msglink client first connects to the msglink server both parties send an After receiving the message from the other party, both parties will check that the protocol versions of the other party are compatible and that the user defined link versions match. If that is not the case, the connection will be closed with code 3001 or 3002. +> Protocol version compatibility is determined by the party with the higher (= newer) version as that one is assumed to know of and be able to judge compatibility with the lower version. If a party receives an auth message with a higher protocol version than it's own, it skips the protocol compatibility check. + The message also contains lists of all the functionality the party can provide to the other one. These lists are used by the receiving party to determine weather they fulfill all it's requirements. If any requirement fails, the connection is immediately closed with the corresponding code described below. This helps to detect simple coding mistakes early and reduce the amount of errors that will occur later during communication. - **events**: one party's incoming event list must be a subset of the other's outgoing event list. Fails with code 3003. Fail reasons: @@ -218,16 +220,19 @@ The message also contains lists of all the functionality the party can provide t Obviously these requirements are only checked approximately. The client doesn't know at that point whether the server ever will emit the "error_occurred" event or even if there will ever be a listener for it. The only thing it knows is that both the server and itself know that this event exists and know how to deal with it should that become necessary later. -If no problems were found, each party sends a login acknowledgement message as a response to the other with the respective transaction ID (not a new one) to complete the login transaction: +If no problems were found, each party sends a authentication acknowledgement message as a response to the other with the respective transaction ID (not a new one) to complete the authentication transaction: ```json { - "type": "login_ack", + "type": "auth_ack", "tid": 1 // now 1 for client, -1 for server } ``` -Only after the login transaction has been successfully completed, is the party allowed to send further messages. +Only after both parties' authentication transactions have been successfully completed, is either party allowed to send further messages. This is defined by one party as both: + +- having sent the auth_ack message in response to the other's auth message +- having received the auth_ack message in response to it's own auth message ## Event messages diff --git a/include/el/msglink/errors.hpp b/include/el/msglink/errors.hpp index 035fe9d..1deb39a 100644 --- a/include/el/msglink/errors.hpp +++ b/include/el/msglink/errors.hpp @@ -15,7 +15,7 @@ msglink exceptions #include #include -#include +#include "../exceptions.hpp" namespace el::msglink { @@ -89,6 +89,15 @@ namespace el::msglink using msglink_error::msglink_error; }; + /** + * @brief link is not compatible with the link of the other party. + * This may be thrown during authentication. + */ + class incompatible_link_error : public msglink_error + { + using msglink_error::msglink_error; + }; + /** * @brief received unknown (invalid) incoming msglink event. * This means, the event is either not defined or defined as diff --git a/include/el/msglink/event.hpp b/include/el/msglink/event.hpp index 9d05001..7854946 100644 --- a/include/el/msglink/event.hpp +++ b/include/el/msglink/event.hpp @@ -13,7 +13,7 @@ msglink event class used to define custom events #pragma once -#include +#include "../codable.hpp" namespace el::msglink diff --git a/include/el/msglink/internal/messages.hpp b/include/el/msglink/internal/messages.hpp new file mode 100644 index 0000000..a7e90c7 --- /dev/null +++ b/include/el/msglink/internal/messages.hpp @@ -0,0 +1,63 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +26.11.23, 20:30 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Codables for internal communication messages +*/ + +#pragma once + +#include +#include "../../codable.hpp" +#include "types.hpp" + +namespace el::msglink +{ + /** + * @brief base type for all messages + */ + struct base_msg_t + { + std::string type; + tid_t tid; + }; + + struct msg_auth_t + : public base_msg_t + , public codable + { + proto_version_t proto_version; + link_version_t link_version; + std::vector events; + std::vector data_sources; + std::vector procedures; + + EL_DEFINE_CODABLE( + msg_auth_t, + type, + tid, + proto_version, + link_version, + events, + data_sources, + procedures + ) + }; + + struct msg_auth_ack_t + : public base_msg_t + , public codable + { + EL_DEFINE_CODABLE( + msg_auth_ack_t, + type, + tid + ) + }; +} // namespace el diff --git a/include/el/msglink/internal/msgtype.hpp b/include/el/msglink/internal/msgtype.hpp new file mode 100644 index 0000000..0b32a74 --- /dev/null +++ b/include/el/msglink/internal/msgtype.hpp @@ -0,0 +1,161 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +20.11.23, 18:35 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Defines all message types possible and conversions from/to string +*/ + +#pragma once + +#include + +#include "../errors.hpp" + + +#define __EL_MSGLINK_MSG_NAME_AUTH "auth" +#define __EL_MSGLINK_MSG_NAME_AUTH_ACK "auth_ack" +#define __EL_MSGLINK_MSG_NAME_EVT_SUB "evt_sub" +#define __EL_MSGLINK_MSG_NAME_EVT_SUB_ACK "evt_sub_ack" +#define __EL_MSGLINK_MSG_NAME_EVT_SUB_NAK "evt_sub_nak" +#define __EL_MSGLINK_MSG_NAME_EVT_UNSUB "evt_unsub" +#define __EL_MSGLINK_MSG_NAME_EVT_EMIT "evt_emit" +#define __EL_MSGLINK_MSG_NAME_DATA_SUB "data_sub" +#define __EL_MSGLINK_MSG_NAME_DATA_SUB_ACK "data_sub_ack" +#define __EL_MSGLINK_MSG_NAME_DATA_SUB_NAK "data_sub_nak" +#define __EL_MSGLINK_MSG_NAME_DATA_UNSUB "data_unsub" +#define __EL_MSGLINK_MSG_NAME_DATA_CHANGE "data_change" +#define __EL_MSGLINK_MSG_NAME_RPC_CALL "rpc_call" +#define __EL_MSGLINK_MSG_NAME_RPC_NAK "rpc_nak" +#define __EL_MSGLINK_MSG_NAME_RPC_ERR "rpc_err" +#define __EL_MSGLINK_MSG_NAME_RPC_RESULT "rpc_result" + + +namespace el::msglink +{ + enum class msg_type_t + { + AUTH, + AUTH_ACK, + EVENT_SUB, + EVENT_SUB_ACK, + EVENT_SUB_NAK, + EVENT_UNSUB, + EVENT_EMIT, + DATA_SUB, + DATA_SUB_ACK, + DATA_SUB_NAK, + DATA_UNSUB, + DATA_CHANGE, + RPC_CALL, + RPC_NAK, + RPC_ERR, + RPC_RESULT, + }; + + const char *msg_type_to_string(const msg_type_t _msg_type) noexcept + { + switch (_msg_type) + { + using enum msg_type_t; + + case AUTH: + return __EL_MSGLINK_MSG_NAME_AUTH; + break; + case AUTH_ACK: + return __EL_MSGLINK_MSG_NAME_AUTH_ACK; + break; + case EVENT_SUB: + return __EL_MSGLINK_MSG_NAME_EVT_SUB; + break; + case EVENT_SUB_ACK: + return __EL_MSGLINK_MSG_NAME_EVT_SUB_ACK; + break; + case EVENT_SUB_NAK: + return __EL_MSGLINK_MSG_NAME_EVT_SUB_NAK; + break; + case EVENT_UNSUB: + return __EL_MSGLINK_MSG_NAME_EVT_UNSUB; + break; + case EVENT_EMIT: + return __EL_MSGLINK_MSG_NAME_EVT_EMIT; + break; + case DATA_SUB: + return __EL_MSGLINK_MSG_NAME_DATA_SUB; + break; + case DATA_SUB_ACK: + return __EL_MSGLINK_MSG_NAME_DATA_SUB_ACK; + break; + case DATA_SUB_NAK: + return __EL_MSGLINK_MSG_NAME_DATA_SUB_NAK; + break; + case DATA_UNSUB: + return __EL_MSGLINK_MSG_NAME_DATA_UNSUB; + break; + case DATA_CHANGE: + return __EL_MSGLINK_MSG_NAME_DATA_CHANGE; + break; + case RPC_CALL: + return __EL_MSGLINK_MSG_NAME_RPC_CALL; + break; + case RPC_NAK: + return __EL_MSGLINK_MSG_NAME_RPC_NAK; + break; + case RPC_ERR: + return __EL_MSGLINK_MSG_NAME_RPC_ERR; + break; + case RPC_RESULT: + return __EL_MSGLINK_MSG_NAME_RPC_RESULT; + break; + + default: + throw invalid_msg_type_error("Invalid enum value: " + std::to_string((int)_msg_type)); + } + } + + msg_type_t msg_type_from_string(const std::string &_msg_type_name) noexcept + { + using enum msg_type_t; + + if (_msg_type_name == __EL_MSGLINK_MSG_NAME_AUTH) + return AUTH; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_AUTH_ACK) + return AUTH_ACK; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_EVT_SUB) + return EVENT_SUB; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_EVT_SUB_ACK) + return EVENT_SUB_ACK; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_EVT_SUB_NAK) + return EVENT_SUB_NAK; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_EVT_UNSUB) + return EVENT_UNSUB; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_EVT_EMIT) + return EVENT_EMIT; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_DATA_SUB) + return DATA_SUB; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_DATA_SUB_ACK) + return DATA_SUB_ACK; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_DATA_SUB_NAK) + return DATA_SUB_NAK; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_DATA_UNSUB) + return DATA_UNSUB; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_DATA_CHANGE) + return DATA_CHANGE; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_RPC_CALL) + return RPC_CALL; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_RPC_NAK) + return RPC_NAK; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_RPC_ERR) + return RPC_ERR; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_RPC_RESULT) + return RPC_RESULT; + else + throw invalid_msg_type_error("Invalid type name: " + _msg_type_name); + } +} // namespace el::msglink + diff --git a/include/el/msglink/internal/proto_version.hpp b/include/el/msglink/internal/proto_version.hpp new file mode 100644 index 0000000..7379eb0 --- /dev/null +++ b/include/el/msglink/internal/proto_version.hpp @@ -0,0 +1,50 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +26.11.23, 21:17 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +functions to check msglink protocol version compatibility +*/ + +#pragma once + +#include + +#include "../../strutil.hpp" +#include "types.hpp" + + +namespace el::msglink::proto_version +{ + // the current protocol version of this source tree + inline const proto_version_t current = {0, 1, 0}; + + // all lower protocol versions compatible with this one + inline const std::set compatible_versions = + { + {0, 1, 0}, + }; + + /** + * @brief checks if the protocol version _other + * is compatible with the current protocol version. + * + * @param _other protocol version to check + * @return true _other is compatible with the current version + * @return false _other is not compatible with the current version + */ + bool is_compatible(const proto_version_t &_other) + { + return compatible_versions.contains(_other); + } + + std::string to_string(const proto_version_t &_ver) + { + return strutil::format("[%u.%u.%u]", _ver[0], _ver[1], _ver[2]); + } +} // namespace el::msglink diff --git a/include/el/msglink/internal/types.hpp b/include/el/msglink/internal/types.hpp new file mode 100644 index 0000000..c086f0d --- /dev/null +++ b/include/el/msglink/internal/types.hpp @@ -0,0 +1,24 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +26.11.23, 21:10 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +msglink type aliases used in other files to easily be able to change types +*/ + +#pragma once + +#include +#include + +namespace el::msglink +{ + using tid_t = int64_t; + using proto_version_t = std::array; + using link_version_t = uint32_t; +} // namespace el::msglink diff --git a/include/el/msglink/wspp.hpp b/include/el/msglink/internal/wspp.hpp similarity index 100% rename from include/el/msglink/wspp.hpp rename to include/el/msglink/internal/wspp.hpp diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index f5c7c42..eacbf48 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -9,7 +9,7 @@ This source code is licensed under the Apache-2.0 license found in the LICENSE file in the root directory of this source tree. This file implements the link class which can be inherited by -the user to define the API/protocol of a link +the user to define the API/protocol of a link (Can be used on client and server side). */ @@ -22,9 +22,14 @@ the user to define the API/protocol of a link #include -#include -#include -#include +#include "../logging.hpp" +#include "../flags.hpp" +#include "event.hpp" +#include "errors.hpp" +#include "internal/msgtype.hpp" +#include "internal/messages.hpp" +#include "internal/types.hpp" +#include "internal/proto_version.hpp" namespace el::msglink @@ -36,24 +41,34 @@ namespace el::msglink * can be transmitted and/or received from/to the other communication party. * It is also responsible for defining the data structure and type associated * with each of those interactions. - * + * * Both clients and server have to define the link in order to be * able to communicate. Ideally, the client and server links would match up * meaning that for every e.g. event one party can send, the other party knows of * and can receive this event. - * + * * If a link receives any interaction from the other party that it either doesn't * known of, cannot decode or otherwise interpret, an error is automatically * reported to the sending party so a language-dependent error handling scheme such * as an exception can catch it. - * + * */ class link { private: - // - bool authentication_done = false; + // the value to step for every new transaction ID generated. + // This is set to either 1 or -1 in the constructor depending on wether + // it is being used by a server or a client + const int8_t tid_step_value; + // running counter for transaction IDs (initialized to tid_step_value) + tid_t tid_counter; + + // flags set to track the authentication process + soflag auth_ack_sent; + soflag auth_ack_received; + // set as soon as the login_ack has been sent and received + soflag authentication_done; // type of the lambda used to wrap event handlers using event_handler_wrapper_t = std::function; @@ -71,36 +86,155 @@ namespace el::msglink private: // methods + decltype(link::tid_counter) generate_new_tid() noexcept + { + // use value before counting, so first value is 1/-1 + // as defined in the spec + auto tmp = tid_counter; + tid_counter += tid_step_value; + return tid_counter; + } + + /** + * @brief updates authentication done flag from + * the other authentication flags' values + */ + void update_auth_done() noexcept + { + if (auth_ack_sent && auth_ack_received) + authentication_done.set(); + } + + /** + * @return link_version_t link version of the link instance + */ + link_version_t get_link_version() const noexcept + { + return _el_msglink_get_link_version(); + } + /** - * @brief handles incoming messages (already parsed) before login is complete - * to perform authentication. - * + * @brief handles incoming messages (already parsed) before authentication is complete + * to perform the authentication. + * * @param _jmsg parsed message */ - void handle_message_pre_login( - const std::string &_msg_type, + void handle_message_pre_auth( + const msg_type_t _msg_type, const int transaction_id, const nlohmann::json &_jmsg - ) { + ) + { + switch (_msg_type) + { + using enum msg_type_t; + + case AUTH: + { + // validate message + msg_auth_t msg(_jmsg); + + // check protocol version if we are the higher one + if (proto_version::current > msg.proto_version) + if (!proto_version::is_compatible(msg.proto_version)) + throw incompatible_link_error(el::strutil::format( + "Incompatible protocol versions: this=%u, other=%u", + proto_version::to_string(proto_version::current).c_str(), + proto_version::to_string(msg.proto_version).c_str() + )); + + // check user defined link version + if (msg.link_version != get_link_version()) + throw incompatible_link_error(el::strutil::format("Link versions don't match: this=%u, other=%u", get_link_version(), msg.link_version)); + } + break; + + case AUTH_ACK: + { + msg_auth_ack_t msg(_jmsg); + + + } + break; + default: + throw malformed_message_error(el::strutil::format("Invalid pre-auth message type: %s", msg_type_to_string(_msg_type))); + break; + } + + update_auth_done(); } /** - * @brief handles incoming messages (already parsed) after login is complete - * and parties are authenticated. - * + * @brief handles incoming messages (already parsed) after authentication is complete + * and both parties are authenticated. + * * @param _jmsg parsed message */ - void handle_message_post_login( - const std::string &_msg_type, + void handle_message_post_auth( + const msg_type_t _msg_type, const int transaction_id, const nlohmann::json &_jmsg - ) { + ) + { + switch (_msg_type) + { + using enum msg_type_t; + + case EVENT_SUB: + break; + case EVENT_SUB_ACK: + break; + case EVENT_SUB_NAK: + break; + case EVENT_UNSUB: + break; + case EVENT_EMIT: + break; + case DATA_SUB: + break; + case DATA_SUB_ACK: + break; + case DATA_SUB_NAK: + break; + case DATA_UNSUB: + break; + case DATA_CHANGE: + break; + case RPC_CALL: + break; + case RPC_NAK: + break; + case RPC_ERR: + break; + case RPC_RESULT: + break; + + default: + throw malformed_message_error(el::strutil::format("Invalid post-auth message type: %s", msg_type_to_string(_msg_type))); + break; + } } protected: + /** + * @return int64_t user defined link version. + * Use the EL_LINK_VERSION macro to generate this function. + */ + virtual link_version_t _el_msglink_get_link_version() const noexcept = 0; + + /** + * @brief Macro used to define the user defined link version inside the link + * definitions. This simply creates a method returning the provided number. + * + * The link version is an integer that defined the version of the + * user defined protocol the link represents. When two parties connect, their + * link versions must match. + */ +#define EL_MSGLINK_LINK_VERSION(version_num) virtual link_version_t _el_msglink_get_link_version() const noexcept override { return version_num; } + /** * @brief Method for registering a link-method event handler * for a bidirectional event. The event handler must be a method @@ -108,37 +242,37 @@ namespace el::msglink * to avoid having to use std::bind to bind every handler * to the instance. When an external handler is needed, this * is the wrong overload. - * + * * Method function pointer: * https://isocpp.org/wiki/faq/pointers-to-members#typedef-for-ptr-to-memfn - * - * @tparam _ET the event class of the event to register + * + * @tparam _ET the event class of the event to register * (must inherit from el::msglink::event, can be deduced from method parameter) * @tparam _LT the link class the handler is a method of (can also be deduced) * @param _handler the handler method for the event */ template _ET, std::derived_from _LT> - void define_event(void (_LT::*_handler)(_ET &)) + void define_event(void (_LT:: *_handler)(_ET &)) { std::string event_name = _ET::_event_name; // save to incoming and outgoing event lists incoming_events.insert(event_name); outgoing_events.insert(event_name); - - std::function handler = _handler; + + std::function handler = _handler; incoming_event_handler_map.emplace( - _ET::_event_name, + _ET::_event_name, [this, handler](const nlohmann::json &_data) { - std::cout << "hievent " << _data << std::endl; - _ET new_event_inst; - new_event_inst = _data; - handler( - static_cast<_LT*>(this), - new_event_inst - ); - } + std::cout << "hievent " << _data << std::endl; + _ET new_event_inst; + new_event_inst = _data; + handler( + static_cast<_LT *>(this), + new_event_inst + ); + } ); } @@ -146,14 +280,29 @@ namespace el::msglink public: + /** + * @brief Construct a new link object. + * + * @param _is_server determines the TID series used (+n or -n) + */ + link(bool _is_server) + : tid_step_value(_is_server ? 1 : -1) + , tid_counter(tid_step_value) + {} + /** * @brief valid link definitions must implement this define method * to define the protocol by calling the specialized define * methods for events and other interactions. - * + * */ virtual void define() noexcept = 0; + void on_connection_established() + { + + } + void on_message(const std::string &_msg_content) { try @@ -165,21 +314,25 @@ namespace el::msglink int transaction_id = jmsg.at("tid"); if (authentication_done) - handle_message_post_login( - msg_type, + { + handle_message_post_auth( + msg_type_from_string(msg_type), transaction_id, jmsg ); + } else - handle_message_pre_login( - msg_type, + { + handle_message_pre_auth( + msg_type_from_string(msg_type), transaction_id, jmsg ); + } } catch (const nlohmann::json::exception &e) { - throw malformed_message_error(el::strutil::format("Malformed link message: %s\n%s", _msg_content.c_str(), e.what())); + throw malformed_message_error(el::strutil::format("Malformed JSON link message: %s\n%s", _msg_content.c_str(), e.what())); } } }; diff --git a/include/el/msglink/msgtype.hpp b/include/el/msglink/msgtype.hpp deleted file mode 100644 index a7b6300..0000000 --- a/include/el/msglink/msgtype.hpp +++ /dev/null @@ -1,143 +0,0 @@ -/* -ELEKTRON © 2023 - now -Written by melektron -www.elektron.work -20.11.23, 18:35 -All rights reserved. - -This source code is licensed under the Apache-2.0 license found in the -LICENSE file in the root directory of this source tree. - -Defines all message types possible and conversions from/to string -*/ - -#pragma once - -#include - -#include "errors.hpp" - - -namespace el::msglink -{ - enum class msg_type_t - { - LOGIN, - LOGIN_ACK, - EVENT_SUB, - EVENT_SUB_ACK, - EVENT_SUB_NAK, - EVENT_UNSUB, - EVENT_EMIT, - DATA_SUB, - DATA_SUB_ACK, - DATA_SUB_NAK, - DATA_UNSUB, - DATA_CHANGE, - RPC_CALL, - RPC_NAK, - RPC_ERR, - RPC_RESULT, - }; - - const char *msg_type_to_string(const msg_type_t _msg_type) noexcept - { - switch (_msg_type) - { - using enum msg_type_t; - - case LOGIN: - return "login"; - break; - case LOGIN_ACK: - return "login_ack"; - break; - case EVENT_SUB: - return "event_sub"; - break; - case EVENT_SUB_ACK: - return "event_sub_ack"; - break; - case EVENT_SUB_NAK: - return "event_sub_nak"; - break; - case EVENT_UNSUB: - return "event_unsub"; - break; - case EVENT_EMIT: - return "event_emit"; - break; - case DATA_SUB: - return "data_sub"; - break; - case DATA_SUB_ACK: - return "data_sub_ack"; - break; - case DATA_SUB_NAK: - return "data_sub_nak"; - break; - case DATA_UNSUB: - return "data_unsub"; - break; - case DATA_CHANGE: - return "data_change"; - break; - case RPC_CALL: - return "rpc_call"; - break; - case RPC_NAK: - return "rpc_nak"; - break; - case RPC_ERR: - return "rpc_err"; - break; - case RPC_RESULT: - return "rpc_result"; - break; - - default: - throw invalid_msg_type_error("Invalid enum value: " + std::to_string((int)_msg_type)); - } - } - - msg_type_t msg_type_from_string(const std::string &_msg_type_name) noexcept - { - using enum msg_type_t; - - if (_msg_type_name == "login") - return LOGIN; - else if (_msg_type_name == "login_ack") - return LOGIN_ACK; - else if (_msg_type_name == "event_sub") - return EVENT_SUB; - else if (_msg_type_name == "event_sub_ack") - return EVENT_SUB_ACK; - else if (_msg_type_name == "event_sub_nak") - return EVENT_SUB_NAK; - else if (_msg_type_name == "event_unsub") - return EVENT_UNSUB; - else if (_msg_type_name == "event_emit") - return EVENT_EMIT; - else if (_msg_type_name == "data_sub") - return DATA_SUB; - else if (_msg_type_name == "data_sub_ack") - return DATA_SUB_ACK; - else if (_msg_type_name == "data_sub_nak") - return DATA_SUB_NAK; - else if (_msg_type_name == "data_unsub") - return DATA_UNSUB; - else if (_msg_type_name == "data_change") - return DATA_CHANGE; - else if (_msg_type_name == "rpc_call") - return RPC_CALL; - else if (_msg_type_name == "rpc_nak") - return RPC_NAK; - else if (_msg_type_name == "rpc_err") - return RPC_ERR; - else if (_msg_type_name == "rpc_result") - return RPC_RESULT; - else - throw invalid_msg_type_error("Invalid type name: " + _msg_type_name); - } -} // namespace el::msglink - diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index b96e8c2..45e280b 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -26,11 +26,11 @@ msglink server class #include -#include -#include +#include "../retcode.hpp" +#include "../logging.hpp" -#include -#include +#include "internal/wspp.hpp" +#include "errors.hpp" #define PRINT_CALL std::cout << __PRETTY_FUNCTION__ << std::endl diff --git a/include/el/strutil.hpp b/include/el/strutil.hpp index d5f938b..cfda2ed 100644 --- a/include/el/strutil.hpp +++ b/include/el/strutil.hpp @@ -105,21 +105,21 @@ namespace el::strutil /** - * @brief stringswitch - a macro based wrapper for if statements - * allowing you to compare std::strings using syntax somewhat similar to - * switch-case statements. As this is purly macro based, there will be - * no namespace annotations unfortunately. + * @brief chain compare - a macro based wrapper for if statements + * allowing you to compare arbitrary types using syntax somewhat similar to + * switch-case statements. As this is purely macro based, there will generate + * if else statements comparing values. * * Limitations: variables created inside the block are local, every case * has to use brackets if it is more than one statement in size */ +#define el_chain_compare(variable) \ + { \ + const auto &__el_strswitch_strtempvar__ = variable; \ + if (false) {} -#define stringswitch(strval) {\ - const std::string &__el_strswitch_strtempvar__ = strval; - -#define scase(strval) if (__el_strswitch_strtempvar__ == strval) - -#define switchend } +#define el_case(value) else if (__el_strswitch_strtempvar__ == value) +#define el_end_compare } }; \ No newline at end of file From 3c56b66ec35b91094553ad664264cf464b32d47f Mon Sep 17 00:00:00 2001 From: melektron Date: Wed, 29 Nov 2023 21:51:14 +0100 Subject: [PATCH 16/50] implemented auth handling to some degree --- include/el/msglink/errors.hpp | 22 ++++- .../el/msglink/internal/link_interface.hpp | 58 +++++++++++++ include/el/msglink/internal/messages.hpp | 11 ++- include/el/msglink/internal/msgtype.hpp | 4 +- include/el/msglink/internal/proto_version.hpp | 5 +- include/el/msglink/internal/types.hpp | 6 +- include/el/msglink/internal/ws_close_code.hpp | 27 +++++++ include/el/msglink/link.hpp | 81 ++++++++++++++++--- 8 files changed, 192 insertions(+), 22 deletions(-) create mode 100644 include/el/msglink/internal/link_interface.hpp create mode 100644 include/el/msglink/internal/ws_close_code.hpp diff --git a/include/el/msglink/errors.hpp b/include/el/msglink/errors.hpp index 1deb39a..b96e0f5 100644 --- a/include/el/msglink/errors.hpp +++ b/include/el/msglink/errors.hpp @@ -15,7 +15,10 @@ msglink exceptions #include #include + #include "../exceptions.hpp" +#include "internal/ws_close_code.hpp" + namespace el::msglink { @@ -95,7 +98,24 @@ namespace el::msglink */ class incompatible_link_error : public msglink_error { - using msglink_error::msglink_error; + close_code_t m_code; + + public: + + incompatible_link_error(close_code_t _code ,const char *_msg) + : msglink_error(_msg) + , m_code(_code) + {} + + incompatible_link_error(close_code_t _code, const std::string &_msg) + : msglink_error(_msg) + , m_code(_code) + {} + + close_code_t code() const noexcept + { + return m_code; + } }; /** diff --git a/include/el/msglink/internal/link_interface.hpp b/include/el/msglink/internal/link_interface.hpp new file mode 100644 index 0000000..a344856 --- /dev/null +++ b/include/el/msglink/internal/link_interface.hpp @@ -0,0 +1,58 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +29.11.23, 08:46 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Class used as a common interface for the underlying networking class +used by the link instance to communicate to the other party. + +Both server and client networking implementations inherit from this and provide +the common interface declared by this class. +*/ + +#pragma once + +#include +#include + + +namespace el::msglink +{ + class link_interface + { + public: + /** + * @brief closes the connection and possibly destroys the link + * (depending on the function of the communication backend, e.g. + * server vs. client) + * + * @param _code the close code specifying the error/reason causing the close. + * @param _reason human readable reason for close. + */ + virtual void close_connection(int _code, std::string _reason) noexcept = 0; + + /** + * @brief encodes and then sends json content through the + * communication channels + * + * @param _jcontent json document to send + */ + void send_message(const nlohmann::json &_jcontent) + { + send_message(_jcontent.dump()); + }; + + protected: + /** + * @brief sends a message via the communication channel. + * + * @param _content string content to send + */ + virtual void send_message(const std::string &_content) = 0; + }; +} // namespace el::msglink diff --git a/include/el/msglink/internal/messages.hpp b/include/el/msglink/internal/messages.hpp index a7e90c7..25a5ff8 100644 --- a/include/el/msglink/internal/messages.hpp +++ b/include/el/msglink/internal/messages.hpp @@ -14,8 +14,11 @@ Codables for internal communication messages #pragma once #include +#include + #include "../../codable.hpp" #include "types.hpp" +#include "msgtype.hpp" namespace el::msglink { @@ -32,11 +35,12 @@ namespace el::msglink : public base_msg_t , public codable { + std::string type = __EL_MSGLINK_MSG_NAME_AUTH; proto_version_t proto_version; link_version_t link_version; - std::vector events; - std::vector data_sources; - std::vector procedures; + std::set events; + std::set data_sources; + std::set procedures; EL_DEFINE_CODABLE( msg_auth_t, @@ -54,6 +58,7 @@ namespace el::msglink : public base_msg_t , public codable { + std::string type = __EL_MSGLINK_MSG_NAME_AUTH_ACK; EL_DEFINE_CODABLE( msg_auth_ack_t, type, diff --git a/include/el/msglink/internal/msgtype.hpp b/include/el/msglink/internal/msgtype.hpp index 0b32a74..2adcf5d 100644 --- a/include/el/msglink/internal/msgtype.hpp +++ b/include/el/msglink/internal/msgtype.hpp @@ -58,7 +58,7 @@ namespace el::msglink RPC_RESULT, }; - const char *msg_type_to_string(const msg_type_t _msg_type) noexcept + const char *msg_type_to_string(const msg_type_t _msg_type) { switch (_msg_type) { @@ -118,7 +118,7 @@ namespace el::msglink } } - msg_type_t msg_type_from_string(const std::string &_msg_type_name) noexcept + msg_type_t msg_type_from_string(const std::string &_msg_type_name) { using enum msg_type_t; diff --git a/include/el/msglink/internal/proto_version.hpp b/include/el/msglink/internal/proto_version.hpp index 7379eb0..7a12bb1 100644 --- a/include/el/msglink/internal/proto_version.hpp +++ b/include/el/msglink/internal/proto_version.hpp @@ -14,13 +14,16 @@ functions to check msglink protocol version compatibility #pragma once #include +#include #include "../../strutil.hpp" -#include "types.hpp" namespace el::msglink::proto_version { + + using proto_version_t = std::array; + // the current protocol version of this source tree inline const proto_version_t current = {0, 1, 0}; diff --git a/include/el/msglink/internal/types.hpp b/include/el/msglink/internal/types.hpp index c086f0d..9698f82 100644 --- a/include/el/msglink/internal/types.hpp +++ b/include/el/msglink/internal/types.hpp @@ -14,11 +14,13 @@ msglink type aliases used in other files to easily be able to change types #pragma once #include -#include + +#include "proto_version.hpp" + namespace el::msglink { using tid_t = int64_t; - using proto_version_t = std::array; + using proto_version::proto_version_t; using link_version_t = uint32_t; } // namespace el::msglink diff --git a/include/el/msglink/internal/ws_close_code.hpp b/include/el/msglink/internal/ws_close_code.hpp new file mode 100644 index 0000000..bbd89ae --- /dev/null +++ b/include/el/msglink/internal/ws_close_code.hpp @@ -0,0 +1,27 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +29.11.23, 09:38 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Websocket close codes specific to msglink. +*/ + +#pragma once + +namespace el::msglink +{ + enum class close_code_t + { + CLOSED_BY_USER = 1000, + PROTO_VERSION_INCOMPATIBLE = 3001, + LINK_VERSION_MISMATCH = 3002, + EVENT_REQUIREMENTS_NOT_SATISFIED = 3003, + DATA_SOURCE_REQUIREMENTS_NOT_SATISFIED = 3004, + RPC_REQUIREMENTS_NOT_SATISFIED = 3005, + }; +} // namespace el::msglink diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index eacbf48..265da4f 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -19,6 +19,7 @@ the user to define the API/protocol of a link #include #include #include +#include #include @@ -30,6 +31,7 @@ the user to define the API/protocol of a link #include "internal/messages.hpp" #include "internal/types.hpp" #include "internal/proto_version.hpp" +#include "internal/link_interface.hpp" namespace el::msglink @@ -56,6 +58,9 @@ namespace el::msglink class link { private: + // the link interface representing the underlying communication class + // used to send messages and manage the connection + link_interface &interface; // the value to step for every new transaction ID generated. // This is set to either 1 or -1 in the constructor depending on wether @@ -74,9 +79,9 @@ namespace el::msglink using event_handler_wrapper_t = std::function; // set of all outgoing events (including bidirectional ones) - std::unordered_set outgoing_events; + std::set outgoing_events; // set of all incoming events (including bidirectional ones) - std::unordered_set incoming_events; + std::set incoming_events; // map of incoming event name to handler function std::unordered_map< @@ -86,7 +91,7 @@ namespace el::msglink private: // methods - decltype(link::tid_counter) generate_new_tid() noexcept + tid_t generate_new_tid() noexcept { // use value before counting, so first value is 1/-1 // as defined in the spec @@ -137,15 +142,46 @@ namespace el::msglink // check protocol version if we are the higher one if (proto_version::current > msg.proto_version) if (!proto_version::is_compatible(msg.proto_version)) - throw incompatible_link_error(el::strutil::format( - "Incompatible protocol versions: this=%u, other=%u", - proto_version::to_string(proto_version::current).c_str(), - proto_version::to_string(msg.proto_version).c_str() - )); + throw incompatible_link_error( + close_code_t::PROTO_VERSION_INCOMPATIBLE, + el::strutil::format( + "Incompatible protocol versions: this=%u, other=%u", + proto_version::to_string(proto_version::current).c_str(), + proto_version::to_string(msg.proto_version).c_str() + ) + ); // check user defined link version if (msg.link_version != get_link_version()) - throw incompatible_link_error(el::strutil::format("Link versions don't match: this=%u, other=%u", get_link_version(), msg.link_version)); + throw incompatible_link_error( + close_code_t::LINK_VERSION_MISMATCH, + el::strutil::format( + "Link versions don't match: this=%u, other=%u", + get_link_version(), + msg.link_version + ) + ); + + // check event list + if (!std::includes( + msg.events.begin(), msg.events.end(), + incoming_events.begin(), incoming_events.end() + )) + throw incompatible_link_error( + close_code_t::EVENT_REQUIREMENTS_NOT_SATISFIED, + "Remote party does not satisfy the event requirements (missing events)" + ); + + // check data sources + + // check procedures + + + // all good, send acknowledgement message + msg_auth_ack_t response; + response.tid = msg.tid; + interface.send_message(response); + auth_ack_sent.set(); } break; @@ -153,7 +189,9 @@ namespace el::msglink { msg_auth_ack_t msg(_jmsg); + // TODO: check transaction ID + auth_ack_received.set(); } break; @@ -233,7 +271,7 @@ namespace el::msglink * user defined protocol the link represents. When two parties connect, their * link versions must match. */ -#define EL_MSGLINK_LINK_VERSION(version_num) virtual link_version_t _el_msglink_get_link_version() const noexcept override { return version_num; } +#define EL_MSGLINK_LINK_VERSION(version_num) virtual el::msglink::link_version_t _el_msglink_get_link_version() const noexcept override { return version_num; } /** * @brief Method for registering a link-method event handler @@ -284,10 +322,12 @@ namespace el::msglink * @brief Construct a new link object. * * @param _is_server determines the TID series used (+n or -n) + * @param _interface interface representing the communication class used to manage connection */ - link(bool _is_server) + link(bool _is_server, link_interface &_interface) : tid_step_value(_is_server ? 1 : -1) , tid_counter(tid_step_value) + , interface(_interface) {} /** @@ -300,7 +340,17 @@ namespace el::msglink void on_connection_established() { - + std::cout << "connection established called" << std::endl; + + // send initial auth message + msg_auth_t msg; + msg.tid = generate_new_tid(); + msg.proto_version = proto_version::current; + msg.link_version = get_link_version(); + msg.events = outgoing_events; + //msg.data_sources = ...; + //msg.procedures = ...; + interface.send_message(msg); } void on_message(const std::string &_msg_content) @@ -332,7 +382,12 @@ namespace el::msglink } catch (const nlohmann::json::exception &e) { - throw malformed_message_error(el::strutil::format("Malformed JSON link message: %s\n%s", _msg_content.c_str(), e.what())); + throw malformed_message_error(el::strutil::format( + "Malformed JSON link message (%s auth): %s\n%s", + authentication_done ? "post" : "pre", + _msg_content.c_str(), + e.what() + )); } } }; From 13fdcafa0d6bd7ce367bc091bbb8f1b2e907d61d Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 3 Dec 2023 23:17:00 +0100 Subject: [PATCH 17/50] implemented transaction system and implemented auth transaction properly --- include/el/exceptions.hpp | 7 +- include/el/logging.hpp | 17 +-- include/el/msglink/errors.hpp | 34 ++++- include/el/msglink/internal/transaction.hpp | 61 +++++++++ include/el/msglink/link.hpp | 133 +++++++++++++++++--- include/el/rtti_utils.hpp | 36 ++++++ 6 files changed, 249 insertions(+), 39 deletions(-) create mode 100644 include/el/msglink/internal/transaction.hpp create mode 100644 include/el/rtti_utils.hpp diff --git a/include/el/exceptions.hpp b/include/el/exceptions.hpp index d949df0..0418460 100644 --- a/include/el/exceptions.hpp +++ b/include/el/exceptions.hpp @@ -16,6 +16,8 @@ el-std base exceptions #include #include +#include "strutil.hpp" + namespace el { @@ -32,8 +34,9 @@ namespace el : m_message(_msg) {} - exception(const std::string &_msg) - : m_message(_msg) + template + exception(const std::string &_msg_fmt, _Args... _args) + : m_message(strutil::format(_msg_fmt, _args...)) {} virtual ~exception() noexcept = default; diff --git a/include/el/logging.hpp b/include/el/logging.hpp index 0e58741..e852ed5 100644 --- a/include/el/logging.hpp +++ b/include/el/logging.hpp @@ -18,11 +18,8 @@ Simple logging framework #include #include -#ifdef __GNUC__ -#include -#endif - -#include +#include "strutil.hpp" +#include "rtti_utils.hpp" // color escape sequences @@ -186,15 +183,7 @@ namespace el::logging */ std::string format_exception(const std::exception &_e) { -#ifdef __GNUC__ - int status = 0; - char *ex_type_name = abi::__cxa_demangle(typeid(_e).name(), nullptr, nullptr, &status); - auto output = std::string(ex_type_name) + ": " + _e.what(); - free(ex_type_name); - return output; -#else - return std::string(typeid(_e).name()) + ": " + _e.what(); -#endif + return rtti::demangle_if_possible(typeid(_e).name()) + "\n what(): " + _e.what(); } } // namespace el::log diff --git a/include/el/msglink/errors.hpp b/include/el/msglink/errors.hpp index b96e0f5..18bbb53 100644 --- a/include/el/msglink/errors.hpp +++ b/include/el/msglink/errors.hpp @@ -92,6 +92,33 @@ namespace el::msglink using msglink_error::msglink_error; }; + /** + * @brief attempted to create or register a new transaction but a + * transaction with the same ID already exists. + */ + class duplicate_transaction_error : public msglink_error + { + using msglink_error::msglink_error; + }; + + /** + * @brief attempted to retrieve an active transaction with invalid ID + * or the active transaction does not match the required type. + */ + class invalid_transaction_error : public msglink_error + { + using msglink_error::msglink_error; + }; + + /** + * @brief received messages which do not conform to the expected + * conversation as defined by the protocol. + */ + class protocol_error : public msglink_error + { + using msglink_error::msglink_error; + }; + /** * @brief link is not compatible with the link of the other party. * This may be thrown during authentication. @@ -106,9 +133,10 @@ namespace el::msglink : msglink_error(_msg) , m_code(_code) {} - - incompatible_link_error(close_code_t _code, const std::string &_msg) - : msglink_error(_msg) + + template + incompatible_link_error(close_code_t _code, const std::string &_msg_fmt, _Args... _args) + : msglink_error(_msg_fmt, _args...) , m_code(_code) {} diff --git a/include/el/msglink/internal/transaction.hpp b/include/el/msglink/internal/transaction.hpp new file mode 100644 index 0000000..d48d861 --- /dev/null +++ b/include/el/msglink/internal/transaction.hpp @@ -0,0 +1,61 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +30.11.23, 13:09 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Structures to represent running transactions +*/ + +#pragma once + +#include + +#include "types.hpp" + + +namespace el::msglink +{ + enum class inout_t + { + INCOMING = 0, + OUTGOING = 1 + }; + + struct transaction_t + { + tid_t id; + const inout_t direction = inout_t::OUTGOING; + + // dummy to make dynamic polymorphic type casting work + virtual void _poly_dummy() const noexcept {}; + + bool is_incoming() const noexcept + { + return direction == inout_t::INCOMING; + } + + bool is_outgoing() const noexcept + { + return direction == inout_t::OUTGOING; + } + + transaction_t() = default; + transaction_t(tid_t _id, inout_t _direction) + : id(_id) + , direction(_direction) + {} + }; + + using transaction_ptr_t = std::shared_ptr; + + struct transaction_auth_t : public transaction_t + { + using transaction_t::transaction_t; + }; + +} // namespace el::msglink diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index 265da4f..a256dcd 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -25,6 +25,7 @@ the user to define the API/protocol of a link #include "../logging.hpp" #include "../flags.hpp" +#include "../rtti_utils.hpp" #include "event.hpp" #include "errors.hpp" #include "internal/msgtype.hpp" @@ -32,6 +33,7 @@ the user to define the API/protocol of a link #include "internal/types.hpp" #include "internal/proto_version.hpp" #include "internal/link_interface.hpp" +#include "internal/transaction.hpp" namespace el::msglink @@ -68,6 +70,8 @@ namespace el::msglink const int8_t tid_step_value; // running counter for transaction IDs (initialized to tid_step_value) tid_t tid_counter; + // map of active transactions that take multiple back and forth messages to complete + std::map active_transactions; // flags set to track the authentication process soflag auth_ack_sent; @@ -91,13 +95,95 @@ namespace el::msglink private: // methods + /** + * @return tid_t new transaction ID according to the + * internal running series + */ tid_t generate_new_tid() noexcept { // use value before counting, so first value is 1/-1 // as defined in the spec auto tmp = tid_counter; tid_counter += tid_step_value; - return tid_counter; + return tmp; + } + + /** + * @brief Creates and registers a new transaction + * with given type, ID and init parameters in the active + * transaction map. + * It then returns a pointer to the created transaction + * + * @tparam _TR transaction type + * @tparam _Args ctor parameter types + * @param _tid transaction ID + * @param _args further ctor arguments + * @return std::shared_ptr<_TR> pointer to created transaction + */ + template _TR, typename... _Args> + inline std::shared_ptr<_TR> create_transaction(tid_t _tid, _Args ..._args) + { + if (active_transactions.contains(_tid)) + throw duplicate_transaction_error("Transaction with ID=%d already exists", _tid); + + auto new_transaction = std::make_shared<_TR>( + _tid, + std::forward<_Args>(_args)... + ); + active_transactions[_tid] = new_transaction; + + return new_transaction; + } + + /** + * @brief Retrieves the active transaction of the required + * type and ID. If there is no transaction with this ID or the + * transaction does not match the expected type, + * invalid_transaction_error is thrown. + * + * @tparam _TR expected transaction type + * @param _tid ID of action to retrieve + * @return std::shared_ptr<_TR> the targeted action + */ + template _TR> + inline std::shared_ptr<_TR> get_transaction(tid_t _tid) + { + transaction_ptr_t transaction; + + try + { + transaction = active_transactions.at(_tid); + } + catch(const std::out_of_range) + { + throw invalid_transaction_error("No active transaction with ID=%d", _tid); + } + + std::shared_ptr<_TR> target_type_transaction = + std::dynamic_pointer_cast<_TR>(transaction); + + if (target_type_transaction == nullptr) + throw invalid_transaction_error( + "Active transaction with ID=%d (%s) does not match the required type %s", + _tid, + rtti::demangle_if_possible(typeid(*transaction).name()).c_str(), + rtti::demangle_if_possible(typeid(_TR).name()).c_str() + ); + + return target_type_transaction; + } + + /** + * @brief completes a transaction by removing it from the + * map of active transactions + * + * @tparam _TR deduced transaction type + * @param _transaction transaction to remove + */ + template _TR> + inline void complete_transaction(const std::shared_ptr<_TR> &_transaction) noexcept + { + active_transactions.erase(_transaction->id); } /** @@ -138,28 +224,25 @@ namespace el::msglink { // validate message msg_auth_t msg(_jmsg); + // this transaction completes immediately, no need to register // check protocol version if we are the higher one if (proto_version::current > msg.proto_version) if (!proto_version::is_compatible(msg.proto_version)) throw incompatible_link_error( - close_code_t::PROTO_VERSION_INCOMPATIBLE, - el::strutil::format( - "Incompatible protocol versions: this=%u, other=%u", - proto_version::to_string(proto_version::current).c_str(), - proto_version::to_string(msg.proto_version).c_str() - ) + close_code_t::PROTO_VERSION_INCOMPATIBLE, + "Incompatible protocol versions: this=%u, other=%u", + proto_version::to_string(proto_version::current).c_str(), + proto_version::to_string(msg.proto_version).c_str() ); // check user defined link version if (msg.link_version != get_link_version()) throw incompatible_link_error( close_code_t::LINK_VERSION_MISMATCH, - el::strutil::format( - "Link versions don't match: this=%u, other=%u", - get_link_version(), - msg.link_version - ) + "Link versions don't match: this=%u, other=%u", + get_link_version(), + msg.link_version ); // check event list @@ -176,8 +259,7 @@ namespace el::msglink // check procedures - - // all good, send acknowledgement message + // all good, send acknowledgement message, transaction complete msg_auth_ack_t response; response.tid = msg.tid; interface.send_message(response); @@ -189,14 +271,20 @@ namespace el::msglink { msg_auth_ack_t msg(_jmsg); - // TODO: check transaction ID + auto transaction = get_transaction(msg.tid); + // check that this is actually a response to our outgoing request + if (!transaction->is_outgoing()) + throw protocol_error("Received AUTH ACK for foreign AUTH transaction"); + // This would mean that the remote party has sent an acknowledgement to its own auth message which makes no sense + + complete_transaction(transaction); auth_ack_received.set(); } break; default: - throw malformed_message_error(el::strutil::format("Invalid pre-auth message type: %s", msg_type_to_string(_msg_type))); + throw malformed_message_error("Invalid pre-auth message type: %s", msg_type_to_string(_msg_type)); break; } @@ -249,7 +337,7 @@ namespace el::msglink break; default: - throw malformed_message_error(el::strutil::format("Invalid post-auth message type: %s", msg_type_to_string(_msg_type))); + throw malformed_message_error("Invalid post-auth message type: %s", msg_type_to_string(_msg_type)); break; } @@ -341,10 +429,15 @@ namespace el::msglink void on_connection_established() { std::cout << "connection established called" << std::endl; + + auto transaction = create_transaction( + generate_new_tid(), + inout_t::OUTGOING + ); // send initial auth message msg_auth_t msg; - msg.tid = generate_new_tid(); + msg.tid = transaction->id; msg.proto_version = proto_version::current; msg.link_version = get_link_version(); msg.events = outgoing_events; @@ -382,12 +475,12 @@ namespace el::msglink } catch (const nlohmann::json::exception &e) { - throw malformed_message_error(el::strutil::format( + throw malformed_message_error( "Malformed JSON link message (%s auth): %s\n%s", authentication_done ? "post" : "pre", _msg_content.c_str(), e.what() - )); + ); } } }; diff --git a/include/el/rtti_utils.hpp b/include/el/rtti_utils.hpp new file mode 100644 index 0000000..bca899d --- /dev/null +++ b/include/el/rtti_utils.hpp @@ -0,0 +1,36 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +03.12.23, 23:03 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Utilities for runtime type information (RTTI) +*/ + +#pragma once + +#include +#ifdef __GNUC__ +#include +#endif + +namespace el::rtti +{ + std::string demangle_if_possible(const char* _typename) + { + +#ifdef __GNUC__ + int status = 0; + char *ex_type_name = abi::__cxa_demangle(_typename, nullptr, nullptr, &status); + std::string output(ex_type_name); + free(ex_type_name); // required by cxxabi + return output; +#else + return _typename +#endif + } +} // namespace el::rtti From eac648c9071c6a6e94c1ffb390dee60803e14c79 Mon Sep 17 00:00:00 2001 From: melektron Date: Mon, 4 Dec 2023 14:59:30 +0100 Subject: [PATCH 18/50] fixed format warning by adding specialized, non formatting dynamic string exception ctor --- include/el/exceptions.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/include/el/exceptions.hpp b/include/el/exceptions.hpp index 0418460..37e6b36 100644 --- a/include/el/exceptions.hpp +++ b/include/el/exceptions.hpp @@ -30,10 +30,15 @@ namespace el std::string m_message; public: + exception(const char *_msg) : m_message(_msg) {} + exception(const std::string &_msg) + : m_message(_msg) + {} + template exception(const std::string &_msg_fmt, _Args... _args) : m_message(strutil::format(_msg_fmt, _args...)) From 177348fecf6c91737e25500adaca9e060086f281 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 10 Dec 2023 14:47:02 +0100 Subject: [PATCH 19/50] improved logging --- include/el/cxxversions.h | 19 ++++- include/el/logging.hpp | 139 +++++++++++++++++++++++++--------- include/el/msglink/link.hpp | 7 +- include/el/msglink/server.hpp | 25 +++--- 4 files changed, 136 insertions(+), 54 deletions(-) diff --git a/include/el/cxxversions.h b/include/el/cxxversions.h index c9da055..7caf94a 100644 --- a/include/el/cxxversions.h +++ b/include/el/cxxversions.h @@ -8,18 +8,31 @@ All rights reserved. This source code is licensed under the Apache-2.0 license found in the LICENSE file in the root directory of this source tree. -Preprocessor definitions for detecting C++ verisons -and enabeling supported features. +Preprocessor definitions for detecting C++ versions +and enabling supported features. -Currentyl this might not work for all compilers and it might not work at all. +The library checks for the ENABLE macro definitions. + +Currently this might not work for all compilers and it might not work at all. In order to bypass all version checking, just #define __EL_ENABLE_CXX11 #define __EL_ENABLE_CXX17 +#define __EL_ENABLE_CXX20 to enable library features for versions not detected using the __cplusplus definition. */ #pragma once +#ifdef __linux__ +#define __EL_PLATFORM_LINUX +#endif +#ifdef __APPLE__ +#define __EL_PLATFORM_APPLE +#endif +#ifdef _WIN32 +#define __EL_PLATFORM_WINDOWS +#endif + // check for C++ 11 compatablility #if __cplusplus > 199711L diff --git a/include/el/logging.hpp b/include/el/logging.hpp index e852ed5..27370e5 100644 --- a/include/el/logging.hpp +++ b/include/el/logging.hpp @@ -13,11 +13,17 @@ Simple logging framework #pragma once +#include "cxxversions.h" + #include #include #include #include +#ifdef __EL_ENABLE_CXX20 +#include +#endif + #include "strutil.hpp" #include "rtti_utils.hpp" @@ -36,33 +42,97 @@ Simple logging framework #define _EL_LOG_FILEW 15 #define _EL_LOG_LINEW 4 - -#define EL_DEFINE_LOGGER() auto logger_inst = el::logging::logger(__FILE__) -#define EL_LOGC(fmt, ...) logger_inst.critical(__LINE__, fmt, ## __VA_ARGS__) -#define EL_LOGE(fmt, ...) logger_inst.error(__LINE__, fmt, ## __VA_ARGS__) -#define EL_LOGW(fmt, ...) logger_inst.warning(__LINE__, fmt, ## __VA_ARGS__) -#define EL_LOGI(fmt, ...) logger_inst.info(__LINE__, fmt, ## __VA_ARGS__) -#define EL_LOGD(fmt, ...) logger_inst.debug(__LINE__, fmt, ## __VA_ARGS__) +#if defined(__EL_PLATFORM_LINUX) +# include +#elif defined(__EL_PLATFORM_WINDOWS) +# include +# define PATH_MAX MAX_PATH +#elif defined(__EL_PLATFORM_APPLE) +# include +#endif + +/** + * Source location information. + * Since C++20 there is builtin portable support for getting + * the source location by using the header. + * It provides the std::source_location type as well as the function + * std::source_location::current() to get the current source location. + * The returned object can be used to retrieve file name, line number, column + * and function name. + * + * Before C++20, macros and other builtin symbols had to be used. This is not quite + * portable however. + * + * We use the C++20 features where possible and fallback to macros otherwise. + */ + +#ifdef __EL_ENABLE_CXX20 + +#define _EL_LOG_FILE std::source_location::current().file_name() +#define _EL_LOG_LINE std::source_location::current().line() +#define _EL_LOG_FUNCTION std::source_location::current().function_name() + +#else + +// these might not be possible for every compiler +#define _EL_LOG_FILE __FILE__ +#define _EL_LOG_LINE __LINE__ +#define _EL_LOG_FUNCTION __PRETTY_FUNCTION__ + +#endif + + +#define EL_DEFINE_LOGGER() el::logging::logger el::logging::logger_inst +#define EL_LOGC(fmt, ...) el::logging::logger_inst.critical(_EL_LOG_FILE, _EL_LOG_LINE, fmt, ## __VA_ARGS__) +#define EL_LOGE(fmt, ...) el::logging::logger_inst.error(_EL_LOG_FILE, _EL_LOG_LINE, fmt, ## __VA_ARGS__) +#define EL_LOGW(fmt, ...) el::logging::logger_inst.warning(_EL_LOG_FILE, _EL_LOG_LINE, fmt, ## __VA_ARGS__) +#define EL_LOGI(fmt, ...) el::logging::logger_inst.info(_EL_LOG_FILE, _EL_LOG_LINE, fmt, ## __VA_ARGS__) +#define EL_LOGD(fmt, ...) el::logging::logger_inst.debug(_EL_LOG_FILE, _EL_LOG_LINE, fmt, ## __VA_ARGS__) #define EL_LOG_EXCEPTION_MSG(msg, ex) EL_LOGE(msg ": %s", el::logging::format_exception(ex).c_str()) #define EL_LOG_EXCEPTION(ex) EL_LOG_EXCEPTION_MSG("Exception occurred", ex) +#define EL_LOG_FUNCTION_CALL() EL_LOGD("Function call: \e[1;3m%s\e", _EL_LOG_FUNCTION) // the color reset happens at end of line anyway, bold is reset there too + + namespace el::logging { class logger { private: - const std::string m_file_name; - void generate_prefix(char *_output_buffer, int _line, const char *_level) + void generate_prefix(char *_output_buffer, const char *_file, int _line, const char *_level) { + // A file name/path can be (_EL_LOG_FILEW - 1) characters long and will be + // printed in a _EL_LOG_FILEW characters wide area, so ther will always be a space + // to the left side. If the path is _EL_LOG_FILEW or more characters long, then it will + // be truncated on the left side and the space will be replaced by an overflow indicator ('<') + + // substring filename for alignment if necessary + char file_name[_EL_LOG_FILEW + 1] = {'\0'}; + size_t file_len = strnlen(_file, PATH_MAX); + if (file_len != PATH_MAX && file_len > (_EL_LOG_FILEW - 1)) // bigger than width -1 because we always want to have one space to the left unless for the overflow indicator + { + // if filename is to wide, truncate it to the _EL_LOG_FILEW rightmost characters + // and add '<' to it's start + strncpy(file_name, _file + (file_len - _EL_LOG_FILEW), _EL_LOG_FILEW); + file_name[_EL_LOG_FILEW] = '\0'; + file_name[0] = '<'; + } + else + { + // otherwise just copy it + strncpy(file_name, _file, _EL_LOG_FILEW); + file_name[_EL_LOG_FILEW] = '\0'; + } + snprintf( _output_buffer, _EL_LOG_PREFIX_BUFFER_SIZE - 1, - "[%*.*s@%-*d] %s: ", + "[%*.*s@%-*d ] %s: ", // at least one space on the side always _EL_LOG_FILEW, _EL_LOG_FILEW, - m_file_name.c_str(), + file_name, _EL_LOG_LINEW, _line, _level @@ -70,108 +140,109 @@ namespace el::logging } public: - logger(std::string _file_name) - : m_file_name(_file_name) - {} + logger() = default; // Critical - void critical(int _line, const std::string &_message) + void critical(const char *_file, int _line, const std::string &_message) { // generate prefix char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; - generate_prefix(prefix_buffer, _line, "C"); + generate_prefix(prefix_buffer, _file, _line, "C"); // print in color std::cout << _EL_LOG_ANSI_COLOR_RED << prefix_buffer << _message << _EL_LOG_ANSI_COLOR_RESET << std::endl; } template - void critical(int _line, const std::string &_fmt, _Args... _args) + void critical(const char *_file, int _line, const std::string &_fmt, _Args... _args) { // format the message - critical(_line, strutil::format(_fmt, _args...)); + critical(_file, _line, strutil::format(_fmt, _args...)); } // Error - void error(int _line, const std::string &_message) + void error(const char *_file, int _line, const std::string &_message) { // generate prefix char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; - generate_prefix(prefix_buffer, _line, "E"); + generate_prefix(prefix_buffer, _file, _line, "E"); // print in color std::cout << _EL_LOG_ANSI_COLOR_RED << prefix_buffer << _message << _EL_LOG_ANSI_COLOR_RESET << std::endl; } template - void error(int _line, const std::string &_fmt, _Args... _args) + void error(const char *_file, int _line, const std::string &_fmt, _Args... _args) { // format the message - error(_line, strutil::format(_fmt, _args...)); + error(_file, _line, strutil::format(_fmt, _args...)); } // Warning - void warning(int _line, const std::string &_message) + void warning(const char *_file, int _line, const std::string &_message) { // generate prefix char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; - generate_prefix(prefix_buffer, _line, "W"); + generate_prefix(prefix_buffer, _file, _line, "W"); // print in color std::cout << _EL_LOG_ANSI_COLOR_YELLOW << prefix_buffer << _message << _EL_LOG_ANSI_COLOR_RESET << std::endl; } template - void warning(int _line, const std::string &_fmt, _Args... _args) + void warning(const char *_file, int _line, const std::string &_fmt, _Args... _args) { // format the message - warning(_line, strutil::format(_fmt, _args...)); + warning(_file, _line, strutil::format(_fmt, _args...)); } // Info - void info(int _line, const std::string &_message) + void info(const char *_file, int _line, const std::string &_message) { // generate prefix char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; - generate_prefix(prefix_buffer, _line, "I"); + generate_prefix(prefix_buffer, _file, _line, "I"); // print in color std::cout << _EL_LOG_ANSI_COLOR_RESET << prefix_buffer << _message << _EL_LOG_ANSI_COLOR_RESET << std::endl; } template - void info(int _line, const std::string &_fmt, _Args... _args) + void info(const char *_file, int _line, const std::string &_fmt, _Args... _args) { // format the message - info(_line, strutil::format(_fmt, _args...)); + info(_file, _line, strutil::format(_fmt, _args...)); } // Debug - void debug(int _line, const std::string &_message) + void debug(const char *_file, int _line, const std::string &_message) { // generate prefix char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; - generate_prefix(prefix_buffer, _line, "D"); + generate_prefix(prefix_buffer, _file, _line, "D"); // print in color std::cout << _EL_LOG_ANSI_COLOR_GREEN << prefix_buffer << _message << _EL_LOG_ANSI_COLOR_RESET << std::endl; } template - void debug(int _line, const std::string &_fmt, _Args... _args) + void debug(const char *_file, int _line, const std::string &_fmt, _Args... _args) { // format the message - debug(_line, strutil::format(_fmt, _args...)); + debug(_file, _line, strutil::format(_fmt, _args...)); } }; + // declaration of global logger instance which has to be defined by the user + extern logger logger_inst; + /** * @brief turns the passed exception into a string * in the format ": " diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index a256dcd..6b9fcfa 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -390,8 +390,9 @@ namespace el::msglink incoming_event_handler_map.emplace( _ET::_event_name, - [this, handler](const nlohmann::json &_data) { - std::cout << "hievent " << _data << std::endl; + [this, handler](const nlohmann::json &_data) + { + EL_LOGD("hievent %s", _data.dump().c_str()); _ET new_event_inst; new_event_inst = _data; handler( @@ -428,7 +429,7 @@ namespace el::msglink void on_connection_established() { - std::cout << "connection established called" << std::endl; + EL_LOGD("connection established called"); auto transaction = create_transaction( generate_new_tid(), diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index 45e280b..4780712 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -33,9 +33,6 @@ msglink server class #include "errors.hpp" -#define PRINT_CALL std::cout << __PRETTY_FUNCTION__ << std::endl - - namespace el::msglink { using namespace std::chrono_literals; @@ -114,7 +111,7 @@ namespace el::msglink else if (_ec) throw unexpected_error(_ec); - std::cout << "ping timer" << std::endl; + EL_LOGD("ping timer"); // send a ping get_connection()->ping(""); // no message needed @@ -142,7 +139,7 @@ namespace el::msglink : m_socket_server(_socket_server) , m_connection(_connection) { - PRINT_CALL; + EL_LOG_FUNCTION_CALL(); // start the first ping schedule_ping(); @@ -154,7 +151,7 @@ namespace el::msglink */ virtual ~connection_handler() { - PRINT_CALL; + EL_LOG_FUNCTION_CALL(); // cancel ping timer if one is running if (m_ping_timer) @@ -168,7 +165,7 @@ namespace el::msglink */ void on_message(wsserver::message_ptr _msg) noexcept { - std::cout << "message: " << _msg->get_payload() << std::endl; + EL_LOGD("message: %s", _msg->get_payload().c_str()); get_connection()->send(_msg->get_payload(), _msg->get_opcode()); } @@ -244,7 +241,7 @@ namespace el::msglink */ void on_open(wspp::connection_hdl _hdl) { - PRINT_CALL; + EL_LOG_FUNCTION_CALL(); if (m_server_state != RUNNING) return; @@ -268,7 +265,7 @@ namespace el::msglink */ void on_message(wspp::connection_hdl _hdl, wsserver::message_ptr _msg) { - PRINT_CALL; + EL_LOG_FUNCTION_CALL(); if (m_server_state != RUNNING) return; @@ -294,7 +291,7 @@ namespace el::msglink */ void on_close(wspp::connection_hdl _hdl) { - PRINT_CALL; + EL_LOG_FUNCTION_CALL(); if (m_server_state != RUNNING) return; @@ -314,7 +311,7 @@ namespace el::msglink */ void on_pong_received(wspp::connection_hdl _hdl, std::string _payload) { - PRINT_CALL; + EL_LOG_FUNCTION_CALL(); if (m_server_state != RUNNING) return; @@ -339,7 +336,7 @@ namespace el::msglink */ void on_pong_timeout(wspp::connection_hdl _hdl, std::string _expected_payload) { - PRINT_CALL; + EL_LOG_FUNCTION_CALL(); if (m_server_state != RUNNING) return; @@ -360,7 +357,7 @@ namespace el::msglink server(int _port) : m_port(_port) { - PRINT_CALL; + EL_LOG_FUNCTION_CALL(); } // never copy or move a server @@ -369,7 +366,7 @@ namespace el::msglink ~server() { - PRINT_CALL; + EL_LOG_FUNCTION_CALL(); } /** From bb8f7c8e13e1886b5519508f20c9d41ededc188f Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 10 Dec 2023 16:41:09 +0100 Subject: [PATCH 20/50] implemented first idea of event subscription --- include/el/msglink/README.md | 2 +- include/el/msglink/errors.hpp | 11 +- include/el/msglink/internal/messages.hpp | 56 ++++++++++ include/el/msglink/internal/transaction.hpp | 23 ++++ include/el/msglink/link.hpp | 113 +++++++++++++------- 5 files changed, 162 insertions(+), 43 deletions(-) diff --git a/include/el/msglink/README.md b/include/el/msglink/README.md index feb51c8..91bc307 100644 --- a/include/el/msglink/README.md +++ b/include/el/msglink/README.md @@ -258,7 +258,7 @@ If the event is unknown by the other party, it will respond with a negative ackn } ``` -Otherwise, a positive acknowledgement will be sent: +Otherwise, a positive acknowledgement will be sent (even if event was already subscribed): ```json { diff --git a/include/el/msglink/errors.hpp b/include/el/msglink/errors.hpp index 18bbb53..4deb278 100644 --- a/include/el/msglink/errors.hpp +++ b/include/el/msglink/errors.hpp @@ -149,17 +149,22 @@ namespace el::msglink /** * @brief received unknown (invalid) incoming msglink event. * This means, the event is either not defined or defined as - * outgoing only. + * outgoing only. This is a protocol error because undefined + * events should be detected and caught during authentication. + * Transmitting messages for unknown events after auth success + * does not conform to the protocol and is likely the result + * of a library implementation mistake. */ - class invalid_incoming_event_error : public msglink_error + class invalid_incoming_event_error : public protocol_error { - using msglink_error::msglink_error; + using protocol_error::protocol_error; }; /** * @brief attempted to emit an unknown (invalid) outgoing msglink event. * This means, the event is either not defined or defined as * incoming only. + * This is only thrown as a result of local emit function calls. */ class invalid_outgoing_event_error : public msglink_error { diff --git a/include/el/msglink/internal/messages.hpp b/include/el/msglink/internal/messages.hpp index 25a5ff8..1323a6c 100644 --- a/include/el/msglink/internal/messages.hpp +++ b/include/el/msglink/internal/messages.hpp @@ -65,4 +65,60 @@ namespace el::msglink tid ) }; + + struct msg_evt_sub_t + : public base_msg_t + , public codable + { + std::string type = __EL_MSGLINK_MSG_NAME_EVT_SUB; + std::string name; + + EL_DEFINE_CODABLE( + msg_evt_sub_t, + type, + tid, + name + ) + }; + + struct msg_evt_sub_nak_t + : public base_msg_t + , public codable + { + std::string type = __EL_MSGLINK_MSG_NAME_EVT_SUB_NAK; + EL_DEFINE_CODABLE( + msg_evt_sub_nak_t, + type, + tid + ) + }; + + struct msg_evt_sub_ack_t + : public base_msg_t + , public codable + { + std::string type = __EL_MSGLINK_MSG_NAME_EVT_SUB_ACK; + EL_DEFINE_CODABLE( + msg_evt_sub_ack_t, + type, + tid + ) + }; + + struct msg_evt_emit_t + : public base_msg_t + , public codable + { + std::string type = __EL_MSGLINK_MSG_NAME_EVT_EMIT; + std::string name; + nlohmann::json data; + + EL_DEFINE_CODABLE( + msg_evt_emit_t, + type, + tid, + name, + data + ) + }; } // namespace el diff --git a/include/el/msglink/internal/transaction.hpp b/include/el/msglink/internal/transaction.hpp index d48d861..08d470d 100644 --- a/include/el/msglink/internal/transaction.hpp +++ b/include/el/msglink/internal/transaction.hpp @@ -44,6 +44,22 @@ namespace el::msglink return direction == inout_t::OUTGOING; } + /** + * @brief asserts that the transaction is outgoing + * and throws a protocol error if it is not. + * + * This might be use for example when an acknowledgement message is received + * where it doesn't make sense in some cases for the remote party to send + * an acknowledgement to it's own request. + * + * @param _exmsg message for the protocol error + */ + void assert_is_outgoing(const char *_exmsg) + { + if (!is_outgoing()) + throw protocol_error(_exmsg); + } + transaction_t() = default; transaction_t(tid_t _id, inout_t _direction) : id(_id) @@ -57,5 +73,12 @@ namespace el::msglink { using transaction_t::transaction_t; }; + + struct transaction_event_sub_t : public transaction_t + { + std::string event_name; + + using transaction_t::transaction_t; + }; } // namespace el::msglink diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index 6b9fcfa..530dc4c 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -82,21 +82,22 @@ namespace el::msglink // type of the lambda used to wrap event handlers using event_handler_wrapper_t = std::function; - // set of all outgoing events (including bidirectional ones) - std::set outgoing_events; - // set of all incoming events (including bidirectional ones) - std::set incoming_events; - - // map of incoming event name to handler function + // set of all possible outgoing events that are defined (including bidirectional ones) + std::set available_outgoing_events; + // set of all outgoing events that the other party has subscribed and therefore need to be transmitted + std::set active_outgoing_events; + // set of all possible incoming events that are defined (including bidirectional ones) + std::set available_incoming_events; + // map of all incoming events that we have currently subscribed to their handlers std::unordered_map< std::string, event_handler_wrapper_t - > incoming_event_handler_map; + > active_incoming_event_handlers; private: // methods /** - * @return tid_t new transaction ID according to the + * @return tid_t new transaction ID according to the * internal running series */ tid_t generate_new_tid() noexcept @@ -113,7 +114,7 @@ namespace el::msglink * with given type, ID and init parameters in the active * transaction map. * It then returns a pointer to the created transaction - * + * * @tparam _TR transaction type * @tparam _Args ctor parameter types * @param _tid transaction ID @@ -125,9 +126,9 @@ namespace el::msglink { if (active_transactions.contains(_tid)) throw duplicate_transaction_error("Transaction with ID=%d already exists", _tid); - + auto new_transaction = std::make_shared<_TR>( - _tid, + _tid, std::forward<_Args>(_args)... ); active_transactions[_tid] = new_transaction; @@ -137,11 +138,11 @@ namespace el::msglink /** * @brief Retrieves the active transaction of the required - * type and ID. If there is no transaction with this ID or the - * transaction does not match the expected type, + * type and ID. If there is no transaction with this ID or the + * transaction does not match the expected type, * invalid_transaction_error is thrown. - * - * @tparam _TR expected transaction type + * + * @tparam _TR expected transaction type * @param _tid ID of action to retrieve * @return std::shared_ptr<_TR> the targeted action */ @@ -154,29 +155,29 @@ namespace el::msglink { transaction = active_transactions.at(_tid); } - catch(const std::out_of_range) + catch (const std::out_of_range) { throw invalid_transaction_error("No active transaction with ID=%d", _tid); } - std::shared_ptr<_TR> target_type_transaction = + std::shared_ptr<_TR> target_type_transaction = std::dynamic_pointer_cast<_TR>(transaction); if (target_type_transaction == nullptr) throw invalid_transaction_error( - "Active transaction with ID=%d (%s) does not match the required type %s", + "Active transaction with ID=%d (%s) does not match the required type %s", _tid, rtti::demangle_if_possible(typeid(*transaction).name()).c_str(), rtti::demangle_if_possible(typeid(_TR).name()).c_str() ); - + return target_type_transaction; } /** - * @brief completes a transaction by removing it from the + * @brief completes a transaction by removing it from the * map of active transactions - * + * * @tparam _TR deduced transaction type * @param _transaction transaction to remove */ @@ -240,15 +241,15 @@ namespace el::msglink if (msg.link_version != get_link_version()) throw incompatible_link_error( close_code_t::LINK_VERSION_MISMATCH, - "Link versions don't match: this=%u, other=%u", - get_link_version(), + "Link versions don't match: this=%u, other=%u", + get_link_version(), msg.link_version ); - + // check event list if (!std::includes( msg.events.begin(), msg.events.end(), - incoming_events.begin(), incoming_events.end() + available_incoming_events.begin(), available_incoming_events.end() )) throw incompatible_link_error( close_code_t::EVENT_REQUIREMENTS_NOT_SATISFIED, @@ -256,7 +257,7 @@ namespace el::msglink ); // check data sources - + // check procedures // all good, send acknowledgement message, transaction complete @@ -272,12 +273,8 @@ namespace el::msglink msg_auth_ack_t msg(_jmsg); auto transaction = get_transaction(msg.tid); + transaction->assert_is_outgoing("Received AUTH ACK for foreign AUTH transaction"); - // check that this is actually a response to our outgoing request - if (!transaction->is_outgoing()) - throw protocol_error("Received AUTH ACK for foreign AUTH transaction"); - // This would mean that the remote party has sent an acknowledgement to its own auth message which makes no sense - complete_transaction(transaction); auth_ack_received.set(); } @@ -308,10 +305,48 @@ namespace el::msglink using enum msg_type_t; case EVENT_SUB: - break; + { + msg_evt_sub_t msg(_jmsg); + + if (!available_outgoing_events.contains(msg.name)) + { + // respond with nak + msg_evt_sub_nak_t response; + response.tid = msg.tid; + interface.send_message(response); + EL_LOGW("Received EVENT_SUB message for invalid event. This is likely a library implementation error and should not happen."); + } + + // otherwise activate (=subscribe to) the event + active_outgoing_events.insert(msg.name); + + // respond with positive acknowledgement, transaction complete + msg_evt_sub_ack_t response; + response.tid = msg.tid; + interface.send_message(response); + } + break; case EVENT_SUB_ACK: + { + msg_evt_sub_ack_t msg(_jmsg); + + // success, simply complete the transaction if it is valid + auto transaction = get_transaction(msg.tid); + transaction->assert_is_outgoing("Received EVT SUB ACK for foreign EVT SUB transaction"); + complete_transaction(transaction); + } break; case EVENT_SUB_NAK: + { + msg_evt_sub_nak_t msg(_jmsg); + + // complete the transaction if it is valid + auto transaction = get_transaction(msg.tid); + transaction->assert_is_outgoing("Received EVT SUB NAK for foreign EVT SUB transaction"); + complete_transaction(transaction); + + // if event sub failed, ... + } break; case EVENT_UNSUB: break; @@ -383,12 +418,12 @@ namespace el::msglink std::string event_name = _ET::_event_name; // save to incoming and outgoing event lists - incoming_events.insert(event_name); - outgoing_events.insert(event_name); + available_incoming_events.insert(event_name); + available_outgoing_events.insert(event_name); std::function handler = _handler; - incoming_event_handler_map.emplace( + active_incoming_event_handlers.emplace( _ET::_event_name, [this, handler](const nlohmann::json &_data) { @@ -430,7 +465,7 @@ namespace el::msglink void on_connection_established() { EL_LOGD("connection established called"); - + auto transaction = create_transaction( generate_new_tid(), inout_t::OUTGOING @@ -441,7 +476,7 @@ namespace el::msglink msg.tid = transaction->id; msg.proto_version = proto_version::current; msg.link_version = get_link_version(); - msg.events = outgoing_events; + msg.events = available_outgoing_events; //msg.data_sources = ...; //msg.procedures = ...; interface.send_message(msg); @@ -477,9 +512,9 @@ namespace el::msglink catch (const nlohmann::json::exception &e) { throw malformed_message_error( - "Malformed JSON link message (%s auth): %s\n%s", + "Malformed JSON link message (%s auth): %s\n%s", authentication_done ? "post" : "pre", - _msg_content.c_str(), + _msg_content.c_str(), e.what() ); } From 185e951d2702a1c8ce026972c032166def8c5f37 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 10 Dec 2023 22:36:27 +0100 Subject: [PATCH 21/50] reworked event sub/unsub/emit a little and got them working for now --- include/el/logging.hpp | 20 ++++++++ include/el/msglink/README.md | 28 +++-------- include/el/msglink/internal/messages.hpp | 21 +++----- include/el/msglink/link.hpp | 64 +++++++++++++----------- 4 files changed, 66 insertions(+), 67 deletions(-) diff --git a/include/el/logging.hpp b/include/el/logging.hpp index 27370e5..99b28c3 100644 --- a/include/el/logging.hpp +++ b/include/el/logging.hpp @@ -88,6 +88,7 @@ Simple logging framework #define EL_LOGW(fmt, ...) el::logging::logger_inst.warning(_EL_LOG_FILE, _EL_LOG_LINE, fmt, ## __VA_ARGS__) #define EL_LOGI(fmt, ...) el::logging::logger_inst.info(_EL_LOG_FILE, _EL_LOG_LINE, fmt, ## __VA_ARGS__) #define EL_LOGD(fmt, ...) el::logging::logger_inst.debug(_EL_LOG_FILE, _EL_LOG_LINE, fmt, ## __VA_ARGS__) +#define EL_LOGT(fmt, ...) el::logging::logger_inst.testing(_EL_LOG_FILE, _EL_LOG_LINE, fmt, ## __VA_ARGS__) #define EL_LOG_EXCEPTION_MSG(msg, ex) EL_LOGE(msg ": %s", el::logging::format_exception(ex).c_str()) #define EL_LOG_EXCEPTION(ex) EL_LOG_EXCEPTION_MSG("Exception occurred", ex) @@ -237,6 +238,25 @@ namespace el::logging // format the message debug(_file, _line, strutil::format(_fmt, _args...)); } + + // Testing (intended for unit testing info output) + + void testing(const char *_file, int _line, const std::string &_message) + { + // generate prefix + char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; + generate_prefix(prefix_buffer, _file, _line, "T"); + + // print in color + std::cout << _EL_LOG_ANSI_COLOR_MAGENTA << prefix_buffer << _message << _EL_LOG_ANSI_COLOR_RESET << std::endl; + } + + template + void testing(const char *_file, int _line, const std::string &_fmt, _Args... _args) + { + // format the message + testing(_file, _line, strutil::format(_fmt, _args...)); + } }; diff --git a/include/el/msglink/README.md b/include/el/msglink/README.md index 91bc307..49c2498 100644 --- a/include/el/msglink/README.md +++ b/include/el/msglink/README.md @@ -143,8 +143,6 @@ The **```type```** property defines the purpose of the message. There are the fo - auth - auth_ack - evt_sub -- evt_sub_ack -- evt_sub_nak - evt_unsub - evt_emit - data_sub @@ -249,27 +247,11 @@ If a communication party has a listener for a specific event, it needs to first - **```name```**: name of the event to be subscribed to -If the event is unknown by the other party, it will respond with a negative acknowledgement: +This assumes the event name is valid and supported by the other party, as that was already negotiated during authentication. Therefore this message is defined to **guarantee** the event will be subscribed after it is received and no response is required. -```json -{ - "type": "evt_sub_nak", - "tid": ... -} -``` - -Otherwise, a positive acknowledgement will be sent (even if event was already subscribed): - -```json -{ - "type": "evt_sub_ack", - "tid": ... -} -``` - -This is the end of this transaction. +In case this message is in fact received for an invalid event, this is due to a library implementation issue. The implementation should log this locally as a warning. -After that, the emitting party will inform the listening one when this event type is emitted using the event emit message: +After receiving this message, the emitting party will inform the listening one when this event type is emitted using the event emit message: ```json @@ -284,6 +266,8 @@ After that, the emitting party will inform the listening one when this event typ - **```name```**: name of the emitted event - **```data```**: a json object containing the data associated with the event. This data will be validated according to the schema defined on the listening party and will cause a local error if it is invalid (error will not be sent to emitting party). Listeners are only called if the data was validated successfully. +Should one party receive an event message that it hasn't subscribed to, a local warning should be logged. This doesn't cause any harm but wastes bandwidth and is likely due to a library implementation issue which is to be fixed. + Once all listeners are disabled on the listening party, it can tell the emitting party that the event information is no longer required with the event unsubscribe message: ```json @@ -296,7 +280,7 @@ Once all listeners are disabled on the listening party, it can tell the emitting - **```name```**: name of the event to unsubscribe from -There are no acknowledgement messages for unsubscribe. Unsubscribe will guarantee that no more events with the given name are received. If the unsubscribed event wasn't subscribed before or doesn't even exist, a local error is thrown on the emitting party only. +Similar to subscribe, there are no acknowledgement messages for unsubscribe. Unsubscribe will guarantee that no more events with the given name are received. If the unsubscribed event wasn't subscribed before or doesn't even exist, a local warning is logged on the receiving party only. # Notes diff --git a/include/el/msglink/internal/messages.hpp b/include/el/msglink/internal/messages.hpp index 1323a6c..d8ef1cf 100644 --- a/include/el/msglink/internal/messages.hpp +++ b/include/el/msglink/internal/messages.hpp @@ -81,27 +81,18 @@ namespace el::msglink ) }; - struct msg_evt_sub_nak_t + struct msg_evt_unsub_t : public base_msg_t , public codable { - std::string type = __EL_MSGLINK_MSG_NAME_EVT_SUB_NAK; - EL_DEFINE_CODABLE( - msg_evt_sub_nak_t, - type, - tid - ) - }; + std::string type = __EL_MSGLINK_MSG_NAME_EVT_UNSUB; + std::string name; - struct msg_evt_sub_ack_t - : public base_msg_t - , public codable - { - std::string type = __EL_MSGLINK_MSG_NAME_EVT_SUB_ACK; EL_DEFINE_CODABLE( - msg_evt_sub_ack_t, + msg_evt_sub_t, type, - tid + tid, + name ) }; diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index 530dc4c..220a985 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -89,7 +89,7 @@ namespace el::msglink // set of all possible incoming events that are defined (including bidirectional ones) std::set available_incoming_events; // map of all incoming events that we have currently subscribed to their handlers - std::unordered_map< + std::unordered_multimap< std::string, event_handler_wrapper_t > active_incoming_event_handlers; @@ -310,48 +310,52 @@ namespace el::msglink if (!available_outgoing_events.contains(msg.name)) { - // respond with nak - msg_evt_sub_nak_t response; - response.tid = msg.tid; - interface.send_message(response); - EL_LOGW("Received EVENT_SUB message for invalid event. This is likely a library implementation error and should not happen."); + EL_LOGW("Received EVENT_SUB message for invalid event. This is likely a library implementation issue and should not happen."); + break; } // otherwise activate (=subscribe to) the event active_outgoing_events.insert(msg.name); - - // respond with positive acknowledgement, transaction complete - msg_evt_sub_ack_t response; - response.tid = msg.tid; - interface.send_message(response); + + // no response required } break; - case EVENT_SUB_ACK: + case EVENT_UNSUB: { - msg_evt_sub_ack_t msg(_jmsg); + msg_evt_unsub_t msg(_jmsg); - // success, simply complete the transaction if it is valid - auto transaction = get_transaction(msg.tid); - transaction->assert_is_outgoing("Received EVT SUB ACK for foreign EVT SUB transaction"); - complete_transaction(transaction); + if (!active_outgoing_events.contains(msg.name)) + { + EL_LOGW("Received EVENT_UNSUB message for an event which was not subscribed and/or doesn't exist. This is likely a library implementation issue and should not happen."); + break; + } + + // otherwise unsubscribe from the event + active_outgoing_events.erase(msg.name); + + // no response required } - break; - case EVENT_SUB_NAK: + break; + case EVENT_EMIT: { - msg_evt_sub_nak_t msg(_jmsg); + msg_evt_emit_t msg(_jmsg); - // complete the transaction if it is valid - auto transaction = get_transaction(msg.tid); - transaction->assert_is_outgoing("Received EVT SUB NAK for foreign EVT SUB transaction"); - complete_transaction(transaction); + if (!active_incoming_event_handlers.contains(msg.name)) + { + EL_LOGW("Received EVENT_EMIT message for an event which was not subscribed to and/or doesn't exist. This is likely a library implementation issue and should not happen."); + break; + } + + // call all the listeners + auto range = active_incoming_event_handlers.equal_range(msg.name); // this doesn't throw even when there are no matches + for (auto it = range.first; it != range.second; ++it) + { + it->second(msg.data); + } - // if event sub failed, ... + // no response required } break; - case EVENT_UNSUB: - break; - case EVENT_EMIT: - break; case DATA_SUB: break; case DATA_SUB_ACK: @@ -424,7 +428,7 @@ namespace el::msglink std::function handler = _handler; active_incoming_event_handlers.emplace( - _ET::_event_name, + event_name, [this, handler](const nlohmann::json &_data) { EL_LOGD("hievent %s", _data.dump().c_str()); From 23658408ded9f43116e6092cb8ac545486532005 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 10 Dec 2023 23:29:23 +0100 Subject: [PATCH 22/50] implemented the transmission of event sub messages after auth done --- include/el/msglink/link.hpp | 72 ++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index 220a985..dba2c0e 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -84,11 +84,14 @@ namespace el::msglink // set of all possible outgoing events that are defined (including bidirectional ones) std::set available_outgoing_events; - // set of all outgoing events that the other party has subscribed and therefore need to be transmitted + // set of all outgoing events that the other party has subscribed to and therefore need to be transmitted std::set active_outgoing_events; // set of all possible incoming events that are defined (including bidirectional ones) std::set available_incoming_events; - // map of all incoming events that we have currently subscribed to their handlers + // set of all incoming events that have been subscribed to and have listeners + std::set active_incoming_events; + // (we use the above set in addition to the handler map because for some cases the set is better suited) + // map of all active incoming events to their handlers std::unordered_multimap< std::string, event_handler_wrapper_t @@ -193,8 +196,11 @@ namespace el::msglink */ void update_auth_done() noexcept { - if (auth_ack_sent && auth_ack_received) + if (auth_ack_sent && auth_ack_received && !authentication_done) + { authentication_done.set(); + on_authentication_done(); + } } /** @@ -205,6 +211,19 @@ namespace el::msglink return _el_msglink_get_link_version(); } + /** + * @brief sends an event subscribe message for a specific event + * + * @param _event_name event to send sub message for + */ + void send_event_subscribe_message(const std::string &_event_name) + { + msg_evt_sub_t msg; + msg.tid = generate_new_tid(); + msg.name = _event_name; + interface.send_message(msg); + } + /** * @brief handles incoming messages (already parsed) before authentication is complete * to perform the authentication. @@ -287,6 +306,25 @@ namespace el::msglink update_auth_done(); } + + /** + * @brief called immediately after authentication done flag is set + * (as soon as both parties are authenticated). + * This function sends some initial post-auth messages to the other + * party. + */ + void on_authentication_done() + { + // send event subscribe messages for all events subscribed before + // auth was complete (e.g. events with fixed handlers created during + // definition) + for (const auto &event_name : active_incoming_events) + { + send_event_subscribe_message(event_name); + } + + // ... do same for datasubs and RPCs + } /** * @brief handles incoming messages (already parsed) after authentication is complete @@ -340,7 +378,7 @@ namespace el::msglink { msg_evt_emit_t msg(_jmsg); - if (!active_incoming_event_handlers.contains(msg.name)) + if (!active_incoming_events.contains(msg.name) || !active_incoming_event_handlers.contains(msg.name)); { EL_LOGW("Received EVENT_EMIT message for an event which was not subscribed to and/or doesn't exist. This is likely a library implementation issue and should not happen."); break; @@ -401,11 +439,13 @@ namespace el::msglink #define EL_MSGLINK_LINK_VERSION(version_num) virtual el::msglink::link_version_t _el_msglink_get_link_version() const noexcept override { return version_num; } /** - * @brief Method for registering a link-method event handler - * for a bidirectional event. The event handler must be a method + * @brief Method for defining a bidirectional event + * and adding a static link-method event listener. + * + * The event listener must be a method * of the link it is registered on. This is a shortcut - * to avoid having to use std::bind to bind every handler - * to the instance. When an external handler is needed, this + * to avoid having to use std::bind to bind listener + * to the instance. When an external listener is needed, this * is the wrong overload. * * Method function pointer: @@ -419,14 +459,15 @@ namespace el::msglink template _ET, std::derived_from _LT> void define_event(void (_LT:: *_handler)(_ET &)) { + // save name and handler function std::string event_name = _ET::_event_name; + std::function handler = _handler; - // save to incoming and outgoing event lists + // define as incoming and outgoing available_incoming_events.insert(event_name); available_outgoing_events.insert(event_name); - std::function handler = _handler; - + // register the handler function active_incoming_event_handlers.emplace( event_name, [this, handler](const nlohmann::json &_data) @@ -440,7 +481,13 @@ namespace el::msglink ); } ); - + // add to subscribed list + active_incoming_events.insert(event_name); + // if authentication is done already (will never happen here but relevant for later) + // send the subscribe message. If auth is not done, sub messages will be sent + // as soon as authentication_done is set. + if (authentication_done) + send_event_subscribe_message(event_name); } @@ -523,6 +570,7 @@ namespace el::msglink ); } } + }; } // namespace el::msglink From 56586b8f2d1e91c4c23e7fa3aee23cc7e56270a3 Mon Sep 17 00:00:00 2001 From: melektron Date: Mon, 18 Dec 2023 00:46:06 +0100 Subject: [PATCH 23/50] implemented event subscribing and unsubscribing (at least internally) --- include/el/msglink/errors.hpp | 11 ++ include/el/msglink/internal/types.hpp | 3 +- include/el/msglink/link.hpp | 210 ++++++++++++++++++++++---- include/el/msglink/subscriptions.hpp | 98 ++++++++++++ 4 files changed, 288 insertions(+), 34 deletions(-) create mode 100644 include/el/msglink/subscriptions.hpp diff --git a/include/el/msglink/errors.hpp b/include/el/msglink/errors.hpp index 4deb278..b230f54 100644 --- a/include/el/msglink/errors.hpp +++ b/include/el/msglink/errors.hpp @@ -119,6 +119,17 @@ namespace el::msglink using msglink_error::msglink_error; }; + /** + * @brief attempted to access some sort of object like + * an event subscription for subscribing/unsubscribing + * but the identifier (name, id number, ...) is invalid + * and the object cannot be accessed. + */ + class invalid_identifier_error : public msglink_error + { + using msglink_error::msglink_error; + }; + /** * @brief link is not compatible with the link of the other party. * This may be thrown during authentication. diff --git a/include/el/msglink/internal/types.hpp b/include/el/msglink/internal/types.hpp index 9698f82..60a6b90 100644 --- a/include/el/msglink/internal/types.hpp +++ b/include/el/msglink/internal/types.hpp @@ -20,7 +20,8 @@ msglink type aliases used in other files to easily be able to change types namespace el::msglink { - using tid_t = int64_t; + using tid_t = int64_t; // transaction ID + using sub_id_t = int64_t; // subscription ID using proto_version::proto_version_t; using link_version_t = uint32_t; } // namespace el::msglink diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index dba2c0e..3bedc8e 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -28,6 +28,7 @@ the user to define the API/protocol of a link #include "../rtti_utils.hpp" #include "event.hpp" #include "errors.hpp" +#include "subscriptions.hpp" #include "internal/msgtype.hpp" #include "internal/messages.hpp" #include "internal/types.hpp" @@ -79,9 +80,6 @@ namespace el::msglink // set as soon as the login_ack has been sent and received soflag authentication_done; - // type of the lambda used to wrap event handlers - using event_handler_wrapper_t = std::function; - // set of all possible outgoing events that are defined (including bidirectional ones) std::set available_outgoing_events; // set of all outgoing events that the other party has subscribed to and therefore need to be transmitted @@ -90,12 +88,20 @@ namespace el::msglink std::set available_incoming_events; // set of all incoming events that have been subscribed to and have listeners std::set active_incoming_events; - // (we use the above set in addition to the handler map because for some cases the set is better suited) - // map of all active incoming events to their handlers + + // running counter for all sorts of subscription IDs + sub_id_t sub_id_counter = 0; + + // map of all active incoming events to their subscription IDs std::unordered_multimap< std::string, - event_handler_wrapper_t - > active_incoming_event_handlers; + sub_id_t + > event_names_to_subscription_id; + // map of subscription ID to event subscription + std::unordered_map< + sub_id_t, + std::shared_ptr + > event_subscription_ids_to_objects; private: // methods @@ -224,6 +230,19 @@ namespace el::msglink interface.send_message(msg); } + /** + * @brief sends an event unsubscribe message for a specific event + * + * @param _event_name event to send unsub message for + */ + void send_event_unsubscribe_message(const std::string &_event_name) + { + msg_evt_unsub_t msg; + msg.tid = generate_new_tid(); + msg.name = _event_name; + interface.send_message(msg); + } + /** * @brief handles incoming messages (already parsed) before authentication is complete * to perform the authentication. @@ -378,17 +397,25 @@ namespace el::msglink { msg_evt_emit_t msg(_jmsg); - if (!active_incoming_events.contains(msg.name) || !active_incoming_event_handlers.contains(msg.name)); + if (!active_incoming_events.contains(msg.name) || !event_names_to_subscription_id.contains(msg.name)) { EL_LOGW("Received EVENT_EMIT message for an event which was not subscribed to and/or doesn't exist. This is likely a library implementation issue and should not happen."); break; } // call all the listeners - auto range = active_incoming_event_handlers.equal_range(msg.name); // this doesn't throw even when there are no matches + auto range = event_names_to_subscription_id.equal_range(msg.name); // this doesn't throw even when there are no matches for (auto it = range.first; it != range.second; ++it) { - it->second(msg.data); + try + { + auto sub = event_subscription_ids_to_objects.at(it->second); + sub->call_handler(msg.data); + } + catch(const std::out_of_range& e) + { + throw invalid_identifier_error("Attempted to call event listener of invalid subscription ID. This is likely due to a library bug."); + } } // no response required @@ -420,6 +447,122 @@ namespace el::msglink } + /** + * @return sub_id_t new unique subscription ID + */ + sub_id_t generate_new_sub_id() noexcept + { + return ++sub_id_counter; + } + + /** + * @brief registers an event subscription in the internal + * map and subscribes the event from the other party + * if it isn't already. + */ + std::shared_ptr add_event_subscription( + const std::string &_event_name, + event_subscription::handler_function_t _handler_function + ) { + std::string event_name = _event_name; + // create subscription object + const sub_id_t sub_id = generate_new_sub_id(); + auto subscription = std::shared_ptr(new event_subscription( + _handler_function, + [this, _event_name, sub_id](void) // cancel function + { + EL_LOGD("cancel event %s:%d", _event_name.c_str(), sub_id); + // create copy of name and ID because lambda and it's captures may be destroyed + // during the below function call + std::string l_event_name = _event_name; + sub_id_t l_sub_id = sub_id; + this->remove_event_subscription(l_event_name, l_sub_id); + } + )); + + // register the subscription + event_subscription_ids_to_objects.emplace( + sub_id, + subscription + ); + event_names_to_subscription_id.emplace( + _event_name, + sub_id + ); + + // activate the event if it is not already active + if (active_incoming_events.contains(_event_name)) + goto exit; // another listener already exits, the event is already active + + // add to list of active events + active_incoming_events.insert(_event_name); + // if authentication is done already send the subscribe message now. + // If auth is not done, sub messages will be sent as soon + // as authentication_done is set. + if (authentication_done) + send_event_subscribe_message(_event_name); + + exit: + return subscription; + } + + /** + * @brief removes an event subscription and deactivates + * the event by sending unsubscribe message if required + * + * @param _event_name + * @param _subscription_id + */ + void remove_event_subscription( + const std::string &_event_name, + sub_id_t _subscription_id + ) { + // count amount of subscriptions left + size_t sub_count = 0; + + // remove the subscription if it is found + auto range = event_names_to_subscription_id.equal_range(_event_name); // this doesn't throw even when there are no matches + for (auto it = range.first; it != range.second; ++it) + { + sub_count++; + + auto sub_id = it->second; + if (sub_id == _subscription_id) + { + // erase the subscription if found + event_names_to_subscription_id.erase(it); + sub_count--; + break; + } + } + + // if there are no subscriptions left, deactivate the event + if (sub_count == 0) + { + active_incoming_events.erase(_event_name); + if (authentication_done) + send_event_unsubscribe_message(_event_name); + } + + // remove from id to sub object set + // Attention: This might cause the object to be destroyed, which + // will cause any parameters passed to this function by reference + // from a lambda context to become dangling pointers. After this, don't + // use parameters anymore if possible even though our cancel() lambda is designed + // in a way to avoid this issue by copying values to stack. + try + { + // make sure the subscription is invalidated + event_subscription_ids_to_objects.at(_subscription_id)->invalidate(); + // possibly delete the element + event_subscription_ids_to_objects.erase(_subscription_id); + } + catch(const std::out_of_range& e) + { + throw invalid_identifier_error("Attempted to remove event subscription with invalid subscription ID %d. This is likely a library bug.", _subscription_id); + } + } + protected: /** @@ -454,40 +597,34 @@ namespace el::msglink * @tparam _ET the event class of the event to register * (must inherit from el::msglink::event, can be deduced from method parameter) * @tparam _LT the link class the handler is a method of (can also be deduced) - * @param _handler the handler method for the event + * @param _listener the handler method for the event */ template _ET, std::derived_from _LT> - void define_event(void (_LT:: *_handler)(_ET &)) - { + std::shared_ptr define_event( + void (_LT:: *_listener)(_ET &) + ) { // save name and handler function std::string event_name = _ET::_event_name; - std::function handler = _handler; + std::function listener = _listener; // define as incoming and outgoing available_incoming_events.insert(event_name); available_outgoing_events.insert(event_name); - // register the handler function - active_incoming_event_handlers.emplace( + // create subscription with handler function + return add_event_subscription( event_name, - [this, handler](const nlohmann::json &_data) - { - EL_LOGD("hievent %s", _data.dump().c_str()); - _ET new_event_inst; - new_event_inst = _data; - handler( - static_cast<_LT *>(this), - new_event_inst - ); - } + [this, listener](const nlohmann::json &_data) + { + EL_LOGD("hievent %s", _data.dump().c_str()); + _ET new_event_inst; + new_event_inst = _data; + listener( + static_cast<_LT *>(this), + new_event_inst + ); + } ); - // add to subscribed list - active_incoming_events.insert(event_name); - // if authentication is done already (will never happen here but relevant for later) - // send the subscribe message. If auth is not done, sub messages will be sent - // as soon as authentication_done is set. - if (authentication_done) - send_event_subscribe_message(event_name); } @@ -505,6 +642,13 @@ namespace el::msglink , interface(_interface) {} + ~link() + { + // invalidate all event subscriptions to make sure there are no dangling pointers + for (auto &[id, sub] : event_subscription_ids_to_objects) + sub->invalidate(); + } + /** * @brief valid link definitions must implement this define method * to define the protocol by calling the specialized define diff --git a/include/el/msglink/subscriptions.hpp b/include/el/msglink/subscriptions.hpp new file mode 100644 index 0000000..d281166 --- /dev/null +++ b/include/el/msglink/subscriptions.hpp @@ -0,0 +1,98 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +17.12.23, 21:10 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Structures representing various types of subscriptions. +*/ + +#pragma once + +#include +#include +#include + +#include "../flags.hpp" +#include "../logging.hpp" +#include "internal/types.hpp" + + +namespace el::msglink +{ + /** + * @brief structure representing an event subscription which + * is used to identify and cancel the subscription later. + * + */ + struct event_subscription + { + protected: + friend class link; + + // invalidates all potential references and callbacks to the link to + // prevent any calls back to a potentially deallocated link instance. + // This is called by the link destructor. + void invalidate() + { + cancel_function = nullptr; + handler_function = nullptr; + } + + // type of the lambda used to wrap event handlers + using handler_function_t = std::function; + // type of the lambda used for the cancel callback + using cancel_function_t = std::function; + + // function called when the event is received + handler_function_t handler_function; + // function called to cancel the event (will be a + // lambda created by the link) + cancel_function_t cancel_function; + + void call_handler(const nlohmann::json &_data) + { + if (handler_function != nullptr) + handler_function(_data); + } + + event_subscription( + handler_function_t _handler_function, + cancel_function_t _cancel_function + ) + : handler_function(_handler_function) + , cancel_function(_cancel_function) + {} + + public: + event_subscription(const event_subscription &) = default; + event_subscription(event_subscription &&) = default; + + /** + * @brief function to cancel the subscription and therefore + * unsubscribe from the event. If already canceled, this does + * nothing. + */ + void cancel() + { + if (cancel_function != nullptr) + { + cancel_function(); + cancel_function = nullptr; // only cancel once + } + } + + /** + * @brief Invalidates the object + */ + ~event_subscription() + { + EL_LOG_FUNCTION_CALL(); + invalidate(); + } + }; +} // namespace el::msglink From 7ba98b6006b17adc38cfbdde936fc933a77be7fe Mon Sep 17 00:00:00 2001 From: melektron Date: Sat, 23 Dec 2023 22:58:34 +0100 Subject: [PATCH 24/50] implemented all event define methods (also for incoming/outgoing only events) --- include/el/msglink/event.hpp | 45 ++++++++- include/el/msglink/link.hpp | 183 ++++++++++++++++++++++++++++++++++- 2 files changed, 219 insertions(+), 9 deletions(-) diff --git a/include/el/msglink/event.hpp b/include/el/msglink/event.hpp index 7854946..43f19d4 100644 --- a/include/el/msglink/event.hpp +++ b/include/el/msglink/event.hpp @@ -13,9 +13,13 @@ msglink event class used to define custom events #pragma once +#include + #include "../codable.hpp" +#include "../cxxversions.h" +#ifdef __EL_ENABLE_CXX20 namespace el::msglink { @@ -46,6 +50,7 @@ namespace el::msglink virtual void _el_msglink_is_incoming_dummy() const noexcept override {} \ EL_DEFINE_DECODABLE(TypeName, __VA_ARGS__) + /** * @brief base class for all outgoing msglink event * definition classes. To create an outgoing event define a class inheriting from this one. @@ -72,11 +77,12 @@ namespace el::msglink virtual void _el_msglink_is_outgoing_dummy() const noexcept override {} \ EL_DEFINE_ENCODABLE(TypeName, __VA_ARGS__) + /** - * @brief base class for all bidirectional (incoming and outgoing) msglink event - * definition classes. It is simply a composite class of outgoing_event - * and incoming_event. This class must satisfy all the requirements of incoming and outgoing - * events. + * @brief shortcut base class for all bidirectional (incoming and outgoing) msglink event + * definition classes. It is simply a composite class inheriting form outgoing_event + * and incoming_event to save you the hassle of having to do that manually. + * Derived classes must satisfy all the requirements of incoming and outgoing events. * To create a bidirectional event define a class inheriting from this one. * Then use the EL_MSGLINK_DEFINE_EVENT macro to generate the required boilerplate. */ @@ -94,4 +100,35 @@ namespace el::msglink virtual void _el_msglink_is_outgoing_dummy() const noexcept override {} \ EL_DEFINE_CODABLE(TypeName, __VA_ARGS__) + + /** + * The following concepts define constraints making sure + * an event class is either ONLY an incoming event or ONLY + * an outgoing event or a bidirectional event (BOTH incoming + * and outgoing) + */ + + /** + * @brief Constrains _ET to be ONLY derived from incoming_event + * and NOT from outgoing_event + */ + template + concept IncomingOnlyEvent = std::derived_from<_ET, incoming_event> && !std::derived_from<_ET, outgoing_event>; + + /** + * @brief Constrains _ET to be ONLY derived from outgoing_event + * and NOT from incoming_event + */ + template + concept OutgoingOnlyEvent = std::derived_from<_ET, outgoing_event> && !std::derived_from<_ET, incoming_event>; + + /** + * @brief Constrains _ET to be derived BOTH from incoming_event + * and from outgoing_event, making it a bidirectional event + */ + template + concept BidirectionalEvent = std::derived_from<_ET, incoming_event> && std::derived_from<_ET, outgoing_event>; + + } // namespace el::msglink +#endif // __EL_ENABLE_CXX20 \ No newline at end of file diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index 3bedc8e..d2b48ca 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -582,16 +582,16 @@ namespace el::msglink #define EL_MSGLINK_LINK_VERSION(version_num) virtual el::msglink::link_version_t _el_msglink_get_link_version() const noexcept override { return version_num; } /** - * @brief Method for defining a bidirectional event - * and adding a static link-method event listener. + * @brief Shortcut for defining a bidirectional event + * and adding an event listener that is a method of the link. * * The event listener must be a method * of the link it is registered on. This is a shortcut * to avoid having to use std::bind to bind listener - * to the instance. When an external listener is needed, this + * to the instance. When an external listener function is needed, this * is the wrong overload. * - * Method function pointer: + * @note Method function pointer: * https://isocpp.org/wiki/faq/pointers-to-members#typedef-for-ptr-to-memfn * * @tparam _ET the event class of the event to register @@ -599,7 +599,7 @@ namespace el::msglink * @tparam _LT the link class the handler is a method of (can also be deduced) * @param _listener the handler method for the event */ - template _ET, std::derived_from _LT> + template _LT> std::shared_ptr define_event( void (_LT:: *_listener)(_ET &) ) { @@ -627,6 +627,179 @@ namespace el::msglink ); } + /** + * @brief Shortcut for defining a bidirectional event + * and adding an event listener that is an arbitrary function. + * + * The event listener can be an arbitrary function matching the call signature + * ``` + * void(_ET &_evt) + * ```. + * If the listener is a method of the link instance, + * there is a special overload to simplify that case. This is not that overload. + * + * @tparam _ET the event class of the event to register + * (must inherit from el::msglink::event, can be deduced from method parameter) + * @param _listener the handler function for the event + */ + template + std::shared_ptr define_event( + void (*_listener)(_ET &) + ) { + // save name and handler function + std::string event_name = _ET::_event_name; + std::function listener = _listener; + + // define as incoming and outgoing + available_incoming_events.insert(event_name); + available_outgoing_events.insert(event_name); + + // create subscription with handler function + return add_event_subscription( + event_name, + [this, listener](const nlohmann::json &_data) + { + EL_LOGD("hievent %s", _data.dump().c_str()); + _ET new_event_inst; + new_event_inst = _data; + listener( + new_event_inst + ); + } + ); + } + + /** + * @brief Defines a bidirectional event. This method does not add + * any listeners. + * + * @tparam _ET the event class of the event to register (must inherit from el::msglink::event) + */ + template + void define_event() { + // save name + std::string event_name = _ET::_event_name; + + // define as incoming and outgoing + available_incoming_events.insert(event_name); + available_outgoing_events.insert(event_name); + } + + /** + * @brief Defines an incoming only event. This method does not add + * any listeners. + * + * @tparam _ET the event class of the event to register (must inherit from el::msglink::incoming_event) + */ + template + void define_event() { + // save name + std::string event_name = _ET::_event_name; + + // define as incoming + available_incoming_events.insert(event_name); + } + + /** + * @brief Shortcut for defining an incoming only event + * and adding an event listener that is a method of the link. + * + * The event listener must be a method + * of the link it is registered on. This is a shortcut + * to avoid having to use std::bind to bind listener + * to the instance. When an external listener function is needed, this + * is the wrong overload. + * + * @note Method function pointer: + * https://isocpp.org/wiki/faq/pointers-to-members#typedef-for-ptr-to-memfn + * + * @tparam _ET the event class of the event to register + * (must inherit from el::msglink::incoming_event, can be deduced from method parameter) + * @tparam _LT the link class the handler is a method of (can also be deduced) + * @param _listener the handler method for the event + */ + template _LT> + std::shared_ptr define_event( + void (_LT:: *_listener)(_ET &) + ) { + // save name and handler function + std::string event_name = _ET::_event_name; + std::function listener = _listener; + + // define as incoming + available_incoming_events.insert(event_name); + + // create subscription with handler function + return add_event_subscription( + event_name, + [this, listener](const nlohmann::json &_data) + { + EL_LOGD("hievent %s", _data.dump().c_str()); + _ET new_event_inst; + new_event_inst = _data; + listener( + static_cast<_LT *>(this), + new_event_inst + ); + } + ); + } + + /** + * @brief Shortcut for defining an incoming only event + * and adding an event listener that is an arbitrary function. + * + * The event listener can be an arbitrary function matching the call signature + * ``` + * void(_ET &_evt) + * ```. + * If the listener is a method of the link instance, + * there is a special overload to simplify that case. This is not that overload. + * + * @tparam _ET the event class of the event to register + * (must inherit from el::msglink::incoming_event, can be deduced from method parameter) + * @param _listener the handler function for the event + */ + template + std::shared_ptr define_event( + void (*_listener)(_ET &) + ) { + // save name and handler function + std::string event_name = _ET::_event_name; + std::function listener = _listener; + + // define as incoming + available_incoming_events.insert(event_name); + + // create subscription with handler function + return add_event_subscription( + event_name, + [this, listener](const nlohmann::json &_data) + { + EL_LOGD("hievent %s", _data.dump().c_str()); + _ET new_event_inst; + new_event_inst = _data; + listener( + new_event_inst + ); + } + ); + } + + /** + * @brief Defines an outgoing only event. + * + * @tparam _ET the event class of the event to register (must inherit from el::msglink::outgoing_event) + */ + template + void define_event() { + // save name + std::string event_name = _ET::_event_name; + + // define as outgoing + available_outgoing_events.insert(event_name); + } + public: From 6b5eeeda4a25776769496d13aa93c5c34f72fbf6 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 24 Dec 2023 00:32:06 +0100 Subject: [PATCH 25/50] fixed issue causing events being unsubscribed prematurely --- include/el/msglink/event.hpp | 8 ++++---- include/el/msglink/link.hpp | 31 ++++++++++++++++++++++--------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/include/el/msglink/event.hpp b/include/el/msglink/event.hpp index 43f19d4..35537bc 100644 --- a/include/el/msglink/event.hpp +++ b/include/el/msglink/event.hpp @@ -84,17 +84,17 @@ namespace el::msglink * and incoming_event to save you the hassle of having to do that manually. * Derived classes must satisfy all the requirements of incoming and outgoing events. * To create a bidirectional event define a class inheriting from this one. - * Then use the EL_MSGLINK_DEFINE_EVENT macro to generate the required boilerplate. + * Then use the EL_MSGLINK_DEFINE_BIDIRECTIONAL_EVENT macro to generate the required boilerplate. */ - struct event : public incoming_event, public outgoing_event + struct bidirectional_event : public incoming_event, public outgoing_event { - virtual ~event() = default; + virtual ~bidirectional_event() = default; }; // (public) generates the necessary boilerplate code for an event class. // The members listed in the arguments will be made codable using el::codable // and are part of the event's data. -#define EL_MSGLINK_DEFINE_EVENT(TypeName, ...) \ +#define EL_MSGLINK_DEFINE_BIDIRECTIONAL_EVENT(TypeName, ...) \ static inline const char *_event_name = #TypeName; \ virtual void _el_msglink_is_incoming_dummy() const noexcept override {} \ virtual void _el_msglink_is_outgoing_dummy() const noexcept override {} \ diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index d2b48ca..6335c64 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -520,21 +520,30 @@ namespace el::msglink // count amount of subscriptions left size_t sub_count = 0; - // remove the subscription if it is found + // iterator to store position to delete + auto target_it = event_names_to_subscription_id.end(); + + // go through all subscriptions, counting them and identifying the ony that is to be deleted auto range = event_names_to_subscription_id.equal_range(_event_name); // this doesn't throw even when there are no matches for (auto it = range.first; it != range.second; ++it) { sub_count++; auto sub_id = it->second; - if (sub_id == _subscription_id) + if (it->second == _subscription_id) { - // erase the subscription if found - event_names_to_subscription_id.erase(it); - sub_count--; - break; + // save position + target_it = it; + // subscription cannot be erased directly here because iterators would be invalidated } } + + // if a subscription was found, delete it now and remove it from count + if (target_it != event_names_to_subscription_id.end()) + { + event_names_to_subscription_id.erase(target_it); + sub_count--; + } // if there are no subscriptions left, deactivate the event if (sub_count == 0) @@ -595,7 +604,8 @@ namespace el::msglink * https://isocpp.org/wiki/faq/pointers-to-members#typedef-for-ptr-to-memfn * * @tparam _ET the event class of the event to register - * (must inherit from el::msglink::event, can be deduced from method parameter) + * (must inherit from el::msglink::incoming_event and el::msglink::outgoing_event + * (aka. el::msglink::bidirectional_event), can be deduced from method parameter) * @tparam _LT the link class the handler is a method of (can also be deduced) * @param _listener the handler method for the event */ @@ -639,7 +649,8 @@ namespace el::msglink * there is a special overload to simplify that case. This is not that overload. * * @tparam _ET the event class of the event to register - * (must inherit from el::msglink::event, can be deduced from method parameter) + * (must inherit from el::msglink::incoming_event and el::msglink::outgoing_event + * (aka. el::msglink::bidirectional_event), can be deduced from method parameter) * @param _listener the handler function for the event */ template @@ -673,7 +684,9 @@ namespace el::msglink * @brief Defines a bidirectional event. This method does not add * any listeners. * - * @tparam _ET the event class of the event to register (must inherit from el::msglink::event) + * @tparam _ET the event class of the event to register + * (must inherit from el::msglink::incoming_event and el::msglink::outgoing_event + * (aka. el::msglink::bidirectional_event)) */ template void define_event() { From 7087e7b617feba5606be332f488e5203b53cd036 Mon Sep 17 00:00:00 2001 From: melektron Date: Tue, 26 Dec 2023 22:47:20 +0100 Subject: [PATCH 26/50] implemented event emitting --- include/el/msglink/event.hpp | 20 +++++++-- include/el/msglink/link.hpp | 80 ++++++++++++++++++++++++++---------- 2 files changed, 76 insertions(+), 24 deletions(-) diff --git a/include/el/msglink/event.hpp b/include/el/msglink/event.hpp index 35537bc..66f2f78 100644 --- a/include/el/msglink/event.hpp +++ b/include/el/msglink/event.hpp @@ -102,9 +102,9 @@ namespace el::msglink /** - * The following concepts define constraints making sure - * an event class is either ONLY an incoming event or ONLY - * an outgoing event or a bidirectional event (BOTH incoming + * The following concepts define constraints that allow targeting specific + * kinds of events such as an event class that is either ONLY an incoming + * event or ONLY an outgoing event or a bidirectional event (BOTH incoming * and outgoing) */ @@ -115,6 +115,13 @@ namespace el::msglink template concept IncomingOnlyEvent = std::derived_from<_ET, incoming_event> && !std::derived_from<_ET, outgoing_event>; + /** + * @brief Constrains _ET to be at derived at least from incoming_event + * (but can additionally also derive from outgoing_event) + */ + template + concept AtLeastIncomingEvent = std::derived_from<_ET, incoming_event>; + /** * @brief Constrains _ET to be ONLY derived from outgoing_event * and NOT from incoming_event @@ -122,6 +129,13 @@ namespace el::msglink template concept OutgoingOnlyEvent = std::derived_from<_ET, outgoing_event> && !std::derived_from<_ET, incoming_event>; + /** + * @brief Constrains _ET to be at derived at least from outgoing_event + * (but can additionally also derive from incoming_event) + */ + template + concept AtLeastOutgoingEvent = std::derived_from<_ET, outgoing_event>; + /** * @brief Constrains _ET to be derived BOTH from incoming_event * and from outgoing_event, making it a bidirectional event diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index 6335c64..49daac4 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -243,6 +243,17 @@ namespace el::msglink interface.send_message(msg); } + void send_event_emit_message( + const std::string &_event_name, + const outgoing_event &_evt + ) { + msg_evt_emit_t msg; + msg.tid = generate_new_tid(); + msg.name = _event_name; + msg.data = _evt; + interface.send_message(msg); + } + /** * @brief handles incoming messages (already parsed) before authentication is complete * to perform the authentication. @@ -399,7 +410,7 @@ namespace el::msglink if (!active_incoming_events.contains(msg.name) || !event_names_to_subscription_id.contains(msg.name)) { - EL_LOGW("Received EVENT_EMIT message for an event which was not subscribed to and/or doesn't exist. This is likely a library implementation issue and should not happen."); + EL_LOGW("Received EVENT_EMIT message for an event which was not subscribed to, isn't incoming and/or doesn't exist. This is likely a library implementation issue and should not happen."); break; } @@ -590,6 +601,30 @@ namespace el::msglink */ #define EL_MSGLINK_LINK_VERSION(version_num) virtual el::msglink::link_version_t _el_msglink_get_link_version() const noexcept override { return version_num; } + /** + * The following methods are used to define events, data subscriptions + * and RPCs and provide optional shortcut functionality + */ + + /** + * @brief Defines a bidirectional event. This method does not add + * any listeners. + * + * @tparam _ET the event class of the event to register + * (must inherit from el::msglink::incoming_event and el::msglink::outgoing_event + * (aka. el::msglink::bidirectional_event)) + */ + template + void define_event() + { + // save name + std::string event_name = _ET::_event_name; + + // define as incoming and outgoing + available_incoming_events.insert(event_name); + available_outgoing_events.insert(event_name); + } + /** * @brief Shortcut for defining a bidirectional event * and adding an event listener that is a method of the link. @@ -680,24 +715,6 @@ namespace el::msglink ); } - /** - * @brief Defines a bidirectional event. This method does not add - * any listeners. - * - * @tparam _ET the event class of the event to register - * (must inherit from el::msglink::incoming_event and el::msglink::outgoing_event - * (aka. el::msglink::bidirectional_event)) - */ - template - void define_event() { - // save name - std::string event_name = _ET::_event_name; - - // define as incoming and outgoing - available_incoming_events.insert(event_name); - available_outgoing_events.insert(event_name); - } - /** * @brief Defines an incoming only event. This method does not add * any listeners. @@ -705,7 +722,8 @@ namespace el::msglink * @tparam _ET the event class of the event to register (must inherit from el::msglink::incoming_event) */ template - void define_event() { + void define_event() + { // save name std::string event_name = _ET::_event_name; @@ -805,7 +823,8 @@ namespace el::msglink * @tparam _ET the event class of the event to register (must inherit from el::msglink::outgoing_event) */ template - void define_event() { + void define_event() + { // save name std::string event_name = _ET::_event_name; @@ -813,6 +832,25 @@ namespace el::msglink available_outgoing_events.insert(event_name); } + public: + /** + * The following functions are used to access events, data subscriptions + * or RPCs such as by registering listeners, emitting events or updating data. + */ + + template + void emit(const _ET &_event) + { + // make sure that this event is defined + if (!available_outgoing_events.contains(_ET::_event_name)) + throw invalid_outgoing_event_error("Event '%s' cannot be emitted because it is not defined as outgoing", _ET::_event_name); + + // check if the event is needed + if (!active_outgoing_events.contains(_ET::_event_name)) + return; + + send_event_emit_message(_ET::_event_name, _event); + } public: From 36e5805e1489c4fd0e63c4b587747c55387c332b Mon Sep 17 00:00:00 2001 From: melektron Date: Wed, 27 Dec 2023 02:32:27 +0100 Subject: [PATCH 27/50] added explaination/roadmap/plan/reasoning for the msglink networking architecture --- .../{README.md => msglink_protocol.md} | 9 +- include/el/msglink/networking_architecture.md | 144 ++++++++++++++++++ 2 files changed, 150 insertions(+), 3 deletions(-) rename include/el/msglink/{README.md => msglink_protocol.md} (95%) create mode 100644 include/el/msglink/networking_architecture.md diff --git a/include/el/msglink/README.md b/include/el/msglink/msglink_protocol.md similarity index 95% rename from include/el/msglink/README.md rename to include/el/msglink/msglink_protocol.md index 49c2498..c4f151c 100644 --- a/include/el/msglink/README.md +++ b/include/el/msglink/msglink_protocol.md @@ -95,7 +95,10 @@ Which of the three options provided by msglink (events, data subscriptions, RPCs > However often times the executing party needs send some result data or outcome of the action back to the emitter. In the past, it was necessary to define a separate request and response event and write code for every type of interaction to sync the two up, wait for the response and so on. This is very tedious and repetitive. > > With msglink, for such a case a procedure can be defined instead of an event. A procedure is basically two events combined, with the only difference being that the listener now returns another object which is sent back to the emitter. This can be integrated nicely with the async programming capability of many programming languages. - + > + > Another difference between procedures and events is the way that they are handled on the receiving side. Since events have no way of returning data or results to the emitter, there may be many listeners that are notified of the event and can perform actions when that happens. Although each of them may cause various actions, like emitting more events as a response, none of them are responsible for or capable of defining one singular "outcome" or "result" of the event. This is a broadcast theme. + > + > With procedures, this is different. Since a procedure has to have one single definite result to be sent back to the caller (roughly equivalent to emitter for events) after it has been handled, there can only be one handler. A procedure may be called from many different places, but on the receiving side, there has to be exactly one handler (function). Since it is necessary for a complete transaction to always return a result, it is also not allowed for there to be no handler at all. So procedures always have exactly one handler. # Protocol details @@ -205,7 +208,7 @@ When a msglink client first connects to the msglink server both parties send an After receiving the message from the other party, both parties will check that the protocol versions of the other party are compatible and that the user defined link versions match. If that is not the case, the connection will be closed with code 3001 or 3002. -> Protocol version compatibility is determined by the party with the higher (= newer) version as that one is assumed to know of and be able to judge compatibility with the lower version. If a party receives an auth message with a higher protocol version than it's own, it skips the protocol compatibility check. +> Protocol version compatibility is determined by the party with the higher (= newer) version as that one is assumed to know of and be able to judge compatibility with the lower version. If a party receives an auth message with a higher protocol version than it's own, it skips the version compatibility check. The message also contains lists of all the functionality the party can provide to the other one. These lists are used by the receiving party to determine weather they fulfill all it's requirements. If any requirement fails, the connection is immediately closed with the corresponding code described below. This helps to detect simple coding mistakes early and reduce the amount of errors that will occur later during communication. @@ -218,7 +221,7 @@ The message also contains lists of all the functionality the party can provide t Obviously these requirements are only checked approximately. The client doesn't know at that point whether the server ever will emit the "error_occurred" event or even if there will ever be a listener for it. The only thing it knows is that both the server and itself know that this event exists and know how to deal with it should that become necessary later. -If no problems were found, each party sends a authentication acknowledgement message as a response to the other with the respective transaction ID (not a new one) to complete the authentication transaction: +If no problems were found, each party sends an authentication acknowledgement message as a response to the other with the respective transaction ID (not a new one) to complete the authentication transaction: ```json { diff --git a/include/el/msglink/networking_architecture.md b/include/el/msglink/networking_architecture.md new file mode 100644 index 0000000..fa179bd --- /dev/null +++ b/include/el/msglink/networking_architecture.md @@ -0,0 +1,144 @@ +# msglink networking architecture + +in addition to defining a protocol and convenient abstraction libraries for using it, msglink libraries also define a number of different networking topologies and convenient support for them in library implementations. + +This page describes the common internal architecture "stack" of msglink implementation libraries as well as different ways multiple communication parties can interact with each other over the network. + + +## Internal architecture + +An application using the msglink protocol is divided into three blocks: + +- **Network Interface**: This block is responsible for opening, maintaining and closing a network connection to the other communication party as well as sending and receiving protocol data units (here messages). It also manages and updates the other blocks higher up in the stack depending on the network connection state. +- **Link Interface**: This is a small block which is used by the link to communicate to the network interface for sending data and controlling the connection based on the protocol. +- **Link**: This block is the responsible for managing the protocol. It receives/sends and decodes/encodes messages from/to the network interface and performs actions according to the msglink protocol. This part is responsible for performing authentication with the other party as soon as a connection is established, checking protocol compatibility, managing event- and data-subscriptions, keeping track of RPC transactions and more. + +## Link configuration + +The user of the msglink library must customize the **link** by defining their application specific events, data subscriptions and remote procedures. They can then use the link (typically an instance of a link class) to emit events or register event handlers, call remote procedures and request/provide data to/from the remote party. + +In the C++ library, this is accomplished by first defining a class/struct for each event, data subscription and remote procedure: + +```cpp +// can be incoming, outgoing or both (bidirectional) +struct didi_event : public el::msglink::bidirectional_event +{ + int height; + std::string shirt_color; + + EL_MSGLINK_DEFINE_BIDIRECTIONAL_EVENT( + didi_event, + height, + shirt_color + ) +}; + +// ... more events and other stuff +``` + +Then, a custom link class has to be created which defines all of the events, data subscriptions and procedures that are part of the application specific "protocol". There, you might also configure some static listener functions called when an e.g. an event is received: + +```cpp + +class my_link : public el::msglink::link +{ + using link::link; // default constructor + + // user-defined link version number that must match on both parties + EL_MSGLINK_LINK_VERSION(1); + + // method to handle didi even + void handle_didi(didi_event &_evt) + { + EL_LOGI( + "Got inout_didi_event with height=%d and shirt_color=%s", + _evt.height, + _evt.shirt_color.c_str() + ); + } + +public: + // object to manage event subscription + std::shared_ptr didi_subscription; + + // override the define method to add protocol definitions + virtual void define() noexcept override + { + // define event and subscribe a listener function to it + // (event type can be inferred from listener function parameter) + didi_subscription = define_event(&my_link::handle_didi); + // without listener, event needs to be named manually: + //define_event(); + + // ... define more events and other stuff + } +}; +``` + +Once the link has been defined, it can the be used when connecting with another communication party. The actual network connection part can vary greatly depending on the use case and is explained in more detail in [Networking topologies](#networking-topologies). + +For the communication to work, both parties need to have a compatible link. The link is compatible if all of the below are true: + +- msglink versions are compatible +- user defined link version (```EL_MSGLINK_LINK_VERSION(...)```) matches +- event, data sub. and procedure requirements match, meaning one party can provide all e.g. events the other needs (see [Authentication Procedure](msglink_protocol.md#authentication-procedure) for details) + + +## Networking topologies + +In the context of this document, the them "networking topology" refers to the relation between client and server parties in a msglink communication session. + +At it's core, msglink is a point-to-point communication protocol. msglink never uses broadcasts or multicasts on a network level. There are always two parties A and B communication with each other using the msglink protocol. + +In the msglink protocol, there is no logical difference between a client and a server. Both communication parties are equal and have equal capabilities (in the bounds defined by the user application). + +However, at some point, a network connection (Websocket, which builds ontop of TCP socket) needs to be established as a communication channel. For that to work, one of the two parties needs to act as a server, listening for a new connection by the other party. It is the job of the **Network Interface** architecture block to manage this connection phase. This network interface block is typically represented by a class. There are multiple network interface blocks that the user of the library can select from to assign a communication party the appropriate role in the connection establishing phase. + +### Networking role selection + +Depending on the application, the user may arbitrarily define either party to be the server or the client by using the corresponding network interface class ```el::msglink::server``` or ```el::msglink::client```. + +In many cases, like a WebApp, the role selection is obvious. In this example, it only makes sense for a program running on the webserver with a public facing IP address to be the msglink server and the browser to be the client.
+In most applications, one party is clearly the "boss" of operations and it therefor makes sense for that one to be the server. + +In a scenario two equivalent devices such as two robots on the same network communicating directly with each other, it might not matter which one is the server. When selecting the before mentioned interface classes, there is only a minimal difference in the library API. + +### Connection loss and reconnects + +In msglink, the protocol an communication state is managed by the **Link** class, while the network connection state is managed by the **Network Interface** class such as ```el::msglink::server``` or ```el::msglink::client```. When a connection is first established, the network interface instance notifies the link instance and it can perform authentication. Once that is done, the two parties might exchange event subscriptions and start communicating. + +But what happens when the connection is lost? msglink intentionally doesn't define any convoluted method for attempting to restore a lost connection and resume communication where it left off. When the connection is closed, it's closed for good and must be restarted from scratch. + +Since the communication is not resumed where it left off, we might as well delete the link instance entirely. As soon as the client detects the connection has closed, it can attempt to reconnect to the server the same way as before. A new link instance would be created and the authentication procedure repeated the same way. But what happens to the event subscriptions and event listeners present before the connection loss? They would just be forgotten entirely and the user application would need to make sure what was required and what listener functions were registered in order to re-subscribe to all the events. This would be very tedious for the user to implement. + +For this reason, in a simple application were there is only one client and server which need to communicate, the **Link** block is completely decoupled from the **Network Interface** block. For the entire lifetime of an application, there will be a single instance of the user-defined link class that is never deleted. The network interface block will simply receive a reference to this instance to control it. Internally in the link class, the user protocol state (subscriptions, ...) is also decoupled from the network and msglink protocol state (authentication, transactions, ...). + +Initially, the link instance is in a disconnected state, were the network interface block has not jet created a connection to the other party. During this time, other parts of the application can already access the link instance and register event listeners or emit events. They will not notice the connection isn't established jet and the link will keep track internally of which events are subscribed and may queue up emitted events (depending on configuration) +As soon as a connection is established and the authentication process is complete, the link will send all the event subscriptions for the currently required events according to it's internal records as well as possibly pushing queued event emissions. + +Any event subscriptions and other state changes made while the connection is established are immediately sent to the other party as well as recorded internally, so the link instance knows at all times what event subscriptions, data subscriptions, etc. it currently needs. + +When the connection is lost, both parties' network connection blocks will reset the link instance' msglink protocol and networking state to a time before authentication. This does not affect user protocol state such as event subscriptions. Form then on, the link will behave like it did before the connection was established. Any access by the rest of the application will be recorded so the current state and missed events can be sent to the other party as soon it reconnects, ensuring the state of the communication subsystem will always be clean and the communication channel behaves as the user application expects. + + +### many-to-one relationship + +In many cases, such as a server for a WebApp, it might be required for a server application to handle connection to multiple independent clients at the same time, dynamically. This is called a many-to-ony relationship. To support this functionality, you can use a special network interface class instead of teh basic server: ```el::msglink::multi_connection_server```. This class dynamically creates link instances for each client that connects to the server as well as calling a "client connected" handler to perform some initialization for the new client. + +This poses a problem when it comes to client state management however. As explained before, the user protocol state is kept within the link instance. In the simple server application only only exactly one connection, there was one global link instance. A server handling many connections needs to initialize links dynamically as soon as a client connects. + +So when a connection is lost, what happens to the link instance? Surely it cannot be kept around forever. Since there are many clients, it is tricky to tell wether a client is trying to reconnect or it's simply a new client connecting, which makes it hard to match a new connection to an existing link. That would require some sort of session identifier, which is currently not supported by msglink (more on that later). + +Luckily we can work around this issue in many cases by changing the design philosophy of the application or just viewing it differently. +When there is a server responsible for *serving* a large number of clients, it is unlikely for the server to *desperately want to get some specific piece of information globally* from *a* specific client. Instead, in most cases the clients probably want's to get notified about events from the server, so the server might not even have any event subscriptions for the client. If the client want's the server to do something, an RPC is probably better suited. And for the cases were client events are listened to by the server, every client probably has it's own event listeners associated to it, which are registered as soon as the link is instantiated when the client connects (probably methods of the link itself). In most cases the server isn't going to say *I now temporarily need to be notified about this event from the client* during runtime. And once a client disconnects, the server probably doesn't care about a specific event from that client, as it is the client which *wants* to send the information to the server. + +### Link lifetime + +For all these reasons, the ```el::msglink::multi_connection_server``` doesn't reconnect clients to existing links. The lifetime of a link is from when the connection is first established to when the connection is closed for whatever reason. + +The simple ```el::msglink::server``` class and also the ```el::msglink::client``` class keep the link around for the lifetime of the application, meaning the link is created when the application launches and deleted when it exits. Since these only have one global link instance, they reconnect it to the connection after the connection is closed. + +> ### Note +> The ```el::msglink::multi_connection_server``` is made for multiple ```el::msglink::client```s to connect to it. Even though the server-side link is not persistent, the client still only has one global link instance which behaves as described in the single-connection example. So when a client looses connection to a multi connection server, it still keeps and re-activates it's subscriptions just as expected. + +In the future it might be possible to add support for some sort of session identifier to the msglink authentication procedure that the client (which has a persistent link) remembers after the initial connect. When the multi connection server detects a connection loss without the connection being properly closed beforehand, it can keep it's link instance alive and reconnect it to the next new connection that authenticates with the same session identifier. From a0a59ed7c924b74b0d8b0d3895dfa6ac52dfa6e4 Mon Sep 17 00:00:00 2001 From: melektron Date: Fri, 29 Dec 2023 00:01:28 +0100 Subject: [PATCH 28/50] fixed a few problems with duplicate functions in header only lib by making them inline --- include/el/logging.hpp | 2 +- include/el/msglink/internal/msgtype.hpp | 4 ++-- include/el/msglink/internal/proto_version.hpp | 4 ++-- include/el/msglink/networking_architecture.md | 6 +++--- include/el/msglink/server.hpp | 11 +++++++++-- include/el/rtti_utils.hpp | 2 +- include/el/strutil.hpp | 6 +++--- 7 files changed, 21 insertions(+), 14 deletions(-) diff --git a/include/el/logging.hpp b/include/el/logging.hpp index 99b28c3..38e0259 100644 --- a/include/el/logging.hpp +++ b/include/el/logging.hpp @@ -272,7 +272,7 @@ namespace el::logging * @param _e the exception to print * @return std::string the printed exception */ - std::string format_exception(const std::exception &_e) + inline std::string format_exception(const std::exception &_e) { return rtti::demangle_if_possible(typeid(_e).name()) + "\n what(): " + _e.what(); } diff --git a/include/el/msglink/internal/msgtype.hpp b/include/el/msglink/internal/msgtype.hpp index 2adcf5d..72f18af 100644 --- a/include/el/msglink/internal/msgtype.hpp +++ b/include/el/msglink/internal/msgtype.hpp @@ -58,7 +58,7 @@ namespace el::msglink RPC_RESULT, }; - const char *msg_type_to_string(const msg_type_t _msg_type) + inline const char *msg_type_to_string(const msg_type_t _msg_type) { switch (_msg_type) { @@ -118,7 +118,7 @@ namespace el::msglink } } - msg_type_t msg_type_from_string(const std::string &_msg_type_name) + inline msg_type_t msg_type_from_string(const std::string &_msg_type_name) { using enum msg_type_t; diff --git a/include/el/msglink/internal/proto_version.hpp b/include/el/msglink/internal/proto_version.hpp index 7a12bb1..04ec94d 100644 --- a/include/el/msglink/internal/proto_version.hpp +++ b/include/el/msglink/internal/proto_version.hpp @@ -41,12 +41,12 @@ namespace el::msglink::proto_version * @return true _other is compatible with the current version * @return false _other is not compatible with the current version */ - bool is_compatible(const proto_version_t &_other) + inline bool is_compatible(const proto_version_t &_other) { return compatible_versions.contains(_other); } - std::string to_string(const proto_version_t &_ver) + inline std::string to_string(const proto_version_t &_ver) { return strutil::format("[%u.%u.%u]", _ver[0], _ver[1], _ver[2]); } diff --git a/include/el/msglink/networking_architecture.md b/include/el/msglink/networking_architecture.md index fa179bd..a98e96b 100644 --- a/include/el/msglink/networking_architecture.md +++ b/include/el/msglink/networking_architecture.md @@ -113,12 +113,12 @@ Since the communication is not resumed where it left off, we might as well delet For this reason, in a simple application were there is only one client and server which need to communicate, the **Link** block is completely decoupled from the **Network Interface** block. For the entire lifetime of an application, there will be a single instance of the user-defined link class that is never deleted. The network interface block will simply receive a reference to this instance to control it. Internally in the link class, the user protocol state (subscriptions, ...) is also decoupled from the network and msglink protocol state (authentication, transactions, ...). -Initially, the link instance is in a disconnected state, were the network interface block has not jet created a connection to the other party. During this time, other parts of the application can already access the link instance and register event listeners or emit events. They will not notice the connection isn't established jet and the link will keep track internally of which events are subscribed and may queue up emitted events (depending on configuration) +Initially, the link instance is in a disconnected state, were the network interface block has not jet created a connection to the other party. During this time, other parts of the application can already access the link instance and register event listeners or emit events. They will not notice the connection isn't established jet and the link will keep track internally of which events are subscribed and may queue up emitted events (depending on configuration). As soon as a connection is established and the authentication process is complete, the link will send all the event subscriptions for the currently required events according to it's internal records as well as possibly pushing queued event emissions. Any event subscriptions and other state changes made while the connection is established are immediately sent to the other party as well as recorded internally, so the link instance knows at all times what event subscriptions, data subscriptions, etc. it currently needs. -When the connection is lost, both parties' network connection blocks will reset the link instance' msglink protocol and networking state to a time before authentication. This does not affect user protocol state such as event subscriptions. Form then on, the link will behave like it did before the connection was established. Any access by the rest of the application will be recorded so the current state and missed events can be sent to the other party as soon it reconnects, ensuring the state of the communication subsystem will always be clean and the communication channel behaves as the user application expects. +When the connection is lost, both parties' network connection blocks will reset the link instance's msglink protocol and networking state to a time before authentication. This does not affect user protocol state such as event subscriptions. Form then on, the link will behave like it did before the connection was established. Any access by the rest of the application will be recorded so the current state and missed events can be sent to the other party as soon it reconnects, ensuring the state of the communication subsystem will always be clean and the communication channel behaves as the user application expects. ### many-to-one relationship @@ -130,7 +130,7 @@ This poses a problem when it comes to client state management however. As explai So when a connection is lost, what happens to the link instance? Surely it cannot be kept around forever. Since there are many clients, it is tricky to tell wether a client is trying to reconnect or it's simply a new client connecting, which makes it hard to match a new connection to an existing link. That would require some sort of session identifier, which is currently not supported by msglink (more on that later). Luckily we can work around this issue in many cases by changing the design philosophy of the application or just viewing it differently. -When there is a server responsible for *serving* a large number of clients, it is unlikely for the server to *desperately want to get some specific piece of information globally* from *a* specific client. Instead, in most cases the clients probably want's to get notified about events from the server, so the server might not even have any event subscriptions for the client. If the client want's the server to do something, an RPC is probably better suited. And for the cases were client events are listened to by the server, every client probably has it's own event listeners associated to it, which are registered as soon as the link is instantiated when the client connects (probably methods of the link itself). In most cases the server isn't going to say *I now temporarily need to be notified about this event from the client* during runtime. And once a client disconnects, the server probably doesn't care about a specific event from that client, as it is the client which *wants* to send the information to the server. +When there is a server responsible for *serving* a large number of clients, it is unlikely for the server to *desperately want to get some specific piece of information globally* from *a* specific client. Instead, in most cases the clients probably want's to get notified about events from the server, so the server might not even have any event subscriptions for the client. If the client want's the server to do something, an RPC is probably better suited. And for the cases were client events are listened to by the server, every client probably has it's own event listeners associated to it, which are registered as soon as the link is instantiated when the client connects (probably methods of the link itself). In most cases the server isn't going to say *I now temporarily need to be notified about this event from that client* during runtime. And once a client disconnects, the server probably doesn't care about a specific event from that client, as it is the *client* which *wants* to send the information to the server. ### Link lifetime diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index 4780712..cdbc661 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -23,6 +23,7 @@ msglink server class #include #include #include +#include #include @@ -31,13 +32,17 @@ msglink server class #include "internal/wspp.hpp" #include "errors.hpp" +#include "link.hpp" namespace el::msglink { using namespace std::chrono_literals; + template _LT> class server; + + template _LT> class connection_handler; /** @@ -49,9 +54,10 @@ namespace el::msglink * the main asio loop, so from the handlers in the * server class. */ + template _LT> class connection_handler { - friend class server; + friend class server<_LT>; private: // state @@ -201,6 +207,7 @@ namespace el::msglink }; + template _LT> class server { @@ -228,7 +235,7 @@ namespace el::msglink // set of connections to corresponding connection handler instance std::map< wspp::connection_hdl, - connection_handler, + connection_handler<_LT>, std::owner_less > m_open_connections; diff --git a/include/el/rtti_utils.hpp b/include/el/rtti_utils.hpp index bca899d..0f98a61 100644 --- a/include/el/rtti_utils.hpp +++ b/include/el/rtti_utils.hpp @@ -20,7 +20,7 @@ Utilities for runtime type information (RTTI) namespace el::rtti { - std::string demangle_if_possible(const char* _typename) + inline std::string demangle_if_possible(const char* _typename) { #ifdef __GNUC__ diff --git a/include/el/strutil.hpp b/include/el/strutil.hpp index cfda2ed..fe7c890 100644 --- a/include/el/strutil.hpp +++ b/include/el/strutil.hpp @@ -58,7 +58,7 @@ namespace el::strutil * @param instr the input string to convert * @return copy of the string in lowercase */ - std::string lowercase(std::string instr) + inline std::string lowercase(std::string instr) { std::for_each(instr.begin(), instr.end(), [](char &c) { c = ::tolower(c); }); @@ -73,7 +73,7 @@ namespace el::strutil * @param instr the input string to convert * @return copy of the string in lowercase */ - std::string uppercase(std::string instr) + inline std::string uppercase(std::string instr) { std::for_each(instr.begin(), instr.end(), [](char &c) { c = ::toupper(c); }); @@ -89,7 +89,7 @@ namespace el::strutil * @param _string The string to store the file contents in. This will overwrite the string. * @return The length of the file (= the number of characters copied to the string) */ - size_t read_file_into_string(std::ifstream &_file, std::string &_string) + inline size_t read_file_into_string(std::ifstream &_file, std::string &_string) { // get file length _file.seekg(0, std::ios::end); From 48f9aa4004ba8dfddecf595a9c398336ac503e43 Mon Sep 17 00:00:00 2001 From: melektron Date: Fri, 29 Dec 2023 01:52:48 +0100 Subject: [PATCH 29/50] started combining link with websocket server, not quite working jet --- include/el/msglink/link.hpp | 3 + include/el/msglink/server.hpp | 123 +++++++++++++++++++++++++++++----- 2 files changed, 111 insertions(+), 15 deletions(-) diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index 49daac4..bb4effb 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -345,6 +345,8 @@ namespace el::msglink */ void on_authentication_done() { + EL_LOG_FUNCTION_CALL(); + // send event subscribe messages for all events subscribed before // auth was complete (e.g. events with fixed handlers created during // definition) @@ -868,6 +870,7 @@ namespace el::msglink ~link() { + EL_LOG_FUNCTION_CALL(); // invalidate all event subscriptions to make sure there are no dangling pointers for (auto &[id, sub] : event_subscription_ids_to_objects) sub->invalidate(); diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index cdbc661..70f07ac 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -31,6 +31,7 @@ msglink server class #include "../logging.hpp" #include "internal/wspp.hpp" +#include "internal/link_interface.hpp" #include "errors.hpp" #include "link.hpp" @@ -55,7 +56,7 @@ namespace el::msglink * server class. */ template _LT> - class connection_handler + class connection_handler : public link_interface { friend class server<_LT>; @@ -69,6 +70,9 @@ namespace el::msglink // asio timer used to schedule keep-alive pings std::shared_ptr m_ping_timer; + + // link instance for handling communication with this client + _LT m_link; private: // methods @@ -125,6 +129,39 @@ namespace el::msglink // when a ping is received in the pong handler, a new ping will // be scheduled. } + + protected: // methods + /** + * @brief implements the link_interface interface + * to allow the link to send messages through the client + * communication channel. + * + * @param _content message content + */ + virtual void send_message(const std::string &_content) override + { + EL_LOGD("Outgoing Message: %s", _content.c_str()); + get_connection()->send(_content); + } + + public: + /** + * @brief implements the link_interface interface + * to allow the link to close the connection at any point. + * + * @param _code close status code + * @param _reason readable reason as required by websocket protocol + */ + virtual void close_connection(int _code, std::string _reason) noexcept override + { + EL_LOG_FUNCTION_CALL(); + // close the connection gracefully + get_connection()->close(_code, _reason); + EL_LOGD("past close"); + + // WARNING: connection_handler instance is destroyed before the terminate() call returns. + // Don't use it here anymore! This must also be regarded by the link. + } public: @@ -135,8 +172,7 @@ namespace el::msglink /** * @brief called during on_open when new connection - * is established. Used to initiate asynchronous - * actions. + * is established. Used only for initialization. * * @param _socket_server * @param _connection @@ -144,16 +180,20 @@ namespace el::msglink connection_handler(wsserver &_socket_server, wspp::connection_hdl _connection) : m_socket_server(_socket_server) , m_connection(_connection) + , m_link( + true, // is server instance + *this // use this connection handler to communicate + ) { EL_LOG_FUNCTION_CALL(); - // start the first ping - schedule_ping(); + // define the link protocol + m_link.define(); } /** * @brief called during on_close when connection is closed or terminated. - * Used to cancel any potential actions. + * Used to clean up resources like canceling any potential actions. */ virtual ~connection_handler() { @@ -164,15 +204,33 @@ namespace el::msglink m_ping_timer->cancel(); } + /** + * @brief called by server immediately after connection is + * established (and probably this instance has been constructed). + * This is used to initiate any communication actions. + * + */ + void on_open() + { + EL_LOG_FUNCTION_CALL(); + + // start the first ping + schedule_ping(); + + // start communication by notifying the link + // TODO: i.e. "connecting" the link to the interface (change how this works) + m_link.on_connection_established(); + } + /** * @brief called by server when message arrives for this connection * * @param _msg the message to handle */ - void on_message(wsserver::message_ptr _msg) noexcept + void on_message(wsserver::message_ptr _msg) { - EL_LOGD("message: %s", _msg->get_payload().c_str()); - get_connection()->send(_msg->get_payload(), _msg->get_opcode()); + EL_LOGD("Incoming Message: %s", _msg->get_payload().c_str()); + m_link.on_message(_msg->get_payload()); } /** @@ -205,6 +263,23 @@ namespace el::msglink // Don't use it here anymore! } + /** + * @brief called by the server when the connection closes (for any reason) + * to stop any communication and other async actions just before the instance + * is deleted. + */ + void on_close() + { + EL_LOG_FUNCTION_CALL(); + + // TODO: invalidate the link (i.e. "disconnect" it from the link iterface + // so it cannot call back to it anymore) + + // cancel ping timer if one is running + if (m_ping_timer) + m_ping_timer->cancel(); + } + }; template _LT> @@ -253,13 +328,17 @@ namespace el::msglink if (m_server_state != RUNNING) return; + // TODO: make atomic + // create new handler instance and save it - m_open_connections.emplace( + auto new_connection = m_open_connections.emplace( std::piecewise_construct, // Needed for in-place construct https://en.cppreference.com/w/cpp/utility/piecewise_construct std::forward_as_tuple(_hdl), std::forward_as_tuple(m_socket_server, _hdl) ); + // notify new connection handler to start communication + new_connection.first->second.on_open(); } /** @@ -303,11 +382,17 @@ namespace el::msglink if (m_server_state != RUNNING) return; - // remove closed connection from connection map - if (!m_open_connections.erase(_hdl)) + // TODO: make thread-safe (atomic) + if (!m_open_connections.contains(_hdl)) { throw invalid_connection_error("Attempted to close an unknown/invalid connection which doesn't seem to exist."); } + + // notify connection to stop communication + m_open_connections.at(_hdl).on_close(); + + // remove closed connection from connection map, deleting the connection handlers + m_open_connections.erase(_hdl); } /** @@ -391,8 +476,13 @@ namespace el::msglink try { - // we don't want any wspp log messages - m_socket_server.set_access_channels(wspp::log::alevel::all); + // wspp log messages off by default + m_socket_server.clear_access_channels(wspp::log::alevel::all); + m_socket_server.clear_error_channels(wspp::log::elevel::all); + // turn on selected logging channels + m_socket_server.set_access_channels(wspp::log::alevel::disconnect); + m_socket_server.set_access_channels(wspp::log::alevel::connect); + m_socket_server.set_access_channels(wspp::log::alevel::fail); m_socket_server.set_error_channels(wspp::log::elevel::all); // initialize asio communication @@ -476,11 +566,14 @@ namespace el::msglink { // stop listening for new connections m_socket_server.stop_listening(); + EL_LOGD("got past 1"); // close all existing connections for (const auto &[hdl, client] : m_open_connections) { - m_socket_server.close(hdl, 0, "server stopped"); + EL_LOGD("got past 2"); + m_socket_server.close(hdl, 0, "server stopped"); // FIXME: Does this actually close connection and call on_close()? + EL_LOGD("got past 3"); } } catch (const wspp::exception &e) From 88343426fd0949f811b99816255b038dd2beed2b Mon Sep 17 00:00:00 2001 From: melektron Date: Wed, 3 Jan 2024 01:55:25 +0100 Subject: [PATCH 30/50] fixed issue with ping being sent during closing handshake causing an error --- include/el/msglink/msglink_protocol.md | 2 + include/el/msglink/server.hpp | 110 +++++++++++++++++-------- 2 files changed, 79 insertions(+), 33 deletions(-) diff --git a/include/el/msglink/msglink_protocol.md b/include/el/msglink/msglink_protocol.md index c4f151c..a53e75c 100644 --- a/include/el/msglink/msglink_protocol.md +++ b/include/el/msglink/msglink_protocol.md @@ -298,6 +298,8 @@ Note for future me: If msglink doesn't fit for some reason in the future, here a ## Link collection - wspp client reconnect: https://github.com/zaphoyd/websocketpp/issues/754#issue-353706390 +- wspp server and websocat client close doesn't work (closing handshake timeout), this person has the same problem I had but got no solutions: https://stackoverflow.com/questions/69447469/do-websocat-handle-closing-handshake-properly-with-a-websocketpp-server + ## TODOS diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index 70f07ac..97f5697 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -73,6 +73,9 @@ namespace el::msglink // link instance for handling communication with this client _LT m_link; + + // set when communication has been canceled to prevent any further actions + soflag m_communication_canceled; private: // methods @@ -129,6 +132,30 @@ namespace el::msglink // when a ping is received in the pong handler, a new ping will // be scheduled. } + + /** + * @brief called by the close_connection() and on_close() method to + * ensure that any async communication procedures are stopped and the link is + * disconnected before entering a potentially invalid + * closing-handshake state. + * This method can be called multiple times and will do nothing after the + * first call. + */ + void cancel_communication() + { + // if already canceled, don't cancel again + if (m_communication_canceled) + return; + + // TODO: invalidate the link (i.e. "disconnect" it from the link iterface + // so it cannot call back to it anymore) + + // cancel ping timer if one is running + if (m_ping_timer) + m_ping_timer->cancel(); + + m_communication_canceled.set(); + } protected: // methods /** @@ -140,28 +167,14 @@ namespace el::msglink */ virtual void send_message(const std::string &_content) override { + // ensure no messages go through after cancel, even though link + // shouldn't call this method anymore after cancel anyway. + if (m_communication_canceled) + return; + EL_LOGD("Outgoing Message: %s", _content.c_str()); get_connection()->send(_content); } - - public: - /** - * @brief implements the link_interface interface - * to allow the link to close the connection at any point. - * - * @param _code close status code - * @param _reason readable reason as required by websocket protocol - */ - virtual void close_connection(int _code, std::string _reason) noexcept override - { - EL_LOG_FUNCTION_CALL(); - // close the connection gracefully - get_connection()->close(_code, _reason); - EL_LOGD("past close"); - - // WARNING: connection_handler instance is destroyed before the terminate() call returns. - // Don't use it here anymore! This must also be regarded by the link. - } public: @@ -204,6 +217,27 @@ namespace el::msglink m_ping_timer->cancel(); } + /** + * @brief implements the link_interface interface + * to allow the link to close the connection at any point. + * + * @param _code close status code + * @param _reason readable reason as required by websocket protocol + */ + virtual void close_connection(int _code, std::string _reason) noexcept override + { + EL_LOG_FUNCTION_CALL(); + + // to avoid communication actions during closing handshake + cancel_communication(); + // close the connection gracefully + get_connection()->close(_code, _reason); + + // WARNING: connection_handler instance is destroyed before the terminate() call returns. + // Don't use it here anymore! This must also be regarded by the link. + } + + /** * @brief called by server immediately after connection is * established (and probably this instance has been constructed). @@ -256,6 +290,8 @@ namespace el::msglink */ void on_pong_timeout(std::string &_expected_payload) { + cancel_communication(); + // terminate connection get_connection()->terminate(std::make_error_code(std::errc::timed_out)); @@ -264,20 +300,16 @@ namespace el::msglink } /** - * @brief called by the server when the connection closes (for any reason) - * to stop any communication and other async actions just before the instance - * is deleted. + * @brief called by the server when the connection has been closed (for any reason, + * whether initiated by client or by server) to stop any potentially still running + * communication procedures. */ void on_close() { EL_LOG_FUNCTION_CALL(); - // TODO: invalidate the link (i.e. "disconnect" it from the link iterface - // so it cannot call back to it anymore) - - // cancel ping timer if one is running - if (m_ping_timer) - m_ping_timer->cancel(); + // might already have been called by close_connection() but doesn't matter. + cancel_communication(); } }; @@ -395,6 +427,17 @@ namespace el::msglink m_open_connections.erase(_hdl); } + /** + * @brief called by wspp when a new connection was attempted but failed + * before it was fully connected. + * + * @param _hdl handle to associated ws connection + */ + void on_fail(wspp::connection_hdl _hdl) + { + EL_LOG_FUNCTION_CALL(); + } + /** * @brief called by wspp when a pong is received. * This is forwarded to the connection handler. @@ -484,6 +527,7 @@ namespace el::msglink m_socket_server.set_access_channels(wspp::log::alevel::connect); m_socket_server.set_access_channels(wspp::log::alevel::fail); m_socket_server.set_error_channels(wspp::log::elevel::all); + //m_socket_server.set_access_channels(wspp::log::alevel::all); // initialize asio communication m_socket_server.init_asio(); @@ -492,6 +536,7 @@ namespace el::msglink m_socket_server.set_open_handler(std::bind(&server::on_open, this, pl::_1)); m_socket_server.set_message_handler(std::bind(&server::on_message, this, pl::_1, pl::_2)); m_socket_server.set_close_handler(std::bind(&server::on_close, this, pl::_1)); + m_socket_server.set_fail_handler(std::bind(&server::on_fail, this, pl::_1)); m_socket_server.set_pong_handler(std::bind(&server::on_pong_received, this, pl::_1, pl::_2)); m_socket_server.set_pong_timeout_handler(std::bind(&server::on_pong_timeout, this, pl::_1, pl::_2)); @@ -566,14 +611,13 @@ namespace el::msglink { // stop listening for new connections m_socket_server.stop_listening(); - EL_LOGD("got past 1"); // close all existing connections - for (const auto &[hdl, client] : m_open_connections) + for (auto &[hdl, client] : m_open_connections) { - EL_LOGD("got past 2"); - m_socket_server.close(hdl, 0, "server stopped"); // FIXME: Does this actually close connection and call on_close()? - EL_LOGD("got past 3"); + // use close_connection method to ensure communication is properly + // stopped, preventing errors + client.close_connection(0, "server stopped"); } } catch (const wspp::exception &e) From b759db9d410f5b9da780612f2c106b4b4ecf7fb7 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 14 Jan 2024 22:23:32 +0100 Subject: [PATCH 31/50] added rudimentary json/codable optional support and started with support for non-ping supporting clients --- include/el/codable.hpp | 7 +- include/el/codable_types.hpp | 127 +++++++++++++++++++++++ include/el/msglink/dependencies.md | 11 ++ include/el/msglink/event.hpp | 8 ++ include/el/msglink/internal/messages.hpp | 2 + include/el/msglink/link.hpp | 3 + include/el/msglink/server.hpp | 2 - 7 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 include/el/codable_types.hpp create mode 100644 include/el/msglink/dependencies.md diff --git a/include/el/codable.hpp b/include/el/codable.hpp index 5a52f9a..904b72f 100644 --- a/include/el/codable.hpp +++ b/include/el/codable.hpp @@ -20,10 +20,10 @@ and depends on it. It must be includable as follows: #pragma once #include - #include -#include +#include "metaprog.hpp" +#include "codable_types.hpp" namespace el @@ -175,7 +175,8 @@ namespace el template \ EL_DECODER(member) \ { \ - member = encoded_data; \ + /* explicit convert using .get to avoid unwanted casting paths */ \ + member = encoded_data.get(); \ } // (private) generates the default encoder/decoder methods for a class member diff --git a/include/el/codable_types.hpp b/include/el/codable_types.hpp new file mode 100644 index 0000000..7f2de7a --- /dev/null +++ b/include/el/codable_types.hpp @@ -0,0 +1,127 @@ +/* +ELEKTRON © 2024 - now +Written by melektron +www.elektron.work +11.01.24, 09:28 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Additional codable support for standard and extended types that are commonly +used. + +This functionality is based on Niels Lohmann's JSON for modern C++ library +and depends on it. It must be includable as follows: + +#include +*/ + +#pragma once + +#include +#include + +namespace el +{ + /** + * Future Ideas: + * + * Codables use the "decode_from_object" and "encode_to_object" methods that can be overloaded + * to support various type conversions. + * + * Unlike the functionality provided by nlohmann's JSON library ("to_json" and "from_json"), + * these functions get the entire context of the containing object. This allows them to be + * more flexible, such as coding optionals. + */ + + + /** + * @brief object key decoder using nlohmann's generic decoding + * mechanism. This enables the decoding of any C++ object + * supported by nlohmann JSON and any types made decodable using + * this mechanism. + * + * @tparam _T datatype to decode from the object + * @param _object json object to decode from + * @param _key the key in the above object to decode + * @param _out_data destination of decoded data + */ + //template + //void decode_from_object( + // const nlohmann::json &_object, + // const std::string &_key, + // _T &_out_data + //) + //{ + // // this uses the from_json() functions to decode + // _out_data = _object.at(_key).get<_T>(); + //} + + /** + * @brief object key encoder using nlohmann's generic encoding + * mechanism. This enables the encoding of any C++ object + * supported by nlohmann JSON and any types made encodable using + * this mechanism. + * + * @tparam _T datatype to encode to the object + * @param _object json object to store encoded output in + * @param _key the key in the above object to store the encoded output + * @param _in_data data to encode + */ + //template + //void encode_to_object( + // nlohmann::json &_object, + // const std::string &_key, + // const _T &_in_data + //) + //{ + // _object[_key] = _in_data; + //} + +} // namespace el + + +NLOHMANN_JSON_NAMESPACE_BEGIN +/** + * @brief (de)serializer for std::optional + * See https://json.nlohmann.me/features/arbitrary_types/#how-do-i-convert-third-party-types + * for explanation why namespace is needed. + * @tparam _T contained type + */ +template +struct adl_serializer> +{ + /** + * @brief nlohmann JSON decoder for optionals. Expects JSON null for + * empty optional case and the value otherwise. + * + * @param _j_input input json data + * @param _t_output decoded optional + */ + static void from_json(const nlohmann::json &_j_input, std::optional<_T> &_t_output) + { + if (_j_input.is_null()) + _t_output.reset(); + else + _t_output = _j_input.get<_T>(); + }; + + /** + * @brief nlohmann JSON encoder for optionals. Emits JSON null for + * empty optional case and the encoded value otherwise. + * + * @tparam _T contained type + * @param _j_output output json data + * @param _t_input optional to encode + */ + static void to_json(nlohmann::json &_j_output, const std::optional<_T> &_t_input) + { + if (!_t_input.has_value()) + _j_output = nullptr; + else + _j_output = *_t_input; + } + +}; +NLOHMANN_JSON_NAMESPACE_END \ No newline at end of file diff --git a/include/el/msglink/dependencies.md b/include/el/msglink/dependencies.md new file mode 100644 index 0000000..ec778e1 --- /dev/null +++ b/include/el/msglink/dependencies.md @@ -0,0 +1,11 @@ +# msglink dependencies + +The C++ implementation of msglink currently uses Niels Lohmann's JSON library as well as WebSocket++. +Both of those are bundled or provided as submodules. + +WebSocket++ needs ASIO for network communication. It needs to be installed, ideally in the standalone version without boost: + +```bash +# On debian or derivatives: +sudo apt install libasio-dev +``` \ No newline at end of file diff --git a/include/el/msglink/event.hpp b/include/el/msglink/event.hpp index 66f2f78..dbe9a36 100644 --- a/include/el/msglink/event.hpp +++ b/include/el/msglink/event.hpp @@ -143,6 +143,14 @@ namespace el::msglink template concept BidirectionalEvent = std::derived_from<_ET, incoming_event> && std::derived_from<_ET, outgoing_event>; + /** + * @brief Constrains _ET to be derived from incoming_event + * and or outgoing_event. This constrains a type to be any + * sort of event. + */ + template + concept AnyEvent = std::derived_from<_ET, incoming_event> || std::derived_from<_ET, outgoing_event>; + } // namespace el::msglink #endif // __EL_ENABLE_CXX20 \ No newline at end of file diff --git a/include/el/msglink/internal/messages.hpp b/include/el/msglink/internal/messages.hpp index d8ef1cf..ee11ffc 100644 --- a/include/el/msglink/internal/messages.hpp +++ b/include/el/msglink/internal/messages.hpp @@ -38,6 +38,7 @@ namespace el::msglink std::string type = __EL_MSGLINK_MSG_NAME_AUTH; proto_version_t proto_version; link_version_t link_version; + std::optional no_ping; std::set events; std::set data_sources; std::set procedures; @@ -48,6 +49,7 @@ namespace el::msglink tid, proto_version, link_version, + no_ping, events, data_sources, procedures diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index bb4effb..f267ce4 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -304,6 +304,9 @@ namespace el::msglink close_code_t::EVENT_REQUIREMENTS_NOT_SATISFIED, "Remote party does not satisfy the event requirements (missing events)" ); + + // TODO: remove + EL_LOGD("no_ping=%s", !msg.no_ping ? "nullptr" : (msg.no_ping.value() ? "true" : "false")); // check data sources diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index 97f5697..f2ea030 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -13,8 +13,6 @@ msglink server class #pragma once -#define ASIO_STANDALONE - #include #include #include From 8824e87d7dd1ca0e8c567055f67aff54e8c8da62 Mon Sep 17 00:00:00 2001 From: melektron Date: Fri, 19 Jan 2024 22:32:04 +0100 Subject: [PATCH 32/50] implemented proper optional support for codable objects --- include/el/codable.hpp | 14 ++- include/el/codable_types.hpp | 164 ++++++++++++++++--------- include/el/msglink/msglink_protocol.md | 25 +++- 3 files changed, 140 insertions(+), 63 deletions(-) diff --git a/include/el/codable.hpp b/include/el/codable.hpp index 904b72f..62280e3 100644 --- a/include/el/codable.hpp +++ b/include/el/codable.hpp @@ -147,14 +147,14 @@ namespace el */ // (private) generates code which uses a member's encoder function to add it to a json object -#define __EL_CODABLE_ENCODE_KEY(member) encode_ ## member (_output[#member]); +#define __EL_CODABLE_ENCODE_KEY(member) encode_ ## member (#member, _output); // (private) generates code which uses a member's decoder function to retrieve it's value from a json object -#define __EL_CODABLE_DECODE_KEY(member) decode_ ## member (_input.at(#member)); +#define __EL_CODABLE_DECODE_KEY(member) decode_ ## member (#member, _input); // (public) generates the declaration of the encoder method for a specific member -#define EL_ENCODER(member) void encode_ ## member (nlohmann::json &encoded_data) const +#define EL_ENCODER(member) void encode_ ## member (const char *member_name, nlohmann::json &encoded_data) const // (public) generates the declaration of the decoder method for a specific member -#define EL_DECODER(member) void decode_ ## member (const nlohmann::json &encoded_data) +#define EL_DECODER(member) void decode_ ## member (const char *member_name, const nlohmann::json &encoded_data) // (private) generates the default encoder method for a member #define __EL_CODABLE_DEFINE_DEFAULT_ENCODER(member) \ @@ -164,7 +164,8 @@ namespace el template \ EL_ENCODER(member) \ { \ - encoded_data = member; \ + /*encoded_data[member_name] = member;*/ \ + ::el::codable_types::encode_to_object(encoded_data, member_name, member); \ } // (private) generates the default decoder method for a member @@ -176,7 +177,8 @@ namespace el EL_DECODER(member) \ { \ /* explicit convert using .get to avoid unwanted casting paths */ \ - member = encoded_data.get(); \ + /*member = encoded_data.at(member_name).get();*/ \ + ::el::codable_types::decode_from_object(encoded_data, member_name, member); \ } // (private) generates the default encoder/decoder methods for a class member diff --git a/include/el/codable_types.hpp b/include/el/codable_types.hpp index 7f2de7a..cdfc58e 100644 --- a/include/el/codable_types.hpp +++ b/include/el/codable_types.hpp @@ -22,17 +22,18 @@ and depends on it. It must be includable as follows: #include #include -namespace el +namespace el::codable_types { /** - * Future Ideas: - * * Codables use the "decode_from_object" and "encode_to_object" methods that can be overloaded * to support various type conversions. * * Unlike the functionality provided by nlohmann's JSON library ("to_json" and "from_json"), * these functions get the entire context of the containing object. This allows them to be * more flexible, such as coding optionals. + * + * The namespace is called "el::codable_types" and not just "el" because this namespace may be extended + * by the user of the library to support conversion of custom types. */ @@ -47,16 +48,16 @@ namespace el * @param _key the key in the above object to decode * @param _out_data destination of decoded data */ - //template - //void decode_from_object( - // const nlohmann::json &_object, - // const std::string &_key, - // _T &_out_data - //) - //{ - // // this uses the from_json() functions to decode - // _out_data = _object.at(_key).get<_T>(); - //} + template + void decode_from_object( + const nlohmann::json &_object, + const std::string &_key, + _T &_out_data + ) + { + // this uses the from_json() functions to decode + _out_data = _object.at(_key).get<_T>(); + } /** * @brief object key encoder using nlohmann's generic encoding @@ -69,59 +70,110 @@ namespace el * @param _key the key in the above object to store the encoded output * @param _in_data data to encode */ - //template - //void encode_to_object( - // nlohmann::json &_object, - // const std::string &_key, - // const _T &_in_data - //) - //{ - // _object[_key] = _in_data; - //} - -} // namespace el - + template + void encode_to_object( + nlohmann::json &_object, + const std::string &_key, + const _T &_in_data + ) + { + _object[_key] = _in_data; + } -NLOHMANN_JSON_NAMESPACE_BEGIN -/** - * @brief (de)serializer for std::optional - * See https://json.nlohmann.me/features/arbitrary_types/#how-do-i-convert-third-party-types - * for explanation why namespace is needed. - * @tparam _T contained type - */ -template -struct adl_serializer> -{ /** - * @brief nlohmann JSON decoder for optionals. Expects JSON null for - * empty optional case and the value otherwise. + * @brief object key decoder overload for optional types. + * This enables the decoding std::optionals from json, for any + * contained type that can be decoded. + * If key is not found in object, optional will be reset (nullptr). + * If key is found, optional is assigned the decoded value. * - * @param _j_input input json data - * @param _t_output decoded optional + * @tparam _T datatype contained in optional + * @param _object json object to decode from + * @param _key the key in the above object to decode + * @param _out_data destination optional of decoded data */ - static void from_json(const nlohmann::json &_j_input, std::optional<_T> &_t_output) + template + void decode_from_object( + const nlohmann::json &_object, + const std::string &_key, + std::optional<_OT> &_out_data + ) { - if (_j_input.is_null()) - _t_output.reset(); + if (!_object.contains(_key)) + _out_data.reset(); else - _t_output = _j_input.get<_T>(); - }; - + _out_data = _object.at(_key).get<_OT>(); + } + /** - * @brief nlohmann JSON encoder for optionals. Emits JSON null for - * empty optional case and the encoded value otherwise. + * @brief object key encoder overload for optional types. + * This enables the encoding std::optionals to json, for any + * contained type that can be encoded. + * If optional is empty (nullptr), the key will not be added + * to output object (if it's already ther, it's untouched). + * If the optional contains a value, it is encoded and added to + * the json object. * - * @tparam _T contained type - * @param _j_output output json data - * @param _t_input optional to encode + * @tparam _T datatype contained in optional + * @param _object json object to store encoded output in + * @param _key the key in the above object to store the encoded output + * @param _in_data optional data to encode */ - static void to_json(nlohmann::json &_j_output, const std::optional<_T> &_t_input) + template + void encode_to_object( + nlohmann::json &_object, + const std::string &_key, + const std::optional<_OT> &_in_data + ) { - if (!_t_input.has_value()) - _j_output = nullptr; - else - _j_output = *_t_input; + if (_in_data.has_value()) + _object[_key] = *_in_data; } -}; +} // namespace codable_types + + +NLOHMANN_JSON_NAMESPACE_BEGIN +/** + * @brief (de)serializer for std::optional + * See https://json.nlohmann.me/features/arbitrary_types/#how-do-i-convert-third-party-types + * for explanation why namespace is needed. + * This has been replaced by the custom encoder/decoder method supporting more functionality. + * @tparam _T contained type + */ +//template +//struct adl_serializer> +//{ +// /** +// * @brief nlohmann JSON decoder for optionals. Expects JSON null for +// * empty optional case and the value otherwise. +// * +// * @param _j_input input json data +// * @param _t_output decoded optional +// */ +// static void from_json(const nlohmann::json &_j_input, std::optional<_T> &_t_output) +// { +// if (_j_input.is_null()) +// _t_output.reset(); +// else +// _t_output = _j_input.get<_T>(); +// }; +// +// /** +// * @brief nlohmann JSON encoder for optionals. Emits JSON null for +// * empty optional case and the encoded value otherwise. +// * +// * @tparam _T contained type +// * @param _j_output output json data +// * @param _t_input optional to encode +// */ +// static void to_json(nlohmann::json &_j_output, const std::optional<_T> &_t_input) +// { +// if (!_t_input.has_value()) +// _j_output = nullptr; +// else +// _j_output = *_t_input; +// } +// +//}; NLOHMANN_JSON_NAMESPACE_END \ No newline at end of file diff --git a/include/el/msglink/msglink_protocol.md b/include/el/msglink/msglink_protocol.md index a53e75c..be0bab8 100644 --- a/include/el/msglink/msglink_protocol.md +++ b/include/el/msglink/msglink_protocol.md @@ -118,7 +118,7 @@ The client needs: - the ability for the user to disable a device, for example because it needs to much power
**Command with response -> RPC: "disable_device"** -> In msglink there is (almost) no difference between the client and the server except for how the socket connection is established (and transaction IDs which are covered below). Therefore, any example described here could just as well work in the other direction. +> In msglink there is (almost) no difference between the client and the server except for how the socket connection is established and who sends pings (and transaction IDs which are covered below). Therefore, any example described here could just as well work in the other direction. ## The basics @@ -145,6 +145,7 @@ The **```type```** property defines the purpose of the message. There are the fo - auth - auth_ack +- pong - evt_sub - evt_unsub - evt_emit @@ -194,6 +195,7 @@ When a msglink client first connects to the msglink server both parties send an "tid": 1, // should be 1 for server and -1 for client according to definition of tid generation above "proto_version": [1, 2, 3], "link_version": 1, + "no_ping": false, // optional boolean "events": ["error_occurred"], "data_sources": ["devices", "power_consumption"], "procedures": ["disable_device"] @@ -202,6 +204,7 @@ When a msglink client first connects to the msglink server both parties send an - **```proto_verison```**: the msglink protocol version (determines whether certain features are supported) - **```link_verison```**: the user-defined link version (version of the user defined protocol) +- **```no_ping```**: flag that can be set by the client if it doesn't support receiving pong messages from "user" code. Every client *must* respond to WS pings with WS pongs, but in some cases (such as browser API) the user code cannot detect this happening. Such a client can set this flag (_true_) during authentication causing the server to send an extra msglink "pong" message whenever a ping-pong procedure has finished, which can be used by the client to determine the health of the connection. This key can be omitted having the same result as a _false_ value. This key is to be ignored by clients if included by servers in their auth message, as only servers are responsible for conducting ping procedures. - **```events```**: a list of events the party may emit (it's outgoing events) - **```data_sources```**: a list of data sources the party can provide (it's outgoing data sources) - **```procedures```**: a list of remote procedures the party provides @@ -235,6 +238,26 @@ Only after both parties' authentication transactions have been successfully comp - having sent the auth_ack message in response to the other's auth message - having received the auth_ack message in response to it's own auth message +## Heartbeat and Pong message + +By default, TCP sockets and also websockets don't detect unplanned connection loss. When a connection is established, communication parties don't know if the connection is still alive unless they attempt to exchange any data. For this reason, the WebSocket protocol defines specific ping-pong functionality. A WebSocket party can send a ping message with some optional data (This is a special message type defined by WebSocket, which is on a lower level than msglink messages. This has nothing to do with msglink messages) to which the other party shall respond with a pong message, containing the same data. If the response is not received in a certain time period, the connection is declared dead. + +In msglink, the server is responsible for sending pings. This is mostly because most server libraries support this feature, were as some clients, such as the browser WebSocket API have now way of detecting or interacting with ping-pong messages. +As soon as the msglink connection is established, the server starts to send pings to the client in regular intervals. If a response (WS pong) is not received in time, the connection is terminated on the server side. + +In case of a broke connection however, the client will not be aware of this termination. For this reason, the client listens to the ping messages received (to which it responds with pong, likely implemented by underlying WebSocket library already) and terminates the connection if no ping message is received in a specific time period. + +If a client doesn't support the detection of ping interactions, it has to set the **```no_ping```** flag during authentication (as described [here](#authentication-procedure)). In this case, the server will send a msglink pong message (which is a regular WebSocket communication message, not a control message like pings and pongs) every time it receives a pong from the client. This poses some overhead, but less than would be caused by replacing WS ping and pong message with custom msglink messages entirely. + +```json +{ + "type": "pong", + "tid": ..., // new transaction ID +} +``` + +The pong message does not contain any additional data. This message is only ever sent from server to client. If a client sends such a message to the server, it has no effect. + ## Event messages From 0bc0574d184499b499302c747c7427f00bb58a45 Mon Sep 17 00:00:00 2001 From: melektron Date: Sat, 20 Jan 2024 23:20:29 +0100 Subject: [PATCH 33/50] changed optional coding to use custom coders and renamed generated coder methods --- include/el/codable.hpp | 8 ++++---- include/el/codable_types.hpp | 10 ++++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/include/el/codable.hpp b/include/el/codable.hpp index 62280e3..8028d4a 100644 --- a/include/el/codable.hpp +++ b/include/el/codable.hpp @@ -147,14 +147,14 @@ namespace el */ // (private) generates code which uses a member's encoder function to add it to a json object -#define __EL_CODABLE_ENCODE_KEY(member) encode_ ## member (#member, _output); +#define __EL_CODABLE_ENCODE_KEY(member) _el_codable_encode_ ## member (#member, _output); // (private) generates code which uses a member's decoder function to retrieve it's value from a json object -#define __EL_CODABLE_DECODE_KEY(member) decode_ ## member (#member, _input); +#define __EL_CODABLE_DECODE_KEY(member) _el_codable_decode_ ## member (#member, _input); // (public) generates the declaration of the encoder method for a specific member -#define EL_ENCODER(member) void encode_ ## member (const char *member_name, nlohmann::json &encoded_data) const +#define EL_ENCODER(member) void _el_codable_encode_ ## member (const char *member_name, nlohmann::json &encoded_data) const // (public) generates the declaration of the decoder method for a specific member -#define EL_DECODER(member) void decode_ ## member (const char *member_name, const nlohmann::json &encoded_data) +#define EL_DECODER(member) void _el_codable_decode_ ## member (const char *member_name, const nlohmann::json &encoded_data) // (private) generates the default encoder method for a member #define __EL_CODABLE_DEFINE_DEFAULT_ENCODER(member) \ diff --git a/include/el/codable_types.hpp b/include/el/codable_types.hpp index cdfc58e..0cbc481 100644 --- a/include/el/codable_types.hpp +++ b/include/el/codable_types.hpp @@ -102,7 +102,12 @@ namespace el::codable_types if (!_object.contains(_key)) _out_data.reset(); else - _out_data = _object.at(_key).get<_OT>(); + { + // default construct the optional to ensure it has a value in case it didn't before + _out_data.emplace(); + decode_from_object<_OT>(_object, _key, *_out_data); // pass content reference is UD without value + //_out_data = _object.at(_key).get<_OT>(); + } } /** @@ -127,7 +132,8 @@ namespace el::codable_types ) { if (_in_data.has_value()) - _object[_key] = *_in_data; + encode_to_object(_object, _key, *_in_data); + //_object[_key] = *_in_data; } } // namespace codable_types From a21b02bc69b71e97a5ede35e2e53496874a0e9b2 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 21 Jan 2024 16:49:18 +0100 Subject: [PATCH 34/50] implemented sending pong message if client requests it --- include/el/msglink/internal/messages.hpp | 12 ++++++ include/el/msglink/internal/msgtype.hpp | 9 +++- include/el/msglink/link.hpp | 53 ++++++++++++++++++++---- include/el/msglink/msglink_protocol.md | 48 ++++++++++----------- include/el/msglink/server.hpp | 3 ++ 5 files changed, 93 insertions(+), 32 deletions(-) diff --git a/include/el/msglink/internal/messages.hpp b/include/el/msglink/internal/messages.hpp index ee11ffc..7728cf1 100644 --- a/include/el/msglink/internal/messages.hpp +++ b/include/el/msglink/internal/messages.hpp @@ -31,6 +31,18 @@ namespace el::msglink tid_t tid; }; + struct msg_pong_t + // not base message because no tid required + : public encodable + { + std::string type = __EL_MSGLINK_MSG_NAME_PONG; + + EL_DEFINE_ENCODABLE( + msg_evt_sub_t, + type + ) + }; + struct msg_auth_t : public base_msg_t , public codable diff --git a/include/el/msglink/internal/msgtype.hpp b/include/el/msglink/internal/msgtype.hpp index 72f18af..29948de 100644 --- a/include/el/msglink/internal/msgtype.hpp +++ b/include/el/msglink/internal/msgtype.hpp @@ -18,6 +18,7 @@ Defines all message types possible and conversions from/to string #include "../errors.hpp" +#define __EL_MSGLINK_MSG_NAME_PONG "pong" #define __EL_MSGLINK_MSG_NAME_AUTH "auth" #define __EL_MSGLINK_MSG_NAME_AUTH_ACK "auth_ack" #define __EL_MSGLINK_MSG_NAME_EVT_SUB "evt_sub" @@ -40,6 +41,7 @@ namespace el::msglink { enum class msg_type_t { + PONG, AUTH, AUTH_ACK, EVENT_SUB, @@ -64,6 +66,9 @@ namespace el::msglink { using enum msg_type_t; + case PONG: + return __EL_MSGLINK_MSG_NAME_PONG; + break; case AUTH: return __EL_MSGLINK_MSG_NAME_AUTH; break; @@ -122,7 +127,9 @@ namespace el::msglink { using enum msg_type_t; - if (_msg_type_name == __EL_MSGLINK_MSG_NAME_AUTH) + if (_msg_type_name == __EL_MSGLINK_MSG_NAME_PONG) + return PONG; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_AUTH) return AUTH; else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_AUTH_ACK) return AUTH_ACK; diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index f267ce4..0b99a7f 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -80,6 +80,9 @@ namespace el::msglink // set as soon as the login_ack has been sent and received soflag authentication_done; + // flag identifying if pong messages need to be sent out + bool pong_messages_required = false; + // set of all possible outgoing events that are defined (including bidirectional ones) std::set available_outgoing_events; // set of all outgoing events that the other party has subscribed to and therefore need to be transmitted @@ -217,6 +220,15 @@ namespace el::msglink return _el_msglink_get_link_version(); } + /** + * @brief sends a pong message + */ + void send_pong_message() + { + msg_pong_t msg; + interface.send_message(msg); + } + /** * @brief sends an event subscribe message for a specific event * @@ -258,11 +270,10 @@ namespace el::msglink * @brief handles incoming messages (already parsed) before authentication is complete * to perform the authentication. * - * @param _jmsg parsed message + * @param _jmsg parsed json message */ void handle_message_pre_auth( const msg_type_t _msg_type, - const int transaction_id, const nlohmann::json &_jmsg ) { @@ -305,6 +316,10 @@ namespace el::msglink "Remote party does not satisfy the event requirements (missing events)" ); + // check if pong messages are required + if (msg.no_ping.has_value()) + pong_messages_required = *msg.no_ping; + // TODO: remove EL_LOGD("no_ping=%s", !msg.no_ping ? "nullptr" : (msg.no_ping.value() ? "true" : "false")); @@ -369,7 +384,6 @@ namespace el::msglink */ void handle_message_post_auth( const msg_type_t _msg_type, - const int transaction_id, const nlohmann::json &_jmsg ) { @@ -887,6 +901,10 @@ namespace el::msglink */ virtual void define() noexcept = 0; + /** + * @brief called by link interface when the connection has been + * established and communication can begin. + */ void on_connection_established() { EL_LOGD("connection established called"); @@ -907,21 +925,32 @@ namespace el::msglink interface.send_message(msg); } + /** + * @brief called by link interface when an incoming message has been received. + * + * @param _msg_content message data + */ void on_message(const std::string &_msg_content) { try { nlohmann::json jmsg = nlohmann::json::parse(_msg_content); - // read message type and transaction ID (always present) + // read message type (always present) std::string msg_type = jmsg.at("type"); - int transaction_id = jmsg.at("tid"); + // if we received a pong message, ignore it but give warning. + // This should never happen as the msglink C++ client doesn't need request this. + // And in case we are a server, we shouldn't be getting it in the first place + if (msg_type == __EL_MSGLINK_MSG_NAME_PONG) + { + EL_LOGW("Received msglink PONG message even though msglink C++ client's don't require it and/or we are a server."); + return; + } if (authentication_done) { handle_message_post_auth( msg_type_from_string(msg_type), - transaction_id, jmsg ); } @@ -929,7 +958,6 @@ namespace el::msglink { handle_message_pre_auth( msg_type_from_string(msg_type), - transaction_id, jmsg ); } @@ -945,6 +973,17 @@ namespace el::msglink } } + /** + * @brief called by link interface of server when WS pong has been + * received. + * Causes pong message to be transmitted if required. + */ + void on_pong_received() + { + if (pong_messages_required) + send_pong_message(); + } + }; } // namespace el::msglink diff --git a/include/el/msglink/msglink_protocol.md b/include/el/msglink/msglink_protocol.md index be0bab8..41a7df0 100644 --- a/include/el/msglink/msglink_protocol.md +++ b/include/el/msglink/msglink_protocol.md @@ -131,7 +131,7 @@ For now, msglink uses json to encode protocol data and user data in websocket me Working messages are just the "normal" messages sent back and forth while the connection is open. -Every message has 2 base properties: +Most messages have 2 base properties: ```json { @@ -143,9 +143,9 @@ Every message has 2 base properties: The **```type```** property defines the purpose of the message. There are the following message types: +- pong - auth - auth_ack -- pong - evt_sub - evt_unsub - evt_emit @@ -159,7 +159,7 @@ The **```type```** property defines the purpose of the message. There are the fo - rpc_err - rpc_result -The **```tid```** property is the transaction ID. The transaction ID is a signed integer number which (within a single session) uniquely identifies the transaction the message belongs to. +The **```tid```** property is the transaction ID. The transaction ID is a signed integer number which (within a single session) uniquely identifies the transaction the message belongs to. The "pong" message type doesn't have this transaction ID. > A transaction is a (from the perspective of the protocol implementation) complete interaction between the two communication parties. It could be an event, a data subscription or an RPC.
This is a scheme used by many networking protocols and is required for the communication parties to know what messages belong together when a single transaction requires multiple back-and-forth messages like during an RPC. This is one of those tedious repetitive things that would otherwise need to be reimplemented for every command-response event pair if it was implemented manually using only events. @@ -173,7 +173,7 @@ Messages can have other properties specific to the message type. ### Closing message -When closing the msglink and therefore websocket connection, custom close codes and reasons are used. The following table describes the possible codes and their meaning: +When closing the msglink and therefore websocket connection, custom websocket close codes and reasons are used. The following table describes the possible codes and their meaning: | Code | Meaning | Notes | |---|---|---| @@ -185,6 +185,26 @@ When closing the msglink and therefore websocket connection, custom close codes | 3005 | RPC requirement(s) unsatisfied | | +## Heartbeat and Pong message + +By default, TCP sockets and also websockets don't detect unplanned connection loss. When a connection is established, communication parties don't know if the connection is still alive unless they attempt to exchange any data. For this reason, the WebSocket protocol defines specific ping-pong functionality. A WebSocket party can send a ping message with some optional data to which the other party shall respond with a pong message, containing the same data. (This is a special message type defined by WebSocket, which is on a lower level than msglink messages. This has nothing to do with msglink messages.) If the response is not received in a certain time period, the connection is declared dead. + +In msglink, the server is responsible for sending pings. This is because most server libraries support this feature, were as some clients, such as the browser WebSocket API have now way of detecting or interacting with ping-pong messages. +As soon as the msglink connection is established, the server starts to send pings to the client in regular intervals. If a response (WS pong) is not received in time, the connection is terminated on the server side. + +In case of a broke connection however, the client will not be aware of this termination. For this reason, the client listens to the ping messages received (to which it responds with pong, likely implemented by underlying WebSocket library already) and terminates the connection if no ping message is received in a specific time period. + +If a client doesn't support the detection of ping interactions, it has to set the **```no_ping```** flag during authentication (as described [here](#authentication-procedure)). In this case, the server will send a msglink pong message (which is a regular WebSocket communication message, not a control message like pings and pongs) every time it receives a pong from the client. This poses some overhead, but less than would be caused by replacing WS ping and pong message with custom msglink messages entirely. + +```json +{ + "type": "pong" +} +``` + +The pong message does not contain any transaction ID or other additional data. This message is only ever sent from server to client. If a client sends such a message to the server, it has no effect. + + ## Authentication procedure When a msglink client first connects to the msglink server both parties send an initial JSON encoded authentication message to the other party containing the following information: @@ -238,26 +258,6 @@ Only after both parties' authentication transactions have been successfully comp - having sent the auth_ack message in response to the other's auth message - having received the auth_ack message in response to it's own auth message -## Heartbeat and Pong message - -By default, TCP sockets and also websockets don't detect unplanned connection loss. When a connection is established, communication parties don't know if the connection is still alive unless they attempt to exchange any data. For this reason, the WebSocket protocol defines specific ping-pong functionality. A WebSocket party can send a ping message with some optional data (This is a special message type defined by WebSocket, which is on a lower level than msglink messages. This has nothing to do with msglink messages) to which the other party shall respond with a pong message, containing the same data. If the response is not received in a certain time period, the connection is declared dead. - -In msglink, the server is responsible for sending pings. This is mostly because most server libraries support this feature, were as some clients, such as the browser WebSocket API have now way of detecting or interacting with ping-pong messages. -As soon as the msglink connection is established, the server starts to send pings to the client in regular intervals. If a response (WS pong) is not received in time, the connection is terminated on the server side. - -In case of a broke connection however, the client will not be aware of this termination. For this reason, the client listens to the ping messages received (to which it responds with pong, likely implemented by underlying WebSocket library already) and terminates the connection if no ping message is received in a specific time period. - -If a client doesn't support the detection of ping interactions, it has to set the **```no_ping```** flag during authentication (as described [here](#authentication-procedure)). In this case, the server will send a msglink pong message (which is a regular WebSocket communication message, not a control message like pings and pongs) every time it receives a pong from the client. This poses some overhead, but less than would be caused by replacing WS ping and pong message with custom msglink messages entirely. - -```json -{ - "type": "pong", - "tid": ..., // new transaction ID -} -``` - -The pong message does not contain any additional data. This message is only ever sent from server to client. If a client sends such a message to the server, it has no effect. - ## Event messages diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index f2ea030..673c030 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -275,6 +275,9 @@ namespace el::msglink { // pong arrived in time, all good, connection alive + // notify link, so pong message can be sent to client if needed + m_link.on_pong_received(); + // schedule a new ping to be sent a bit later. schedule_ping(); } From fe4f721f4164046ceae77b41a060dfb51c1959d5 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 21 Jan 2024 20:01:55 +0100 Subject: [PATCH 35/50] added RAII controlled subscription handle --- include/el/msglink/link.hpp | 22 +++++---- include/el/msglink/subscriptions.hpp | 67 +++++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index 0b99a7f..4ba7188 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -490,11 +490,11 @@ namespace el::msglink * map and subscribes the event from the other party * if it isn't already. */ - std::shared_ptr add_event_subscription( + subscription_hdl_ptr add_event_subscription( const std::string &_event_name, event_subscription::handler_function_t _handler_function ) { - std::string event_name = _event_name; + std::string event_name = _event_name; // copy for lambda capture // create subscription object const sub_id_t sub_id = generate_new_sub_id(); auto subscription = std::shared_ptr(new event_subscription( @@ -502,8 +502,8 @@ namespace el::msglink [this, _event_name, sub_id](void) // cancel function { EL_LOGD("cancel event %s:%d", _event_name.c_str(), sub_id); - // create copy of name and ID because lambda and it's captures may be destroyed - // during the below function call + // create copy of name and ID because this lambda and it's captures + // may be destroyed during the below function call std::string l_event_name = _event_name; sub_id_t l_sub_id = sub_id; this->remove_event_subscription(l_event_name, l_sub_id); @@ -533,7 +533,11 @@ namespace el::msglink send_event_subscribe_message(_event_name); exit: - return subscription; + return subscription_hdl_ptr( + new subscription_hdl( + subscription + ) + ); } /** @@ -664,7 +668,7 @@ namespace el::msglink * @param _listener the handler method for the event */ template _LT> - std::shared_ptr define_event( + event_sub_hdl_ptr define_event( void (_LT:: *_listener)(_ET &) ) { // save name and handler function @@ -708,7 +712,7 @@ namespace el::msglink * @param _listener the handler function for the event */ template - std::shared_ptr define_event( + event_sub_hdl_ptr define_event( void (*_listener)(_ET &) ) { // save name and handler function @@ -769,7 +773,7 @@ namespace el::msglink * @param _listener the handler method for the event */ template _LT> - std::shared_ptr define_event( + event_sub_hdl_ptr define_event( void (_LT:: *_listener)(_ET &) ) { // save name and handler function @@ -811,7 +815,7 @@ namespace el::msglink * @param _listener the handler function for the event */ template - std::shared_ptr define_event( + event_sub_hdl_ptr define_event( void (*_listener)(_ET &) ) { // save name and handler function diff --git a/include/el/msglink/subscriptions.hpp b/include/el/msglink/subscriptions.hpp index d281166..369851e 100644 --- a/include/el/msglink/subscriptions.hpp +++ b/include/el/msglink/subscriptions.hpp @@ -29,7 +29,7 @@ namespace el::msglink * is used to identify and cancel the subscription later. * */ - struct event_subscription + class event_subscription { protected: friend class link; @@ -95,4 +95,69 @@ namespace el::msglink invalidate(); } }; + + /** + * @brief a class which handles subscription lifetime using + * RAII functionality. + * When a subscription is returned from the msglink library + * to user code, it is wrapped by a subscription handle. The handle + * itself is wrapped by a shared_ptr, so there is only one handle + * per subscription. + * The subscription is automatically canceled when the lifetime of + * the subscription handle ends, i.e. when no nobody holds a reference + * to it anymore. + * One can also cancel the subscription manually before this happens using + * the cancel() method. + * + * This is to prevent callbacks to class instances which don't exist anymore + * when forgetting to cancel subscriptions in class destructors. + */ + template + class subscription_hdl + { + friend class link; + + protected: + + // the managed subscription + std::shared_ptr<_SUB_T> subscription_ptr; + + // no copy or move + subscription_hdl(const subscription_hdl &) = delete; + subscription_hdl(subscription_hdl &&) = delete; + // only link is allowed to construct instance with valid subscription pointer + subscription_hdl(std::shared_ptr<_SUB_T> _sub_ptr) + : subscription_ptr(_sub_ptr) + { + EL_LOG_FUNCTION_CALL();} + + public: + ~subscription_hdl() + { + EL_LOG_FUNCTION_CALL(); + if (subscription_ptr != nullptr) + subscription_ptr->cancel(); + } + + /** + * @brief cancels the subscription even if there + * are still references to the subscription_hdl. + * Usually, this is not required. + */ + void cancel() + { + if (subscription_ptr != nullptr) + subscription_ptr->cancel(); + } + }; + + // shortcut for shared pointer to subscription handle + template + using subscription_hdl_ptr = std::shared_ptr>; + + /** + * @brief shortcut for shared_ptr to subscription_hdl for event_subscription. + * This type is library user-facing, so this is a short alias. + */ + using event_sub_hdl_ptr = subscription_hdl_ptr; } // namespace el::msglink From 617bb031c7bd5675707de613e7172d59c043aed7 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 21 Jan 2024 21:33:46 +0100 Subject: [PATCH 36/50] attempted to improve event macro mismatch errors by using static asserts, but failed --- include/el/msglink/event.hpp | 72 ++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/include/el/msglink/event.hpp b/include/el/msglink/event.hpp index dbe9a36..489a8be 100644 --- a/include/el/msglink/event.hpp +++ b/include/el/msglink/event.hpp @@ -14,6 +14,7 @@ msglink event class used to define custom events #pragma once #include +#include #include "../codable.hpp" #include "../cxxversions.h" @@ -23,7 +24,6 @@ msglink event class used to define custom events namespace el::msglink { - /** * @brief base class for all incoming msglink event * definition classes. To create an incoming event define a class inheriting from this one. @@ -40,16 +40,9 @@ namespace el::msglink // dummy method which must be overriden to ensure the correct // generate macro is used. virtual void _el_msglink_is_incoming_dummy() const noexcept = 0; - }; - -// (public) generates the necessary boilerplate code for an incoming event class. -// The members listed in the arguments will be made decodable using el::decodable -// and are part of the event's data. -#define EL_MSGLINK_DEFINE_INCOMING_EVENT(TypeName, ...) \ - static inline const char *_event_name = #TypeName; \ - virtual void _el_msglink_is_incoming_dummy() const noexcept override {} \ - EL_DEFINE_DECODABLE(TypeName, __VA_ARGS__) + bool __isincoming; + }; /** * @brief base class for all outgoing msglink event @@ -69,15 +62,6 @@ namespace el::msglink virtual void _el_msglink_is_outgoing_dummy() const noexcept = 0; }; -// (public) generates the necessary boilerplate code for an outgoing event class. -// The members listed in the arguments will be made encodable using el::encodable -// and are part of the event's data. -#define EL_MSGLINK_DEFINE_OUTGOING_EVENT(TypeName, ...) \ - static inline const char *_event_name = #TypeName; \ - virtual void _el_msglink_is_outgoing_dummy() const noexcept override {} \ - EL_DEFINE_ENCODABLE(TypeName, __VA_ARGS__) - - /** * @brief shortcut base class for all bidirectional (incoming and outgoing) msglink event * definition classes. It is simply a composite class inheriting form outgoing_event @@ -91,15 +75,6 @@ namespace el::msglink virtual ~bidirectional_event() = default; }; -// (public) generates the necessary boilerplate code for an event class. -// The members listed in the arguments will be made codable using el::codable -// and are part of the event's data. -#define EL_MSGLINK_DEFINE_BIDIRECTIONAL_EVENT(TypeName, ...) \ - static inline const char *_event_name = #TypeName; \ - virtual void _el_msglink_is_incoming_dummy() const noexcept override {} \ - virtual void _el_msglink_is_outgoing_dummy() const noexcept override {} \ - EL_DEFINE_CODABLE(TypeName, __VA_ARGS__) - /** * The following concepts define constraints that allow targeting specific @@ -152,5 +127,46 @@ namespace el::msglink concept AnyEvent = std::derived_from<_ET, incoming_event> || std::derived_from<_ET, outgoing_event>; +// (public) generates the necessary boilerplate code for an incoming event class. +// The members listed in the arguments will be made decodable using el::decodable +// and are part of the event's data. +#define EL_MSGLINK_DEFINE_INCOMING_EVENT(TypeName, ...) \ + static inline const char *_event_name = #TypeName; \ + template\ + struct __check_base\ + {\ + static_assert( \ + std::is_function, \ + "EL_MSGLINK_DEFINE_INCOMING_EVENT macro may only be used for incoming events" \ + ); \ + };\ + EL_DEFINE_DECODABLE(TypeName, __VA_ARGS__)\ + __check_base __check; + + +// (public) generates the necessary boilerplate code for an outgoing event class. +// The members listed in the arguments will be made encodable using el::encodable +// and are part of the event's data. +#define EL_MSGLINK_DEFINE_OUTGOING_EVENT(TypeName, ...) \ + static inline const char *_event_name = #TypeName; \ + static_assert( \ + ::el::msglink::OutgoingOnlyEvent, \ + "EL_MSGLINK_DEFINE_OUTGOING_EVENT macro may only be used for outgoing events" \ + ); \ + EL_DEFINE_ENCODABLE(TypeName, __VA_ARGS__) + + +// (public) generates the necessary boilerplate code for an event class. +// The members listed in the arguments will be made codable using el::codable +// and are part of the event's data. +#define EL_MSGLINK_DEFINE_BIDIRECTIONAL_EVENT(TypeName, ...) \ + static inline const char *_event_name = #TypeName; \ + static_assert( \ + ::el::msglink::BidirectionalEvent, \ + "EL_MSGLINK_DEFINE_BIDIRECTIONAL_EVENT macro may only be used for bidirectional events" \ + ); \ + EL_DEFINE_CODABLE(TypeName, __VA_ARGS__) + + } // namespace el::msglink #endif // __EL_ENABLE_CXX20 \ No newline at end of file From d16f093fd8208e04590dcecd94e61eadc645f0d7 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 21 Jan 2024 22:20:44 +0100 Subject: [PATCH 37/50] undid attempted event improvements and started working on procedures. procedure classes done for now --- include/el/codable.hpp | 58 +++++++++++ include/el/msglink/event.hpp | 52 ++++----- include/el/msglink/procedure.hpp | 174 +++++++++++++++++++++++++++++++ 3 files changed, 253 insertions(+), 31 deletions(-) create mode 100644 include/el/msglink/procedure.hpp diff --git a/include/el/codable.hpp b/include/el/codable.hpp index 8028d4a..7c07996 100644 --- a/include/el/codable.hpp +++ b/include/el/codable.hpp @@ -19,8 +19,13 @@ and depends on it. It must be includable as follows: #pragma once +#include "cxxversions.h" + #include #include +#ifdef __EL_ENABLE_CXX20 +#include +#endif #include "metaprog.hpp" #include "codable_types.hpp" @@ -120,6 +125,59 @@ namespace el }; +#ifdef __EL_ENABLE_CXX20 +/** + * The following concepts define constraints that allow targeting specific + * kinds of codables such as a class that is either ONLY an encodable + * or ONLY a decodable or both. + */ + + /** + * @brief Constrains _T to be ONLY derived from encodable + * and NOT from decodable + */ + template + concept EncodableOnly = std::derived_from<_T, encodable> && !std::derived_from<_T, decodable>; + + /** + * @brief Constrains _T to be at derived at least from encodable + * (but can additionally also derive from decodable) + */ + template + concept AtLeaseEncodable = std::derived_from<_T, encodable>; + + /** + * @brief Constrains _T to be ONLY derived from decodable + * and NOT from encodable + */ + template + concept DecodableOnly = std::derived_from<_T, decodable> && !std::derived_from<_T, encodable>; + + /** + * @brief Constrains _T to be at derived at least from decodable + * (but can additionally also derive from encodable) + */ + template + concept AtLeastDecodable = std::derived_from<_T, decodable>; + + /** + * @brief Constrains _T to be derived BOTH from encodable + * and from decodable, making it a full codable + */ + template + concept FullCodable = std::derived_from<_T, encodable> && std::derived_from<_T, decodable>; + + /** + * @brief Constrains _T to be derived from encodable + * and/or decodable. This constrains a type to be any + * sort of codable. + */ + template + concept AnyCodable = std::derived_from<_T, encodable> || std::derived_from<_T, decodable>; + +#endif // __EL_ENABLE_CXX20 + + /** * Automatic constructor generation * diff --git a/include/el/msglink/event.hpp b/include/el/msglink/event.hpp index 489a8be..e6518b3 100644 --- a/include/el/msglink/event.hpp +++ b/include/el/msglink/event.hpp @@ -13,14 +13,15 @@ msglink event class used to define custom events #pragma once +#include "../cxxversions.h" + +#ifdef __EL_ENABLE_CXX20 #include -#include +#endif #include "../codable.hpp" -#include "../cxxversions.h" -#ifdef __EL_ENABLE_CXX20 namespace el::msglink { @@ -39,7 +40,7 @@ namespace el::msglink // dummy method which must be overriden to ensure the correct // generate macro is used. - virtual void _el_msglink_is_incoming_dummy() const noexcept = 0; + virtual void _el_msglink_is_incoming_event_dummy() const noexcept = 0; bool __isincoming; }; @@ -59,7 +60,7 @@ namespace el::msglink // dummy method which must be overriden to ensure the correct // generate macro is used. - virtual void _el_msglink_is_outgoing_dummy() const noexcept = 0; + virtual void _el_msglink_is_outgoing_event_dummy() const noexcept = 0; }; /** @@ -76,6 +77,7 @@ namespace el::msglink }; +#ifdef __EL_ENABLE_CXX20 /** * The following concepts define constraints that allow targeting specific * kinds of events such as an event class that is either ONLY an incoming @@ -125,48 +127,36 @@ namespace el::msglink */ template concept AnyEvent = std::derived_from<_ET, incoming_event> || std::derived_from<_ET, outgoing_event>; + +#endif // __EL_ENABLE_CXX20 // (public) generates the necessary boilerplate code for an incoming event class. // The members listed in the arguments will be made decodable using el::decodable // and are part of the event's data. -#define EL_MSGLINK_DEFINE_INCOMING_EVENT(TypeName, ...) \ - static inline const char *_event_name = #TypeName; \ - template\ - struct __check_base\ - {\ - static_assert( \ - std::is_function, \ - "EL_MSGLINK_DEFINE_INCOMING_EVENT macro may only be used for incoming events" \ - ); \ - };\ - EL_DEFINE_DECODABLE(TypeName, __VA_ARGS__)\ - __check_base __check; +#define EL_MSGLINK_DEFINE_INCOMING_EVENT(TypeName, ...) \ + static inline const char *_event_name = #TypeName; \ + virtual void _el_msglink_is_incoming_event_dummy() const noexcept override {} \ + EL_DEFINE_DECODABLE(TypeName, __VA_ARGS__) // (public) generates the necessary boilerplate code for an outgoing event class. // The members listed in the arguments will be made encodable using el::encodable // and are part of the event's data. -#define EL_MSGLINK_DEFINE_OUTGOING_EVENT(TypeName, ...) \ - static inline const char *_event_name = #TypeName; \ - static_assert( \ - ::el::msglink::OutgoingOnlyEvent, \ - "EL_MSGLINK_DEFINE_OUTGOING_EVENT macro may only be used for outgoing events" \ - ); \ +#define EL_MSGLINK_DEFINE_OUTGOING_EVENT(TypeName, ...) \ + static inline const char *_event_name = #TypeName; \ + virtual void _el_msglink_is_outgoing_event_dummy() const noexcept override {} \ EL_DEFINE_ENCODABLE(TypeName, __VA_ARGS__) // (public) generates the necessary boilerplate code for an event class. // The members listed in the arguments will be made codable using el::codable // and are part of the event's data. -#define EL_MSGLINK_DEFINE_BIDIRECTIONAL_EVENT(TypeName, ...) \ - static inline const char *_event_name = #TypeName; \ - static_assert( \ - ::el::msglink::BidirectionalEvent, \ - "EL_MSGLINK_DEFINE_BIDIRECTIONAL_EVENT macro may only be used for bidirectional events" \ - ); \ +#define EL_MSGLINK_DEFINE_BIDIRECTIONAL_EVENT(TypeName, ...) \ + static inline const char *_event_name = #TypeName; \ + virtual void _el_msglink_is_incoming_event_dummy() const noexcept override {} \ + virtual void _el_msglink_is_outgoing_event_dummy() const noexcept override {} \ EL_DEFINE_CODABLE(TypeName, __VA_ARGS__) -} // namespace el::msglink -#endif // __EL_ENABLE_CXX20 \ No newline at end of file +} // namespace el::msglink \ No newline at end of file diff --git a/include/el/msglink/procedure.hpp b/include/el/msglink/procedure.hpp new file mode 100644 index 0000000..bc6a627 --- /dev/null +++ b/include/el/msglink/procedure.hpp @@ -0,0 +1,174 @@ +/* +ELEKTRON © 2024 - now +Written by melektron +www.elektron.work +21.01.24, 20:10 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +msglink procedure class used to define custom procedures +*/ + +#pragma once + +#include "../cxxversions.h" + +#include +#ifdef __EL_ENABLE_CXX20 +#include +#endif + +#include "../codable.hpp" + + +namespace el::msglink +{ + + + /** + * @brief base class for all incoming msglink procedure + * definition classes. To create an incoming procedure define a class inheriting from this one. + * Then use the EL_MSGLINK_DEFINE_INCOMING_PROCEDURE macro to generate the required boilerplate. + * Incoming procedures must additionally have one structure called "parameters" which must be an el::decodable + * and one structure called "results" which must be an el::encodable. They can be defined with + * default codable definition macros. + */ + struct incoming_procedure + { + // dummy method which must be overriden to ensure the correct + // generate macro is used. + virtual void _el_msglink_is_incoming_proc_dummy() const noexcept = 0; + }; + + /** + * @brief base class for all outgoing msglink procedure + * definition classes. To create an outgoing procedure define a class inheriting from this one. + * Then use the EL_MSGLINK_DEFINE_OUTGOING_PROCEDURE macro to generate the required boilerplate. + * Outgoing procedures must additionally have one structure called "parameters" which must be an el::encodable + * and one structure called "results" which must be an el::decodable. They can be defined with + * default codable definition macros. + */ + struct outgoing_procedure : public el::encodable + { + // dummy method which must be overriden to ensure the correct + // generate macro is used. + virtual void _el_msglink_is_outgoing_proc_dummy() const noexcept = 0; + }; + + /** + * @brief shortcut base class for all bidirectional (incoming and outgoing) msglink procedure + * definition classes. It is simply a composite class inheriting form outgoing_procedure + * and incoming_procedure to save you the hassle of having to do that manually. + * Derived classes must satisfy all the requirements of incoming and outgoing procedures. + * To create a bidirectional procedure define a class inheriting from this one. + * Then use the EL_MSGLINK_DEFINE_BIDIRECTIONAL_PROCEDURE macro to generate the required boilerplate. + */ + struct bidirectional_procedure : public incoming_procedure, public outgoing_procedure + {}; + + +#ifdef __EL_ENABLE_CXX20 + /** + * The following concepts define constraints that allow targeting specific + * kinds of procedures such as an procedure class that is either ONLY an incoming + * procedure or ONLY an outgoing procedure or a bidirectional procedure (BOTH incoming + * and outgoing) + */ + + /** + * @brief Constrains _ET to be ONLY derived from incoming_procedure + * and NOT from outgoing_procedure + */ + template + concept IncomingOnlyProcedure = std::derived_from<_ET, incoming_procedure> && !std::derived_from<_ET, outgoing_procedure>; + + /** + * @brief Constrains _ET to be at derived at least from incoming_procedure + * (but can additionally also derive from outgoing_procedure) + */ + template + concept AtLeastIncomingProcedure = std::derived_from<_ET, incoming_procedure>; + + /** + * @brief Constrains _ET to be ONLY derived from outgoing_procedure + * and NOT from incoming_procedure + */ + template + concept OutgoingOnlyProcedure = std::derived_from<_ET, outgoing_procedure> && !std::derived_from<_ET, incoming_procedure>; + + /** + * @brief Constrains _ET to be at derived at least from outgoing_procedure + * (but can additionally also derive from incoming_procedure) + */ + template + concept AtLeastOutgoingProcedure = std::derived_from<_ET, outgoing_procedure>; + + /** + * @brief Constrains _ET to be derived BOTH from incoming_procedure + * and from outgoing_procedure, making it a bidirectional procedure + */ + template + concept BidirectionalProcedure = std::derived_from<_ET, incoming_procedure> && std::derived_from<_ET, outgoing_procedure>; + + /** + * @brief Constrains _ET to be derived from incoming_procedure + * and or outgoing_procedure. This constrains a type to be any + * sort of procedure. + */ + template + concept AnyProcedure = std::derived_from<_ET, incoming_procedure> || std::derived_from<_ET, outgoing_procedure>; + +#endif // __EL_ENABLE_CXX20 + + +// (public) generates the necessary boilerplate code for an incoming procedure class. +// The first and only argument is the structure type itself which is used to get the name. +#define EL_MSGLINK_DEFINE_INCOMING_PROCEDURE(TypeName, ...) \ + static inline const char *_procedure_name = #TypeName; \ + virtual void _el_msglink_is_incoming_proc_dummy() const noexcept override {} \ + static_assert( \ + std::is_base_of<::el::decodable, parameters>::value, \ + "incoming procedure parameters type must be decodable" \ + ); \ + static_assert( \ + std::is_base_of<::el::encodable, results>::value, \ + "incoming procedure results type must be encodable" \ + ); + + +// (public) generates the necessary boilerplate code for an outgoing procedure class. +// The first and only argument is the structure type itself which is used to get the name. +#define EL_MSGLINK_DEFINE_OUTGOING_PROCEDURE(TypeName, ...) \ + static inline const char *_procedure_name = #TypeName; \ + virtual void _el_msglink_is_outgoing_proc_dummy() const noexcept override {} \ + static_assert( \ + std::is_base_of<::el::encodable, parameters>::value, \ + "incoming procedure parameters type must be encodable" \ + ); \ + static_assert( \ + std::is_base_of<::el::decodable, results>::value, \ + "incoming procedure results type must be decodable" \ + ); + + +// (public) generates the necessary boilerplate code for a procedure class. +// The first and only argument is the structure type itself which is used to get the name. +#define EL_MSGLINK_DEFINE_BIDIRECTIONAL_PROCEDURE(TypeName, ...) \ + static inline const char *_procedure_name = #TypeName; \ + virtual void _el_msglink_is_incoming_proc_dummy() const noexcept override {} \ + virtual void _el_msglink_is_outgoing_proc_dummy() const noexcept override {} \ + static_assert( \ + std::is_base_of<::el::decodable, parameters>::value && \ + std::is_base_of<::el::encodable, parameters>::value, \ + "bidirectional procedure parameters type must be en- and decodable" \ + ); \ + static_assert( \ + std::is_base_of<::el::decodable, results>::value && \ + std::is_base_of<::el::encodable, results>::value, \ + "bidirectional procedure results type must be en- and decodable" \ + ); + + +} // namespace el::msglink \ No newline at end of file From a66ee5c923689a5331dfcb15a26fa7fe2b0bad84 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 28 Jan 2024 01:23:42 +0100 Subject: [PATCH 38/50] got function call to work in most simple form (no error handling jet) --- include/el/msglink/function.hpp | 174 +++++++++++++++++ include/el/msglink/internal/messages.hpp | 52 +++++- include/el/msglink/internal/msgtype.hpp | 43 ++--- include/el/msglink/internal/transaction.hpp | 2 +- include/el/msglink/internal/ws_close_code.hpp | 2 +- include/el/msglink/link.hpp | 176 ++++++++++++++++-- include/el/msglink/msglink_protocol.md | 37 ++-- include/el/msglink/procedure.hpp | 174 ----------------- 8 files changed, 422 insertions(+), 238 deletions(-) create mode 100644 include/el/msglink/function.hpp delete mode 100644 include/el/msglink/procedure.hpp diff --git a/include/el/msglink/function.hpp b/include/el/msglink/function.hpp new file mode 100644 index 0000000..705782c --- /dev/null +++ b/include/el/msglink/function.hpp @@ -0,0 +1,174 @@ +/* +ELEKTRON © 2024 - now +Written by melektron +www.elektron.work +21.01.24, 20:10 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +msglink function class used to define custom remote functions +*/ + +#pragma once + +#include "../cxxversions.h" + +#include +#ifdef __EL_ENABLE_CXX20 +#include +#endif + +#include "../codable.hpp" + + +namespace el::msglink +{ + + + /** + * @brief base class for all incoming msglink function + * definition classes. To create an incoming function define a class inheriting from this one. + * Then use the EL_MSGLINK_DEFINE_INCOMING_FUNCTION macro to generate the required boilerplate. + * Incoming functions must additionally have one structure called "parameters_t" which must be an el::decodable + * and one structure called "results_t" which must be an el::encodable. They can be defined with + * default codable definition macros. + */ + struct incoming_function + { + // dummy method which must be overriden to ensure the correct + // generate macro is used. + virtual void _el_msglink_is_incoming_proc_dummy() const noexcept = 0; + }; + + /** + * @brief base class for all outgoing msglink function + * definition classes. To create an outgoing function define a class inheriting from this one. + * Then use the EL_MSGLINK_DEFINE_OUTGOING_FUNCTION macro to generate the required boilerplate. + * Outgoing functions must additionally have one structure called "parameters_t" which must be an el::encodable + * and one structure called "results_t" which must be an el::decodable. They can be defined with + * default codable definition macros. + */ + struct outgoing_function + { + // dummy method which must be overriden to ensure the correct + // generate macro is used. + virtual void _el_msglink_is_outgoing_proc_dummy() const noexcept = 0; + }; + + /** + * @brief shortcut base class for all bidirectional (incoming and outgoing) msglink function + * definition classes. It is simply a composite class inheriting form outgoing_function + * and incoming_function to save you the hassle of having to do that manually. + * Derived classes must satisfy all the requirements of incoming and outgoing functions. + * To create a bidirectional function define a class inheriting from this one. + * Then use the EL_MSGLINK_DEFINE_BIDIRECTIONAL_FUNCTION macro to generate the required boilerplate. + */ + struct bidirectional_function : public incoming_function, public outgoing_function + {}; + + +#ifdef __EL_ENABLE_CXX20 + /** + * The following concepts define constraints that allow targeting specific + * kinds of functions such as an function class that is either ONLY an incoming + * function or ONLY an outgoing function or a bidirectional function (BOTH incoming + * and outgoing) + */ + + /** + * @brief Constrains _ET to be ONLY derived from incoming_function + * and NOT from outgoing_function + */ + template + concept IncomingOnlyFunction = std::derived_from<_ET, incoming_function> && !std::derived_from<_ET, outgoing_function>; + + /** + * @brief Constrains _ET to be at derived at least from incoming_function + * (but can additionally also derive from outgoing_function) + */ + template + concept AtLeastIncomingFunction = std::derived_from<_ET, incoming_function>; + + /** + * @brief Constrains _ET to be ONLY derived from outgoing_function + * and NOT from incoming_function + */ + template + concept OutgoingOnlyFunction = std::derived_from<_ET, outgoing_function> && !std::derived_from<_ET, incoming_function>; + + /** + * @brief Constrains _ET to be at derived at least from outgoing_function + * (but can additionally also derive from incoming_function) + */ + template + concept AtLeastOutgoingFunction = std::derived_from<_ET, outgoing_function>; + + /** + * @brief Constrains _ET to be derived BOTH from incoming_function + * and from outgoing_function, making it a bidirectional function + */ + template + concept BidirectionalFunction = std::derived_from<_ET, incoming_function> && std::derived_from<_ET, outgoing_function>; + + /** + * @brief Constrains _ET to be derived from incoming_function + * and or outgoing_function. This constrains a type to be any + * sort of function. + */ + template + concept AnyFunction = std::derived_from<_ET, incoming_function> || std::derived_from<_ET, outgoing_function>; + +#endif // __EL_ENABLE_CXX20 + + +// (public) generates the necessary boilerplate code for an incoming function class. +// The first and only argument is the structure type itself which is used to get the name. +#define EL_MSGLINK_DEFINE_INCOMING_FUNCTION(TypeName, ...) \ + static inline const char *_function_name = #TypeName; \ + virtual void _el_msglink_is_incoming_proc_dummy() const noexcept override {} \ + static_assert( \ + std::is_base_of<::el::decodable, parameters_t>::value, \ + "incoming function parameters type must be decodable" \ + ); \ + static_assert( \ + std::is_base_of<::el::encodable, results_t>::value, \ + "incoming function results type must be encodable" \ + ); + + +// (public) generates the necessary boilerplate code for an outgoing function class. +// The first and only argument is the structure type itself which is used to get the name. +#define EL_MSGLINK_DEFINE_OUTGOING_FUNCTION(TypeName, ...) \ + static inline const char *_function_name = #TypeName; \ + virtual void _el_msglink_is_outgoing_proc_dummy() const noexcept override {} \ + static_assert( \ + std::is_base_of<::el::encodable, parameters_t>::value, \ + "incoming function parameters type must be encodable" \ + ); \ + static_assert( \ + std::is_base_of<::el::decodable, results_t>::value, \ + "incoming function results type must be decodable" \ + ); + + +// (public) generates the necessary boilerplate code for a function class. +// The first and only argument is the structure type itself which is used to get the name. +#define EL_MSGLINK_DEFINE_BIDIRECTIONAL_FUNCTION(TypeName, ...) \ + static inline const char *_function_name = #TypeName; \ + virtual void _el_msglink_is_incoming_proc_dummy() const noexcept override {} \ + virtual void _el_msglink_is_outgoing_proc_dummy() const noexcept override {} \ + static_assert( \ + std::is_base_of<::el::decodable, parameters_t>::value && \ + std::is_base_of<::el::encodable, parameters_t>::value, \ + "bidirectional function parameters type must be en- and decodable" \ + ); \ + static_assert( \ + std::is_base_of<::el::decodable, results_t>::value && \ + std::is_base_of<::el::encodable, results_t>::value, \ + "bidirectional function results type must be en- and decodable" \ + ); + + +} // namespace el::msglink \ No newline at end of file diff --git a/include/el/msglink/internal/messages.hpp b/include/el/msglink/internal/messages.hpp index 7728cf1..a6d81fb 100644 --- a/include/el/msglink/internal/messages.hpp +++ b/include/el/msglink/internal/messages.hpp @@ -53,7 +53,7 @@ namespace el::msglink std::optional no_ping; std::set events; std::set data_sources; - std::set procedures; + std::set functions; EL_DEFINE_CODABLE( msg_auth_t, @@ -64,7 +64,7 @@ namespace el::msglink no_ping, events, data_sources, - procedures + functions ) }; @@ -126,4 +126,52 @@ namespace el::msglink data ) }; + + struct msg_func_call_t + : public base_msg_t + , public codable + { + std::string type = __EL_MSGLINK_MSG_NAME_FUNC_CALL; + std::string name; + nlohmann::json params; + + EL_DEFINE_CODABLE( + msg_func_call_t, + type, + tid, + name, + params + ) + }; + + struct msg_func_err_t + : public base_msg_t + , public codable + { + std::string type = __EL_MSGLINK_MSG_NAME_FUNC_ERR; + std::string info; + + EL_DEFINE_CODABLE( + msg_func_call_t, + type, + tid, + info + ) + }; + + struct msg_func_result_t + : public base_msg_t + , public codable + { + std::string type = __EL_MSGLINK_MSG_NAME_FUNC_RESULT; + nlohmann::json results; + + EL_DEFINE_CODABLE( + msg_func_call_t, + type, + tid, + results + ) + }; + } // namespace el diff --git a/include/el/msglink/internal/msgtype.hpp b/include/el/msglink/internal/msgtype.hpp index 29948de..cbf3df2 100644 --- a/include/el/msglink/internal/msgtype.hpp +++ b/include/el/msglink/internal/msgtype.hpp @@ -31,10 +31,9 @@ Defines all message types possible and conversions from/to string #define __EL_MSGLINK_MSG_NAME_DATA_SUB_NAK "data_sub_nak" #define __EL_MSGLINK_MSG_NAME_DATA_UNSUB "data_unsub" #define __EL_MSGLINK_MSG_NAME_DATA_CHANGE "data_change" -#define __EL_MSGLINK_MSG_NAME_RPC_CALL "rpc_call" -#define __EL_MSGLINK_MSG_NAME_RPC_NAK "rpc_nak" -#define __EL_MSGLINK_MSG_NAME_RPC_ERR "rpc_err" -#define __EL_MSGLINK_MSG_NAME_RPC_RESULT "rpc_result" +#define __EL_MSGLINK_MSG_NAME_FUNC_CALL "func_call" +#define __EL_MSGLINK_MSG_NAME_FUNC_ERR "func_err" +#define __EL_MSGLINK_MSG_NAME_FUNC_RESULT "func_result" namespace el::msglink @@ -54,10 +53,9 @@ namespace el::msglink DATA_SUB_NAK, DATA_UNSUB, DATA_CHANGE, - RPC_CALL, - RPC_NAK, - RPC_ERR, - RPC_RESULT, + FUNC_CALL, + FUNC_ERR, + FUNC_RESULT, }; inline const char *msg_type_to_string(const msg_type_t _msg_type) @@ -105,17 +103,14 @@ namespace el::msglink case DATA_CHANGE: return __EL_MSGLINK_MSG_NAME_DATA_CHANGE; break; - case RPC_CALL: - return __EL_MSGLINK_MSG_NAME_RPC_CALL; + case FUNC_CALL: + return __EL_MSGLINK_MSG_NAME_FUNC_CALL; break; - case RPC_NAK: - return __EL_MSGLINK_MSG_NAME_RPC_NAK; + case FUNC_ERR: + return __EL_MSGLINK_MSG_NAME_FUNC_ERR; break; - case RPC_ERR: - return __EL_MSGLINK_MSG_NAME_RPC_ERR; - break; - case RPC_RESULT: - return __EL_MSGLINK_MSG_NAME_RPC_RESULT; + case FUNC_RESULT: + return __EL_MSGLINK_MSG_NAME_FUNC_RESULT; break; default: @@ -153,14 +148,12 @@ namespace el::msglink return DATA_UNSUB; else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_DATA_CHANGE) return DATA_CHANGE; - else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_RPC_CALL) - return RPC_CALL; - else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_RPC_NAK) - return RPC_NAK; - else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_RPC_ERR) - return RPC_ERR; - else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_RPC_RESULT) - return RPC_RESULT; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_FUNC_CALL) + return FUNC_CALL; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_FUNC_ERR) + return FUNC_ERR; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_FUNC_RESULT) + return FUNC_RESULT; else throw invalid_msg_type_error("Invalid type name: " + _msg_type_name); } diff --git a/include/el/msglink/internal/transaction.hpp b/include/el/msglink/internal/transaction.hpp index 08d470d..9c5cfae 100644 --- a/include/el/msglink/internal/transaction.hpp +++ b/include/el/msglink/internal/transaction.hpp @@ -74,7 +74,7 @@ namespace el::msglink using transaction_t::transaction_t; }; - struct transaction_event_sub_t : public transaction_t + struct transaction_rpc_t : public transaction_t { std::string event_name; diff --git a/include/el/msglink/internal/ws_close_code.hpp b/include/el/msglink/internal/ws_close_code.hpp index bbd89ae..737d170 100644 --- a/include/el/msglink/internal/ws_close_code.hpp +++ b/include/el/msglink/internal/ws_close_code.hpp @@ -22,6 +22,6 @@ namespace el::msglink LINK_VERSION_MISMATCH = 3002, EVENT_REQUIREMENTS_NOT_SATISFIED = 3003, DATA_SOURCE_REQUIREMENTS_NOT_SATISFIED = 3004, - RPC_REQUIREMENTS_NOT_SATISFIED = 3005, + FUNCTION_REQUIREMENTS_NOT_SATISFIED = 3005, }; } // namespace el::msglink diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index 4ba7188..044a77c 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -27,6 +27,7 @@ the user to define the API/protocol of a link #include "../flags.hpp" #include "../rtti_utils.hpp" #include "event.hpp" +#include "function.hpp" #include "errors.hpp" #include "subscriptions.hpp" #include "internal/msgtype.hpp" @@ -105,6 +106,20 @@ namespace el::msglink sub_id_t, std::shared_ptr > event_subscription_ids_to_objects; + + // set of all possible outgoing functions that this party may want to call on the other party + std::set available_outgoing_functions; + // set of all possible incoming functions that may be called by the other party + std::set available_incoming_functions; + + // type of the intermediary handler function + using function_handler_function_t = std::function; + + // map of incoming function names to their handlers + std::unordered_map< + std::string, + function_handler_function_t + > function_names_to_functions; private: // methods @@ -306,7 +321,16 @@ namespace el::msglink msg.link_version ); + // check if pong messages are required + if (msg.no_ping.has_value()) + pong_messages_required = *msg.no_ping; + + // TODO: remove + EL_LOGD("no_ping=%s", !msg.no_ping ? "nullptr" : (msg.no_ping.value() ? "true" : "false")); + // check event list + // all the events I may require (incoming) must be included in the events + // the other party can provide (it's outgoing events) if (!std::includes( msg.events.begin(), msg.events.end(), available_incoming_events.begin(), available_incoming_events.end() @@ -316,16 +340,19 @@ namespace el::msglink "Remote party does not satisfy the event requirements (missing events)" ); - // check if pong messages are required - if (msg.no_ping.has_value()) - pong_messages_required = *msg.no_ping; - - // TODO: remove - EL_LOGD("no_ping=%s", !msg.no_ping ? "nullptr" : (msg.no_ping.value() ? "true" : "false")); - // check data sources - // check procedures + // check functions + // all the functions this party may call (outgoing) must be included in the other + // parties callable (incoming) functions + if (!std::includes( + msg.functions.begin(), msg.functions.end(), + available_outgoing_functions.begin(), available_outgoing_functions.end() + )) + throw incompatible_link_error( + close_code_t::EVENT_REQUIREMENTS_NOT_SATISFIED, + "Remote party does not satisfy the function requirements (missing functions)" + ); // all good, send acknowledgement message, transaction complete msg_auth_ack_t response; @@ -461,13 +488,30 @@ namespace el::msglink break; case DATA_CHANGE: break; - case RPC_CALL: - break; - case RPC_NAK: - break; - case RPC_ERR: + case FUNC_CALL: + { + msg_func_call_t msg(_jmsg); + + if (!available_incoming_functions.contains(msg.name) || !function_names_to_functions.contains(msg.name)) + { + EL_LOGW("Received FUNC_CALL message for a function which isn't incoming and/or doesn't exist. This is likely a library implementation issue and should not happen."); + break; + } + + // run the handler + nlohmann::json results_object = function_names_to_functions.at(msg.name)(msg.params); + // TODO: catch exceptions and return error message in case of one + + // send result + msg_func_result_t response; + response.tid = msg.tid; + response.results = results_object; + interface.send_message(response); + } + break; + case FUNC_ERR: break; - case RPC_RESULT: + case FUNC_RESULT: break; default: @@ -855,6 +899,106 @@ namespace el::msglink available_outgoing_events.insert(event_name); } + + /** + * == Functions == + * + */ + + + /** + * @brief Shortcut for defining an incoming only function + * with a function that is a method of the link. + * + * The function must be a method + * of the link it is registered on. This is a shortcut + * to avoid having to use std::bind to bind the function + * to the instance. When an external function is needed, this + * is the wrong overload. + * + * @note Method function pointer: + * https://isocpp.org/wiki/faq/pointers-to-members#typedef-for-ptr-to-memfn + * + * @tparam _FT the function class of the function to register + * (must inherit from el::msglink::incoming_function, can be deduced from method parameter) + * @tparam _LT the link class the handler function is a method of (can also be deduced) + * @param _handler the method containing the function code + */ + template _LT> + void define_function( + typename _FT::results_t (_LT:: *_handler)(typename _FT::parameters_t &) + ) { + // save name and handler function + std::string function_name = _FT::_function_name; + std::function handler_fn = _handler; + + // define as incoming + available_incoming_functions.insert(function_name); + + // create intermediary handler for data conversion + function_names_to_functions[function_name] = [this, handler_fn](const nlohmann::json &_data) -> nlohmann::json + { + EL_LOGD("proc hdl %s", _data.dump().c_str()); + typename _FT::parameters_t function_parameters = _data; + return static_cast(handler_fn( + static_cast<_LT *>(this), + function_parameters + )); + }; + } + + /** + * @brief Shortcut for defining an incoming only function + * with an arbitrary handler function. + * + * The handler can be an arbitrary function matching the call signature + * ``` + * _FT::results_t(_FT::parameters_t &_params) + * ```. + * If the handler is a method of the link instance, + * there is a special overload to simplify that case. This is not that overload. + * + * @tparam _FT the function class of the function to register + * (must inherit from el::msglink::incoming_function, can be deduced from method parameter) + * @param _handler the method containing the function code + */ + template + void define_function( + typename _FT::results_t (*_handler)(typename _FT::parameters_t &) + ) { + // save name and handler function + std::string function_name = _FT::_function_name; + std::function handler_fn = _handler; + + // define as incoming + available_incoming_functions.insert(function_name); + + // create intermediary handler for data conversion + function_names_to_functions[function_name] = [this, handler_fn](const nlohmann::json &_data) -> nlohmann::json + { + EL_LOGD("proc hdl %s", _data.dump().c_str()); + typename _FT::parameters_t function_parameters = _data; + return static_cast(handler_fn( + function_parameters + )); + }; + } + + /** + * @brief Defines an outgoing only function. + * + * @tparam _FT the function class of the function to register (must inherit from el::msglink::outgoing_function) + */ + template + void define_event() + { + // save name + std::string function_name = _FT::_function_name; + + // define as outgoing + available_outgoing_functions.insert(function_name); + } + public: /** * The following functions are used to access events, data subscriptions @@ -923,9 +1067,9 @@ namespace el::msglink msg.tid = transaction->id; msg.proto_version = proto_version::current; msg.link_version = get_link_version(); - msg.events = available_outgoing_events; + msg.events = available_outgoing_events; // all events this party can provide (so the other one can subscribe to them) //msg.data_sources = ...; - //msg.procedures = ...; + msg.functions = available_incoming_functions; // all functions this party can provide (so the other one can call them) interface.send_message(msg); } diff --git a/include/el/msglink/msglink_protocol.md b/include/el/msglink/msglink_protocol.md index 41a7df0..8a2a59e 100644 --- a/include/el/msglink/msglink_protocol.md +++ b/include/el/msglink/msglink_protocol.md @@ -20,7 +20,7 @@ msglink is similar to Socket.IO, except it provides additional functionality ext - Strict types and data validation - Type-Defined events - Data subscriptions -- Remote Procedure calls (with return data) +- Remote function calls ## Strict types and data validation @@ -54,15 +54,15 @@ These data sources do not need to be part of a static API definition that is har The same functionality can be implemented with simple messages, however since this is used so frequently, it gets repetitive and error-prone quite quickly. By implementing this feature in a library, the repetitive parts can be abstracted and we can even use language/framework specific features to make such "remote" data sources even more convenient to use (e.g. React State or Svelt Stores). -## Remote Procedure calls +## Remote function calls -Another common use-case for messaging protocols is a remote procedure calls. A remote procedure call consists of one communication party sending some data to the other one and causing some code to run there. Once the other party is finished, it will return the result to the calling party. +A common use-case for messaging protocols is a remote procedure calls. This means that one party can trigger the execution of some code (the procedure) by the other party (with some optional input data). This functionality is basically equivalent to an msglink event (events even provide the ability for multiple "procedures"). Sometimes it is required to report some outcome or return some data to the calling party after the procedure has run. This is where msglink remote **function** calls come in. A remote function call consists of one communication party sending some data to the other one and causing some code to run there. Once the other party is finished, it will return the result to the calling party. -A remote procedure call can be implemented by emitting an event and running some action in the even listener. At the end of the listener, another event has to be emitted containing the result returned to the client. +A remote function call can be implemented by emitting an event and running some action in the even listener. At the end of the listener, another event has to be emitted containing the result returned to the client. -There are a few problems with manually implementing this using events. First of all, in order for the request and response events to be associated with each other, some sort of unique ID must be added by the caller that is then also returned in the response so the caller can associate the result with any particular call. Second, an application is likely to have many different procedures to be called, so the additional overhead of defining and emitting separate request and response events (with this ID management) for every procedure is quite tedious and error prone, as it involves re-implementing the same functionality multiple times. +There are a few problems with manually implementing this using events. First of all, in order for the request and response events to be associated with each other, some sort of unique ID must be added by the caller that is then also returned in the response so the caller can associate the result with any particular call. Second, an application is likely to have many different functions to be called, so the additional overhead of defining and emitting separate request and response events (with this ID management) for every function is quite tedious and error prone, as it involves re-implementing the same functionality multiple times. -msglink avoids this by implementing the base functionality once and providing a language-specific and clean way to define procedures in one place with input data, result data and name. This is similar to [JSON-RPC](https://en.wikipedia.org/wiki/JSON-RPC) but provides the additional data validation and automatic parsing functionality described above. +msglink avoids this by implementing the base functionality once and providing a language-specific and clean way to define functions in one place with input data, result data and name. This is similar to [JSON-RPC](https://en.wikipedia.org/wiki/JSON-RPC) but provides the additional data validation and automatic parsing functionality described above. ## Decision criteria @@ -94,11 +94,11 @@ Which of the three options provided by msglink (events, data subscriptions, RPCs > > However often times the executing party needs send some result data or outcome of the action back to the emitter. In the past, it was necessary to define a separate request and response event and write code for every type of interaction to sync the two up, wait for the response and so on. This is very tedious and repetitive. > - > With msglink, for such a case a procedure can be defined instead of an event. A procedure is basically two events combined, with the only difference being that the listener now returns another object which is sent back to the emitter. This can be integrated nicely with the async programming capability of many programming languages. + > With msglink, for such a case a function can be defined instead of an event. A function is basically two events combined, with the only difference being that the listener now returns another object which is sent back to the emitter. This can be integrated nicely with the async programming capability of many programming languages. > - > Another difference between procedures and events is the way that they are handled on the receiving side. Since events have no way of returning data or results to the emitter, there may be many listeners that are notified of the event and can perform actions when that happens. Although each of them may cause various actions, like emitting more events as a response, none of them are responsible for or capable of defining one singular "outcome" or "result" of the event. This is a broadcast theme. + > Another difference between functions and events is the way that they are handled on the receiving side. Since events have no way of returning data or results to the emitter, there may be many listeners that are notified of the event and can perform actions when that happens. Although each of them may cause various actions, like emitting more events as a response, none of them are responsible for or capable of defining one singular "outcome" or "result" of the event. This is a broadcast theme. > - > With procedures, this is different. Since a procedure has to have one single definite result to be sent back to the caller (roughly equivalent to emitter for events) after it has been handled, there can only be one handler. A procedure may be called from many different places, but on the receiving side, there has to be exactly one handler (function). Since it is necessary for a complete transaction to always return a result, it is also not allowed for there to be no handler at all. So procedures always have exactly one handler. + > With functions, this is different. Since a function has to have one single definite result to be sent back to the caller (roughly equivalent to emitter for events) after it has been handled, there can only be one handler. A function may be called from many different places, but on the receiving side, there has to be exactly one handler (function). Since it is necessary for a complete transaction to always return a result, it is also not allowed for there to be no handler at all. So functions always have exactly one handler. # Protocol details @@ -154,10 +154,9 @@ The **```type```** property defines the purpose of the message. There are the fo - data_sub_nak - data_unsub - data_change -- rpc_call -- rpc_nak -- rpc_err -- rpc_result +- func_call +- func_err +- func_result The **```tid```** property is the transaction ID. The transaction ID is a signed integer number which (within a single session) uniquely identifies the transaction the message belongs to. The "pong" message type doesn't have this transaction ID. @@ -182,7 +181,7 @@ When closing the msglink and therefore websocket connection, custom websocket cl | 3002 | link version mismatch | | | 3003 | Event requirement(s) unsatisfied | | | 3004 | Data source requirement(s) unsatisfied | | -| 3005 | RPC requirement(s) unsatisfied | | +| 3005 | Function requirement(s) unsatisfied | | ## Heartbeat and Pong message @@ -218,7 +217,7 @@ When a msglink client first connects to the msglink server both parties send an "no_ping": false, // optional boolean "events": ["error_occurred"], "data_sources": ["devices", "power_consumption"], - "procedures": ["disable_device"] + "functions": ["disable_device"] } ``` @@ -227,7 +226,7 @@ When a msglink client first connects to the msglink server both parties send an - **```no_ping```**: flag that can be set by the client if it doesn't support receiving pong messages from "user" code. Every client *must* respond to WS pings with WS pongs, but in some cases (such as browser API) the user code cannot detect this happening. Such a client can set this flag (_true_) during authentication causing the server to send an extra msglink "pong" message whenever a ping-pong procedure has finished, which can be used by the client to determine the health of the connection. This key can be omitted having the same result as a _false_ value. This key is to be ignored by clients if included by servers in their auth message, as only servers are responsible for conducting ping procedures. - **```events```**: a list of events the party may emit (it's outgoing events) - **```data_sources```**: a list of data sources the party can provide (it's outgoing data sources) -- **```procedures```**: a list of remote procedures the party provides +- **```functions```**: a list of remote functions the party provides (it's incoming functions) After receiving the message from the other party, both parties will check that the protocol versions of the other party are compatible and that the user defined link versions match. If that is not the case, the connection will be closed with code 3001 or 3002. @@ -239,8 +238,8 @@ The message also contains lists of all the functionality the party can provide t - If one party may want to listen for an event the other party doesn't even know about and will never be able to emit - **data sources**: one party's data subscription list must be a subset of the other's data source list. Fails with code 3004. Fail reasons: - If one party may subscribe to a source the other doesn't know about and provide -- **remote procedure calls**: one party's called procedures list must be a subset of the other's callable procedure list. Fails with code 3005. Fail reasons: - - If one party may call a procedure the other doesn't know about and cannot handle +- **remote function calls**: one party's outgoing (called) function list must be a subset of the other's incoming (callable) function list. Fails with code 3005. Fail reasons: + - If one party may call a function the other doesn't know about and cannot handle Obviously these requirements are only checked approximately. The client doesn't know at that point whether the server ever will emit the "error_occurred" event or even if there will ever be a listener for it. The only thing it knows is that both the server and itself know that this event exists and know how to deal with it should that become necessary later. @@ -337,5 +336,5 @@ Note for future me: If msglink doesn't fit for some reason in the future, here a // Then implement a corresponding msglink client (reconnects, ...) // Then implement state-management calls on a link (e.g. on_connect/on_disconnect, possibly a "currently not connected but attempting to reconnect, don't give up jet" state) // Then add support for data subscriptions (they need more state management (e.g. requests ) in their own classes) - // Then add support for remote procedure calls. + // Then add support for remote function calls. ``` \ No newline at end of file diff --git a/include/el/msglink/procedure.hpp b/include/el/msglink/procedure.hpp deleted file mode 100644 index bc6a627..0000000 --- a/include/el/msglink/procedure.hpp +++ /dev/null @@ -1,174 +0,0 @@ -/* -ELEKTRON © 2024 - now -Written by melektron -www.elektron.work -21.01.24, 20:10 -All rights reserved. - -This source code is licensed under the Apache-2.0 license found in the -LICENSE file in the root directory of this source tree. - -msglink procedure class used to define custom procedures -*/ - -#pragma once - -#include "../cxxversions.h" - -#include -#ifdef __EL_ENABLE_CXX20 -#include -#endif - -#include "../codable.hpp" - - -namespace el::msglink -{ - - - /** - * @brief base class for all incoming msglink procedure - * definition classes. To create an incoming procedure define a class inheriting from this one. - * Then use the EL_MSGLINK_DEFINE_INCOMING_PROCEDURE macro to generate the required boilerplate. - * Incoming procedures must additionally have one structure called "parameters" which must be an el::decodable - * and one structure called "results" which must be an el::encodable. They can be defined with - * default codable definition macros. - */ - struct incoming_procedure - { - // dummy method which must be overriden to ensure the correct - // generate macro is used. - virtual void _el_msglink_is_incoming_proc_dummy() const noexcept = 0; - }; - - /** - * @brief base class for all outgoing msglink procedure - * definition classes. To create an outgoing procedure define a class inheriting from this one. - * Then use the EL_MSGLINK_DEFINE_OUTGOING_PROCEDURE macro to generate the required boilerplate. - * Outgoing procedures must additionally have one structure called "parameters" which must be an el::encodable - * and one structure called "results" which must be an el::decodable. They can be defined with - * default codable definition macros. - */ - struct outgoing_procedure : public el::encodable - { - // dummy method which must be overriden to ensure the correct - // generate macro is used. - virtual void _el_msglink_is_outgoing_proc_dummy() const noexcept = 0; - }; - - /** - * @brief shortcut base class for all bidirectional (incoming and outgoing) msglink procedure - * definition classes. It is simply a composite class inheriting form outgoing_procedure - * and incoming_procedure to save you the hassle of having to do that manually. - * Derived classes must satisfy all the requirements of incoming and outgoing procedures. - * To create a bidirectional procedure define a class inheriting from this one. - * Then use the EL_MSGLINK_DEFINE_BIDIRECTIONAL_PROCEDURE macro to generate the required boilerplate. - */ - struct bidirectional_procedure : public incoming_procedure, public outgoing_procedure - {}; - - -#ifdef __EL_ENABLE_CXX20 - /** - * The following concepts define constraints that allow targeting specific - * kinds of procedures such as an procedure class that is either ONLY an incoming - * procedure or ONLY an outgoing procedure or a bidirectional procedure (BOTH incoming - * and outgoing) - */ - - /** - * @brief Constrains _ET to be ONLY derived from incoming_procedure - * and NOT from outgoing_procedure - */ - template - concept IncomingOnlyProcedure = std::derived_from<_ET, incoming_procedure> && !std::derived_from<_ET, outgoing_procedure>; - - /** - * @brief Constrains _ET to be at derived at least from incoming_procedure - * (but can additionally also derive from outgoing_procedure) - */ - template - concept AtLeastIncomingProcedure = std::derived_from<_ET, incoming_procedure>; - - /** - * @brief Constrains _ET to be ONLY derived from outgoing_procedure - * and NOT from incoming_procedure - */ - template - concept OutgoingOnlyProcedure = std::derived_from<_ET, outgoing_procedure> && !std::derived_from<_ET, incoming_procedure>; - - /** - * @brief Constrains _ET to be at derived at least from outgoing_procedure - * (but can additionally also derive from incoming_procedure) - */ - template - concept AtLeastOutgoingProcedure = std::derived_from<_ET, outgoing_procedure>; - - /** - * @brief Constrains _ET to be derived BOTH from incoming_procedure - * and from outgoing_procedure, making it a bidirectional procedure - */ - template - concept BidirectionalProcedure = std::derived_from<_ET, incoming_procedure> && std::derived_from<_ET, outgoing_procedure>; - - /** - * @brief Constrains _ET to be derived from incoming_procedure - * and or outgoing_procedure. This constrains a type to be any - * sort of procedure. - */ - template - concept AnyProcedure = std::derived_from<_ET, incoming_procedure> || std::derived_from<_ET, outgoing_procedure>; - -#endif // __EL_ENABLE_CXX20 - - -// (public) generates the necessary boilerplate code for an incoming procedure class. -// The first and only argument is the structure type itself which is used to get the name. -#define EL_MSGLINK_DEFINE_INCOMING_PROCEDURE(TypeName, ...) \ - static inline const char *_procedure_name = #TypeName; \ - virtual void _el_msglink_is_incoming_proc_dummy() const noexcept override {} \ - static_assert( \ - std::is_base_of<::el::decodable, parameters>::value, \ - "incoming procedure parameters type must be decodable" \ - ); \ - static_assert( \ - std::is_base_of<::el::encodable, results>::value, \ - "incoming procedure results type must be encodable" \ - ); - - -// (public) generates the necessary boilerplate code for an outgoing procedure class. -// The first and only argument is the structure type itself which is used to get the name. -#define EL_MSGLINK_DEFINE_OUTGOING_PROCEDURE(TypeName, ...) \ - static inline const char *_procedure_name = #TypeName; \ - virtual void _el_msglink_is_outgoing_proc_dummy() const noexcept override {} \ - static_assert( \ - std::is_base_of<::el::encodable, parameters>::value, \ - "incoming procedure parameters type must be encodable" \ - ); \ - static_assert( \ - std::is_base_of<::el::decodable, results>::value, \ - "incoming procedure results type must be decodable" \ - ); - - -// (public) generates the necessary boilerplate code for a procedure class. -// The first and only argument is the structure type itself which is used to get the name. -#define EL_MSGLINK_DEFINE_BIDIRECTIONAL_PROCEDURE(TypeName, ...) \ - static inline const char *_procedure_name = #TypeName; \ - virtual void _el_msglink_is_incoming_proc_dummy() const noexcept override {} \ - virtual void _el_msglink_is_outgoing_proc_dummy() const noexcept override {} \ - static_assert( \ - std::is_base_of<::el::decodable, parameters>::value && \ - std::is_base_of<::el::encodable, parameters>::value, \ - "bidirectional procedure parameters type must be en- and decodable" \ - ); \ - static_assert( \ - std::is_base_of<::el::decodable, results>::value && \ - std::is_base_of<::el::encodable, results>::value, \ - "bidirectional procedure results type must be en- and decodable" \ - ); - - -} // namespace el::msglink \ No newline at end of file From 17787e237430ec198ab4046989fb5c3d38da39e8 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 28 Jan 2024 20:08:54 +0100 Subject: [PATCH 39/50] implemented error handling for msglink RFCs and bidirectional functions --- include/el/msglink/link.hpp | 127 ++++++++++++++++++++++++++++------ include/el/msglink/server.hpp | 12 ---- 2 files changed, 107 insertions(+), 32 deletions(-) diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index 044a77c..aa12564 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -109,17 +109,16 @@ namespace el::msglink // set of all possible outgoing functions that this party may want to call on the other party std::set available_outgoing_functions; - // set of all possible incoming functions that may be called by the other party - std::set available_incoming_functions; + // set of incoming not required, below map is used // type of the intermediary handler function using function_handler_function_t = std::function; - // map of incoming function names to their handlers + // map of all incoming function names to their handlers std::unordered_map< std::string, function_handler_function_t - > function_names_to_functions; + > available_incoming_function_names_to_functions; private: // methods @@ -492,17 +491,29 @@ namespace el::msglink { msg_func_call_t msg(_jmsg); - if (!available_incoming_functions.contains(msg.name) || !function_names_to_functions.contains(msg.name)) + if (!available_incoming_function_names_to_functions.contains(msg.name)) { EL_LOGW("Received FUNC_CALL message for a function which isn't incoming and/or doesn't exist. This is likely a library implementation issue and should not happen."); break; } // run the handler - nlohmann::json results_object = function_names_to_functions.at(msg.name)(msg.params); - // TODO: catch exceptions and return error message in case of one + nlohmann::json results_object; + try + { + results_object = available_incoming_function_names_to_functions.at(msg.name)(msg.params); + } + catch (const std::exception &_e) + { + // error during handler execution, respond with error message + msg_func_err_t response; + response.tid = msg.tid; + response.info = _e.what(); + interface.send_message(response); + break; + } - // send result + // otherwise send result msg_func_result_t response; response.tid = msg.tid; response.results = results_object; @@ -904,7 +915,84 @@ namespace el::msglink * == Functions == * */ + + /** + * @brief Shortcut for defining a bidirectional function + * with a function that is a method of the link. + * + * The function must be a method + * of the link it is registered on. This is a shortcut + * to avoid having to use std::bind to bind the function + * to the instance. When an external function is needed, this + * is the wrong overload. + * + * @note Method function pointer: + * https://isocpp.org/wiki/faq/pointers-to-members#typedef-for-ptr-to-memfn + * + * @tparam _FT the function class of the function to register + * (must inherit from el::msglink::incoming_function and el::msglink::outgoing_function, can be deduced from method parameter) + * @tparam _LT the link class the handler function is a method of (can also be deduced) + * @param _handler the method containing the function code + */ + template _LT> + void define_function( + typename _FT::results_t (_LT:: *_handler)(typename _FT::parameters_t &) + ) { + // save name and handler function + std::string function_name = _FT::_function_name; + std::function handler_fn = _handler; + + // define outgoing + available_outgoing_functions.insert(function_name); + // define as incoming by creating intermediary handler for data conversion + available_incoming_function_names_to_functions[function_name] = [this, handler_fn](const nlohmann::json &_data) -> nlohmann::json + { + EL_LOGD("proc hdl %s", _data.dump().c_str()); + typename _FT::parameters_t function_parameters = _data; + return static_cast(handler_fn( + static_cast<_LT *>(this), + function_parameters + )); + }; + } + + /** + * @brief Shortcut for defining a bidirectional function + * with an arbitrary handler function. + * + * The handler can be an arbitrary function matching the call signature + * ``` + * _FT::results_t(_FT::parameters_t &_params) + * ```. + * If the handler is a method of the link instance, + * there is a special overload to simplify that case. This is not that overload. + * + * @tparam _FT the function class of the function to register + * (must inherit from el::msglink::incoming_function and el::msglink::outgoing_function, can be deduced from method parameter) + * @param _handler the method containing the function code + */ + template + void define_function( + typename _FT::results_t (*_handler)(typename _FT::parameters_t &) + ) { + // save name and handler function + std::string function_name = _FT::_function_name; + std::function handler_fn = _handler; + + // define as outgoing + available_outgoing_functions.insert(function_name); + + // define as incoming by creating intermediary handler for data conversion + available_incoming_function_names_to_functions[function_name] = [this, handler_fn](const nlohmann::json &_data) -> nlohmann::json + { + EL_LOGD("proc hdl %s", _data.dump().c_str()); + typename _FT::parameters_t function_parameters = _data; + return static_cast(handler_fn( + function_parameters + )); + }; + } /** * @brief Shortcut for defining an incoming only function @@ -932,11 +1020,8 @@ namespace el::msglink std::string function_name = _FT::_function_name; std::function handler_fn = _handler; - // define as incoming - available_incoming_functions.insert(function_name); - - // create intermediary handler for data conversion - function_names_to_functions[function_name] = [this, handler_fn](const nlohmann::json &_data) -> nlohmann::json + // define as incoming by creating intermediary handler for data conversion + available_incoming_function_names_to_functions[function_name] = [this, handler_fn](const nlohmann::json &_data) -> nlohmann::json { EL_LOGD("proc hdl %s", _data.dump().c_str()); typename _FT::parameters_t function_parameters = _data; @@ -970,11 +1055,8 @@ namespace el::msglink std::string function_name = _FT::_function_name; std::function handler_fn = _handler; - // define as incoming - available_incoming_functions.insert(function_name); - - // create intermediary handler for data conversion - function_names_to_functions[function_name] = [this, handler_fn](const nlohmann::json &_data) -> nlohmann::json + // define as incoming by creating intermediary handler for data conversion + available_incoming_function_names_to_functions[function_name] = [this, handler_fn](const nlohmann::json &_data) -> nlohmann::json { EL_LOGD("proc hdl %s", _data.dump().c_str()); typename _FT::parameters_t function_parameters = _data; @@ -990,7 +1072,7 @@ namespace el::msglink * @tparam _FT the function class of the function to register (must inherit from el::msglink::outgoing_function) */ template - void define_event() + void define_function() { // save name std::string function_name = _FT::_function_name; @@ -1069,7 +1151,12 @@ namespace el::msglink msg.link_version = get_link_version(); msg.events = available_outgoing_events; // all events this party can provide (so the other one can subscribe to them) //msg.data_sources = ...; - msg.functions = available_incoming_functions; // all functions this party can provide (so the other one can call them) + // all functions this party can provide (so the other one can call them) + std::transform( // using transform to fill msg.functions with key from function map + available_incoming_function_names_to_functions.begin(), available_incoming_function_names_to_functions.end(), + std::inserter(msg.functions, msg.functions.end()), + [](auto entry){ return entry.first; } + ); interface.send_message(msg); } diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index 673c030..e8e78f5 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -121,8 +121,6 @@ namespace el::msglink return; else if (_ec) throw unexpected_error(_ec); - - EL_LOGD("ping timer"); // send a ping get_connection()->ping(""); // no message needed @@ -356,8 +354,6 @@ namespace el::msglink */ void on_open(wspp::connection_hdl _hdl) { - EL_LOG_FUNCTION_CALL(); - if (m_server_state != RUNNING) return; @@ -384,8 +380,6 @@ namespace el::msglink */ void on_message(wspp::connection_hdl _hdl, wsserver::message_ptr _msg) { - EL_LOG_FUNCTION_CALL(); - if (m_server_state != RUNNING) return; @@ -410,8 +404,6 @@ namespace el::msglink */ void on_close(wspp::connection_hdl _hdl) { - EL_LOG_FUNCTION_CALL(); - if (m_server_state != RUNNING) return; @@ -447,8 +439,6 @@ namespace el::msglink */ void on_pong_received(wspp::connection_hdl _hdl, std::string _payload) { - EL_LOG_FUNCTION_CALL(); - if (m_server_state != RUNNING) return; @@ -472,8 +462,6 @@ namespace el::msglink */ void on_pong_timeout(wspp::connection_hdl _hdl, std::string _expected_payload) { - EL_LOG_FUNCTION_CALL(); - if (m_server_state != RUNNING) return; From e2dc398fdedddc806516ee0efe8c4bb23743afc7 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 28 Jan 2024 22:23:04 +0100 Subject: [PATCH 40/50] added link error handling --- include/el/msglink/internal/ws_close_code.hpp | 28 ++++++++- include/el/msglink/link.hpp | 5 +- include/el/msglink/msglink_protocol.md | 1 + include/el/msglink/server.hpp | 61 +++++++++++++++++-- 4 files changed, 86 insertions(+), 9 deletions(-) diff --git a/include/el/msglink/internal/ws_close_code.hpp b/include/el/msglink/internal/ws_close_code.hpp index 737d170..9198eb9 100644 --- a/include/el/msglink/internal/ws_close_code.hpp +++ b/include/el/msglink/internal/ws_close_code.hpp @@ -15,7 +15,7 @@ Websocket close codes specific to msglink. namespace el::msglink { - enum class close_code_t + enum class close_code_t : uint16_t { CLOSED_BY_USER = 1000, PROTO_VERSION_INCOMPATIBLE = 3001, @@ -23,5 +23,31 @@ namespace el::msglink EVENT_REQUIREMENTS_NOT_SATISFIED = 3003, DATA_SOURCE_REQUIREMENTS_NOT_SATISFIED = 3004, FUNCTION_REQUIREMENTS_NOT_SATISFIED = 3005, + UNDEFINED_LINK_ERROR = 3100 }; + + inline const char *close_code_name(close_code_t _code) + { + switch (_code) + { + using enum close_code_t; + + case CLOSED_BY_USER: + return "closed by user"; + case PROTO_VERSION_INCOMPATIBLE: + return "proto version incompatible"; + case LINK_VERSION_MISMATCH: + return "link version mismatch"; + case EVENT_REQUIREMENTS_NOT_SATISFIED: + return "event requirements not satisfied"; + case DATA_SOURCE_REQUIREMENTS_NOT_SATISFIED: + return "data source requirements not satisfied"; + case FUNCTION_REQUIREMENTS_NOT_SATISFIED: + return "function requirements not satisfied"; + case UNDEFINED_LINK_ERROR: + return "undefined link error"; + default: + return "N/A"; + }; + } } // namespace el::msglink diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index aa12564..ce3d656 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -324,9 +324,6 @@ namespace el::msglink if (msg.no_ping.has_value()) pong_messages_required = *msg.no_ping; - // TODO: remove - EL_LOGD("no_ping=%s", !msg.no_ping ? "nullptr" : (msg.no_ping.value() ? "true" : "false")); - // check event list // all the events I may require (incoming) must be included in the events // the other party can provide (it's outgoing events) @@ -349,7 +346,7 @@ namespace el::msglink available_outgoing_functions.begin(), available_outgoing_functions.end() )) throw incompatible_link_error( - close_code_t::EVENT_REQUIREMENTS_NOT_SATISFIED, + close_code_t::FUNCTION_REQUIREMENTS_NOT_SATISFIED, "Remote party does not satisfy the function requirements (missing functions)" ); diff --git a/include/el/msglink/msglink_protocol.md b/include/el/msglink/msglink_protocol.md index 8a2a59e..ae6d2da 100644 --- a/include/el/msglink/msglink_protocol.md +++ b/include/el/msglink/msglink_protocol.md @@ -182,6 +182,7 @@ When closing the msglink and therefore websocket connection, custom websocket cl | 3003 | Event requirement(s) unsatisfied | | | 3004 | Data source requirement(s) unsatisfied | | | 3005 | Function requirement(s) unsatisfied | | +| 3100 | Undefined link error (unknown exception in link) | | ## Heartbeat and Pong message diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index e8e78f5..fbbfaba 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -172,6 +172,51 @@ namespace el::msglink get_connection()->send(_content); } + /** + * @brief runs a passed lambda containing a link method call + * and handles exceptions thrown by the link, closing the connection + * when that happens. + * When a link throws an incompatible_link_error, the connection is + * closed with the appropriate close code. + * Other exceptions cause the connection to close code for + * undefined error. + * + * @param _lambda code to run with exception handling + */ + void run_with_exception_handling(std::function _lambda) + { + try + { + _lambda(); + } + catch (const incompatible_link_error &e) + { + EL_LOG_EXCEPTION_MSG("Remote link is not compatible", e); + EL_LOGE( + "Closing connection with code %d (%s)", + e.code(), + close_code_name(e.code()) + ); + get_connection()->close( + (uint16_t)e.code(), + close_code_name(e.code()) + ); + } + catch (const std::exception &e) + { + EL_LOG_EXCEPTION_MSG("Unknown exception in link", e); + EL_LOGE( + "Closing connection with code %d (%s)", + close_code_t::UNDEFINED_LINK_ERROR, + close_code_name(close_code_t::UNDEFINED_LINK_ERROR) + ); + get_connection()->close( + (uint16_t)close_code_t::UNDEFINED_LINK_ERROR, + close_code_name(close_code_t::UNDEFINED_LINK_ERROR) + ); + } + } + public: // connection handler is supposed to be instantiated in-place exactly once per @@ -197,7 +242,9 @@ namespace el::msglink EL_LOG_FUNCTION_CALL(); // define the link protocol - m_link.define(); + run_with_exception_handling([&](){ + this->m_link.define(); + }); } /** @@ -249,7 +296,9 @@ namespace el::msglink // start communication by notifying the link // TODO: i.e. "connecting" the link to the interface (change how this works) - m_link.on_connection_established(); + run_with_exception_handling([&](){ + this->m_link.on_connection_established(); + }); } /** @@ -260,7 +309,9 @@ namespace el::msglink void on_message(wsserver::message_ptr _msg) { EL_LOGD("Incoming Message: %s", _msg->get_payload().c_str()); - m_link.on_message(_msg->get_payload()); + run_with_exception_handling([&](){ + this->m_link.on_message(_msg->get_payload()); + }); } /** @@ -274,7 +325,9 @@ namespace el::msglink // pong arrived in time, all good, connection alive // notify link, so pong message can be sent to client if needed - m_link.on_pong_received(); + run_with_exception_handling([&](){ + m_link.on_pong_received(); + }); // schedule a new ping to be sent a bit later. schedule_ping(); From 3906607ce3045ff60f9f54f7e4abb843bcf48604 Mon Sep 17 00:00:00 2001 From: melektron Date: Tue, 30 Jan 2024 00:21:29 +0100 Subject: [PATCH 41/50] implemented outgoing function calls (still needs to be made properly thread-safe) and better exception handling --- include/el/msglink/errors.hpp | 10 +++ include/el/msglink/internal/transaction.hpp | 8 +- include/el/msglink/internal/ws_close_code.hpp | 6 ++ include/el/msglink/link.hpp | 83 +++++++++++++++++-- include/el/msglink/msglink_protocol.md | 2 + include/el/msglink/server.hpp | 83 ++++++++++++------- 6 files changed, 153 insertions(+), 39 deletions(-) diff --git a/include/el/msglink/errors.hpp b/include/el/msglink/errors.hpp index b230f54..5133ccc 100644 --- a/include/el/msglink/errors.hpp +++ b/include/el/msglink/errors.hpp @@ -182,6 +182,16 @@ namespace el::msglink using msglink_error::msglink_error; }; + /** + * @brief this is thrown in a remote function call when the remote + * party responds with an error. + * The what() string contains the info provided by the remote client + */ + class remote_function_error : public msglink_error + { + using msglink_error::msglink_error; + }; + /** * @brief exception used to indicate an unexpected * error code occurred like e.g. in some asio-related operation. diff --git a/include/el/msglink/internal/transaction.hpp b/include/el/msglink/internal/transaction.hpp index 9c5cfae..3ae732d 100644 --- a/include/el/msglink/internal/transaction.hpp +++ b/include/el/msglink/internal/transaction.hpp @@ -14,6 +14,7 @@ Structures to represent running transactions #pragma once #include +#include #include "types.hpp" @@ -74,9 +75,12 @@ namespace el::msglink using transaction_t::transaction_t; }; - struct transaction_rpc_t : public transaction_t + struct transaction_function_call_t : public transaction_t // only used for outgoing { - std::string event_name; + // function called when the result message is received. + std::function handle_result; + // function called when the error message is received + std::function handle_error; using transaction_t::transaction_t; }; diff --git a/include/el/msglink/internal/ws_close_code.hpp b/include/el/msglink/internal/ws_close_code.hpp index 9198eb9..b1b7b34 100644 --- a/include/el/msglink/internal/ws_close_code.hpp +++ b/include/el/msglink/internal/ws_close_code.hpp @@ -23,6 +23,8 @@ namespace el::msglink EVENT_REQUIREMENTS_NOT_SATISFIED = 3003, DATA_SOURCE_REQUIREMENTS_NOT_SATISFIED = 3004, FUNCTION_REQUIREMENTS_NOT_SATISFIED = 3005, + MALFORMED_MESSAGE = 3006, + PROTOCOL_ERROR = 3007, UNDEFINED_LINK_ERROR = 3100 }; @@ -44,6 +46,10 @@ namespace el::msglink return "data source requirements not satisfied"; case FUNCTION_REQUIREMENTS_NOT_SATISFIED: return "function requirements not satisfied"; + case MALFORMED_MESSAGE: + return "malformed message"; + case PROTOCOL_ERROR: + return "protocol error"; case UNDEFINED_LINK_ERROR: return "undefined link error"; default: diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index ce3d656..0fa5de7 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -20,6 +20,7 @@ the user to define the API/protocol of a link #include #include #include +#include #include @@ -371,7 +372,7 @@ namespace el::msglink break; default: - throw malformed_message_error("Invalid pre-auth message type: %s", msg_type_to_string(_msg_type)); + throw protocol_error("Invalid pre-auth message type: %s", msg_type_to_string(_msg_type)); break; } @@ -518,12 +519,34 @@ namespace el::msglink } break; case FUNC_ERR: - break; + { + msg_func_err_t msg(_jmsg); + auto transaction = get_transaction(msg.tid); + + // run error callback to complete future + if (transaction->handle_error != nullptr) + transaction->handle_error(msg.info); + + // complete transaction -> destroys lambdas and releases promise + complete_transaction(transaction); + } + break; case FUNC_RESULT: - break; + { + msg_func_result_t msg(_jmsg); + auto transaction = get_transaction(msg.tid); + + // run result callback to complete future + if (transaction->handle_result != nullptr) + transaction->handle_result(msg.results); + + // complete transaction -> destroys lambdas and releases promise + complete_transaction(transaction); + } + break; default: - throw malformed_message_error("Invalid post-auth message type: %s", msg_type_to_string(_msg_type)); + throw protocol_error("Invalid post-auth message type: %s", msg_type_to_string(_msg_type)); break; } @@ -1079,11 +1102,11 @@ namespace el::msglink } public: + /** * The following functions are used to access events, data subscriptions * or RPCs such as by registering listeners, emitting events or updating data. */ - template void emit(const _ET &_event) { @@ -1098,6 +1121,56 @@ namespace el::msglink send_event_emit_message(_ET::_event_name, _event); } + template + std::future call(const typename _FT::parameters_t &_params) + { + // create the transaction (this this initializes the promise) + auto transaction = create_transaction( + generate_new_tid(), + inout_t::OUTGOING + ); + + // create promise for result (shared, will be deleted when lambdas are released) + auto promise = std::make_shared>(); + + // register response handlers + // (being careful not to introduce cyclic references via shared ptr) + transaction->handle_result = [promise]( + const nlohmann::json &_result + ) { + try + { + // decode results from json and return them + promise->set_value(_result); + } + catch (const std::exception &) + { + // set exception if decode fails + promise->set_exception(std::current_exception()); + } + }; + transaction->handle_error = [promise]( + const std::string &_info + ) { + // save error in promise + promise->set_exception( + std::make_exception_ptr( + remote_function_error(_info) + ) + ); + }; + + // send call message + msg_func_call_t msg; + msg.tid = transaction->id; + msg.name = _FT::_function_name; + msg.params = _params; + interface.send_message(msg); + + // return future + return promise->get_future(); + } + public: /** diff --git a/include/el/msglink/msglink_protocol.md b/include/el/msglink/msglink_protocol.md index ae6d2da..615fce9 100644 --- a/include/el/msglink/msglink_protocol.md +++ b/include/el/msglink/msglink_protocol.md @@ -182,6 +182,8 @@ When closing the msglink and therefore websocket connection, custom websocket cl | 3003 | Event requirement(s) unsatisfied | | | 3004 | Data source requirement(s) unsatisfied | | | 3005 | Function requirement(s) unsatisfied | | +| 3006 | Malformed message | includes both syntactical errors and undecodable data in terms of wrong/missing fields | +| 3007 | Protocol error | | | 3100 | Undefined link error (unknown exception in link) | | diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index fbbfaba..1100688 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -152,24 +152,24 @@ namespace el::msglink m_communication_canceled.set(); } - - protected: // methods + /** - * @brief implements the link_interface interface - * to allow the link to send messages through the client - * communication channel. + * @brief closes the connection with one of the custom + * msglink close codes and logs the event to console * - * @param _content message content + * @param _code */ - virtual void send_message(const std::string &_content) override + void close_with_log(close_code_t _code) { - // ensure no messages go through after cancel, even though link - // shouldn't call this method anymore after cancel anyway. - if (m_communication_canceled) - return; - - EL_LOGD("Outgoing Message: %s", _content.c_str()); - get_connection()->send(_content); + EL_LOGI( + "Closing connection with code %d (%s)", + _code, + close_code_name(_code) + ); + get_connection()->close( + (uint16_t)_code, + close_code_name(_code) + ); } /** @@ -192,31 +192,50 @@ namespace el::msglink catch (const incompatible_link_error &e) { EL_LOG_EXCEPTION_MSG("Remote link is not compatible", e); - EL_LOGE( - "Closing connection with code %d (%s)", - e.code(), - close_code_name(e.code()) - ); - get_connection()->close( - (uint16_t)e.code(), - close_code_name(e.code()) - ); + close_with_log(e.code()); + } + catch (const invalid_transaction_error &e) + { + EL_LOG_EXCEPTION_MSG("Invalid transaction", e); + EL_LOGW("Ignoring invalid transaction message"); + } + catch (const malformed_message_error &e) + { + EL_LOG_EXCEPTION_MSG("Received malformed data", e); + close_with_log(close_code_t::MALFORMED_MESSAGE); + } + catch (const protocol_error &e) + { + EL_LOG_EXCEPTION_MSG("Communication doesn not comply with protocol", e); + close_with_log(close_code_t::PROTOCOL_ERROR); } catch (const std::exception &e) { EL_LOG_EXCEPTION_MSG("Unknown exception in link", e); - EL_LOGE( - "Closing connection with code %d (%s)", - close_code_t::UNDEFINED_LINK_ERROR, - close_code_name(close_code_t::UNDEFINED_LINK_ERROR) - ); - get_connection()->close( - (uint16_t)close_code_t::UNDEFINED_LINK_ERROR, - close_code_name(close_code_t::UNDEFINED_LINK_ERROR) - ); + close_with_log(close_code_t::UNDEFINED_LINK_ERROR); } } + protected: // methods + /** + * @brief implements the link_interface interface + * to allow the link to send messages through the client + * communication channel. + * + * @param _content message content + */ + virtual void send_message(const std::string &_content) override + { + // ensure no messages go through after cancel, even though link + // shouldn't call this method anymore after cancel anyway. + if (m_communication_canceled) + return; + + EL_LOGD("Outgoing Message: %s", _content.c_str()); + get_connection()->send(_content); + } + + public: // connection handler is supposed to be instantiated in-place exactly once per From cfba3cf84529a25f989df007863aa0d936f181f8 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 4 Feb 2024 15:55:08 +0100 Subject: [PATCH 42/50] implemented thread-safety of server class --- include/el/msglink/server.hpp | 124 +++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 53 deletions(-) diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index 1100688..44cfc57 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -13,7 +13,6 @@ msglink server class #pragma once -#include #include #include #include @@ -24,6 +23,8 @@ msglink server class #include #include +#include +#include #include "../retcode.hpp" #include "../logging.hpp" @@ -61,7 +62,7 @@ namespace el::msglink private: // state // the server managing this client connection - wsserver &m_socket_server; + wsserver &socket_server; // a handle to the connection handled by this client wspp::connection_hdl m_connection; @@ -90,7 +91,7 @@ namespace el::msglink */ wsserver::connection_ptr get_connection() { - return m_socket_server.get_con_from_hdl(m_connection); + return socket_server.get_con_from_hdl(m_connection); } /** @@ -251,7 +252,7 @@ namespace el::msglink * @param _connection */ connection_handler(wsserver &_socket_server, wspp::connection_hdl _connection) - : m_socket_server(_socket_server) + : socket_server(_socket_server) , m_connection(_connection) , m_link( true, // is server instance @@ -393,11 +394,11 @@ namespace el::msglink // == Configuration // port to serve on - int m_port; + int port; // == State // the websocket server used for transport - wsserver m_socket_server; + wsserver socket_server; // enumeration managing current server state enum server_state_t @@ -408,14 +409,18 @@ namespace el::msglink FAILED = 3, // run() exited with error STOPPED = 4 // run() exited cleanly (through stop() or other natural way) }; - std::atomic m_server_state { UNINITIALIZED }; + std::atomic server_state { UNINITIALIZED }; + // mutex to guard the server state, so state can not be changed from + // another thread while a function checking it at first is running. + std::mutex mu_server_state; + // set of connections to corresponding connection handler instance std::map< wspp::connection_hdl, connection_handler<_LT>, std::owner_less - > m_open_connections; + > open_connections; private: @@ -426,16 +431,17 @@ namespace el::msglink */ void on_open(wspp::connection_hdl _hdl) { - if (m_server_state != RUNNING) + std::lock_guard lock(mu_server_state); + if (server_state != RUNNING) return; // TODO: make atomic // create new handler instance and save it - auto new_connection = m_open_connections.emplace( + auto new_connection = open_connections.emplace( std::piecewise_construct, // Needed for in-place construct https://en.cppreference.com/w/cpp/utility/piecewise_construct std::forward_as_tuple(_hdl), - std::forward_as_tuple(m_socket_server, _hdl) + std::forward_as_tuple(socket_server, _hdl) ); // notify new connection handler to start communication @@ -452,13 +458,14 @@ namespace el::msglink */ void on_message(wspp::connection_hdl _hdl, wsserver::message_ptr _msg) { - if (m_server_state != RUNNING) + std::lock_guard lock(mu_server_state); + if (server_state != RUNNING) return; // forward message to connection handler try { - m_open_connections.at(_hdl).on_message(_msg); + open_connections.at(_hdl).on_message(_msg); } catch (const std::out_of_range &e) { @@ -476,20 +483,21 @@ namespace el::msglink */ void on_close(wspp::connection_hdl _hdl) { - if (m_server_state != RUNNING) + std::lock_guard lock(mu_server_state); + if (server_state != RUNNING) return; // TODO: make thread-safe (atomic) - if (!m_open_connections.contains(_hdl)) + if (!open_connections.contains(_hdl)) { throw invalid_connection_error("Attempted to close an unknown/invalid connection which doesn't seem to exist."); } // notify connection to stop communication - m_open_connections.at(_hdl).on_close(); + open_connections.at(_hdl).on_close(); // remove closed connection from connection map, deleting the connection handlers - m_open_connections.erase(_hdl); + open_connections.erase(_hdl); } /** @@ -511,13 +519,14 @@ namespace el::msglink */ void on_pong_received(wspp::connection_hdl _hdl, std::string _payload) { - if (m_server_state != RUNNING) + std::lock_guard lock(mu_server_state); + if (server_state != RUNNING) return; // forward message to connection handler try { - m_open_connections.at(_hdl).on_pong_received(_payload); + open_connections.at(_hdl).on_pong_received(_payload); } catch (const std::out_of_range &e) { @@ -534,13 +543,14 @@ namespace el::msglink */ void on_pong_timeout(wspp::connection_hdl _hdl, std::string _expected_payload) { - if (m_server_state != RUNNING) + std::lock_guard lock(mu_server_state); + if (server_state != RUNNING) return; // forward message to connection handler try { - m_open_connections.at(_hdl).on_pong_timeout(_expected_payload); + open_connections.at(_hdl).on_pong_timeout(_expected_payload); } catch (const std::out_of_range &e) { @@ -551,7 +561,7 @@ namespace el::msglink public: server(int _port) - : m_port(_port) + : port(_port) { EL_LOG_FUNCTION_CALL(); } @@ -575,34 +585,35 @@ namespace el::msglink */ void initialize() { - if (m_server_state != UNINITIALIZED) + std::lock_guard lock(mu_server_state); + if (server_state != UNINITIALIZED) throw initialization_error("msglink server instance is single use, cannot re-initialize"); try { // wspp log messages off by default - m_socket_server.clear_access_channels(wspp::log::alevel::all); - m_socket_server.clear_error_channels(wspp::log::elevel::all); + socket_server.clear_access_channels(wspp::log::alevel::all); + socket_server.clear_error_channels(wspp::log::elevel::all); // turn on selected logging channels - m_socket_server.set_access_channels(wspp::log::alevel::disconnect); - m_socket_server.set_access_channels(wspp::log::alevel::connect); - m_socket_server.set_access_channels(wspp::log::alevel::fail); - m_socket_server.set_error_channels(wspp::log::elevel::all); - //m_socket_server.set_access_channels(wspp::log::alevel::all); + socket_server.set_access_channels(wspp::log::alevel::disconnect); + socket_server.set_access_channels(wspp::log::alevel::connect); + socket_server.set_access_channels(wspp::log::alevel::fail); + socket_server.set_error_channels(wspp::log::elevel::all); + //socket_server.set_access_channels(wspp::log::alevel::all); // initialize asio communication - m_socket_server.init_asio(); + socket_server.init_asio(); // register callback handlers (More handlers: https://docs.websocketpp.org/reference_8handlers.html) - m_socket_server.set_open_handler(std::bind(&server::on_open, this, pl::_1)); - m_socket_server.set_message_handler(std::bind(&server::on_message, this, pl::_1, pl::_2)); - m_socket_server.set_close_handler(std::bind(&server::on_close, this, pl::_1)); - m_socket_server.set_fail_handler(std::bind(&server::on_fail, this, pl::_1)); - m_socket_server.set_pong_handler(std::bind(&server::on_pong_received, this, pl::_1, pl::_2)); - m_socket_server.set_pong_timeout_handler(std::bind(&server::on_pong_timeout, this, pl::_1, pl::_2)); + socket_server.set_open_handler(std::bind(&server::on_open, this, pl::_1)); + socket_server.set_message_handler(std::bind(&server::on_message, this, pl::_1, pl::_2)); + socket_server.set_close_handler(std::bind(&server::on_close, this, pl::_1)); + socket_server.set_fail_handler(std::bind(&server::on_fail, this, pl::_1)); + socket_server.set_pong_handler(std::bind(&server::on_pong_received, this, pl::_1, pl::_2)); + socket_server.set_pong_timeout_handler(std::bind(&server::on_pong_timeout, this, pl::_1, pl::_2)); // set reuse addr flag to allow faster restart times - m_socket_server.set_reuse_addr(true); + socket_server.set_reuse_addr(true); } catch (const wspp::exception &e) @@ -610,7 +621,7 @@ namespace el::msglink throw socket_error(e); } - m_server_state = INITIALIZED; + server_state = INITIALIZED; } /** @@ -623,32 +634,37 @@ namespace el::msglink */ void run() { - if (m_server_state == UNINITIALIZED) + std::unique_lock lock(mu_server_state); + if (server_state == UNINITIALIZED) throw launch_error("called server::run() before server::initialize()"); - else if (m_server_state != INITIALIZED) + else if (server_state != INITIALIZED) throw launch_error("called server::run() multiple times (msglink server instance is single use, cannot run multiple times)"); - + try { // listen on configured port - m_socket_server.listen(m_port); + socket_server.listen(port); // start accepting - m_socket_server.start_accept(); - + socket_server.start_accept(); + + server_state = RUNNING; + // free the lock so state can be accessed by callbacks + lock.unlock(); // run the io loop - m_server_state = RUNNING; - m_socket_server.run(); - m_server_state = STOPPED; + socket_server.run(); + // re-acquire ownership after loop exit. This might block until close function is done + lock.lock(); + server_state = STOPPED; } catch (const wspp::exception &e) { - m_server_state = FAILED; + server_state = FAILED; throw socket_error(e); } catch (...) { - m_server_state = FAILED; + server_state = FAILED; throw; } @@ -665,16 +681,17 @@ namespace el::msglink */ void stop() { + std::lock_guard lock(mu_server_state); // do nothing if server is not running - if (m_server_state != RUNNING) return; + if (server_state != RUNNING) return; try { // stop listening for new connections - m_socket_server.stop_listening(); + socket_server.stop_listening(); // close all existing connections - for (auto &[hdl, client] : m_open_connections) + for (auto &[hdl, client] : open_connections) { // use close_connection method to ensure communication is properly // stopped, preventing errors @@ -686,6 +703,7 @@ namespace el::msglink throw socket_error(e); } + // at this point mutex will be unlocked and run function will finish } }; From 648dd9e13fd6365922cf6018fe31e114523e1baf Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 4 Feb 2024 16:59:12 +0100 Subject: [PATCH 43/50] implemented thread safety in the link --- include/el/msglink/link.hpp | 82 +++++++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 7 deletions(-) diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index 0fa5de7..86b685c 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -72,9 +72,11 @@ namespace el::msglink // it is being used by a server or a client const int8_t tid_step_value; // running counter for transaction IDs (initialized to tid_step_value) - tid_t tid_counter; + std::atomic tid_counter; // uses atomic fetch and count // map of active transactions that take multiple back and forth messages to complete std::map active_transactions; + // mutex to guard transaction map + std::mutex mu_active_transactions; // flags set to track the authentication process soflag auth_ack_sent; @@ -95,7 +97,7 @@ namespace el::msglink std::set active_incoming_events; // running counter for all sorts of subscription IDs - sub_id_t sub_id_counter = 0; + std::atomic sub_id_counter = 0; // uses atomic increment // map of all active incoming events to their subscription IDs std::unordered_multimap< @@ -121,6 +123,11 @@ namespace el::msglink function_handler_function_t > available_incoming_function_names_to_functions; + // mutex tu guard and mutually exclude parallelism of calls by the library user + // that have something to do with event/datasource/function definition, listener registration + // and receiving/sending which all requires access to the above declared sets and maps. + std::recursive_mutex mu_user_calls; + private: // methods /** @@ -129,11 +136,10 @@ namespace el::msglink */ tid_t generate_new_tid() noexcept { - // use value before counting, so first value is 1/-1 - // as defined in the spec - auto tmp = tid_counter; - tid_counter += tid_step_value; - return tmp; + // no lock needed here because of atomic. + // fetch the OLD value and then add step value atomically + // so first value is 1/-1 as defined in the spec + return tid_counter.fetch_add(tid_step_value); } /** @@ -151,6 +157,8 @@ namespace el::msglink template _TR, typename... _Args> inline std::shared_ptr<_TR> create_transaction(tid_t _tid, _Args ..._args) { + std::lock_guard lock(mu_active_transactions); + if (active_transactions.contains(_tid)) throw duplicate_transaction_error("Transaction with ID=%d already exists", _tid); @@ -176,6 +184,8 @@ namespace el::msglink template _TR> inline std::shared_ptr<_TR> get_transaction(tid_t _tid) { + std::lock_guard lock(mu_active_transactions); + transaction_ptr_t transaction; try @@ -211,6 +221,8 @@ namespace el::msglink template _TR> inline void complete_transaction(const std::shared_ptr<_TR> &_transaction) noexcept { + std::lock_guard lock(mu_active_transactions); + active_transactions.erase(_transaction->id); } @@ -270,6 +282,13 @@ namespace el::msglink interface.send_message(msg); } + /** + * @brief encodes event data and sens an event emit message for a + * specific event + * + * @param _event_name event to send + * @param _evt data to encode + */ void send_event_emit_message( const std::string &_event_name, const outgoing_event &_evt @@ -292,6 +311,8 @@ namespace el::msglink const nlohmann::json &_jmsg ) { + // locked by on_message + switch (_msg_type) { using enum msg_type_t; @@ -388,6 +409,10 @@ namespace el::msglink void on_authentication_done() { EL_LOG_FUNCTION_CALL(); + + // this should already be locked by on_message, but lock again just to make sure + // in case that changes in the future + std::lock_guard lock(mu_user_calls); // send event subscribe messages for all events subscribed before // auth was complete (e.g. events with fixed handlers created during @@ -411,6 +436,8 @@ namespace el::msglink const nlohmann::json &_jmsg ) { + // locked by on_message + switch (_msg_type) { using enum msg_type_t; @@ -569,6 +596,8 @@ namespace el::msglink const std::string &_event_name, event_subscription::handler_function_t _handler_function ) { + std::lock_guard lock(mu_user_calls); + std::string event_name = _event_name; // copy for lambda capture // create subscription object const sub_id_t sub_id = generate_new_sub_id(); @@ -626,6 +655,8 @@ namespace el::msglink const std::string &_event_name, sub_id_t _subscription_id ) { + std::lock_guard lock(mu_user_calls); + // count amount of subscriptions left size_t sub_count = 0; @@ -715,6 +746,8 @@ namespace el::msglink template void define_event() { + std::lock_guard lock(mu_user_calls); + // save name std::string event_name = _ET::_event_name; @@ -746,6 +779,8 @@ namespace el::msglink event_sub_hdl_ptr define_event( void (_LT:: *_listener)(_ET &) ) { + std::lock_guard lock(mu_user_calls); + // save name and handler function std::string event_name = _ET::_event_name; std::function listener = _listener; @@ -790,6 +825,8 @@ namespace el::msglink event_sub_hdl_ptr define_event( void (*_listener)(_ET &) ) { + std::lock_guard lock(mu_user_calls); + // save name and handler function std::string event_name = _ET::_event_name; std::function listener = _listener; @@ -822,6 +859,8 @@ namespace el::msglink template void define_event() { + std::lock_guard lock(mu_user_calls); + // save name std::string event_name = _ET::_event_name; @@ -851,6 +890,8 @@ namespace el::msglink event_sub_hdl_ptr define_event( void (_LT:: *_listener)(_ET &) ) { + std::lock_guard lock(mu_user_calls); + // save name and handler function std::string event_name = _ET::_event_name; std::function listener = _listener; @@ -893,6 +934,8 @@ namespace el::msglink event_sub_hdl_ptr define_event( void (*_listener)(_ET &) ) { + std::lock_guard lock(mu_user_calls); + // save name and handler function std::string event_name = _ET::_event_name; std::function listener = _listener; @@ -923,6 +966,8 @@ namespace el::msglink template void define_event() { + std::lock_guard lock(mu_user_calls); + // save name std::string event_name = _ET::_event_name; @@ -958,6 +1003,8 @@ namespace el::msglink void define_function( typename _FT::results_t (_LT:: *_handler)(typename _FT::parameters_t &) ) { + std::lock_guard lock(mu_user_calls); + // save name and handler function std::string function_name = _FT::_function_name; std::function handler_fn = _handler; @@ -996,6 +1043,8 @@ namespace el::msglink void define_function( typename _FT::results_t (*_handler)(typename _FT::parameters_t &) ) { + std::lock_guard lock(mu_user_calls); + // save name and handler function std::string function_name = _FT::_function_name; std::function handler_fn = _handler; @@ -1036,6 +1085,8 @@ namespace el::msglink void define_function( typename _FT::results_t (_LT:: *_handler)(typename _FT::parameters_t &) ) { + std::lock_guard lock(mu_user_calls); + // save name and handler function std::string function_name = _FT::_function_name; std::function handler_fn = _handler; @@ -1071,6 +1122,8 @@ namespace el::msglink void define_function( typename _FT::results_t (*_handler)(typename _FT::parameters_t &) ) { + std::lock_guard lock(mu_user_calls); + // save name and handler function std::string function_name = _FT::_function_name; std::function handler_fn = _handler; @@ -1094,6 +1147,8 @@ namespace el::msglink template void define_function() { + std::lock_guard lock(mu_user_calls); + // save name std::string function_name = _FT::_function_name; @@ -1110,6 +1165,8 @@ namespace el::msglink template void emit(const _ET &_event) { + std::lock_guard lock(mu_user_calls); + // make sure that this event is defined if (!available_outgoing_events.contains(_ET::_event_name)) throw invalid_outgoing_event_error("Event '%s' cannot be emitted because it is not defined as outgoing", _ET::_event_name); @@ -1124,6 +1181,8 @@ namespace el::msglink template std::future call(const typename _FT::parameters_t &_params) { + std::lock_guard lock(mu_user_calls); + // create the transaction (this this initializes the promise) auto transaction = create_transaction( generate_new_tid(), @@ -1209,6 +1268,10 @@ namespace el::msglink { EL_LOGD("connection established called"); + // lock because this uses the maps, so no user call is allowed to modify them until done here + std::lock_guard lock(mu_user_calls); + + auto transaction = create_transaction( generate_new_tid(), inout_t::OUTGOING @@ -1237,6 +1300,9 @@ namespace el::msglink */ void on_message(const std::string &_msg_content) { + // lock here so no other external user calls can run while an incoming message is handeled + std::lock_guard lock(mu_user_calls); + try { nlohmann::json jmsg = nlohmann::json::parse(_msg_content); @@ -1285,6 +1351,8 @@ namespace el::msglink */ void on_pong_received() { + // no lock required here because this doesn't access any user-defined data + if (pong_messages_required) send_pong_message(); } From f55c0cf0833b65d0f5709bb0a93f0748214b3bd5 Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 4 Feb 2024 17:05:44 +0100 Subject: [PATCH 44/50] removed unused dependencies to ts-stl --- include/el/msglink/server.hpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index 44cfc57..4043514 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -13,6 +13,7 @@ msglink server class #pragma once +#include #include #include #include @@ -23,8 +24,6 @@ msglink server class #include #include -#include -#include #include "../retcode.hpp" #include "../logging.hpp" From 5382685a459d757ac1e792c44a430d5eb49bfbdc Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 25 Feb 2024 01:20:52 +0100 Subject: [PATCH 45/50] added the concept of a global class tree context for thread safety and adopted server and connection handler for it (link is next) --- include/el/msglink/internal/context.hpp | 54 ++++++++++++++++ include/el/msglink/server.hpp | 83 +++++++++++++++++-------- 2 files changed, 112 insertions(+), 25 deletions(-) create mode 100644 include/el/msglink/internal/context.hpp diff --git a/include/el/msglink/internal/context.hpp b/include/el/msglink/internal/context.hpp new file mode 100644 index 0000000..53cef97 --- /dev/null +++ b/include/el/msglink/internal/context.hpp @@ -0,0 +1,54 @@ +/* +ELEKTRON © 2024 - now +Written by melektron +www.elektron.work +23.02.24, 09:38 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Context class, of which an instance is created by the communication class and that is shared with all +objects related to the msglink communication class tree instance, such as client handlers and links. +This class contains global context data required for communication +*/ + +#pragma once + +#include + + +namespace el::msglink +{ + class ct_context + { + public: // types + + // type of the global class tree lock + using lock_type_t = std::unique_lock; + + private: + // mutex to guard the state of the entire msglink communication class tree and make + // it entirely thread safe. + // This has to be locked at the beginning of every public method call or other external entry + // into the class tree (such as asio callback). Lock using get_lock() method. + std::mutex master_guard; + + public: + + ct_context() = default; + + /** + * @brief acquires a lock on the master class tree guard + * and returns it. The lock is held until the object is destructed. + * + * @return lock_type_t (std::unique_lock) + */ + lock_type_t get_lock() + { + return lock_type_t(master_guard); + } + }; + +} // namespace el::msglink + diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index 4043514..7eaebfe 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -29,6 +29,7 @@ msglink server class #include "../logging.hpp" #include "internal/wspp.hpp" +#include "internal/context.hpp" #include "internal/link_interface.hpp" #include "errors.hpp" #include "link.hpp" @@ -50,17 +51,19 @@ namespace el::msglink * actions that are specific to but needed for all open connections. * * Methods of this class are only allowed to be called from within - * the main asio loop, so from the handlers in the - * server class. + * the msglink class tree, they are not end-user facing. This class expects + * the global class tree guard to be locked for all methods called from outside + * (there are also some asio callbacks inside) */ template _LT> class connection_handler : public link_interface { - friend class server<_LT>; private: // state + // global class tree context passed from server + ct_context &ctx; - // the server managing this client connection + // the websocket server managing this client connection (passed from msglink server) wsserver &socket_server; // a handle to the connection handled by this client @@ -113,9 +116,13 @@ namespace el::msglink /** * @brief initiates a websocket ping. * This is called periodically by timer. + * + * @attention (external entry: asio cb) */ void handle_ping_timer(const std::error_code &_ec) { + auto lock = ctx.get_lock(); + // if timer was canceled, do nothing. if (_ec == wspp::transport::error::operation_aborted) // the set_timer method intercepts the handler and changes the code to a non-default asio one return; @@ -247,13 +254,16 @@ namespace el::msglink * @brief called during on_open when new connection * is established. Used only for initialization. * - * @param _socket_server - * @param _connection + * @param _ctx global class tree context + * @param _socket_server websocket server the connection belongs to + * @param _connection handle to the connection */ - connection_handler(wsserver &_socket_server, wspp::connection_hdl _connection) - : socket_server(_socket_server) + connection_handler(ct_context &_ctx, wsserver &_socket_server, wspp::connection_hdl _connection) + : ctx(_ctx) + , socket_server(_socket_server) , m_connection(_connection) , m_link( + // TODO: pass context true, // is server instance *this // use this connection handler to communicate ) @@ -390,6 +400,8 @@ namespace el::msglink { private: + // global communication class tree context + ct_context ctx; // == Configuration // port to serve on @@ -409,10 +421,6 @@ namespace el::msglink STOPPED = 4 // run() exited cleanly (through stop() or other natural way) }; std::atomic server_state { UNINITIALIZED }; - // mutex to guard the server state, so state can not be changed from - // another thread while a function checking it at first is running. - std::mutex mu_server_state; - // set of connections to corresponding connection handler instance std::map< @@ -426,21 +434,22 @@ namespace el::msglink /** * @brief new websocket connection opened (fully connected) * This instantiates a connection handler. + * + * @attention (external entry: asio cb) * @param hdl websocket connection handle */ void on_open(wspp::connection_hdl _hdl) { - std::lock_guard lock(mu_server_state); + auto lock = ctx.get_lock(); + if (server_state != RUNNING) return; - // TODO: make atomic - // create new handler instance and save it auto new_connection = open_connections.emplace( std::piecewise_construct, // Needed for in-place construct https://en.cppreference.com/w/cpp/utility/piecewise_construct std::forward_as_tuple(_hdl), - std::forward_as_tuple(socket_server, _hdl) + std::forward_as_tuple(ctx, socket_server, _hdl) ); // notify new connection handler to start communication @@ -452,12 +461,14 @@ namespace el::msglink * This forwards the call to the appropriate connection handler * or throws if the connection is invalid. * + * @attention (external entry: asio cb) * @param _hdl ws connection handle * @param _msg message that was received */ void on_message(wspp::connection_hdl _hdl, wsserver::message_ptr _msg) { - std::lock_guard lock(mu_server_state); + auto lock = ctx.get_lock(); + if (server_state != RUNNING) return; @@ -478,15 +489,16 @@ namespace el::msglink * the associated connection handler and therefore * stops any tasks going on with that connection. * + * @attention (external entry: asio cb) * @param _hdl ws connection handle that has been closed */ void on_close(wspp::connection_hdl _hdl) { - std::lock_guard lock(mu_server_state); + auto lock = ctx.get_lock(); + if (server_state != RUNNING) return; - // TODO: make thread-safe (atomic) if (!open_connections.contains(_hdl)) { throw invalid_connection_error("Attempted to close an unknown/invalid connection which doesn't seem to exist."); @@ -503,10 +515,12 @@ namespace el::msglink * @brief called by wspp when a new connection was attempted but failed * before it was fully connected. * + * @attention (external entry: asio cb) * @param _hdl handle to associated ws connection */ void on_fail(wspp::connection_hdl _hdl) { + //auto lock = ctx.get_lock(); EL_LOG_FUNCTION_CALL(); } @@ -514,11 +528,13 @@ namespace el::msglink * @brief called by wspp when a pong is received. * This is forwarded to the connection handler. * + * @attention (external entry: asio cb) * @param _hdl handle to associated ws connection */ void on_pong_received(wspp::connection_hdl _hdl, std::string _payload) { - std::lock_guard lock(mu_server_state); + auto lock = ctx.get_lock(); + if (server_state != RUNNING) return; @@ -538,11 +554,13 @@ namespace el::msglink * used by the keepalive system to detect connection loss. * This call is forwarded to connection handler. * + * @attention (external entry: asio cb) * @param _hdl handle to connection where timeout occurred */ void on_pong_timeout(wspp::connection_hdl _hdl, std::string _expected_payload) { - std::lock_guard lock(mu_server_state); + auto lock = ctx.get_lock(); + if (server_state != RUNNING) return; @@ -559,6 +577,12 @@ namespace el::msglink public: + /** + * @brief Construct a new server object + * + * @attention (external entry: public method) + * @param _port TCP port to listen on + */ server(int _port) : port(_port) { @@ -569,6 +593,9 @@ namespace el::msglink server(const server &) = delete; server(server &&) = delete; + /** + * @attention (external entry: public method) + */ ~server() { EL_LOG_FUNCTION_CALL(); @@ -578,13 +605,15 @@ namespace el::msglink * @brief initializes the server setting up all transport settings * and preparing the server to run. This MUST be called before run(). * + * @attention (external entry: public method) * @throws msglink::initialization_error invalid state to initialize * @throws msglink::socket_error error while configuring networking * @throws other std exceptions possible */ void initialize() { - std::lock_guard lock(mu_server_state); + auto lock = ctx.get_lock(); + if (server_state != UNINITIALIZED) throw initialization_error("msglink server instance is single use, cannot re-initialize"); @@ -626,6 +655,7 @@ namespace el::msglink /** * @brief runs the server I/O loop (blocking) * + * @attention (external entry: public method) * @throws msglink::launch_error couldn't run server because of invalid state (e.g. not initialized) * @throws msglink::socket_error network communication / websocket error occurred * @throws other msglink::msglink_error? @@ -633,7 +663,8 @@ namespace el::msglink */ void run() { - std::unique_lock lock(mu_server_state); + auto lock = ctx.get_lock(); + if (server_state == UNINITIALIZED) throw launch_error("called server::run() before server::initialize()"); else if (server_state != INITIALIZED) @@ -673,14 +704,16 @@ namespace el::msglink * @brief stops the server if it is running, does nothing * otherwise (if it's not running). * - * This can be called from any thread. (TODO: make sure using mutex) + * This can be called from any thread. * + * @attention (external entry: public method) * @throws msglink::socket_error networking error occurred while stopping server * @throws other msglink::msglink_error? */ void stop() { - std::lock_guard lock(mu_server_state); + auto lock = ctx.get_lock(); + // do nothing if server is not running if (server_state != RUNNING) return; From 80e50246e5b2af3d1fc221ce52385382d0129d7b Mon Sep 17 00:00:00 2001 From: melektron Date: Sun, 25 Feb 2024 22:46:34 +0100 Subject: [PATCH 46/50] removed the old recursive link user call lock in favour of the global context lock --- include/el/msglink/link.hpp | 94 +++++++++++++++-------------------- include/el/msglink/server.hpp | 2 +- 2 files changed, 41 insertions(+), 55 deletions(-) diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index 86b685c..afcef9f 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -37,6 +37,7 @@ the user to define the API/protocol of a link #include "internal/proto_version.hpp" #include "internal/link_interface.hpp" #include "internal/transaction.hpp" +#include "internal/context.hpp" namespace el::msglink @@ -63,6 +64,9 @@ namespace el::msglink class link { private: + // global class tree context + ct_context &ctx; + // the link interface representing the underlying communication class // used to send messages and manage the connection link_interface &interface; @@ -76,7 +80,7 @@ namespace el::msglink // map of active transactions that take multiple back and forth messages to complete std::map active_transactions; // mutex to guard transaction map - std::mutex mu_active_transactions; + std::mutex mu_active_transactions; // TODO: no longer needed with context? // flags set to track the authentication process soflag auth_ack_sent; @@ -123,11 +127,6 @@ namespace el::msglink function_handler_function_t > available_incoming_function_names_to_functions; - // mutex tu guard and mutually exclude parallelism of calls by the library user - // that have something to do with event/datasource/function definition, listener registration - // and receiving/sending which all requires access to the above declared sets and maps. - std::recursive_mutex mu_user_calls; - private: // methods /** @@ -311,8 +310,6 @@ namespace el::msglink const nlohmann::json &_jmsg ) { - // locked by on_message - switch (_msg_type) { using enum msg_type_t; @@ -410,10 +407,6 @@ namespace el::msglink { EL_LOG_FUNCTION_CALL(); - // this should already be locked by on_message, but lock again just to make sure - // in case that changes in the future - std::lock_guard lock(mu_user_calls); - // send event subscribe messages for all events subscribed before // auth was complete (e.g. events with fixed handlers created during // definition) @@ -436,7 +429,6 @@ namespace el::msglink const nlohmann::json &_jmsg ) { - // locked by on_message switch (_msg_type) { @@ -491,6 +483,9 @@ namespace el::msglink try { auto sub = event_subscription_ids_to_objects.at(it->second); + // TODO: possibly release lock here temporarily as control is passed to user code? + // TODO: or maybe make this an async asio call? + // TODO: if lock is released, shouldn't it be done inside the subscription? sub->call_handler(msg.data); } catch(const std::out_of_range& e) @@ -526,6 +521,8 @@ namespace el::msglink nlohmann::json results_object; try { + // TODO: possibly release lock here temporarily as control is passed to user code? + // TODO: or maybe make this an async asio call? results_object = available_incoming_function_names_to_functions.at(msg.name)(msg.params); } catch (const std::exception &_e) @@ -596,7 +593,6 @@ namespace el::msglink const std::string &_event_name, event_subscription::handler_function_t _handler_function ) { - std::lock_guard lock(mu_user_calls); std::string event_name = _event_name; // copy for lambda capture // create subscription object @@ -655,8 +651,6 @@ namespace el::msglink const std::string &_event_name, sub_id_t _subscription_id ) { - std::lock_guard lock(mu_user_calls); - // count amount of subscriptions left size_t sub_count = 0; @@ -746,8 +740,6 @@ namespace el::msglink template void define_event() { - std::lock_guard lock(mu_user_calls); - // save name std::string event_name = _ET::_event_name; @@ -779,8 +771,6 @@ namespace el::msglink event_sub_hdl_ptr define_event( void (_LT:: *_listener)(_ET &) ) { - std::lock_guard lock(mu_user_calls); - // save name and handler function std::string event_name = _ET::_event_name; std::function listener = _listener; @@ -825,8 +815,6 @@ namespace el::msglink event_sub_hdl_ptr define_event( void (*_listener)(_ET &) ) { - std::lock_guard lock(mu_user_calls); - // save name and handler function std::string event_name = _ET::_event_name; std::function listener = _listener; @@ -859,8 +847,6 @@ namespace el::msglink template void define_event() { - std::lock_guard lock(mu_user_calls); - // save name std::string event_name = _ET::_event_name; @@ -890,8 +876,6 @@ namespace el::msglink event_sub_hdl_ptr define_event( void (_LT:: *_listener)(_ET &) ) { - std::lock_guard lock(mu_user_calls); - // save name and handler function std::string event_name = _ET::_event_name; std::function listener = _listener; @@ -934,8 +918,6 @@ namespace el::msglink event_sub_hdl_ptr define_event( void (*_listener)(_ET &) ) { - std::lock_guard lock(mu_user_calls); - // save name and handler function std::string event_name = _ET::_event_name; std::function listener = _listener; @@ -966,8 +948,6 @@ namespace el::msglink template void define_event() { - std::lock_guard lock(mu_user_calls); - // save name std::string event_name = _ET::_event_name; @@ -1003,8 +983,6 @@ namespace el::msglink void define_function( typename _FT::results_t (_LT:: *_handler)(typename _FT::parameters_t &) ) { - std::lock_guard lock(mu_user_calls); - // save name and handler function std::string function_name = _FT::_function_name; std::function handler_fn = _handler; @@ -1043,8 +1021,6 @@ namespace el::msglink void define_function( typename _FT::results_t (*_handler)(typename _FT::parameters_t &) ) { - std::lock_guard lock(mu_user_calls); - // save name and handler function std::string function_name = _FT::_function_name; std::function handler_fn = _handler; @@ -1085,8 +1061,6 @@ namespace el::msglink void define_function( typename _FT::results_t (_LT:: *_handler)(typename _FT::parameters_t &) ) { - std::lock_guard lock(mu_user_calls); - // save name and handler function std::string function_name = _FT::_function_name; std::function handler_fn = _handler; @@ -1122,8 +1096,6 @@ namespace el::msglink void define_function( typename _FT::results_t (*_handler)(typename _FT::parameters_t &) ) { - std::lock_guard lock(mu_user_calls); - // save name and handler function std::string function_name = _FT::_function_name; std::function handler_fn = _handler; @@ -1147,8 +1119,6 @@ namespace el::msglink template void define_function() { - std::lock_guard lock(mu_user_calls); - // save name std::string function_name = _FT::_function_name; @@ -1162,10 +1132,18 @@ namespace el::msglink * The following functions are used to access events, data subscriptions * or RPCs such as by registering listeners, emitting events or updating data. */ + + /** + * @brief emits a msglink event. + * + * @attention (external entry: public method) + * @tparam _ET event type to emit + * @param _event event body to emit + */ template void emit(const _ET &_event) { - std::lock_guard lock(mu_user_calls); + auto lock = ctx.get_lock(); // make sure that this event is defined if (!available_outgoing_events.contains(_ET::_event_name)) @@ -1177,11 +1155,24 @@ namespace el::msglink send_event_emit_message(_ET::_event_name, _event); } - + + /** + * @brief calls (or rather initiates) a msglink remote function. + * This returns a future that will contain the result of the function as soon + * as the remote party as responded with the result, or an exception if it + * responds with an error. This "call" method is supposed to be called from an + * external thread that is also able to await the future. Awaiting the future on the + * communication thread will block the asio i/o loop and therefore result in a deadlock. + * + * @attention (external entry: public method) + * @tparam _FT type of the function to call + * @param _params function parameters to pass + * @return std::future future function results data + */ template std::future call(const typename _FT::parameters_t &_params) { - std::lock_guard lock(mu_user_calls); + auto lock = ctx.get_lock(); // create the transaction (this this initializes the promise) auto transaction = create_transaction( @@ -1194,7 +1185,7 @@ namespace el::msglink // register response handlers // (being careful not to introduce cyclic references via shared ptr) - transaction->handle_result = [promise]( + transaction->handle_result = [promise]( // called from withing message handler, no external entry const nlohmann::json &_result ) { try @@ -1208,7 +1199,7 @@ namespace el::msglink promise->set_exception(std::current_exception()); } }; - transaction->handle_error = [promise]( + transaction->handle_error = [promise]( // called from withing message handler, no external entry const std::string &_info ) { // save error in promise @@ -1235,11 +1226,13 @@ namespace el::msglink /** * @brief Construct a new link object. * + * @param _ctx global class tree context passed from the owning class * @param _is_server determines the TID series used (+n or -n) * @param _interface interface representing the communication class used to manage connection */ - link(bool _is_server, link_interface &_interface) - : tid_step_value(_is_server ? 1 : -1) + link(ct_context &_ctx, bool _is_server, link_interface &_interface) + : ctx(_ctx) + , tid_step_value(_is_server ? 1 : -1) , tid_counter(tid_step_value) , interface(_interface) {} @@ -1268,10 +1261,6 @@ namespace el::msglink { EL_LOGD("connection established called"); - // lock because this uses the maps, so no user call is allowed to modify them until done here - std::lock_guard lock(mu_user_calls); - - auto transaction = create_transaction( generate_new_tid(), inout_t::OUTGOING @@ -1300,9 +1289,6 @@ namespace el::msglink */ void on_message(const std::string &_msg_content) { - // lock here so no other external user calls can run while an incoming message is handeled - std::lock_guard lock(mu_user_calls); - try { nlohmann::json jmsg = nlohmann::json::parse(_msg_content); diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index 7eaebfe..1beaf7e 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -263,7 +263,7 @@ namespace el::msglink , socket_server(_socket_server) , m_connection(_connection) , m_link( - // TODO: pass context + ctx, true, // is server instance *this // use this connection handler to communicate ) From 62bd56f8bd927aacd9f61f194f728d861b2f5251 Mon Sep 17 00:00:00 2001 From: melektron Date: Mon, 4 Mar 2024 17:18:13 +0100 Subject: [PATCH 47/50] implemented global thread safety (even in constructors) using context. User callbacks are run without a lock. (TODO: functino calls not jet) --- include/el/msglink/internal/context.hpp | 86 ++++++++++++++-- include/el/msglink/link.hpp | 17 +++- include/el/msglink/server.hpp | 8 +- include/el/msglink/subscriptions.hpp | 126 ++++++++++++++++++------ 4 files changed, 193 insertions(+), 44 deletions(-) diff --git a/include/el/msglink/internal/context.hpp b/include/el/msglink/internal/context.hpp index 53cef97..9824df8 100644 --- a/include/el/msglink/internal/context.hpp +++ b/include/el/msglink/internal/context.hpp @@ -16,27 +16,74 @@ This class contains global context data required for communication #pragma once #include - +#include namespace el::msglink { + + class tracking_mutex : + public std::mutex + { + private: + std::atomic m_holder = std::thread::id{}; + + public: + void lock() + { + std::mutex::lock(); + m_holder = std::this_thread::get_id(); + EL_LOGD("ct locked"); + } + + void unlock() + { + m_holder = std::thread::id(); + std::mutex::unlock(); + EL_LOGD("ct unlocked"); + } + + bool try_lock() + { + if (std::mutex::try_lock()) { + m_holder = std::thread::id(); + return true; + } + return false; + } + + /** + * @return true if the mutex is locked by the caller of this method. + */ + bool locked_by_caller() const + { + return m_holder == std::this_thread::get_id(); + } + + }; + class ct_context { public: // types - - // type of the global class tree lock - using lock_type_t = std::unique_lock; + using mutex_type_t = tracking_mutex; + using lock_type_t = std::unique_lock; private: // mutex to guard the state of the entire msglink communication class tree and make // it entirely thread safe. // This has to be locked at the beginning of every public method call or other external entry // into the class tree (such as asio callback). Lock using get_lock() method. - std::mutex master_guard; + mutex_type_t master_guard; + + public: + // the main io service used for communication and callback scheduling + std::unique_ptr io_service; public: - ct_context() = default; + ct_context() + : io_service(new asio::io_service()) + { + } /** * @brief acquires a lock on the master class tree guard @@ -48,6 +95,33 @@ namespace el::msglink { return lock_type_t(master_guard); } + + /** + * @brief acquires a lock on the master class tree guard + * and returns it unless a lock is already held by the calling thread. + * If the lock is already held, an empty unique_lock is returned. + * Because of this, the returned object should not be accessed with the assumption of an owning lock. + * If the lock is held by it, it is held until the object is destroyed. + * In any case, this function guarantees the calling thread is holding the lock + * after return. + * + * This function is used in places when it cannot be guaranteed that a call is external, such as in destructors. + * It should only be used sparely. + * + * @return lock_type_t lock if the lock was not locked jet + */ + lock_type_t get_soft_lock() + { + // This operation is not atomic. It could happen, that between this owner check + // and the following attemted lock, another thread locks the mutex. In that case, the calling + // thread has to wait. + // This is not a problem however. The only thing this protects against is that the same thread + // doesn't try to re-acquire the lock. + if (master_guard.locked_by_caller()) + return lock_type_t(); + // actually get the lock + return lock_type_t(master_guard); + } }; } // namespace el::msglink diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index afcef9f..d97ea09 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -483,10 +483,14 @@ namespace el::msglink try { auto sub = event_subscription_ids_to_objects.at(it->second); - // TODO: possibly release lock here temporarily as control is passed to user code? - // TODO: or maybe make this an async asio call? - // TODO: if lock is released, shouldn't it be done inside the subscription? - sub->call_handler(msg.data); + // the callback is scheduled using asio, so it is called independently of this + // call stack. This way, the subscription can manage the lock and a lock + // is not held during the callback to the user code. + EL_LOGD("Hello from direct callback"); + ctx.io_service->post([sub, data = msg.data](){ + sub->asio_cb_call_handler(data); + }); + EL_LOGD("After post"); } catch(const std::out_of_range& e) { @@ -598,6 +602,7 @@ namespace el::msglink // create subscription object const sub_id_t sub_id = generate_new_sub_id(); auto subscription = std::shared_ptr(new event_subscription( + ctx, _handler_function, [this, _event_name, sub_id](void) // cancel function { @@ -635,6 +640,7 @@ namespace el::msglink exit: return subscription_hdl_ptr( new subscription_hdl( + ctx, subscription ) ); @@ -1239,6 +1245,9 @@ namespace el::msglink ~link() { + // possibly external entry + auto lock = ctx.get_soft_lock(); + EL_LOG_FUNCTION_CALL(); // invalidate all event subscriptions to make sure there are no dangling pointers for (auto &[id, sub] : event_subscription_ids_to_objects) diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp index 1beaf7e..ee87121 100644 --- a/include/el/msglink/server.hpp +++ b/include/el/msglink/server.hpp @@ -282,6 +282,7 @@ namespace el::msglink */ virtual ~connection_handler() { + auto lock = ctx.get_soft_lock(); EL_LOG_FUNCTION_CALL(); // cancel ping timer if one is running @@ -598,6 +599,7 @@ namespace el::msglink */ ~server() { + auto lock = ctx.get_soft_lock(); EL_LOG_FUNCTION_CALL(); } @@ -629,8 +631,8 @@ namespace el::msglink socket_server.set_error_channels(wspp::log::elevel::all); //socket_server.set_access_channels(wspp::log::alevel::all); - // initialize asio communication - socket_server.init_asio(); + // initialize asio communication (use external io service from context) + socket_server.init_asio(ctx.io_service.get()); // register callback handlers (More handlers: https://docs.websocketpp.org/reference_8handlers.html) socket_server.set_open_handler(std::bind(&server::on_open, this, pl::_1)); @@ -689,11 +691,13 @@ namespace el::msglink } catch (const wspp::exception &e) { + lock.lock(); server_state = FAILED; throw socket_error(e); } catch (...) { + lock.lock(); // TODO: may be called on lock exception in which case no second lock should be attempted. server_state = FAILED; throw; } diff --git a/include/el/msglink/subscriptions.hpp b/include/el/msglink/subscriptions.hpp index 369851e..dd1f29e 100644 --- a/include/el/msglink/subscriptions.hpp +++ b/include/el/msglink/subscriptions.hpp @@ -20,6 +20,7 @@ Structures representing various types of subscriptions. #include "../flags.hpp" #include "../logging.hpp" #include "internal/types.hpp" +#include "internal/context.hpp" namespace el::msglink @@ -27,44 +28,76 @@ namespace el::msglink /** * @brief structure representing an event subscription which * is used to identify and cancel the subscription later. - * + * This is not a public class. It is only for use withing the communication + * class tree. */ class event_subscription { protected: friend class link; - // invalidates all potential references and callbacks to the link to - // prevent any calls back to a potentially deallocated link instance. - // This is called by the link destructor. - void invalidate() - { - cancel_function = nullptr; - handler_function = nullptr; - } - // type of the lambda used to wrap event handlers using handler_function_t = std::function; // type of the lambda used for the cancel callback using cancel_function_t = std::function; + // global class tree context + ct_context &ctx; + // function called when the event is received handler_function_t handler_function; // function called to cancel the event (will be a // lambda created by the link) cancel_function_t cancel_function; - void call_handler(const nlohmann::json &_data) + // invalidates all potential references and callbacks to the link to + // prevent any calls back to a potentially deallocated link instance. + // This is called by the link destructor, so it is no external entry + void invalidate() + { + cancel_function = nullptr; + handler_function = nullptr; + } + + /** + * @brief Function to be called from an asio callback + * when the event is received. Because this is called by + * an asio callback, this is an external entry. + * + * @attention (external entry: asio callback) + * @param _data + */ + void asio_cb_call_handler(const nlohmann::json &_data) { + auto lock = ctx.get_lock(); + if (handler_function != nullptr) - handler_function(_data); + { + auto handler_copy = handler_function; + // Unlock to allow user callback to run without lock, so it can call external tree entries + lock.unlock(); + // possible inconsistency problem when subscription is canceled by another thread right here. + // The function will still be called because of the copy, but user may not expect it. + // At this time, I don't know how to solve this without keeping the tree locked. + handler_copy(_data); + lock.lock(); // re-lock after callback in case more has to be done + } } + /** + * @brief Construct a new subscription + * + * @param _ctx global class tree context + * @param _handler_function user code handler function (with decode wrapper) + * @param _cancel_function internal cancellation function + */ event_subscription( + ct_context &_ctx, handler_function_t _handler_function, cancel_function_t _cancel_function ) - : handler_function(_handler_function) + : ctx(_ctx) + , handler_function(_handler_function) , cancel_function(_cancel_function) {} @@ -72,10 +105,25 @@ namespace el::msglink event_subscription(const event_subscription &) = default; event_subscription(event_subscription &&) = default; + /** + * @brief Invalidates the object. + */ + ~event_subscription() + { + auto lock = ctx.get_soft_lock(); + + EL_LOG_FUNCTION_CALL(); + + invalidate(); + } + /** * @brief function to cancel the subscription and therefore * unsubscribe from the event. If already canceled, this does * nothing. + * + * This is only called from subscription handle and is therefore + * not an external entry. */ void cancel() { @@ -85,15 +133,6 @@ namespace el::msglink cancel_function = nullptr; // only cancel once } } - - /** - * @brief Invalidates the object - */ - ~event_subscription() - { - EL_LOG_FUNCTION_CALL(); - invalidate(); - } }; /** @@ -101,16 +140,19 @@ namespace el::msglink * RAII functionality. * When a subscription is returned from the msglink library * to user code, it is wrapped by a subscription handle. The handle - * itself is wrapped by a shared_ptr, so there is only one handle + * itself is wrapped by a shared_ptr, so there is only ever one handle * per subscription. * The subscription is automatically canceled when the lifetime of * the subscription handle ends, i.e. when no nobody holds a reference * to it anymore. - * One can also cancel the subscription manually before this happens using - * the cancel() method. - * * This is to prevent callbacks to class instances which don't exist anymore * when forgetting to cancel subscriptions in class destructors. + * One can also cancel the subscription manually before this happens using + * the cancel() method though. + * + * @attention The subscription handle is an entirely external, public object. Apart + * from construction, it is never accessed from the communication class tree. + * As such, all functions it calls internally count as external entries. */ template class subscription_hdl @@ -119,21 +161,37 @@ namespace el::msglink protected: - // the managed subscription - std::shared_ptr<_SUB_T> subscription_ptr; + // global class tree context + ct_context &ctx; + + // the managed subscription (this instance and the link instance are the only two references of this) + std::shared_ptr<_SUB_T> subscription_ptr; // must be a counted reference (not weak) to ensure ->cancel() is called on subscription // no copy or move subscription_hdl(const subscription_hdl &) = delete; subscription_hdl(subscription_hdl &&) = delete; + // only link is allowed to construct instance with valid subscription pointer - subscription_hdl(std::shared_ptr<_SUB_T> _sub_ptr) - : subscription_ptr(_sub_ptr) + subscription_hdl( + ct_context &_ctx, + std::shared_ptr<_SUB_T> _sub_ptr + ) + : ctx(_ctx) + , subscription_ptr(_sub_ptr) { - EL_LOG_FUNCTION_CALL();} + EL_LOG_FUNCTION_CALL(); + } public: + + /** + * cancels the subscription + */ ~subscription_hdl() { + // possibly external entry + auto lock = ctx.get_soft_lock(); + EL_LOG_FUNCTION_CALL(); if (subscription_ptr != nullptr) subscription_ptr->cancel(); @@ -143,15 +201,19 @@ namespace el::msglink * @brief cancels the subscription even if there * are still references to the subscription_hdl. * Usually, this is not required. + * + * @attention (external entry: public method) */ void cancel() { + auto lock = ctx.get_lock(); + if (subscription_ptr != nullptr) subscription_ptr->cancel(); } }; - // shortcut for shared pointer to subscription handle + // shortcut for shared pointer to subscription handle which is the object actually returned to user code template using subscription_hdl_ptr = std::shared_ptr>; From 1c420c2d4e78cf912b52ed464bba36f589e8a751 Mon Sep 17 00:00:00 2001 From: melektron Date: Tue, 5 Mar 2024 10:16:28 +0100 Subject: [PATCH 48/50] implemented thread-safety and lock-free calling of function handlers --- include/el/msglink/link.hpp | 52 +++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index d97ea09..8099830 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -486,11 +486,9 @@ namespace el::msglink // the callback is scheduled using asio, so it is called independently of this // call stack. This way, the subscription can manage the lock and a lock // is not held during the callback to the user code. - EL_LOGD("Hello from direct callback"); ctx.io_service->post([sub, data = msg.data](){ sub->asio_cb_call_handler(data); }); - EL_LOGD("After post"); } catch(const std::out_of_range& e) { @@ -520,30 +518,40 @@ namespace el::msglink EL_LOGW("Received FUNC_CALL message for a function which isn't incoming and/or doesn't exist. This is likely a library implementation issue and should not happen."); break; } + + // the handler is scheduled using asio, so it is called independently of this + // call stack. This way, no lock is held during the callback to user code. + // the handler function is copied from the locked map, so it is guaranteed to be valid during the asio callback. + ctx.io_service->post([this, msg = msg, handler = available_incoming_function_names_to_functions.at(msg.name)](){ + // run the handler without holding the lock + nlohmann::json results_object; + try + { + results_object = handler(msg.params); + } + catch (const std::exception &_e) + { + // acquire lock since this is an external callback + auto lock = ctx.get_lock(); + + // error during handler execution, respond with error message + msg_func_err_t response; + response.tid = msg.tid; + response.info = _e.what(); + interface.send_message(response); + return; + } - // run the handler - nlohmann::json results_object; - try - { - // TODO: possibly release lock here temporarily as control is passed to user code? - // TODO: or maybe make this an async asio call? - results_object = available_incoming_function_names_to_functions.at(msg.name)(msg.params); - } - catch (const std::exception &_e) - { - // error during handler execution, respond with error message - msg_func_err_t response; + // re-acquire lock from external callback + auto lock = ctx.get_lock(); + + // send result on success + msg_func_result_t response; response.tid = msg.tid; - response.info = _e.what(); + response.results = results_object; interface.send_message(response); - break; - } + }); - // otherwise send result - msg_func_result_t response; - response.tid = msg.tid; - response.results = results_object; - interface.send_message(response); } break; case FUNC_ERR: From bb9f8680a953d1209695f7887387791ca4e6c25a Mon Sep 17 00:00:00 2001 From: melektron Date: Mon, 11 Mar 2024 17:28:46 +0100 Subject: [PATCH 49/50] removed now unnecessary transaction locks --- include/el/msglink/link.hpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp index 8099830..8c0a83b 100644 --- a/include/el/msglink/link.hpp +++ b/include/el/msglink/link.hpp @@ -79,8 +79,6 @@ namespace el::msglink std::atomic tid_counter; // uses atomic fetch and count // map of active transactions that take multiple back and forth messages to complete std::map active_transactions; - // mutex to guard transaction map - std::mutex mu_active_transactions; // TODO: no longer needed with context? // flags set to track the authentication process soflag auth_ack_sent; @@ -156,8 +154,6 @@ namespace el::msglink template _TR, typename... _Args> inline std::shared_ptr<_TR> create_transaction(tid_t _tid, _Args ..._args) { - std::lock_guard lock(mu_active_transactions); - if (active_transactions.contains(_tid)) throw duplicate_transaction_error("Transaction with ID=%d already exists", _tid); @@ -183,8 +179,6 @@ namespace el::msglink template _TR> inline std::shared_ptr<_TR> get_transaction(tid_t _tid) { - std::lock_guard lock(mu_active_transactions); - transaction_ptr_t transaction; try @@ -220,8 +214,6 @@ namespace el::msglink template _TR> inline void complete_transaction(const std::shared_ptr<_TR> &_transaction) noexcept { - std::lock_guard lock(mu_active_transactions); - active_transactions.erase(_transaction->id); } From a3ec859961c5c5f648d1dbb04ce3630c58b9ec0f Mon Sep 17 00:00:00 2001 From: melektron Date: Tue, 18 Jun 2024 00:17:19 +0200 Subject: [PATCH 50/50] linear mapping function and flags to disable exceptions --- include/el/cxxversions.h | 15 +++++++++++++ include/el/nummap.hpp | 46 ++++++++++++++++++++++++++++++++++++++++ include/el/strutil.hpp | 8 +++++++ include/el/types.hpp | 7 ++++-- 4 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 include/el/nummap.hpp diff --git a/include/el/cxxversions.h b/include/el/cxxversions.h index 7caf94a..5aba88c 100644 --- a/include/el/cxxversions.h +++ b/include/el/cxxversions.h @@ -63,4 +63,19 @@ to enable library features for versions not detected using the __cplusplus defin #define __EL_CXX20 #define __EL_ENABLE_CXX20 +#endif + + +/** + * == Library feature selection == + * Some features might not be desired on certain platforms, + * such as the use of RTTI in embedded systems. Flags defined + * here are used to switch of these features. This may + * have the effect of certain classes acting differently. + */ + +#ifndef EL_DISABLE_EXCEPTIONS + +#define __EL_ENABLE_EXCEPTIONS + #endif \ No newline at end of file diff --git a/include/el/nummap.hpp b/include/el/nummap.hpp new file mode 100644 index 0000000..20d5f80 --- /dev/null +++ b/include/el/nummap.hpp @@ -0,0 +1,46 @@ +/* +ELEKTRON © 2024 - now +Written by melektron +www.elektron.work +18.06.24, 00:07 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +numerical mapping functions +*/ + +#pragma once + +#include "cxxversions.h" + + +namespace el +{ + /** + * @brief linearly maps a number from range [_in_a .. _in_b] to + * range [_out_a .. _out_b]. This may include inversions + * in direction (e.g. mapping 1-4 to 4-1). The number + * does not have to be within and is not clamped to the specified ranges, + * they merely serve as the two points required to identify a linear function. + * + * @tparam _NT number type used (when using small integers, internally used division may cause unusable output) + * @param _x number to map + * @param _in_a first input value (that will be mapped to _out_a) + * @param _in_b second input value (that will be mapped to _out_b) + * @param _out_a first output value (mapped from _in_a) + * @param _out_b second output value (mapped from _in_b) + * @return _NT mapped number + */ + template + _NT map_lin( + _NT _x, + _NT _in_a, + _NT _in_b, + _NT _out_a, + _NT _out_b + ) { + return (_x - _in_a) * (_out_b - _out_a) / (_in_b - _in_a) + _out_a; + } +} // namespace el diff --git a/include/el/strutil.hpp b/include/el/strutil.hpp index fe7c890..e31541c 100644 --- a/include/el/strutil.hpp +++ b/include/el/strutil.hpp @@ -20,6 +20,9 @@ Utility functions operating for strings, mostly STL compatible string types. #include #include +#include "cxxversions.h" + + namespace el::strutil { /** @@ -42,7 +45,12 @@ namespace el::strutil int len_or_error = std::snprintf(nullptr, 0, _fmt.c_str(), _args...) + 1; // extra space for null byte if( len_or_error <= 0 ) + #ifdef __EL_ENABLE_EXCEPTIONS throw std::runtime_error( "Error during formatting." ); + #else + return ""; + #endif + auto len = static_cast(len_or_error); std::unique_ptr _cstr(new char[len]); diff --git a/include/el/types.hpp b/include/el/types.hpp index 63471e5..9b57406 100644 --- a/include/el/types.hpp +++ b/include/el/types.hpp @@ -70,7 +70,7 @@ namespace el::types std::string to_string() const noexcept { - return strutil::format("(r=%3d, g=%3d, b=%3d)", r, g, b); + return strutil::format("(r=%3d, g=%3d, b=%3d)", r, g, b); } /** @@ -122,7 +122,7 @@ namespace el::types std::string to_string() const noexcept { - return strutil::format("(r=%3lf, g=%3lf, b=%3lf)", r, g, b); + return strutil::format("(r=%3lf, g=%3lf, b=%3lf)", r, g, b); } /** @@ -149,5 +149,8 @@ namespace el::types } }; + // destructures RGB colors into three function parameters + #define EL_RGB_DESTRUCTURE(c) (c).r, (c).g, (c).b + using mac_t = uint64_t; }; \ No newline at end of file