From 922e01018f97b22de0b17c30fbce5884c5526f6e Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Tue, 2 Apr 2024 10:35:30 -0400 Subject: [PATCH 01/35] Started websocket support --- docker/ubuntu/Dockerfile | 2 +- src/mtconnect/sink/rest_sink/session_impl.cpp | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docker/ubuntu/Dockerfile b/docker/ubuntu/Dockerfile index 7221661d..7372a546 100644 --- a/docker/ubuntu/Dockerfile +++ b/docker/ubuntu/Dockerfile @@ -63,7 +63,7 @@ RUN apt-get update \ rake \ ruby \ && rm -rf /var/lib/apt/lists/* \ - && pip install conan -v 'conan==2.0.9' + && pip install conan # make an agent directory and cd into it WORKDIR /root/agent diff --git a/src/mtconnect/sink/rest_sink/session_impl.cpp b/src/mtconnect/sink/rest_sink/session_impl.cpp index 93aae58f..5faa2931 100644 --- a/src/mtconnect/sink/rest_sink/session_impl.cpp +++ b/src/mtconnect/sink/rest_sink/session_impl.cpp @@ -44,6 +44,7 @@ namespace mtconnect::sink::rest_sink { namespace algo = boost::algorithm; namespace sys = boost::system; namespace ssl = boost::asio::ssl; + namespace ws = boost::beast::websocket; using namespace std; using std::placeholders::_1; @@ -196,7 +197,7 @@ namespace mtconnect::sink::rest_sink { auto &msg = m_parser->get(); const auto &remote = beast::get_lowest_layer(derived().stream()).socket().remote_endpoint(); - + // Check for put, post, or delete if (msg.method() != http::verb::get) { @@ -242,6 +243,12 @@ namespace mtconnect::sink::rest_sink { LOG(info) << "ReST Request: From [" << m_request->m_foreignIp << ':' << remote.port() << "]: " << msg.method() << " " << msg.target(); + + // Check if this is a websocket upgrade request. If so, begin a websocket session. + if (ws::is_upgrade(msg)) + { + LOG(debug) << "Upgrading to websocket request"; + } if (!m_dispatch(shared_ptr(), m_request)) { From 27ec271c13f53ab7d0943835e8109bd417f2f48b Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Sat, 6 Apr 2024 17:21:40 -0400 Subject: [PATCH 02/35] use new uuid to check for device with * uuid shdr command --- src/mtconnect/source/adapter/shdr/shdr_adapter.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/mtconnect/source/adapter/shdr/shdr_adapter.cpp b/src/mtconnect/source/adapter/shdr/shdr_adapter.cpp index d3c685ab..161eb13b 100644 --- a/src/mtconnect/source/adapter/shdr/shdr_adapter.cpp +++ b/src/mtconnect/source/adapter/shdr/shdr_adapter.cpp @@ -227,9 +227,7 @@ namespace mtconnect::source::adapter::shdr { if (command == "uuid") { - DevicePtr dp; - if (auto dev = GetOption(m_options, configuration::Device); - dev && (dp = m_pipeline.getContext()->m_contract->findDevice(*dev))) + if (auto dp = m_pipeline.getContext()->m_contract->findDevice(value); dp) { if (!dp->preserveUuid()) { From b5ff77581419b0cd1a22a2c0c0078cf2a2936924 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Sat, 6 Apr 2024 17:25:04 -0400 Subject: [PATCH 03/35] Version 2.3.0.6 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f47aa6a8..1459c364 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ set(AGENT_VERSION_MAJOR 2) set(AGENT_VERSION_MINOR 3) set(AGENT_VERSION_PATCH 0) -set(AGENT_VERSION_BUILD 5) +set(AGENT_VERSION_BUILD 6) set(AGENT_VERSION_RC "") # This minimum version is to support Visual Studio 2019 and C++ feature checking and FetchContent From 4dd060246294a9d923cd352d4b20759627df8062 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Sat, 6 Apr 2024 17:47:17 -0400 Subject: [PATCH 04/35] upgraded to boost 1.84 --- conan/mqtt_cpp/conanfile.py | 2 +- conanfile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conan/mqtt_cpp/conanfile.py b/conan/mqtt_cpp/conanfile.py index 456503ef..308a349c 100644 --- a/conan/mqtt_cpp/conanfile.py +++ b/conan/mqtt_cpp/conanfile.py @@ -10,7 +10,7 @@ class MqttcppConan(ConanFile): url = "https://github.com/redboltz/mqtt_cpp" description = "MQTT client/server for C++14 based on Boost.Asio" topics = ("mqtt") - requires = ["boost/1.82.0"] + requires = ["boost/1.84.0"] no_copy_source = True exports_sources = "include/*" diff --git a/conanfile.py b/conanfile.py index 5dfe8587..2c384fb3 100644 --- a/conanfile.py +++ b/conanfile.py @@ -118,7 +118,7 @@ def build_requirements(self): self.tool_requires_version("doxygen", [1, 9, 4]) def requirements(self): - self.requires("boost/1.82.0", headers=True, libs=True, transitive_headers=True, transitive_libs=True) + self.requires("boost/1.84.0", headers=True, libs=True, transitive_headers=True, transitive_libs=True) self.requires("libxml2/2.10.3", headers=True, libs=True, visible=True, transitive_headers=True, transitive_libs=True) self.requires("date/2.4.1", headers=True, libs=True, transitive_headers=True, transitive_libs=True) self.requires("nlohmann_json/3.9.1", headers=True, libs=False, transitive_headers=True, transitive_libs=False) From bca17068736662ca3c6d058c2c7594708a5878d2 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Mon, 8 Apr 2024 13:52:13 -0400 Subject: [PATCH 05/35] checkpoint with initial parsing of json --- agent_lib/CMakeLists.txt | 1 + src/mtconnect/sink/rest_sink/request.hpp | 1 + src/mtconnect/sink/rest_sink/session_impl.cpp | 32 +- src/mtconnect/sink/rest_sink/session_impl.hpp | 14 +- .../sink/rest_sink/websocket_session.hpp | 293 ++++++++++++++++++ 5 files changed, 338 insertions(+), 3 deletions(-) create mode 100644 src/mtconnect/sink/rest_sink/websocket_session.hpp diff --git a/agent_lib/CMakeLists.txt b/agent_lib/CMakeLists.txt index ac5763e4..4e0a9970 100644 --- a/agent_lib/CMakeLists.txt +++ b/agent_lib/CMakeLists.txt @@ -266,6 +266,7 @@ set(AGENT_SOURCES "${SOURCE_DIR}/sink/rest_sink/session.hpp" "${SOURCE_DIR}/sink/rest_sink/session_impl.hpp" "${SOURCE_DIR}/sink/rest_sink/tls_dector.hpp" + "${SOURCE_DIR}/sink/rest_sink/websocket_session.hpp" # src/sink/rest_sink SOURCE_FILES_ONLY diff --git a/src/mtconnect/sink/rest_sink/request.hpp b/src/mtconnect/sink/rest_sink/request.hpp index f62f37db..5271c791 100644 --- a/src/mtconnect/sink/rest_sink/request.hpp +++ b/src/mtconnect/sink/rest_sink/request.hpp @@ -67,6 +67,7 @@ namespace mtconnect::sink::rest_sink { uint16_t m_foreignPort; ///< The requestors Port QueryMap m_query; ///< The parsed query parameters ParameterMap m_parameters; ///< The parsed path parameters + bool m_parsed { false }; ///< Flag the request as parsed /// @brief Find a parameter by type /// @tparam T the type of the parameter diff --git a/src/mtconnect/sink/rest_sink/session_impl.cpp b/src/mtconnect/sink/rest_sink/session_impl.cpp index 5faa2931..2ddd38e3 100644 --- a/src/mtconnect/sink/rest_sink/session_impl.cpp +++ b/src/mtconnect/sink/rest_sink/session_impl.cpp @@ -34,6 +34,7 @@ #include "request.hpp" #include "response.hpp" #include "tls_dector.hpp" +#include "websocket_session.hpp" namespace mtconnect::sink::rest_sink { namespace beast = boost::beast; // from @@ -248,9 +249,9 @@ namespace mtconnect::sink::rest_sink { if (ws::is_upgrade(msg)) { LOG(debug) << "Upgrading to websocket request"; + upgrade(std::move(msg)); } - - if (!m_dispatch(shared_ptr(), m_request)) + else if (!m_dispatch(shared_ptr(), m_request)) { ostringstream txt; txt << "Failed to find handler for " << msg.method() << " " << msg.target(); @@ -468,6 +469,16 @@ namespace mtconnect::sink::rest_sink { writeResponse(std::move(response)); } } + + SessionPtr HttpSession::upgradeToWebsocket(RequestMessage &&msg) + { + return std::make_shared(std::move(m_stream), + std::move(m_request), + std::move(msg), + m_dispatch, + m_errorFunction); + } + /// @brief A secure https session class HttpsSession : public SessionImpl @@ -525,6 +536,16 @@ namespace mtconnect::sink::rest_sink { m_stream.async_shutdown(beast::bind_front_handler(&HttpsSession::shutdown, shared_ptr())); } } + + /// @brief Upgrade the current connection to a websocket connection. + SessionPtr upgradeToWebsocket(RequestMessage &&msg) + { + return std::make_shared(std::move(m_stream), + std::move(m_request), + std::move(msg), + m_dispatch, + m_errorFunction); + } protected: void handshake(beast::error_code ec, size_t bytes_used) @@ -548,6 +569,13 @@ namespace mtconnect::sink::rest_sink { beast::ssl_stream m_stream; bool m_closing {false}; }; + + template + void SessionImpl::upgrade(RequestMessage &&msg) + { + LOG(debug) << "Upgrading session to websockets"; + derived().upgradeToWebsocket(std::move(msg))->run(); + } void TlsDector::run() { diff --git a/src/mtconnect/sink/rest_sink/session_impl.hpp b/src/mtconnect/sink/rest_sink/session_impl.hpp index 404d25b9..0d811a84 100644 --- a/src/mtconnect/sink/rest_sink/session_impl.hpp +++ b/src/mtconnect/sink/rest_sink/session_impl.hpp @@ -35,6 +35,12 @@ namespace mtconnect { } namespace sink::rest_sink { + template + class WebsocketSession; + template + using WebsocketSessionPtr = std::shared_ptr>; + + /// @brief A session implementation `Derived` subclass pattern /// @tparam subclass of this class to use the same methods with http or https protocol streams template @@ -61,7 +67,7 @@ namespace mtconnect { return std::dynamic_pointer_cast(shared_from_this()); } /// @brief get this as the `Derived` type - /// @return + /// @return the subclass Derived &derived() { return static_cast(*this); } /// @name Session Interface @@ -74,6 +80,8 @@ namespace mtconnect { void closeStream() override; ///@} protected: + using RequestMessage = boost::beast::http::request; + template void addHeaders(const Response &response, T &res); @@ -81,6 +89,7 @@ namespace mtconnect { void sent(boost::system::error_code ec, size_t len); void read(); void reset(); + void upgrade(RequestMessage &&msg); protected: using RequestParser = boost::beast::http::request_parser; @@ -144,6 +153,9 @@ namespace mtconnect { boost::beast::error_code ec; m_stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); } + + /// @brief Upgrade the current connection to a websocket connection. + SessionPtr upgradeToWebsocket(RequestMessage &&msg); protected: boost::beast::tcp_stream m_stream; diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp new file mode 100644 index 00000000..2d87ced2 --- /dev/null +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -0,0 +1,293 @@ +// +// Copyright Copyright 2009-2022, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#pragma once + +#include +#include + +#include +#include + +#include +#include +#include + +#include "mtconnect/config.hpp" +#include "mtconnect/configuration/config_options.hpp" +#include "mtconnect/utilities.hpp" +#include "session.hpp" + +namespace mtconnect::sink::rest_sink { + namespace beast = boost::beast; + + /// @brief A websocket session that provides a pubsub interface using REST parameters + template + class WebsocketSession : public Session + { + public: + using RequestMessage = boost::beast::http::request; + + WebsocketSession(RequestPtr &&request, RequestMessage &&msg, Dispatch dispatch, ErrorFunction func) + : Session(dispatch, func), m_request(std::move(request)), m_msg(std::move(msg)) + { + } + + /// @brief Session cannot be copied. + WebsocketSession(const WebsocketSession &) = delete; + ~WebsocketSession() = default; + + /// @brief get this as the `Derived` type + /// @return the subclass + Derived &derived() { return static_cast(*this); } + + + void run() override + { + using namespace boost::beast; + + // Set suggested timeout settings for the websocket + derived().stream().set_option( + websocket::stream_base::timeout::suggested( + beast::role_type::server)); + + // Set a decorator to change the Server of the handshake + derived().stream().set_option( + websocket::stream_base::decorator( + [](websocket::response_type& res) + { + res.set(http::field::server, + GetAgentVersion() + + " MTConnectAgent"); + })); + + // Accept the websocket handshake + derived().stream().async_accept( + m_msg, + beast::bind_front_handler( + &WebsocketSession::onAccept, + derived().shared_ptr())); + } + + void writeResponse(ResponsePtr &&response, Complete complete = nullptr) override + { + + } + + void writeFailureResponse(ResponsePtr &&response, Complete complete = nullptr) override + { + } + + void beginStreaming(const std::string &mimeType, Complete complete) override + { + } + + void writeChunk(const std::string &chunk, Complete complete) override + { + + } + + void closeStream() override + { + } + + protected: + void onAccept(boost::beast::error_code ec) + { + if (ec) + { + fail(status::internal_server_error, "Error occurred in accpet", ec); + return; + } + + derived().stream().async_read( + m_buffer, + beast::bind_front_handler( + &WebsocketSession::onRead, + derived().shared_ptr())); + + } + + void onRead(beast::error_code ec, + std::size_t len) + { + using namespace rapidjson; + using namespace std; + + // Parse the buffer as a JSON request with parameters matching + // REST API + derived().stream().text(derived().stream().got_text()); + auto buffer = beast::buffers_to_string(m_buffer.data()); + + Document doc; + doc.Parse(buffer.c_str(), len); + + if (doc.HasParseError()) + { + LOG(warning) << "Websocket Read Error(offset (" << doc.GetErrorOffset() << "): " << + GetParseError_En(doc.GetParseError()); + LOG(warning) << " " << buffer; + } + if (!doc.IsObject()) + { + LOG(warning) << "Websocket Read Error: JSON message does not have a top level object"; + LOG(warning) << " " << buffer; + } + else + { + // Extract the parameters from the json doc to map them to the REST + // protocol parameters + auto request = std::make_shared(); + request->m_verb = beast::http::verb::get; + const auto &object = doc.GetObject(); + + for (auto &it : object) + { + switch (it.value.GetType()) + { + case rapidjson::kNullType: + // Skip nulls + break; + case rapidjson::kFalseType: + request->m_parameters.emplace(make_pair(it.name.GetString(), false)); + break; + case rapidjson::kTrueType: + request->m_parameters.emplace(make_pair(it.name.GetString(), true)); + break; + case rapidjson::kObjectType: + break; + case rapidjson::kArrayType: + break; + case rapidjson::kStringType: + request->m_parameters.emplace(make_pair(it.name.GetString(), string(it.value.GetString()))); + + break; + case rapidjson::kNumberType: + if (it.value.Is()) + request->m_parameters.emplace(make_pair(it.name.GetString(), it.value.Get())); + else if (it.value.Is()) + request->m_parameters.emplace(make_pair(it.name.GetString(), it.value.Get())); + else if (it.value.Is()) + request->m_parameters.emplace(make_pair(it.name.GetString(), (uint64_t) it.value.Get())); + else if (it.value.Is()) + request->m_parameters.emplace(make_pair(it.name.GetString(), it.value.Get())); + else if (it.value.Is()) + request->m_parameters.emplace(make_pair(it.name.GetString(), it.value.Get())); + + break; + } + } + + m_request->m_parsed = true; + if (!m_dispatch(derived().shared_ptr(), m_request)) + { + ostringstream txt; + txt << "Failed to find handler for " << buffer; + LOG(error) << txt.str(); + } + } + + derived().stream().async_read( + m_buffer, + beast::bind_front_handler( + &WebsocketSession::onRead, + derived().shared_ptr())); + } + + protected: + RequestPtr m_request; + RequestMessage m_msg; + beast::flat_buffer m_buffer; + }; + + template + using WebsocketSessionPtr = std::shared_ptr>; + + class PlainWebsocketSession : public WebsocketSession + { + public: + using Stream = beast::websocket::stream; + + PlainWebsocketSession(beast::tcp_stream&& stream, RequestPtr &&request, RequestMessage &&msg, Dispatch dispatch, ErrorFunction func) + : WebsocketSession(std::move(request), std::move(msg), dispatch, func), m_stream(std::move(stream)) + { + } + ~PlainWebsocketSession() + { + close(); + } + + void close() override + { + NAMED_SCOPE("PlainWebsocketSession::close"); + + m_request.reset(); + m_stream.close(beast::websocket::close_code::none); + } + + auto &stream() { return m_stream; } + + /// @brief Get a pointer cast as an Websocket Session + /// @return shared pointer to an Websocket session + std::shared_ptr shared_ptr() + { + return std::dynamic_pointer_cast(shared_from_this()); + } + + + + protected: + Stream m_stream; + }; + + class TlsWebsocketSession : public WebsocketSession + { + public: + using Stream = beast::websocket::stream>; + + TlsWebsocketSession(beast::ssl_stream&& stream, RequestPtr &&request, RequestMessage &&msg, Dispatch dispatch, ErrorFunction func) + : WebsocketSession(std::move(request), std::move(msg), dispatch, func), m_stream(std::move(stream)) + { + } + ~TlsWebsocketSession() + { + close(); + } + + auto &stream() { return m_stream; } + + void close() override + { + NAMED_SCOPE("TlsWebsocketSession::close"); + + m_request.reset(); + m_stream.close(beast::websocket::close_code::none); + } + + /// @brief Get a pointer cast as an TLS Websocket Session + /// @return shared pointer to an TLS Websocket session + std::shared_ptr shared_ptr() + { + return std::dynamic_pointer_cast(shared_from_this()); + } + + protected: + Stream m_stream; + }; + +} + From 7003e19dfea5dc8c4109f96ac07bdb656655016b Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Thu, 11 Apr 2024 17:47:25 -0400 Subject: [PATCH 06/35] First steps to adding routing support to websocket request. --- src/mtconnect/sink/rest_sink/request.hpp | 5 ++- src/mtconnect/sink/rest_sink/rest_service.cpp | 16 ++++--- src/mtconnect/sink/rest_sink/rest_service.hpp | 7 --- src/mtconnect/sink/rest_sink/routing.hpp | 21 +++++++-- src/mtconnect/sink/rest_sink/server.hpp | 44 +++++++++++++++---- .../sink/rest_sink/websocket_session.hpp | 6 ++- 6 files changed, 73 insertions(+), 26 deletions(-) diff --git a/src/mtconnect/sink/rest_sink/request.hpp b/src/mtconnect/sink/rest_sink/request.hpp index 9a24e223..164ddcd3 100644 --- a/src/mtconnect/sink/rest_sink/request.hpp +++ b/src/mtconnect/sink/rest_sink/request.hpp @@ -67,7 +67,10 @@ namespace mtconnect::sink::rest_sink { uint16_t m_foreignPort; ///< The requestors Port QueryMap m_query; ///< The parsed query parameters ParameterMap m_parameters; ///< The parsed path parameters - bool m_parsed { false }; ///< Flag the request as parsed + + std::optional m_requestId; ///< Request id from websocket sub + std::optional m_command; ///< Specific request from websocket + /// @brief Find a parameter by type /// @tparam T the type of the parameter diff --git a/src/mtconnect/sink/rest_sink/rest_service.cpp b/src/mtconnect/sink/rest_sink/rest_service.cpp index 9237a464..6d71d9ea 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.cpp +++ b/src/mtconnect/sink/rest_sink/rest_service.cpp @@ -122,7 +122,8 @@ namespace mtconnect { createAssetRoutings(); createPutObservationRoutings(); createFileRoutings(); - + m_server->addCommands(); + makeLoopbackSource(m_sinkContract->m_pipelineContext); } @@ -499,7 +500,8 @@ namespace mtconnect { "/{device}/probe?pretty={bool:false}&deviceType={string}", handler}) .document("MTConnect probe request", "Provides metadata service for the MTConnect Devices information model for " - "device identified by `device` matching `name` or `uuid`."); + "device identified by `device` matching `name` or `uuid`.") + .command("probe"); // Must be last m_server @@ -642,7 +644,9 @@ namespace mtconnect { "/{device}/" + asset + "?type={string}", deleteHandler}) .document("Delete all assets for a device and type", "Device and type are optional. If they are not given, it assumes there is " - "no constraint"); + "no constraint") + .command("asset"); +; } } } @@ -682,7 +686,8 @@ namespace mtconnect { m_server->addRouting({boost::beast::http::verb::get, "/{device}/current?" + qp, handler}) .document("MTConnect current request", "Gets a stapshot of the state of all the observations for device `device` " - "optionally filtered by the `path`"); + "optionally filtered by the `path`") + .command("current"); } void RestService::createSampleRoutings() @@ -727,7 +732,8 @@ namespace mtconnect { .document("MTConnect sample request", "Gets a time series of at maximum `count` observations for device `device` " "optionally filtered by the `path` and starting at `from`. By default, from is " - "the first available observation known to the agent"); + "the first available observation known to the agent") + .command("sample"); } void RestService::createPutObservationRoutings() diff --git a/src/mtconnect/sink/rest_sink/rest_service.hpp b/src/mtconnect/sink/rest_sink/rest_service.hpp index 51505fd5..923f210d 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.hpp +++ b/src/mtconnect/sink/rest_sink/rest_service.hpp @@ -321,22 +321,15 @@ namespace mtconnect { protected: // Loopback boost::asio::io_context &m_context; - boost::asio::io_context::strand m_strand; - std::string m_schemaVersion; - ConfigOptions m_options; - std::shared_ptr m_loopback; - uint64_t m_instanceId; - std::unique_ptr m_server; // Buffers FileCache m_fileCache; - bool m_logStreamData {false}; }; } // namespace sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/routing.hpp b/src/mtconnect/sink/rest_sink/routing.hpp index ca8bdf38..5b235b56 100644 --- a/src/mtconnect/sink/rest_sink/routing.hpp +++ b/src/mtconnect/sink/rest_sink/routing.hpp @@ -53,8 +53,8 @@ namespace mtconnect::sink::rest_sink { /// @param[in] function the function to call if matches /// @param[in] swagger `true` if swagger related Routing(boost::beast::http::verb verb, const std::string &pattern, const Function function, - bool swagger = false) - : m_verb(verb), m_function(function), m_swagger(swagger) + bool swagger = false, std::optional request = std::nullopt) + : m_verb(verb), m_command(request), m_function(function), m_swagger(swagger) { std::string s(pattern); @@ -79,8 +79,8 @@ namespace mtconnect::sink::rest_sink { /// @param[in] function the function to call if matches /// @param[in] swagger `true` if swagger related Routing(boost::beast::http::verb verb, const std::regex &pattern, const Function function, - bool swagger = false) - : m_verb(verb), m_pattern(pattern), m_function(function), m_swagger(swagger) + bool swagger = false, std::optional request = std::nullopt) + : m_verb(verb), m_pattern(pattern), m_command(request), m_function(function), m_swagger(swagger) {} /// @brief Added summary and description to the routing @@ -224,6 +224,17 @@ namespace mtconnect::sink::rest_sink { const auto &getPath() const { return m_path; } /// @brief Get the routing `verb` const auto &getVerb() const { return m_verb; } + + /// @brief Get the optional command associated with the routing + /// @returns optional routing + const auto &getCommand() const { return m_command; } + + /// @brief Sets the command associated with this routing for use with websockets + /// @param command the command + void command(const std::string &command) + { + m_command = command; + } protected: void pathParameters(std::string s) @@ -360,7 +371,9 @@ namespace mtconnect::sink::rest_sink { std::optional m_path; ParameterList m_pathParameters; QuerySet m_queryParameters; + std::optional m_command; Function m_function; + std::optional m_summary; std::optional m_description; diff --git a/src/mtconnect/sink/rest_sink/server.hpp b/src/mtconnect/sink/rest_sink/server.hpp index d72fd176..9e5987aa 100644 --- a/src/mtconnect/sink/rest_sink/server.hpp +++ b/src/mtconnect/sink/rest_sink/server.hpp @@ -159,16 +159,31 @@ namespace mtconnect::sink::rest_sink { { try { - for (auto &r : m_routings) + if (request->m_command) { - if (r.matches(session, request)) - return true; + auto route = m_commands.find(*request->m_command); + if (route != m_commands.end()) + { + + } + + std::stringstream txt; + txt << session->getRemote().address() << ": Cannot find handler for command: " << *request->m_command; + session->fail(boost::beast::http::status::not_found, txt.str()); + } + else + { + for (auto &r : m_routings) + { + if (r.matches(session, request)) + return true; + } + + std::stringstream txt; + txt << session->getRemote().address() << ": Cannot find handler for: " << request->m_verb + << " " << request->m_path; + session->fail(boost::beast::http::status::not_found, txt.str()); } - - std::stringstream txt; - txt << session->getRemote().address() << ": Cannot find handler for: " << request->m_verb - << " " << request->m_path; - session->fail(boost::beast::http::status::not_found, txt.str()); } catch (RequestError &re) { @@ -217,8 +232,20 @@ namespace mtconnect::sink::rest_sink { auto &route = m_routings.emplace_back(routing); if (m_parameterDocumentation) route.documentParameters(*m_parameterDocumentation); + if (route.getCommand()) + m_commands.emplace(*route.getCommand(), &route); return route; } + + /// @brief Setup commands from routings + void addCommands() + { + for (auto &route : m_routings) + { + if (route.getCommand()) + m_commands.emplace(*route.getCommand(), &route); + } + } /// @brief Add common set of documentation for all rest routings /// @param[in] docs Parameter documentation @@ -272,6 +299,7 @@ namespace mtconnect::sink::rest_sink { std::set m_allowPutsFrom; std::list m_routings; + std::map m_commands; std::unique_ptr m_fileCache; ErrorFunction m_errorFunction; FieldList m_fields; diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp index 2d87ced2..46043fe2 100644 --- a/src/mtconnect/sink/rest_sink/websocket_session.hpp +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -192,7 +192,11 @@ namespace mtconnect::sink::rest_sink { } } - m_request->m_parsed = true; + if (object.HasMember("command")) + request->m_command = object["command"].GetString(); + if (object.HasMember("id")) + request->m_requestId = object["id"].GetString(); + if (!m_dispatch(derived().shared_ptr(), m_request)) { ostringstream txt; From c85b3b30b2fe7c905c94bd63062f2a344be041f9 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Thu, 11 Apr 2024 18:32:59 -0400 Subject: [PATCH 07/35] Fixed port in url for MQTT service. --- CMakeLists.txt | 3 ++- src/mtconnect/mqtt/mqtt_client_impl.hpp | 1 + src/mtconnect/source/adapter/mqtt/mqtt_adapter.cpp | 11 +++-------- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1459c364..f55bb6b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,8 @@ set(AGENT_VERSION_MAJOR 2) set(AGENT_VERSION_MINOR 3) set(AGENT_VERSION_PATCH 0) -set(AGENT_VERSION_BUILD 6) +set(AGENT_VERSION_BUILD 7 +) set(AGENT_VERSION_RC "") # This minimum version is to support Visual Studio 2019 and C++ feature checking and FetchContent diff --git a/src/mtconnect/mqtt/mqtt_client_impl.hpp b/src/mtconnect/mqtt/mqtt_client_impl.hpp index dc1747d4..f40f6973 100644 --- a/src/mtconnect/mqtt/mqtt_client_impl.hpp +++ b/src/mtconnect/mqtt/mqtt_client_impl.hpp @@ -164,6 +164,7 @@ namespace mtconnect { m_connected = false; if (m_handler && m_handler->m_disconnected) m_handler->m_disconnected(shared_from_this()); + m_handler->m_disconnected(shared_from_this()); if (m_running) { reconnect(); diff --git a/src/mtconnect/source/adapter/mqtt/mqtt_adapter.cpp b/src/mtconnect/source/adapter/mqtt/mqtt_adapter.cpp index de1399a0..00a4d61c 100644 --- a/src/mtconnect/source/adapter/mqtt/mqtt_adapter.cpp +++ b/src/mtconnect/source/adapter/mqtt/mqtt_adapter.cpp @@ -85,12 +85,7 @@ namespace mtconnect { { m_options[configuration::MqttHost] = m_options[configuration::Host]; } - if (!HasOption(m_options, configuration::MqttPort) && - HasOption(m_options, configuration::Port)) - { - m_options[configuration::MqttPort] = m_options[configuration::Port]; - } - else + if (!HasOption(m_options, configuration::MqttPort)) { m_options[configuration::MqttPort] = 1883; } @@ -123,7 +118,7 @@ namespace mtconnect { !IsOptionSet(m_options, configuration::MqttWs)) { m_client = make_shared(m_ioContext, m_options, - move(clientHandler)); + std::move(clientHandler)); } else if (IsOptionSet(m_options, configuration::MqttWs) && IsOptionSet(m_options, configuration::MqttTls)) @@ -139,7 +134,7 @@ namespace mtconnect { else { m_client = make_shared(m_ioContext, m_options, - move(clientHandler)); + std::move(clientHandler)); } m_identity = m_client->getIdentity(); From 0dd7ed93f73bd485a9d339466d2bd157431093a5 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Fri, 12 Apr 2024 09:52:41 -0400 Subject: [PATCH 08/35] fixed crash when closing mqtt client. set close handler to nullptr if already disconnected. --- src/mtconnect/mqtt/mqtt_client_impl.hpp | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/mtconnect/mqtt/mqtt_client_impl.hpp b/src/mtconnect/mqtt/mqtt_client_impl.hpp index f40f6973..25b039b6 100644 --- a/src/mtconnect/mqtt/mqtt_client_impl.hpp +++ b/src/mtconnect/mqtt/mqtt_client_impl.hpp @@ -236,6 +236,7 @@ namespace mtconnect { { client->async_disconnect(10s, [client, url](mqtt::error_code ec) { LOG(warning) << url << " disconnected: " << ec.message(); + client->set_close_handler(nullptr); }); client.reset(); @@ -412,6 +413,13 @@ namespace mtconnect { public: using base = MqttClientImpl; using base::base; + /// @brief Get a shared pointer to this + /// @return shared pointer to this + shared_ptr getptr() + { + return static_pointer_cast(shared_from_this()); + } + /// @brief Get the Mqtt TCP Client /// @return pointer to the Mqtt TCP Client auto &getClient() @@ -438,6 +446,12 @@ namespace mtconnect { public: using base = MqttClientImpl; using base::base; + /// @brief Get a shared pointer to this + /// @return shared pointer to this + shared_ptr getptr() + { + return static_pointer_cast(shared_from_this()); + } /// @brief Get the Mqtt TLS Client /// @return pointer to the Mqtt TLS Client @@ -481,6 +495,13 @@ namespace mtconnect { public: using base = MqttClientImpl; using base::base; + /// @brief Get a shared pointer to this + /// @return shared pointer to this + shared_ptr getptr() + { + return static_pointer_cast(shared_from_this()); + } + /// @brief Get the Mqtt TLS WebSocket Client /// @return pointer to the Mqtt TLS WebSocket Client auto &getClient() @@ -513,6 +534,13 @@ namespace mtconnect { public: using base = MqttClientImpl; using base::base; + /// @brief Get a shared pointer to this + /// @return shared pointer to this + shared_ptr getptr() + { + return static_pointer_cast(shared_from_this()); + } + /// @brief Get the Mqtt TLS WebSocket Client /// @return pointer to the Mqtt TLS WebSocket Client auto &getClient() From 211f691662ae4403b9b72da8c5ac6caae5550f79 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Fri, 12 Apr 2024 12:39:49 -0400 Subject: [PATCH 09/35] Changed from command to request --- src/mtconnect/sink/rest_sink/websocket_session.hpp | 4 ++-- test_package/resources/samples/test_config.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp index 46043fe2..add1a6d9 100644 --- a/src/mtconnect/sink/rest_sink/websocket_session.hpp +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -192,8 +192,8 @@ namespace mtconnect::sink::rest_sink { } } - if (object.HasMember("command")) - request->m_command = object["command"].GetString(); + if (object.HasMember("request")) + request->m_command = object["request"].GetString(); if (object.HasMember("id")) request->m_requestId = object["id"].GetString(); diff --git a/test_package/resources/samples/test_config.xml b/test_package/resources/samples/test_config.xml index b42a291c..cda9baf0 100644 --- a/test_package/resources/samples/test_config.xml +++ b/test_package/resources/samples/test_config.xml @@ -1,6 +1,6 @@ -
+
Linux CNC Device From e6b4f154ea1b63159f4e3e07a78417a36af702ee Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Tue, 16 Apr 2024 10:11:40 -0400 Subject: [PATCH 10/35] working to the point of response --- README.md | 12 +++- src/mtconnect/sink/rest_sink/routing.hpp | 57 ++++++++++--------- src/mtconnect/sink/rest_sink/server.hpp | 13 +++-- .../sink/rest_sink/websocket_session.hpp | 55 +++++++++++++----- 4 files changed, 90 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 55d322ba..ef6f285f 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -MTConnect C++ Agent Version 2.3 +MTConnect C++ Agent Version 2.5 -------- [![Build MTConnect C++ Agent](https://github.com/mtconnect/cppagent/actions/workflows/build.yml/badge.svg)](https://github.com/mtconnect/cppagent/actions/workflows/build.yml) @@ -13,6 +13,10 @@ the devices and the location of the adapter. Pre-built binary releases for Windows are available from [Releases](https://github.com/mtconnect/cppagent/releases) for those who do not want to build the agent themselves. For *NIX users, you will need libxml2, cppunit, and cmake as well as build essentials. +Version 2.5.0 Added validation of observations in the stream + +Version 2.4.0 Added support for version 2.4 + Version 2.3.0 Support for all Version 2.3 standard changes and JSON ingress to MQTT adapter. Version 2.2.0 Support for all Version 2.2 standard changes and dynamic configuration from adapters. Upgrade to conan 2. @@ -639,11 +643,17 @@ Configuration Parameters * `SuppressIPAddress` - Suppress the Adapter IP Address and port when creating the Agent Device ids and names. This applies to all adapters. *Default*: false + +* `Validation` - Turns on validation of model components and observations + + *Default*: false * `WorkerThreads` - The number of operating system threads dedicated to the Agent *Default*: 1 + + #### Adapter General Configuration These can be overridden on a per-adapter basis diff --git a/src/mtconnect/sink/rest_sink/routing.hpp b/src/mtconnect/sink/rest_sink/routing.hpp index 5b235b56..f4048ea1 100644 --- a/src/mtconnect/sink/rest_sink/routing.hpp +++ b/src/mtconnect/sink/rest_sink/routing.hpp @@ -165,46 +165,49 @@ namespace mtconnect::sink::rest_sink { { try { - request->m_parameters.clear(); - std::smatch m; - if (m_verb == request->m_verb && std::regex_match(request->m_path, m, m_pattern)) + if (!request->m_command) { - auto s = m.begin(); - s++; - for (auto &p : m_pathParameters) + request->m_parameters.clear(); + std::smatch m; + if (m_verb == request->m_verb && std::regex_match(request->m_path, m, m_pattern)) { - if (s != m.end()) + auto s = m.begin(); + s++; + for (auto &p : m_pathParameters) { - ParameterValue v(s->str()); - request->m_parameters.emplace(make_pair(p.m_name, v)); - s++; + if (s != m.end()) + { + ParameterValue v(s->str()); + request->m_parameters.emplace(make_pair(p.m_name, v)); + s++; + } } } + } - for (auto &p : m_queryParameters) + for (auto &p : m_queryParameters) + { + auto q = request->m_query.find(p.m_name); + if (q != request->m_query.end()) { - auto q = request->m_query.find(p.m_name); - if (q != request->m_query.end()) + try { - try - { - auto v = convertValue(q->second, p.m_type); - request->m_parameters.emplace(make_pair(p.m_name, v)); - } - catch (ParameterError &e) - { - std::string msg = - std::string("for query parameter '") + p.m_name + "': " + e.what(); - throw ParameterError(msg); - } + auto v = convertValue(q->second, p.m_type); + request->m_parameters.emplace(make_pair(p.m_name, v)); } - else if (!std::holds_alternative(p.m_default)) + catch (ParameterError &e) { - request->m_parameters.emplace(make_pair(p.m_name, p.m_default)); + std::string msg = + std::string("for query parameter '") + p.m_name + "': " + e.what(); + throw ParameterError(msg); } } - return m_function(session, request); + else if (!std::holds_alternative(p.m_default)) + { + request->m_parameters.emplace(make_pair(p.m_name, p.m_default)); + } } + return m_function(session, request); } catch (ParameterError &e) diff --git a/src/mtconnect/sink/rest_sink/server.hpp b/src/mtconnect/sink/rest_sink/server.hpp index 9e5987aa..2adde659 100644 --- a/src/mtconnect/sink/rest_sink/server.hpp +++ b/src/mtconnect/sink/rest_sink/server.hpp @@ -164,12 +164,15 @@ namespace mtconnect::sink::rest_sink { auto route = m_commands.find(*request->m_command); if (route != m_commands.end()) { - + if (route->second->matches(session, request)) + return true; + } + else + { + std::stringstream txt; + txt << session->getRemote().address() << ": Cannot find handler for command: " << *request->m_command; + session->fail(boost::beast::http::status::not_found, txt.str()); } - - std::stringstream txt; - txt << session->getRemote().address() << ": Cannot find handler for command: " << *request->m_command; - session->fail(boost::beast::http::status::not_found, txt.str()); } else { diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp index add1a6d9..aabb473f 100644 --- a/src/mtconnect/sink/rest_sink/websocket_session.hpp +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -85,7 +85,17 @@ namespace mtconnect::sink::rest_sink { void writeResponse(ResponsePtr &&response, Complete complete = nullptr) override { + beast::flat_buffer buf; + buf.prepare(response->m_body.size()); + buf. + + ws_.async_write( + buf.data(), + beast::bind_front_handler( + &WebsocketSession::sent, + derived().shared_ptr())); + } void writeFailureResponse(ResponsePtr &&response, Complete complete = nullptr) override @@ -122,6 +132,12 @@ namespace mtconnect::sink::rest_sink { } + void sent(beast::error_code ec, + std::size_t len) + { + + } + void onRead(beast::error_code ec, std::size_t len) { @@ -132,6 +148,7 @@ namespace mtconnect::sink::rest_sink { // REST API derived().stream().text(derived().stream().got_text()); auto buffer = beast::buffers_to_string(m_buffer.data()); + m_buffer.consume(m_buffer.size()); Document doc; doc.Parse(buffer.c_str(), len); @@ -151,8 +168,7 @@ namespace mtconnect::sink::rest_sink { { // Extract the parameters from the json doc to map them to the REST // protocol parameters - auto request = std::make_shared(); - request->m_verb = beast::http::verb::get; + m_request->m_verb = beast::http::verb::get; const auto &object = doc.GetObject(); for (auto &it : object) @@ -163,39 +179,50 @@ namespace mtconnect::sink::rest_sink { // Skip nulls break; case rapidjson::kFalseType: - request->m_parameters.emplace(make_pair(it.name.GetString(), false)); + m_request->m_parameters.emplace(make_pair(it.name.GetString(), false)); break; case rapidjson::kTrueType: - request->m_parameters.emplace(make_pair(it.name.GetString(), true)); + m_request->m_parameters.emplace(make_pair(it.name.GetString(), true)); break; case rapidjson::kObjectType: break; case rapidjson::kArrayType: break; case rapidjson::kStringType: - request->m_parameters.emplace(make_pair(it.name.GetString(), string(it.value.GetString()))); + m_request->m_parameters.emplace(make_pair(it.name.GetString(), string(it.value.GetString()))); break; case rapidjson::kNumberType: if (it.value.Is()) - request->m_parameters.emplace(make_pair(it.name.GetString(), it.value.Get())); + m_request->m_parameters.emplace(make_pair(it.name.GetString(), it.value.Get())); else if (it.value.Is()) - request->m_parameters.emplace(make_pair(it.name.GetString(), it.value.Get())); + m_request->m_parameters.emplace(make_pair(it.name.GetString(), it.value.Get())); else if (it.value.Is()) - request->m_parameters.emplace(make_pair(it.name.GetString(), (uint64_t) it.value.Get())); + m_request->m_parameters.emplace(make_pair(it.name.GetString(), (uint64_t) it.value.Get())); else if (it.value.Is()) - request->m_parameters.emplace(make_pair(it.name.GetString(), it.value.Get())); + m_request->m_parameters.emplace(make_pair(it.name.GetString(), it.value.Get())); else if (it.value.Is()) - request->m_parameters.emplace(make_pair(it.name.GetString(), it.value.Get())); + m_request->m_parameters.emplace(make_pair(it.name.GetString(), it.value.Get())); break; } } - if (object.HasMember("request")) - request->m_command = object["request"].GetString(); - if (object.HasMember("id")) - request->m_requestId = object["id"].GetString(); + if (m_request->m_parameters.count("request") > 0) + { + m_request->m_command = get(m_request->m_parameters["request"]); + m_request->m_parameters.erase("request"); + } + if (m_request->m_parameters.count("id") > 0) + { + auto &v =m_request->m_parameters["id"]; + string id = visit(overloaded { + [](monostate m) { return ""s; }, + [](auto v) { return boost::lexical_cast(v); } + }, v); + m_request->m_requestId = id; + m_request->m_parameters.erase("id"); + } if (!m_dispatch(derived().shared_ptr(), m_request)) { From f7802f7d90916e8df02eb9fba45c42e5154fb45f Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Tue, 16 Apr 2024 14:56:08 -0400 Subject: [PATCH 11/35] checkpoint with write response --- src/mtconnect/mqtt/mqtt_client_impl.hpp | 8 +- src/mtconnect/sink/rest_sink/request.hpp | 5 +- src/mtconnect/sink/rest_sink/response.hpp | 1 + src/mtconnect/sink/rest_sink/rest_service.cpp | 58 ++-- src/mtconnect/sink/rest_sink/routing.hpp | 19 +- src/mtconnect/sink/rest_sink/server.hpp | 9 +- src/mtconnect/sink/rest_sink/session_impl.cpp | 27 +- src/mtconnect/sink/rest_sink/session_impl.hpp | 9 +- .../sink/rest_sink/websocket_session.hpp | 272 ++++++++++-------- src/mtconnect/validation/observations.hpp | 6 +- 10 files changed, 221 insertions(+), 193 deletions(-) diff --git a/src/mtconnect/mqtt/mqtt_client_impl.hpp b/src/mtconnect/mqtt/mqtt_client_impl.hpp index 56eb701c..3e166dc6 100644 --- a/src/mtconnect/mqtt/mqtt_client_impl.hpp +++ b/src/mtconnect/mqtt/mqtt_client_impl.hpp @@ -164,7 +164,7 @@ namespace mtconnect { m_connected = false; if (m_handler && m_handler->m_disconnected) m_handler->m_disconnected(shared_from_this()); - m_handler->m_disconnected(shared_from_this()); + m_handler->m_disconnected(shared_from_this()); if (m_running) { reconnect(); @@ -419,7 +419,7 @@ namespace mtconnect { { return static_pointer_cast(shared_from_this()); } - + /// @brief Get the Mqtt TCP Client /// @return pointer to the Mqtt TCP Client auto &getClient() @@ -501,7 +501,7 @@ namespace mtconnect { { return static_pointer_cast(shared_from_this()); } - + /// @brief Get the Mqtt TLS WebSocket Client /// @return pointer to the Mqtt TLS WebSocket Client auto &getClient() @@ -540,7 +540,7 @@ namespace mtconnect { { return static_pointer_cast(shared_from_this()); } - + /// @brief Get the Mqtt TLS WebSocket Client /// @return pointer to the Mqtt TLS WebSocket Client auto &getClient() diff --git a/src/mtconnect/sink/rest_sink/request.hpp b/src/mtconnect/sink/rest_sink/request.hpp index 164ddcd3..ac3f2c33 100644 --- a/src/mtconnect/sink/rest_sink/request.hpp +++ b/src/mtconnect/sink/rest_sink/request.hpp @@ -68,9 +68,8 @@ namespace mtconnect::sink::rest_sink { QueryMap m_query; ///< The parsed query parameters ParameterMap m_parameters; ///< The parsed path parameters - std::optional m_requestId; ///< Request id from websocket sub - std::optional m_command; ///< Specific request from websocket - + std::optional m_requestId; ///< Request id from websocket sub + std::optional m_command; ///< Specific request from websocket /// @brief Find a parameter by type /// @tparam T the type of the parameter diff --git a/src/mtconnect/sink/rest_sink/response.hpp b/src/mtconnect/sink/rest_sink/response.hpp index a67bdbe6..b19fecff 100644 --- a/src/mtconnect/sink/rest_sink/response.hpp +++ b/src/mtconnect/sink/rest_sink/response.hpp @@ -64,6 +64,7 @@ namespace mtconnect { std::chrono::seconds m_expires; ///< how long should this session should stay open before it is closed bool m_close {false}; ///< `true` if this session should closed after it responds + std::optional m_requestId; ///< Request id from websocket sub CachedFilePtr m_file; ///< Cached file if a file is being returned }; diff --git a/src/mtconnect/sink/rest_sink/rest_service.cpp b/src/mtconnect/sink/rest_sink/rest_service.cpp index 6d71d9ea..dee758ba 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.cpp +++ b/src/mtconnect/sink/rest_sink/rest_service.cpp @@ -123,7 +123,7 @@ namespace mtconnect { createPutObservationRoutings(); createFileRoutings(); m_server->addCommands(); - + makeLoopbackSource(m_sinkContract->m_pipelineContext); } @@ -435,8 +435,10 @@ namespace mtconnect { // Request Routings // ----------------------------------------------------------- - static inline void respond(rest_sink::SessionPtr session, rest_sink::ResponsePtr &&response) + static inline void respond(rest_sink::SessionPtr session, rest_sink::ResponsePtr &&response, + std::optional id = std::nullopt) { + response->m_requestId = id; session->writeResponse(std::move(response)); } @@ -485,7 +487,7 @@ namespace mtconnect { return false; } - respond(session, probeRequest(printer, device, pretty, deviceType)); + respond(session, probeRequest(printer, device, pretty, deviceType), request->m_requestId); return true; }; @@ -526,8 +528,10 @@ namespace mtconnect { auto count = *request->parameter("count"); auto printer = printerForAccepts(request->m_accepts); - respond(session, assetRequest(printer, count, removed, request->parameter("type"), - request->parameter("device"))); + respond(session, + assetRequest(printer, count, removed, request->parameter("type"), + request->parameter("device")), + request->m_requestId); return true; }; @@ -542,14 +546,15 @@ namespace mtconnect { string id; while (getline(str, id, ';')) ids.emplace_back(id); - respond(session, assetIdsRequest(printer, ids)); + respond(session, assetIdsRequest(printer, ids), request->m_requestId); } else { auto printer = printerForAccepts(request->m_accepts); auto error = printError(printer, "INVALID_REQUEST", "No asset given"); - respond(session, make_unique(rest_sink::status::bad_request, error, - printer->mimeType())); + respond(session, + make_unique(rest_sink::status::bad_request, error, printer->mimeType()), + request->m_requestId); } return true; }; @@ -581,7 +586,8 @@ namespace mtconnect { respond(session, putAssetRequest(printer, request->m_body, request->parameter("type"), request->parameter("device"), - request->parameter("assetId"))); + request->parameter("assetId")), + request->m_requestId); return true; }; @@ -596,13 +602,15 @@ namespace mtconnect { while (getline(str, id, ';')) ids.emplace_back(id); - respond(session, deleteAssetRequest(printer, ids)); + respond(session, deleteAssetRequest(printer, ids), request->m_requestId); } else { - respond(session, deleteAllAssetsRequest(printerForAccepts(request->m_accepts), - request->parameter("device"), - request->parameter("type"))); + respond(session, + deleteAllAssetsRequest(printerForAccepts(request->m_accepts), + request->parameter("device"), + request->parameter("type")), + request->m_requestId); } return true; }; @@ -646,7 +654,7 @@ namespace mtconnect { "Device and type are optional. If they are not given, it assumes there is " "no constraint") .command("asset"); -; + ; } } } @@ -665,12 +673,13 @@ namespace mtconnect { } else { - respond(session, currentRequest(printerForAccepts(request->m_accepts), - request->parameter("device"), - request->parameter("at"), - request->parameter("path"), - *request->parameter("pretty"), - request->parameter("deviceType"))); + respond( + session, + currentRequest( + printerForAccepts(request->m_accepts), request->parameter("device"), + request->parameter("at"), request->parameter("path"), + *request->parameter("pretty"), request->parameter("deviceType")), + request->m_requestId); } return true; }; @@ -712,7 +721,8 @@ namespace mtconnect { printerForAccepts(request->m_accepts), *request->parameter("count"), request->parameter("device"), request->parameter("from"), request->parameter("to"), request->parameter("path"), - *request->parameter("pretty"), request->parameter("deviceType"))); + *request->parameter("pretty"), request->parameter("deviceType")), + request->m_requestId); } return true; }; @@ -751,8 +761,10 @@ namespace mtconnect { queries.erase("time"); auto device = request->parameter("device"); - respond(session, putObservationRequest(printerForAccepts(request->m_accepts), *device, - queries, ts)); + respond( + session, + putObservationRequest(printerForAccepts(request->m_accepts), *device, queries, ts), + request->m_requestId); return true; } else diff --git a/src/mtconnect/sink/rest_sink/routing.hpp b/src/mtconnect/sink/rest_sink/routing.hpp index f4048ea1..23053422 100644 --- a/src/mtconnect/sink/rest_sink/routing.hpp +++ b/src/mtconnect/sink/rest_sink/routing.hpp @@ -80,7 +80,11 @@ namespace mtconnect::sink::rest_sink { /// @param[in] swagger `true` if swagger related Routing(boost::beast::http::verb verb, const std::regex &pattern, const Function function, bool swagger = false, std::optional request = std::nullopt) - : m_verb(verb), m_pattern(pattern), m_command(request), m_function(function), m_swagger(swagger) + : m_verb(verb), + m_pattern(pattern), + m_command(request), + m_function(function), + m_swagger(swagger) {} /// @brief Added summary and description to the routing @@ -197,8 +201,7 @@ namespace mtconnect::sink::rest_sink { } catch (ParameterError &e) { - std::string msg = - std::string("for query parameter '") + p.m_name + "': " + e.what(); + std::string msg = std::string("for query parameter '") + p.m_name + "': " + e.what(); throw ParameterError(msg); } } @@ -227,17 +230,14 @@ namespace mtconnect::sink::rest_sink { const auto &getPath() const { return m_path; } /// @brief Get the routing `verb` const auto &getVerb() const { return m_verb; } - + /// @brief Get the optional command associated with the routing /// @returns optional routing const auto &getCommand() const { return m_command; } - + /// @brief Sets the command associated with this routing for use with websockets /// @param command the command - void command(const std::string &command) - { - m_command = command; - } + void command(const std::string &command) { m_command = command; } protected: void pathParameters(std::string s) @@ -376,7 +376,6 @@ namespace mtconnect::sink::rest_sink { QuerySet m_queryParameters; std::optional m_command; Function m_function; - std::optional m_summary; std::optional m_description; diff --git a/src/mtconnect/sink/rest_sink/server.hpp b/src/mtconnect/sink/rest_sink/server.hpp index 2adde659..740efab8 100644 --- a/src/mtconnect/sink/rest_sink/server.hpp +++ b/src/mtconnect/sink/rest_sink/server.hpp @@ -170,7 +170,8 @@ namespace mtconnect::sink::rest_sink { else { std::stringstream txt; - txt << session->getRemote().address() << ": Cannot find handler for command: " << *request->m_command; + txt << session->getRemote().address() + << ": Cannot find handler for command: " << *request->m_command; session->fail(boost::beast::http::status::not_found, txt.str()); } } @@ -184,7 +185,7 @@ namespace mtconnect::sink::rest_sink { std::stringstream txt; txt << session->getRemote().address() << ": Cannot find handler for: " << request->m_verb - << " " << request->m_path; + << " " << request->m_path; session->fail(boost::beast::http::status::not_found, txt.str()); } } @@ -239,7 +240,7 @@ namespace mtconnect::sink::rest_sink { m_commands.emplace(*route.getCommand(), &route); return route; } - + /// @brief Setup commands from routings void addCommands() { @@ -302,7 +303,7 @@ namespace mtconnect::sink::rest_sink { std::set m_allowPutsFrom; std::list m_routings; - std::map m_commands; + std::map m_commands; std::unique_ptr m_fileCache; ErrorFunction m_errorFunction; FieldList m_fields; diff --git a/src/mtconnect/sink/rest_sink/session_impl.cpp b/src/mtconnect/sink/rest_sink/session_impl.cpp index 52651e38..d84c43fd 100644 --- a/src/mtconnect/sink/rest_sink/session_impl.cpp +++ b/src/mtconnect/sink/rest_sink/session_impl.cpp @@ -198,7 +198,7 @@ namespace mtconnect::sink::rest_sink { auto &msg = m_parser->get(); const auto &remote = beast::get_lowest_layer(derived().stream()).socket().remote_endpoint(); - + // Check for put, post, or delete if (msg.method() != http::verb::get) { @@ -244,7 +244,7 @@ namespace mtconnect::sink::rest_sink { LOG(info) << "ReST Request: From [" << m_request->m_foreignIp << ':' << remote.port() << "]: " << msg.method() << " " << msg.target(); - + // Check if this is a websocket upgrade request. If so, begin a websocket session. if (ws::is_upgrade(msg)) { @@ -469,17 +469,13 @@ namespace mtconnect::sink::rest_sink { writeResponse(std::move(response)); } } - + SessionPtr HttpSession::upgradeToWebsocket(RequestMessage &&msg) { - return std::make_shared(std::move(m_stream), - std::move(m_request), - std::move(msg), - m_dispatch, - m_errorFunction); + return std::make_shared(std::move(m_stream), std::move(m_request), + std::move(msg), m_dispatch, m_errorFunction); } - /// @brief A secure https session class HttpsSession : public SessionImpl { @@ -536,15 +532,12 @@ namespace mtconnect::sink::rest_sink { m_stream.async_shutdown(beast::bind_front_handler(&HttpsSession::shutdown, shared_ptr())); } } - + /// @brief Upgrade the current connection to a websocket connection. SessionPtr upgradeToWebsocket(RequestMessage &&msg) { - return std::make_shared(std::move(m_stream), - std::move(m_request), - std::move(msg), - m_dispatch, - m_errorFunction); + return std::make_shared(std::move(m_stream), std::move(m_request), + std::move(msg), m_dispatch, m_errorFunction); } protected: @@ -569,8 +562,8 @@ namespace mtconnect::sink::rest_sink { beast::ssl_stream m_stream; bool m_closing {false}; }; - - template + + template void SessionImpl::upgrade(RequestMessage &&msg) { LOG(debug) << "Upgrading session to websockets"; diff --git a/src/mtconnect/sink/rest_sink/session_impl.hpp b/src/mtconnect/sink/rest_sink/session_impl.hpp index 27be3497..07ab7fbd 100644 --- a/src/mtconnect/sink/rest_sink/session_impl.hpp +++ b/src/mtconnect/sink/rest_sink/session_impl.hpp @@ -35,12 +35,11 @@ namespace mtconnect { } namespace sink::rest_sink { - template + template class WebsocketSession; - template + template using WebsocketSessionPtr = std::shared_ptr>; - - + /// @brief A session implementation `Derived` subclass pattern /// @tparam subclass of this class to use the same methods with http or https protocol streams template @@ -153,7 +152,7 @@ namespace mtconnect { boost::beast::error_code ec; m_stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); } - + /// @brief Upgrade the current connection to a websocket connection. SessionPtr upgradeToWebsocket(RequestMessage &&msg); diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp index aabb473f..f39b559e 100644 --- a/src/mtconnect/sink/rest_sink/websocket_session.hpp +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -20,12 +20,11 @@ #include #include -#include -#include - #include #include #include +#include +#include #include "mtconnect/config.hpp" #include "mtconnect/configuration/config_options.hpp" @@ -34,18 +33,26 @@ namespace mtconnect::sink::rest_sink { namespace beast = boost::beast; - + + struct WebsocketRequest + { + std::string m_requestId; + std::optional m_streamBuffer; + Complete m_complete; + bool m_streaming {false}; + }; + /// @brief A websocket session that provides a pubsub interface using REST parameters - template + template class WebsocketSession : public Session { public: using RequestMessage = boost::beast::http::request; - WebsocketSession(RequestPtr &&request, RequestMessage &&msg, Dispatch dispatch, ErrorFunction func) - : Session(dispatch, func), m_request(std::move(request)), m_msg(std::move(msg)) - { - } + WebsocketSession(RequestPtr &&request, RequestMessage &&msg, Dispatch dispatch, + ErrorFunction func) + : Session(dispatch, func), m_request(std::move(request)), m_msg(std::move(msg)) + {} /// @brief Session cannot be copied. WebsocketSession(const WebsocketSession &) = delete; @@ -54,67 +61,69 @@ namespace mtconnect::sink::rest_sink { /// @brief get this as the `Derived` type /// @return the subclass Derived &derived() { return static_cast(*this); } - - + void run() override { using namespace boost::beast; - + // Set suggested timeout settings for the websocket derived().stream().set_option( - websocket::stream_base::timeout::suggested( - beast::role_type::server)); + websocket::stream_base::timeout::suggested(beast::role_type::server)); // Set a decorator to change the Server of the handshake derived().stream().set_option( - websocket::stream_base::decorator( - [](websocket::response_type& res) - { - res.set(http::field::server, - GetAgentVersion() + - " MTConnectAgent"); + websocket::stream_base::decorator([](websocket::response_type &res) { + res.set(http::field::server, GetAgentVersion() + " MTConnectAgent"); })); // Accept the websocket handshake derived().stream().async_accept( - m_msg, - beast::bind_front_handler( - &WebsocketSession::onAccept, - derived().shared_ptr())); + m_msg, beast::bind_front_handler(&WebsocketSession::onAccept, derived().shared_ptr())); } void writeResponse(ResponsePtr &&response, Complete complete = nullptr) override { - beast::flat_buffer buf; - - buf.prepare(response->m_body.size()); - buf. - - ws_.async_write( - buf.data(), - beast::bind_front_handler( - &WebsocketSession::sent, - derived().shared_ptr())); + if (response->m_requestId) + { + auto id = *(response->m_requestId); + auto it = m_requests.find(id); + if (it != m_requests.end()) + { + using namespace std::placeholders; + auto &req = it->second; + req.m_complete = complete; + req.m_streamBuffer.emplace(); + std::ostream str(&req.m_streamBuffer.value()); + + str << response->m_body; + + derived().stream().text(derived().stream().got_text()); + derived().stream().async_write( + req.m_streamBuffer->data(), + beast::bind_handler( + [this, id](beast::error_code ec, std::size_t len) { sent(ec, len, id); }, _1, + _2)); + } + else + { + LOG(error) << "WebsocketSession::writeResponse: Cannot find request for id: " << id; + } + } + else + { + LOG(error) << "WebsocketSession::writeResponse: No request id for websocket"; + } } - - void writeFailureResponse(ResponsePtr &&response, Complete complete = nullptr) override - { - } - - void beginStreaming(const std::string &mimeType, Complete complete) override - { - } - - void writeChunk(const std::string &chunk, Complete complete) override - { - - } - - void closeStream() override - { - } - + + void writeFailureResponse(ResponsePtr &&response, Complete complete = nullptr) override {} + + void beginStreaming(const std::string &mimeType, Complete complete) override {} + + void writeChunk(const std::string &chunk, Complete complete) override {} + + void closeStream() override {} + protected: void onAccept(boost::beast::error_code ec) { @@ -123,40 +132,54 @@ namespace mtconnect::sink::rest_sink { fail(status::internal_server_error, "Error occurred in accpet", ec); return; } - - derived().stream().async_read( - m_buffer, - beast::bind_front_handler( - &WebsocketSession::onRead, - derived().shared_ptr())); + derived().stream().async_read( + m_buffer, beast::bind_front_handler(&WebsocketSession::onRead, derived().shared_ptr())); } - - void sent(beast::error_code ec, - std::size_t len) + + void sent(beast::error_code ec, std::size_t len, const std::string &id) { - + auto it = m_requests.find(id); + if (it != m_requests.end()) + { + auto &req = it->second; + if (req.m_complete) + { + req.m_complete(); + } + + if (!req.m_streaming) + { + m_requests.erase(id); + } + } + else + { + LOG(error) << "WebsocketSession::sent: Cannot find request for id: " << id; + } } - - void onRead(beast::error_code ec, - std::size_t len) + + void onRead(beast::error_code ec, std::size_t len) { + if (ec) + return fail(boost::beast::http::status::internal_server_error, "shutdown", ec); + using namespace rapidjson; using namespace std; - + // Parse the buffer as a JSON request with parameters matching // REST API derived().stream().text(derived().stream().got_text()); auto buffer = beast::buffers_to_string(m_buffer.data()); m_buffer.consume(m_buffer.size()); - + Document doc; doc.Parse(buffer.c_str(), len); - + if (doc.HasParseError()) { - LOG(warning) << "Websocket Read Error(offset (" << doc.GetErrorOffset() << "): " << - GetParseError_En(doc.GetParseError()); + LOG(warning) << "Websocket Read Error(offset (" << doc.GetErrorOffset() + << "): " << GetParseError_En(doc.GetParseError()); LOG(warning) << " " << buffer; } if (!doc.IsObject()) @@ -170,7 +193,7 @@ namespace mtconnect::sink::rest_sink { // protocol parameters m_request->m_verb = beast::http::verb::get; const auto &object = doc.GetObject(); - + for (auto &it : object) { switch (it.value.GetType()) @@ -185,29 +208,35 @@ namespace mtconnect::sink::rest_sink { m_request->m_parameters.emplace(make_pair(it.name.GetString(), true)); break; case rapidjson::kObjectType: - break; + break; case rapidjson::kArrayType: break; case rapidjson::kStringType: - m_request->m_parameters.emplace(make_pair(it.name.GetString(), string(it.value.GetString()))); + m_request->m_parameters.emplace( + make_pair(it.name.GetString(), string(it.value.GetString()))); break; case rapidjson::kNumberType: if (it.value.Is()) - m_request->m_parameters.emplace(make_pair(it.name.GetString(), it.value.Get())); + m_request->m_parameters.emplace( + make_pair(it.name.GetString(), it.value.Get())); else if (it.value.Is()) - m_request->m_parameters.emplace(make_pair(it.name.GetString(), it.value.Get())); + m_request->m_parameters.emplace( + make_pair(it.name.GetString(), it.value.Get())); else if (it.value.Is()) - m_request->m_parameters.emplace(make_pair(it.name.GetString(), (uint64_t) it.value.Get())); + m_request->m_parameters.emplace( + make_pair(it.name.GetString(), (uint64_t)it.value.Get())); else if (it.value.Is()) - m_request->m_parameters.emplace(make_pair(it.name.GetString(), it.value.Get())); + m_request->m_parameters.emplace( + make_pair(it.name.GetString(), it.value.Get())); else if (it.value.Is()) - m_request->m_parameters.emplace(make_pair(it.name.GetString(), it.value.Get())); + m_request->m_parameters.emplace( + make_pair(it.name.GetString(), it.value.Get())); break; } } - + if (m_request->m_parameters.count("request") > 0) { m_request->m_command = get(m_request->m_parameters["request"]); @@ -215,15 +244,17 @@ namespace mtconnect::sink::rest_sink { } if (m_request->m_parameters.count("id") > 0) { - auto &v =m_request->m_parameters["id"]; - string id = visit(overloaded { - [](monostate m) { return ""s; }, - [](auto v) { return boost::lexical_cast(v); } - }, v); + auto &v = m_request->m_parameters["id"]; + string id = visit(overloaded {[](monostate m) { return ""s; }, + [](auto v) { return boost::lexical_cast(v); }}, + v); m_request->m_requestId = id; m_request->m_parameters.erase("id"); } + auto &req = m_requests[*(m_request->m_requestId)]; + req.m_requestId = *(m_request->m_requestId); + if (!m_dispatch(derived().shared_ptr(), m_request)) { ostringstream txt; @@ -231,45 +262,42 @@ namespace mtconnect::sink::rest_sink { LOG(error) << txt.str(); } } - + derived().stream().async_read( - m_buffer, - beast::bind_front_handler( - &WebsocketSession::onRead, - derived().shared_ptr())); + m_buffer, beast::bind_front_handler(&WebsocketSession::onRead, derived().shared_ptr())); } - + protected: RequestPtr m_request; RequestMessage m_msg; beast::flat_buffer m_buffer; + std::map m_requests; }; - - template + + template using WebsocketSessionPtr = std::shared_ptr>; - + class PlainWebsocketSession : public WebsocketSession { public: using Stream = beast::websocket::stream; - - PlainWebsocketSession(beast::tcp_stream&& stream, RequestPtr &&request, RequestMessage &&msg, Dispatch dispatch, ErrorFunction func) - : WebsocketSession(std::move(request), std::move(msg), dispatch, func), m_stream(std::move(stream)) - { - } - ~PlainWebsocketSession() - { - close(); - } - + + PlainWebsocketSession(beast::tcp_stream &&stream, RequestPtr &&request, RequestMessage &&msg, + Dispatch dispatch, ErrorFunction func) + : WebsocketSession(std::move(request), std::move(msg), dispatch, func), + m_stream(std::move(stream)) + {} + ~PlainWebsocketSession() { close(); } + void close() override { NAMED_SCOPE("PlainWebsocketSession::close"); - + m_request.reset(); - m_stream.close(beast::websocket::close_code::none); + if (m_stream.is_open()) + m_stream.close(beast::websocket::close_code::none); } - + auto &stream() { return m_stream; } /// @brief Get a pointer cast as an Websocket Session @@ -278,37 +306,34 @@ namespace mtconnect::sink::rest_sink { { return std::dynamic_pointer_cast(shared_from_this()); } - - - + protected: Stream m_stream; }; - + class TlsWebsocketSession : public WebsocketSession { public: using Stream = beast::websocket::stream>; - - TlsWebsocketSession(beast::ssl_stream&& stream, RequestPtr &&request, RequestMessage &&msg, Dispatch dispatch, ErrorFunction func) - : WebsocketSession(std::move(request), std::move(msg), dispatch, func), m_stream(std::move(stream)) - { - } - ~TlsWebsocketSession() - { - close(); - } + + TlsWebsocketSession(beast::ssl_stream &&stream, RequestPtr &&request, + RequestMessage &&msg, Dispatch dispatch, ErrorFunction func) + : WebsocketSession(std::move(request), std::move(msg), dispatch, func), + m_stream(std::move(stream)) + {} + ~TlsWebsocketSession() { close(); } auto &stream() { return m_stream; } - + void close() override { NAMED_SCOPE("TlsWebsocketSession::close"); m_request.reset(); - m_stream.close(beast::websocket::close_code::none); + if (m_stream.is_open()) + m_stream.close(beast::websocket::close_code::none); } - + /// @brief Get a pointer cast as an TLS Websocket Session /// @return shared pointer to an TLS Websocket session std::shared_ptr shared_ptr() @@ -319,6 +344,5 @@ namespace mtconnect::sink::rest_sink { protected: Stream m_stream; }; - -} +} // namespace mtconnect::sink::rest_sink diff --git a/src/mtconnect/validation/observations.hpp b/src/mtconnect/validation/observations.hpp index 0eebdbed..c5a843ed 100644 --- a/src/mtconnect/validation/observations.hpp +++ b/src/mtconnect/validation/observations.hpp @@ -23,13 +23,13 @@ #include "../utilities.hpp" namespace mtconnect { - + /// @brief MTConnect validation containers namespace validation { - + /// @brief Observation validation containers namespace observations { - + /// @brief Validation type for observations using Validation = std::unordered_map>; From 51c57b967dc6d28842a52a9f8aa47ded54194e72 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Thu, 18 Apr 2024 22:36:26 -0400 Subject: [PATCH 12/35] Handled threading issues and send queue --- src/mtconnect/observation/change_observer.cpp | 4 + src/mtconnect/sink/rest_sink/rest_service.cpp | 28 ++- src/mtconnect/sink/rest_sink/rest_service.hpp | 6 +- src/mtconnect/sink/rest_sink/session.hpp | 5 +- src/mtconnect/sink/rest_sink/session_impl.cpp | 4 +- src/mtconnect/sink/rest_sink/session_impl.hpp | 5 +- .../sink/rest_sink/websocket_session.hpp | 209 ++++++++++++++---- 7 files changed, 206 insertions(+), 55 deletions(-) diff --git a/src/mtconnect/observation/change_observer.cpp b/src/mtconnect/observation/change_observer.cpp index 88b5c5da..dde38e9a 100644 --- a/src/mtconnect/observation/change_observer.cpp +++ b/src/mtconnect/observation/change_observer.cpp @@ -145,9 +145,12 @@ namespace mtconnect::observation { void AsyncObserver::handlerCompleted() { + NAMED_SCOPE("AsyncObserver::handlerCompleted"); + m_last = std::chrono::system_clock::now(); if (m_endOfBuffer) { + LOG(trace) << "End of buffer"; using std::placeholders::_1; m_observer.waitForSignal(m_heartbeat); } @@ -159,6 +162,7 @@ namespace mtconnect::observation { void AsyncObserver::handleSignal(boost::system::error_code ec) { + NAMED_SCOPE("AsyncObserver::handleSignal"); using namespace buffer; using std::placeholders::_1; diff --git a/src/mtconnect/sink/rest_sink/rest_service.cpp b/src/mtconnect/sink/rest_sink/rest_service.cpp index dee758ba..5cb1f3a0 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.cpp +++ b/src/mtconnect/sink/rest_sink/rest_service.cpp @@ -669,7 +669,8 @@ namespace mtconnect { streamCurrentRequest( session, printerForAccepts(request->m_accepts), *interval, request->parameter("device"), request->parameter("path"), - *request->parameter("pretty"), request->parameter("deviceType")); + *request->parameter("pretty"), request->parameter("deviceType"), + request->m_requestId); } else { @@ -711,7 +712,8 @@ namespace mtconnect { *request->parameter("heartbeat"), *request->parameter("count"), request->parameter("device"), request->parameter("from"), request->parameter("path"), *request->parameter("pretty"), - request->parameter("deviceType")); + request->parameter("deviceType"), + request->m_requestId); } else { @@ -932,6 +934,7 @@ namespace mtconnect { rest_sink::SessionPtr m_session; ofstream m_log; bool m_pretty {false}; + std::optional m_requestId; }; void RestService::streamSampleRequest(rest_sink::SessionPtr session, const Printer *printer, @@ -939,7 +942,8 @@ namespace mtconnect { const int count, const std::optional &device, const std::optional &from, const std::optional &path, bool pretty, - const std::optional &deviceType) + const std::optional &deviceType, + const std::optional &requestId) { NAMED_SCOPE("RestService::streamSampleRequest"); @@ -973,6 +977,7 @@ namespace mtconnect { asyncResponse->m_printer = printer; asyncResponse->m_sink = getptr(); asyncResponse->m_pretty = pretty; + asyncResponse->m_requestId = requestId; if (m_logStreamData) { @@ -990,7 +995,8 @@ namespace mtconnect { session->beginStreaming( printer->mimeType(), asio::bind_executor(m_strand, - boost::bind(&AsyncObserver::handlerCompleted, asyncResponse))); + boost::bind(&AsyncObserver::handlerCompleted, asyncResponse)), + requestId); } SequenceNumber_t RestService::streamNextSampleChunk( @@ -1021,7 +1027,9 @@ namespace mtconnect { asyncResponse->m_session->writeChunk( content, asio::bind_executor( - m_strand, boost::bind(&AsyncObserver::handlerCompleted, asyncResponse))); + m_strand, boost::bind(&AsyncObserver::handlerCompleted, asyncResponse) + ), + asyncResponse->m_requestId); return end; } @@ -1059,13 +1067,15 @@ namespace mtconnect { FilterSetOpt m_filter; boost::asio::steady_timer m_timer; bool m_pretty {false}; + std::optional m_requestId; }; void RestService::streamCurrentRequest(SessionPtr session, const Printer *printer, const int interval, const std::optional &device, const std::optional &path, bool pretty, - const std::optional &deviceType) + const std::optional &deviceType, + const std::optional &requestId) { checkRange(printer, interval, 0, numeric_limits().max(), "interval"); DevicePtr dev {nullptr}; @@ -1084,11 +1094,13 @@ namespace mtconnect { asyncResponse->m_printer = printer; asyncResponse->m_service = getptr(); asyncResponse->m_pretty = pretty; + asyncResponse->m_requestId = requestId; asyncResponse->m_session->beginStreaming( printer->mimeType(), boost::asio::bind_executor(m_strand, [this, asyncResponse]() { streamNextCurrent(asyncResponse, boost::system::error_code {}); - })); + }), + requestId); } void RestService::streamNextCurrent(std::shared_ptr asyncResponse, @@ -1127,7 +1139,7 @@ namespace mtconnect { asyncResponse->m_timer.expires_from_now(asyncResponse->m_interval); asyncResponse->m_timer.async_wait(boost::asio::bind_executor( m_strand, boost::bind(&RestService::streamNextCurrent, this, asyncResponse, _1))); - })); + }), asyncResponse->m_requestId); } catch (RequestError &re) { diff --git a/src/mtconnect/sink/rest_sink/rest_service.hpp b/src/mtconnect/sink/rest_sink/rest_service.hpp index 923f210d..93423144 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.hpp +++ b/src/mtconnect/sink/rest_sink/rest_service.hpp @@ -149,7 +149,8 @@ namespace mtconnect { const std::optional &from = std::nullopt, const std::optional &path = std::nullopt, bool pretty = false, - const std::optional &deviceType = std::nullopt); + const std::optional &deviceType = std::nullopt, + const std::optional &responseId = std::nullopt); /// @brief Handler for a streaming current /// @param[in] session session to stream data to @@ -162,7 +163,8 @@ namespace mtconnect { const std::optional &device = std::nullopt, const std::optional &path = std::nullopt, bool pretty = false, - const std::optional &deviceType = std::nullopt); + const std::optional &deviceType = std::nullopt, + const std::optional &responseId = std::nullopt); /// @brief Handler for put/post observation /// @param[in] p printer for response generation /// @param[in] device device diff --git a/src/mtconnect/sink/rest_sink/session.hpp b/src/mtconnect/sink/rest_sink/session.hpp index c4ed9041..dcb30e91 100644 --- a/src/mtconnect/sink/rest_sink/session.hpp +++ b/src/mtconnect/sink/rest_sink/session.hpp @@ -64,11 +64,12 @@ namespace mtconnect::sink::rest_sink { /// @brief begin streaming data to the client using x-multipart-replace /// @param mimeType the mime type of the response /// @param complete completion callback - virtual void beginStreaming(const std::string &mimeType, Complete complete) = 0; + virtual void beginStreaming(const std::string &mimeType, Complete complete, std::optional requestId = std::nullopt) = 0; /// @brief write a chunk for a streaming session /// @param chunk the chunk to write /// @param complete a completion callback - virtual void writeChunk(const std::string &chunk, Complete complete) = 0; + virtual void writeChunk(const std::string &chunk, Complete complete, + std::optional requestId = std::nullopt) = 0; /// @brief close the session virtual void close() = 0; /// @brief close the stream diff --git a/src/mtconnect/sink/rest_sink/session_impl.cpp b/src/mtconnect/sink/rest_sink/session_impl.cpp index d84c43fd..cce9a988 100644 --- a/src/mtconnect/sink/rest_sink/session_impl.cpp +++ b/src/mtconnect/sink/rest_sink/session_impl.cpp @@ -287,7 +287,7 @@ namespace mtconnect::sink::rest_sink { } template - void SessionImpl::beginStreaming(const std::string &mimeType, Complete complete) + void SessionImpl::beginStreaming(const std::string &mimeType, Complete complete, std::optional requestId) { NAMED_SCOPE("SessionImpl::beginStreaming"); @@ -321,7 +321,7 @@ namespace mtconnect::sink::rest_sink { } template - void SessionImpl::writeChunk(const std::string &body, Complete complete) + void SessionImpl::writeChunk(const std::string &body, Complete complete, std::optional requestId) { NAMED_SCOPE("SessionImpl::writeChunk"); diff --git a/src/mtconnect/sink/rest_sink/session_impl.hpp b/src/mtconnect/sink/rest_sink/session_impl.hpp index 07ab7fbd..2c046171 100644 --- a/src/mtconnect/sink/rest_sink/session_impl.hpp +++ b/src/mtconnect/sink/rest_sink/session_impl.hpp @@ -74,8 +74,9 @@ namespace mtconnect { void run() override; void writeResponse(ResponsePtr &&response, Complete complete = nullptr) override; void writeFailureResponse(ResponsePtr &&response, Complete complete = nullptr) override; - void beginStreaming(const std::string &mimeType, Complete complete) override; - void writeChunk(const std::string &chunk, Complete complete) override; + void beginStreaming(const std::string &mimeType, Complete complete, std::optional requestId = std::nullopt) override; + void writeChunk(const std::string &chunk, Complete complete, + std::optional requestId = std::nullopt) override; void closeStream() override; ///@} protected: diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp index f39b559e..c580facf 100644 --- a/src/mtconnect/sink/rest_sink/websocket_session.hpp +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -36,6 +37,8 @@ namespace mtconnect::sink::rest_sink { struct WebsocketRequest { + WebsocketRequest(const std::string &id) + : m_requestId(id) { } std::string m_requestId; std::optional m_streamBuffer; Complete m_complete; @@ -46,6 +49,19 @@ namespace mtconnect::sink::rest_sink { template class WebsocketSession : public Session { + protected: + struct Message { + Message(const std::string &body, Complete &complete, + const std::string &requestId) + : m_body(body), m_complete(complete), m_requestId(requestId) + { + } + + std::string m_body; + Complete m_complete; + std::string m_requestId; + }; + public: using RequestMessage = boost::beast::http::request; @@ -69,7 +85,7 @@ namespace mtconnect::sink::rest_sink { // Set suggested timeout settings for the websocket derived().stream().set_option( websocket::stream_base::timeout::suggested(beast::role_type::server)); - + // Set a decorator to change the Server of the handshake derived().stream().set_option( websocket::stream_base::decorator([](websocket::response_type &res) { @@ -78,51 +94,78 @@ namespace mtconnect::sink::rest_sink { // Accept the websocket handshake derived().stream().async_accept( - m_msg, beast::bind_front_handler(&WebsocketSession::onAccept, derived().shared_ptr())); + m_msg, boost::asio::bind_executor(derived().getExecutor(), beast::bind_front_handler(&WebsocketSession::onAccept, derived().shared_ptr()))); } void writeResponse(ResponsePtr &&response, Complete complete = nullptr) override { - if (response->m_requestId) + NAMED_SCOPE("WebsocketSession::writeResponse"); + if (!response->m_requestId) { - auto id = *(response->m_requestId); + boost::system::error_code ec; + return fail(status::bad_request, "Missing request Id", ec); + } + + writeChunk(response->m_body, complete, response->m_requestId); + } + + void writeFailureResponse(ResponsePtr &&response, Complete complete = nullptr) override + { + NAMED_SCOPE("WebsocketSession::writeFailureResponse"); + writeChunk(response->m_body, complete, response->m_requestId); + } + + void beginStreaming(const std::string &mimeType, Complete complete, std::optional requestId = std::nullopt) override + { + if (requestId) + { + auto id = *(requestId); auto it = m_requests.find(id); if (it != m_requests.end()) { - using namespace std::placeholders; - auto &req = it->second; - req.m_complete = complete; - req.m_streamBuffer.emplace(); - std::ostream str(&req.m_streamBuffer.value()); - - str << response->m_body; - - derived().stream().text(derived().stream().got_text()); - derived().stream().async_write( - req.m_streamBuffer->data(), - beast::bind_handler( - [this, id](beast::error_code ec, std::size_t len) { sent(ec, len, id); }, _1, - _2)); + req.m_streaming = true; + + if (complete) + complete(); } else { - LOG(error) << "WebsocketSession::writeResponse: Cannot find request for id: " << id; + LOG(error) << "Cannot find request for id: " << id; } } else { - LOG(error) << "WebsocketSession::writeResponse: No request id for websocket"; + LOG(error) << "No request id for websocket"; } } - void writeFailureResponse(ResponsePtr &&response, Complete complete = nullptr) override {} - - void beginStreaming(const std::string &mimeType, Complete complete) override {} - - void writeChunk(const std::string &chunk, Complete complete) override {} + void writeChunk(const std::string &chunk, Complete complete, std::optional requestId = std::nullopt) override + { + NAMED_SCOPE("WebsocketSession::writeChunk"); + if (requestId) + { + LOG(trace) << "Waiting for mutex"; + std::lock_guard lock(m_mutex); + + if (m_busy) + { + m_messageQueue.emplace_back(chunk, complete, *requestId); + } + else + { + send(chunk, complete, *requestId); + } + } + else + { + LOG(error) << "No request id for websocket"; + } + } - void closeStream() override {} + void closeStream() override + { + } protected: void onAccept(boost::beast::error_code ec) @@ -136,31 +179,101 @@ namespace mtconnect::sink::rest_sink { derived().stream().async_read( m_buffer, beast::bind_front_handler(&WebsocketSession::onRead, derived().shared_ptr())); } - - void sent(beast::error_code ec, std::size_t len, const std::string &id) + + void send(const std::string body, Complete complete, const std::string &requestId) { - auto it = m_requests.find(id); + NAMED_SCOPE("WebsocketSession::send"); + + using namespace std::placeholders; + + auto it = m_requests.find(requestId); if (it != m_requests.end()) { auto &req = it->second; - if (req.m_complete) + req.m_complete = complete; + req.m_streamBuffer.emplace(); + std::ostream str(&req.m_streamBuffer.value()); + + str << body; + + auto ref = derived().shared_ptr(); + + LOG(trace) << "writing chunk for ws: " << requestId; + + m_busy = true; + + derived().stream().text(derived().stream().got_text()); + derived().stream().async_write( + req.m_streamBuffer->data(), + beast::bind_handler( + [ref, requestId](beast::error_code ec, std::size_t len) { ref->sent(ec, len, requestId); }, _1, + _2)); + } + else + { + LOG(error) << "Cannot find request for id: " << requestId; + } + } + + void sent(beast::error_code ec, std::size_t len, const std::string &id) + { + NAMED_SCOPE("WebsocketSession::sent"); + + if (ec) + { + return fail(status::bad_request, "Missing request Id", ec); + } + + { + LOG(trace) << "Waiting for mutex"; + std::lock_guard lock(m_mutex); + + LOG(trace) << "sent chunk for ws: " << id; + + auto it = m_requests.find(id); + if (it != m_requests.end()) { - req.m_complete(); + auto &req = it->second; + if (req.m_complete) + { + boost::asio::post(derived().stream().get_executor(), + req.m_complete); + } + + if (!req.m_streaming) + { + m_requests.erase(id); + } + + if (m_messageQueue.size() == 0) + { + m_busy = false; + } } - - if (!req.m_streaming) + else { - m_requests.erase(id); + LOG(error) << "WebsocketSession::sent: Cannot find request for id: " << id; } } - else + { - LOG(error) << "WebsocketSession::sent: Cannot find request for id: " << id; + LOG(trace) << "Waiting for mutex to send next"; + std::lock_guard lock(m_mutex); + + // Check for queued messages + if (m_messageQueue.size() > 0) + { + auto &msg = m_messageQueue.front(); + send(msg.m_body, msg.m_complete, msg.m_requestId); + m_messageQueue.pop_front(); + } } } void onRead(beast::error_code ec, std::size_t len) { + NAMED_SCOPE("PlainWebsocketSession::onRead"); + if (ec) return fail(boost::beast::http::status::internal_server_error, "shutdown", ec); @@ -252,9 +365,16 @@ namespace mtconnect::sink::rest_sink { m_request->m_parameters.erase("id"); } - auto &req = m_requests[*(m_request->m_requestId)]; - req.m_requestId = *(m_request->m_requestId); - + auto &id = *(m_request->m_requestId); + + auto res = m_requests.emplace(id, id); + if (!res.second) + { + LOG(error) << "Duplicate request id: " << id; + boost::system::error_code ec; + return fail(status::bad_request, "Duplicate request Id", ec); + } + if (!m_dispatch(derived().shared_ptr(), m_request)) { ostringstream txt; @@ -272,6 +392,9 @@ namespace mtconnect::sink::rest_sink { RequestMessage m_msg; beast::flat_buffer m_buffer; std::map m_requests; + std::mutex m_mutex; + std::atomic_bool m_busy; + std::deque m_messageQueue; }; template @@ -297,6 +420,10 @@ namespace mtconnect::sink::rest_sink { if (m_stream.is_open()) m_stream.close(beast::websocket::close_code::none); } + + auto getExecutor() { + return m_stream.get_executor(); + } auto &stream() { return m_stream; } @@ -324,6 +451,10 @@ namespace mtconnect::sink::rest_sink { ~TlsWebsocketSession() { close(); } auto &stream() { return m_stream; } + + auto getExecutor() { + return m_stream.get_executor(); + } void close() override { From 558d625bc547f4a08861d641c351d7c0a778e0ff Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Thu, 18 Apr 2024 22:51:13 -0400 Subject: [PATCH 13/35] fixed expiry timeout --- src/mtconnect/sink/rest_sink/websocket_session.hpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp index c580facf..11bea044 100644 --- a/src/mtconnect/sink/rest_sink/websocket_session.hpp +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -409,7 +409,9 @@ namespace mtconnect::sink::rest_sink { Dispatch dispatch, ErrorFunction func) : WebsocketSession(std::move(request), std::move(msg), dispatch, func), m_stream(std::move(stream)) - {} + { + beast::get_lowest_layer(m_stream).expires_never(); + } ~PlainWebsocketSession() { close(); } void close() override @@ -447,7 +449,9 @@ namespace mtconnect::sink::rest_sink { RequestMessage &&msg, Dispatch dispatch, ErrorFunction func) : WebsocketSession(std::move(request), std::move(msg), dispatch, func), m_stream(std::move(stream)) - {} + { + beast::get_lowest_layer(m_stream).expires_never(); + } ~TlsWebsocketSession() { close(); } auto &stream() { return m_stream; } From 70f1ec1954f0e91fd8df4e7b6c8b66aab30d9be9 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Thu, 18 Apr 2024 22:53:46 -0400 Subject: [PATCH 14/35] Formatted new code --- src/mtconnect/observation/change_observer.cpp | 2 +- src/mtconnect/sink/rest_sink/rest_service.cpp | 48 +++++---- src/mtconnect/sink/rest_sink/session.hpp | 3 +- src/mtconnect/sink/rest_sink/session_impl.cpp | 6 +- src/mtconnect/sink/rest_sink/session_impl.hpp | 3 +- .../sink/rest_sink/websocket_session.hpp | 100 +++++++++--------- 6 files changed, 84 insertions(+), 78 deletions(-) diff --git a/src/mtconnect/observation/change_observer.cpp b/src/mtconnect/observation/change_observer.cpp index dde38e9a..e186a592 100644 --- a/src/mtconnect/observation/change_observer.cpp +++ b/src/mtconnect/observation/change_observer.cpp @@ -146,7 +146,7 @@ namespace mtconnect::observation { void AsyncObserver::handlerCompleted() { NAMED_SCOPE("AsyncObserver::handlerCompleted"); - + m_last = std::chrono::system_clock::now(); if (m_endOfBuffer) { diff --git a/src/mtconnect/sink/rest_sink/rest_service.cpp b/src/mtconnect/sink/rest_sink/rest_service.cpp index 5cb1f3a0..10e4d478 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.cpp +++ b/src/mtconnect/sink/rest_sink/rest_service.cpp @@ -666,11 +666,11 @@ namespace mtconnect { auto interval = request->parameter("interval"); if (interval) { - streamCurrentRequest( - session, printerForAccepts(request->m_accepts), *interval, - request->parameter("device"), request->parameter("path"), - *request->parameter("pretty"), request->parameter("deviceType"), - request->m_requestId); + streamCurrentRequest(session, printerForAccepts(request->m_accepts), *interval, + request->parameter("device"), + request->parameter("path"), + *request->parameter("pretty"), + request->parameter("deviceType"), request->m_requestId); } else { @@ -712,8 +712,7 @@ namespace mtconnect { *request->parameter("heartbeat"), *request->parameter("count"), request->parameter("device"), request->parameter("from"), request->parameter("path"), *request->parameter("pretty"), - request->parameter("deviceType"), - request->m_requestId); + request->parameter("deviceType"), request->m_requestId); } else { @@ -996,7 +995,7 @@ namespace mtconnect { printer->mimeType(), asio::bind_executor(m_strand, boost::bind(&AsyncObserver::handlerCompleted, asyncResponse)), - requestId); + requestId); } SequenceNumber_t RestService::streamNextSampleChunk( @@ -1026,10 +1025,10 @@ namespace mtconnect { asyncResponse->m_log << content << endl; asyncResponse->m_session->writeChunk( - content, asio::bind_executor( - m_strand, boost::bind(&AsyncObserver::handlerCompleted, asyncResponse) - ), - asyncResponse->m_requestId); + content, + asio::bind_executor(m_strand, + boost::bind(&AsyncObserver::handlerCompleted, asyncResponse)), + asyncResponse->m_requestId); return end; } @@ -1097,10 +1096,13 @@ namespace mtconnect { asyncResponse->m_requestId = requestId; asyncResponse->m_session->beginStreaming( - printer->mimeType(), boost::asio::bind_executor(m_strand, [this, asyncResponse]() { - streamNextCurrent(asyncResponse, boost::system::error_code {}); - }), - requestId); + printer->mimeType(), + boost::asio::bind_executor(m_strand, + [this, asyncResponse]() { + streamNextCurrent(asyncResponse, + boost::system::error_code {}); + }), + requestId); } void RestService::streamNextCurrent(std::shared_ptr asyncResponse, @@ -1135,11 +1137,15 @@ namespace mtconnect { asyncResponse->m_session->writeChunk( fetchCurrentData(asyncResponse->m_printer, asyncResponse->m_filter, nullopt, asyncResponse->m_pretty), - boost::asio::bind_executor(m_strand, [this, asyncResponse]() { - asyncResponse->m_timer.expires_from_now(asyncResponse->m_interval); - asyncResponse->m_timer.async_wait(boost::asio::bind_executor( - m_strand, boost::bind(&RestService::streamNextCurrent, this, asyncResponse, _1))); - }), asyncResponse->m_requestId); + boost::asio::bind_executor( + m_strand, + [this, asyncResponse]() { + asyncResponse->m_timer.expires_from_now(asyncResponse->m_interval); + asyncResponse->m_timer.async_wait(boost::asio::bind_executor( + m_strand, + boost::bind(&RestService::streamNextCurrent, this, asyncResponse, _1))); + }), + asyncResponse->m_requestId); } catch (RequestError &re) { diff --git a/src/mtconnect/sink/rest_sink/session.hpp b/src/mtconnect/sink/rest_sink/session.hpp index dcb30e91..4e2bc52a 100644 --- a/src/mtconnect/sink/rest_sink/session.hpp +++ b/src/mtconnect/sink/rest_sink/session.hpp @@ -64,7 +64,8 @@ namespace mtconnect::sink::rest_sink { /// @brief begin streaming data to the client using x-multipart-replace /// @param mimeType the mime type of the response /// @param complete completion callback - virtual void beginStreaming(const std::string &mimeType, Complete complete, std::optional requestId = std::nullopt) = 0; + virtual void beginStreaming(const std::string &mimeType, Complete complete, + std::optional requestId = std::nullopt) = 0; /// @brief write a chunk for a streaming session /// @param chunk the chunk to write /// @param complete a completion callback diff --git a/src/mtconnect/sink/rest_sink/session_impl.cpp b/src/mtconnect/sink/rest_sink/session_impl.cpp index cce9a988..40777da0 100644 --- a/src/mtconnect/sink/rest_sink/session_impl.cpp +++ b/src/mtconnect/sink/rest_sink/session_impl.cpp @@ -287,7 +287,8 @@ namespace mtconnect::sink::rest_sink { } template - void SessionImpl::beginStreaming(const std::string &mimeType, Complete complete, std::optional requestId) + void SessionImpl::beginStreaming(const std::string &mimeType, Complete complete, + std::optional requestId) { NAMED_SCOPE("SessionImpl::beginStreaming"); @@ -321,7 +322,8 @@ namespace mtconnect::sink::rest_sink { } template - void SessionImpl::writeChunk(const std::string &body, Complete complete, std::optional requestId) + void SessionImpl::writeChunk(const std::string &body, Complete complete, + std::optional requestId) { NAMED_SCOPE("SessionImpl::writeChunk"); diff --git a/src/mtconnect/sink/rest_sink/session_impl.hpp b/src/mtconnect/sink/rest_sink/session_impl.hpp index 2c046171..b890a603 100644 --- a/src/mtconnect/sink/rest_sink/session_impl.hpp +++ b/src/mtconnect/sink/rest_sink/session_impl.hpp @@ -74,7 +74,8 @@ namespace mtconnect { void run() override; void writeResponse(ResponsePtr &&response, Complete complete = nullptr) override; void writeFailureResponse(ResponsePtr &&response, Complete complete = nullptr) override; - void beginStreaming(const std::string &mimeType, Complete complete, std::optional requestId = std::nullopt) override; + void beginStreaming(const std::string &mimeType, Complete complete, + std::optional requestId = std::nullopt) override; void writeChunk(const std::string &chunk, Complete complete, std::optional requestId = std::nullopt) override; void closeStream() override; diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp index 11bea044..bd7d321f 100644 --- a/src/mtconnect/sink/rest_sink/websocket_session.hpp +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -22,8 +22,8 @@ #include #include -#include #include +#include #include #include @@ -37,8 +37,7 @@ namespace mtconnect::sink::rest_sink { struct WebsocketRequest { - WebsocketRequest(const std::string &id) - : m_requestId(id) { } + WebsocketRequest(const std::string &id) : m_requestId(id) {} std::string m_requestId; std::optional m_streamBuffer; Complete m_complete; @@ -50,18 +49,17 @@ namespace mtconnect::sink::rest_sink { class WebsocketSession : public Session { protected: - struct Message { - Message(const std::string &body, Complete &complete, - const std::string &requestId) - : m_body(body), m_complete(complete), m_requestId(requestId) - { - } - + struct Message + { + Message(const std::string &body, Complete &complete, const std::string &requestId) + : m_body(body), m_complete(complete), m_requestId(requestId) + {} + std::string m_body; Complete m_complete; std::string m_requestId; }; - + public: using RequestMessage = boost::beast::http::request; @@ -85,7 +83,7 @@ namespace mtconnect::sink::rest_sink { // Set suggested timeout settings for the websocket derived().stream().set_option( websocket::stream_base::timeout::suggested(beast::role_type::server)); - + // Set a decorator to change the Server of the handshake derived().stream().set_option( websocket::stream_base::decorator([](websocket::response_type &res) { @@ -94,7 +92,9 @@ namespace mtconnect::sink::rest_sink { // Accept the websocket handshake derived().stream().async_accept( - m_msg, boost::asio::bind_executor(derived().getExecutor(), beast::bind_front_handler(&WebsocketSession::onAccept, derived().shared_ptr()))); + m_msg, boost::asio::bind_executor(derived().getExecutor(), + beast::bind_front_handler(&WebsocketSession::onAccept, + derived().shared_ptr()))); } void writeResponse(ResponsePtr &&response, Complete complete = nullptr) override @@ -109,13 +109,14 @@ namespace mtconnect::sink::rest_sink { writeChunk(response->m_body, complete, response->m_requestId); } - void writeFailureResponse(ResponsePtr &&response, Complete complete = nullptr) override + void writeFailureResponse(ResponsePtr &&response, Complete complete = nullptr) override { NAMED_SCOPE("WebsocketSession::writeFailureResponse"); writeChunk(response->m_body, complete, response->m_requestId); } - void beginStreaming(const std::string &mimeType, Complete complete, std::optional requestId = std::nullopt) override + void beginStreaming(const std::string &mimeType, Complete complete, + std::optional requestId = std::nullopt) override { if (requestId) { @@ -125,7 +126,7 @@ namespace mtconnect::sink::rest_sink { { auto &req = it->second; req.m_streaming = true; - + if (complete) complete(); } @@ -140,14 +141,15 @@ namespace mtconnect::sink::rest_sink { } } - void writeChunk(const std::string &chunk, Complete complete, std::optional requestId = std::nullopt) override + void writeChunk(const std::string &chunk, Complete complete, + std::optional requestId = std::nullopt) override { NAMED_SCOPE("WebsocketSession::writeChunk"); if (requestId) { LOG(trace) << "Waiting for mutex"; std::lock_guard lock(m_mutex); - + if (m_busy) { m_messageQueue.emplace_back(chunk, complete, *requestId); @@ -163,9 +165,7 @@ namespace mtconnect::sink::rest_sink { } } - void closeStream() override - { - } + void closeStream() override {} protected: void onAccept(boost::beast::error_code ec) @@ -179,13 +179,13 @@ namespace mtconnect::sink::rest_sink { derived().stream().async_read( m_buffer, beast::bind_front_handler(&WebsocketSession::onRead, derived().shared_ptr())); } - + void send(const std::string body, Complete complete, const std::string &requestId) { NAMED_SCOPE("WebsocketSession::send"); using namespace std::placeholders; - + auto it = m_requests.find(requestId); if (it != m_requests.end()) { @@ -193,21 +193,22 @@ namespace mtconnect::sink::rest_sink { req.m_complete = complete; req.m_streamBuffer.emplace(); std::ostream str(&req.m_streamBuffer.value()); - + str << body; - + auto ref = derived().shared_ptr(); - + LOG(trace) << "writing chunk for ws: " << requestId; - + m_busy = true; - + derived().stream().text(derived().stream().got_text()); - derived().stream().async_write( - req.m_streamBuffer->data(), + derived().stream().async_write(req.m_streamBuffer->data(), beast::bind_handler( - [ref, requestId](beast::error_code ec, std::size_t len) { ref->sent(ec, len, requestId); }, _1, - _2)); + [ref, requestId](beast::error_code ec, std::size_t len) { + ref->sent(ec, len, requestId); + }, + _1, _2)); } else { @@ -218,33 +219,32 @@ namespace mtconnect::sink::rest_sink { void sent(beast::error_code ec, std::size_t len, const std::string &id) { NAMED_SCOPE("WebsocketSession::sent"); - + if (ec) { return fail(status::bad_request, "Missing request Id", ec); } - + { LOG(trace) << "Waiting for mutex"; std::lock_guard lock(m_mutex); LOG(trace) << "sent chunk for ws: " << id; - + auto it = m_requests.find(id); if (it != m_requests.end()) { auto &req = it->second; if (req.m_complete) { - boost::asio::post(derived().stream().get_executor(), - req.m_complete); + boost::asio::post(derived().stream().get_executor(), req.m_complete); } - + if (!req.m_streaming) { m_requests.erase(id); } - + if (m_messageQueue.size() == 0) { m_busy = false; @@ -255,11 +255,11 @@ namespace mtconnect::sink::rest_sink { LOG(error) << "WebsocketSession::sent: Cannot find request for id: " << id; } } - + { LOG(trace) << "Waiting for mutex to send next"; std::lock_guard lock(m_mutex); - + // Check for queued messages if (m_messageQueue.size() > 0) { @@ -273,7 +273,7 @@ namespace mtconnect::sink::rest_sink { void onRead(beast::error_code ec, std::size_t len) { NAMED_SCOPE("PlainWebsocketSession::onRead"); - + if (ec) return fail(boost::beast::http::status::internal_server_error, "shutdown", ec); @@ -366,7 +366,7 @@ namespace mtconnect::sink::rest_sink { } auto &id = *(m_request->m_requestId); - + auto res = m_requests.emplace(id, id); if (!res.second) { @@ -374,7 +374,7 @@ namespace mtconnect::sink::rest_sink { boost::system::error_code ec; return fail(status::bad_request, "Duplicate request Id", ec); } - + if (!m_dispatch(derived().shared_ptr(), m_request)) { ostringstream txt; @@ -422,10 +422,8 @@ namespace mtconnect::sink::rest_sink { if (m_stream.is_open()) m_stream.close(beast::websocket::close_code::none); } - - auto getExecutor() { - return m_stream.get_executor(); - } + + auto getExecutor() { return m_stream.get_executor(); } auto &stream() { return m_stream; } @@ -455,10 +453,8 @@ namespace mtconnect::sink::rest_sink { ~TlsWebsocketSession() { close(); } auto &stream() { return m_stream; } - - auto getExecutor() { - return m_stream.get_executor(); - } + + auto getExecutor() { return m_stream.get_executor(); } void close() override { From 8877c03ea5bde604f02a90fc9160b76103aeaa29 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Fri, 19 Apr 2024 11:13:26 -0400 Subject: [PATCH 15/35] Working version with error handling --- .../sink/rest_sink/websocket_session.hpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp index bd7d321f..fcf91939 100644 --- a/src/mtconnect/sink/rest_sink/websocket_session.hpp +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -150,7 +150,7 @@ namespace mtconnect::sink::rest_sink { LOG(trace) << "Waiting for mutex"; std::lock_guard lock(m_mutex); - if (m_busy) + if (m_busy || m_messageQueue.size() > 0) { m_messageQueue.emplace_back(chunk, complete, *requestId); } @@ -165,8 +165,6 @@ namespace mtconnect::sink::rest_sink { } } - void closeStream() override {} - protected: void onAccept(boost::beast::error_code ec) { @@ -372,7 +370,7 @@ namespace mtconnect::sink::rest_sink { { LOG(error) << "Duplicate request id: " << id; boost::system::error_code ec; - return fail(status::bad_request, "Duplicate request Id", ec); + fail(status::bad_request, "Duplicate request Id", ec); } if (!m_dispatch(derived().shared_ptr(), m_request)) @@ -380,6 +378,8 @@ namespace mtconnect::sink::rest_sink { ostringstream txt; txt << "Failed to find handler for " << buffer; LOG(error) << txt.str(); + boost::system::error_code ec; + fail(status::bad_request, "Duplicate request Id", ec); } } @@ -419,10 +419,16 @@ namespace mtconnect::sink::rest_sink { NAMED_SCOPE("PlainWebsocketSession::close"); m_request.reset(); + closeStream(); + } + + void closeStream() override + { if (m_stream.is_open()) m_stream.close(beast::websocket::close_code::none); } + auto getExecutor() { return m_stream.get_executor(); } auto &stream() { return m_stream; } @@ -461,6 +467,11 @@ namespace mtconnect::sink::rest_sink { NAMED_SCOPE("TlsWebsocketSession::close"); m_request.reset(); + closeStream(); + } + + void closeStream() override + { if (m_stream.is_open()) m_stream.close(beast::websocket::close_code::none); } From 20ce83abf1339a360de72cf29a9043b59f330c65 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Fri, 19 Apr 2024 11:36:37 -0400 Subject: [PATCH 16/35] Added request id to the printers for XML --- src/mtconnect/printer/json_printer.cpp | 12 +++-- src/mtconnect/printer/json_printer.hpp | 12 +++-- src/mtconnect/printer/printer.hpp | 15 ++++--- src/mtconnect/printer/xml_printer.cpp | 27 ++++++++---- src/mtconnect/printer/xml_printer.hpp | 13 +++--- src/mtconnect/sink/rest_sink/rest_service.cpp | 44 ++++++++++++------- src/mtconnect/sink/rest_sink/rest_service.hpp | 28 +++++++----- 7 files changed, 98 insertions(+), 53 deletions(-) diff --git a/src/mtconnect/printer/json_printer.cpp b/src/mtconnect/printer/json_printer.cpp index 28720379..c604a653 100644 --- a/src/mtconnect/printer/json_printer.cpp +++ b/src/mtconnect/printer/json_printer.cpp @@ -113,7 +113,8 @@ namespace mtconnect::printer { std::string JsonPrinter::printErrors(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const ProtoErrorList &list, - bool pretty) const + bool pretty, + const std::optional requestId) const { defaultSchemaVersion(); @@ -170,7 +171,8 @@ namespace mtconnect::printer { const unsigned int assetCount, const std::list &devices, const std::map *count, - bool includeHidden, bool pretty) const + bool includeHidden, bool pretty, + const std::optional requestId) const { defaultSchemaVersion(); @@ -197,7 +199,8 @@ namespace mtconnect::printer { std::string JsonPrinter::printAssets(const uint64_t instanceId, const unsigned int bufferSize, const unsigned int assetCount, const asset::AssetList &asset, - bool pretty) const + bool pretty, + const std::optional requestId) const { defaultSchemaVersion(); @@ -397,7 +400,8 @@ namespace mtconnect::printer { std::string JsonPrinter::printSample(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const uint64_t firstSeq, const uint64_t lastSeq, ObservationList &observations, - bool pretty) const + bool pretty, + const std::optional requestId) const { defaultSchemaVersion(); diff --git a/src/mtconnect/printer/json_printer.hpp b/src/mtconnect/printer/json_printer.hpp index fd392133..102cf51e 100644 --- a/src/mtconnect/printer/json_printer.hpp +++ b/src/mtconnect/printer/json_printer.hpp @@ -32,21 +32,25 @@ namespace mtconnect::printer { std::string printErrors(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const ProtoErrorList &list, - bool pretty = false) const override; + bool pretty = false, + const std::optional requestId = std::nullopt) const override; std::string printProbe(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const unsigned int assetBufferSize, const unsigned int assetCount, const std::list &devices, const std::map *count = nullptr, - bool includeHidden = false, bool pretty = false) const override; + bool includeHidden = false, bool pretty = false, + const std::optional requestId = std::nullopt) const override; std::string printSample(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const uint64_t firstSeq, const uint64_t lastSeq, observation::ObservationList &results, - bool pretty = false) const override; + bool pretty = false, + const std::optional requestId = std::nullopt) const override; std::string printAssets(const uint64_t anInstanceId, const unsigned int bufferSize, const unsigned int assetCount, const asset::AssetList &asset, - bool pretty = false) const override; + bool pretty = false, + const std::optional requestId = std::nullopt) const override; std::string mimeType() const override { return "application/mtconnect+json"; } uint32_t getJsonVersion() const { return m_jsonVersion; } diff --git a/src/mtconnect/printer/printer.hpp b/src/mtconnect/printer/printer.hpp index 52943c17..38727109 100644 --- a/src/mtconnect/printer/printer.hpp +++ b/src/mtconnect/printer/printer.hpp @@ -63,7 +63,8 @@ namespace mtconnect { /// @return the error document virtual std::string printError(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const std::string &errorCode, - const std::string &errorText, bool pretty = false) const + const std::string &errorText, bool pretty = false, + const std::optional requestId = std::nullopt) const { return printErrors(instanceId, bufferSize, nextSeq, {{errorCode, errorText}}); } @@ -75,7 +76,8 @@ namespace mtconnect { /// @return the MTConnect Error document virtual std::string printErrors(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const ProtoErrorList &list, - bool pretty = false) const = 0; + bool pretty = false, + const std::optional requestId = std::nullopt) const = 0; /// @brief Generate an MTConnect Devices document /// @param[in] instanceId the instance id /// @param[in] bufferSize the buffer size @@ -90,7 +92,8 @@ namespace mtconnect { const unsigned int assetCount, const std::list &devices, const std::map *count = nullptr, - bool includeHidden = false, bool pretty = false) const = 0; + bool includeHidden = false, bool pretty = false, + const std::optional requestId = std::nullopt) const = 0; /// @brief Print a MTConnect Streams document /// @param[in] instanceId the instance id /// @param[in] bufferSize the buffer size @@ -102,7 +105,8 @@ namespace mtconnect { virtual std::string printSample(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const uint64_t firstSeq, const uint64_t lastSeq, observation::ObservationList &results, - bool pretty = false) const = 0; + bool pretty = false, + const std::optional requestId = std::nullopt) const = 0; /// @brief Generate an MTConnect Assets document /// @param[in] anInstanceId the instance id /// @param[in] bufferSize the buffer size @@ -111,7 +115,8 @@ namespace mtconnect { /// @return the MTConnect Assets document virtual std::string printAssets(const uint64_t anInstanceId, const unsigned int bufferSize, const unsigned int assetCount, asset::AssetList const &asset, - bool pretty = false) const = 0; + bool pretty = false, + const std::optional requestId = std::nullopt) const = 0; /// @brief get the mime type for the documents /// @return the mime type virtual std::string mimeType() const = 0; diff --git a/src/mtconnect/printer/xml_printer.cpp b/src/mtconnect/printer/xml_printer.cpp index 56e05947..2e5badb3 100644 --- a/src/mtconnect/printer/xml_printer.cpp +++ b/src/mtconnect/printer/xml_printer.cpp @@ -344,7 +344,7 @@ namespace mtconnect::printer { std::string XmlPrinter::printErrors(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const ProtoErrorList &list, - bool pretty) const + bool pretty, const std::optional requestId) const { string ret; @@ -352,7 +352,8 @@ namespace mtconnect::printer { { XmlWriter writer(m_pretty || pretty); - initXmlDoc(writer, eERROR, instanceId, bufferSize, 0, 0, nextSeq, nextSeq - 1); + initXmlDoc(writer, eERROR, instanceId, bufferSize, 0, 0, nextSeq, 0, nextSeq - 1, + nullptr, requestId); { AutoElement e1(writer, "Errors"); @@ -382,7 +383,7 @@ namespace mtconnect::printer { const uint64_t nextSeq, const unsigned int assetBufferSize, const unsigned int assetCount, const list &deviceList, const std::map *count, bool includeHidden, - bool pretty) const + bool pretty, const std::optional requestId) const { string ret; @@ -391,7 +392,7 @@ namespace mtconnect::printer { XmlWriter writer(m_pretty || pretty); initXmlDoc(writer, eDEVICES, instanceId, bufferSize, assetBufferSize, assetCount, nextSeq, 0, - nextSeq - 1, count); + nextSeq - 1, count, requestId); { AutoElement devices(writer, "Devices"); @@ -419,7 +420,7 @@ namespace mtconnect::printer { string XmlPrinter::printSample(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const uint64_t firstSeq, const uint64_t lastSeq, ObservationList &observations, - bool pretty) const + bool pretty, const std::optional requestId) const { string ret; @@ -427,7 +428,8 @@ namespace mtconnect::printer { { XmlWriter writer(m_pretty || pretty); - initXmlDoc(writer, eSTREAMS, instanceId, bufferSize, 0, 0, nextSeq, firstSeq, lastSeq); + initXmlDoc(writer, eSTREAMS, instanceId, bufferSize, 0, 0, nextSeq, firstSeq, lastSeq, + nullptr, requestId); AutoElement streams(writer, "Streams"); @@ -499,13 +501,15 @@ namespace mtconnect::printer { string XmlPrinter::printAssets(const uint64_t instanceId, const unsigned int bufferSize, const unsigned int assetCount, const AssetList &asset, - bool pretty) const + bool pretty, + const std::optional requestId) const { string ret; try { XmlWriter writer(m_pretty || pretty); - initXmlDoc(writer, eASSETS, instanceId, 0u, bufferSize, assetCount, 0ull); + initXmlDoc(writer, eASSETS, instanceId, 0u, bufferSize, assetCount, 0ull, + 0, 0, nullptr, requestId); { AutoElement ele(writer, "Assets"); @@ -541,7 +545,8 @@ namespace mtconnect::printer { const uint64_t instanceId, const unsigned int bufferSize, const unsigned int assetBufferSize, const unsigned int assetCount, const uint64_t nextSeq, const uint64_t firstSeq, - const uint64_t lastSeq, const map *count) const + const uint64_t lastSeq, const map *count, + const std::optional requestId) const { THROW_IF_XML2_ERROR(xmlTextWriterStartDocument(writer, nullptr, "UTF-8", nullptr)); @@ -647,6 +652,10 @@ namespace mtconnect::printer { sprintf(version, "%d.%d.%d.%d", AGENT_VERSION_MAJOR, AGENT_VERSION_MINOR, AGENT_VERSION_PATCH, AGENT_VERSION_BUILD); addAttribute(writer, "version", version); + + if (requestId) + addAttribute(writer, "requestId", *requestId); + int major, minor; char c; diff --git a/src/mtconnect/printer/xml_printer.hpp b/src/mtconnect/printer/xml_printer.hpp index 781a297c..337f1fae 100644 --- a/src/mtconnect/printer/xml_printer.hpp +++ b/src/mtconnect/printer/xml_printer.hpp @@ -45,21 +45,23 @@ namespace mtconnect { std::string printErrors(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const ProtoErrorList &list, - bool pretty = false) const override; + bool pretty = false, + const std::optional requestId = std::nullopt) const override; std::string printProbe(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const unsigned int assetBufferSize, const unsigned int assetCount, const std::list &devices, const std::map *count = nullptr, - bool includeHidden = false, bool pretty = false) const override; + bool includeHidden = false, bool pretty = false, + const std::optional requestId = std::nullopt) const override; std::string printSample(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const uint64_t firstSeq, const uint64_t lastSeq, observation::ObservationList &results, - bool pretty = false) const override; + bool pretty = false, const std::optional requestId = std::nullopt) const override; std::string printAssets(const uint64_t anInstanceId, const unsigned int bufferSize, const unsigned int assetCount, const asset::AssetList &asset, - bool pretty = false) const override; + bool pretty = false, const std::optional requestId = std::nullopt) const override; std::string mimeType() const override { return "text/xml"; } /// @brief Add a Devices XML device namespace @@ -167,7 +169,8 @@ namespace mtconnect { const unsigned int bufferSize, const unsigned int assetBufferSize, const unsigned int assetCount, const uint64_t nextSeq, const uint64_t firstSeq = 0, const uint64_t lastSeq = 0, - const std::map *counts = nullptr) const; + const std::map *counts = nullptr, + const std::optional requestId = std::nullopt) const; // Helper to print individual components and details void printProbeHelper(xmlTextWriterPtr writer, device_model::ComponentPtr component, diff --git a/src/mtconnect/sink/rest_sink/rest_service.cpp b/src/mtconnect/sink/rest_sink/rest_service.cpp index 10e4d478..fb9951d5 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.cpp +++ b/src/mtconnect/sink/rest_sink/rest_service.cpp @@ -795,7 +795,8 @@ namespace mtconnect { ResponsePtr RestService::probeRequest(const Printer *printer, const std::optional &device, bool pretty, - const std::optional &deviceType) + const std::optional &deviceType, + const std::optional &requestId) { NAMED_SCOPE("RestService::probeRequest"); @@ -832,7 +833,8 @@ namespace mtconnect { const std::optional &device, const std::optional &at, const std::optional &path, bool pretty, - const std::optional &deviceType) + const std::optional &deviceType, + const std::optional &requestId) { using namespace rest_sink; DevicePtr dev {nullptr}; @@ -858,7 +860,8 @@ namespace mtconnect { const std::optional &from, const std::optional &to, const std::optional &path, bool pretty, - const std::optional &deviceType) + const std::optional &deviceType, + const std::optional &requestId) { using namespace rest_sink; DevicePtr dev {nullptr}; @@ -1019,7 +1022,8 @@ namespace mtconnect { string content = fetchSampleData(asyncResponse->m_printer, asyncResponse->getFilter(), asyncResponse->m_count, from, nullopt, end, - asyncObserver->m_endOfBuffer, asyncResponse->m_pretty); + asyncObserver->m_endOfBuffer, asyncResponse->m_pretty, + asyncResponse->m_requestId); if (m_logStreamData) asyncResponse->m_log << content << endl; @@ -1136,7 +1140,7 @@ namespace mtconnect { asyncResponse->m_session->writeChunk( fetchCurrentData(asyncResponse->m_printer, asyncResponse->m_filter, nullopt, - asyncResponse->m_pretty), + asyncResponse->m_pretty, asyncResponse->m_requestId), boost::asio::bind_executor( m_strand, [this, asyncResponse]() { @@ -1168,7 +1172,8 @@ namespace mtconnect { ResponsePtr RestService::assetRequest(const Printer *printer, const int32_t count, const bool removed, const std::optional &type, - const std::optional &device, bool pretty) + const std::optional &device, bool pretty, + const std::optional &requestId) { using namespace rest_sink; @@ -1186,12 +1191,13 @@ namespace mtconnect { status::ok, printer->printAssets( m_instanceId, uint32_t(m_sinkContract->getAssetStorage()->getMaxAssets()), - uint32_t(m_sinkContract->getAssetStorage()->getCount()), list, pretty), + uint32_t(m_sinkContract->getAssetStorage()->getCount()), list, pretty, requestId), printer->mimeType()); } ResponsePtr RestService::assetIdsRequest(const Printer *printer, - const std::list &ids, bool pretty) + const std::list &ids, bool pretty, + const std::optional &requestId) { using namespace rest_sink; @@ -1205,7 +1211,7 @@ namespace mtconnect { auto message = str.str().substr(0, str.str().size() - 2); return make_unique(status::not_found, - printError(printer, "ASSET_NOT_FOUND", message, pretty), + printError(printer, "ASSET_NOT_FOUND", message, pretty, requestId), printer->mimeType()); } else @@ -1214,7 +1220,7 @@ namespace mtconnect { status::ok, printer->printAssets( m_instanceId, uint32_t(m_sinkContract->getAssetStorage()->getMaxAssets()), - uint32_t(m_sinkContract->getAssetStorage()->getCount()), list, pretty), + uint32_t(m_sinkContract->getAssetStorage()->getCount()), list, pretty, requestId), printer->mimeType()); } } @@ -1400,13 +1406,15 @@ namespace mtconnect { } string RestService::printError(const Printer *printer, const string &errorCode, - const string &text, bool pretty) const + const string &text, bool pretty, + const std::optional &requestId) const { LOG(debug) << "Returning error " << errorCode << ": " << text; if (printer) return printer->printError( m_instanceId, m_sinkContract->getCircularBuffer().getBufferSize(), - m_sinkContract->getCircularBuffer().getSequence(), errorCode, text, pretty); + m_sinkContract->getCircularBuffer().getSequence(), errorCode, text, pretty, + requestId); else return errorCode + ": " + text; } @@ -1482,7 +1490,8 @@ namespace mtconnect { // ------------------------------------------- string RestService::fetchCurrentData(const Printer *printer, const FilterSetOpt &filterSet, - const optional &at, bool pretty) + const optional &at, bool pretty, + const std::optional &requestId) { ObservationList observations; SequenceNumber_t firstSeq, seq; @@ -1506,13 +1515,15 @@ namespace mtconnect { } return printer->printSample(m_instanceId, m_sinkContract->getCircularBuffer().getBufferSize(), - seq, firstSeq, seq - 1, observations, pretty); + seq, firstSeq, seq - 1, observations, pretty, + requestId); } string RestService::fetchSampleData(const Printer *printer, const FilterSetOpt &filterSet, int count, const std::optional &from, const std::optional &to, - SequenceNumber_t &end, bool &endOfBuffer, bool pretty) + SequenceNumber_t &end, bool &endOfBuffer, bool pretty, + const std::optional &requestId) { std::unique_ptr observations; SequenceNumber_t firstSeq, lastSeq; @@ -1542,7 +1553,8 @@ namespace mtconnect { } return printer->printSample(m_instanceId, m_sinkContract->getCircularBuffer().getBufferSize(), - end, firstSeq, lastSeq, *observations, pretty); + end, firstSeq, lastSeq, *observations, pretty, + requestId); } } // namespace sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/rest_service.hpp b/src/mtconnect/sink/rest_sink/rest_service.hpp index 93423144..5957b401 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.hpp +++ b/src/mtconnect/sink/rest_sink/rest_service.hpp @@ -101,7 +101,8 @@ namespace mtconnect { ResponsePtr probeRequest(const printer::Printer *p, const std::optional &device = std::nullopt, bool pretty = false, - const std::optional &deviceType = std::nullopt); + const std::optional &deviceType = std::nullopt, + const std::optional &requestId = std::nullopt); /// @brief Handler for a current request /// @param[in] p printer for doc generation @@ -115,7 +116,8 @@ namespace mtconnect { const std::optional &at = std::nullopt, const std::optional &path = std::nullopt, bool pretty = false, - const std::optional &deviceType = std::nullopt); + const std::optional &deviceType = std::nullopt, + const std::optional &requestId = std::nullopt); /// @brief Handler for a sample request /// @param[in] p printer for doc generation @@ -132,7 +134,8 @@ namespace mtconnect { const std::optional &to = std::nullopt, const std::optional &path = std::nullopt, bool pretty = false, - const std::optional &deviceType = std::nullopt); + const std::optional &deviceType = std::nullopt, + const std::optional &requestId = std::nullopt); /// @brief Handler for a streaming sample /// @param[in] session session to stream data to /// @param[in] p printer for doc generation @@ -150,7 +153,7 @@ namespace mtconnect { const std::optional &path = std::nullopt, bool pretty = false, const std::optional &deviceType = std::nullopt, - const std::optional &responseId = std::nullopt); + const std::optional &requestId = std::nullopt); /// @brief Handler for a streaming current /// @param[in] session session to stream data to @@ -164,7 +167,7 @@ namespace mtconnect { const std::optional &path = std::nullopt, bool pretty = false, const std::optional &deviceType = std::nullopt, - const std::optional &responseId = std::nullopt); + const std::optional &requestId = std::nullopt); /// @brief Handler for put/post observation /// @param[in] p printer for response generation /// @param[in] device device @@ -207,7 +210,8 @@ namespace mtconnect { ResponsePtr assetRequest(const printer::Printer *p, const int32_t count, const bool removed, const std::optional &type = std::nullopt, const std::optional &device = std::nullopt, - bool pretty = false); + bool pretty = false, + const std::optional &requestId = std::nullopt); /// @brief Asset request handler using a list of asset ids /// @param[in] p printer for the response document @@ -215,7 +219,8 @@ namespace mtconnect { /// @param[in] pretty `true` to ensure response is formatted /// @return MTConnect Assets response document ResponsePtr assetIdsRequest(const printer::Printer *p, const std::list &ids, - bool pretty = false); + bool pretty = false, + const std::optional &requestId = std::nullopt); /// @brief Asset request handler to update an asset /// @param p printer for the response document @@ -262,7 +267,8 @@ namespace mtconnect { /// @param text descriptive error text /// @return MTConnect Error document std::string printError(const printer::Printer *printer, const std::string &errorCode, - const std::string &text, bool pretty = false) const; + const std::string &text, bool pretty = false, + const std::optional &requestId = std::nullopt) const; /// @name For testing only ///@{ @@ -301,13 +307,15 @@ namespace mtconnect { // Current Data Collection std::string fetchCurrentData(const printer::Printer *printer, const FilterSetOpt &filterSet, - const std::optional &at, bool pretty = false); + const std::optional &at, bool pretty = false, + const std::optional &requestId = std::nullopt); // Sample data collection std::string fetchSampleData(const printer::Printer *printer, const FilterSetOpt &filterSet, int count, const std::optional &from, const std::optional &to, SequenceNumber_t &end, - bool &endOfBuffer, bool pretty = false); + bool &endOfBuffer, bool pretty = false, + const std::optional &requestId = std::nullopt); // Verification methods template From ac8dbebf5aee6053247200f41e184aac3302357e Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Fri, 19 Apr 2024 12:37:28 -0400 Subject: [PATCH 17/35] Added format to all the requests --- src/mtconnect/printer/json_printer.cpp | 23 ++- src/mtconnect/printer/json_printer.hpp | 42 ++--- src/mtconnect/printer/printer.hpp | 46 +++-- src/mtconnect/printer/xml_printer.cpp | 18 +- src/mtconnect/printer/xml_printer.hpp | 40 ++--- src/mtconnect/sink/rest_sink/rest_service.cpp | 158 ++++++++++-------- src/mtconnect/sink/rest_sink/rest_service.hpp | 21 ++- .../sink/rest_sink/websocket_session.hpp | 3 +- 8 files changed, 198 insertions(+), 153 deletions(-) diff --git a/src/mtconnect/printer/json_printer.cpp b/src/mtconnect/printer/json_printer.cpp index c604a653..3aa2ea39 100644 --- a/src/mtconnect/printer/json_printer.cpp +++ b/src/mtconnect/printer/json_printer.cpp @@ -60,7 +60,8 @@ namespace mtconnect::printer { template inline void header(AutoJsonObject &obj, const string &version, const string &hostname, const uint64_t instanceId, const unsigned int bufferSize, - const string &schemaVersion, const string modelChangeTime, bool validation) + const string &schemaVersion, const string modelChangeTime, bool validation, + const std::optional &requestId) { obj.AddPairs("version", version, "creationTime", getCurrentTime(GMT), "testIndicator", false, "instanceId", instanceId, "sender", hostname, "schemaVersion", schemaVersion); @@ -71,6 +72,8 @@ namespace mtconnect::printer { obj.AddPairs("bufferSize", bufferSize); if (validation) obj.AddPairs("validation", true); + if (requestId) + obj.AddPairs("requestId", *requestId); } template @@ -78,10 +81,11 @@ namespace mtconnect::printer { const string &hostname, const uint64_t instanceId, const unsigned int bufferSize, const unsigned int assetBufferSize, const unsigned int assetCount, const string &schemaVersion, - const string modelChangeTime, const bool validation) + const string modelChangeTime, const bool validation, + const std::optional &requestId) { header(obj, version, hostname, instanceId, bufferSize, schemaVersion, modelChangeTime, - validation); + validation, requestId); obj.AddPairs("assetBufferSize", assetBufferSize, "assetCount", assetCount); } @@ -90,10 +94,11 @@ namespace mtconnect::printer { const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSequence, const uint64_t firstSequence, const uint64_t lastSequence, const string &schemaVersion, - const string modelChangeTime, const bool validation) + const string modelChangeTime, const bool validation, + const std::optional &requestId) { header(obj, version, hostname, instanceId, bufferSize, schemaVersion, modelChangeTime, - validation); + validation, requestId); obj.AddPairs("nextSequence", nextSequence, "lastSequence", lastSequence, "firstSequence", firstSequence); } @@ -128,7 +133,7 @@ namespace mtconnect::printer { { AutoJsonObject obj(writer, "Header"); header(obj, m_version, m_senderName, instanceId, bufferSize, *m_schemaVersion, - m_modelChangeTime, m_validation); + m_modelChangeTime, m_validation, requestId); } { if (m_jsonVersion > 1) @@ -186,7 +191,7 @@ namespace mtconnect::printer { { AutoJsonObject obj(writer, "Header"); probeAssetHeader(obj, m_version, m_senderName, instanceId, bufferSize, assetBufferSize, - assetCount, *m_schemaVersion, m_modelChangeTime, m_validation); + assetCount, *m_schemaVersion, m_modelChangeTime, m_validation, requestId); } { obj.Key("Devices"); @@ -214,7 +219,7 @@ namespace mtconnect::printer { { AutoJsonObject obj(writer, "Header"); probeAssetHeader(obj, m_version, m_senderName, instanceId, 0, bufferSize, assetCount, - *m_schemaVersion, m_modelChangeTime, m_validation); + *m_schemaVersion, m_modelChangeTime, m_validation, requestId); } { obj.Key("Assets"); @@ -413,7 +418,7 @@ namespace mtconnect::printer { { AutoJsonObject obj(writer, "Header"); streamHeader(obj, m_version, m_senderName, instanceId, bufferSize, nextSeq, firstSeq, - lastSeq, *m_schemaVersion, m_modelChangeTime, m_validation); + lastSeq, *m_schemaVersion, m_modelChangeTime, m_validation, requestId); } { diff --git a/src/mtconnect/printer/json_printer.hpp b/src/mtconnect/printer/json_printer.hpp index 102cf51e..119f77b8 100644 --- a/src/mtconnect/printer/json_printer.hpp +++ b/src/mtconnect/printer/json_printer.hpp @@ -30,27 +30,27 @@ namespace mtconnect::printer { JsonPrinter(uint32_t jsonVersion, bool pretty = false, bool validation = false); ~JsonPrinter() override = default; - std::string printErrors(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const ProtoErrorList &list, - bool pretty = false, - const std::optional requestId = std::nullopt) const override; - - std::string printProbe(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const unsigned int assetBufferSize, - const unsigned int assetCount, const std::list &devices, - const std::map *count = nullptr, - bool includeHidden = false, bool pretty = false, - const std::optional requestId = std::nullopt) const override; - - std::string printSample(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const uint64_t firstSeq, const uint64_t lastSeq, - observation::ObservationList &results, - bool pretty = false, - const std::optional requestId = std::nullopt) const override; - std::string printAssets(const uint64_t anInstanceId, const unsigned int bufferSize, - const unsigned int assetCount, const asset::AssetList &asset, - bool pretty = false, - const std::optional requestId = std::nullopt) const override; + std::string printErrors( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const ProtoErrorList &list, bool pretty = false, + const std::optional requestId = std::nullopt) const override; + + std::string printProbe( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const unsigned int assetBufferSize, const unsigned int assetCount, + const std::list &devices, const std::map *count = nullptr, + bool includeHidden = false, bool pretty = false, + const std::optional requestId = std::nullopt) const override; + + std::string printSample( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const uint64_t firstSeq, const uint64_t lastSeq, observation::ObservationList &results, + bool pretty = false, + const std::optional requestId = std::nullopt) const override; + std::string printAssets( + const uint64_t anInstanceId, const unsigned int bufferSize, const unsigned int assetCount, + const asset::AssetList &asset, bool pretty = false, + const std::optional requestId = std::nullopt) const override; std::string mimeType() const override { return "application/mtconnect+json"; } uint32_t getJsonVersion() const { return m_jsonVersion; } diff --git a/src/mtconnect/printer/printer.hpp b/src/mtconnect/printer/printer.hpp index 38727109..d6526a98 100644 --- a/src/mtconnect/printer/printer.hpp +++ b/src/mtconnect/printer/printer.hpp @@ -61,10 +61,10 @@ namespace mtconnect { /// @param[in] errorCode an error code /// @param[in] errorText the error text /// @return the error document - virtual std::string printError(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const std::string &errorCode, - const std::string &errorText, bool pretty = false, - const std::optional requestId = std::nullopt) const + virtual std::string printError( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const std::string &errorCode, const std::string &errorText, bool pretty = false, + const std::optional requestId = std::nullopt) const { return printErrors(instanceId, bufferSize, nextSeq, {{errorCode, errorText}}); } @@ -74,10 +74,10 @@ namespace mtconnect { /// @param[in] nextSeq the next sequence /// @param[in] list the list of errors /// @return the MTConnect Error document - virtual std::string printErrors(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const ProtoErrorList &list, - bool pretty = false, - const std::optional requestId = std::nullopt) const = 0; + virtual std::string printErrors( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const ProtoErrorList &list, bool pretty = false, + const std::optional requestId = std::nullopt) const = 0; /// @brief Generate an MTConnect Devices document /// @param[in] instanceId the instance id /// @param[in] bufferSize the buffer size @@ -87,13 +87,12 @@ namespace mtconnect { /// @param[in] devices a list of devices /// @param[in] count optional asset count and type association /// @return the MTConnect Devices document - virtual std::string printProbe(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const unsigned int assetBufferSize, - const unsigned int assetCount, - const std::list &devices, - const std::map *count = nullptr, - bool includeHidden = false, bool pretty = false, - const std::optional requestId = std::nullopt) const = 0; + virtual std::string printProbe( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const unsigned int assetBufferSize, const unsigned int assetCount, + const std::list &devices, const std::map *count = nullptr, + bool includeHidden = false, bool pretty = false, + const std::optional requestId = std::nullopt) const = 0; /// @brief Print a MTConnect Streams document /// @param[in] instanceId the instance id /// @param[in] bufferSize the buffer size @@ -102,21 +101,20 @@ namespace mtconnect { /// @param[in] lastSeq the last sequnce /// @param[in] results a list of observations /// @return the MTConnect Streams document - virtual std::string printSample(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const uint64_t firstSeq, - const uint64_t lastSeq, observation::ObservationList &results, - bool pretty = false, - const std::optional requestId = std::nullopt) const = 0; + virtual std::string printSample( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const uint64_t firstSeq, const uint64_t lastSeq, observation::ObservationList &results, + bool pretty = false, const std::optional requestId = std::nullopt) const = 0; /// @brief Generate an MTConnect Assets document /// @param[in] anInstanceId the instance id /// @param[in] bufferSize the buffer size /// @param[in] assetCount the asset count /// @param[in] asset the list of assets /// @return the MTConnect Assets document - virtual std::string printAssets(const uint64_t anInstanceId, const unsigned int bufferSize, - const unsigned int assetCount, asset::AssetList const &asset, - bool pretty = false, - const std::optional requestId = std::nullopt) const = 0; + virtual std::string printAssets( + const uint64_t anInstanceId, const unsigned int bufferSize, const unsigned int assetCount, + asset::AssetList const &asset, bool pretty = false, + const std::optional requestId = std::nullopt) const = 0; /// @brief get the mime type for the documents /// @return the mime type virtual std::string mimeType() const = 0; diff --git a/src/mtconnect/printer/xml_printer.cpp b/src/mtconnect/printer/xml_printer.cpp index 2e5badb3..8a6db2f1 100644 --- a/src/mtconnect/printer/xml_printer.cpp +++ b/src/mtconnect/printer/xml_printer.cpp @@ -352,8 +352,8 @@ namespace mtconnect::printer { { XmlWriter writer(m_pretty || pretty); - initXmlDoc(writer, eERROR, instanceId, bufferSize, 0, 0, nextSeq, 0, nextSeq - 1, - nullptr, requestId); + initXmlDoc(writer, eERROR, instanceId, bufferSize, 0, 0, nextSeq, 0, nextSeq - 1, nullptr, + requestId); { AutoElement e1(writer, "Errors"); @@ -419,8 +419,8 @@ namespace mtconnect::printer { string XmlPrinter::printSample(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const uint64_t firstSeq, - const uint64_t lastSeq, ObservationList &observations, - bool pretty, const std::optional requestId) const + const uint64_t lastSeq, ObservationList &observations, bool pretty, + const std::optional requestId) const { string ret; @@ -500,16 +500,15 @@ namespace mtconnect::printer { } string XmlPrinter::printAssets(const uint64_t instanceId, const unsigned int bufferSize, - const unsigned int assetCount, const AssetList &asset, - bool pretty, + const unsigned int assetCount, const AssetList &asset, bool pretty, const std::optional requestId) const { string ret; try { XmlWriter writer(m_pretty || pretty); - initXmlDoc(writer, eASSETS, instanceId, 0u, bufferSize, assetCount, 0ull, - 0, 0, nullptr, requestId); + initXmlDoc(writer, eASSETS, instanceId, 0u, bufferSize, assetCount, 0ull, 0, 0, nullptr, + requestId); { AutoElement ele(writer, "Assets"); @@ -652,11 +651,10 @@ namespace mtconnect::printer { sprintf(version, "%d.%d.%d.%d", AGENT_VERSION_MAJOR, AGENT_VERSION_MINOR, AGENT_VERSION_PATCH, AGENT_VERSION_BUILD); addAttribute(writer, "version", version); - + if (requestId) addAttribute(writer, "requestId", *requestId); - int major, minor; char c; stringstream v(*m_schemaVersion); diff --git a/src/mtconnect/printer/xml_printer.hpp b/src/mtconnect/printer/xml_printer.hpp index 337f1fae..d5eba57d 100644 --- a/src/mtconnect/printer/xml_printer.hpp +++ b/src/mtconnect/printer/xml_printer.hpp @@ -43,25 +43,27 @@ namespace mtconnect { XmlPrinter(bool pretty = false, bool validation = false); ~XmlPrinter() override = default; - std::string printErrors(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const ProtoErrorList &list, - bool pretty = false, - const std::optional requestId = std::nullopt) const override; - - std::string printProbe(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const unsigned int assetBufferSize, - const unsigned int assetCount, const std::list &devices, - const std::map *count = nullptr, - bool includeHidden = false, bool pretty = false, - const std::optional requestId = std::nullopt) const override; - - std::string printSample(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const uint64_t firstSeq, - const uint64_t lastSeq, observation::ObservationList &results, - bool pretty = false, const std::optional requestId = std::nullopt) const override; - std::string printAssets(const uint64_t anInstanceId, const unsigned int bufferSize, - const unsigned int assetCount, const asset::AssetList &asset, - bool pretty = false, const std::optional requestId = std::nullopt) const override; + std::string printErrors( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const ProtoErrorList &list, bool pretty = false, + const std::optional requestId = std::nullopt) const override; + + std::string printProbe( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const unsigned int assetBufferSize, const unsigned int assetCount, + const std::list &devices, const std::map *count = nullptr, + bool includeHidden = false, bool pretty = false, + const std::optional requestId = std::nullopt) const override; + + std::string printSample( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const uint64_t firstSeq, const uint64_t lastSeq, observation::ObservationList &results, + bool pretty = false, + const std::optional requestId = std::nullopt) const override; + std::string printAssets( + const uint64_t anInstanceId, const unsigned int bufferSize, const unsigned int assetCount, + const asset::AssetList &asset, bool pretty = false, + const std::optional requestId = std::nullopt) const override; std::string mimeType() const override { return "text/xml"; } /// @brief Add a Devices XML device namespace diff --git a/src/mtconnect/sink/rest_sink/rest_service.cpp b/src/mtconnect/sink/rest_sink/rest_service.cpp index fb9951d5..b9e15023 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.cpp +++ b/src/mtconnect/sink/rest_sink/rest_service.cpp @@ -113,6 +113,7 @@ namespace mtconnect { {"from", QUERY, "Sequence number at to start reporting observations"}, {"interval", QUERY, "Time in ms between publishing data–starts streaming"}, {"pretty", QUERY, "Instructs the result to be pretty printed"}, + {"format", QUERY, "The format of the response document: 'xml' or 'json'"}, {"heartbeat", QUERY, "Time in ms between publishing a empty document when no data has changed"}}); @@ -475,8 +476,8 @@ namespace mtconnect { auto device = request->parameter("device"); auto pretty = *request->parameter("pretty"); auto deviceType = request->parameter("deviceType"); - - auto printer = printerForAccepts(request->m_accepts); + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); if (device && !ends_with(request->m_path, string("probe")) && m_sinkContract->findDeviceByUUIDorName(*device) == nullptr) @@ -487,19 +488,21 @@ namespace mtconnect { return false; } - respond(session, probeRequest(printer, device, pretty, deviceType), request->m_requestId); + respond(session, probeRequest(printer, device, pretty, deviceType, request->m_requestId), + request->m_requestId); return true; }; m_server ->addRouting({boost::beast::http::verb::get, - "/probe?pretty={bool:false}&deviceType={string}", handler}) + "/probe?pretty={bool:false}&deviceType={string}&format={string}", handler}) .document("MTConnect probe request", "Provides metadata service for the MTConnect Devices information model for all " "devices."); m_server ->addRouting({boost::beast::http::verb::get, - "/{device}/probe?pretty={bool:false}&deviceType={string}", handler}) + "/{device}/probe?pretty={bool:false}&deviceType={string}&format={string}", + handler}) .document("MTConnect probe request", "Provides metadata service for the MTConnect Devices information model for " "device identified by `device` matching `name` or `uuid`.") @@ -507,14 +510,15 @@ namespace mtconnect { // Must be last m_server - ->addRouting( - {boost::beast::http::verb::get, "/?pretty={bool:false}&deviceType={string}", handler}) + ->addRouting({boost::beast::http::verb::get, + "/?pretty={bool:false}&deviceType={string}&format={string}", handler}) .document("MTConnect probe request", "Provides metadata service for the MTConnect Devices information model for all " "devices."); m_server ->addRouting({boost::beast::http::verb::get, - "/{device}?pretty={bool:false}&deviceType={string}", handler}) + "/{device}?pretty={bool:false}&deviceType={string}&format={string}", + handler}) .document("MTConnect probe request", "Provides metadata service for the MTConnect Devices information model for " "device identified by `device` matching `name` or `uuid`."); @@ -526,11 +530,13 @@ namespace mtconnect { auto handler = [&](SessionPtr session, RequestPtr request) -> bool { auto removed = *request->parameter("removed"); auto count = *request->parameter("count"); - auto printer = printerForAccepts(request->m_accepts); + auto pretty = *request->parameter("pretty"); + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); respond(session, assetRequest(printer, count, removed, request->parameter("type"), - request->parameter("device")), + request->parameter("device"), pretty, request->m_requestId), request->m_requestId); return true; }; @@ -539,6 +545,7 @@ namespace mtconnect { auto asset = request->parameter("assetIds"); if (asset) { + auto pretty = *request->parameter("pretty"); auto printer = m_sinkContract->getPrinter(acceptFormat(request->m_accepts)); list ids; @@ -546,11 +553,13 @@ namespace mtconnect { string id; while (getline(str, id, ';')) ids.emplace_back(id); - respond(session, assetIdsRequest(printer, ids), request->m_requestId); + respond(session, assetIdsRequest(printer, ids, pretty, request->m_requestId), + request->m_requestId); } else { - auto printer = printerForAccepts(request->m_accepts); + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); auto error = printError(printer, "INVALID_REQUEST", "No asset given"); respond(session, make_unique(rest_sink::status::bad_request, error, printer->mimeType()), @@ -561,7 +570,7 @@ namespace mtconnect { string qp( "type={string}&removed={bool:false}&" - "count={integer:100}&device={string}&pretty={bool:false}"); + "count={integer:100}&device={string}&pretty={bool:false}&format={string}"); m_server->addRouting({boost::beast::http::verb::get, "/assets?" + qp, handler}) .document("MTConnect assets request", "Returns up to `count` assets"); m_server->addRouting({boost::beast::http::verb::get, "/asset?" + qp, handler}) @@ -582,7 +591,8 @@ namespace mtconnect { if (m_server->arePutsAllowed()) { auto putHandler = [&](SessionPtr session, RequestPtr request) -> bool { - auto printer = printerForAccepts(request->m_accepts); + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); respond(session, putAssetRequest(printer, request->m_body, request->parameter("type"), request->parameter("device"), @@ -598,7 +608,8 @@ namespace mtconnect { list ids; stringstream str(*asset); string id; - auto printer = printerForAccepts(request->m_accepts); + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); while (getline(str, id, ';')) ids.emplace_back(id); @@ -606,9 +617,11 @@ namespace mtconnect { } else { + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); + respond(session, - deleteAllAssetsRequest(printerForAccepts(request->m_accepts), - request->parameter("device"), + deleteAllAssetsRequest(printer, request->parameter("device"), request->parameter("type")), request->m_requestId); } @@ -622,34 +635,42 @@ namespace mtconnect { { m_server ->addRouting( - {t, "/" + asset + "/{assetId}?device={string}&type={string}", putHandler}) + {t, "/" + asset + "/{assetId}?device={string}&type={string}&format={string}", + putHandler}) .document("Upload an asset by identified by `assetId`", "Updates or adds an asset with the asset XML in the body"); - m_server->addRouting({t, "/" + asset + "?device={string}&type={string}", putHandler}) + m_server + ->addRouting( + {t, "/" + asset + "?device={string}&type={string}&format={string}", putHandler}) .document("Upload an asset by identified by `assetId`", "Updates or adds an asset with the asset XML in the body"); - m_server->addRouting({t, "/{device}/" + asset + "/{assetId}?type={string}", putHandler}) + m_server + ->addRouting({t, "/{device}/" + asset + "/{assetId}?type={string}&format={string}", + putHandler}) .document("Upload an asset by identified by `assetId`", "Updates or adds an asset with the asset XML in the body"); - m_server->addRouting({t, "/{device}/" + asset + "?type={string}", putHandler}) + m_server + ->addRouting( + {t, "/{device}/" + asset + "?type={string}&format={string}", putHandler}) .document("Upload an asset by identified by `assetId`", "Updates or adds an asset with the asset XML in the body"); } m_server ->addRouting({boost::beast::http::verb::delete_, - "/" + asset + "?device={string}&type={string}", deleteHandler}) + "/" + asset + "?device={string}&type={string}&format={string}", + deleteHandler}) .document("Delete all assets for a device and type", "Device and type are optional. If they are not given, it assumes there is " "no constraint"); m_server - ->addRouting( - {boost::beast::http::verb::delete_, "/" + asset + "/{assetId}", deleteHandler}) + ->addRouting({boost::beast::http::verb::delete_, + "/" + asset + "/{assetId}?format={string}", deleteHandler}) .document("Delete asset identified by `assetId`", "Marks the asset as removed and creates an AssetRemoved event"); m_server ->addRouting({boost::beast::http::verb::delete_, - "/{device}/" + asset + "?type={string}", deleteHandler}) + "/{device}/" + asset + "?type={string}&format={string}", deleteHandler}) .document("Delete all assets for a device and type", "Device and type are optional. If they are not given, it assumes there is " "no constraint") @@ -666,20 +687,25 @@ namespace mtconnect { auto interval = request->parameter("interval"); if (interval) { - streamCurrentRequest(session, printerForAccepts(request->m_accepts), *interval, - request->parameter("device"), + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); + + streamCurrentRequest(session, printer, *interval, request->parameter("device"), request->parameter("path"), *request->parameter("pretty"), request->parameter("deviceType"), request->m_requestId); } else { + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); + respond( session, - currentRequest( - printerForAccepts(request->m_accepts), request->parameter("device"), - request->parameter("at"), request->parameter("path"), - *request->parameter("pretty"), request->parameter("deviceType")), + currentRequest(printer, request->parameter("device"), + request->parameter("at"), request->parameter("path"), + *request->parameter("pretty"), + request->parameter("deviceType"), request->m_requestId), request->m_requestId); } return true; @@ -688,7 +714,7 @@ namespace mtconnect { string qp( "path={string}&at={unsigned_integer}&" "interval={integer}&pretty={bool:false}&" - "deviceType={string}"); + "deviceType={string}&format={string}"); m_server->addRouting({boost::beast::http::verb::get, "/current?" + qp, handler}) .document("MTConnect current request", "Gets a stapshot of the state of all the observations for all devices " @@ -707,23 +733,29 @@ namespace mtconnect { auto interval = request->parameter("interval"); if (interval) { + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); + streamSampleRequest( - session, printerForAccepts(request->m_accepts), *interval, - *request->parameter("heartbeat"), *request->parameter("count"), - request->parameter("device"), request->parameter("from"), - request->parameter("path"), *request->parameter("pretty"), - request->parameter("deviceType"), request->m_requestId); + session, printer, *interval, *request->parameter("heartbeat"), + *request->parameter("count"), request->parameter("device"), + request->parameter("from"), request->parameter("path"), + *request->parameter("pretty"), request->parameter("deviceType"), + request->m_requestId); } else { - respond( - session, - sampleRequest( - printerForAccepts(request->m_accepts), *request->parameter("count"), - request->parameter("device"), request->parameter("from"), - request->parameter("to"), request->parameter("path"), - *request->parameter("pretty"), request->parameter("deviceType")), - request->m_requestId); + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); + + respond(session, + sampleRequest( + printer, *request->parameter("count"), + request->parameter("device"), request->parameter("from"), + request->parameter("to"), request->parameter("path"), + *request->parameter("pretty"), request->parameter("deviceType"), + request->m_requestId), + request->m_requestId); } return true; }; @@ -733,7 +765,7 @@ namespace mtconnect { "interval={integer}&count={integer:100}&" "heartbeat={integer:10000}&to={unsigned_integer}&" "pretty={bool:false}&" - "deviceType={string}"); + "deviceType={string}&format={string}"); m_server->addRouting({boost::beast::http::verb::get, "/sample?" + qp, handler}) .document("MTConnect sample request", "Gets a time series of at maximum `count` observations for all devices " @@ -761,11 +793,11 @@ namespace mtconnect { if (ts) queries.erase("time"); auto device = request->parameter("device"); + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); - respond( - session, - putObservationRequest(printerForAccepts(request->m_accepts), *device, queries, ts), - request->m_requestId); + respond(session, putObservationRequest(printer, *device, queries, ts), + request->m_requestId); return true; } else @@ -825,7 +857,7 @@ namespace mtconnect { m_sinkContract->getCircularBuffer().getSequence(), uint32_t(m_sinkContract->getAssetStorage()->getMaxAssets()), uint32_t(m_sinkContract->getAssetStorage()->getCount()), deviceList, - &counts, false, pretty), + &counts, false, pretty, requestId), printer->mimeType()); } @@ -851,7 +883,7 @@ namespace mtconnect { // Check if there is a frequency to stream data or not return make_unique(rest_sink::status::ok, - fetchCurrentData(printer, filter, at, pretty), + fetchCurrentData(printer, filter, at, pretty, requestId), printer->mimeType()); } @@ -882,7 +914,7 @@ namespace mtconnect { return make_unique( rest_sink::status::ok, - fetchSampleData(printer, filter, count, from, to, end, endOfBuffer, pretty), + fetchSampleData(printer, filter, count, from, to, end, endOfBuffer, pretty, requestId), printer->mimeType()); } @@ -1210,9 +1242,9 @@ namespace mtconnect { str << id << ", "; auto message = str.str().substr(0, str.str().size() - 2); - return make_unique(status::not_found, - printError(printer, "ASSET_NOT_FOUND", message, pretty, requestId), - printer->mimeType()); + return make_unique( + status::not_found, printError(printer, "ASSET_NOT_FOUND", message, pretty, requestId), + printer->mimeType()); } else { @@ -1400,11 +1432,6 @@ namespace mtconnect { return "xml"; } - const Printer *RestService::printerForAccepts(const std::string &accepts) const - { - return m_sinkContract->getPrinter(acceptFormat(accepts)); - } - string RestService::printError(const Printer *printer, const string &errorCode, const string &text, bool pretty, const std::optional &requestId) const @@ -1413,8 +1440,7 @@ namespace mtconnect { if (printer) return printer->printError( m_instanceId, m_sinkContract->getCircularBuffer().getBufferSize(), - m_sinkContract->getCircularBuffer().getSequence(), errorCode, text, pretty, - requestId); + m_sinkContract->getCircularBuffer().getSequence(), errorCode, text, pretty, requestId); else return errorCode + ": " + text; } @@ -1515,8 +1541,7 @@ namespace mtconnect { } return printer->printSample(m_instanceId, m_sinkContract->getCircularBuffer().getBufferSize(), - seq, firstSeq, seq - 1, observations, pretty, - requestId); + seq, firstSeq, seq - 1, observations, pretty, requestId); } string RestService::fetchSampleData(const Printer *printer, const FilterSetOpt &filterSet, @@ -1553,8 +1578,7 @@ namespace mtconnect { } return printer->printSample(m_instanceId, m_sinkContract->getCircularBuffer().getBufferSize(), - end, firstSeq, lastSeq, *observations, pretty, - requestId); + end, firstSeq, lastSeq, *observations, pretty, requestId); } } // namespace sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/rest_service.hpp b/src/mtconnect/sink/rest_sink/rest_service.hpp index 5957b401..f58c7df2 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.hpp +++ b/src/mtconnect/sink/rest_sink/rest_service.hpp @@ -259,7 +259,26 @@ namespace mtconnect { /// @brief get a printer given a list of formats from the Accepts header /// @param accepts the accepts header /// @return pointer to a printer - const printer::Printer *printerForAccepts(const std::string &accepts) const; + const printer::Printer *printerForAccepts(const std::string &accepts) const + { + return m_sinkContract->getPrinter(acceptFormat(accepts)); + } + + /// @brief get a printer for a format or using the accepts header. Falls back to header accept + /// if format incorrect. + /// @param accepts the accept header of the request + /// @param format optional format query param + /// @return pointer to a printer + const printer::Printer *getPrinter(const std::string &accepts, + std::optional format) const + { + const printer::Printer *printer = nullptr; + if (format) + printer = m_sinkContract->getPrinter(*format); + if (printer == nullptr) + printer = printerForAccepts(accepts); + return printer; + } /// @brief Generate an MTConnect Error document /// @param printer printer to generate error diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp index fcf91939..e8677ebd 100644 --- a/src/mtconnect/sink/rest_sink/websocket_session.hpp +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -421,14 +421,13 @@ namespace mtconnect::sink::rest_sink { m_request.reset(); closeStream(); } - + void closeStream() override { if (m_stream.is_open()) m_stream.close(beast::websocket::close_code::none); } - auto getExecutor() { return m_stream.get_executor(); } auto &stream() { return m_stream; } From 64fa6a2a16171b58fbd91d78a431d8d02e010bbb Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Wed, 24 Apr 2024 17:03:48 -0400 Subject: [PATCH 18/35] Fixed session leak with observer circular ref in handler. --- src/mtconnect/observation/change_observer.hpp | 15 ++++ .../sink/mqtt_sink/mqtt2_service.cpp | 5 ++ src/mtconnect/sink/rest_sink/rest_service.cpp | 9 +++ src/mtconnect/sink/rest_sink/session.hpp | 8 ++ .../sink/rest_sink/websocket_session.hpp | 75 +++++++++++++------ 5 files changed, 91 insertions(+), 21 deletions(-) diff --git a/src/mtconnect/observation/change_observer.hpp b/src/mtconnect/observation/change_observer.hpp index 3f4e5c2e..43943915 100644 --- a/src/mtconnect/observation/change_observer.hpp +++ b/src/mtconnect/observation/change_observer.hpp @@ -142,6 +142,14 @@ namespace mtconnect::observation { /// @brief try to lock the mutex auto try_lock() { return m_mutex.try_lock(); } ///@} + + /// @brief clear the observer information. + void clear() + { + std::unique_lock lock(m_mutex); + m_signalers.clear(); + m_handler.clear(); + } private: boost::asio::io_context::strand &m_strand; @@ -237,6 +245,13 @@ namespace mtconnect::observation { /// @brief method to determine if the sink is running virtual bool isRunning() = 0; + + /// @brief Stop all timers and release resources. + virtual bool cancel() + { + m_observer.clear(); + return true; + } /// @brief handler callback when an action needs to be taken /// diff --git a/src/mtconnect/sink/mqtt_sink/mqtt2_service.cpp b/src/mtconnect/sink/mqtt_sink/mqtt2_service.cpp index 0a859fa8..fa977071 100644 --- a/src/mtconnect/sink/mqtt_sink/mqtt2_service.cpp +++ b/src/mtconnect/sink/mqtt_sink/mqtt2_service.cpp @@ -163,6 +163,11 @@ namespace mtconnect { auto client = m_client.lock(); return client && client->isRunning() && client->isConnected(); } + + bool cancel() override + { + return true; + } DevicePtr m_device; std::weak_ptr m_client; diff --git a/src/mtconnect/sink/rest_sink/rest_service.cpp b/src/mtconnect/sink/rest_sink/rest_service.cpp index b9e15023..389e34ce 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.cpp +++ b/src/mtconnect/sink/rest_sink/rest_service.cpp @@ -934,6 +934,7 @@ namespace mtconnect { if (sink && isRunning()) { m_session->fail(status, message); + cancel(); } else { @@ -959,6 +960,13 @@ namespace mtconnect { return server->isRunning(); } } + + bool cancel() override + { + observation::AsyncObserver::cancel(); + m_session.reset(); + return true; + } std::weak_ptr m_sink; //! weak shared pointer to the sink. handles shutdown timer race @@ -1012,6 +1020,7 @@ namespace mtconnect { asyncResponse->m_sink = getptr(); asyncResponse->m_pretty = pretty; asyncResponse->m_requestId = requestId; + session->addObserver(asyncResponse); if (m_logStreamData) { diff --git a/src/mtconnect/sink/rest_sink/session.hpp b/src/mtconnect/sink/rest_sink/session.hpp index 4e2bc52a..8d3f7641 100644 --- a/src/mtconnect/sink/rest_sink/session.hpp +++ b/src/mtconnect/sink/rest_sink/session.hpp @@ -25,6 +25,7 @@ #include #include "mtconnect/config.hpp" +#include "mtconnect/observation/change_observer.hpp" #include "routing.hpp" namespace mtconnect::sink::rest_sink { @@ -117,6 +118,12 @@ namespace mtconnect::sink::rest_sink { m_message = msg; m_unauthorized = true; } + + /// @brief Add an observer to the list for cleanup later. + void addObserver(std::weak_ptr observer) + { + m_observers.push_back(observer); + } protected: Dispatch m_dispatch; @@ -127,6 +134,7 @@ namespace mtconnect::sink::rest_sink { bool m_allowPuts {false}; std::set m_allowPutsFrom; boost::asio::ip::tcp::endpoint m_remote; + std::list> m_observers; }; } // namespace mtconnect::sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp index e8677ebd..e38b0c4d 100644 --- a/src/mtconnect/sink/rest_sink/websocket_session.hpp +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -96,6 +96,29 @@ namespace mtconnect::sink::rest_sink { beast::bind_front_handler(&WebsocketSession::onAccept, derived().shared_ptr()))); } + + void close() override + { + NAMED_SCOPE("PlainWebsocketSession::close"); + if (!m_isOpen) + return; + + auto ptr = derived().shared_ptr(); + + m_request.reset(); + m_requests.clear(); + for (auto obs : m_observers) + { + auto optr = obs.lock(); + if (optr) + { + optr->cancel(); + } + } + closeStream(); + + m_isOpen = false; + } void writeResponse(ResponsePtr &&response, Complete complete = nullptr) override { @@ -128,7 +151,9 @@ namespace mtconnect::sink::rest_sink { req.m_streaming = true; if (complete) + { complete(); + } } else { @@ -145,6 +170,12 @@ namespace mtconnect::sink::rest_sink { std::optional requestId = std::nullopt) override { NAMED_SCOPE("WebsocketSession::writeChunk"); + + if (!derived().stream().is_open()) + { + return; + } + if (requestId) { LOG(trace) << "Waiting for mutex"; @@ -173,6 +204,8 @@ namespace mtconnect::sink::rest_sink { fail(status::internal_server_error, "Error occurred in accpet", ec); return; } + + m_isOpen = true; derived().stream().async_read( m_buffer, beast::bind_front_handler(&WebsocketSession::onRead, derived().shared_ptr())); @@ -188,7 +221,7 @@ namespace mtconnect::sink::rest_sink { if (it != m_requests.end()) { auto &req = it->second; - req.m_complete = complete; + req.m_complete = std::move(complete); req.m_streamBuffer.emplace(); std::ostream str(&req.m_streamBuffer.value()); @@ -236,6 +269,7 @@ namespace mtconnect::sink::rest_sink { if (req.m_complete) { boost::asio::post(derived().stream().get_executor(), req.m_complete); + req.m_complete = nullptr; } if (!req.m_streaming) @@ -277,6 +311,12 @@ namespace mtconnect::sink::rest_sink { using namespace rapidjson; using namespace std; + + if (len == 0) + { + LOG(trace) << "Empty message received"; + return; + } // Parse the buffer as a JSON request with parameters matching // REST API @@ -395,6 +435,7 @@ namespace mtconnect::sink::rest_sink { std::mutex m_mutex; std::atomic_bool m_busy; std::deque m_messageQueue; + bool m_isOpen { false }; }; template @@ -412,19 +453,15 @@ namespace mtconnect::sink::rest_sink { { beast::get_lowest_layer(m_stream).expires_never(); } - ~PlainWebsocketSession() { close(); } - - void close() override - { - NAMED_SCOPE("PlainWebsocketSession::close"); - - m_request.reset(); - closeStream(); + ~PlainWebsocketSession() + { + if (m_isOpen) + close(); } void closeStream() override { - if (m_stream.is_open()) + if (m_isOpen && m_stream.is_open()) m_stream.close(beast::websocket::close_code::none); } @@ -455,23 +492,19 @@ namespace mtconnect::sink::rest_sink { { beast::get_lowest_layer(m_stream).expires_never(); } - ~TlsWebsocketSession() { close(); } - + ~TlsWebsocketSession() + { + if (m_isOpen) + close(); + } + auto &stream() { return m_stream; } auto getExecutor() { return m_stream.get_executor(); } - void close() override - { - NAMED_SCOPE("TlsWebsocketSession::close"); - - m_request.reset(); - closeStream(); - } - void closeStream() override { - if (m_stream.is_open()) + if (m_isOpen && m_stream.is_open()) m_stream.close(beast::websocket::close_code::none); } From 3bd86f491b8112b2839c78dd7aa7b3f7c9f0fae8 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Wed, 24 Apr 2024 17:07:11 -0400 Subject: [PATCH 19/35] Added code to session impl as well. --- src/mtconnect/sink/rest_sink/session_impl.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/mtconnect/sink/rest_sink/session_impl.cpp b/src/mtconnect/sink/rest_sink/session_impl.cpp index 40777da0..f1eeef2e 100644 --- a/src/mtconnect/sink/rest_sink/session_impl.cpp +++ b/src/mtconnect/sink/rest_sink/session_impl.cpp @@ -527,6 +527,18 @@ namespace mtconnect::sink::rest_sink { if (!m_closing) { m_closing = true; + + // Release all references from observers. + for (auto obs : m_observers) + { + auto optr = obs.lock(); + if (optr) + { + optr->cancel(); + } + } + + // Set the timeout. beast::get_lowest_layer(m_stream).expires_after(std::chrono::seconds(30)); From 6201b2c63e70545832b87b4d5fb7992388054318 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Wed, 24 Apr 2024 17:11:05 -0400 Subject: [PATCH 20/35] Fixed test issues --- test_package/agent_test_helper.hpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test_package/agent_test_helper.hpp b/test_package/agent_test_helper.hpp index 13c14fc9..a0f93a0b 100644 --- a/test_package/agent_test_helper.hpp +++ b/test_package/agent_test_helper.hpp @@ -78,13 +78,15 @@ namespace mtconnect { writeResponse(std::move(response), complete); } } - void beginStreaming(const std::string &mimeType, Complete complete) override + void beginStreaming(const std::string &mimeType, Complete complete, + std::optional requestId = std::nullopt) override { m_mimeType = mimeType; m_streaming = true; complete(); } - void writeChunk(const std::string &chunk, Complete complete) override + void writeChunk(const std::string &chunk, Complete complete, + std::optional requestId = std::nullopt) override { m_chunkBody = chunk; if (m_streaming) From bbad3dc2363f14fb27f5112fb7f4713777c646ff Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Wed, 24 Apr 2024 17:33:08 -0400 Subject: [PATCH 21/35] fixed bug in dispatch --- src/mtconnect/sink/rest_sink/routing.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/mtconnect/sink/rest_sink/routing.hpp b/src/mtconnect/sink/rest_sink/routing.hpp index 23053422..0710f15b 100644 --- a/src/mtconnect/sink/rest_sink/routing.hpp +++ b/src/mtconnect/sink/rest_sink/routing.hpp @@ -187,6 +187,10 @@ namespace mtconnect::sink::rest_sink { } } } + else + { + return false; + } } for (auto &p : m_queryParameters) From a89c4c78df0e8efa644917c89ec5d72530bf36a4 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Wed, 24 Apr 2024 21:31:27 -0400 Subject: [PATCH 22/35] refactored mqtt sinks and tests. Added cancel to websockets --- agent_lib/CMakeLists.txt | 8 +- src/mtconnect/configuration/agent_config.cpp | 4 +- src/mtconnect/observation/change_observer.hpp | 16 +- .../sink/mqtt_sink/mqtt2_service.cpp | 357 ---------- .../sink/mqtt_sink/mqtt2_service.hpp | 205 ------ .../sink/mqtt_sink/mqtt_legacy_service.cpp | 197 ++++++ .../sink/mqtt_sink/mqtt_legacy_service.hpp | 107 +++ src/mtconnect/sink/mqtt_sink/mqtt_service.cpp | 297 +++++++-- src/mtconnect/sink/mqtt_sink/mqtt_service.hpp | 108 ++- src/mtconnect/sink/rest_sink/rest_service.cpp | 22 +- src/mtconnect/sink/rest_sink/session.hpp | 16 +- src/mtconnect/sink/rest_sink/session_impl.cpp | 3 +- .../sink/rest_sink/websocket_session.hpp | 26 +- test_package/CMakeLists.txt | 2 +- test_package/agent_test_helper.hpp | 40 +- test_package/mqtt_isolated_test.cpp | 2 +- test_package/mqtt_legacy_sink_test.cpp | 630 ++++++++++++++++++ test_package/mqtt_sink_2_test.cpp | 426 ------------ test_package/mqtt_sink_test.cpp | 408 +++--------- 19 files changed, 1451 insertions(+), 1423 deletions(-) delete mode 100644 src/mtconnect/sink/mqtt_sink/mqtt2_service.cpp delete mode 100644 src/mtconnect/sink/mqtt_sink/mqtt2_service.hpp create mode 100644 src/mtconnect/sink/mqtt_sink/mqtt_legacy_service.cpp create mode 100644 src/mtconnect/sink/mqtt_sink/mqtt_legacy_service.hpp create mode 100644 test_package/mqtt_legacy_sink_test.cpp delete mode 100644 test_package/mqtt_sink_2_test.cpp diff --git a/agent_lib/CMakeLists.txt b/agent_lib/CMakeLists.txt index 26afac8c..c3928467 100644 --- a/agent_lib/CMakeLists.txt +++ b/agent_lib/CMakeLists.txt @@ -246,13 +246,13 @@ set(AGENT_SOURCES # src/sink/mqtt_sink HEADER_FILE_ONLY - "${SOURCE_DIR}/sink/mqtt_sink/mqtt_service.hpp" - "${SOURCE_DIR}/sink/mqtt_sink/mqtt2_service.hpp" + "${SOURCE_DIR}/sink/mqtt_sink/mqtt_legacy_service.hpp" + "${SOURCE_DIR}/sink/mqtt_sink/mqtt_service.hpp" #src/sink/mqtt_sink SOURCE_FILES_ONLY - "${SOURCE_DIR}/sink/mqtt_sink/mqtt_service.cpp" - "${SOURCE_DIR}/sink/mqtt_sink/mqtt2_service.cpp" + "${SOURCE_DIR}/sink/mqtt_sink/mqtt_legacy_service.cpp" + "${SOURCE_DIR}/sink/mqtt_sink/mqtt_service.cpp" # src/sink/rest_sink HEADER_FILE_ONLY diff --git a/src/mtconnect/configuration/agent_config.cpp b/src/mtconnect/configuration/agent_config.cpp index bd134930..d372df52 100644 --- a/src/mtconnect/configuration/agent_config.cpp +++ b/src/mtconnect/configuration/agent_config.cpp @@ -58,7 +58,7 @@ #include "mtconnect/configuration/config_options.hpp" #include "mtconnect/device_model/device.hpp" #include "mtconnect/printer/xml_printer.hpp" -#include "mtconnect/sink/mqtt_sink/mqtt2_service.hpp" +#include "mtconnect/sink/mqtt_sink/mqtt_legacy_service.hpp" #include "mtconnect/sink/mqtt_sink/mqtt_service.hpp" #include "mtconnect/sink/rest_sink/rest_service.hpp" #include "mtconnect/source/adapter/agent_adapter/agent_adapter.hpp" @@ -112,8 +112,8 @@ namespace mtconnect::configuration { bool success = false; + sink::mqtt_sink::MqttLegacyService::registerFactory(m_sinkFactory); sink::mqtt_sink::MqttService::registerFactory(m_sinkFactory); - sink::mqtt_sink::Mqtt2Service::registerFactory(m_sinkFactory); sink::rest_sink::RestService::registerFactory(m_sinkFactory); adapter::shdr::ShdrAdapter::registerFactory(m_sourceFactory); adapter::mqtt_adapter::MqttAdapter::registerFactory(m_sourceFactory); diff --git a/src/mtconnect/observation/change_observer.hpp b/src/mtconnect/observation/change_observer.hpp index 43943915..b38cc616 100644 --- a/src/mtconnect/observation/change_observer.hpp +++ b/src/mtconnect/observation/change_observer.hpp @@ -142,13 +142,14 @@ namespace mtconnect::observation { /// @brief try to lock the mutex auto try_lock() { return m_mutex.try_lock(); } ///@} - + /// @brief clear the observer information. void clear() { std::unique_lock lock(m_mutex); - m_signalers.clear(); + m_signalers.clear(); m_handler.clear(); + m_timer.cancel(); } private: @@ -245,7 +246,7 @@ namespace mtconnect::observation { /// @brief method to determine if the sink is running virtual bool isRunning() = 0; - + /// @brief Stop all timers and release resources. virtual bool cancel() { @@ -265,9 +266,15 @@ namespace mtconnect::observation { auto getSequence() const { return m_sequence; } auto isEndOfBuffer() const { return m_endOfBuffer; } const auto &getFilter() const { return m_filter; } + const auto &getRequestId() const { return m_requestId; } + ///@} + ///@{ + /// @name setters + + /// @brief sets the optonal request id for webservices. + void setRequestId(const std::optional &id) { m_requestId = id; } ///@} - /// mutable bool m_endOfBuffer {false}; //! Public indicator that we are at the end of the buffer @@ -291,5 +298,6 @@ namespace mtconnect::observation { ChangeObserver m_observer; //! the change observer mtconnect::buffer::CircularBuffer &m_buffer; //! reference to the circular buffer + std::optional m_requestId; //! request id }; } // namespace mtconnect::observation diff --git a/src/mtconnect/sink/mqtt_sink/mqtt2_service.cpp b/src/mtconnect/sink/mqtt_sink/mqtt2_service.cpp deleted file mode 100644 index fa977071..00000000 --- a/src/mtconnect/sink/mqtt_sink/mqtt2_service.cpp +++ /dev/null @@ -1,357 +0,0 @@ -// -// Copyright Copyright 2009-2023, AMT – The Association For Manufacturing Technology (“AMT”) -// All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#include "mqtt2_service.hpp" - -#include - -#include "mtconnect/configuration/config_options.hpp" -#include "mtconnect/entity/entity.hpp" -#include "mtconnect/entity/factory.hpp" -#include "mtconnect/entity/json_parser.hpp" -#include "mtconnect/mqtt/mqtt_client_impl.hpp" -#include "mtconnect/printer/json_printer.hpp" - -using ptree = boost::property_tree::ptree; -using json = nlohmann::json; - -using namespace std; -using namespace mtconnect; -using namespace mtconnect::asset; - -namespace asio = boost::asio; -namespace config = ::mtconnect::configuration; - -namespace mtconnect { - namespace sink { - namespace mqtt_sink { - // get obeservation in - // create a json printer - // call print - - Mqtt2Service::Mqtt2Service(boost::asio::io_context &context, sink::SinkContractPtr &&contract, - const ConfigOptions &options, const ptree &config) - : Sink("Mqtt2Service", std::move(contract)), - m_context(context), - m_strand(context), - m_options(options), - m_currentTimer(context) - { - // Unique id number for agent instance - m_instanceId = getCurrentTimeInSec(); - - auto jsonPrinter = dynamic_cast(m_sinkContract->getPrinter("json")); - - m_jsonPrinter = make_unique(jsonPrinter->getJsonVersion()); - - m_printer = std::make_unique(jsonPrinter->getJsonVersion()); - - GetOptions(config, m_options, options); - AddOptions(config, m_options, - {{configuration::ProbeTopic, string()}, - {configuration::MqttCaCert, string()}, - {configuration::MqttPrivateKey, string()}, - {configuration::MqttCert, string()}, - {configuration::MqttClientId, string()}, - {configuration::MqttUserName, string()}, - {configuration::MqttPassword, string()}}); - AddDefaultedOptions( - config, m_options, - {{configuration::MqttHost, "127.0.0.1"s}, - {configuration::DeviceTopic, "MTConnect/Probe/[device]"s}, - {configuration::AssetTopic, "MTConnect/Asset/[device]"s}, - {configuration::MqttLastWillTopic, "MTConnect/Probe/[device]/Availability"s}, - {configuration::CurrentTopic, "MTConnect/Current/[device]"s}, - {configuration::SampleTopic, "MTConnect/Sample/[device]"s}, - {configuration::MqttCurrentInterval, 10000ms}, - {configuration::MqttSampleInterval, 500ms}, - {configuration::MqttSampleCount, 1000}, - {configuration::MqttPort, 1883}, - {configuration::MqttTls, false}}); - - int maxTopicDepth {GetOption(options, configuration::MqttMaxTopicDepth).value_or(7)}; - - m_deviceTopic = GetOption(m_options, configuration::ProbeTopic) - .value_or(get(m_options[configuration::DeviceTopic])); - m_assetTopic = getTopic(configuration::AssetTopic, maxTopicDepth); - m_currentTopic = getTopic(configuration::CurrentTopic, maxTopicDepth); - m_sampleTopic = getTopic(configuration::SampleTopic, maxTopicDepth); - - m_currentInterval = *GetOption(m_options, configuration::MqttCurrentInterval); - m_sampleInterval = *GetOption(m_options, configuration::MqttSampleInterval); - - m_sampleCount = *GetOption(m_options, configuration::MqttSampleCount); - } - - void Mqtt2Service::start() - { - if (!m_client) - { - auto clientHandler = make_unique(); - clientHandler->m_connected = [this](shared_ptr client) { - // Publish latest devices, assets, and observations - auto &circ = m_sinkContract->getCircularBuffer(); - std::lock_guard lock(circ); - client->connectComplete(); - - client->publish(m_lastWillTopic, "AVAILABLE"); - pubishInitialContent(); - }; - - auto agentDevice = m_sinkContract->getDeviceByName("Agent"); - auto lwtTopic = get(m_options[configuration::MqttLastWillTopic]); - m_lastWillTopic = formatTopic(lwtTopic, agentDevice, "Agent"); - - if (IsOptionSet(m_options, configuration::MqttTls)) - { - m_client = make_shared(m_context, m_options, std::move(clientHandler), - m_lastWillTopic, "UNAVAILABLE"s); - } - else - { - m_client = make_shared(m_context, m_options, std::move(clientHandler), - m_lastWillTopic, "UNAVAILABLE"s); - } - } - m_client->start(); - } - - void Mqtt2Service::stop() - { - // stop client side - if (m_client) - m_client->stop(); - - m_currentTimer.cancel(); - } - - struct AsyncSample : public observation::AsyncObserver - { - AsyncSample(boost::asio::io_context::strand &strand, - mtconnect::buffer::CircularBuffer &buffer, FilterSet &&filter, - std::chrono::milliseconds interval, std::chrono::milliseconds heartbeat, - std::shared_ptr client, DevicePtr device) - : observation::AsyncObserver(strand, buffer, std::move(filter), interval, heartbeat), - m_device(device), - m_client(client) - {} - - void fail(boost::beast::http::status status, const std::string &message) override - { - LOG(error) << "MQTT Sample Failed: " << message; - } - - bool isRunning() override - { - if (m_sink.expired()) - return false; - - auto client = m_client.lock(); - return client && client->isRunning() && client->isConnected(); - } - - bool cancel() override - { - return true; - } - - DevicePtr m_device; - std::weak_ptr m_client; - std::weak_ptr - m_sink; //! weak shared pointer to the sink. handles shutdown timer race - }; - - void Mqtt2Service::pubishInitialContent() - { - using std::placeholders::_1; - for (auto &dev : m_sinkContract->getDevices()) - { - publish(dev); - - AssetList list; - m_sinkContract->getAssetStorage()->getAssets(list, 100000, true, *(dev->getUuid())); - for (auto &asset : list) - { - publish(asset); - } - } - - auto seq = m_sinkContract->getCircularBuffer().getSequence(); - for (auto &dev : m_sinkContract->getDevices()) - { - FilterSet filterSet = filterForDevice(dev); - auto sampler = - make_shared(m_strand, m_sinkContract->getCircularBuffer(), - std::move(filterSet), m_sampleInterval, 600s, m_client, dev); - sampler->m_sink = getptr(); - sampler->m_handler = boost::bind(&Mqtt2Service::publishSample, this, _1); - sampler->observe(seq, [this](const std::string &id) { - return m_sinkContract->getDataItemById(id).get(); - }); - sampler->handlerCompleted(); - } - - publishCurrent(boost::system::error_code {}); - } - - /// @brief publish sample when observations arrive. - SequenceNumber_t Mqtt2Service::publishSample( - std::shared_ptr observer) - { - auto sampler = std::dynamic_pointer_cast(observer); - auto topic = formatTopic(m_sampleTopic, sampler->m_device); - LOG(debug) << "Publishing sample for: " << topic; - - std::unique_ptr observations; - SequenceNumber_t end {0}; - std::string doc; - SequenceNumber_t firstSeq, lastSeq; - - { - auto &buffer = m_sinkContract->getCircularBuffer(); - std::lock_guard lock(buffer); - - lastSeq = buffer.getSequence() - 1; - observations = - buffer.getObservations(m_sampleCount, sampler->getFilter(), sampler->getSequence(), - nullopt, end, firstSeq, observer->m_endOfBuffer); - } - - doc = m_printer->printSample(m_instanceId, - m_sinkContract->getCircularBuffer().getBufferSize(), end, - firstSeq, lastSeq, *observations, false); - - m_client->asyncPublish(topic, doc, [sampler, topic](std::error_code ec) { - if (!ec) - { - sampler->handlerCompleted(); - } - else - { - LOG(warning) << "Async publish failed for " << topic << ": " << ec.message(); - } - }); - - return end; - } - - void Mqtt2Service::publishCurrent(boost::system::error_code ec) - { - if (ec) - { - LOG(warning) << "Mqtt2Service::publishCurrent: " << ec.message(); - return; - } - - if (!m_client->isRunning() || !m_client->isConnected()) - { - LOG(warning) << "Mqtt2Service::publishCurrent: client stopped"; - return; - } - - for (auto &device : m_sinkContract->getDevices()) - { - auto topic = formatTopic(m_currentTopic, device); - LOG(debug) << "Publishing current for: " << topic; - - ObservationList observations; - SequenceNumber_t firstSeq, seq; - auto filterSet = filterForDevice(device); - - { - auto &buffer = m_sinkContract->getCircularBuffer(); - std::lock_guard lock(buffer); - - firstSeq = buffer.getFirstSequence(); - seq = buffer.getSequence(); - m_sinkContract->getCircularBuffer().getLatest().getObservations(observations, - filterSet); - } - - auto doc = m_printer->printSample(m_instanceId, - m_sinkContract->getCircularBuffer().getBufferSize(), - seq, firstSeq, seq - 1, observations); - - m_client->publish(topic, doc); - } - - using std::placeholders::_1; - m_currentTimer.expires_after(m_currentInterval); - m_currentTimer.async_wait(boost::asio::bind_executor( - m_strand, boost::bind(&Mqtt2Service::publishCurrent, this, _1))); - } - - bool Mqtt2Service::publish(observation::ObservationPtr &observation) - { - // Since we are doing periodic publishing, there is nothing to do here. - return true; - } - - bool Mqtt2Service::publish(device_model::DevicePtr device) - { - m_filters.clear(); - - auto topic = formatTopic(m_deviceTopic, device); - auto doc = m_jsonPrinter->print(device); - - stringstream buffer; - buffer << doc; - - if (m_client) - m_client->publish(topic, buffer.str()); - - return true; - } - - bool Mqtt2Service::publish(asset::AssetPtr asset) - { - auto uuid = asset->getDeviceUuid(); - DevicePtr dev; - if (uuid) - dev = m_sinkContract->findDeviceByUUIDorName(*uuid); - auto topic = formatTopic(m_assetTopic, dev); - if (topic.back() != '/') - topic.append("/"); - topic.append(asset->getAssetId()); - - LOG(debug) << "Publishing Asset to topic: " << topic; - - auto doc = m_jsonPrinter->print(asset); - - stringstream buffer; - buffer << doc; - - if (m_client) - m_client->publish(topic, buffer.str()); - - return true; - } - - // Register the service with the sink factory - void Mqtt2Service::registerFactory(SinkFactory &factory) - { - factory.registerFactory( - "Mqtt2Service", - [](const std::string &name, boost::asio::io_context &io, SinkContractPtr &&contract, - const ConfigOptions &options, const boost::property_tree::ptree &block) -> SinkPtr { - auto sink = std::make_shared(io, std::move(contract), options, block); - return sink; - }); - } - } // namespace mqtt_sink - } // namespace sink -} // namespace mtconnect diff --git a/src/mtconnect/sink/mqtt_sink/mqtt2_service.hpp b/src/mtconnect/sink/mqtt_sink/mqtt2_service.hpp deleted file mode 100644 index 0e1ddbab..00000000 --- a/src/mtconnect/sink/mqtt_sink/mqtt2_service.hpp +++ /dev/null @@ -1,205 +0,0 @@ -// -// Copyright Copyright 2009-2023, AMT – The Association For Manufacturing Technology (“AMT”) -// All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#pragma once - -#include "boost/asio/io_context.hpp" -#include - -#include - -#include "mtconnect/buffer/checkpoint.hpp" -#include "mtconnect/config.hpp" -#include "mtconnect/configuration/agent_config.hpp" -#include "mtconnect/entity/json_printer.hpp" -#include "mtconnect/mqtt/mqtt_client.hpp" -#include "mtconnect/observation/observation.hpp" -#include "mtconnect/printer//json_printer.hpp" -#include "mtconnect/printer/printer.hpp" -#include "mtconnect/printer/xml_printer_helper.hpp" -#include "mtconnect/sink/sink.hpp" -#include "mtconnect/utilities.hpp" - -using namespace std; -using namespace mtconnect; -using namespace mtconnect::entity; -using namespace mtconnect::mqtt_client; - -using json = nlohmann::json; - -namespace mtconnect { - class XmlPrinter; - - namespace sink { - - /// @brief MTConnect Mqtt implemention namespace - - namespace mqtt_sink { - - struct AsyncSample; - - class AGENT_LIB_API Mqtt2Service : public sink::Sink - { - // dynamic loading of sink - - public: - /// @brief Create a Mqtt Service sink - /// @param context the boost asio io_context - /// @param contract the Sink Contract from the agent - /// @param options configuration options - /// @param config additional configuration options if specified directly as a sink - Mqtt2Service(boost::asio::io_context &context, sink::SinkContractPtr &&contract, - const ConfigOptions &options, const boost::property_tree::ptree &config); - - ~Mqtt2Service() = default; - - // Sink Methods - /// @brief Start the Mqtt service - void start() override; - - /// @brief Shutdown the Mqtt service - void stop() override; - - /// @brief Receive an observation - /// - /// This does nothing since we are periodically publishing current and samples - /// - /// @param observation shared pointer to the observation - /// @return `true` if the publishing was successful - bool publish(observation::ObservationPtr &observation) override; - - /// @brief Receive an asset - /// @param asset shared point to the asset - /// @return `true` if successful - bool publish(asset::AssetPtr asset) override; - - /// @brief Receive a device - /// @param device shared pointer to the device - /// @return `true` if successful - bool publish(device_model::DevicePtr device) override; - - /// @brief Publsh all devices, assets, and begin async timer-based publishing - void pubishInitialContent(); - - /// @brief Publish a current using `CurrentInterval` option. - void publishCurrent(boost::system::error_code ec); - - /// @brief publish sample when observations arrive. - SequenceNumber_t publishSample(std::shared_ptr sampler); - - /// @brief Register the Sink factory to create this sink - /// @param factory - static void registerFactory(SinkFactory &factory); - - /// @brief gets a Mqtt Client - /// @return MqttClient - std::shared_ptr getClient() { return m_client; } - - /// @brief Mqtt Client is Connected or not - /// @return `true` when the client was connected - bool isConnected() { return m_client && m_client->isConnected(); } - - protected: - const FilterSet &filterForDevice(const DevicePtr &device) - { - auto filter = m_filters.find(*(device->getUuid())); - if (filter == m_filters.end()) - { - auto pos = m_filters.emplace(*(device->getUuid()), FilterSet()); - filter = pos.first; - auto &set = filter->second; - for (const auto &wdi : device->getDeviceDataItems()) - { - const auto di = wdi.lock(); - if (di) - set.insert(di->getId()); - } - } - return filter->second; - } - - std::string formatTopic(const std::string &topic, const DevicePtr device, - const std::string defaultUuid = "Unknown") - { - string uuid; - string formatted {topic}; - if (!device) - uuid = defaultUuid; - else - { - uuid = *(device->getUuid()); - if (std::dynamic_pointer_cast(device)) - { - uuid.insert(0, "Agent_"); - } - } - - if (formatted.find("[device]") == std::string::npos) - { - if (formatted.back() != '/') - formatted.append("/"); - formatted.append(uuid); - } - else - { - boost::replace_all(formatted, "[device]", uuid); - } - return formatted; - } - - std::string getTopic(const std::string &option, int maxTopicDepth) - { - auto topic {get(m_options[option])}; - auto depth = std::count(topic.begin(), topic.end(), '/'); - - if (depth > maxTopicDepth) - LOG(warning) << "Mqtt Option " << option - << " exceeds maximum number of levels: " << maxTopicDepth; - - return topic; - } - - protected: - std::string m_deviceTopic; //! Device topic prefix - std::string m_assetTopic; //! Asset topic prefix - std::string m_currentTopic; //! Current topic prefix - std::string m_sampleTopic; //! Sample topic prefix - std::string m_lastWillTopic; //! Topic to publish the last will when disconnected - - std::chrono::milliseconds m_currentInterval; //! Interval in ms to update current - std::chrono::milliseconds m_sampleInterval; //! min interval in ms to update sample - - uint64_t m_instanceId; - - boost::asio::io_context &m_context; - boost::asio::io_context::strand m_strand; - - ConfigOptions m_options; - - std::unique_ptr m_jsonPrinter; - std::unique_ptr m_printer; - - std::shared_ptr m_client; - boost::asio::steady_timer m_currentTimer; - int m_sampleCount; //! Timer for current requests - - std::map m_filters; - std::map> m_samplers; - }; - } // namespace mqtt_sink - } // namespace sink -} // namespace mtconnect diff --git a/src/mtconnect/sink/mqtt_sink/mqtt_legacy_service.cpp b/src/mtconnect/sink/mqtt_sink/mqtt_legacy_service.cpp new file mode 100644 index 00000000..46aea8a1 --- /dev/null +++ b/src/mtconnect/sink/mqtt_sink/mqtt_legacy_service.cpp @@ -0,0 +1,197 @@ +// +// Copyright Copyright 2009-2024, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "mqtt_legacy_service.hpp" + +#include "mtconnect/configuration/config_options.hpp" +#include "mtconnect/entity/entity.hpp" +#include "mtconnect/entity/factory.hpp" +#include "mtconnect/entity/json_parser.hpp" +#include "mtconnect/mqtt/mqtt_client_impl.hpp" +#include "mtconnect/printer/json_printer.hpp" + +using ptree = boost::property_tree::ptree; + +using namespace std; +using namespace mtconnect::asset; + +namespace asio = boost::asio; +namespace config = ::mtconnect::configuration; + +namespace mtconnect { + namespace sink { + namespace mqtt_sink { + // get obeservation in + // create a json printer + // call print + + MqttLegacyService::MqttLegacyService(boost::asio::io_context &context, + sink::SinkContractPtr &&contract, + const ConfigOptions &options, const ptree &config) + : Sink("MqttLegacyService", std::move(contract)), m_context(context), m_options(options) + { + auto jsonPrinter = dynamic_cast(m_sinkContract->getPrinter("json")); + m_jsonPrinter = make_unique(jsonPrinter->getJsonVersion()); + + GetOptions(config, m_options, options); + AddOptions(config, m_options, + {{configuration::ProbeTopic, string()}, + {configuration::MqttCaCert, string()}, + {configuration::MqttPrivateKey, string()}, + {configuration::MqttCert, string()}, + {configuration::MqttUserName, string()}, + {configuration::MqttPassword, string()}, + {configuration::MqttClientId, string()}}); + AddDefaultedOptions(config, m_options, + {{configuration::MqttHost, "127.0.0.1"s}, + {configuration::DeviceTopic, "MTConnect/Device/"s}, + {configuration::AssetTopic, "MTConnect/Asset/"s}, + {configuration::ObservationTopic, "MTConnect/Observation/"s}, + {configuration::MqttPort, 1883}, + {configuration::MqttTls, false}}); + + auto clientHandler = make_unique(); + clientHandler->m_connected = [this](shared_ptr client) { + // Publish latest devices, assets, and observations + auto &circ = m_sinkContract->getCircularBuffer(); + std::lock_guard lock(circ); + client->connectComplete(); + + for (auto &dev : m_sinkContract->getDevices()) + { + publish(dev); + } + + auto obsList {circ.getLatest().getObservations()}; + for (auto &obs : obsList) + { + observation::ObservationPtr p {obs.second}; + publish(p); + } + + AssetList list; + m_sinkContract->getAssetStorage()->getAssets(list, 100000); + for (auto &asset : list) + { + publish(asset); + } + }; + + m_devicePrefix = GetOption(m_options, configuration::ProbeTopic) + .value_or(get(m_options[configuration::DeviceTopic])); + m_assetPrefix = get(m_options[configuration::AssetTopic]); + m_observationPrefix = get(m_options[configuration::ObservationTopic]); + + if (IsOptionSet(m_options, configuration::MqttTls)) + { + m_client = make_shared(m_context, m_options, std::move(clientHandler)); + } + else + { + m_client = make_shared(m_context, m_options, std::move(clientHandler)); + } + } + + void MqttLegacyService::start() + { + // mqtt client side not a server side... + if (!m_client) + return; + + m_client->start(); + } + + void MqttLegacyService::stop() + { + // stop client side + if (m_client) + m_client->stop(); + } + + std::shared_ptr MqttLegacyService::getClient() { return m_client; } + + bool MqttLegacyService::publish(observation::ObservationPtr &observation) + { + // get the data item from observation + if (observation->isOrphan()) + return false; + + DataItemPtr dataItem = observation->getDataItem(); + + auto topic = m_observationPrefix + dataItem->getTopic(); // client asyn topic + auto content = dataItem->getTopicName(); // client asyn content + + // We may want to use the observation from the checkpoint. + string doc; + if (observation->getDataItem()->isCondition()) + { + doc = m_jsonPrinter->print(observation); + } + else + { + doc = m_jsonPrinter->printEntity(observation); + } + + if (m_client) + m_client->publish(topic, doc); + + return true; + } + + bool MqttLegacyService::publish(device_model::DevicePtr device) + { + auto topic = m_devicePrefix + *device->getUuid(); + auto doc = m_jsonPrinter->print(device); + + stringstream buffer; + buffer << doc; + + if (m_client) + m_client->publish(topic, buffer.str()); + + return true; + } + + bool MqttLegacyService::publish(asset::AssetPtr asset) + { + auto topic = m_assetPrefix + get(asset->getIdentity()); + auto doc = m_jsonPrinter->print(asset); + + stringstream buffer; + buffer << doc; + + if (m_client) + m_client->publish(topic, buffer.str()); + + return true; + } + + // Register the service with the sink factory + void MqttLegacyService::registerFactory(SinkFactory &factory) + { + factory.registerFactory( + "MqttLegacyService", + [](const std::string &name, boost::asio::io_context &io, SinkContractPtr &&contract, + const ConfigOptions &options, const boost::property_tree::ptree &block) -> SinkPtr { + auto sink = + std::make_shared(io, std::move(contract), options, block); + return sink; + }); + } + } // namespace mqtt_sink + } // namespace sink +} // namespace mtconnect diff --git a/src/mtconnect/sink/mqtt_sink/mqtt_legacy_service.hpp b/src/mtconnect/sink/mqtt_sink/mqtt_legacy_service.hpp new file mode 100644 index 00000000..ab84d97d --- /dev/null +++ b/src/mtconnect/sink/mqtt_sink/mqtt_legacy_service.hpp @@ -0,0 +1,107 @@ +// +// Copyright Copyright 2009-2024, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#pragma once + +#include "boost/asio/io_context.hpp" +#include + +#include "mtconnect/buffer/checkpoint.hpp" +#include "mtconnect/config.hpp" +#include "mtconnect/configuration/agent_config.hpp" +#include "mtconnect/entity/json_printer.hpp" +#include "mtconnect/mqtt/mqtt_client.hpp" +#include "mtconnect/observation/observation.hpp" +#include "mtconnect/printer/printer.hpp" +#include "mtconnect/printer/xml_printer_helper.hpp" +#include "mtconnect/sink/sink.hpp" +#include "mtconnect/utilities.hpp" + +using namespace std; +using namespace mtconnect::entity; +using namespace mtconnect::mqtt_client; + +namespace mtconnect { + class XmlPrinter; + + namespace sink { + + /// @brief MTConnect Mqtt implemention namespace + + namespace mqtt_sink { + class AGENT_LIB_API MqttLegacyService : public sink::Sink + { + // dynamic loading of sink + + public: + /// @brief Create a Mqtt Service sink + /// @param context the boost asio io_context + /// @param contract the Sink Contract from the agent + /// @param options configuration options + /// @param config additional configuration options if specified directly as a sink + MqttLegacyService(boost::asio::io_context &context, sink::SinkContractPtr &&contract, + const ConfigOptions &options, const boost::property_tree::ptree &config); + + ~MqttLegacyService() = default; + + // Sink Methods + /// @brief Start the Mqtt service + void start() override; + + /// @brief Shutdown the Mqtt service + void stop() override; + + /// @brief Receive an observation + /// @param observation shared pointer to the observation + /// @return `true` if the publishing was successful + bool publish(observation::ObservationPtr &observation) override; + + /// @brief Receive an asset + /// @param asset shared point to the asset + /// @return `true` if successful + bool publish(asset::AssetPtr asset) override; + + /// @brief Receive a device + /// @param device shared pointer to the device + /// @return `true` if successful + bool publish(device_model::DevicePtr device) override; + + /// @brief Register the Sink factory to create this sink + /// @param factory + static void registerFactory(SinkFactory &factory); + + /// @brief gets a Mqtt Client + /// @return MqttClient + std::shared_ptr getClient(); + + /// @brief Mqtt Client is Connected or not + /// @return `true` when the client was connected + bool isConnected() { return m_client && m_client->isConnected(); } + + protected: + std::string m_devicePrefix; + std::string m_assetPrefix; + std::string m_observationPrefix; + + boost::asio::io_context &m_context; + ConfigOptions m_options; + std::unique_ptr m_jsonPrinter; + std::shared_ptr m_client; + }; + } // namespace mqtt_sink + } // namespace sink +} // namespace mtconnect diff --git a/src/mtconnect/sink/mqtt_sink/mqtt_service.cpp b/src/mtconnect/sink/mqtt_sink/mqtt_service.cpp index b28b72d0..7c17edbf 100644 --- a/src/mtconnect/sink/mqtt_sink/mqtt_service.cpp +++ b/src/mtconnect/sink/mqtt_sink/mqtt_service.cpp @@ -1,5 +1,5 @@ // -// Copyright Copyright 2009-2024, AMT – The Association For Manufacturing Technology (“AMT”) +// Copyright Copyright 2009-2023, AMT – The Association For Manufacturing Technology (“AMT”) // All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,8 @@ #include "mqtt_service.hpp" +#include + #include "mtconnect/configuration/config_options.hpp" #include "mtconnect/entity/entity.hpp" #include "mtconnect/entity/factory.hpp" @@ -25,8 +27,10 @@ #include "mtconnect/printer/json_printer.hpp" using ptree = boost::property_tree::ptree; +using json = nlohmann::json; using namespace std; +using namespace mtconnect; using namespace mtconnect::asset; namespace asio = boost::asio; @@ -41,119 +45,264 @@ namespace mtconnect { MqttService::MqttService(boost::asio::io_context &context, sink::SinkContractPtr &&contract, const ConfigOptions &options, const ptree &config) - : Sink("MqttService", std::move(contract)), m_context(context), m_options(options) + : Sink("MqttService", std::move(contract)), + m_context(context), + m_strand(context), + m_options(options), + m_currentTimer(context) { + // Unique id number for agent instance + m_instanceId = getCurrentTimeInSec(); + auto jsonPrinter = dynamic_cast(m_sinkContract->getPrinter("json")); + m_jsonPrinter = make_unique(jsonPrinter->getJsonVersion()); + m_printer = std::make_unique(jsonPrinter->getJsonVersion()); + GetOptions(config, m_options, options); AddOptions(config, m_options, {{configuration::ProbeTopic, string()}, {configuration::MqttCaCert, string()}, {configuration::MqttPrivateKey, string()}, {configuration::MqttCert, string()}, + {configuration::MqttClientId, string()}, {configuration::MqttUserName, string()}, - {configuration::MqttPassword, string()}, - {configuration::MqttClientId, string()}}); - AddDefaultedOptions(config, m_options, - {{configuration::MqttHost, "127.0.0.1"s}, - {configuration::DeviceTopic, "MTConnect/Device/"s}, - {configuration::AssetTopic, "MTConnect/Asset/"s}, - {configuration::ObservationTopic, "MTConnect/Observation/"s}, - {configuration::MqttPort, 1883}, - {configuration::MqttTls, false}}); - - auto clientHandler = make_unique(); - clientHandler->m_connected = [this](shared_ptr client) { - // Publish latest devices, assets, and observations - auto &circ = m_sinkContract->getCircularBuffer(); - std::lock_guard lock(circ); - client->connectComplete(); - - for (auto &dev : m_sinkContract->getDevices()) + {configuration::MqttPassword, string()}}); + AddDefaultedOptions( + config, m_options, + {{configuration::MqttHost, "127.0.0.1"s}, + {configuration::DeviceTopic, "MTConnect/Probe/[device]"s}, + {configuration::AssetTopic, "MTConnect/Asset/[device]"s}, + {configuration::MqttLastWillTopic, "MTConnect/Probe/[device]/Availability"s}, + {configuration::CurrentTopic, "MTConnect/Current/[device]"s}, + {configuration::SampleTopic, "MTConnect/Sample/[device]"s}, + {configuration::MqttCurrentInterval, 10000ms}, + {configuration::MqttSampleInterval, 500ms}, + {configuration::MqttSampleCount, 1000}, + {configuration::MqttPort, 1883}, + {configuration::MqttTls, false}}); + + int maxTopicDepth {GetOption(options, configuration::MqttMaxTopicDepth).value_or(7)}; + + m_deviceTopic = GetOption(m_options, configuration::ProbeTopic) + .value_or(get(m_options[configuration::DeviceTopic])); + m_assetTopic = getTopic(configuration::AssetTopic, maxTopicDepth); + m_currentTopic = getTopic(configuration::CurrentTopic, maxTopicDepth); + m_sampleTopic = getTopic(configuration::SampleTopic, maxTopicDepth); + + m_currentInterval = *GetOption(m_options, configuration::MqttCurrentInterval); + m_sampleInterval = *GetOption(m_options, configuration::MqttSampleInterval); + + m_sampleCount = *GetOption(m_options, configuration::MqttSampleCount); + } + + void MqttService::start() + { + if (!m_client) + { + auto clientHandler = make_unique(); + clientHandler->m_connected = [this](shared_ptr client) { + // Publish latest devices, assets, and observations + auto &circ = m_sinkContract->getCircularBuffer(); + std::lock_guard lock(circ); + client->connectComplete(); + + client->publish(m_lastWillTopic, "AVAILABLE"); + pubishInitialContent(); + }; + + auto agentDevice = m_sinkContract->getDeviceByName("Agent"); + auto lwtTopic = get(m_options[configuration::MqttLastWillTopic]); + m_lastWillTopic = formatTopic(lwtTopic, agentDevice, "Agent"); + + if (IsOptionSet(m_options, configuration::MqttTls)) { - publish(dev); + m_client = make_shared(m_context, m_options, std::move(clientHandler), + m_lastWillTopic, "UNAVAILABLE"s); } - - auto obsList {circ.getLatest().getObservations()}; - for (auto &obs : obsList) + else { - observation::ObservationPtr p {obs.second}; - publish(p); + m_client = make_shared(m_context, m_options, std::move(clientHandler), + m_lastWillTopic, "UNAVAILABLE"s); } + } + m_client->start(); + } + + void MqttService::stop() + { + // stop client side + if (m_client) + m_client->stop(); + + m_currentTimer.cancel(); + } + + struct AsyncSample : public observation::AsyncObserver + { + AsyncSample(boost::asio::io_context::strand &strand, + mtconnect::buffer::CircularBuffer &buffer, FilterSet &&filter, + std::chrono::milliseconds interval, std::chrono::milliseconds heartbeat, + std::shared_ptr client, DevicePtr device) + : observation::AsyncObserver(strand, buffer, std::move(filter), interval, heartbeat), + m_device(device), + m_client(client) + {} + + void fail(boost::beast::http::status status, const std::string &message) override + { + LOG(error) << "MQTT Sample Failed: " << message; + } + + bool isRunning() override + { + if (m_sink.expired()) + return false; + + auto client = m_client.lock(); + return client && client->isRunning() && client->isConnected(); + } + + bool cancel() override { return true; } + + DevicePtr m_device; + std::weak_ptr m_client; + std::weak_ptr + m_sink; //! weak shared pointer to the sink. handles shutdown timer race + }; + + void MqttService::pubishInitialContent() + { + using std::placeholders::_1; + for (auto &dev : m_sinkContract->getDevices()) + { + publish(dev); AssetList list; - m_sinkContract->getAssetStorage()->getAssets(list, 100000); + m_sinkContract->getAssetStorage()->getAssets(list, 100000, true, *(dev->getUuid())); for (auto &asset : list) { publish(asset); } - }; - - m_devicePrefix = GetOption(m_options, configuration::ProbeTopic) - .value_or(get(m_options[configuration::DeviceTopic])); - m_assetPrefix = get(m_options[configuration::AssetTopic]); - m_observationPrefix = get(m_options[configuration::ObservationTopic]); - - if (IsOptionSet(m_options, configuration::MqttTls)) - { - m_client = make_shared(m_context, m_options, std::move(clientHandler)); } - else + + auto seq = m_sinkContract->getCircularBuffer().getSequence(); + for (auto &dev : m_sinkContract->getDevices()) { - m_client = make_shared(m_context, m_options, std::move(clientHandler)); + FilterSet filterSet = filterForDevice(dev); + auto sampler = + make_shared(m_strand, m_sinkContract->getCircularBuffer(), + std::move(filterSet), m_sampleInterval, 600s, m_client, dev); + sampler->m_sink = getptr(); + sampler->m_handler = boost::bind(&MqttService::publishSample, this, _1); + sampler->observe(seq, [this](const std::string &id) { + return m_sinkContract->getDataItemById(id).get(); + }); + sampler->handlerCompleted(); } + + publishCurrent(boost::system::error_code {}); } - void MqttService::start() + /// @brief publish sample when observations arrive. + SequenceNumber_t MqttService::publishSample( + std::shared_ptr observer) { - // mqtt client side not a server side... - if (!m_client) - return; + auto sampler = std::dynamic_pointer_cast(observer); + auto topic = formatTopic(m_sampleTopic, sampler->m_device); + LOG(debug) << "Publishing sample for: " << topic; - m_client->start(); - } + std::unique_ptr observations; + SequenceNumber_t end {0}; + std::string doc; + SequenceNumber_t firstSeq, lastSeq; - void MqttService::stop() - { - // stop client side - if (m_client) - m_client->stop(); - } + { + auto &buffer = m_sinkContract->getCircularBuffer(); + std::lock_guard lock(buffer); - std::shared_ptr MqttService::getClient() { return m_client; } + lastSeq = buffer.getSequence() - 1; + observations = + buffer.getObservations(m_sampleCount, sampler->getFilter(), sampler->getSequence(), + nullopt, end, firstSeq, observer->m_endOfBuffer); + } - bool MqttService::publish(observation::ObservationPtr &observation) - { - // get the data item from observation - if (observation->isOrphan()) - return false; + doc = m_printer->printSample(m_instanceId, + m_sinkContract->getCircularBuffer().getBufferSize(), end, + firstSeq, lastSeq, *observations, false); - DataItemPtr dataItem = observation->getDataItem(); + m_client->asyncPublish(topic, doc, [sampler, topic](std::error_code ec) { + if (!ec) + { + sampler->handlerCompleted(); + } + else + { + LOG(warning) << "Async publish failed for " << topic << ": " << ec.message(); + } + }); - auto topic = m_observationPrefix + dataItem->getTopic(); // client asyn topic - auto content = dataItem->getTopicName(); // client asyn content + return end; + } - // We may want to use the observation from the checkpoint. - string doc; - if (observation->getDataItem()->isCondition()) + void MqttService::publishCurrent(boost::system::error_code ec) + { + if (ec) { - doc = m_jsonPrinter->print(observation); + LOG(warning) << "Mqtt2Service::publishCurrent: " << ec.message(); + return; } - else + + if (!m_client->isRunning() || !m_client->isConnected()) { - doc = m_jsonPrinter->printEntity(observation); + LOG(warning) << "Mqtt2Service::publishCurrent: client stopped"; + return; } - if (m_client) + for (auto &device : m_sinkContract->getDevices()) + { + auto topic = formatTopic(m_currentTopic, device); + LOG(debug) << "Publishing current for: " << topic; + + ObservationList observations; + SequenceNumber_t firstSeq, seq; + auto filterSet = filterForDevice(device); + + { + auto &buffer = m_sinkContract->getCircularBuffer(); + std::lock_guard lock(buffer); + + firstSeq = buffer.getFirstSequence(); + seq = buffer.getSequence(); + m_sinkContract->getCircularBuffer().getLatest().getObservations(observations, + filterSet); + } + + auto doc = m_printer->printSample(m_instanceId, + m_sinkContract->getCircularBuffer().getBufferSize(), + seq, firstSeq, seq - 1, observations); + m_client->publish(topic, doc); + } + + using std::placeholders::_1; + m_currentTimer.expires_after(m_currentInterval); + m_currentTimer.async_wait(boost::asio::bind_executor( + m_strand, boost::bind(&MqttService::publishCurrent, this, _1))); + } + bool MqttService::publish(observation::ObservationPtr &observation) + { + // Since we are doing periodic publishing, there is nothing to do here. return true; } bool MqttService::publish(device_model::DevicePtr device) { - auto topic = m_devicePrefix + *device->getUuid(); + m_filters.clear(); + + auto topic = formatTopic(m_deviceTopic, device); auto doc = m_jsonPrinter->print(device); stringstream buffer; @@ -167,7 +316,17 @@ namespace mtconnect { bool MqttService::publish(asset::AssetPtr asset) { - auto topic = m_assetPrefix + get(asset->getIdentity()); + auto uuid = asset->getDeviceUuid(); + DevicePtr dev; + if (uuid) + dev = m_sinkContract->findDeviceByUUIDorName(*uuid); + auto topic = formatTopic(m_assetTopic, dev); + if (topic.back() != '/') + topic.append("/"); + topic.append(asset->getAssetId()); + + LOG(debug) << "Publishing Asset to topic: " << topic; + auto doc = m_jsonPrinter->print(asset); stringstream buffer; diff --git a/src/mtconnect/sink/mqtt_sink/mqtt_service.hpp b/src/mtconnect/sink/mqtt_sink/mqtt_service.hpp index e94d01f5..6cba5240 100644 --- a/src/mtconnect/sink/mqtt_sink/mqtt_service.hpp +++ b/src/mtconnect/sink/mqtt_sink/mqtt_service.hpp @@ -1,5 +1,5 @@ // -// Copyright Copyright 2009-2024, AMT – The Association For Manufacturing Technology (“AMT”) +// Copyright Copyright 2009-2023, AMT – The Association For Manufacturing Technology (“AMT”) // All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,21 +20,27 @@ #include "boost/asio/io_context.hpp" #include +#include + #include "mtconnect/buffer/checkpoint.hpp" #include "mtconnect/config.hpp" #include "mtconnect/configuration/agent_config.hpp" #include "mtconnect/entity/json_printer.hpp" #include "mtconnect/mqtt/mqtt_client.hpp" #include "mtconnect/observation/observation.hpp" +#include "mtconnect/printer//json_printer.hpp" #include "mtconnect/printer/printer.hpp" #include "mtconnect/printer/xml_printer_helper.hpp" #include "mtconnect/sink/sink.hpp" #include "mtconnect/utilities.hpp" using namespace std; +using namespace mtconnect; using namespace mtconnect::entity; using namespace mtconnect::mqtt_client; +using json = nlohmann::json; + namespace mtconnect { class XmlPrinter; @@ -43,6 +49,9 @@ namespace mtconnect { /// @brief MTConnect Mqtt implemention namespace namespace mqtt_sink { + + struct AsyncSample; + class AGENT_LIB_API MqttService : public sink::Sink { // dynamic loading of sink @@ -66,6 +75,9 @@ namespace mtconnect { void stop() override; /// @brief Receive an observation + /// + /// This does nothing since we are periodically publishing current and samples + /// /// @param observation shared pointer to the observation /// @return `true` if the publishing was successful bool publish(observation::ObservationPtr &observation) override; @@ -80,27 +92,113 @@ namespace mtconnect { /// @return `true` if successful bool publish(device_model::DevicePtr device) override; + /// @brief Publsh all devices, assets, and begin async timer-based publishing + void pubishInitialContent(); + + /// @brief Publish a current using `CurrentInterval` option. + void publishCurrent(boost::system::error_code ec); + + /// @brief publish sample when observations arrive. + SequenceNumber_t publishSample(std::shared_ptr sampler); + /// @brief Register the Sink factory to create this sink /// @param factory static void registerFactory(SinkFactory &factory); /// @brief gets a Mqtt Client /// @return MqttClient - std::shared_ptr getClient(); + std::shared_ptr getClient() { return m_client; } /// @brief Mqtt Client is Connected or not /// @return `true` when the client was connected bool isConnected() { return m_client && m_client->isConnected(); } protected: - std::string m_devicePrefix; - std::string m_assetPrefix; - std::string m_observationPrefix; + const FilterSet &filterForDevice(const DevicePtr &device) + { + auto filter = m_filters.find(*(device->getUuid())); + if (filter == m_filters.end()) + { + auto pos = m_filters.emplace(*(device->getUuid()), FilterSet()); + filter = pos.first; + auto &set = filter->second; + for (const auto &wdi : device->getDeviceDataItems()) + { + const auto di = wdi.lock(); + if (di) + set.insert(di->getId()); + } + } + return filter->second; + } + + std::string formatTopic(const std::string &topic, const DevicePtr device, + const std::string defaultUuid = "Unknown") + { + string uuid; + string formatted {topic}; + if (!device) + uuid = defaultUuid; + else + { + uuid = *(device->getUuid()); + if (std::dynamic_pointer_cast(device)) + { + uuid.insert(0, "Agent_"); + } + } + + if (formatted.find("[device]") == std::string::npos) + { + if (formatted.back() != '/') + formatted.append("/"); + formatted.append(uuid); + } + else + { + boost::replace_all(formatted, "[device]", uuid); + } + return formatted; + } + + std::string getTopic(const std::string &option, int maxTopicDepth) + { + auto topic {get(m_options[option])}; + auto depth = std::count(topic.begin(), topic.end(), '/'); + + if (depth > maxTopicDepth) + LOG(warning) << "Mqtt Option " << option + << " exceeds maximum number of levels: " << maxTopicDepth; + + return topic; + } + + protected: + std::string m_deviceTopic; //! Device topic prefix + std::string m_assetTopic; //! Asset topic prefix + std::string m_currentTopic; //! Current topic prefix + std::string m_sampleTopic; //! Sample topic prefix + std::string m_lastWillTopic; //! Topic to publish the last will when disconnected + + std::chrono::milliseconds m_currentInterval; //! Interval in ms to update current + std::chrono::milliseconds m_sampleInterval; //! min interval in ms to update sample + + uint64_t m_instanceId; boost::asio::io_context &m_context; + boost::asio::io_context::strand m_strand; + ConfigOptions m_options; + std::unique_ptr m_jsonPrinter; + std::unique_ptr m_printer; + std::shared_ptr m_client; + boost::asio::steady_timer m_currentTimer; + int m_sampleCount; //! Timer for current requests + + std::map m_filters; + std::map> m_samplers; }; } // namespace mqtt_sink } // namespace sink diff --git a/src/mtconnect/sink/rest_sink/rest_service.cpp b/src/mtconnect/sink/rest_sink/rest_service.cpp index 389e34ce..48905e15 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.cpp +++ b/src/mtconnect/sink/rest_sink/rest_service.cpp @@ -115,7 +115,8 @@ namespace mtconnect { {"pretty", QUERY, "Instructs the result to be pretty printed"}, {"format", QUERY, "The format of the response document: 'xml' or 'json'"}, {"heartbeat", QUERY, - "Time in ms between publishing a empty document when no data has changed"}}); + "Time in ms between publishing a empty document when no data has changed"}, + {"requestId", PATH, "webservice request id"}}); createProbeRoutings(); createCurrentRoutings(); @@ -760,6 +761,11 @@ namespace mtconnect { return true; }; + auto cancelHandler = [&](SessionPtr session, RequestPtr request) -> bool { + auto requestId = *request->parameter("requestId"); + return session->cancelRequest(requestId); + }; + string qp( "path={string}&from={unsigned_integer}&" "interval={integer}&count={integer:100}&" @@ -777,6 +783,9 @@ namespace mtconnect { "optionally filtered by the `path` and starting at `from`. By default, from is " "the first available observation known to the agent") .command("sample"); + m_server + ->addRouting({boost::beast::http::verb::get, "/cancel/requestId={string}", cancelHandler}) + .document("MTConnect WebServices Cancel Stream", "Cancels a streaming sample request"); } void RestService::createPutObservationRoutings() @@ -960,11 +969,11 @@ namespace mtconnect { return server->isRunning(); } } - + bool cancel() override { observation::AsyncObserver::cancel(); - m_session.reset(); + m_session.reset(); return true; } @@ -976,7 +985,6 @@ namespace mtconnect { rest_sink::SessionPtr m_session; ofstream m_log; bool m_pretty {false}; - std::optional m_requestId; }; void RestService::streamSampleRequest(rest_sink::SessionPtr session, const Printer *printer, @@ -1019,7 +1027,7 @@ namespace mtconnect { asyncResponse->m_printer = printer; asyncResponse->m_sink = getptr(); asyncResponse->m_pretty = pretty; - asyncResponse->m_requestId = requestId; + asyncResponse->setRequestId(requestId); session->addObserver(asyncResponse); if (m_logStreamData) @@ -1064,7 +1072,7 @@ namespace mtconnect { string content = fetchSampleData(asyncResponse->m_printer, asyncResponse->getFilter(), asyncResponse->m_count, from, nullopt, end, asyncObserver->m_endOfBuffer, asyncResponse->m_pretty, - asyncResponse->m_requestId); + asyncResponse->getRequestId()); if (m_logStreamData) asyncResponse->m_log << content << endl; @@ -1073,7 +1081,7 @@ namespace mtconnect { content, asio::bind_executor(m_strand, boost::bind(&AsyncObserver::handlerCompleted, asyncResponse)), - asyncResponse->m_requestId); + asyncResponse->getRequestId()); return end; } diff --git a/src/mtconnect/sink/rest_sink/session.hpp b/src/mtconnect/sink/rest_sink/session.hpp index 8d3f7641..ba690894 100644 --- a/src/mtconnect/sink/rest_sink/session.hpp +++ b/src/mtconnect/sink/rest_sink/session.hpp @@ -118,13 +118,27 @@ namespace mtconnect::sink::rest_sink { m_message = msg; m_unauthorized = true; } - + /// @brief Add an observer to the list for cleanup later. void addObserver(std::weak_ptr observer) { m_observers.push_back(observer); } + bool cancelRequest(const std::string &requestId) + { + for (auto &obs : m_observers) + { + auto pobs = obs.lock(); + if (pobs && pobs->getRequestId() == requestId) + { + pobs->cancel(); + return true; + } + } + return false; + } + protected: Dispatch m_dispatch; ErrorFunction m_errorFunction; diff --git a/src/mtconnect/sink/rest_sink/session_impl.cpp b/src/mtconnect/sink/rest_sink/session_impl.cpp index f1eeef2e..cdd04d2e 100644 --- a/src/mtconnect/sink/rest_sink/session_impl.cpp +++ b/src/mtconnect/sink/rest_sink/session_impl.cpp @@ -527,7 +527,7 @@ namespace mtconnect::sink::rest_sink { if (!m_closing) { m_closing = true; - + // Release all references from observers. for (auto obs : m_observers) { @@ -538,7 +538,6 @@ namespace mtconnect::sink::rest_sink { } } - // Set the timeout. beast::get_lowest_layer(m_stream).expires_after(std::chrono::seconds(30)); diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp index e38b0c4d..f06b916c 100644 --- a/src/mtconnect/sink/rest_sink/websocket_session.hpp +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -96,15 +96,15 @@ namespace mtconnect::sink::rest_sink { beast::bind_front_handler(&WebsocketSession::onAccept, derived().shared_ptr()))); } - + void close() override { NAMED_SCOPE("PlainWebsocketSession::close"); if (!m_isOpen) return; - + auto ptr = derived().shared_ptr(); - + m_request.reset(); m_requests.clear(); for (auto obs : m_observers) @@ -116,7 +116,7 @@ namespace mtconnect::sink::rest_sink { } } closeStream(); - + m_isOpen = false; } @@ -170,12 +170,12 @@ namespace mtconnect::sink::rest_sink { std::optional requestId = std::nullopt) override { NAMED_SCOPE("WebsocketSession::writeChunk"); - + if (!derived().stream().is_open()) { return; } - + if (requestId) { LOG(trace) << "Waiting for mutex"; @@ -204,7 +204,7 @@ namespace mtconnect::sink::rest_sink { fail(status::internal_server_error, "Error occurred in accpet", ec); return; } - + m_isOpen = true; derived().stream().async_read( @@ -311,7 +311,7 @@ namespace mtconnect::sink::rest_sink { using namespace rapidjson; using namespace std; - + if (len == 0) { LOG(trace) << "Empty message received"; @@ -435,7 +435,7 @@ namespace mtconnect::sink::rest_sink { std::mutex m_mutex; std::atomic_bool m_busy; std::deque m_messageQueue; - bool m_isOpen { false }; + bool m_isOpen {false}; }; template @@ -453,8 +453,8 @@ namespace mtconnect::sink::rest_sink { { beast::get_lowest_layer(m_stream).expires_never(); } - ~PlainWebsocketSession() - { + ~PlainWebsocketSession() + { if (m_isOpen) close(); } @@ -492,12 +492,12 @@ namespace mtconnect::sink::rest_sink { { beast::get_lowest_layer(m_stream).expires_never(); } - ~TlsWebsocketSession() + ~TlsWebsocketSession() { if (m_isOpen) close(); } - + auto &stream() { return m_stream; } auto getExecutor() { return m_stream.get_executor(); } diff --git a/test_package/CMakeLists.txt b/test_package/CMakeLists.txt index 08a4e1ff..ec596101 100644 --- a/test_package/CMakeLists.txt +++ b/test_package/CMakeLists.txt @@ -249,8 +249,8 @@ add_agent_test(tls_http_server FALSE sink/rest_sink TRUE) add_agent_test(routing FALSE sink/rest_sink) add_agent_test(mqtt_isolated FALSE mqtt_isolated TRUE) +add_agent_test(mqtt_legacy_sink FALSE sink/mqtt_legacy_sink TRUE) add_agent_test(mqtt_sink FALSE sink/mqtt_sink TRUE) -add_agent_test(mqtt_sink_2 FALSE sink/mqtt_sink_2 TRUE) add_agent_test(json_printer_asset TRUE json) add_agent_test(json_printer_error TRUE json) diff --git a/test_package/agent_test_helper.hpp b/test_package/agent_test_helper.hpp index a0f93a0b..4d16eb47 100644 --- a/test_package/agent_test_helper.hpp +++ b/test_package/agent_test_helper.hpp @@ -29,7 +29,7 @@ #include "mtconnect/configuration/agent_config.hpp" #include "mtconnect/configuration/config_options.hpp" #include "mtconnect/pipeline/pipeline.hpp" -#include "mtconnect/sink/mqtt_sink/mqtt2_service.hpp" +#include "mtconnect/sink/mqtt_sink/mqtt_legacy_service.hpp" #include "mtconnect/sink/mqtt_sink/mqtt_service.hpp" #include "mtconnect/sink/rest_sink/response.hpp" #include "mtconnect/sink/rest_sink/rest_service.hpp" @@ -124,8 +124,8 @@ class AgentTestHelper ~AgentTestHelper() { + m_mqttLegacyService.reset(); m_mqttService.reset(); - m_mqtt2Service.reset(); m_restService.reset(); m_adapter.reset(); if (m_agent) @@ -171,21 +171,21 @@ class AgentTestHelper return rest; } - std::shared_ptr getMqttService() + std::shared_ptr getMqttLegacyService() { using namespace mtconnect; - sink::SinkPtr sink = m_agent->findSink("MqttService"); - std::shared_ptr mqtt = - std::dynamic_pointer_cast(sink); + sink::SinkPtr sink = m_agent->findSink("MqttLegacyService"); + std::shared_ptr mqtt = + std::dynamic_pointer_cast(sink); return mqtt; } - std::shared_ptr getMqtt2Service() + std::shared_ptr getMqttService() { using namespace mtconnect; - sink::SinkPtr mqttSink = m_agent->findSink("Mqtt2Service"); - std::shared_ptr mqtt2 = - std::dynamic_pointer_cast(mqttSink); + sink::SinkPtr mqttSink = m_agent->findSink("MqttService"); + std::shared_ptr mqtt2 = + std::dynamic_pointer_cast(mqttSink); return mqtt2; } @@ -199,8 +199,8 @@ class AgentTestHelper using ptree = boost::property_tree::ptree; sink::rest_sink::RestService::registerFactory(m_sinkFactory); + sink::mqtt_sink::MqttLegacyService::registerFactory(m_sinkFactory); sink::mqtt_sink::MqttService::registerFactory(m_sinkFactory); - sink::mqtt_sink::Mqtt2Service::registerFactory(m_sinkFactory); source::adapter::shdr::ShdrAdapter::registerFactory(m_sourceFactory); ConfigOptions options = ops; @@ -232,24 +232,24 @@ class AgentTestHelper m_restService = std::dynamic_pointer_cast(sink); m_agent->addSink(m_restService); - if (HasOption(options, "MqttSink")) + if (HasOption(options, "MqttLegacySink")) { auto mqttContract = m_agent->makeSinkContract(); mqttContract->m_pipelineContext = m_context; - auto mqttsink = m_sinkFactory.make("MqttService", "MqttService", m_ioContext, + auto mqttsink = m_sinkFactory.make("MqttLegacyService", "MqttLegacyService", m_ioContext, std::move(mqttContract), options, ptree {}); - m_mqttService = std::dynamic_pointer_cast(mqttsink); - m_agent->addSink(m_mqttService); + m_mqttLegacyService = std::dynamic_pointer_cast(mqttsink); + m_agent->addSink(m_mqttLegacyService); } - if (HasOption(options, "Mqtt2Sink")) + if (HasOption(options, "MqttSink")) { auto mqttContract = m_agent->makeSinkContract(); mqttContract->m_pipelineContext = m_context; - auto mqtt2sink = m_sinkFactory.make("Mqtt2Service", "Mqtt2Service", m_ioContext, + auto mqtt2sink = m_sinkFactory.make("MqttService", "MqttService", m_ioContext, std::move(mqttContract), options, ptree {}); - m_mqtt2Service = std::dynamic_pointer_cast(mqtt2sink); - m_agent->addSink(m_mqtt2Service); + m_mqttService = std::dynamic_pointer_cast(mqtt2sink); + m_agent->addSink(m_mqttService); } m_agent->initialize(m_context); @@ -324,8 +324,8 @@ class AgentTestHelper mhttp::Server *m_server {nullptr}; std::shared_ptr m_context; std::shared_ptr m_adapter; + std::shared_ptr m_mqttLegacyService; std::shared_ptr m_mqttService; - std::shared_ptr m_mqtt2Service; std::shared_ptr m_restService; std::shared_ptr m_loopback; diff --git a/test_package/mqtt_isolated_test.cpp b/test_package/mqtt_isolated_test.cpp index 9789436c..4e7cbc56 100644 --- a/test_package/mqtt_isolated_test.cpp +++ b/test_package/mqtt_isolated_test.cpp @@ -181,7 +181,7 @@ class MqttIsolatedUnitTest : public testing::Test std::unique_ptr m_jsonPrinter; std::shared_ptr m_server; std::shared_ptr m_client; - std::shared_ptr m_service; + std::shared_ptr m_service; std::unique_ptr m_agentTestHelper; uint16_t m_port {0}; }; diff --git a/test_package/mqtt_legacy_sink_test.cpp b/test_package/mqtt_legacy_sink_test.cpp new file mode 100644 index 00000000..4647c924 --- /dev/null +++ b/test_package/mqtt_legacy_sink_test.cpp @@ -0,0 +1,630 @@ +// +// Copyright Copyright 2009-2024, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/// @file +/// Test MQTT 1 Service + +// Ensure that gtest is the first header otherwise Windows raises an error +#include +// Keep this comment to keep gtest.h above. (clang-format off/on is not working here!) + +#include + +#include + +#include "agent_test_helper.hpp" +#include "mtconnect/buffer/checkpoint.hpp" +#include "mtconnect/device_model/data_item/data_item.hpp" +#include "mtconnect/entity/entity.hpp" +#include "mtconnect/entity/json_parser.hpp" +#include "mtconnect/mqtt/mqtt_client_impl.hpp" +#include "mtconnect/mqtt/mqtt_server_impl.hpp" +#include "mtconnect/printer//json_printer.hpp" +#include "mtconnect/sink/mqtt_sink/mqtt_legacy_service.hpp" + +using namespace std; +using namespace mtconnect; +using namespace mtconnect::device_model::data_item; +using namespace mtconnect::sink::mqtt_sink; +using namespace mtconnect::asset; +using namespace mtconnect::configuration; + +// main +int main(int argc, char *argv[]) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} + +using json = nlohmann::json; + +class MqttSinkTest : public testing::Test +{ +protected: + void SetUp() override + { + m_agentTestHelper = make_unique(); + m_jsonPrinter = std::make_unique(2, true); + } + + void TearDown() override + { + const auto agent = m_agentTestHelper->getAgent(); + if (agent) + { + m_agentTestHelper->getAgent()->stop(); + m_agentTestHelper->m_ioContext.run_for(100ms); + } + if (m_client) + { + m_client->stop(); + m_agentTestHelper->m_ioContext.run_for(100ms); + m_client.reset(); + } + if (m_server) + { + m_server->stop(); + m_agentTestHelper->m_ioContext.run_for(500ms); + m_server.reset(); + } + m_agentTestHelper.reset(); + m_jsonPrinter.reset(); + } + + void createAgent(std::string testFile = {}, ConfigOptions options = {}) + { + if (testFile == "") + testFile = "/samples/test_config.xml"; + + ConfigOptions opts(options); + MergeOptions(opts, {{"MqttLegacySink", true}, + {configuration::MqttPort, m_port}, + {configuration::MqttHost, "127.0.0.1"s}}); + m_agentTestHelper->createAgent(testFile, 8, 4, "2.0", 25, false, true, opts); + addAdapter(); + + m_agentTestHelper->getAgent()->start(); + } + + void createServer(const ConfigOptions &options) + { + using namespace mtconnect::configuration; + ConfigOptions opts(options); + MergeOptions(opts, {{ServerIp, "127.0.0.1"s}, + {MqttPort, 0}, + {MqttTls, false}, + {AutoAvailable, false}, + {RealTime, false}}); + + m_server = + make_shared(m_agentTestHelper->m_ioContext, opts); + } + + template + bool waitFor(const chrono::duration &time, function pred) + { + boost::asio::steady_timer timer(m_agentTestHelper->m_ioContext); + timer.expires_from_now(time); + bool timeout = false; + timer.async_wait([&timeout](boost::system::error_code ec) { + if (!ec) + { + timeout = true; + } + }); + + while (!timeout && !pred()) + { + m_agentTestHelper->m_ioContext.run_for(100ms); + } + timer.cancel(); + + return pred(); + } + + void startServer() + { + if (m_server) + { + bool start = m_server->start(); + if (start) + { + m_port = m_server->getPort(); + m_agentTestHelper->m_ioContext.run_for(500ms); + } + } + } + + void createClient(const ConfigOptions &options, unique_ptr &&handler) + { + ConfigOptions opts(options); + MergeOptions(opts, {{MqttHost, "127.0.0.1"s}, + {MqttPort, m_port}, + {MqttTls, false}, + {AutoAvailable, false}, + {RealTime, false}}); + m_client = make_shared(m_agentTestHelper->m_ioContext, + opts, std::move(handler)); + } + + bool startClient() + { + bool started = m_client && m_client->start(); + if (started) + { + return waitFor(5s, [this]() { return m_client->isConnected(); }); + } + return started; + } + + void addAdapter(ConfigOptions options = ConfigOptions {}) + { + m_agentTestHelper->addAdapter(options, "localhost", 0, + m_agentTestHelper->m_agent->getDefaultDevice()->getName()); + } + + std::unique_ptr m_jsonPrinter; + std::shared_ptr m_server; + std::shared_ptr m_client; + std::shared_ptr m_service; + std::unique_ptr m_agentTestHelper; + uint16_t m_port {0}; +}; + +TEST_F(MqttSinkTest, mqtt_sink_should_be_loaded_by_agent) +{ + createAgent(); + auto service = m_agentTestHelper->getMqttLegacyService(); + + ASSERT_TRUE(service); +} + +TEST_F(MqttSinkTest, mqtt_sink_should_connect_to_broker) +{ + ConfigOptions options; + createServer(options); + startServer(); + + ASSERT_NE(0, m_port); + + createAgent(); + auto service = m_agentTestHelper->getMqttLegacyService(); + + ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); +} + +TEST_F(MqttSinkTest, mqtt_sink_should_connect_to_broker_with_UserNameandPassword) +{ + ConfigOptions options {{MqttUserName, "MQTT-SINK"s}, {MqttPassword, "mtconnect"s}}; + createServer(options); + startServer(); + + ASSERT_NE(0, m_port); + + createAgent("", options); + auto service = m_agentTestHelper->getMqttLegacyService(); + + ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); +} + +TEST_F(MqttSinkTest, mqtt_sink_should_connect_to_broker_without_UserNameandPassword) +{ + ConfigOptions options; + createServer(options); + startServer(); + + ASSERT_NE(0, m_port); + + createAgent(); + auto service = m_agentTestHelper->getMqttLegacyService(); + + ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); +} + +TEST_F(MqttSinkTest, mqtt_sink_should_publish_device) +{ + ConfigOptions options; + createServer(options); + startServer(); + ASSERT_NE(0, m_port); + + entity::JsonParser parser; + + auto handler = make_unique(); + bool gotDevice = false; + handler->m_receive = [&gotDevice, &parser](std::shared_ptr client, + const std::string &topic, const std::string &payload) { + EXPECT_EQ("MTConnect/Device/000", topic); + + ErrorList list; + auto ptr = parser.parse(device_model::Device::getRoot(), payload, "2.0", list); + EXPECT_EQ(0, list.size()); + auto dev = dynamic_pointer_cast(ptr); + EXPECT_TRUE(dev); + EXPECT_EQ("LinuxCNC", dev->getComponentName()); + EXPECT_EQ("000", *dev->getUuid()); + + gotDevice = true; + }; + + createClient(options, std::move(handler)); + ASSERT_TRUE(startClient()); + m_client->subscribe("MTConnect/Device/000"); + + createAgent(); + + auto service = m_agentTestHelper->getMqttLegacyService(); + + ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); + + ASSERT_TRUE(waitFor(5s, [&gotDevice]() { return gotDevice; })); +} + +TEST_F(MqttSinkTest, mqtt_sink_should_publish_Streams) +{ + ConfigOptions options; + createServer(options); + startServer(); + ASSERT_NE(0, m_port); + + entity::JsonParser parser; + + auto handler = make_unique(); + bool foundLineDataItem = false; + handler->m_receive = [&foundLineDataItem](std::shared_ptr client, + const std::string &topic, const std::string &payload) { + EXPECT_EQ("MTConnect/Observation/000/Controller[Controller]/Path/Events/Line[line]", topic); + + auto jdoc = json::parse(payload); + string value = jdoc.at("/value"_json_pointer).get(); + EXPECT_EQ("204", value); + foundLineDataItem = true; + }; + createClient(options, std::move(handler)); + ASSERT_TRUE(startClient()); + + createAgent(); + auto service = m_agentTestHelper->getMqttLegacyService(); + ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); + + m_client->subscribe("MTConnect/Observation/000/Controller[Controller]/Path/Events/Line[line]"); + m_agentTestHelper->m_adapter->processData("2021-02-01T12:00:00Z|line|204"); + + ASSERT_TRUE(waitFor(5s, [&foundLineDataItem]() { return foundLineDataItem; })); +} + +TEST_F(MqttSinkTest, mqtt_sink_should_publish_Asset) +{ + ConfigOptions options; + createServer(options); + startServer(); + ASSERT_NE(0, m_port); + + entity::JsonParser parser; + + auto handler = make_unique(); + bool gotControllerDataItem = false; + handler->m_receive = [&gotControllerDataItem](std::shared_ptr, + const std::string &topic, + const std::string &payload) { + EXPECT_EQ("MTConnect/Asset/0001", topic); + auto jdoc = json::parse(payload); + string id = jdoc.at("/Part/assetId"_json_pointer).get(); + EXPECT_EQ("0001", id); + gotControllerDataItem = true; + }; + createClient(options, std::move(handler)); + ASSERT_TRUE(startClient()); + + createAgent(); + auto service = m_agentTestHelper->getMqttLegacyService(); + ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); + m_client->subscribe("MTConnect/Asset/0001"); + + m_agentTestHelper->m_adapter->processData( + "2021-02-01T12:00:00Z|@ASSET@|@1|Part|TEST 1"); + + ASSERT_TRUE(waitFor(5s, [&gotControllerDataItem]() { return gotControllerDataItem; })); +} + +TEST_F(MqttSinkTest, mqtt_sink_should_publish_RotaryMode) +{ + ConfigOptions options; + createServer(options); + startServer(); + ASSERT_NE(0, m_port); + + entity::JsonParser parser; + + auto handler = make_unique(); + bool gotRotaryMode = false; + handler->m_receive = [&gotRotaryMode](std::shared_ptr, const std::string &topic, + const std::string &payload) { + EXPECT_EQ("MTConnect/Observation/000/Axes[Axes]/Rotary[C]/Samples/SpindleSpeed.Actual[Sspeed]", + topic); + auto jdoc = json::parse(payload); + + double v = jdoc.at("/value"_json_pointer).get(); + EXPECT_EQ(5000.0, v); + gotRotaryMode = true; + }; + + createClient(options, std::move(handler)); + ASSERT_TRUE(startClient()); + + createAgent(); + auto service = m_agentTestHelper->getMqttLegacyService(); + ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); + m_client->subscribe( + "MTConnect/Observation/000/Axes[Axes]/Rotary[C]/Samples/SpindleSpeed.Actual[Sspeed]"); + + m_agentTestHelper->m_adapter->processData( + "2021-02-01T12:00:00Z|block|G01X00|Sspeed|5000|line|204"); + + ASSERT_TRUE(waitFor(5s, [&gotRotaryMode]() { return gotRotaryMode; })); +} + +TEST_F(MqttSinkTest, mqtt_sink_should_publish_Dataset) +{ + ConfigOptions options; + createServer(options); + startServer(); + ASSERT_NE(0, m_port); + entity::JsonParser parser; + auto handler = make_unique(); + bool gotControllerDataItem = false; + handler->m_receive = [&gotControllerDataItem](std::shared_ptr, + const std::string &topic, + const std::string &payload) { + EXPECT_EQ( + "MTConnect/Observation/000/Controller[Controller]/Path[path]/Events/VariableDataSet[vars]", + topic); + auto jdoc = json::parse(payload); + auto id = jdoc.at("/value/a"_json_pointer).get(); + EXPECT_EQ(1, id); + gotControllerDataItem = true; + }; + createClient(options, std::move(handler)); + ASSERT_TRUE(startClient()); + createAgent("/samples/data_set.xml"); + auto service = m_agentTestHelper->getMqttLegacyService(); + ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); + m_client->subscribe( + "MTConnect/Observation/000/Controller[Controller]/Path[path]/Events/VariableDataSet[vars]"); + + m_agentTestHelper->m_adapter->processData("TIME|vars|a=1 b=2 c=3"); + ASSERT_TRUE(waitFor(5s, [&gotControllerDataItem]() { return gotControllerDataItem; })); +} + +TEST_F(MqttSinkTest, mqtt_sink_should_publish_Table) +{ + ConfigOptions options; + createServer(options); + startServer(); + ASSERT_NE(0, m_port); + entity::JsonParser parser; + auto handler = make_unique(); + bool gotControllerDataItem = false; + handler->m_receive = [&gotControllerDataItem](std::shared_ptr, + const std::string &topic, + const std::string &payload) { + EXPECT_EQ( + "MTConnect/Observation/000/Controller[Controller]/Path[path]/Events/WorkOffsetTable[wpo]", + topic); + auto jdoc = json::parse(payload); + + auto jValue = jdoc.at("/value"_json_pointer); + int count = 0; + if (jValue.is_object()) + { + for (auto &[key, value] : jValue.items()) + { + if (key == "G53.1" || key == "G53.2" || key == "G53.3") + { + for (auto &[subKey, subValue] : value.items()) + { + if (key == "G53.1" && ((subKey == "X" && subValue.get() == 1) || + (subKey == "Y" && subValue.get() == 2) || + (subKey == "Z" && subValue.get() == 3))) + { + count++; + } + else if (key == "G53.2" && ((subKey == "X" && subValue.get() == 4) || + (subKey == "Y" && subValue.get() == 5) || + (subKey == "Z" && subValue.get() == 6))) + { + count++; + } + else if (key == "G53.3" && ((subKey == "X" && subValue.get() == 7.0) || + (subKey == "Y" && subValue.get() == 8.0) || + (subKey == "Z" && subValue.get() == 9.0) || + (subKey == "U" && subValue.get() == 10.0))) + { + count++; + } + } + } + } + EXPECT_EQ(10, count); + gotControllerDataItem = true; + } + }; + createClient(options, std::move(handler)); + ASSERT_TRUE(startClient()); + createAgent("/samples/data_set.xml"); + auto service = m_agentTestHelper->getMqttLegacyService(); + ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); + m_client->subscribe( + "MTConnect/Observation/000/Controller[Controller]/Path[path]/Events/" + "WorkOffsetTable[wpo]"); + + m_agentTestHelper->m_adapter->processData( + "2021-02-01T12:00:00Z|wpo|G53.1={X=1.0 Y=2.0 Z=3.0} G53.2={X=4.0 Y=5.0 Z=6.0}" + "G53.3={X=7.0 Y=8.0 Z=9 U=10.0}"); + + ASSERT_TRUE(waitFor(5s, [&gotControllerDataItem]() { return gotControllerDataItem; })); +} + +TEST_F(MqttSinkTest, mqtt_sink_should_publish_Temperature) +{ + ConfigOptions options; + createServer(options); + startServer(); + ASSERT_NE(0, m_port); + + entity::JsonParser parser; + + auto handler = make_unique(); + bool gotTemperature = false; + handler->m_receive = [&gotTemperature](std::shared_ptr, const std::string &topic, + const std::string &payload) { + EXPECT_EQ( + "MTConnect/Observation/000/Axes[Axes]/Linear[Z]/Motor[motor_name]/Samples/" + "Temperature[z_motor_temp]", + topic); + auto jdoc = json::parse(payload); + + auto value = jdoc.at("/value"_json_pointer).get(); + EXPECT_EQ(81.0, value); + gotTemperature = true; + }; + + createClient(options, std::move(handler)); + ASSERT_TRUE(startClient()); + + createAgent(); + auto service = m_agentTestHelper->getMqttLegacyService(); + ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); + m_client->subscribe( + "MTConnect/Observation/000/Axes[Axes]/Linear[Z]/Motor[motor_name]/Samples/" + "Temperature[z_motor_temp]"); + + m_agentTestHelper->m_adapter->processData("2018-04-27T05:00:26.555666|z_motor_temp|81"); + + ASSERT_TRUE(waitFor(5s, [&gotTemperature]() { return gotTemperature; })); +} + +TEST_F(MqttSinkTest, mqtt_sink_should_publish_LinearLoad) +{ + ConfigOptions options; + createServer(options); + startServer(); + ASSERT_NE(0, m_port); + entity::JsonParser parser; + auto handler = make_unique(); + bool gotLinearLoad = false; + handler->m_receive = [&gotLinearLoad](std::shared_ptr, const std::string &topic, + const std::string &payload) { + EXPECT_EQ("MTConnect/Observation/000/Axes[Axes]/Linear[X]/Samples/Load[Xload]", topic); + auto jdoc = json::parse(payload); + auto value = jdoc.at("/value"_json_pointer).get(); + EXPECT_EQ(50.0, value); + gotLinearLoad = true; + }; + createClient(options, std::move(handler)); + ASSERT_TRUE(startClient()); + createAgent(); + auto service = m_agentTestHelper->getMqttLegacyService(); + ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); + m_client->subscribe("MTConnect/Observation/000/Axes[Axes]/Linear[X]/Samples/Load[Xload]"); + + m_agentTestHelper->m_adapter->processData("2018-04-27T05:00:26.555666|Xload|50"); + ASSERT_TRUE(waitFor(5s, [&gotLinearLoad]() { return gotLinearLoad; })); +} + +TEST_F(MqttSinkTest, mqtt_sink_should_publish_DynamicCalibration) +{ + ConfigOptions options; + createServer(options); + startServer(); + ASSERT_NE(0, m_port); + + entity::JsonParser parser; + + auto handler = make_unique(); + bool gotCalibration = false; + handler->m_receive = [this, &gotCalibration](std::shared_ptr, + const std::string &topic, + const std::string &payload) { + EXPECT_EQ( + "MTConnect/Observation/000/Axes[Axes]/Linear[X]/Samples/PositionTimeSeries.Actual[Xts]", + topic); + auto jdoc = json::parse(payload); + + auto value = jdoc.at("/value"_json_pointer); + ASSERT_TRUE(value.is_array()); + EXPECT_EQ(25, value.size()); + gotCalibration = true; + }; + + createClient(options, std::move(handler)); + ASSERT_TRUE(startClient()); + + createAgent(); + auto service = m_agentTestHelper->getMqttLegacyService(); + ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); + + m_client->subscribe( + "MTConnect/Observation/000/Axes[Axes]/Linear[X]/Samples/PositionTimeSeries.Actual[Xts]"); + + m_agentTestHelper->m_adapter->processData( + "2021-02-01T12:00:00Z|Xts|25|| 5118 5118 5118 5118 5118 5118 5118 5118 5118 5118 5118 5118 " + "5119 5119 5118 " + "5118 5117 5117 5119 5119 5118 5118 5118 5118 5118"); + + ASSERT_TRUE(waitFor(5s, [&gotCalibration]() { return gotCalibration; })); +} + +/// @test check if the condition includes the state as the key +TEST_F(MqttSinkTest, mqtt_should_publish_conditions_with_the_state_as_the_key) +{ + ConfigOptions options; + createServer(options); + startServer(); + ASSERT_NE(0, m_port); + entity::JsonParser parser; + auto handler = make_unique(); + bool gotCondition = false; + + handler->m_receive = [&gotCondition](std::shared_ptr, const std::string &topic, + const std::string &payload) { + EXPECT_EQ("MTConnect/Observation/000/Axes[Axes]/Rotary[C]/Condition/Temperature", topic); + auto jdoc = json::parse(payload); + EXPECT_EQ("Temperature is too high", jdoc.at("/Fault/value"_json_pointer).get()); + EXPECT_EQ("X111", jdoc.at("/Fault/nativeCode"_json_pointer).get()); + EXPECT_EQ("BAD", jdoc.at("/Fault/nativeSeverity"_json_pointer).get()); + EXPECT_EQ("HIGH", jdoc.at("/Fault/qualifier"_json_pointer).get()); + EXPECT_EQ("TEMPERATURE", jdoc.at("/Fault/type"_json_pointer).get()); + + gotCondition = true; + }; + createClient(options, std::move(handler)); + ASSERT_TRUE(startClient()); + createAgent(); + + auto di = m_agentTestHelper->m_agent->getDataItemById("ctmp"); + ASSERT_TRUE(di); + ASSERT_EQ("000/Axes[Axes]/Rotary[C]/Condition/Temperature", di->getTopic()); + + auto service = m_agentTestHelper->getMqttLegacyService(); + ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); + m_client->subscribe("MTConnect/Observation/000/Axes[Axes]/Rotary[C]/Condition/Temperature"); + + m_agentTestHelper->m_adapter->processData( + "2018-04-27T05:00:26.555666|ctmp|fault|X111|BAD|HIGH|Temperature is too high"); + ASSERT_TRUE(waitFor(5s, [&gotCondition]() { return gotCondition; })); +} diff --git a/test_package/mqtt_sink_2_test.cpp b/test_package/mqtt_sink_2_test.cpp deleted file mode 100644 index bdae5342..00000000 --- a/test_package/mqtt_sink_2_test.cpp +++ /dev/null @@ -1,426 +0,0 @@ -// -// Copyright Copyright 2009-2024, AMT – The Association For Manufacturing Technology (“AMT”) -// All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Ensure that gtest is the first header otherwise Windows raises an error -#include -// Keep this comment to keep gtest.h above. (clang-format off/on is not working here!) - -#include - -#include - -#include "agent_test_helper.hpp" -#include "json_helper.hpp" -#include "mtconnect/buffer/checkpoint.hpp" -#include "mtconnect/device_model/data_item/data_item.hpp" -#include "mtconnect/entity/entity.hpp" -#include "mtconnect/entity/json_parser.hpp" -#include "mtconnect/mqtt/mqtt_client_impl.hpp" -#include "mtconnect/mqtt/mqtt_server_impl.hpp" -#include "mtconnect/printer//json_printer.hpp" -#include "mtconnect/sink/mqtt_sink/mqtt2_service.hpp" -#include "test_utilities.hpp" - -using namespace std; -using namespace mtconnect; -using namespace mtconnect::device_model::data_item; -using namespace mtconnect::sink::mqtt_sink; -using namespace mtconnect::asset; -using namespace mtconnect::configuration; - -// main -int main(int argc, char *argv[]) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} - -using json = nlohmann::json; - -class MqttSink2Test : public testing::Test -{ -protected: - void SetUp() override - { - m_agentTestHelper = make_unique(); - m_jsonPrinter = std::make_unique(2, true); - } - - void TearDown() override - { - const auto agent = m_agentTestHelper->getAgent(); - if (agent) - { - m_agentTestHelper->getAgent()->stop(); - m_agentTestHelper->m_ioContext.run_for(100ms); - } - if (m_client) - { - m_client->stop(); - m_agentTestHelper->m_ioContext.run_for(500ms); - m_client.reset(); - } - if (m_server) - { - m_server->stop(); - m_agentTestHelper->m_ioContext.run_for(500ms); - m_server.reset(); - } - m_agentTestHelper.reset(); - m_jsonPrinter.reset(); - } - - void createAgent(std::string testFile = {}, ConfigOptions options = {}) - { - if (testFile == "") - testFile = "/samples/test_config.xml"; - - ConfigOptions opts(options); - MergeOptions(opts, {{"Mqtt2Sink", true}, - {configuration::MqttPort, m_port}, - {MqttCurrentInterval, 200ms}, - {MqttSampleInterval, 100ms}, - {configuration::MqttHost, "127.0.0.1"s}}); - m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.0", 25, false, true, opts); - addAdapter(); - - m_agentTestHelper->getAgent()->start(); - } - - void createServer(const ConfigOptions &options) - { - using namespace mtconnect::configuration; - ConfigOptions opts(options); - MergeOptions(opts, {{ServerIp, "127.0.0.1"s}, - {MqttPort, 0}, - {MqttTls, false}, - {AutoAvailable, false}, - {RealTime, false}}); - - m_server = - make_shared(m_agentTestHelper->m_ioContext, opts); - } - - template - bool waitFor(const chrono::duration &time, function pred) - { - boost::asio::steady_timer timer(m_agentTestHelper->m_ioContext); - timer.expires_after(time); - bool timeout = false; - timer.async_wait([&timeout](boost::system::error_code ec) { - if (!ec) - { - timeout = true; - } - }); - - while (!timeout && !pred()) - { - m_agentTestHelper->m_ioContext.run_for(100ms); - } - timer.cancel(); - - return pred(); - } - - void startServer() - { - if (m_server) - { - bool start = m_server->start(); - if (start) - { - m_port = m_server->getPort(); - m_agentTestHelper->m_ioContext.run_for(500ms); - } - } - } - - void createClient(const ConfigOptions &options, unique_ptr &&handler) - { - ConfigOptions opts(options); - MergeOptions(opts, {{MqttHost, "127.0.0.1"s}, - {MqttPort, m_port}, - {MqttTls, false}, - {AutoAvailable, false}, - {RealTime, false}}); - m_client = make_shared(m_agentTestHelper->m_ioContext, - opts, std::move(handler)); - } - - bool startClient() - { - bool started = m_client && m_client->start(); - if (started) - { - return waitFor(1s, [this]() { return m_client->isConnected(); }); - } - return started; - } - - void addAdapter(ConfigOptions options = ConfigOptions {}) - { - m_agentTestHelper->addAdapter(options, "localhost", 0, - m_agentTestHelper->m_agent->getDefaultDevice()->getName()); - } - - std::unique_ptr m_jsonPrinter; - std::shared_ptr m_server; - std::shared_ptr m_client; - std::unique_ptr m_agentTestHelper; - uint16_t m_port {0}; -}; - -TEST_F(MqttSink2Test, mqtt_sink_flat_formatt_check) -{ - ConfigOptions options {{MqttMaxTopicDepth, 9}, {ProbeTopic, "Device/F/l/a/t/F/o/r/m/a/t"s}}; - createServer(options); - startServer(); - - ASSERT_NE(0, m_port); - - createAgent("", options); - auto service = m_agentTestHelper->getMqtt2Service(); - - ASSERT_TRUE(waitFor(10s, [&service]() { return service->isConnected(); })); -} - -TEST_F(MqttSink2Test, mqtt_sink_should_publish_Probe) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - auto handler = make_unique(); - bool gotDevice = false; - handler->m_receive = [&gotDevice, &parser](std::shared_ptr client, - const std::string &topic, const std::string &payload) { - EXPECT_EQ("MTConnect/Probe/000", topic); - - ErrorList list; - auto ptr = parser.parse(device_model::Device::getRoot(), payload, "2.0", list); - EXPECT_EQ(0, list.size()); - auto dev = dynamic_pointer_cast(ptr); - EXPECT_TRUE(dev); - EXPECT_EQ("LinuxCNC", dev->getComponentName()); - EXPECT_EQ("000", *dev->getUuid()); - - gotDevice = true; - }; - - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - m_client->subscribe("MTConnect/Probe/000"); - - createAgent(); - - auto service = m_agentTestHelper->getMqtt2Service(); - - ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); - - ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); -} - -TEST_F(MqttSink2Test, mqtt_sink_should_publish_Sample) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - auto handler = make_unique(); - bool gotSample = false; - handler->m_receive = [&gotSample](std::shared_ptr client, const std::string &topic, - const std::string &payload) { - EXPECT_EQ("MTConnect/Sample/000", topic); - - auto jdoc = json::parse(payload); - auto streams = jdoc.at("/MTConnectStreams/Streams/0/DeviceStream"_json_pointer); - EXPECT_EQ(string("LinuxCNC"), streams.at("/name"_json_pointer).get()); - - gotSample = true; - }; - - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - m_client->subscribe("MTConnect/Sample/000"); - - createAgent(); - - auto service = m_agentTestHelper->getMqtt2Service(); - - ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); - ASSERT_FALSE(gotSample); - - m_agentTestHelper->m_adapter->processData("2021-02-01T12:00:00Z|line|204"); - ASSERT_TRUE(waitFor(10s, [&gotSample]() { return gotSample; })); -} - -TEST_F(MqttSink2Test, mqtt_sink_should_publish_Current) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - auto handler = make_unique(); - bool gotCurrent = false; - handler->m_receive = [&gotCurrent](std::shared_ptr client, const std::string &topic, - const std::string &payload) { - EXPECT_EQ("MTConnect/Current/000", topic); - - auto jdoc = json::parse(payload); - auto streams = jdoc.at("/MTConnectStreams/Streams/0/DeviceStream"_json_pointer); - EXPECT_EQ(string("LinuxCNC"), streams.at("/name"_json_pointer).get()); - - gotCurrent = true; - }; - - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - m_client->subscribe("MTConnect/Current/000"); - - createAgent(); - - auto service = m_agentTestHelper->getMqtt2Service(); - - ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); - ASSERT_TRUE(gotCurrent); - - gotCurrent = false; - ASSERT_TRUE(waitFor(1s, [&gotCurrent]() { return gotCurrent; })); -} - -TEST_F(MqttSink2Test, mqtt_sink_should_publish_Probe_with_uuid_first) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - auto handler = make_unique(); - bool gotDevice = false; - handler->m_receive = [&gotDevice, &parser](std::shared_ptr client, - const std::string &topic, const std::string &payload) { - EXPECT_EQ("MTConnect/000/Probe", topic); - - ErrorList list; - auto ptr = parser.parse(device_model::Device::getRoot(), payload, "2.0", list); - EXPECT_EQ(0, list.size()); - auto dev = dynamic_pointer_cast(ptr); - EXPECT_TRUE(dev); - EXPECT_EQ("LinuxCNC", dev->getComponentName()); - EXPECT_EQ("000", *dev->getUuid()); - - gotDevice = true; - }; - - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - m_client->subscribe("MTConnect/000/Probe"); - - createAgent("", {{configuration::ProbeTopic, "MTConnect/[device]/Probe"s}}); - - auto service = m_agentTestHelper->getMqtt2Service(); - - ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); - - ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); -} - -TEST_F(MqttSink2Test, mqtt_sink_should_publish_Probe_no_device_in_format) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - auto handler = make_unique(); - bool gotDevice = false; - handler->m_receive = [&gotDevice, &parser](std::shared_ptr client, - const std::string &topic, const std::string &payload) { - EXPECT_EQ("MTConnect/Probe/000", topic); - - ErrorList list; - auto ptr = parser.parse(device_model::Device::getRoot(), payload, "2.0", list); - EXPECT_EQ(0, list.size()); - auto dev = dynamic_pointer_cast(ptr); - EXPECT_TRUE(dev); - EXPECT_EQ("LinuxCNC", dev->getComponentName()); - EXPECT_EQ("000", *dev->getUuid()); - - gotDevice = true; - }; - - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - m_client->subscribe("MTConnect/Probe/000"); - - createAgent("", {{configuration::ProbeTopic, "MTConnect/Probe"s}}); - - auto service = m_agentTestHelper->getMqtt2Service(); - - ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); - - ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); -} - -TEST_F(MqttSink2Test, mqtt_sink_should_publish_agent_device) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - DevicePtr ad; - string agent_topic; - - auto handler = make_unique(); - bool gotDevice = false; - handler->m_receive = [&gotDevice, &parser, &agent_topic, &ad](std::shared_ptr client, - const std::string &topic, - const std::string &payload) { - EXPECT_EQ(agent_topic, topic); - gotDevice = true; - }; - - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - createAgent(); - - ad = m_agentTestHelper->m_agent->getAgentDevice(); - agent_topic = "MTConnect/Probe/Agent_"s + *ad->getUuid(); - m_client->subscribe(agent_topic); - - auto service = m_agentTestHelper->getMqtt2Service(); - - ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); - - ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); -} diff --git a/test_package/mqtt_sink_test.cpp b/test_package/mqtt_sink_test.cpp index 6675811f..5aa93846 100644 --- a/test_package/mqtt_sink_test.cpp +++ b/test_package/mqtt_sink_test.cpp @@ -14,10 +14,6 @@ // See the License for the specific language governing permissions and // limitations under the License. // - -/// @file -/// Test MQTT 1 Service - // Ensure that gtest is the first header otherwise Windows raises an error #include // Keep this comment to keep gtest.h above. (clang-format off/on is not working here!) @@ -27,6 +23,7 @@ #include #include "agent_test_helper.hpp" +#include "json_helper.hpp" #include "mtconnect/buffer/checkpoint.hpp" #include "mtconnect/device_model/data_item/data_item.hpp" #include "mtconnect/entity/entity.hpp" @@ -35,6 +32,7 @@ #include "mtconnect/mqtt/mqtt_server_impl.hpp" #include "mtconnect/printer//json_printer.hpp" #include "mtconnect/sink/mqtt_sink/mqtt_service.hpp" +#include "test_utilities.hpp" using namespace std; using namespace mtconnect; @@ -52,7 +50,7 @@ int main(int argc, char *argv[]) using json = nlohmann::json; -class MqttSinkTest : public testing::Test +class MqttSink2Test : public testing::Test { protected: void SetUp() override @@ -72,7 +70,7 @@ class MqttSinkTest : public testing::Test if (m_client) { m_client->stop(); - m_agentTestHelper->m_ioContext.run_for(100ms); + m_agentTestHelper->m_ioContext.run_for(500ms); m_client.reset(); } if (m_server) @@ -93,8 +91,10 @@ class MqttSinkTest : public testing::Test ConfigOptions opts(options); MergeOptions(opts, {{"MqttSink", true}, {configuration::MqttPort, m_port}, + {MqttCurrentInterval, 200ms}, + {MqttSampleInterval, 100ms}, {configuration::MqttHost, "127.0.0.1"s}}); - m_agentTestHelper->createAgent(testFile, 8, 4, "2.0", 25, false, true, opts); + m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.0", 25, false, true, opts); addAdapter(); m_agentTestHelper->getAgent()->start(); @@ -118,7 +118,7 @@ class MqttSinkTest : public testing::Test bool waitFor(const chrono::duration &time, function pred) { boost::asio::steady_timer timer(m_agentTestHelper->m_ioContext); - timer.expires_from_now(time); + timer.expires_after(time); bool timeout = false; timer.async_wait([&timeout](boost::system::error_code ec) { if (!ec) @@ -166,7 +166,7 @@ class MqttSinkTest : public testing::Test bool started = m_client && m_client->start(); if (started) { - return waitFor(5s, [this]() { return m_client->isConnected(); }); + return waitFor(1s, [this]() { return m_client->isConnected(); }); } return started; } @@ -180,36 +180,13 @@ class MqttSinkTest : public testing::Test std::unique_ptr m_jsonPrinter; std::shared_ptr m_server; std::shared_ptr m_client; - std::shared_ptr m_service; std::unique_ptr m_agentTestHelper; uint16_t m_port {0}; }; -TEST_F(MqttSinkTest, mqtt_sink_should_be_loaded_by_agent) -{ - createAgent(); - auto service = m_agentTestHelper->getMqttService(); - - ASSERT_TRUE(service); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_connect_to_broker) +TEST_F(MqttSink2Test, mqtt_sink_flat_formatt_check) { - ConfigOptions options; - createServer(options); - startServer(); - - ASSERT_NE(0, m_port); - - createAgent(); - auto service = m_agentTestHelper->getMqttService(); - - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_connect_to_broker_with_UserNameandPassword) -{ - ConfigOptions options {{MqttUserName, "MQTT-SINK"s}, {MqttPassword, "mtconnect"s}}; + ConfigOptions options {{MqttMaxTopicDepth, 9}, {ProbeTopic, "Device/F/l/a/t/F/o/r/m/a/t"s}}; createServer(options); startServer(); @@ -218,24 +195,10 @@ TEST_F(MqttSinkTest, mqtt_sink_should_connect_to_broker_with_UserNameandPassword createAgent("", options); auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_connect_to_broker_without_UserNameandPassword) -{ - ConfigOptions options; - createServer(options); - startServer(); - - ASSERT_NE(0, m_port); - - createAgent(); - auto service = m_agentTestHelper->getMqttService(); - - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); + ASSERT_TRUE(waitFor(10s, [&service]() { return service->isConnected(); })); } -TEST_F(MqttSinkTest, mqtt_sink_should_publish_device) +TEST_F(MqttSink2Test, mqtt_sink_should_publish_Probe) { ConfigOptions options; createServer(options); @@ -248,7 +211,7 @@ TEST_F(MqttSinkTest, mqtt_sink_should_publish_device) bool gotDevice = false; handler->m_receive = [&gotDevice, &parser](std::shared_ptr client, const std::string &topic, const std::string &payload) { - EXPECT_EQ("MTConnect/Device/000", topic); + EXPECT_EQ("MTConnect/Probe/000", topic); ErrorList list; auto ptr = parser.parse(device_model::Device::getRoot(), payload, "2.0", list); @@ -263,18 +226,18 @@ TEST_F(MqttSinkTest, mqtt_sink_should_publish_device) createClient(options, std::move(handler)); ASSERT_TRUE(startClient()); - m_client->subscribe("MTConnect/Device/000"); + m_client->subscribe("MTConnect/Probe/000"); createAgent(); auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); + ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); - ASSERT_TRUE(waitFor(5s, [&gotDevice]() { return gotDevice; })); + ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); } -TEST_F(MqttSinkTest, mqtt_sink_should_publish_Streams) +TEST_F(MqttSink2Test, mqtt_sink_should_publish_Sample) { ConfigOptions options; createServer(options); @@ -284,64 +247,34 @@ TEST_F(MqttSinkTest, mqtt_sink_should_publish_Streams) entity::JsonParser parser; auto handler = make_unique(); - bool foundLineDataItem = false; - handler->m_receive = [&foundLineDataItem](std::shared_ptr client, - const std::string &topic, const std::string &payload) { - EXPECT_EQ("MTConnect/Observation/000/Controller[Controller]/Path/Events/Line[line]", topic); + bool gotSample = false; + handler->m_receive = [&gotSample](std::shared_ptr client, const std::string &topic, + const std::string &payload) { + EXPECT_EQ("MTConnect/Sample/000", topic); auto jdoc = json::parse(payload); - string value = jdoc.at("/value"_json_pointer).get(); - EXPECT_EQ("204", value); - foundLineDataItem = true; - }; - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); + auto streams = jdoc.at("/MTConnectStreams/Streams/0/DeviceStream"_json_pointer); + EXPECT_EQ(string("LinuxCNC"), streams.at("/name"_json_pointer).get()); - createAgent(); - auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - - m_client->subscribe("MTConnect/Observation/000/Controller[Controller]/Path/Events/Line[line]"); - m_agentTestHelper->m_adapter->processData("2021-02-01T12:00:00Z|line|204"); - - ASSERT_TRUE(waitFor(5s, [&foundLineDataItem]() { return foundLineDataItem; })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_publish_Asset) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - auto handler = make_unique(); - bool gotControllerDataItem = false; - handler->m_receive = [&gotControllerDataItem](std::shared_ptr, - const std::string &topic, - const std::string &payload) { - EXPECT_EQ("MTConnect/Asset/0001", topic); - auto jdoc = json::parse(payload); - string id = jdoc.at("/Part/assetId"_json_pointer).get(); - EXPECT_EQ("0001", id); - gotControllerDataItem = true; + gotSample = true; }; + createClient(options, std::move(handler)); ASSERT_TRUE(startClient()); + m_client->subscribe("MTConnect/Sample/000"); createAgent(); + auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe("MTConnect/Asset/0001"); - m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|@ASSET@|@1|Part|TEST 1"); + ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); + ASSERT_FALSE(gotSample); - ASSERT_TRUE(waitFor(5s, [&gotControllerDataItem]() { return gotControllerDataItem; })); + m_agentTestHelper->m_adapter->processData("2021-02-01T12:00:00Z|line|204"); + ASSERT_TRUE(waitFor(10s, [&gotSample]() { return gotSample; })); } -TEST_F(MqttSinkTest, mqtt_sink_should_publish_RotaryMode) +TEST_F(MqttSink2Test, mqtt_sink_should_publish_Current) { ConfigOptions options; createServer(options); @@ -351,135 +284,34 @@ TEST_F(MqttSinkTest, mqtt_sink_should_publish_RotaryMode) entity::JsonParser parser; auto handler = make_unique(); - bool gotRotaryMode = false; - handler->m_receive = [&gotRotaryMode](std::shared_ptr, const std::string &topic, - const std::string &payload) { - EXPECT_EQ("MTConnect/Observation/000/Axes[Axes]/Rotary[C]/Samples/SpindleSpeed.Actual[Sspeed]", - topic); + bool gotCurrent = false; + handler->m_receive = [&gotCurrent](std::shared_ptr client, const std::string &topic, + const std::string &payload) { + EXPECT_EQ("MTConnect/Current/000", topic); + auto jdoc = json::parse(payload); + auto streams = jdoc.at("/MTConnectStreams/Streams/0/DeviceStream"_json_pointer); + EXPECT_EQ(string("LinuxCNC"), streams.at("/name"_json_pointer).get()); - double v = jdoc.at("/value"_json_pointer).get(); - EXPECT_EQ(5000.0, v); - gotRotaryMode = true; + gotCurrent = true; }; createClient(options, std::move(handler)); ASSERT_TRUE(startClient()); + m_client->subscribe("MTConnect/Current/000"); createAgent(); - auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe( - "MTConnect/Observation/000/Axes[Axes]/Rotary[C]/Samples/SpindleSpeed.Actual[Sspeed]"); - - m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|block|G01X00|Sspeed|5000|line|204"); - - ASSERT_TRUE(waitFor(5s, [&gotRotaryMode]() { return gotRotaryMode; })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_publish_Dataset) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - entity::JsonParser parser; - auto handler = make_unique(); - bool gotControllerDataItem = false; - handler->m_receive = [&gotControllerDataItem](std::shared_ptr, - const std::string &topic, - const std::string &payload) { - EXPECT_EQ( - "MTConnect/Observation/000/Controller[Controller]/Path[path]/Events/VariableDataSet[vars]", - topic); - auto jdoc = json::parse(payload); - auto id = jdoc.at("/value/a"_json_pointer).get(); - EXPECT_EQ(1, id); - gotControllerDataItem = true; - }; - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - createAgent("/samples/data_set.xml"); - auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe( - "MTConnect/Observation/000/Controller[Controller]/Path[path]/Events/VariableDataSet[vars]"); - - m_agentTestHelper->m_adapter->processData("TIME|vars|a=1 b=2 c=3"); - ASSERT_TRUE(waitFor(5s, [&gotControllerDataItem]() { return gotControllerDataItem; })); -} -TEST_F(MqttSinkTest, mqtt_sink_should_publish_Table) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - entity::JsonParser parser; - auto handler = make_unique(); - bool gotControllerDataItem = false; - handler->m_receive = [&gotControllerDataItem](std::shared_ptr, - const std::string &topic, - const std::string &payload) { - EXPECT_EQ( - "MTConnect/Observation/000/Controller[Controller]/Path[path]/Events/WorkOffsetTable[wpo]", - topic); - auto jdoc = json::parse(payload); - - auto jValue = jdoc.at("/value"_json_pointer); - int count = 0; - if (jValue.is_object()) - { - for (auto &[key, value] : jValue.items()) - { - if (key == "G53.1" || key == "G53.2" || key == "G53.3") - { - for (auto &[subKey, subValue] : value.items()) - { - if (key == "G53.1" && ((subKey == "X" && subValue.get() == 1) || - (subKey == "Y" && subValue.get() == 2) || - (subKey == "Z" && subValue.get() == 3))) - { - count++; - } - else if (key == "G53.2" && ((subKey == "X" && subValue.get() == 4) || - (subKey == "Y" && subValue.get() == 5) || - (subKey == "Z" && subValue.get() == 6))) - { - count++; - } - else if (key == "G53.3" && ((subKey == "X" && subValue.get() == 7.0) || - (subKey == "Y" && subValue.get() == 8.0) || - (subKey == "Z" && subValue.get() == 9.0) || - (subKey == "U" && subValue.get() == 10.0))) - { - count++; - } - } - } - } - EXPECT_EQ(10, count); - gotControllerDataItem = true; - } - }; - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - createAgent("/samples/data_set.xml"); auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe( - "MTConnect/Observation/000/Controller[Controller]/Path[path]/Events/" - "WorkOffsetTable[wpo]"); - m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|wpo|G53.1={X=1.0 Y=2.0 Z=3.0} G53.2={X=4.0 Y=5.0 Z=6.0}" - "G53.3={X=7.0 Y=8.0 Z=9 U=10.0}"); + ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); + ASSERT_TRUE(gotCurrent); - ASSERT_TRUE(waitFor(5s, [&gotControllerDataItem]() { return gotControllerDataItem; })); + gotCurrent = false; + ASSERT_TRUE(waitFor(1s, [&gotCurrent]() { return gotCurrent; })); } -TEST_F(MqttSinkTest, mqtt_sink_should_publish_Temperature) +TEST_F(MqttSink2Test, mqtt_sink_should_publish_Probe_with_uuid_first) { ConfigOptions options; createServer(options); @@ -489,64 +321,36 @@ TEST_F(MqttSinkTest, mqtt_sink_should_publish_Temperature) entity::JsonParser parser; auto handler = make_unique(); - bool gotTemperature = false; - handler->m_receive = [&gotTemperature](std::shared_ptr, const std::string &topic, - const std::string &payload) { - EXPECT_EQ( - "MTConnect/Observation/000/Axes[Axes]/Linear[Z]/Motor[motor_name]/Samples/" - "Temperature[z_motor_temp]", - topic); - auto jdoc = json::parse(payload); + bool gotDevice = false; + handler->m_receive = [&gotDevice, &parser](std::shared_ptr client, + const std::string &topic, const std::string &payload) { + EXPECT_EQ("MTConnect/000/Probe", topic); + + ErrorList list; + auto ptr = parser.parse(device_model::Device::getRoot(), payload, "2.0", list); + EXPECT_EQ(0, list.size()); + auto dev = dynamic_pointer_cast(ptr); + EXPECT_TRUE(dev); + EXPECT_EQ("LinuxCNC", dev->getComponentName()); + EXPECT_EQ("000", *dev->getUuid()); - auto value = jdoc.at("/value"_json_pointer).get(); - EXPECT_EQ(81.0, value); - gotTemperature = true; + gotDevice = true; }; createClient(options, std::move(handler)); ASSERT_TRUE(startClient()); + m_client->subscribe("MTConnect/000/Probe"); - createAgent(); - auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe( - "MTConnect/Observation/000/Axes[Axes]/Linear[Z]/Motor[motor_name]/Samples/" - "Temperature[z_motor_temp]"); - - m_agentTestHelper->m_adapter->processData("2018-04-27T05:00:26.555666|z_motor_temp|81"); - - ASSERT_TRUE(waitFor(5s, [&gotTemperature]() { return gotTemperature; })); -} + createAgent("", {{configuration::ProbeTopic, "MTConnect/[device]/Probe"s}}); -TEST_F(MqttSinkTest, mqtt_sink_should_publish_LinearLoad) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - entity::JsonParser parser; - auto handler = make_unique(); - bool gotLinearLoad = false; - handler->m_receive = [&gotLinearLoad](std::shared_ptr, const std::string &topic, - const std::string &payload) { - EXPECT_EQ("MTConnect/Observation/000/Axes[Axes]/Linear[X]/Samples/Load[Xload]", topic); - auto jdoc = json::parse(payload); - auto value = jdoc.at("/value"_json_pointer).get(); - EXPECT_EQ(50.0, value); - gotLinearLoad = true; - }; - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - createAgent(); auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe("MTConnect/Observation/000/Axes[Axes]/Linear[X]/Samples/Load[Xload]"); - m_agentTestHelper->m_adapter->processData("2018-04-27T05:00:26.555666|Xload|50"); - ASSERT_TRUE(waitFor(5s, [&gotLinearLoad]() { return gotLinearLoad; })); + ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); + + ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); } -TEST_F(MqttSinkTest, mqtt_sink_should_publish_DynamicCalibration) +TEST_F(MqttSink2Test, mqtt_sink_should_publish_Probe_no_device_in_format) { ConfigOptions options; createServer(options); @@ -556,75 +360,67 @@ TEST_F(MqttSinkTest, mqtt_sink_should_publish_DynamicCalibration) entity::JsonParser parser; auto handler = make_unique(); - bool gotCalibration = false; - handler->m_receive = [this, &gotCalibration](std::shared_ptr, - const std::string &topic, - const std::string &payload) { - EXPECT_EQ( - "MTConnect/Observation/000/Axes[Axes]/Linear[X]/Samples/PositionTimeSeries.Actual[Xts]", - topic); - auto jdoc = json::parse(payload); + bool gotDevice = false; + handler->m_receive = [&gotDevice, &parser](std::shared_ptr client, + const std::string &topic, const std::string &payload) { + EXPECT_EQ("MTConnect/Probe/000", topic); - auto value = jdoc.at("/value"_json_pointer); - ASSERT_TRUE(value.is_array()); - EXPECT_EQ(25, value.size()); - gotCalibration = true; + ErrorList list; + auto ptr = parser.parse(device_model::Device::getRoot(), payload, "2.0", list); + EXPECT_EQ(0, list.size()); + auto dev = dynamic_pointer_cast(ptr); + EXPECT_TRUE(dev); + EXPECT_EQ("LinuxCNC", dev->getComponentName()); + EXPECT_EQ("000", *dev->getUuid()); + + gotDevice = true; }; createClient(options, std::move(handler)); ASSERT_TRUE(startClient()); + m_client->subscribe("MTConnect/Probe/000"); - createAgent(); - auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); + createAgent("", {{configuration::ProbeTopic, "MTConnect/Probe"s}}); - m_client->subscribe( - "MTConnect/Observation/000/Axes[Axes]/Linear[X]/Samples/PositionTimeSeries.Actual[Xts]"); + auto service = m_agentTestHelper->getMqttService(); - m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|Xts|25|| 5118 5118 5118 5118 5118 5118 5118 5118 5118 5118 5118 5118 " - "5119 5119 5118 " - "5118 5117 5117 5119 5119 5118 5118 5118 5118 5118"); + ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); - ASSERT_TRUE(waitFor(5s, [&gotCalibration]() { return gotCalibration; })); + ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); } -/// @test check if the condition includes the state as the key -TEST_F(MqttSinkTest, mqtt_should_publish_conditions_with_the_state_as_the_key) +TEST_F(MqttSink2Test, mqtt_sink_should_publish_agent_device) { ConfigOptions options; createServer(options); startServer(); ASSERT_NE(0, m_port); + entity::JsonParser parser; - auto handler = make_unique(); - bool gotCondition = false; - handler->m_receive = [&gotCondition](std::shared_ptr, const std::string &topic, - const std::string &payload) { - EXPECT_EQ("MTConnect/Observation/000/Axes[Axes]/Rotary[C]/Condition/Temperature", topic); - auto jdoc = json::parse(payload); - EXPECT_EQ("Temperature is too high", jdoc.at("/Fault/value"_json_pointer).get()); - EXPECT_EQ("X111", jdoc.at("/Fault/nativeCode"_json_pointer).get()); - EXPECT_EQ("BAD", jdoc.at("/Fault/nativeSeverity"_json_pointer).get()); - EXPECT_EQ("HIGH", jdoc.at("/Fault/qualifier"_json_pointer).get()); - EXPECT_EQ("TEMPERATURE", jdoc.at("/Fault/type"_json_pointer).get()); + DevicePtr ad; + string agent_topic; - gotCondition = true; + auto handler = make_unique(); + bool gotDevice = false; + handler->m_receive = [&gotDevice, &parser, &agent_topic, &ad](std::shared_ptr client, + const std::string &topic, + const std::string &payload) { + EXPECT_EQ(agent_topic, topic); + gotDevice = true; }; + createClient(options, std::move(handler)); ASSERT_TRUE(startClient()); createAgent(); - auto di = m_agentTestHelper->m_agent->getDataItemById("ctmp"); - ASSERT_TRUE(di); - ASSERT_EQ("000/Axes[Axes]/Rotary[C]/Condition/Temperature", di->getTopic()); + ad = m_agentTestHelper->m_agent->getAgentDevice(); + agent_topic = "MTConnect/Probe/Agent_"s + *ad->getUuid(); + m_client->subscribe(agent_topic); auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe("MTConnect/Observation/000/Axes[Axes]/Rotary[C]/Condition/Temperature"); - m_agentTestHelper->m_adapter->processData( - "2018-04-27T05:00:26.555666|ctmp|fault|X111|BAD|HIGH|Temperature is too high"); - ASSERT_TRUE(waitFor(5s, [&gotCondition]() { return gotCondition; })); + ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); + + ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); } From 4e8f8fd41d4c121e4b2c8765be217d80acdab796 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Wed, 24 Apr 2024 22:14:49 -0400 Subject: [PATCH 23/35] fixed cancel --- src/mtconnect/observation/change_observer.cpp | 3 ++- src/mtconnect/observation/change_observer.hpp | 2 +- src/mtconnect/sink/rest_sink/rest_service.cpp | 25 ++++++++++++++----- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/mtconnect/observation/change_observer.cpp b/src/mtconnect/observation/change_observer.cpp index e186a592..a2c8a151 100644 --- a/src/mtconnect/observation/change_observer.cpp +++ b/src/mtconnect/observation/change_observer.cpp @@ -47,7 +47,8 @@ namespace mtconnect::observation { void ChangeObserver::handler(boost::system::error_code ec) { - boost::asio::post(m_strand, boost::bind(m_handler, ec)); + if (m_handler) + boost::asio::post(m_strand, boost::bind(m_handler, ec)); } // Signaler Management diff --git a/src/mtconnect/observation/change_observer.hpp b/src/mtconnect/observation/change_observer.hpp index b38cc616..4591e082 100644 --- a/src/mtconnect/observation/change_observer.hpp +++ b/src/mtconnect/observation/change_observer.hpp @@ -147,9 +147,9 @@ namespace mtconnect::observation { void clear() { std::unique_lock lock(m_mutex); + m_timer.cancel(); m_signalers.clear(); m_handler.clear(); - m_timer.cancel(); } private: diff --git a/src/mtconnect/sink/rest_sink/rest_service.cpp b/src/mtconnect/sink/rest_sink/rest_service.cpp index 48905e15..2c870ca2 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.cpp +++ b/src/mtconnect/sink/rest_sink/rest_service.cpp @@ -116,7 +116,7 @@ namespace mtconnect { {"format", QUERY, "The format of the response document: 'xml' or 'json'"}, {"heartbeat", QUERY, "Time in ms between publishing a empty document when no data has changed"}, - {"requestId", PATH, "webservice request id"}}); + {"id", PATH, "webservice request id"}}); createProbeRoutings(); createCurrentRoutings(); @@ -762,8 +762,21 @@ namespace mtconnect { }; auto cancelHandler = [&](SessionPtr session, RequestPtr request) -> bool { - auto requestId = *request->parameter("requestId"); - return session->cancelRequest(requestId); + if (request->m_requestId) + { + auto requestId = *request->m_requestId; + auto success = session->cancelRequest(requestId); + auto response = make_unique( + status::ok, "{ \"success\": \""s + (success ? "true" : "false") + "\"}", + "application/json"); + + respond(session, std::move(response), request->m_requestId); + return true; + } + else + { + return false; + } }; string qp( @@ -783,9 +796,9 @@ namespace mtconnect { "optionally filtered by the `path` and starting at `from`. By default, from is " "the first available observation known to the agent") .command("sample"); - m_server - ->addRouting({boost::beast::http::verb::get, "/cancel/requestId={string}", cancelHandler}) - .document("MTConnect WebServices Cancel Stream", "Cancels a streaming sample request"); + m_server->addRouting({boost::beast::http::verb::get, "/cancel/id={string}", cancelHandler}) + .document("MTConnect WebServices Cancel Stream", "Cancels a streaming sample request") + .command("cancel"); } void RestService::createPutObservationRoutings() From 350a031a4bcdc64a8fc8fa3ae33694d471e54a76 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Wed, 24 Apr 2024 22:59:06 -0400 Subject: [PATCH 24/35] refactored async observer and created abstract async response to cancel timed responses --- src/mtconnect/observation/change_observer.cpp | 2 +- src/mtconnect/observation/change_observer.hpp | 46 ++++++++++++------- src/mtconnect/sink/rest_sink/rest_service.cpp | 33 ++++++++----- src/mtconnect/sink/rest_sink/session.hpp | 4 +- 4 files changed, 54 insertions(+), 31 deletions(-) diff --git a/src/mtconnect/observation/change_observer.cpp b/src/mtconnect/observation/change_observer.cpp index a2c8a151..b15a646e 100644 --- a/src/mtconnect/observation/change_observer.cpp +++ b/src/mtconnect/observation/change_observer.cpp @@ -100,7 +100,7 @@ namespace mtconnect::observation { buffer::CircularBuffer &buffer, FilterSet &&filter, std::chrono::milliseconds interval, std::chrono::milliseconds heartbeat) - : m_interval(interval), + : AsyncResponse(interval), m_heartbeat(heartbeat), m_last(std::chrono::system_clock::now()), m_filter(std::move(filter)), diff --git a/src/mtconnect/observation/change_observer.hpp b/src/mtconnect/observation/change_observer.hpp index 4591e082..e3f5bc40 100644 --- a/src/mtconnect/observation/change_observer.hpp +++ b/src/mtconnect/observation/change_observer.hpp @@ -194,6 +194,32 @@ namespace mtconnect::observation { std::list m_observers; }; + /// @brief Abstract class for things asynchronouos timers + class AGENT_LIB_API AsyncResponse : public std::enable_shared_from_this + { + public: + AsyncResponse(std::chrono::milliseconds interval) : m_interval(interval) {} + + virtual bool cancel() = 0; + + /// @brief method to determine if the sink is running + virtual bool isRunning() = 0; + + /// @brief get the request id for webservices + const auto &getRequestId() const { return m_requestId; } + + /// @brief sets the optonal request id for webservices. + void setRequestId(const std::optional &id) { m_requestId = id; } + + /// @brief Get the interval + const auto &getInterval() const { return m_interval; } + + protected: + std::chrono::milliseconds m_interval { + 0}; //! the minimum amout of time to wait before calling the handler + std::optional m_requestId; //! request id + }; + /// @brief Asyncronous change context for waiting for changes /// /// This class must be subclassed and provide a fail and isRunning method. @@ -205,7 +231,7 @@ namespace mtconnect::observation { /// /// The handler and sequence numbers are handled inside the circular buffer lock to prevent race /// conditions with incoming data. - class AGENT_LIB_API AsyncObserver : public std::enable_shared_from_this + class AGENT_LIB_API AsyncObserver : public AsyncResponse { public: /// @Brief callback when observations are ready @@ -226,7 +252,7 @@ namespace mtconnect::observation { virtual ~AsyncObserver() = default; /// @brief Get a shared pointed - auto getptr() const { return const_cast(this)->shared_from_this(); } + auto getptr() { return std::dynamic_pointer_cast(shared_from_this()); } /// @brief sets up the `ChangeObserver` using the filter and initializes the references to the /// buffer @@ -244,11 +270,8 @@ namespace mtconnect::observation { /// @brief abstract call to failure handler virtual void fail(boost::beast::http::status status, const std::string &message) = 0; - /// @brief method to determine if the sink is running - virtual bool isRunning() = 0; - /// @brief Stop all timers and release resources. - virtual bool cancel() + bool cancel() override { m_observer.clear(); return true; @@ -266,14 +289,6 @@ namespace mtconnect::observation { auto getSequence() const { return m_sequence; } auto isEndOfBuffer() const { return m_endOfBuffer; } const auto &getFilter() const { return m_filter; } - const auto &getRequestId() const { return m_requestId; } - ///@} - - ///@{ - /// @name setters - - /// @brief sets the optonal request id for webservices. - void setRequestId(const std::optional &id) { m_requestId = id; } ///@} mutable bool m_endOfBuffer {false}; //! Public indicator that we are at the end of the buffer @@ -288,8 +303,6 @@ namespace mtconnect::observation { protected: SequenceNumber_t m_sequence {0}; //! the current sequence number - std::chrono::milliseconds m_interval { - 0}; //! the minimum amout of time to wait before calling the handler std::chrono::milliseconds m_heartbeat { 0}; //! the maximum amount of time to wait before sending a heartbeat std::chrono::system_clock::time_point m_last; //! the last time the handler completed @@ -298,6 +311,5 @@ namespace mtconnect::observation { ChangeObserver m_observer; //! the change observer mtconnect::buffer::CircularBuffer &m_buffer; //! reference to the circular buffer - std::optional m_requestId; //! request id }; } // namespace mtconnect::observation diff --git a/src/mtconnect/sink/rest_sink/rest_service.cpp b/src/mtconnect/sink/rest_sink/rest_service.cpp index 2c870ca2..94d41323 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.cpp +++ b/src/mtconnect/sink/rest_sink/rest_service.cpp @@ -1119,20 +1119,30 @@ namespace mtconnect { return 0; } - struct AsyncCurrentResponse + struct AsyncCurrentResponse : public AsyncResponse { - AsyncCurrentResponse(rest_sink::SessionPtr session, asio::io_context &context) - : m_session(session), m_timer(context) + AsyncCurrentResponse(rest_sink::SessionPtr session, asio::io_context &context, + chrono::milliseconds interval) + : AsyncResponse(interval), m_session(session), m_timer(context) {} + auto getptr() { return dynamic_pointer_cast(shared_from_this()); } + + bool cancel() override + { + m_timer.cancel(); + m_session.reset(); + return true; + } + + bool isRunning() override { return (bool)m_session; } + std::weak_ptr m_service; rest_sink::SessionPtr m_session; - chrono::milliseconds m_interval; const Printer *m_printer {nullptr}; FilterSetOpt m_filter; boost::asio::steady_timer m_timer; bool m_pretty {false}; - std::optional m_requestId; }; void RestService::streamCurrentRequest(SessionPtr session, const Printer *printer, @@ -1149,17 +1159,18 @@ namespace mtconnect { dev = checkDevice(printer, *device); } - auto asyncResponse = make_shared(session, m_context); + auto asyncResponse = + make_shared(session, m_context, chrono::milliseconds {interval}); if (path || device || deviceType) { asyncResponse->m_filter = make_optional(); checkPath(printer, path, dev, *asyncResponse->m_filter, deviceType); } - asyncResponse->m_interval = chrono::milliseconds {interval}; asyncResponse->m_printer = printer; asyncResponse->m_service = getptr(); asyncResponse->m_pretty = pretty; - asyncResponse->m_requestId = requestId; + asyncResponse->setRequestId(requestId); + session->addObserver(asyncResponse); asyncResponse->m_session->beginStreaming( printer->mimeType(), @@ -1202,16 +1213,16 @@ namespace mtconnect { asyncResponse->m_session->writeChunk( fetchCurrentData(asyncResponse->m_printer, asyncResponse->m_filter, nullopt, - asyncResponse->m_pretty, asyncResponse->m_requestId), + asyncResponse->m_pretty, asyncResponse->getRequestId()), boost::asio::bind_executor( m_strand, [this, asyncResponse]() { - asyncResponse->m_timer.expires_from_now(asyncResponse->m_interval); + asyncResponse->m_timer.expires_from_now(asyncResponse->getInterval()); asyncResponse->m_timer.async_wait(boost::asio::bind_executor( m_strand, boost::bind(&RestService::streamNextCurrent, this, asyncResponse, _1))); }), - asyncResponse->m_requestId); + asyncResponse->getRequestId()); } catch (RequestError &re) { diff --git a/src/mtconnect/sink/rest_sink/session.hpp b/src/mtconnect/sink/rest_sink/session.hpp index ba690894..c49a2658 100644 --- a/src/mtconnect/sink/rest_sink/session.hpp +++ b/src/mtconnect/sink/rest_sink/session.hpp @@ -120,7 +120,7 @@ namespace mtconnect::sink::rest_sink { } /// @brief Add an observer to the list for cleanup later. - void addObserver(std::weak_ptr observer) + void addObserver(std::weak_ptr observer) { m_observers.push_back(observer); } @@ -148,7 +148,7 @@ namespace mtconnect::sink::rest_sink { bool m_allowPuts {false}; std::set m_allowPutsFrom; boost::asio::ip::tcp::endpoint m_remote; - std::list> m_observers; + std::list> m_observers; }; } // namespace mtconnect::sink::rest_sink From 2e324a059bd413cd070f5da2bc6f32180dcf24e7 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Thu, 25 Apr 2024 12:57:02 -0400 Subject: [PATCH 25/35] Fixed race in change observer destructor. --- src/mtconnect/observation/change_observer.cpp | 11 +++ src/mtconnect/observation/change_observer.hpp | 8 +-- src/mtconnect/sink/rest_sink/rest_service.cpp | 71 +++++++++++-------- 3 files changed, 55 insertions(+), 35 deletions(-) diff --git a/src/mtconnect/observation/change_observer.cpp b/src/mtconnect/observation/change_observer.cpp index b15a646e..e106bb62 100644 --- a/src/mtconnect/observation/change_observer.cpp +++ b/src/mtconnect/observation/change_observer.cpp @@ -29,14 +29,25 @@ using namespace std; namespace mtconnect::observation { ChangeObserver::~ChangeObserver() { + std::lock_guard scopedLock(m_mutex); + clear(); + } + + void ChangeObserver::clear() + { + std::unique_lock lock(m_mutex); + m_timer.cancel(); + m_handler.clear(); for (const auto signaler : m_signalers) signaler->removeObserver(this); + m_signalers.clear(); } void ChangeObserver::addSignaler(ChangeSignaler *sig) { m_signalers.emplace_back(sig); } bool ChangeObserver::removeSignaler(ChangeSignaler *sig) { + std::lock_guard scopedLock(m_mutex); auto newEndPos = std::remove(m_signalers.begin(), m_signalers.end(), sig); if (newEndPos == m_signalers.end()) return false; diff --git a/src/mtconnect/observation/change_observer.hpp b/src/mtconnect/observation/change_observer.hpp index e3f5bc40..c274e848 100644 --- a/src/mtconnect/observation/change_observer.hpp +++ b/src/mtconnect/observation/change_observer.hpp @@ -144,13 +144,7 @@ namespace mtconnect::observation { ///@} /// @brief clear the observer information. - void clear() - { - std::unique_lock lock(m_mutex); - m_timer.cancel(); - m_signalers.clear(); - m_handler.clear(); - } + void clear(); private: boost::asio::io_context::strand &m_strand; diff --git a/src/mtconnect/sink/rest_sink/rest_service.cpp b/src/mtconnect/sink/rest_sink/rest_service.cpp index 94d41323..82c1df6c 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.cpp +++ b/src/mtconnect/sink/rest_sink/rest_service.cpp @@ -1090,12 +1090,14 @@ namespace mtconnect { if (m_logStreamData) asyncResponse->m_log << content << endl; - asyncResponse->m_session->writeChunk( - content, - asio::bind_executor(m_strand, - boost::bind(&AsyncObserver::handlerCompleted, asyncResponse)), - asyncResponse->getRequestId()); - + if (asyncResponse->m_session) + { + asyncResponse->m_session->writeChunk( + content, + asio::bind_executor(m_strand, + boost::bind(&AsyncObserver::handlerCompleted, asyncResponse)), + asyncResponse->getRequestId()); + } return end; } @@ -1103,9 +1105,12 @@ namespace mtconnect { { LOG(error) << asyncResponse->m_session->getRemote().address() << ": Error processing request: " << re.what(); - ResponsePtr resp = std::make_unique(re); - asyncResponse->m_session->writeResponse(std::move(resp)); - asyncResponse->m_session->close(); + if (asyncResponse->m_session) + { + ResponsePtr resp = std::make_unique(re); + asyncResponse->m_session->writeResponse(std::move(resp)); + asyncResponse->m_session->close(); + } } catch (...) @@ -1194,7 +1199,7 @@ namespace mtconnect { if (!service || !m_server || !m_server->isRunning()) { LOG(warning) << "Trying to send chunk when service has stopped"; - if (service) + if (service && asyncResponse->m_session) { asyncResponse->m_session->fail(boost::beast::http::status::internal_server_error, "Agent shutting down, aborting stream"); @@ -1206,31 +1211,38 @@ namespace mtconnect { { LOG(warning) << "Unexpected error streamNextCurrent, aborting"; LOG(warning) << ec.category().message(ec.value()) << ": " << ec.message(); - asyncResponse->m_session->fail(boost::beast::http::status::internal_server_error, - "Unexpected error streamNextCurrent, aborting"); + if (asyncResponse->m_session) + asyncResponse->m_session->fail(boost::beast::http::status::internal_server_error, + "Unexpected error streamNextCurrent, aborting"); return; } - asyncResponse->m_session->writeChunk( - fetchCurrentData(asyncResponse->m_printer, asyncResponse->m_filter, nullopt, - asyncResponse->m_pretty, asyncResponse->getRequestId()), - boost::asio::bind_executor( - m_strand, - [this, asyncResponse]() { - asyncResponse->m_timer.expires_from_now(asyncResponse->getInterval()); - asyncResponse->m_timer.async_wait(boost::asio::bind_executor( - m_strand, - boost::bind(&RestService::streamNextCurrent, this, asyncResponse, _1))); - }), - asyncResponse->getRequestId()); + if (asyncResponse->m_session) + { + asyncResponse->m_session->writeChunk( + fetchCurrentData(asyncResponse->m_printer, asyncResponse->m_filter, nullopt, + asyncResponse->m_pretty, asyncResponse->getRequestId()), + boost::asio::bind_executor( + m_strand, + [this, asyncResponse]() { + asyncResponse->m_timer.expires_from_now(asyncResponse->getInterval()); + asyncResponse->m_timer.async_wait(boost::asio::bind_executor( + m_strand, + boost::bind(&RestService::streamNextCurrent, this, asyncResponse, _1))); + }), + asyncResponse->getRequestId()); + } } catch (RequestError &re) { LOG(error) << asyncResponse->m_session->getRemote().address() << ": Error processing request: " << re.what(); - ResponsePtr resp = std::make_unique(re); - asyncResponse->m_session->writeResponse(std::move(resp)); - asyncResponse->m_session->close(); + if (asyncResponse->m_session) + { + ResponsePtr resp = std::make_unique(re); + asyncResponse->m_session->writeResponse(std::move(resp)); + asyncResponse->m_session->close(); + } } catch (...) @@ -1238,7 +1250,10 @@ namespace mtconnect { std::stringstream txt; txt << asyncResponse->m_session->getRemote().address() << ": Unknown Error thrown"; LOG(error) << txt.str(); - asyncResponse->m_session->fail(boost::beast::http::status::not_found, txt.str()); + if (asyncResponse->m_session) + { + asyncResponse->m_session->fail(boost::beast::http::status::not_found, txt.str()); + } } } From 8b24f2e94231814e07f1b1ba7f6a5290929b396f Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Sat, 27 Apr 2024 14:02:39 -0400 Subject: [PATCH 26/35] fixed requests in websockets --- src/mtconnect/observation/change_observer.cpp | 2 +- src/mtconnect/sink/rest_sink/request.hpp | 3 + .../sink/rest_sink/websocket_session.hpp | 57 ++++++++++--------- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/mtconnect/observation/change_observer.cpp b/src/mtconnect/observation/change_observer.cpp index e106bb62..f489b287 100644 --- a/src/mtconnect/observation/change_observer.cpp +++ b/src/mtconnect/observation/change_observer.cpp @@ -32,7 +32,7 @@ namespace mtconnect::observation { std::lock_guard scopedLock(m_mutex); clear(); } - + void ChangeObserver::clear() { std::unique_lock lock(m_mutex); diff --git a/src/mtconnect/sink/rest_sink/request.hpp b/src/mtconnect/sink/rest_sink/request.hpp index ac3f2c33..40eb1217 100644 --- a/src/mtconnect/sink/rest_sink/request.hpp +++ b/src/mtconnect/sink/rest_sink/request.hpp @@ -57,6 +57,9 @@ namespace mtconnect::sink::rest_sink { /// The request can be a simple reply response or streaming request struct Request { + Request() = default; + Request(const Request &request) = default; + boost::beast::http::verb m_verb; ///< GET, PUT, POST, or DELETE std::string m_body; ///< The body of the request std::string m_accepts; ///< The accepts header diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp index f06b916c..013d8601 100644 --- a/src/mtconnect/sink/rest_sink/websocket_session.hpp +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -42,6 +42,7 @@ namespace mtconnect::sink::rest_sink { std::optional m_streamBuffer; Complete m_complete; bool m_streaming {false}; + RequestPtr m_request; }; /// @brief A websocket session that provides a pubsub interface using REST parameters @@ -342,7 +343,10 @@ namespace mtconnect::sink::rest_sink { { // Extract the parameters from the json doc to map them to the REST // protocol parameters - m_request->m_verb = beast::http::verb::get; + auto request = make_unique(*m_request); + + request->m_verb = beast::http::verb::get; + request->m_parameters.clear(); const auto &object = doc.GetObject(); for (auto &it : object) @@ -353,58 +357,56 @@ namespace mtconnect::sink::rest_sink { // Skip nulls break; case rapidjson::kFalseType: - m_request->m_parameters.emplace(make_pair(it.name.GetString(), false)); + request->m_parameters.emplace(make_pair(it.name.GetString(), false)); break; case rapidjson::kTrueType: - m_request->m_parameters.emplace(make_pair(it.name.GetString(), true)); + request->m_parameters.emplace(make_pair(it.name.GetString(), true)); break; case rapidjson::kObjectType: break; case rapidjson::kArrayType: break; case rapidjson::kStringType: - m_request->m_parameters.emplace( + request->m_parameters.emplace( make_pair(it.name.GetString(), string(it.value.GetString()))); break; case rapidjson::kNumberType: if (it.value.Is()) - m_request->m_parameters.emplace( - make_pair(it.name.GetString(), it.value.Get())); + request->m_parameters.emplace(make_pair(it.name.GetString(), it.value.Get())); else if (it.value.Is()) - m_request->m_parameters.emplace( + request->m_parameters.emplace( make_pair(it.name.GetString(), it.value.Get())); else if (it.value.Is()) - m_request->m_parameters.emplace( + request->m_parameters.emplace( make_pair(it.name.GetString(), (uint64_t)it.value.Get())); else if (it.value.Is()) - m_request->m_parameters.emplace( + request->m_parameters.emplace( make_pair(it.name.GetString(), it.value.Get())); else if (it.value.Is()) - m_request->m_parameters.emplace( + request->m_parameters.emplace( make_pair(it.name.GetString(), it.value.Get())); break; } } - if (m_request->m_parameters.count("request") > 0) + if (request->m_parameters.count("request") > 0) { - m_request->m_command = get(m_request->m_parameters["request"]); - m_request->m_parameters.erase("request"); + request->m_command = get(request->m_parameters["request"]); + request->m_parameters.erase("request"); } - if (m_request->m_parameters.count("id") > 0) + if (request->m_parameters.count("id") > 0) { - auto &v = m_request->m_parameters["id"]; + auto &v = request->m_parameters["id"]; string id = visit(overloaded {[](monostate m) { return ""s; }, [](auto v) { return boost::lexical_cast(v); }}, v); - m_request->m_requestId = id; - m_request->m_parameters.erase("id"); + request->m_requestId = id; + request->m_parameters.erase("id"); } - auto &id = *(m_request->m_requestId); - + auto &id = *(request->m_requestId); auto res = m_requests.emplace(id, id); if (!res.second) { @@ -412,14 +414,17 @@ namespace mtconnect::sink::rest_sink { boost::system::error_code ec; fail(status::bad_request, "Duplicate request Id", ec); } - - if (!m_dispatch(derived().shared_ptr(), m_request)) + else { - ostringstream txt; - txt << "Failed to find handler for " << buffer; - LOG(error) << txt.str(); - boost::system::error_code ec; - fail(status::bad_request, "Duplicate request Id", ec); + res.first->second.m_request = std::move(request); + if (!m_dispatch(derived().shared_ptr(), res.first->second.m_request)) + { + ostringstream txt; + txt << "Failed to find handler for " << buffer; + LOG(error) << txt.str(); + boost::system::error_code ec; + fail(status::bad_request, "Duplicate request Id", ec); + } } } From 1ee15c1b999d310b9d5b41bc66c9e8c683b495e4 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Sun, 28 Apr 2024 22:48:37 -0400 Subject: [PATCH 27/35] merged main-dev --- src/mtconnect/agent.cpp | 4 ++-- src/mtconnect/device_model/data_item/data_item.cpp | 2 +- src/mtconnect/device_model/data_item/data_item.hpp | 4 ++-- test_package/agent_test.cpp | 3 +-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/mtconnect/agent.cpp b/src/mtconnect/agent.cpp index 864d7f1a..958912a3 100644 --- a/src/mtconnect/agent.cpp +++ b/src/mtconnect/agent.cpp @@ -299,7 +299,7 @@ namespace mtconnect { { if (item.expired()) continue; - + auto di = item.lock(); if (di->hasInitialValue()) { @@ -307,7 +307,7 @@ namespace mtconnect { } } } - + std::lock_guard lock(m_circularBuffer); if (m_circularBuffer.addToBuffer(observation) != 0) { diff --git a/src/mtconnect/device_model/data_item/data_item.cpp b/src/mtconnect/device_model/data_item/data_item.cpp index 53f238b9..b0596fbd 100644 --- a/src/mtconnect/device_model/data_item/data_item.cpp +++ b/src/mtconnect/device_model/data_item/data_item.cpp @@ -201,7 +201,7 @@ namespace mtconnect { } } } - + if (const auto &init = maybeGet("InitialValue"); init) { m_initialValue = *init; diff --git a/src/mtconnect/device_model/data_item/data_item.hpp b/src/mtconnect/device_model/data_item/data_item.hpp index 7f62fdbd..8a7c6959 100644 --- a/src/mtconnect/device_model/data_item/data_item.hpp +++ b/src/mtconnect/device_model/data_item/data_item.hpp @@ -139,11 +139,11 @@ namespace mtconnect { /// @brief get the topic name leaf node for this data item /// @return the topic name const auto &getTopicName() const { return m_topicName; } - + /// @brief get the initial value if one is set /// @return optional initial value const auto &getInitialValue() const { return m_initialValue; } - + Category getCategory() const { return m_category; } Representation getRepresentation() const { return m_representation; } SpecialClass getSpecialClass() const { return m_specialClass; } diff --git a/test_package/agent_test.cpp b/test_package/agent_test.cpp index f3cabd50..c859d1a5 100644 --- a/test_package/agent_test.cpp +++ b/test_package/agent_test.cpp @@ -3139,7 +3139,6 @@ TEST_F(AgentTest, should_not_set_validation_flag_in_header_when_validation_is_fa } } - TEST_F(AgentTest, should_initialize_observaton_to_initial_value_when_available) { m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.2", 4, true); @@ -3153,7 +3152,7 @@ TEST_F(AgentTest, should_initialize_observaton_to_initial_value_when_available) PARSE_XML_RESPONSE("/current"); ASSERT_XML_PATH_EQUAL(doc, "//m:DeviceStream//m:PartCount", "UNAVAILABLE"); } - + m_agentTestHelper->m_adapter->processData("2024-01-22T20:00:00Z|avail|AVAILABLE"); { From a6f5f8f9378a3b846130d15a0ecd1e9f477d3d9c Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Sun, 28 Apr 2024 22:54:22 -0400 Subject: [PATCH 28/35] Removed extra character --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f5560bb9..2ea263a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ set(AGENT_VERSION_MAJOR 2) set(AGENT_VERSION_MINOR 5) set(AGENT_VERSION_PATCH 0) set(AGENT_VERSION_BUILD 0) -set(AGENT_VERSION_RC "RC2") +set(AGENT_VERSION_RC "RC2") # This minimum version is to support Visual Studio 2019 and C++ feature checking and FetchContent cmake_minimum_required(VERSION 3.23 FATAL_ERROR) From 389aa9f5fbd79637c8720528894ba05baf980ab8 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Thu, 2 May 2024 16:56:23 -0400 Subject: [PATCH 29/35] Added initial websocket test --- src/mtconnect/sink/rest_sink/routing.hpp | 6 +- .../sink/rest_sink/websocket_session.hpp | 11 +- test_package/CMakeLists.txt | 1 + test_package/websockets_test.cpp | 246 ++++++++++++++++++ 4 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 test_package/websockets_test.cpp diff --git a/src/mtconnect/sink/rest_sink/routing.hpp b/src/mtconnect/sink/rest_sink/routing.hpp index 0710f15b..bfa584ee 100644 --- a/src/mtconnect/sink/rest_sink/routing.hpp +++ b/src/mtconnect/sink/rest_sink/routing.hpp @@ -241,7 +241,11 @@ namespace mtconnect::sink::rest_sink { /// @brief Sets the command associated with this routing for use with websockets /// @param command the command - void command(const std::string &command) { m_command = command; } + auto &command(const std::string &command) + { + m_command = command; + return *this; + } protected: void pathParameters(std::string s) diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp index 013d8601..84eda57e 100644 --- a/src/mtconnect/sink/rest_sink/websocket_session.hpp +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -104,7 +104,14 @@ namespace mtconnect::sink::rest_sink { if (!m_isOpen) return; - auto ptr = derived().shared_ptr(); + m_isOpen = false; + + auto wptr = weak_from_this(); + std::shared_ptr ptr; + if (!wptr.expired()) + { + ptr = wptr.lock(); + } m_request.reset(); m_requests.clear(); @@ -117,8 +124,6 @@ namespace mtconnect::sink::rest_sink { } } closeStream(); - - m_isOpen = false; } void writeResponse(ResponsePtr &&response, Complete complete = nullptr) override diff --git a/test_package/CMakeLists.txt b/test_package/CMakeLists.txt index ec596101..b0fb4c83 100644 --- a/test_package/CMakeLists.txt +++ b/test_package/CMakeLists.txt @@ -245,6 +245,7 @@ add_agent_test(qname FALSE entity) add_agent_test(file_cache FALSE sink/rest_sink) add_agent_test(http_server FALSE sink/rest_sink TRUE) +add_agent_test(websockets FALSE sink/rest_sink TRUE) add_agent_test(tls_http_server FALSE sink/rest_sink TRUE) add_agent_test(routing FALSE sink/rest_sink) diff --git a/test_package/websockets_test.cpp b/test_package/websockets_test.cpp new file mode 100644 index 00000000..0ca15442 --- /dev/null +++ b/test_package/websockets_test.cpp @@ -0,0 +1,246 @@ +// +// Copyright Copyright 2009-2024, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Ensure that gtest is the first header otherwise Windows raises an error +#include +// Keep this comment to keep gtest.h above. (clang-format off/on is not working here!) + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "mtconnect/logging.hpp" +#include "mtconnect/sink/rest_sink/server.hpp" + +using namespace std; +using namespace mtconnect; +using namespace mtconnect::sink::rest_sink; + +namespace asio = boost::asio; +namespace beast = boost::beast; +namespace http = boost::beast::http; +using tcp = boost::asio::ip::tcp; +namespace websocket = beast::websocket; + +// main +int main(int argc, char* argv[]) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} + +class Client +{ +public: + Client(asio::io_context& ioc) : m_context(ioc), m_stream(ioc) {} + + ~Client() { close(); } + + void fail(beast::error_code ec, char const* what) + { + LOG(error) << what << ": " << ec.message() << "\n"; + m_done = true; + m_ec = ec; + } + + void connect(unsigned short port, asio::yield_context yield) + { + beast::error_code ec; + + // These objects perform our I/O + tcp::endpoint server(asio::ip::address_v4::from_string("127.0.0.1"), port); + + // Make the connection on the IP address we get from a lookup + beast::get_lowest_layer(m_stream).async_connect(server, yield[ec]); + + if (ec) + { + return fail(ec, "connect"); + } + + m_stream.set_option(websocket::stream_base::timeout::suggested(beast::role_type::client)); + + m_stream.set_option(websocket::stream_base::decorator([](websocket::request_type& req) { + req.set(http::field::user_agent, + std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client"); + })); + + string host = "127.0.0.1:" + std::to_string(port); + m_stream.async_handshake(host, "/", yield[ec]); + + if (ec) + { + return fail(ec, "connect"); + } + + m_connected = true; + + m_stream.async_read(m_buffer, beast::bind_front_handler(&Client::onRead, this)); + } + + void onRead(beast::error_code ec, std::size_t bytes_transferred) + { + m_result = beast::buffers_to_string(m_buffer.data()); + m_buffer.consume(m_buffer.size()); + + m_done = true; + } + + void request(const string& payload, asio::yield_context yield) + { + cout << "spawnRequest: done: false" << endl; + m_done = false; + beast::error_code ec; + + m_stream.async_write(asio::buffer(payload), yield[ec]); + + waitFor(2s, [this]() { return m_done; }); + } + + template + bool waitFor(const chrono::duration& time, function pred) + { + boost::asio::steady_timer timer(m_context); + timer.expires_from_now(time); + bool timeout = false; + timer.async_wait([&timeout](boost::system::error_code ec) { + if (!ec) + { + timeout = true; + } + }); + + while (!timeout && !pred()) + { + m_context.run_for(500ms); + } + timer.cancel(); + + return pred(); + } + + void close() + { + beast::error_code ec; + + // Gracefully close the socket + m_stream.next_layer().shutdown(tcp::socket::shutdown_both, ec); + } + + bool m_connected {false}; + int m_status; + std::string m_result; + asio::io_context& m_context; + bool m_done {false}; + websocket::stream m_stream; + beast::flat_buffer m_buffer; + boost::beast::error_code m_ec; + beast::flat_buffer m_b; + int m_count {0}; +}; + +class WebsocketsTest : public testing::Test +{ +protected: + void SetUp() override + { + using namespace mtconnect::configuration; + m_server = make_unique(m_context, ConfigOptions {{Port, 0}, {ServerIp, "127.0.0.1"s}}); + } + + void createServer(const ConfigOptions& options) + { + using namespace mtconnect::configuration; + ConfigOptions opts {{Port, 0}, {ServerIp, "127.0.0.1"s}}; + opts.merge(ConfigOptions(options)); + m_server = make_unique(m_context, opts); + } + + void start() + { + m_server->start(); + while (!m_server->isListening()) + m_context.run_one(); + m_client = make_unique(m_context); + } + + void startClient() + { + m_client->m_connected = false; + asio::spawn(m_context, + std::bind(&Client::connect, m_client.get(), + static_cast(m_server->getPort()), std::placeholders::_1)); + + m_client->waitFor(1s, [this]() { return m_client->m_connected; }); + } + + void TearDown() override + { + m_server.reset(); + m_client.reset(); + } + + asio::io_context m_context; + unique_ptr m_server; + unique_ptr m_client; +}; + +TEST_F(WebsocketsTest, should_connect_to_server) +{ + start(); + startClient(); + + ASSERT_TRUE(m_client->m_connected); +} + +TEST_F(WebsocketsTest, should_make_simple_request) +{ + weak_ptr savedSession; + + auto probe = [&](SessionPtr session, RequestPtr request) -> bool { + savedSession = session; + ResponsePtr resp = make_unique(status::ok); + resp->m_body = "All Devices for "s + *request->m_requestId; + resp->m_requestId = request->m_requestId; + session->writeResponse(std::move(resp), []() { cout << "Written" << endl; }); + return true; + }; + + m_server->addRouting({boost::beast::http::verb::get, "/probe", probe}).command("probe"); + m_server->addCommands(); + + start(); + startClient(); + + asio::spawn(m_context, std::bind(&Client::request, m_client.get(), + "{\"id\":\"1\",\"request\":\"probe\"}"s, std::placeholders::_1)); + + m_client->waitFor(2s, [this]() { return m_client->m_done; }); + + ASSERT_TRUE(m_client->m_done); + ASSERT_EQ("All Devices for 1", m_client->m_result); +} From dfdf1d98408f915d2733cca4348bfbd65f24c47d Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Thu, 2 May 2024 21:15:39 -0400 Subject: [PATCH 30/35] cast to parameter value --- .../sink/rest_sink/websocket_session.hpp | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp index 84eda57e..34ee931e 100644 --- a/src/mtconnect/sink/rest_sink/websocket_session.hpp +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -362,10 +362,12 @@ namespace mtconnect::sink::rest_sink { // Skip nulls break; case rapidjson::kFalseType: - request->m_parameters.emplace(make_pair(it.name.GetString(), false)); + request->m_parameters.emplace( + make_pair(it.name.GetString(), ParameterValue(bool(false)))); break; case rapidjson::kTrueType: - request->m_parameters.emplace(make_pair(it.name.GetString(), true)); + request->m_parameters.emplace( + make_pair(it.name.GetString(), ParameterValue(bool(true)))); break; case rapidjson::kObjectType: break; @@ -373,24 +375,25 @@ namespace mtconnect::sink::rest_sink { break; case rapidjson::kStringType: request->m_parameters.emplace( - make_pair(it.name.GetString(), string(it.value.GetString()))); + make_pair(it.name.GetString(), ParameterValue(it.value.GetString()))); break; case rapidjson::kNumberType: if (it.value.Is()) - request->m_parameters.emplace(make_pair(it.name.GetString(), it.value.Get())); + request->m_parameters.emplace( + make_pair(it.name.GetString(), ParameterValue(it.value.Get()))); else if (it.value.Is()) request->m_parameters.emplace( - make_pair(it.name.GetString(), it.value.Get())); + make_pair(it.name.GetString(), ParameterValue(it.value.Get()))); else if (it.value.Is()) - request->m_parameters.emplace( - make_pair(it.name.GetString(), (uint64_t)it.value.Get())); + request->m_parameters.emplace(make_pair( + it.name.GetString(), ParameterValue((uint64_t)it.value.Get()))); else if (it.value.Is()) request->m_parameters.emplace( - make_pair(it.name.GetString(), it.value.Get())); + make_pair(it.name.GetString(), ParameterValue(it.value.Get()))); else if (it.value.Is()) request->m_parameters.emplace( - make_pair(it.name.GetString(), it.value.Get())); + make_pair(it.name.GetString(), ParameterValue(it.value.Get()))); break; } From e80975b862ab748bb84548931d9d062872e63a19 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Thu, 2 May 2024 22:48:40 -0400 Subject: [PATCH 31/35] windows port of websockets --- agent_lib/CMakeLists.txt | 2 +- .../sink/rest_sink/websocket_session.hpp | 36 +++++++++++-------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/agent_lib/CMakeLists.txt b/agent_lib/CMakeLists.txt index c3928467..03ca6fd8 100644 --- a/agent_lib/CMakeLists.txt +++ b/agent_lib/CMakeLists.txt @@ -335,8 +335,8 @@ if(MSVC) # The modules including Beast required the /bigobj option in Windows set_property(SOURCE + "${SOURCE_DIR}/sink/mqtt_sink/mqtt_legacy_service.cpp" "${SOURCE_DIR}/sink/mqtt_sink/mqtt_service.cpp" - "${SOURCE_DIR}/sink/mqtt_sink/mqtt2_service.cpp" "${SOURCE_DIR}/sink/rest_sink/session_impl.cpp" "${SOURCE_DIR}/source/adapter/mqtt/mqtt_adapter.cpp" "${SOURCE_DIR}/source/adapter/agent_adapter/agent_adapter.cpp" diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp index 34ee931e..7a84bfbc 100644 --- a/src/mtconnect/sink/rest_sink/websocket_session.hpp +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -352,7 +352,15 @@ namespace mtconnect::sink::rest_sink { request->m_verb = beast::http::verb::get; request->m_parameters.clear(); +#ifdef GetObject +#define __GOSave__ GetObject +#undef GetObject +#endif + const auto &object = doc.GetObject(); +#ifdef __GOSave__ +#define GetObject __GOSave__ +#endif for (auto &it : object) { @@ -363,11 +371,11 @@ namespace mtconnect::sink::rest_sink { break; case rapidjson::kFalseType: request->m_parameters.emplace( - make_pair(it.name.GetString(), ParameterValue(bool(false)))); + make_pair(string(it.name.GetString()), ParameterValue(false))); break; case rapidjson::kTrueType: request->m_parameters.emplace( - make_pair(it.name.GetString(), ParameterValue(bool(true)))); + make_pair(string(it.name.GetString()), ParameterValue(true))); break; case rapidjson::kObjectType: break; @@ -375,25 +383,25 @@ namespace mtconnect::sink::rest_sink { break; case rapidjson::kStringType: request->m_parameters.emplace( - make_pair(it.name.GetString(), ParameterValue(it.value.GetString()))); + make_pair(it.name.GetString(), ParameterValue(string(it.value.GetString())))); break; case rapidjson::kNumberType: - if (it.value.Is()) + if (it.value.IsInt()) + request->m_parameters.emplace( + make_pair(it.name.GetString(), ParameterValue(it.value.GetInt()))); + else if (it.value.IsUint()) request->m_parameters.emplace( - make_pair(it.name.GetString(), ParameterValue(it.value.Get()))); - else if (it.value.Is()) + make_pair(it.name.GetString(), ParameterValue(uint64_t(it.value.GetUint())))); + else if (it.value.IsInt64()) request->m_parameters.emplace( - make_pair(it.name.GetString(), ParameterValue(it.value.Get()))); - else if (it.value.Is()) - request->m_parameters.emplace(make_pair( - it.name.GetString(), ParameterValue((uint64_t)it.value.Get()))); - else if (it.value.Is()) + make_pair(it.name.GetString(), ParameterValue(uint64_t(it.value.GetInt64())))); + else if (it.value.IsUint64()) request->m_parameters.emplace( - make_pair(it.name.GetString(), ParameterValue(it.value.Get()))); - else if (it.value.Is()) + make_pair(it.name.GetString(), ParameterValue(it.value.GetUint64()))); + else if (it.value.IsDouble()) request->m_parameters.emplace( - make_pair(it.name.GetString(), ParameterValue(it.value.Get()))); + make_pair(it.name.GetString(), ParameterValue(double(it.value.GetDouble())))); break; } From eb350cb36fff661563839b646b46bc78d5422444 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Fri, 3 May 2024 04:44:30 -0400 Subject: [PATCH 32/35] removed named scope from websocket session --- src/mtconnect/sink/rest_sink/websocket_session.hpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp index 7a84bfbc..c0bd12a2 100644 --- a/src/mtconnect/sink/rest_sink/websocket_session.hpp +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -100,7 +100,7 @@ namespace mtconnect::sink::rest_sink { void close() override { - NAMED_SCOPE("PlainWebsocketSession::close"); + //NAMED_SCOPE("PlainWebsocketSession::close"); if (!m_isOpen) return; @@ -128,7 +128,7 @@ namespace mtconnect::sink::rest_sink { void writeResponse(ResponsePtr &&response, Complete complete = nullptr) override { - NAMED_SCOPE("WebsocketSession::writeResponse"); + //NAMED_SCOPE("WebsocketSession::writeResponse"); if (!response->m_requestId) { boost::system::error_code ec; @@ -140,7 +140,7 @@ namespace mtconnect::sink::rest_sink { void writeFailureResponse(ResponsePtr &&response, Complete complete = nullptr) override { - NAMED_SCOPE("WebsocketSession::writeFailureResponse"); + //NAMED_SCOPE("WebsocketSession::writeFailureResponse"); writeChunk(response->m_body, complete, response->m_requestId); } @@ -175,7 +175,7 @@ namespace mtconnect::sink::rest_sink { void writeChunk(const std::string &chunk, Complete complete, std::optional requestId = std::nullopt) override { - NAMED_SCOPE("WebsocketSession::writeChunk"); + //NAMED_SCOPE("WebsocketSession::writeChunk"); if (!derived().stream().is_open()) { @@ -219,7 +219,7 @@ namespace mtconnect::sink::rest_sink { void send(const std::string body, Complete complete, const std::string &requestId) { - NAMED_SCOPE("WebsocketSession::send"); + //NAMED_SCOPE("WebsocketSession::send"); using namespace std::placeholders; @@ -255,7 +255,7 @@ namespace mtconnect::sink::rest_sink { void sent(beast::error_code ec, std::size_t len, const std::string &id) { - NAMED_SCOPE("WebsocketSession::sent"); + //NAMED_SCOPE("WebsocketSession::sent"); if (ec) { @@ -310,7 +310,7 @@ namespace mtconnect::sink::rest_sink { void onRead(beast::error_code ec, std::size_t len) { - NAMED_SCOPE("PlainWebsocketSession::onRead"); + //NAMED_SCOPE("PlainWebsocketSession::onRead"); if (ec) return fail(boost::beast::http::status::internal_server_error, "shutdown", ec); From 8c92d733663d71879b19436e5339d1a114f009f8 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Fri, 3 May 2024 05:49:44 -0400 Subject: [PATCH 33/35] Added BOOST_USE_WINAPI_VERSION for boost 1.84 --- conan/profiles/vs32 | 1 + conan/profiles/vs32debug | 1 + conan/profiles/vs32shared | 1 + conanfile.py | 3 ++- src/mtconnect/sink/rest_sink/websocket_session.hpp | 14 +++++++------- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/conan/profiles/vs32 b/conan/profiles/vs32 index f78b6c43..a053784c 100644 --- a/conan/profiles/vs32 +++ b/conan/profiles/vs32 @@ -3,6 +3,7 @@ include(default) [settings] compiler=msvc compiler.cppstd=17 +winver=0x0600 arch=x86 compiler.runtime=static compiler.runtime_type=Release diff --git a/conan/profiles/vs32debug b/conan/profiles/vs32debug index 44563790..b725433f 100644 --- a/conan/profiles/vs32debug +++ b/conan/profiles/vs32debug @@ -3,6 +3,7 @@ include(default) [settings] compiler=msvc compiler.cppstd=17 +winver=0x0600 arch=x86 compiler.runtime=static compiler.runtime_type=Debug diff --git a/conan/profiles/vs32shared b/conan/profiles/vs32shared index cac3622e..c8f19481 100644 --- a/conan/profiles/vs32shared +++ b/conan/profiles/vs32shared @@ -4,6 +4,7 @@ include(default) compiler=msvc compiler.cppstd=17 arch=x86 +winver=0x0600 compiler.runtime=dynamic compiler.runtime_type=Release build_type=Release diff --git a/conanfile.py b/conanfile.py index 2c384fb3..60916572 100644 --- a/conanfile.py +++ b/conanfile.py @@ -35,7 +35,7 @@ class MTConnectAgentConan(ConanFile): "with_ruby": True, "development": False, "shared": False, - "winver": "0x600", + "winver": "0x602", "with_docs": False, "cpack": False, "agent_prefix": None, @@ -227,6 +227,7 @@ def package_info(self): winver=str(self.options.winver) self.cpp_info.defines.append("WINVER=" + winver) self.cpp_info.defines.append("_WIN32_WINNT=" + winver) + self.cpp_info.defines.append("BOOST_USE_WINAPI_VERSION=" + winver) def package(self): cmake = CMake(self) diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp index c0bd12a2..7a84bfbc 100644 --- a/src/mtconnect/sink/rest_sink/websocket_session.hpp +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -100,7 +100,7 @@ namespace mtconnect::sink::rest_sink { void close() override { - //NAMED_SCOPE("PlainWebsocketSession::close"); + NAMED_SCOPE("PlainWebsocketSession::close"); if (!m_isOpen) return; @@ -128,7 +128,7 @@ namespace mtconnect::sink::rest_sink { void writeResponse(ResponsePtr &&response, Complete complete = nullptr) override { - //NAMED_SCOPE("WebsocketSession::writeResponse"); + NAMED_SCOPE("WebsocketSession::writeResponse"); if (!response->m_requestId) { boost::system::error_code ec; @@ -140,7 +140,7 @@ namespace mtconnect::sink::rest_sink { void writeFailureResponse(ResponsePtr &&response, Complete complete = nullptr) override { - //NAMED_SCOPE("WebsocketSession::writeFailureResponse"); + NAMED_SCOPE("WebsocketSession::writeFailureResponse"); writeChunk(response->m_body, complete, response->m_requestId); } @@ -175,7 +175,7 @@ namespace mtconnect::sink::rest_sink { void writeChunk(const std::string &chunk, Complete complete, std::optional requestId = std::nullopt) override { - //NAMED_SCOPE("WebsocketSession::writeChunk"); + NAMED_SCOPE("WebsocketSession::writeChunk"); if (!derived().stream().is_open()) { @@ -219,7 +219,7 @@ namespace mtconnect::sink::rest_sink { void send(const std::string body, Complete complete, const std::string &requestId) { - //NAMED_SCOPE("WebsocketSession::send"); + NAMED_SCOPE("WebsocketSession::send"); using namespace std::placeholders; @@ -255,7 +255,7 @@ namespace mtconnect::sink::rest_sink { void sent(beast::error_code ec, std::size_t len, const std::string &id) { - //NAMED_SCOPE("WebsocketSession::sent"); + NAMED_SCOPE("WebsocketSession::sent"); if (ec) { @@ -310,7 +310,7 @@ namespace mtconnect::sink::rest_sink { void onRead(beast::error_code ec, std::size_t len) { - //NAMED_SCOPE("PlainWebsocketSession::onRead"); + NAMED_SCOPE("PlainWebsocketSession::onRead"); if (ec) return fail(boost::beast::http::status::internal_server_error, "shutdown", ec); From b23b28b123bd1a1fa65efec5ebee5f7f3ccbf283 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Fri, 3 May 2024 10:17:57 -0400 Subject: [PATCH 34/35] added boost windows api version for windows 1.84 porting --- conan/profiles/vs32 | 3 ++- conan/profiles/vs32debug | 4 +++- conan/profiles/vs32shared | 2 +- conanfile.py | 5 ++++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/conan/profiles/vs32 b/conan/profiles/vs32 index a053784c..fb406b01 100644 --- a/conan/profiles/vs32 +++ b/conan/profiles/vs32 @@ -3,9 +3,10 @@ include(default) [settings] compiler=msvc compiler.cppstd=17 -winver=0x0600 arch=x86 compiler.runtime=static compiler.runtime_type=Release build_type=Release +[options] +winver=0x0600 diff --git a/conan/profiles/vs32debug b/conan/profiles/vs32debug index b725433f..54344e40 100644 --- a/conan/profiles/vs32debug +++ b/conan/profiles/vs32debug @@ -3,8 +3,10 @@ include(default) [settings] compiler=msvc compiler.cppstd=17 -winver=0x0600 arch=x86 compiler.runtime=static compiler.runtime_type=Debug build_type=Debug + +[options] +winver=0x0600 diff --git a/conan/profiles/vs32shared b/conan/profiles/vs32shared index c8f19481..9bf550db 100644 --- a/conan/profiles/vs32shared +++ b/conan/profiles/vs32shared @@ -4,10 +4,10 @@ include(default) compiler=msvc compiler.cppstd=17 arch=x86 -winver=0x0600 compiler.runtime=dynamic compiler.runtime_type=Release build_type=Release [options] shared=True +winver=0x0600 diff --git a/conanfile.py b/conanfile.py index 60916572..02cd0438 100644 --- a/conanfile.py +++ b/conanfile.py @@ -35,7 +35,7 @@ class MTConnectAgentConan(ConanFile): "with_ruby": True, "development": False, "shared": False, - "winver": "0x602", + "winver": "0x0602", "with_docs": False, "cpack": False, "agent_prefix": None, @@ -139,6 +139,9 @@ def configure(self): if self.options.shared: self.options["boost/*"].shared = True self.package_type = "shared-library" + + if is_msvc(self): + self.options["boost/*"].extra_b2_flags = ("define=BOOST_USE_WINAPI_VERSION=" + str(self.options.winver)) # Make sure shared builds use shared boost if is_msvc(self) and self.options.shared: From ca11039fbf4eabcdcb9a10f0aee02f3df4ea591f Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Fri, 3 May 2024 11:45:33 -0400 Subject: [PATCH 35/35] removed legacy mqtt service --- agent_lib/CMakeLists.txt | 3 - src/mtconnect/configuration/agent_config.cpp | 2 - .../sink/mqtt_sink/mqtt_legacy_service.cpp | 197 ------ .../sink/mqtt_sink/mqtt_legacy_service.hpp | 107 --- src/mtconnect/sink/rest_sink/rest_service.cpp | 4 +- test_package/CMakeLists.txt | 1 - test_package/agent_test_helper.hpp | 23 - test_package/mqtt_isolated_test.cpp | 2 +- test_package/mqtt_legacy_sink_test.cpp | 630 ------------------ test_package/mqtt_sink_test.cpp | 17 +- 10 files changed, 12 insertions(+), 974 deletions(-) delete mode 100644 src/mtconnect/sink/mqtt_sink/mqtt_legacy_service.cpp delete mode 100644 src/mtconnect/sink/mqtt_sink/mqtt_legacy_service.hpp delete mode 100644 test_package/mqtt_legacy_sink_test.cpp diff --git a/agent_lib/CMakeLists.txt b/agent_lib/CMakeLists.txt index 03ca6fd8..9571fc07 100644 --- a/agent_lib/CMakeLists.txt +++ b/agent_lib/CMakeLists.txt @@ -246,12 +246,10 @@ set(AGENT_SOURCES # src/sink/mqtt_sink HEADER_FILE_ONLY - "${SOURCE_DIR}/sink/mqtt_sink/mqtt_legacy_service.hpp" "${SOURCE_DIR}/sink/mqtt_sink/mqtt_service.hpp" #src/sink/mqtt_sink SOURCE_FILES_ONLY - "${SOURCE_DIR}/sink/mqtt_sink/mqtt_legacy_service.cpp" "${SOURCE_DIR}/sink/mqtt_sink/mqtt_service.cpp" # src/sink/rest_sink HEADER_FILE_ONLY @@ -335,7 +333,6 @@ if(MSVC) # The modules including Beast required the /bigobj option in Windows set_property(SOURCE - "${SOURCE_DIR}/sink/mqtt_sink/mqtt_legacy_service.cpp" "${SOURCE_DIR}/sink/mqtt_sink/mqtt_service.cpp" "${SOURCE_DIR}/sink/rest_sink/session_impl.cpp" "${SOURCE_DIR}/source/adapter/mqtt/mqtt_adapter.cpp" diff --git a/src/mtconnect/configuration/agent_config.cpp b/src/mtconnect/configuration/agent_config.cpp index d372df52..e9bf4013 100644 --- a/src/mtconnect/configuration/agent_config.cpp +++ b/src/mtconnect/configuration/agent_config.cpp @@ -58,7 +58,6 @@ #include "mtconnect/configuration/config_options.hpp" #include "mtconnect/device_model/device.hpp" #include "mtconnect/printer/xml_printer.hpp" -#include "mtconnect/sink/mqtt_sink/mqtt_legacy_service.hpp" #include "mtconnect/sink/mqtt_sink/mqtt_service.hpp" #include "mtconnect/sink/rest_sink/rest_service.hpp" #include "mtconnect/source/adapter/agent_adapter/agent_adapter.hpp" @@ -112,7 +111,6 @@ namespace mtconnect::configuration { bool success = false; - sink::mqtt_sink::MqttLegacyService::registerFactory(m_sinkFactory); sink::mqtt_sink::MqttService::registerFactory(m_sinkFactory); sink::rest_sink::RestService::registerFactory(m_sinkFactory); adapter::shdr::ShdrAdapter::registerFactory(m_sourceFactory); diff --git a/src/mtconnect/sink/mqtt_sink/mqtt_legacy_service.cpp b/src/mtconnect/sink/mqtt_sink/mqtt_legacy_service.cpp deleted file mode 100644 index 46aea8a1..00000000 --- a/src/mtconnect/sink/mqtt_sink/mqtt_legacy_service.cpp +++ /dev/null @@ -1,197 +0,0 @@ -// -// Copyright Copyright 2009-2024, AMT – The Association For Manufacturing Technology (“AMT”) -// All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#include "mqtt_legacy_service.hpp" - -#include "mtconnect/configuration/config_options.hpp" -#include "mtconnect/entity/entity.hpp" -#include "mtconnect/entity/factory.hpp" -#include "mtconnect/entity/json_parser.hpp" -#include "mtconnect/mqtt/mqtt_client_impl.hpp" -#include "mtconnect/printer/json_printer.hpp" - -using ptree = boost::property_tree::ptree; - -using namespace std; -using namespace mtconnect::asset; - -namespace asio = boost::asio; -namespace config = ::mtconnect::configuration; - -namespace mtconnect { - namespace sink { - namespace mqtt_sink { - // get obeservation in - // create a json printer - // call print - - MqttLegacyService::MqttLegacyService(boost::asio::io_context &context, - sink::SinkContractPtr &&contract, - const ConfigOptions &options, const ptree &config) - : Sink("MqttLegacyService", std::move(contract)), m_context(context), m_options(options) - { - auto jsonPrinter = dynamic_cast(m_sinkContract->getPrinter("json")); - m_jsonPrinter = make_unique(jsonPrinter->getJsonVersion()); - - GetOptions(config, m_options, options); - AddOptions(config, m_options, - {{configuration::ProbeTopic, string()}, - {configuration::MqttCaCert, string()}, - {configuration::MqttPrivateKey, string()}, - {configuration::MqttCert, string()}, - {configuration::MqttUserName, string()}, - {configuration::MqttPassword, string()}, - {configuration::MqttClientId, string()}}); - AddDefaultedOptions(config, m_options, - {{configuration::MqttHost, "127.0.0.1"s}, - {configuration::DeviceTopic, "MTConnect/Device/"s}, - {configuration::AssetTopic, "MTConnect/Asset/"s}, - {configuration::ObservationTopic, "MTConnect/Observation/"s}, - {configuration::MqttPort, 1883}, - {configuration::MqttTls, false}}); - - auto clientHandler = make_unique(); - clientHandler->m_connected = [this](shared_ptr client) { - // Publish latest devices, assets, and observations - auto &circ = m_sinkContract->getCircularBuffer(); - std::lock_guard lock(circ); - client->connectComplete(); - - for (auto &dev : m_sinkContract->getDevices()) - { - publish(dev); - } - - auto obsList {circ.getLatest().getObservations()}; - for (auto &obs : obsList) - { - observation::ObservationPtr p {obs.second}; - publish(p); - } - - AssetList list; - m_sinkContract->getAssetStorage()->getAssets(list, 100000); - for (auto &asset : list) - { - publish(asset); - } - }; - - m_devicePrefix = GetOption(m_options, configuration::ProbeTopic) - .value_or(get(m_options[configuration::DeviceTopic])); - m_assetPrefix = get(m_options[configuration::AssetTopic]); - m_observationPrefix = get(m_options[configuration::ObservationTopic]); - - if (IsOptionSet(m_options, configuration::MqttTls)) - { - m_client = make_shared(m_context, m_options, std::move(clientHandler)); - } - else - { - m_client = make_shared(m_context, m_options, std::move(clientHandler)); - } - } - - void MqttLegacyService::start() - { - // mqtt client side not a server side... - if (!m_client) - return; - - m_client->start(); - } - - void MqttLegacyService::stop() - { - // stop client side - if (m_client) - m_client->stop(); - } - - std::shared_ptr MqttLegacyService::getClient() { return m_client; } - - bool MqttLegacyService::publish(observation::ObservationPtr &observation) - { - // get the data item from observation - if (observation->isOrphan()) - return false; - - DataItemPtr dataItem = observation->getDataItem(); - - auto topic = m_observationPrefix + dataItem->getTopic(); // client asyn topic - auto content = dataItem->getTopicName(); // client asyn content - - // We may want to use the observation from the checkpoint. - string doc; - if (observation->getDataItem()->isCondition()) - { - doc = m_jsonPrinter->print(observation); - } - else - { - doc = m_jsonPrinter->printEntity(observation); - } - - if (m_client) - m_client->publish(topic, doc); - - return true; - } - - bool MqttLegacyService::publish(device_model::DevicePtr device) - { - auto topic = m_devicePrefix + *device->getUuid(); - auto doc = m_jsonPrinter->print(device); - - stringstream buffer; - buffer << doc; - - if (m_client) - m_client->publish(topic, buffer.str()); - - return true; - } - - bool MqttLegacyService::publish(asset::AssetPtr asset) - { - auto topic = m_assetPrefix + get(asset->getIdentity()); - auto doc = m_jsonPrinter->print(asset); - - stringstream buffer; - buffer << doc; - - if (m_client) - m_client->publish(topic, buffer.str()); - - return true; - } - - // Register the service with the sink factory - void MqttLegacyService::registerFactory(SinkFactory &factory) - { - factory.registerFactory( - "MqttLegacyService", - [](const std::string &name, boost::asio::io_context &io, SinkContractPtr &&contract, - const ConfigOptions &options, const boost::property_tree::ptree &block) -> SinkPtr { - auto sink = - std::make_shared(io, std::move(contract), options, block); - return sink; - }); - } - } // namespace mqtt_sink - } // namespace sink -} // namespace mtconnect diff --git a/src/mtconnect/sink/mqtt_sink/mqtt_legacy_service.hpp b/src/mtconnect/sink/mqtt_sink/mqtt_legacy_service.hpp deleted file mode 100644 index ab84d97d..00000000 --- a/src/mtconnect/sink/mqtt_sink/mqtt_legacy_service.hpp +++ /dev/null @@ -1,107 +0,0 @@ -// -// Copyright Copyright 2009-2024, AMT – The Association For Manufacturing Technology (“AMT”) -// All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#pragma once - -#include "boost/asio/io_context.hpp" -#include - -#include "mtconnect/buffer/checkpoint.hpp" -#include "mtconnect/config.hpp" -#include "mtconnect/configuration/agent_config.hpp" -#include "mtconnect/entity/json_printer.hpp" -#include "mtconnect/mqtt/mqtt_client.hpp" -#include "mtconnect/observation/observation.hpp" -#include "mtconnect/printer/printer.hpp" -#include "mtconnect/printer/xml_printer_helper.hpp" -#include "mtconnect/sink/sink.hpp" -#include "mtconnect/utilities.hpp" - -using namespace std; -using namespace mtconnect::entity; -using namespace mtconnect::mqtt_client; - -namespace mtconnect { - class XmlPrinter; - - namespace sink { - - /// @brief MTConnect Mqtt implemention namespace - - namespace mqtt_sink { - class AGENT_LIB_API MqttLegacyService : public sink::Sink - { - // dynamic loading of sink - - public: - /// @brief Create a Mqtt Service sink - /// @param context the boost asio io_context - /// @param contract the Sink Contract from the agent - /// @param options configuration options - /// @param config additional configuration options if specified directly as a sink - MqttLegacyService(boost::asio::io_context &context, sink::SinkContractPtr &&contract, - const ConfigOptions &options, const boost::property_tree::ptree &config); - - ~MqttLegacyService() = default; - - // Sink Methods - /// @brief Start the Mqtt service - void start() override; - - /// @brief Shutdown the Mqtt service - void stop() override; - - /// @brief Receive an observation - /// @param observation shared pointer to the observation - /// @return `true` if the publishing was successful - bool publish(observation::ObservationPtr &observation) override; - - /// @brief Receive an asset - /// @param asset shared point to the asset - /// @return `true` if successful - bool publish(asset::AssetPtr asset) override; - - /// @brief Receive a device - /// @param device shared pointer to the device - /// @return `true` if successful - bool publish(device_model::DevicePtr device) override; - - /// @brief Register the Sink factory to create this sink - /// @param factory - static void registerFactory(SinkFactory &factory); - - /// @brief gets a Mqtt Client - /// @return MqttClient - std::shared_ptr getClient(); - - /// @brief Mqtt Client is Connected or not - /// @return `true` when the client was connected - bool isConnected() { return m_client && m_client->isConnected(); } - - protected: - std::string m_devicePrefix; - std::string m_assetPrefix; - std::string m_observationPrefix; - - boost::asio::io_context &m_context; - ConfigOptions m_options; - std::unique_ptr m_jsonPrinter; - std::shared_ptr m_client; - }; - } // namespace mqtt_sink - } // namespace sink -} // namespace mtconnect diff --git a/src/mtconnect/sink/rest_sink/rest_service.cpp b/src/mtconnect/sink/rest_sink/rest_service.cpp index 82c1df6c..21086547 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.cpp +++ b/src/mtconnect/sink/rest_sink/rest_service.cpp @@ -531,7 +531,7 @@ namespace mtconnect { auto handler = [&](SessionPtr session, RequestPtr request) -> bool { auto removed = *request->parameter("removed"); auto count = *request->parameter("count"); - auto pretty = *request->parameter("pretty"); + auto pretty = request->parameter("pretty").value_or(false); auto format = request->parameter("format"); auto printer = getPrinter(request->m_accepts, format); @@ -546,7 +546,7 @@ namespace mtconnect { auto asset = request->parameter("assetIds"); if (asset) { - auto pretty = *request->parameter("pretty"); + auto pretty = request->parameter("pretty").value_or(false); auto printer = m_sinkContract->getPrinter(acceptFormat(request->m_accepts)); list ids; diff --git a/test_package/CMakeLists.txt b/test_package/CMakeLists.txt index b0fb4c83..05427a32 100644 --- a/test_package/CMakeLists.txt +++ b/test_package/CMakeLists.txt @@ -250,7 +250,6 @@ add_agent_test(tls_http_server FALSE sink/rest_sink TRUE) add_agent_test(routing FALSE sink/rest_sink) add_agent_test(mqtt_isolated FALSE mqtt_isolated TRUE) -add_agent_test(mqtt_legacy_sink FALSE sink/mqtt_legacy_sink TRUE) add_agent_test(mqtt_sink FALSE sink/mqtt_sink TRUE) add_agent_test(json_printer_asset TRUE json) diff --git a/test_package/agent_test_helper.hpp b/test_package/agent_test_helper.hpp index 4d16eb47..16c7a5cf 100644 --- a/test_package/agent_test_helper.hpp +++ b/test_package/agent_test_helper.hpp @@ -29,7 +29,6 @@ #include "mtconnect/configuration/agent_config.hpp" #include "mtconnect/configuration/config_options.hpp" #include "mtconnect/pipeline/pipeline.hpp" -#include "mtconnect/sink/mqtt_sink/mqtt_legacy_service.hpp" #include "mtconnect/sink/mqtt_sink/mqtt_service.hpp" #include "mtconnect/sink/rest_sink/response.hpp" #include "mtconnect/sink/rest_sink/rest_service.hpp" @@ -124,7 +123,6 @@ class AgentTestHelper ~AgentTestHelper() { - m_mqttLegacyService.reset(); m_mqttService.reset(); m_restService.reset(); m_adapter.reset(); @@ -171,15 +169,6 @@ class AgentTestHelper return rest; } - std::shared_ptr getMqttLegacyService() - { - using namespace mtconnect; - sink::SinkPtr sink = m_agent->findSink("MqttLegacyService"); - std::shared_ptr mqtt = - std::dynamic_pointer_cast(sink); - return mqtt; - } - std::shared_ptr getMqttService() { using namespace mtconnect; @@ -199,7 +188,6 @@ class AgentTestHelper using ptree = boost::property_tree::ptree; sink::rest_sink::RestService::registerFactory(m_sinkFactory); - sink::mqtt_sink::MqttLegacyService::registerFactory(m_sinkFactory); sink::mqtt_sink::MqttService::registerFactory(m_sinkFactory); source::adapter::shdr::ShdrAdapter::registerFactory(m_sourceFactory); @@ -232,16 +220,6 @@ class AgentTestHelper m_restService = std::dynamic_pointer_cast(sink); m_agent->addSink(m_restService); - if (HasOption(options, "MqttLegacySink")) - { - auto mqttContract = m_agent->makeSinkContract(); - mqttContract->m_pipelineContext = m_context; - auto mqttsink = m_sinkFactory.make("MqttLegacyService", "MqttLegacyService", m_ioContext, - std::move(mqttContract), options, ptree {}); - m_mqttLegacyService = std::dynamic_pointer_cast(mqttsink); - m_agent->addSink(m_mqttLegacyService); - } - if (HasOption(options, "MqttSink")) { auto mqttContract = m_agent->makeSinkContract(); @@ -324,7 +302,6 @@ class AgentTestHelper mhttp::Server *m_server {nullptr}; std::shared_ptr m_context; std::shared_ptr m_adapter; - std::shared_ptr m_mqttLegacyService; std::shared_ptr m_mqttService; std::shared_ptr m_restService; std::shared_ptr m_loopback; diff --git a/test_package/mqtt_isolated_test.cpp b/test_package/mqtt_isolated_test.cpp index 4e7cbc56..9789436c 100644 --- a/test_package/mqtt_isolated_test.cpp +++ b/test_package/mqtt_isolated_test.cpp @@ -181,7 +181,7 @@ class MqttIsolatedUnitTest : public testing::Test std::unique_ptr m_jsonPrinter; std::shared_ptr m_server; std::shared_ptr m_client; - std::shared_ptr m_service; + std::shared_ptr m_service; std::unique_ptr m_agentTestHelper; uint16_t m_port {0}; }; diff --git a/test_package/mqtt_legacy_sink_test.cpp b/test_package/mqtt_legacy_sink_test.cpp deleted file mode 100644 index 4647c924..00000000 --- a/test_package/mqtt_legacy_sink_test.cpp +++ /dev/null @@ -1,630 +0,0 @@ -// -// Copyright Copyright 2009-2024, AMT – The Association For Manufacturing Technology (“AMT”) -// All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -/// @file -/// Test MQTT 1 Service - -// Ensure that gtest is the first header otherwise Windows raises an error -#include -// Keep this comment to keep gtest.h above. (clang-format off/on is not working here!) - -#include - -#include - -#include "agent_test_helper.hpp" -#include "mtconnect/buffer/checkpoint.hpp" -#include "mtconnect/device_model/data_item/data_item.hpp" -#include "mtconnect/entity/entity.hpp" -#include "mtconnect/entity/json_parser.hpp" -#include "mtconnect/mqtt/mqtt_client_impl.hpp" -#include "mtconnect/mqtt/mqtt_server_impl.hpp" -#include "mtconnect/printer//json_printer.hpp" -#include "mtconnect/sink/mqtt_sink/mqtt_legacy_service.hpp" - -using namespace std; -using namespace mtconnect; -using namespace mtconnect::device_model::data_item; -using namespace mtconnect::sink::mqtt_sink; -using namespace mtconnect::asset; -using namespace mtconnect::configuration; - -// main -int main(int argc, char *argv[]) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} - -using json = nlohmann::json; - -class MqttSinkTest : public testing::Test -{ -protected: - void SetUp() override - { - m_agentTestHelper = make_unique(); - m_jsonPrinter = std::make_unique(2, true); - } - - void TearDown() override - { - const auto agent = m_agentTestHelper->getAgent(); - if (agent) - { - m_agentTestHelper->getAgent()->stop(); - m_agentTestHelper->m_ioContext.run_for(100ms); - } - if (m_client) - { - m_client->stop(); - m_agentTestHelper->m_ioContext.run_for(100ms); - m_client.reset(); - } - if (m_server) - { - m_server->stop(); - m_agentTestHelper->m_ioContext.run_for(500ms); - m_server.reset(); - } - m_agentTestHelper.reset(); - m_jsonPrinter.reset(); - } - - void createAgent(std::string testFile = {}, ConfigOptions options = {}) - { - if (testFile == "") - testFile = "/samples/test_config.xml"; - - ConfigOptions opts(options); - MergeOptions(opts, {{"MqttLegacySink", true}, - {configuration::MqttPort, m_port}, - {configuration::MqttHost, "127.0.0.1"s}}); - m_agentTestHelper->createAgent(testFile, 8, 4, "2.0", 25, false, true, opts); - addAdapter(); - - m_agentTestHelper->getAgent()->start(); - } - - void createServer(const ConfigOptions &options) - { - using namespace mtconnect::configuration; - ConfigOptions opts(options); - MergeOptions(opts, {{ServerIp, "127.0.0.1"s}, - {MqttPort, 0}, - {MqttTls, false}, - {AutoAvailable, false}, - {RealTime, false}}); - - m_server = - make_shared(m_agentTestHelper->m_ioContext, opts); - } - - template - bool waitFor(const chrono::duration &time, function pred) - { - boost::asio::steady_timer timer(m_agentTestHelper->m_ioContext); - timer.expires_from_now(time); - bool timeout = false; - timer.async_wait([&timeout](boost::system::error_code ec) { - if (!ec) - { - timeout = true; - } - }); - - while (!timeout && !pred()) - { - m_agentTestHelper->m_ioContext.run_for(100ms); - } - timer.cancel(); - - return pred(); - } - - void startServer() - { - if (m_server) - { - bool start = m_server->start(); - if (start) - { - m_port = m_server->getPort(); - m_agentTestHelper->m_ioContext.run_for(500ms); - } - } - } - - void createClient(const ConfigOptions &options, unique_ptr &&handler) - { - ConfigOptions opts(options); - MergeOptions(opts, {{MqttHost, "127.0.0.1"s}, - {MqttPort, m_port}, - {MqttTls, false}, - {AutoAvailable, false}, - {RealTime, false}}); - m_client = make_shared(m_agentTestHelper->m_ioContext, - opts, std::move(handler)); - } - - bool startClient() - { - bool started = m_client && m_client->start(); - if (started) - { - return waitFor(5s, [this]() { return m_client->isConnected(); }); - } - return started; - } - - void addAdapter(ConfigOptions options = ConfigOptions {}) - { - m_agentTestHelper->addAdapter(options, "localhost", 0, - m_agentTestHelper->m_agent->getDefaultDevice()->getName()); - } - - std::unique_ptr m_jsonPrinter; - std::shared_ptr m_server; - std::shared_ptr m_client; - std::shared_ptr m_service; - std::unique_ptr m_agentTestHelper; - uint16_t m_port {0}; -}; - -TEST_F(MqttSinkTest, mqtt_sink_should_be_loaded_by_agent) -{ - createAgent(); - auto service = m_agentTestHelper->getMqttLegacyService(); - - ASSERT_TRUE(service); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_connect_to_broker) -{ - ConfigOptions options; - createServer(options); - startServer(); - - ASSERT_NE(0, m_port); - - createAgent(); - auto service = m_agentTestHelper->getMqttLegacyService(); - - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_connect_to_broker_with_UserNameandPassword) -{ - ConfigOptions options {{MqttUserName, "MQTT-SINK"s}, {MqttPassword, "mtconnect"s}}; - createServer(options); - startServer(); - - ASSERT_NE(0, m_port); - - createAgent("", options); - auto service = m_agentTestHelper->getMqttLegacyService(); - - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_connect_to_broker_without_UserNameandPassword) -{ - ConfigOptions options; - createServer(options); - startServer(); - - ASSERT_NE(0, m_port); - - createAgent(); - auto service = m_agentTestHelper->getMqttLegacyService(); - - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_publish_device) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - auto handler = make_unique(); - bool gotDevice = false; - handler->m_receive = [&gotDevice, &parser](std::shared_ptr client, - const std::string &topic, const std::string &payload) { - EXPECT_EQ("MTConnect/Device/000", topic); - - ErrorList list; - auto ptr = parser.parse(device_model::Device::getRoot(), payload, "2.0", list); - EXPECT_EQ(0, list.size()); - auto dev = dynamic_pointer_cast(ptr); - EXPECT_TRUE(dev); - EXPECT_EQ("LinuxCNC", dev->getComponentName()); - EXPECT_EQ("000", *dev->getUuid()); - - gotDevice = true; - }; - - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - m_client->subscribe("MTConnect/Device/000"); - - createAgent(); - - auto service = m_agentTestHelper->getMqttLegacyService(); - - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - - ASSERT_TRUE(waitFor(5s, [&gotDevice]() { return gotDevice; })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_publish_Streams) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - auto handler = make_unique(); - bool foundLineDataItem = false; - handler->m_receive = [&foundLineDataItem](std::shared_ptr client, - const std::string &topic, const std::string &payload) { - EXPECT_EQ("MTConnect/Observation/000/Controller[Controller]/Path/Events/Line[line]", topic); - - auto jdoc = json::parse(payload); - string value = jdoc.at("/value"_json_pointer).get(); - EXPECT_EQ("204", value); - foundLineDataItem = true; - }; - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - - createAgent(); - auto service = m_agentTestHelper->getMqttLegacyService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - - m_client->subscribe("MTConnect/Observation/000/Controller[Controller]/Path/Events/Line[line]"); - m_agentTestHelper->m_adapter->processData("2021-02-01T12:00:00Z|line|204"); - - ASSERT_TRUE(waitFor(5s, [&foundLineDataItem]() { return foundLineDataItem; })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_publish_Asset) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - auto handler = make_unique(); - bool gotControllerDataItem = false; - handler->m_receive = [&gotControllerDataItem](std::shared_ptr, - const std::string &topic, - const std::string &payload) { - EXPECT_EQ("MTConnect/Asset/0001", topic); - auto jdoc = json::parse(payload); - string id = jdoc.at("/Part/assetId"_json_pointer).get(); - EXPECT_EQ("0001", id); - gotControllerDataItem = true; - }; - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - - createAgent(); - auto service = m_agentTestHelper->getMqttLegacyService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe("MTConnect/Asset/0001"); - - m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|@ASSET@|@1|Part|TEST 1"); - - ASSERT_TRUE(waitFor(5s, [&gotControllerDataItem]() { return gotControllerDataItem; })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_publish_RotaryMode) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - auto handler = make_unique(); - bool gotRotaryMode = false; - handler->m_receive = [&gotRotaryMode](std::shared_ptr, const std::string &topic, - const std::string &payload) { - EXPECT_EQ("MTConnect/Observation/000/Axes[Axes]/Rotary[C]/Samples/SpindleSpeed.Actual[Sspeed]", - topic); - auto jdoc = json::parse(payload); - - double v = jdoc.at("/value"_json_pointer).get(); - EXPECT_EQ(5000.0, v); - gotRotaryMode = true; - }; - - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - - createAgent(); - auto service = m_agentTestHelper->getMqttLegacyService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe( - "MTConnect/Observation/000/Axes[Axes]/Rotary[C]/Samples/SpindleSpeed.Actual[Sspeed]"); - - m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|block|G01X00|Sspeed|5000|line|204"); - - ASSERT_TRUE(waitFor(5s, [&gotRotaryMode]() { return gotRotaryMode; })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_publish_Dataset) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - entity::JsonParser parser; - auto handler = make_unique(); - bool gotControllerDataItem = false; - handler->m_receive = [&gotControllerDataItem](std::shared_ptr, - const std::string &topic, - const std::string &payload) { - EXPECT_EQ( - "MTConnect/Observation/000/Controller[Controller]/Path[path]/Events/VariableDataSet[vars]", - topic); - auto jdoc = json::parse(payload); - auto id = jdoc.at("/value/a"_json_pointer).get(); - EXPECT_EQ(1, id); - gotControllerDataItem = true; - }; - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - createAgent("/samples/data_set.xml"); - auto service = m_agentTestHelper->getMqttLegacyService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe( - "MTConnect/Observation/000/Controller[Controller]/Path[path]/Events/VariableDataSet[vars]"); - - m_agentTestHelper->m_adapter->processData("TIME|vars|a=1 b=2 c=3"); - ASSERT_TRUE(waitFor(5s, [&gotControllerDataItem]() { return gotControllerDataItem; })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_publish_Table) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - entity::JsonParser parser; - auto handler = make_unique(); - bool gotControllerDataItem = false; - handler->m_receive = [&gotControllerDataItem](std::shared_ptr, - const std::string &topic, - const std::string &payload) { - EXPECT_EQ( - "MTConnect/Observation/000/Controller[Controller]/Path[path]/Events/WorkOffsetTable[wpo]", - topic); - auto jdoc = json::parse(payload); - - auto jValue = jdoc.at("/value"_json_pointer); - int count = 0; - if (jValue.is_object()) - { - for (auto &[key, value] : jValue.items()) - { - if (key == "G53.1" || key == "G53.2" || key == "G53.3") - { - for (auto &[subKey, subValue] : value.items()) - { - if (key == "G53.1" && ((subKey == "X" && subValue.get() == 1) || - (subKey == "Y" && subValue.get() == 2) || - (subKey == "Z" && subValue.get() == 3))) - { - count++; - } - else if (key == "G53.2" && ((subKey == "X" && subValue.get() == 4) || - (subKey == "Y" && subValue.get() == 5) || - (subKey == "Z" && subValue.get() == 6))) - { - count++; - } - else if (key == "G53.3" && ((subKey == "X" && subValue.get() == 7.0) || - (subKey == "Y" && subValue.get() == 8.0) || - (subKey == "Z" && subValue.get() == 9.0) || - (subKey == "U" && subValue.get() == 10.0))) - { - count++; - } - } - } - } - EXPECT_EQ(10, count); - gotControllerDataItem = true; - } - }; - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - createAgent("/samples/data_set.xml"); - auto service = m_agentTestHelper->getMqttLegacyService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe( - "MTConnect/Observation/000/Controller[Controller]/Path[path]/Events/" - "WorkOffsetTable[wpo]"); - - m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|wpo|G53.1={X=1.0 Y=2.0 Z=3.0} G53.2={X=4.0 Y=5.0 Z=6.0}" - "G53.3={X=7.0 Y=8.0 Z=9 U=10.0}"); - - ASSERT_TRUE(waitFor(5s, [&gotControllerDataItem]() { return gotControllerDataItem; })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_publish_Temperature) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - auto handler = make_unique(); - bool gotTemperature = false; - handler->m_receive = [&gotTemperature](std::shared_ptr, const std::string &topic, - const std::string &payload) { - EXPECT_EQ( - "MTConnect/Observation/000/Axes[Axes]/Linear[Z]/Motor[motor_name]/Samples/" - "Temperature[z_motor_temp]", - topic); - auto jdoc = json::parse(payload); - - auto value = jdoc.at("/value"_json_pointer).get(); - EXPECT_EQ(81.0, value); - gotTemperature = true; - }; - - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - - createAgent(); - auto service = m_agentTestHelper->getMqttLegacyService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe( - "MTConnect/Observation/000/Axes[Axes]/Linear[Z]/Motor[motor_name]/Samples/" - "Temperature[z_motor_temp]"); - - m_agentTestHelper->m_adapter->processData("2018-04-27T05:00:26.555666|z_motor_temp|81"); - - ASSERT_TRUE(waitFor(5s, [&gotTemperature]() { return gotTemperature; })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_publish_LinearLoad) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - entity::JsonParser parser; - auto handler = make_unique(); - bool gotLinearLoad = false; - handler->m_receive = [&gotLinearLoad](std::shared_ptr, const std::string &topic, - const std::string &payload) { - EXPECT_EQ("MTConnect/Observation/000/Axes[Axes]/Linear[X]/Samples/Load[Xload]", topic); - auto jdoc = json::parse(payload); - auto value = jdoc.at("/value"_json_pointer).get(); - EXPECT_EQ(50.0, value); - gotLinearLoad = true; - }; - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - createAgent(); - auto service = m_agentTestHelper->getMqttLegacyService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe("MTConnect/Observation/000/Axes[Axes]/Linear[X]/Samples/Load[Xload]"); - - m_agentTestHelper->m_adapter->processData("2018-04-27T05:00:26.555666|Xload|50"); - ASSERT_TRUE(waitFor(5s, [&gotLinearLoad]() { return gotLinearLoad; })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_publish_DynamicCalibration) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - auto handler = make_unique(); - bool gotCalibration = false; - handler->m_receive = [this, &gotCalibration](std::shared_ptr, - const std::string &topic, - const std::string &payload) { - EXPECT_EQ( - "MTConnect/Observation/000/Axes[Axes]/Linear[X]/Samples/PositionTimeSeries.Actual[Xts]", - topic); - auto jdoc = json::parse(payload); - - auto value = jdoc.at("/value"_json_pointer); - ASSERT_TRUE(value.is_array()); - EXPECT_EQ(25, value.size()); - gotCalibration = true; - }; - - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - - createAgent(); - auto service = m_agentTestHelper->getMqttLegacyService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - - m_client->subscribe( - "MTConnect/Observation/000/Axes[Axes]/Linear[X]/Samples/PositionTimeSeries.Actual[Xts]"); - - m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|Xts|25|| 5118 5118 5118 5118 5118 5118 5118 5118 5118 5118 5118 5118 " - "5119 5119 5118 " - "5118 5117 5117 5119 5119 5118 5118 5118 5118 5118"); - - ASSERT_TRUE(waitFor(5s, [&gotCalibration]() { return gotCalibration; })); -} - -/// @test check if the condition includes the state as the key -TEST_F(MqttSinkTest, mqtt_should_publish_conditions_with_the_state_as_the_key) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - entity::JsonParser parser; - auto handler = make_unique(); - bool gotCondition = false; - - handler->m_receive = [&gotCondition](std::shared_ptr, const std::string &topic, - const std::string &payload) { - EXPECT_EQ("MTConnect/Observation/000/Axes[Axes]/Rotary[C]/Condition/Temperature", topic); - auto jdoc = json::parse(payload); - EXPECT_EQ("Temperature is too high", jdoc.at("/Fault/value"_json_pointer).get()); - EXPECT_EQ("X111", jdoc.at("/Fault/nativeCode"_json_pointer).get()); - EXPECT_EQ("BAD", jdoc.at("/Fault/nativeSeverity"_json_pointer).get()); - EXPECT_EQ("HIGH", jdoc.at("/Fault/qualifier"_json_pointer).get()); - EXPECT_EQ("TEMPERATURE", jdoc.at("/Fault/type"_json_pointer).get()); - - gotCondition = true; - }; - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - createAgent(); - - auto di = m_agentTestHelper->m_agent->getDataItemById("ctmp"); - ASSERT_TRUE(di); - ASSERT_EQ("000/Axes[Axes]/Rotary[C]/Condition/Temperature", di->getTopic()); - - auto service = m_agentTestHelper->getMqttLegacyService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe("MTConnect/Observation/000/Axes[Axes]/Rotary[C]/Condition/Temperature"); - - m_agentTestHelper->m_adapter->processData( - "2018-04-27T05:00:26.555666|ctmp|fault|X111|BAD|HIGH|Temperature is too high"); - ASSERT_TRUE(waitFor(5s, [&gotCondition]() { return gotCondition; })); -} diff --git a/test_package/mqtt_sink_test.cpp b/test_package/mqtt_sink_test.cpp index 5aa93846..e3c63af4 100644 --- a/test_package/mqtt_sink_test.cpp +++ b/test_package/mqtt_sink_test.cpp @@ -50,7 +50,7 @@ int main(int argc, char *argv[]) using json = nlohmann::json; -class MqttSink2Test : public testing::Test +class MqttSinkTest : public testing::Test { protected: void SetUp() override @@ -62,6 +62,7 @@ class MqttSink2Test : public testing::Test void TearDown() override { const auto agent = m_agentTestHelper->getAgent(); + m_agentTestHelper->m_ioContext.run_for(500ms); if (agent) { m_agentTestHelper->getAgent()->stop(); @@ -184,7 +185,7 @@ class MqttSink2Test : public testing::Test uint16_t m_port {0}; }; -TEST_F(MqttSink2Test, mqtt_sink_flat_formatt_check) +TEST_F(MqttSinkTest, mqtt_sink_flat_formatt_check) { ConfigOptions options {{MqttMaxTopicDepth, 9}, {ProbeTopic, "Device/F/l/a/t/F/o/r/m/a/t"s}}; createServer(options); @@ -198,7 +199,7 @@ TEST_F(MqttSink2Test, mqtt_sink_flat_formatt_check) ASSERT_TRUE(waitFor(10s, [&service]() { return service->isConnected(); })); } -TEST_F(MqttSink2Test, mqtt_sink_should_publish_Probe) +TEST_F(MqttSinkTest, mqtt_sink_should_publish_Probe) { ConfigOptions options; createServer(options); @@ -237,7 +238,7 @@ TEST_F(MqttSink2Test, mqtt_sink_should_publish_Probe) ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); } -TEST_F(MqttSink2Test, mqtt_sink_should_publish_Sample) +TEST_F(MqttSinkTest, mqtt_sink_should_publish_Sample) { ConfigOptions options; createServer(options); @@ -274,7 +275,7 @@ TEST_F(MqttSink2Test, mqtt_sink_should_publish_Sample) ASSERT_TRUE(waitFor(10s, [&gotSample]() { return gotSample; })); } -TEST_F(MqttSink2Test, mqtt_sink_should_publish_Current) +TEST_F(MqttSinkTest, mqtt_sink_should_publish_Current) { ConfigOptions options; createServer(options); @@ -311,7 +312,7 @@ TEST_F(MqttSink2Test, mqtt_sink_should_publish_Current) ASSERT_TRUE(waitFor(1s, [&gotCurrent]() { return gotCurrent; })); } -TEST_F(MqttSink2Test, mqtt_sink_should_publish_Probe_with_uuid_first) +TEST_F(MqttSinkTest, mqtt_sink_should_publish_Probe_with_uuid_first) { ConfigOptions options; createServer(options); @@ -350,7 +351,7 @@ TEST_F(MqttSink2Test, mqtt_sink_should_publish_Probe_with_uuid_first) ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); } -TEST_F(MqttSink2Test, mqtt_sink_should_publish_Probe_no_device_in_format) +TEST_F(MqttSinkTest, mqtt_sink_should_publish_Probe_no_device_in_format) { ConfigOptions options; createServer(options); @@ -389,7 +390,7 @@ TEST_F(MqttSink2Test, mqtt_sink_should_publish_Probe_no_device_in_format) ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); } -TEST_F(MqttSink2Test, mqtt_sink_should_publish_agent_device) +TEST_F(MqttSinkTest, mqtt_sink_should_publish_agent_device) { ConfigOptions options; createServer(options);