diff --git a/CMakeLists.txt b/CMakeLists.txt index aa76fcdf..e3c72fa2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,7 +67,17 @@ set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/CMakeModules) include(AosCoreLib) # Poco lib -find_package(Poco REQUIRED Util DataSQLite JSON) +find_package( + Poco + REQUIRED + Crypto + DataSQLite + Foundation + JSON + Net + NetSSL + Util +) # Systemd lib find_package(PkgConfig REQUIRED) diff --git a/CMakeModules/AosCoreLib.cmake b/CMakeModules/AosCoreLib.cmake index 9c0229c6..a758f897 100644 --- a/CMakeModules/AosCoreLib.cmake +++ b/CMakeModules/AosCoreLib.cmake @@ -13,7 +13,7 @@ ExternalProject_Add( aoscore PREFIX ${aoscore_build_dir} GIT_REPOSITORY https://github.com/aoscloud/aos_core_lib_cpp.git - GIT_TAG main + GIT_TAG develop GIT_PROGRESS TRUE GIT_SHALLOW TRUE CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${aoscore_build_dir} @@ -23,6 +23,7 @@ ExternalProject_Add( file(MAKE_DIRECTORY ${aoscore_build_dir}/include) add_library(aoscommon STATIC IMPORTED GLOBAL) +add_dependencies(aoscommon aoscore) set_target_properties(aoscommon PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${aoscore_build_dir}/include) set_target_properties(aoscommon PROPERTIES IMPORTED_LOCATION ${aoscore_build_dir}/lib/libaoscommoncpp.a) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c992738c..014dcbd5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,10 +22,11 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}) # ###################################################################################################################### add_subdirectory(app) +add_subdirectory(config) add_subdirectory(database) add_subdirectory(logger) -add_subdirectory(config) add_subdirectory(utils) +add_subdirectory(visidentifier) # ###################################################################################################################### # Sources diff --git a/src/logger/logger.hpp b/src/logger/logger.hpp index a1351ccd..2bf2e905 100644 --- a/src/logger/logger.hpp +++ b/src/logger/logger.hpp @@ -19,8 +19,9 @@ class LogModuleType { public: enum class Enum { eApp, - eDatabase, eConfig, + eDatabase, + eVISIdentifier, eNumModules, }; @@ -28,8 +29,9 @@ class LogModuleType { { static const char* const sLogModuleTypeStrings[] = { "app", - "database", "config", + "database", + "visidentifier", }; return aos::Array(sLogModuleTypeStrings, aos::ArraySize(sLogModuleTypeStrings)); diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt index 95cb7563..91137ed2 100644 --- a/src/utils/CMakeLists.txt +++ b/src/utils/CMakeLists.txt @@ -11,7 +11,11 @@ set(TARGET utils) # Sources # ###################################################################################################################### -set(SOURCES time.cpp) +set(SOURCES json.cpp time.cpp) + +# ###################################################################################################################### +# Includes +# ###################################################################################################################### # ###################################################################################################################### # Target @@ -23,4 +27,8 @@ add_library(${TARGET} STATIC ${SOURCES}) # Labraries # ###################################################################################################################### -target_link_libraries(${TARGET} PUBLIC aoscommon) +target_link_libraries( + ${TARGET} + PUBLIC aoscommon Poco::Foundation + PRIVATE Poco::JSON +) diff --git a/src/utils/json.cpp b/src/utils/json.cpp new file mode 100644 index 00000000..08de1d0b --- /dev/null +++ b/src/utils/json.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "utils/json.hpp" + +namespace UtilsJson { + +aos::RetWithError ParseJson(const std::string& json) noexcept +{ + try { + auto parser = Poco::JSON::Parser(); + + return parser.parse(json); + } catch (const Poco::JSON::JSONException& e) { + return {{}, aos::ErrorEnum::eInvalidArgument}; + } catch (...) { + return {{}, aos::ErrorEnum::eFailed}; + } +} + +aos::RetWithError ParseJson(std::istream& in) noexcept +{ + try { + auto parser = Poco::JSON::Parser(); + + return parser.parse(in); + } catch (const Poco::JSON::JSONException& e) { + return {{}, aos::ErrorEnum::eInvalidArgument}; + } catch (...) { + return {{}, aos::ErrorEnum::eFailed}; + } +} + +Poco::Dynamic::Var FindByPath(const Poco::Dynamic::Var object, const std::vector& keys) +{ + if (keys.empty()) { + return object; + } + + Poco::Dynamic::Var result = object; + + for (const auto& key : keys) { + + if (result.type() == typeid(Poco::JSON::Object)) { + result = result.extract().get(key); + } else if (result.type() == typeid(Poco::JSON::Object::Ptr)) { + result = result.extract()->get(key); + } else { + result.clear(); + + break; + } + } + + return result; +} + +} // namespace UtilsJson diff --git a/src/utils/json.hpp b/src/utils/json.hpp new file mode 100644 index 00000000..46208c16 --- /dev/null +++ b/src/utils/json.hpp @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef UTILS_JSON_HPP_ +#define UTILS_JSON_HPP_ + +#include +#include + +#include + +#include + +namespace UtilsJson { +/** + * Parses json string. + * + * @param json json string. + * @return aos::RetWithError . + */ +aos::RetWithError ParseJson(const std::string& json) noexcept; + +/** + * Parses input stream. + * + * @param in input stream. + * @return aos::RetWithError . + */ +aos::RetWithError ParseJson(std::istream& in) noexcept; + +/** + * Finds value of the json by path + * + * @param object json object. + * @param path json path. + * @return Poco::Dynamic::Var. + */ +Poco::Dynamic::Var FindByPath(const Poco::Dynamic::Var object, const std::vector& path); + +} // namespace UtilsJson + +#endif diff --git a/src/visidentifier/CMakeLists.txt b/src/visidentifier/CMakeLists.txt new file mode 100644 index 00000000..37c25c6e --- /dev/null +++ b/src/visidentifier/CMakeLists.txt @@ -0,0 +1,34 @@ +# +# Copyright (C) 2024 Renesas Electronics Corporation. +# Copyright (C) 2024 EPAM Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +set(TARGET visidentifier) + +# ###################################################################################################################### +# Sources +# ###################################################################################################################### + +set(SOURCES pocowsclient.cpp visconfig.cpp visidentifier.cpp vismessage.cpp wsclientevent.cpp wspendingrequests.cpp) + +# ###################################################################################################################### +# Includes +# ###################################################################################################################### + +# ###################################################################################################################### +# Target +# ###################################################################################################################### + +add_library(${TARGET} STATIC ${SOURCES}) + +# ###################################################################################################################### +# Labraries +# ###################################################################################################################### + +target_link_libraries( + ${TARGET} + PUBLIC aoscommon aosiam Poco::Foundation + PRIVATE Poco::Crypto Poco::Net Poco::NetSSL +) diff --git a/src/visidentifier/log.hpp b/src/visidentifier/log.hpp new file mode 100644 index 00000000..45195946 --- /dev/null +++ b/src/visidentifier/log.hpp @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef LOG_HPP_ +#define LOG_HPP_ + +#include "logger/logger.hpp" + +#define LOG_DBG() LOG_MODULE_DBG(AosLogModule(LogModuleEnum::eVISIdentifier)) +#define LOG_INF() LOG_MODULE_INF(AosLogModule(LogModuleEnum::eVISIdentifier)) +#define LOG_WRN() LOG_MODULE_WRN(AosLogModule(LogModuleEnum::eVISIdentifier)) +#define LOG_ERR() LOG_MODULE_ERR(AosLogModule(LogModuleEnum::eVISIdentifier)) + +#endif diff --git a/src/visidentifier/pocowsclient.cpp b/src/visidentifier/pocowsclient.cpp new file mode 100644 index 00000000..6e69fd25 --- /dev/null +++ b/src/visidentifier/pocowsclient.cpp @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include + +#include "log.hpp" +#include "pocowsclient.hpp" +#include "utils/json.hpp" +#include "vismessage.hpp" +#include "wsexception.hpp" + +/*********************************************************************************************************************** + * Statics + **********************************************************************************************************************/ +template +static auto OnScopeExit(F&& f) +{ + return std::unique_ptr::type>((void*)1, std::forward(f)); +} + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +PocoWSClient::PocoWSClient(const VISConfig& config, MessageHandlerFunc handler) + : mConfig(config) + , mHandleSubscription(std::move(handler)) +{ + mHttpRequest.setMethod(Poco::Net::HTTPRequest::HTTP_GET); + mHttpRequest.setVersion(Poco::Net::HTTPMessage::HTTP_1_1); +} + +void PocoWSClient::Connect() +{ + std::lock_guard lock(mMutex); + + if (mIsConnected) { + return; + } + + const Poco::URI uri(mConfig.GetVISServer()); + + try { + StopReceiveFramesThread(); + + Poco::Net::Context::Ptr context = new Poco::Net::Context( + Poco::Net::Context::TLS_CLIENT_USE, "", mConfig.GetCaCertFile(), "", Poco::Net::Context::VERIFY_NONE, 9); + + // HTTPSClientSession is not copyable or movable. + mClientSession = std::make_unique(uri.getHost(), uri.getPort(), context); + mWebSocket.emplace(Poco::Net::WebSocket(*mClientSession, mHttpRequest, mHttpResponse)); + + mIsConnected = true; + mWSClientErrorEvent.Reset(); + + StartReceiveFramesThread(); + + LOG_INF() << "PocoWSClient::Connect succeeded. URI: " << uri.toString().c_str(); + } catch (const std::exception& e) { + LOG_ERR() << "PocoWSClient::Connect failed. URI: " << uri.toString().c_str() << " with error: " << e.what(); + + throw WSException(e.what(), AOS_ERROR_WRAP(aos::ErrorEnum::eFailed)); + } +} + +void PocoWSClient::Close() +{ + std::lock_guard lock(mMutex); + + LOG_INF() << "Close Web Socket client"; + + try { + if (mIsConnected) { + mWebSocket->shutdown(); + } + } catch (const std::exception& e) { + LOG_ERR() << AosException(e.what(), AOS_ERROR_WRAP(aos::ErrorEnum::eFailed)).what(); + } + + mIsConnected = false; + mWSClientErrorEvent.Set(WSClientEvent::EventEnum::CLOSED, "ws connection has been closed on the client side."); +} + +void PocoWSClient::Disconnect() +{ + std::lock_guard lock(mMutex); + + LOG_INF() << "Disconnect Web Socket client"; + + if (!mIsConnected) { + return; + } + + try { + mWebSocket->shutdown(); + mWebSocket->close(); + } catch (const std::exception& e) { + LOG_ERR() << AosException(e.what(), AOS_ERROR_WRAP(aos::ErrorEnum::eFailed)).what(); + } + + mIsConnected = false; +} + +std::string PocoWSClient::GenerateRequestID() +{ + const auto uuid = aos::uuid::CreateUUID(); + const auto uuidStr = aos::uuid::UUIDToString(uuid); + + return {uuidStr.begin(), uuidStr.end()}; +} + +WSClientEvent::Details PocoWSClient::WaitForEvent() +{ + return mWSClientErrorEvent.Wait(); +} + +PocoWSClient::ByteArray PocoWSClient::SendRequest(const std::string& requestId, const ByteArray& message) +{ + auto requestParams = std::make_shared(requestId); + mPendingRequests.Add(requestParams); + + const auto onScopeExit = OnScopeExit([&](void*) { mPendingRequests.Remove(requestParams); }); + + AsyncSendMessage(message); + + LOG_DBG() << "Waiting server response: requestId = " << requestId.c_str(); + + std::string response; + if (!requestParams->TryWaitForResponse(response, mConfig.GetWebSocketTimeout())) { + LOG_ERR() << "SendRequest timed out: requestId = " << requestId.c_str(); + + throw WSException("", AOS_ERROR_WRAP(aos::ErrorEnum::eTimeout)); + } + + LOG_DBG() << "Got server response: requestId = " << requestId.c_str() << ", response = " << response.c_str(); + + return {response.cbegin(), response.cend()}; +} + +void PocoWSClient::AsyncSendMessage(const ByteArray& message) +{ + if (message.empty()) { + return; + } + + std::lock_guard lock(mMutex); + + if (!mIsConnected) { + throw WSException("Not connected", AOS_ERROR_WRAP(aos::ErrorEnum::eFailed)); + } + + try { + using namespace std::chrono; + + mWebSocket->setSendTimeout(duration_cast(mConfig.GetWebSocketTimeout())); + + const int len = mWebSocket->sendFrame(&message.front(), message.size(), Poco::Net::WebSocket::FRAME_TEXT); + + LOG_DBG() << "Sent " << len << "/" << message.size() << " bytes."; + } catch (const std::exception& e) { + mWSClientErrorEvent.Set(WSClientEvent::EventEnum::FAILED, e.what()); + + throw WSException(e.what(), AOS_ERROR_WRAP(aos::ErrorEnum::eFailed)); + } +} + +PocoWSClient::~PocoWSClient() +{ + Close(); + StopReceiveFramesThread(); +} + +/*********************************************************************************************************************** + * Private + **********************************************************************************************************************/ + +void PocoWSClient::HandleResponse(const std::string& frame) +{ + try { + Poco::Dynamic::Var objectVar; + aos::Error err; + + aos::Tie(objectVar, err) = UtilsJson::ParseJson(frame); + AOS_ERROR_CHECK_AND_THROW("can't parse as json", err); + + const auto object = objectVar.extract(); + + if (object.isNull()) { + return; + } + + if (const auto action = object->get(VISMessage::cActionTagName); action == "subscription") { + mHandleSubscription(frame); + + return; + } + + const auto requestId = object->get(VISMessage::cRequestIdTagName).convert(); + if (requestId.empty()) { + throw AosException("invalid requestId tag received"); + } + + if (!mPendingRequests.SetResponse(requestId, frame)) { + mHandleSubscription(frame); + } + } catch (const Poco::Exception& e) { + LOG_ERR() << AosException(e.what(), AOS_ERROR_WRAP(aos::ErrorEnum::eFailed)).what(); + } +} + +void PocoWSClient::ReceiveFrames() +{ + LOG_DBG() << "PocoWSClient::ReceiveFrames has been started."; + + try { + int flags; + int n; + Poco::Buffer buffer(0); + + do { + n = mWebSocket->receiveFrame(buffer, flags); + LOG_DBG() << "recived frame: bytes = " << n << ", flags = " << flags; + + if ((flags & Poco::Net::WebSocket::FRAME_OP_BITMASK) == Poco::Net::WebSocket::FRAME_OP_CLOSE) { + mWSClientErrorEvent.Set(WSClientEvent::EventEnum::FAILED, "got Close frame from server"); + + return; + } + + if (n > 0) { + + const std::string message(buffer.begin(), buffer.end()); + + buffer.resize(0); + + HandleResponse(message); + } + + } while (flags != 0 || n != 0); + } catch (const Poco::Exception& e) { + LOG_DBG() << AosException(e.what(), AOS_ERROR_WRAP(aos::ErrorEnum::eRuntime)).what(); + + mWSClientErrorEvent.Set(WSClientEvent::EventEnum::FAILED, e.what()); + + return; + } + + mWSClientErrorEvent.Set(WSClientEvent::EventEnum::FAILED, "ReceiveFrames stopped"); +} + +void PocoWSClient::StartReceiveFramesThread() +{ + StopReceiveFramesThread(); + + mReceivedFramesThread = std::thread(&PocoWSClient::ReceiveFrames, this); +} + +void PocoWSClient::StopReceiveFramesThread() +{ + if (mReceivedFramesThread.joinable()) { + mReceivedFramesThread.join(); + } +} diff --git a/src/visidentifier/pocowsclient.hpp b/src/visidentifier/pocowsclient.hpp new file mode 100644 index 00000000..c02c50da --- /dev/null +++ b/src/visidentifier/pocowsclient.hpp @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef POCOWSCLIENT_HPP_ +#define POCOWSCLIENT_HPP_ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "visidentifier/visconfig.hpp" +#include "visidentifier/wsclient.hpp" +#include "wsclientevent.hpp" +#include "wspendingrequests.hpp" + +/** + * Poco web socket client. + */ +class PocoWSClient : public WSClientItf { +public: + /** + * Creates Web socket client instance. + * + * @param config VIS config. + * @param handler handler functor. + */ + PocoWSClient(const VISConfig& config, MessageHandlerFunc handler); + + /** + * Connects to Web Socket server. + */ + void Connect() override; + + /** + * Closes Web Socket client. + */ + void Close() override; + + /** + * Disconnects Web Socket client. + */ + void Disconnect() override; + + /** + * Generates request id. + * + * @returns std::string + */ + std::string GenerateRequestID() override; + + /** + * Waits for Web Socket Client Event. + * + * @returns WSClientEvent::Details + */ + WSClientEvent::Details WaitForEvent() override; + + /** + * Sends request. Blocks till the response is received or timed-out (WSException is thrown). + * + * @param requestId request id + * @param message request payload + * @returns ByteArray + */ + ByteArray SendRequest(const std::string& requestId, const ByteArray& message) override; + + /** + * Sends message. Doesn't wait for response. + * + * @param message request payload + */ + void AsyncSendMessage(const ByteArray& message); + + /** + * Destroys web socket client instance. + */ + ~PocoWSClient() override; + +private: + void HandleResponse(const std::string& frame); + void ReceiveFrames(); + void StartReceiveFramesThread(); + void StopReceiveFramesThread(); + + VISConfig mConfig; + std::recursive_mutex mMutex; + std::thread mReceivedFramesThread; + std::unique_ptr mClientSession; + std::optional mWebSocket; + bool mIsConnected {false}; + Poco::Net::HTTPRequest mHttpRequest; + Poco::Net::HTTPResponse mHttpResponse; + PendingRequests mPendingRequests; + MessageHandlerFunc mHandleSubscription; + WSClientEvent mWSClientErrorEvent; +}; + +#endif diff --git a/src/visidentifier/visconfig.cpp b/src/visidentifier/visconfig.cpp new file mode 100644 index 00000000..72367252 --- /dev/null +++ b/src/visidentifier/visconfig.cpp @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "utils/json.hpp" +#include "visconfig.hpp" + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +VISConfig::VISConfig( + const std::string& visServer, const std::string& caCertFile, const UtilsTime::Duration& webSocketTimeout) + : mVISServer(visServer) + , mCaCertFile(caCertFile) + , mWebSocketTimeout(webSocketTimeout) +{ +} + +aos::Error VISConfig::Init(const Poco::Dynamic::Var& params) +{ + auto var = UtilsJson::FindByPath(params, {cVisServerTagName}); + + if (var.isEmpty()) { + return AOS_ERROR_WRAP(aos::ErrorEnum::eNotFound); + } + + if (!var.isString()) { + return AOS_ERROR_WRAP(aos::ErrorEnum::eInvalidArgument); + } + + mVISServer = var.extract(); + + var = UtilsJson::FindByPath(params, {cCaCertFileTagName}); + if (var.isString()) { + mCaCertFile = var.extract(); + } + + var = UtilsJson::FindByPath(params, {cWebSocketTimeoutTagName}); + if (var.isString()) { + aos::Error err; + UtilsTime::Duration duration; + + aos::Tie(duration, err) = UtilsTime::ParseDuration(var.extract()); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + mWebSocketTimeout = std::move(duration); + } + + return aos::ErrorEnum::eNone; +} + +const std::string& VISConfig::GetVISServer() const +{ + return mVISServer; +} + +const std::string& VISConfig::GetCaCertFile() const +{ + return mCaCertFile; +} + +const UtilsTime::Duration& VISConfig::GetWebSocketTimeout() const +{ + return mWebSocketTimeout; +} + +Poco::Dynamic::Var VISConfig::ToJSON() const +{ + Poco::JSON::Object object; + + object.set(cVisServerTagName, mVISServer); + object.set(cCaCertFileTagName, mCaCertFile); + object.set(cWebSocketTimeoutTagName, std::to_string(mWebSocketTimeout.count()).append("ns")); + + return object; +} + +std::string VISConfig::ToString() const +{ + std::ostringstream jsonStream; + + Poco::JSON::Stringifier::stringify(ToJSON(), jsonStream); + + return jsonStream.str(); +} diff --git a/src/visidentifier/visconfig.hpp b/src/visidentifier/visconfig.hpp new file mode 100644 index 00000000..7035ddb8 --- /dev/null +++ b/src/visidentifier/visconfig.hpp @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef VISCONFIG_HPP_ +#define VISCONFIG_HPP_ + +#include + +#include + +#include + +#include "utils/time.hpp" + +/** + * VIS configuration. + */ +class VISConfig { +public: + /** + * Creates a new object instance. + */ + VISConfig() = default; + + /** + * Creates a new object instance. + * + * @param visServer VIS Server URI. + * @param caCertFile path to ca cert file. + * @param webSocketTimeout web socket timeout. + */ + VISConfig(const std::string& visServer, const std::string& caCertFile, const UtilsTime::Duration& webSocketTimeout); + + /** + * Initializes VIS Config from params. + * + * @param params VIS Config params. + * @return Error. + */ + aos::Error Init(const Poco::Dynamic::Var& params); + + /** + * Returns VIS server URI. + * + * @returns const std::string&. + */ + const std::string& GetVISServer() const; + + /** + * Returns ca cert file path. + * + * @returns const std::string&. + */ + const std::string& GetCaCertFile() const; + + /** + * Returns web socket timeout. + * + * @returns const UtilsTime::Duration&. + */ + const UtilsTime::Duration& GetWebSocketTimeout() const; + + /** + * Returns JSON representation of a VISConfig object. + * + * @returns Poco::Dynamic::Var. + */ + Poco::Dynamic::Var ToJSON() const; + + /** + * Returns JSON representation of a VISConfig object. + * + * @returns std::string. + */ + std::string ToString() const; + +private: + static constexpr const char* cVisServerTagName = "visServer"; + static constexpr const char* cCaCertFileTagName = "caCertFile"; + static constexpr const char* cWebSocketTimeoutTagName = "webSocketTimeout"; + + std::string mVISServer; + std::string mCaCertFile; + UtilsTime::Duration mWebSocketTimeout {std::chrono::seconds(120)}; +}; + +#endif diff --git a/src/visidentifier/visidentifier.cpp b/src/visidentifier/visidentifier.cpp new file mode 100644 index 00000000..fa0b0b01 --- /dev/null +++ b/src/visidentifier/visidentifier.cpp @@ -0,0 +1,416 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "log.hpp" +#include "pocowsclient.hpp" +#include "utils/json.hpp" +#include "visidentifier.hpp" +#include "vismessage.hpp" +#include "wsexception.hpp" + +/*********************************************************************************************************************** + * VISSubscriptions + **********************************************************************************************************************/ + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +void VISSubscriptions::RegisterSubscription(const std::string& subscriptionId, Handler&& subscriptionHandler) +{ + std::lock_guard lock(mMutex); + + LOG_DBG() << "Registred subscription id = " << subscriptionId.c_str(); + + mSubscriptionMap[subscriptionId] = std::move(subscriptionHandler); +} + +aos::Error VISSubscriptions::ProcessSubscription(const std::string& subscriptionId, const Poco::Dynamic::Var value) +{ + std::lock_guard lock(mMutex); + + const auto it = mSubscriptionMap.find(subscriptionId); + + if (it == mSubscriptionMap.cend()) { + LOG_ERR() << "Unexpected subscription id: = " << subscriptionId.c_str(); + + return aos::ErrorEnum::eNotFound; + } + + return it->second(value); +} + +/*********************************************************************************************************************** + * VISIdentifier + **********************************************************************************************************************/ + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +VISIdentifier::VISIdentifier() + : mWSClientIsConnected {Poco::Event::EventType::EVENT_MANUALRESET} + , mStopHandleSubjectsChangedThread {Poco::Event::EventType::EVENT_AUTORESET} +{ +} + +aos::Error VISIdentifier::Init( + const Config& config, std::shared_ptr subjectsObserverPtr) +{ + if (subjectsObserverPtr == nullptr) { + return AOS_ERROR_WRAP(aos::ErrorEnum::eInvalidArgument); + } + std::lock_guard lock(mMutex); + + if (auto err = InitWSClient(config); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + mSubjectsObserverPtr = std::move(subjectsObserverPtr); + + mHandleConnectionThread = std::thread(&VISIdentifier::HandleConnection, this); + + return aos::ErrorEnum::eNone; +} + +aos::RetWithError> VISIdentifier::GetSystemID() +{ + std::lock_guard lock(mMutex); + + if (mSystemId.IsEmpty()) { + try { + const VISMessage responseMessage(SendGetRequest(cVinVISPath)); + + if (!responseMessage.Is(VISActionEnum::eGet)) { + return {{}, AOS_ERROR_WRAP(aos::ErrorEnum::eFailed)}; + } + + const auto systemId = GetValueByPath(responseMessage.GetJSON(), cVinVISPath); + if (systemId.empty()) { + return {{}, AOS_ERROR_WRAP(aos::ErrorEnum::eFailed)}; + } + + if (systemId.size() > mSystemId.MaxSize()) { + return {{}, AOS_ERROR_WRAP(aos::ErrorEnum::eNoMemory)}; + } + + mSystemId = systemId.c_str(); + } catch (const Poco::Exception& e) { + LOG_ERR() << e.what(); + + return {{}, AOS_ERROR_WRAP(aos::ErrorEnum::eFailed)}; + } + } + + return mSystemId; +} + +aos::RetWithError> VISIdentifier::GetUnitModel() +{ + std::lock_guard lock(mMutex); + + if (mUnitModel.IsEmpty()) { + try { + const VISMessage responseMessage(SendGetRequest(cUnitModelPath)); + + if (!responseMessage.Is(VISActionEnum::eGet)) { + return {{}, AOS_ERROR_WRAP(aos::ErrorEnum::eFailed)}; + } + + const auto unitModel = GetValueByPath(responseMessage.GetJSON(), cUnitModelPath); + if (unitModel.empty()) { + return {{}, AOS_ERROR_WRAP(aos::ErrorEnum::eFailed)}; + } + + if (unitModel.size() > mUnitModel.MaxSize()) { + return {{}, AOS_ERROR_WRAP(aos::ErrorEnum::eNoMemory)}; + } + + mUnitModel = unitModel.c_str(); + } catch (const Poco::Exception& e) { + LOG_ERR() << e.what(); + + return {{}, AOS_ERROR_WRAP(aos::ErrorEnum::eFailed)}; + } + } + + return mUnitModel; +} + +aos::Error VISIdentifier::GetSubjects(aos::Array>& subjects) +{ + std::lock_guard lock(mMutex); + + if (mSubjects.IsEmpty()) { + try { + const VISMessage responseMessage(SendGetRequest(cSubjectsVISPath)); + + if (!responseMessage.Is(VISActionEnum::eGet)) { + return AOS_ERROR_WRAP(aos::ErrorEnum::eFailed); + } + + const auto responseSubjects = GetValueArrayByPath(responseMessage.GetJSON(), cSubjectsVISPath); + + for (const auto& subject : responseSubjects) { + if (auto err = mSubjects.PushBack(subject.c_str()); !err.IsNone()) { + mSubjects.Clear(); + + return AOS_ERROR_WRAP(err); + } + } + } catch (const Poco::Exception& e) { + LOG_ERR() << e.what(); + + return AOS_ERROR_WRAP(aos::ErrorEnum::eFailed); + } + } + + if (mSubjects.Size() > subjects.MaxSize()) { + return AOS_ERROR_WRAP(aos::ErrorEnum::eNoMemory); + } + + subjects = mSubjects; + + return aos::ErrorEnum::eNone; +} + +VISIdentifier::~VISIdentifier() +{ + Close(); +} + +/*********************************************************************************************************************** + * Protected + **********************************************************************************************************************/ + +aos::Error VISIdentifier::InitWSClient(const Config& config) +{ + VISConfig visConfig; + + auto err = visConfig.Init(config.mIdentifier.mParams); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + mWsClientPtr = std::make_shared( + visConfig, std::bind(&VISIdentifier::HandleSubscription, this, std::placeholders::_1)); + + return aos::ErrorEnum::eNone; +} + +void VISIdentifier::SetWSClient(WSClientItfPtr wsClient) +{ + mWsClientPtr = std::move(wsClient); +} + +WSClientItfPtr VISIdentifier::GetWSClient() +{ + return mWsClientPtr; +} + +void VISIdentifier::HandleSubscription(const std::string& message) +{ + try { + const VISMessage notification(message); + const auto subscriptionId = notification.GetValue(VISMessage::cSubscriptionIdTagName); + + if (!notification.Is(VISActionEnum::eSubscriptionNotification) || subscriptionId.empty()) { + LOG_ERR() << "Unexpected message received: message = " << notification.ToString().c_str(); + + return; + } + + const auto err = mSubscriptions.ProcessSubscription(subscriptionId, notification.GetJSON()); + if (!err.IsNone()) { + LOG_ERR() << "Failed to process subscription: err = " << err.Message() + << ", message = " << notification.ToString().c_str(); + } + } catch (const Poco::Exception& e) { + LOG_ERR() << e.message().c_str(); + } +} + +void VISIdentifier::WaitUntilConnected() +{ + mWSClientIsConnected.wait(); +} + +/*********************************************************************************************************************** + * Private + **********************************************************************************************************************/ +void VISIdentifier::Close() +{ + try { + if (mWsClientPtr) { + SendUnsubscribeAllRequest(); + + mStopHandleSubjectsChangedThread.set(); + mWsClientPtr->Close(); + } + + if (mHandleConnectionThread.joinable()) { + mHandleConnectionThread.join(); + } + + mWSClientIsConnected.reset(); + + LOG_INF() << "VISIdentifier has been stopped."; + + } catch (const AosException& e) { + LOG_ERR() << e.what(); + } +} + +void VISIdentifier::HandleConnection() +{ + do { + try { + mWsClientPtr->Connect(); + + Subscribe( + cSubjectsVISPath, std::bind(&VISIdentifier::HandleSubjectsSubscription, this, std::placeholders::_1)); + + mSystemId.Clear(); + mUnitModel.Clear(); + mSubjects.Clear(); + + mWSClientIsConnected.set(); + + // block on Wait + const auto wsClientEvent = mWsClientPtr->WaitForEvent(); + + if (wsClientEvent.mCode == WSClientEvent::EventEnum::CLOSED) { + LOG_INF() << "WS Client connection has been closed. Stopping Vis Identifier Handle Connection thread"; + + return; + } + + mWSClientIsConnected.reset(); + mWsClientPtr->Disconnect(); + + } catch (const WSException& e) { + LOG_ERR() << "WSException has been caught: message = " << e.message().c_str(); + + mWSClientIsConnected.reset(); + mWsClientPtr->Disconnect(); + } catch (const Poco::Exception& e) { + LOG_ERR() << "Poco::Exception has been caught: message = " << e.message().c_str(); + } catch (...) { + LOG_ERR() << "Unknown exception caught."; + } + } while (!mStopHandleSubjectsChangedThread.tryWait(cWSClientReconnectMilliseconds)); +} + +aos::Error VISIdentifier::HandleSubjectsSubscription(Poco::Dynamic::Var value) +{ + try { + aos::StaticArray, aos::cMaxSubjectIDSize> newSubjects; + + const auto responseSubjects = GetValueArrayByPath(value, cSubjectsVISPath); + + for (const auto& subject : responseSubjects) { + if (auto err = newSubjects.PushBack(subject.c_str()); !err.IsNone()) { + return err; + } + } + + std::lock_guard lock(mMutex); + + if (mSubjects != newSubjects) { + mSubjects = std::move(newSubjects); + mSubjectsObserverPtr->SubjectsChanged(mSubjects); + } + } catch (const Poco::Exception& e) { + LOG_ERR() << e.message().c_str(); + + return aos::ErrorEnum::eFailed; + } + + return aos::ErrorEnum::eNone; +} + +std::string VISIdentifier::SendGetRequest(const std::string& path) +{ + const auto requestId = mWsClientPtr->GenerateRequestID(); + const VISMessage getMessage(VISActionEnum::eGet, requestId, path); + + WaitUntilConnected(); + + const auto response = mWsClientPtr->SendRequest(requestId, getMessage.ToByteArray()); + + return {response.cbegin(), response.cend()}; +} + +void VISIdentifier::SendUnsubscribeAllRequest() +{ + try { + const VISMessage request(VISActionEnum::eUnsubscribeAll, mWsClientPtr->GenerateRequestID(), ""); + + mWsClientPtr->AsyncSendMessage(request.ToByteArray()); + + } catch (const AosException& e) { + LOG_ERR() << e.what(); + } +} + +void VISIdentifier::Subscribe(const std::string& path, VISSubscriptions::Handler&& callback) +{ + const auto requestId = mWsClientPtr->GenerateRequestID(); + const VISMessage subscribeMessage(VISActionEnum::eSubscribe, requestId, path); + + const auto response = mWsClientPtr->SendRequest(requestId, subscribeMessage.ToByteArray()); + const VISMessage responseVISMessage(std::string {response.cbegin(), response.cend()}); + + mSubscriptions.RegisterSubscription( + responseVISMessage.GetValue(VISMessage::cSubscriptionIdTagName), std::move(callback)); +} + +std::string VISIdentifier::GetValueByPath(Poco::Dynamic::Var object, const std::string& valueChildTagName) +{ + auto var = UtilsJson::FindByPath(object, {VISMessage::cValueTagName}); + + if (var.isString()) { + return var.extract(); + } + + var = UtilsJson::FindByPath(var, {valueChildTagName}); + + return var.extract(); +} + +std::vector VISIdentifier::GetValueArrayByPath( + Poco::Dynamic::Var object, const std::string& valueChildTagName) +{ + auto var = UtilsJson::FindByPath(object, {VISMessage::cValueTagName}); + + const auto isArray = [](const Poco::Dynamic::Var var) { + return var.type() == typeid(Poco::JSON::Array) || var.type() == typeid(Poco::JSON::Array::Ptr); + }; + + if (!isArray(var)) { + var = UtilsJson::FindByPath(var, {valueChildTagName}); + } + + Poco::JSON::Array::Ptr array; + if (var.type() == typeid(Poco::JSON::Array::Ptr)) { + array = var.extract(); + } else if (var.type() == typeid(Poco::JSON::Array)) { + array = new Poco::JSON::Array(var.extract()); + } else { + throw Poco::JSON::JSONException("key not found or not an array"); + } + + std::vector valueArray; + + for (const auto& i : *array) { + valueArray.push_back(i.convert()); + } + + return valueArray; +} diff --git a/src/visidentifier/visidentifier.hpp b/src/visidentifier/visidentifier.hpp new file mode 100644 index 00000000..11c64e66 --- /dev/null +++ b/src/visidentifier/visidentifier.hpp @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef VISIDENTIFIER_HPP_ +#define VISIDENTIFIER_HPP_ + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "config/config.hpp" +#include "visidentifier/wsclient.hpp" + +/** + * VIS Subscriptions. + */ +class VISSubscriptions { +public: + using Handler = std::function; + + /** + * Register subscription. + * + * @param subscriptionId subscription id. + * @param subscriptionHandler subscription handler. + * @return Error. + */ + void RegisterSubscription(const std::string& subscriptionId, Handler&& subscriptionHandler); + + /** + * Process subscription. + * + * @param subscriptionId subscription id. + * @param value subscription value. + * @return Error. + */ + aos::Error ProcessSubscription(const std::string& subscriptionId, const Poco::Dynamic::Var value); + +private: + std::mutex mMutex; + std::map mSubscriptionMap; +}; + +/** + * VIS Identifier. + */ +class VISIdentifier : public aos::iam::identhandler::IdentHandlerItf { +public: + /** + * Creates a new object instance. + */ + VISIdentifier(); + + /** + * Initializes vis identifier. + * + * @param config config object. + * @param subjectsObserverPtr subject observer pointer. + * @return Error. + */ + aos::Error Init( + const Config& config, std::shared_ptr subjectsObserverPtr); + + /** + * Returns System ID. + * + * @returns RetWithError. + */ + aos::RetWithError> GetSystemID() override; + + /** + * Returns unit model. + * + * @returns RetWithError. + */ + aos::RetWithError> GetUnitModel() override; + + /** + * Returns subjects. + * + * @param[out] subjects result subjects. + * @returns Error. + */ + aos::Error GetSubjects(aos::Array>& subjects) override; + + /** + * Destroys vis identifier object instance. + */ + ~VISIdentifier() override; + +protected: + virtual aos::Error InitWSClient(const Config& config); + void SetWSClient(WSClientItfPtr wsClient); + WSClientItfPtr GetWSClient(); + void HandleSubscription(const std::string& message); + void WaitUntilConnected(); + +private: + static constexpr const char* cVinVISPath = "Attribute.Vehicle.VehicleIdentification.VIN"; + static constexpr const char* cUnitModelPath = "Attribute.Aos.UnitModel"; + static constexpr const char* cSubjectsVISPath = "Attribute.Aos.Subjects"; + static const long cWSClientReconnectMilliseconds = 2000; + + void Close(); + void HandleConnection(); + aos::Error HandleSubjectsSubscription(Poco::Dynamic::Var value); + std::string SendGetRequest(const std::string& path); + void SendUnsubscribeAllRequest(); + void Subscribe(const std::string& path, VISSubscriptions::Handler&& callback); + std::string GetValueByPath(Poco::Dynamic::Var object, const std::string& valueChildTagName); + std::vector GetValueArrayByPath(Poco::Dynamic::Var object, const std::string& valueChildTagName); + + std::shared_ptr mWsClientPtr; + std::shared_ptr mSubjectsObserverPtr; + VISSubscriptions mSubscriptions; + aos::StaticString mSystemId; + aos::StaticString mUnitModel; + aos::StaticArray, aos::cMaxSubjectIDSize> mSubjects; + std::thread mHandleConnectionThread; + Poco::Event mWSClientIsConnected; + Poco::Event mStopHandleSubjectsChangedThread; + std::mutex mMutex; +}; + +#endif diff --git a/src/visidentifier/vismessage.cpp b/src/visidentifier/vismessage.cpp new file mode 100644 index 00000000..f8f294e3 --- /dev/null +++ b/src/visidentifier/vismessage.cpp @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "utils/exception.hpp" +#include "utils/json.hpp" +#include "vismessage.hpp" + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +VISMessage::VISMessage(const VISAction action) + : mAction(action) +{ + mJsonObject.set(cActionTagName, mAction.ToString().CStr()); +} + +VISMessage::VISMessage(const VISAction action, const std::string& requestId, const std::string& path) + : VISMessage(action) +{ + if (!requestId.empty()) { + mJsonObject.set(cRequestIdTagName, requestId); + } + + if (!path.empty()) { + mJsonObject.set(cPathTagName, path); + } +} + +VISMessage::VISMessage(const std::string& jsonStr) +{ + try { + Poco::Dynamic::Var objectVar; + aos::Error err; + + aos::Tie(objectVar, err) = UtilsJson::ParseJson(jsonStr); + AOS_ERROR_CHECK_AND_THROW("can't parse as json", err); + + mJsonObject = std::move(*objectVar.extract()); + + mAction.FromString(mJsonObject.getValue(cActionTagName).c_str()); + } catch (const Poco::Exception& e) { + throw AosException(e.message(), AOS_ERROR_WRAP(aos::ErrorEnum::eFailed)); + } +} + +bool VISMessage::Is(const VISAction actionType) const +{ + return mAction == actionType; +} + +const JsonObject& VISMessage::GetJSON() const +{ + return mJsonObject; +} + +std::string VISMessage::ToString() const +{ + std::ostringstream jsonStream; + Poco::JSON::Stringifier::stringify(mJsonObject, jsonStream); + + return jsonStream.str(); +} + +std::vector VISMessage::ToByteArray() const +{ + const auto str = ToString(); + + return {str.cbegin(), str.cend()}; +} diff --git a/src/visidentifier/vismessage.hpp b/src/visidentifier/vismessage.hpp new file mode 100644 index 00000000..22081cfc --- /dev/null +++ b/src/visidentifier/vismessage.hpp @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef VISMESSAGE_HPP_ +#define VISMESSAGE_HPP_ + +#include +#include + +#include + +#include +#include + +/** + * Supported Vehicle Information Service actions. + */ +class VISActionType { +public: + enum class Enum { + eGet, + eSubscribe, + eSubscriptionNotification, + eUnsubscribeAll, + eNumActions, + }; + + static const aos::Array GetStrings() + { + static const char* const sVISActionTypeStrings[] = { + "get", + "subscribe", + "subscription", + "unsubscribeAll", + }; + + return aos::Array(sVISActionTypeStrings, aos::ArraySize(sVISActionTypeStrings)); + }; +}; + +using VISActionEnum = VISActionType::Enum; +using VISAction = aos::EnumStringer; +using JsonObject = Poco::JSON::Object; +using JsonObjectPtr = JsonObject::Ptr; + +/** + * Vehicle Information Service message + */ +class VISMessage { +public: + static constexpr const char* cActionTagName = "action"; + static constexpr const char* cPathTagName = "path"; + static constexpr const char* cRequestIdTagName = "requestId"; + static constexpr const char* cSubscriptionIdTagName = "subscriptionId"; + static constexpr const char* cValueTagName = "value"; + + /** + * Creates Vehicle Information Service message. + * + * @param action The type of action requested by the client or delivered by the server. + */ + VISMessage(const VISAction action); + + /** + * Creates Vehicle Information Service message. + * + * @param action The type of action requested by the client or delivered by the server. + * @param requestId request id. + * @param path path. + */ + VISMessage(const VISAction action, const std::string& requestId, const std::string& path); + + /** + * Creates Vehicle Information Service message. + * + * @param jsonStr JSON string that contains Vehicle Information Service message. + */ + VISMessage(const std::string& jsonStr); + + /** + * Checks if Vehicle Information Service message has specified type. + * + * @param actionType action type to check. + * @return bool result + */ + bool Is(const VISAction actionType) const; + + /** + * Return const Vehicle Information Service message json object + * + * @return const JsonObjectPtr& + */ + const JsonObject& GetJSON() const; + + /** + * Converts Vehicle Information Service message to string. + * + * @return std::string. + */ + std::string ToString() const; + + /** + * Converts Vehicle Information Service message to byte array. + * + * @return std::vector. + */ + std::vector ToByteArray() const; + + /** + * Sets Vehicle Information Service message key-value. + * + * @param key VIS message key. + * @param value VIS message value. + * @return + */ + template + void SetKeyValue(const std::string& key, const V& value) + { + mJsonObject.set(key, value); + } + + /** + * Gets Vehicle Information Service message value by key. + * + * @param key VIS message key. + * @return + */ + template + T GetValue(const std::string& key) const + { + return mJsonObject.getValue(key); + } + +private: + VISAction mAction; + JsonObject mJsonObject; +}; + +#endif diff --git a/src/visidentifier/wsclient.hpp b/src/visidentifier/wsclient.hpp new file mode 100644 index 00000000..7b592748 --- /dev/null +++ b/src/visidentifier/wsclient.hpp @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef WSCLIENT_HPP_ +#define WSCLIENT_HPP_ + +#include +#include +#include +#include + +#include "utils/time.hpp" +#include "wsclientevent.hpp" + +/** + * Web socket client interface. + */ +class WSClientItf { +public: + using ByteArray = std::vector; + using MessageHandlerFunc = std::function; + + /** + * Connects to Web Socket server. + */ + virtual void Connect() = 0; + + /** + * Closes Web Socket client. + */ + virtual void Close() = 0; + + /** + * Disconnects Web Socket client. + */ + virtual void Disconnect() = 0; + + /** + * Generates request id. + * + * @returns std::string + */ + virtual std::string GenerateRequestID() = 0; + + /** + * Waits for Web Socket Client Event. + * + * @returns WSClientEvent::Details + */ + virtual WSClientEvent::Details WaitForEvent() = 0; + + /** + * Sends request. Blocks till the response is received or timed-out (WSException is thrown). + * + * @param requestId request id + * @param message request payload + * @returns ByteArray + */ + virtual ByteArray SendRequest(const std::string& requestId, const ByteArray& message) = 0; + + /** + * Sends message. Doesn't wait for response. + * + * @param message request payload + */ + virtual void AsyncSendMessage(const ByteArray& message) = 0; + + /** + * Destroys web socket client instance. + */ + virtual ~WSClientItf() = default; +}; + +using WSClientItfPtr = std::shared_ptr; + +#endif diff --git a/src/visidentifier/wsclientevent.cpp b/src/visidentifier/wsclientevent.cpp new file mode 100644 index 00000000..4bff9c28 --- /dev/null +++ b/src/visidentifier/wsclientevent.cpp @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "wsclientevent.hpp" + +WSClientEvent::Details WSClientEvent::Wait() +{ + // blocking wait + mEvent.wait(); + + return mDetails; +} + +void WSClientEvent::Set(const EventEnum code, const std::string& message) +{ + mDetails.mCode = code; + mDetails.mMessage = message; + + mEvent.set(); +} + +void WSClientEvent::Reset() +{ + mEvent.reset(); +} diff --git a/src/visidentifier/wsclientevent.hpp b/src/visidentifier/wsclientevent.hpp new file mode 100644 index 00000000..f0495c5f --- /dev/null +++ b/src/visidentifier/wsclientevent.hpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef WSCLIENTEVENT_HPP_ +#define WSCLIENTEVENT_HPP_ + +#include + +#include + +/** + * Web socket client event. + */ +class WSClientEvent { +public: + /** + * Web socket client event enum. + */ + enum class EventEnum { CLOSED, FAILED }; + + struct Details { + EventEnum mCode; + std::string mMessage; + }; + + /** + * Waits for event is to be set. + * + * @returns Details + */ + Details Wait(); + + /** + * Sets event with the passed details. + * + * @param code event enum value + * @param message event message + * @returns std::pair + */ + void Set(const EventEnum code, const std::string& message); + + /** + * Resets event. + */ + void Reset(); + +private: + Poco::Event mEvent; + Details mDetails; +}; + +#endif diff --git a/src/visidentifier/wsexception.hpp b/src/visidentifier/wsexception.hpp new file mode 100644 index 00000000..827709dd --- /dev/null +++ b/src/visidentifier/wsexception.hpp @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef WSEXCEPTION_HPP_ +#define WSEXCEPTION_HPP_ + +#include "utils/exception.hpp" + +/** + * Web socket exception. + */ +class WSException : public AosException { +public: + /** + * Creates WSException exception instance. + * + * @param message exception message. + * @param err Aos error. + */ + explicit WSException(const std::string& message, const aos::Error& err = aos::ErrorEnum::eFailed) + : AosException(message, err) {}; +}; + +#endif diff --git a/src/visidentifier/wspendingrequests.cpp b/src/visidentifier/wspendingrequests.cpp new file mode 100644 index 00000000..1a25b0ef --- /dev/null +++ b/src/visidentifier/wspendingrequests.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "wspendingrequests.hpp" + +/*********************************************************************************************************************** + * RequestParams + **********************************************************************************************************************/ + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +RequestParams::RequestParams(const std::string& requestId) + : mRequestId(requestId) +{ +} + +void RequestParams::SetResponse(const std::string& response) +{ + mResponse = response; + mEvent.set(); +} + +const std::string& RequestParams::GetRequestId() const +{ + return mRequestId; +} + +bool RequestParams::TryWaitForResponse(std::string& result, const UtilsTime::Duration timeout) +{ + using namespace std::chrono; + + if (mEvent.tryWait(duration_cast(timeout).count())) { + result = mResponse; + + return true; + } + + return false; +} + +bool RequestParams::operator<(const RequestParams& rhs) const +{ + return mRequestId < rhs.mRequestId; +} + +/*********************************************************************************************************************** + * PendingRequests + **********************************************************************************************************************/ + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +void PendingRequests::Add(RequestParamsPtr requestParamsPtr) +{ + std::lock_guard lock(mMutex); + + mRequests.push_back(std::move(requestParamsPtr)); +} + +void PendingRequests::Remove(RequestParamsPtr requestParamsPtr) +{ + std::lock_guard lock(mMutex); + + mRequests.erase(std::remove(mRequests.begin(), mRequests.end(), requestParamsPtr)); +} + +bool PendingRequests::SetResponse(const std::string& requestId, const std::string& response) +{ + std::lock_guard lock(mMutex); + + const auto itPendingMessage = std::find_if(mRequests.begin(), mRequests.end(), + [&requestId](const auto& pendingRequest) { return pendingRequest->GetRequestId() == requestId; }); + + if (itPendingMessage == mRequests.end()) { + return false; + } + + (*itPendingMessage)->SetResponse(response); + + return true; +} diff --git a/src/visidentifier/wspendingrequests.hpp b/src/visidentifier/wspendingrequests.hpp new file mode 100644 index 00000000..c018e041 --- /dev/null +++ b/src/visidentifier/wspendingrequests.hpp @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef WSPENDINGREQUESTS_HPP_ +#define WSPENDINGREQUESTS_HPP_ + +#include +#include +#include + +#include + +#include "utils/time.hpp" + +/** + * Request Params. + */ +class RequestParams { +public: + /** + * Creates Request Params instance. + * + * @param requestId request id. + */ + RequestParams(const std::string& requestId); + + /** + * Sets response and event. + * + * @param requestId request id. + */ + void SetResponse(const std::string& response); + + /** + * Returns request id. + * + * @return const std::string&. + */ + const std::string& GetRequestId() const; + + /** + * Blocks up to timeout milliseconds waiting for response to be set. + * + * @param result[out] contains response value on success. + * @param timeout wait timeout. + * + * @return bool - true if response was set within specified timeout. + */ + bool TryWaitForResponse(std::string& result, const UtilsTime::Duration timeout); + + /** + * Compares request params. + * + * @param rhs request param to compare with. + * @return bool. + */ + bool operator<(const RequestParams& rhs) const; + +private: + std::string mRequestId; + std::string mResponse; + Poco::Event mEvent; +}; + +using RequestParamsPtr = std::shared_ptr; + +/** + * Pending Requests. + */ +class PendingRequests { +public: + /** + * Add request + * + * @param requestParamsPtr request params pointer. + */ + void Add(RequestParamsPtr requestParamsPtr); + + /** + * Remove request + * + * @param requestId request id. + */ + void Remove(RequestParamsPtr requestParamsPtr); + + /** + * Set request response + * + * @param requestId request id. + * @param response response. + */ + bool SetResponse(const std::string& requestId, const std::string& response); + +private: + std::mutex mMutex; + std::vector mRequests; +}; + +#endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2cb53006..867bc58e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,19 +15,34 @@ set(TARGET aos_iamanager_test) # Includes # ###################################################################################################################### +include(create-certs.cmake) include_directories(${CMAKE_SOURCE_DIR}/src) # ###################################################################################################################### # Sources # ###################################################################################################################### -set(SOURCES utils/exception_test.cpp utils/time_test.cpp database/database_test.cpp config/config_test.cpp) +set(SOURCES + config/config_test.cpp + database/database_test.cpp + utils/exception_test.cpp + utils/json_test.cpp + utils/time_test.cpp + utils/exception_test.cpp + utils/json_test.cpp + visidentifier/pocowsclient_test.cpp + visidentifier/visconfig_test.cpp + visidentifier/visidentifier_test.cpp + visidentifier/vismessage_test.cpp + visidentifier/visserver.cpp +) # ###################################################################################################################### # Target # ###################################################################################################################### add_executable(${TARGET} ${SOURCES}) +target_include_directories(${TARGET} PRIVATE include) gtest_discover_tests(${TARGET}) @@ -38,10 +53,15 @@ gtest_discover_tests(${TARGET}) target_link_libraries( ${TARGET} logger - utils - database - config + visidentifier aoscommon + config + database + utils + Poco::Crypto + Poco::Net + Poco::NetSSL + Poco::JSON Poco::Util GTest::gmock_main ) diff --git a/tests/create-certs.cmake b/tests/create-certs.cmake new file mode 100644 index 00000000..cc8cb69a --- /dev/null +++ b/tests/create-certs.cmake @@ -0,0 +1,61 @@ +# +# Copyright (C) 2024 Renesas Electronics Corporation. +# Copyright (C) 2024 EPAM Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# ###################################################################################################################### +# Create PKCS11 test certificates +# ###################################################################################################################### + +set(CERTIFICATES_DIR ${CMAKE_CURRENT_BINARY_DIR}/certificates) + +file(MAKE_DIRECTORY ${CERTIFICATES_DIR}) + +message("\nGenerating PKCS11 test certificates...") + +message("\nCreate a Certificate Authority private key...") +execute_process( + COMMAND openssl req -new -newkey rsa:2048 -nodes -out ${CERTIFICATES_DIR}/ca.csr -keyout ${CERTIFICATES_DIR}/ca.key + -nodes -subj "/CN=Aos Cloud" COMMAND_ERROR_IS_FATAL ANY +) + +message("\nCreate a CA self-signed certificate...") +execute_process( + COMMAND openssl x509 -signkey ${CERTIFICATES_DIR}/ca.key -days 365 -req -in ${CERTIFICATES_DIR}/ca.csr -out + ${CERTIFICATES_DIR}/ca.pem COMMAND_ERROR_IS_FATAL ANY +) + +message("\nIssue a client certificate...") +execute_process(COMMAND openssl genrsa -out ${CERTIFICATES_DIR}/client.key 2048 COMMAND_ERROR_IS_FATAL ANY) + +execute_process( + COMMAND openssl req -new -key ${CERTIFICATES_DIR}/client.key -out ${CERTIFICATES_DIR}/client.csr -nodes -subj + "/CN=Aos Core" COMMAND_ERROR_IS_FATAL ANY +) + +execute_process( + COMMAND openssl x509 -req -days 365 -in ${CERTIFICATES_DIR}/client.csr -CA ${CERTIFICATES_DIR}/ca.pem -CAkey + ${CERTIFICATES_DIR}/ca.key -set_serial 01 -out ${CERTIFICATES_DIR}/client.cer COMMAND_ERROR_IS_FATAL ANY +) + +message("\nConvert PEM to DER...") +execute_process( + COMMAND openssl x509 -outform der -in ${CERTIFICATES_DIR}/ca.pem -out ${CERTIFICATES_DIR}/ca.cer.der + COMMAND_ERROR_IS_FATAL ANY +) + +execute_process( + COMMAND openssl x509 -outform der -in ${CERTIFICATES_DIR}/client.cer -out ${CERTIFICATES_DIR}/client.cer.der + COMMAND_ERROR_IS_FATAL ANY +) + +message("\nCreate a single certificate chain file...") +file(WRITE ${CERTIFICATES_DIR}/client-ca-chain.pem "") +file(READ ${CERTIFICATES_DIR}/client.cer CONTENT) +file(APPEND ${CERTIFICATES_DIR}/client-ca-chain.pem "${CONTENT}") +file(READ ${CERTIFICATES_DIR}/ca.pem CONTENT) +file(APPEND ${CERTIFICATES_DIR}/client-ca-chain.pem "${CONTENT}") + +message("\nGenerating PKCS11 test certificates done!\n") diff --git a/tests/include/mocks/vissubjectsobservermock.hpp b/tests/include/mocks/vissubjectsobservermock.hpp new file mode 100644 index 00000000..66a4f128 --- /dev/null +++ b/tests/include/mocks/vissubjectsobservermock.hpp @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef VIS_SUBJECTS_OBSERVER_MOCK_HPP_ +#define VIS_SUBJECTS_OBSERVER_MOCK_HPP_ + +#include +#include +#include + +/** + * Subjects observer mock. + */ +class VisSubjectsObserverMock : public aos::iam::identhandler::SubjectsObserverItf { +public: + MOCK_METHOD(aos::Error, SubjectsChanged, (const aos::Array>&), (override)); +}; + +using VisSubjectsObserverMockPtr = std::shared_ptr; + +#endif diff --git a/tests/include/mocks/wsclientmock.hpp b/tests/include/mocks/wsclientmock.hpp new file mode 100644 index 00000000..fed23311 --- /dev/null +++ b/tests/include/mocks/wsclientmock.hpp @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef WS_CLIENT_MOCK_HPP_ +#define WS_CLIENT_MOCK_HPP_ + +#include "visidentifier/wsclient.hpp" +#include +#include + +/** + * Subjects observer mock. + */ +class WSClientMock : public WSClientItf { +public: + MOCK_METHOD(void, Connect, (), (override)); + MOCK_METHOD(void, Close, (), (override)); + MOCK_METHOD(void, Disconnect, (), (override)); + MOCK_METHOD(std::string, GenerateRequestID, (), (override)); + MOCK_METHOD(WSClientEvent::Details, WaitForEvent, (), (override)); + MOCK_METHOD(ByteArray, SendRequest, (const std::string&, const ByteArray&), (override)); + MOCK_METHOD(void, AsyncSendMessage, (const ByteArray&), (override)); +}; + +using WSClientMockPtr = std::shared_ptr; + +#endif diff --git a/tests/utils/json_test.cpp b/tests/utils/json_test.cpp new file mode 100644 index 00000000..cc4a4249 --- /dev/null +++ b/tests/utils/json_test.cpp @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024s EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include + +#include "logger/logger.hpp" +#include "utils/json.hpp" + +using namespace testing; + +/*********************************************************************************************************************** + * Static + **********************************************************************************************************************/ + +class JsonTest : public Test { }; + +/*********************************************************************************************************************** + * Tests + **********************************************************************************************************************/ + +namespace UtilsJson { + +TEST_F(JsonTest, ParseJsonSucceedsFromString) +{ + aos::Error err; + Poco::Dynamic::Var result; + + ASSERT_NO_THROW(aos::Tie(result, err) = ParseJson(R"({"key":"value"})")); + EXPECT_EQ(result.type(), typeid(Poco::JSON::Object::Ptr)); +} + +TEST_F(JsonTest, ParseJsonSucceedsFromStream) +{ + aos::Error err; + Poco::Dynamic::Var result; + std::istringstream in(R"({"key": "value"})"); + + ASSERT_TRUE(in.good()); + + ASSERT_NO_THROW(aos::Tie(result, err) = ParseJson(in)); + EXPECT_EQ(result.type(), typeid(Poco::JSON::Object::Ptr)); +} + +TEST_F(JsonTest, ParseJsonFailsFromString) +{ + aos::Error err; + Poco::Dynamic::Var result; + + ASSERT_NO_THROW(aos::Tie(result, err) = ParseJson("")); + EXPECT_TRUE(err.Is(aos::ErrorEnum::eInvalidArgument)); + EXPECT_TRUE(result.isEmpty()); +} + +TEST_F(JsonTest, ParseJsonFailsFromStream) +{ + aos::Error err; + Poco::Dynamic::Var result; + std::ifstream in; + + ASSERT_NO_THROW(aos::Tie(result, err) = ParseJson(in)); + EXPECT_TRUE(err.Is(aos::ErrorEnum::eInvalidArgument)); + EXPECT_TRUE(result.isEmpty()); +} + +TEST_F(JsonTest, FindByPathSucceeds) +{ + Poco::JSON::Object object; + object.set("key", "value"); + + auto res = FindByPath(object, std::vector {"key"}); + EXPECT_TRUE(res.isString()); + EXPECT_EQ(res.extract(), "value"); + + Poco::JSON::Object::Ptr objectPtr = new Poco::JSON::Object(object); + + res = FindByPath(objectPtr, std::vector {"key"}); + EXPECT_TRUE(res.isString()); + EXPECT_EQ(res.extract(), "value"); +} + +TEST_F(JsonTest, FindByPathSucceedsEmptyPath) +{ + Poco::JSON::Object object; + object.set("key", "value"); + + auto res = FindByPath(object, {}); + EXPECT_FALSE(res.isEmpty()); + EXPECT_EQ(res.type(), typeid(object)); +} + +TEST_F(JsonTest, FindByPathSucceedsOnNestedJson) +{ + Poco::JSON::Object value; + value.set("key", "value"); + value.set("aos.key", "aos.value"); + + Poco::JSON::Object object; + object.set("data", value); + + auto res = FindByPath(object, {"data", "aos.key"}); + ASSERT_TRUE(res.isString()); + EXPECT_EQ(res.extract(), "aos.value"); + + res = FindByPath(object, {"data", "key"}); + ASSERT_TRUE(res.isString()); + EXPECT_EQ(res.extract(), "value"); +} + +TEST_F(JsonTest, FindByPathFails) +{ + Poco::JSON::Object value; + value.set("key", "value"); + + Poco::JSON::Object object; + object.set("data", value); + + auto res = FindByPath(object, {"key"}); + EXPECT_TRUE(res.isEmpty()); + + res = FindByPath(object, {"data", "key", "doesnt-exist"}); + EXPECT_TRUE(res.isEmpty()); + + res = FindByPath(Poco::Dynamic::Var(), {"data", "key", "doesnt-exist"}); + EXPECT_TRUE(res.isEmpty()); +} + +} // namespace UtilsJson diff --git a/tests/visidentifier/pocowsclient_test.cpp b/tests/visidentifier/pocowsclient_test.cpp new file mode 100644 index 00000000..70bdd48e --- /dev/null +++ b/tests/visidentifier/pocowsclient_test.cpp @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include + +#include "logger/logger.hpp" +#include "mocks/vissubjectsobservermock.hpp" +#include "visidentifier/pocowsclient.hpp" +#include "visidentifier/visidentifier.hpp" +#include "visidentifier/wsexception.hpp" +#include "visserver.hpp" + +using namespace testing; + +/*********************************************************************************************************************** + * Static + **********************************************************************************************************************/ + +static const std::string cWebSockerURI("wss://localhost:4566"); +static const std::string cServerCertPath("certificates/ca.pem"); +static const std::string cServerKeyPath("certificates/ca.key"); +static const std::string cClientCertPath {"certificates/client.cer"}; + +/*********************************************************************************************************************** + * Suite + **********************************************************************************************************************/ + +class PocoWSClientTests : public Test { +protected: + static const VISConfig cConfig; + + void SetUp() override + { + ASSERT_NO_THROW(mWsClientPtr = std::make_shared(cConfig, WSClientItf::MessageHandlerFunc())); + } + + // This method is called before any test cases in the test suite + static void SetUpTestSuite() + { + static Logger mLogger; + + mLogger.SetBackend(Logger::Backend::eStdIO); + mLogger.SetLogLevel(aos::LogLevelEnum::eDebug); + mLogger.Init(); + + Poco::Net::initializeSSL(); + + VISWebSocketServer::Instance().Start(cServerKeyPath, cServerCertPath, cWebSockerURI); + + ASSERT_TRUE(VISWebSocketServer::Instance().TryWaitServiceStart()); + } + + static void TearDownTestSuite() + { + VISWebSocketServer::Instance().Stop(); + + Poco::Net::uninitializeSSL(); + } + + std::shared_ptr mWsClientPtr; +}; + +const VISConfig PocoWSClientTests::cConfig {cWebSockerURI, cClientCertPath, std::chrono::seconds(5)}; + +/*********************************************************************************************************************** + * Tests + **********************************************************************************************************************/ + +TEST_F(PocoWSClientTests, Connect) +{ + ASSERT_NO_THROW(mWsClientPtr->Connect()); + ASSERT_NO_THROW(mWsClientPtr->Connect()); +} + +TEST_F(PocoWSClientTests, Close) +{ + ASSERT_NO_THROW(mWsClientPtr->Connect()); + ASSERT_NO_THROW(mWsClientPtr->Close()); + ASSERT_NO_THROW(mWsClientPtr->Close()); +} + +TEST_F(PocoWSClientTests, Disconnect) +{ + ASSERT_NO_THROW(mWsClientPtr->Disconnect()); + + ASSERT_NO_THROW(mWsClientPtr->Connect()); + ASSERT_NO_THROW(mWsClientPtr->Disconnect()); +} + +TEST_F(PocoWSClientTests, GenerateRequestID) +{ + std::string requestId; + ASSERT_NO_THROW(requestId = mWsClientPtr->GenerateRequestID()); + ASSERT_FALSE(requestId.empty()); +} + +TEST_F(PocoWSClientTests, AsyncSendMessageSucceeds) +{ + const WSClientItf::ByteArray message = {'t', 'e', 's', 't'}; + + ASSERT_NO_THROW(mWsClientPtr->Connect()); + ASSERT_NO_THROW(mWsClientPtr->AsyncSendMessage(message)); +} + +TEST_F(PocoWSClientTests, AsyncSendMessageNotConnected) +{ + try { + const WSClientItf::ByteArray message = {'t', 'e', 's', 't'}; + + mWsClientPtr->AsyncSendMessage(message); + } catch (const WSException& e) { + EXPECT_EQ(e.GetError(), aos::ErrorEnum::eFailed); + } catch (...) { + FAIL() << "WSException expected"; + } +} + +TEST_F(PocoWSClientTests, AsyncSendMessageFails) +{ + mWsClientPtr->Connect(); + + TearDownTestSuite(); + + try { + const WSClientItf::ByteArray message = {'t', 'e', 's', 't'}; + + mWsClientPtr->AsyncSendMessage(message); + } catch (const WSException& e) { + EXPECT_EQ(e.GetError(), aos::ErrorEnum::eFailed); + } catch (...) { + FAIL() << "WSException expected"; + } + + SetUpTestSuite(); +} + +TEST_F(PocoWSClientTests, VisidentifierGetSystemID) +{ + VISIdentifier visIdentifier; + + Config config; + config.mIdentifier.mParams = cConfig.ToJSON(); + + auto observerPtr = std::make_shared(); + + auto err = visIdentifier.Init(config, observerPtr); + ASSERT_TRUE(err.IsNone()) << err.Message(); + + const std::string expectedSystemId {"test-system-id"}; + VISParams::Instance().Set("Attribute.Vehicle.VehicleIdentification.VIN", expectedSystemId); + + const auto systemId = visIdentifier.GetSystemID(); + EXPECT_TRUE(systemId.mError.IsNone()) << systemId.mError.Message(); + EXPECT_STREQ(systemId.mValue.CStr(), expectedSystemId.c_str()); +} + +TEST_F(PocoWSClientTests, VisidentifierGetUnitModel) +{ + VISIdentifier visIdentifier; + + Config config; + config.mIdentifier.mParams = cConfig.ToJSON(); + + auto observerPtr = std::make_shared(); + + auto err = visIdentifier.Init(config, observerPtr); + ASSERT_TRUE(err.IsNone()) << err.Message(); + + const std::string expectedUnitModel {"test-unit-model"}; + VISParams::Instance().Set("Attribute.Aos.UnitModel", expectedUnitModel); + + const auto unitModel = visIdentifier.GetUnitModel(); + EXPECT_TRUE(unitModel.mError.IsNone()) << unitModel.mError.Message(); + EXPECT_STREQ(unitModel.mValue.CStr(), expectedUnitModel.c_str()); +} + +TEST_F(PocoWSClientTests, VisidentifierGetSubjects) +{ + VISIdentifier visIdentifier; + + Config config; + config.mIdentifier.mParams = cConfig.ToJSON(); + + auto observerPtr = std::make_shared(); + + auto err = visIdentifier.Init(config, observerPtr); + ASSERT_TRUE(err.IsNone()) << err.Message(); + + const std::vector testSubjects {"1", "2", "3"}; + VISParams::Instance().Set("Attribute.Aos.Subjects", testSubjects); + aos::StaticArray, 3> expectedSubjects; + + for (const auto& testSubject : testSubjects) { + expectedSubjects.PushBack(testSubject.c_str()); + } + + aos::StaticArray, 3> receivedSubjects; + + err = visIdentifier.GetSubjects(receivedSubjects); + ASSERT_TRUE(err.IsNone()) << err.Message(); + + ASSERT_EQ(receivedSubjects, expectedSubjects); +} diff --git a/tests/visidentifier/visconfig_test.cpp b/tests/visidentifier/visconfig_test.cpp new file mode 100644 index 00000000..e5cb3ca9 --- /dev/null +++ b/tests/visidentifier/visconfig_test.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include + +#include "visidentifier/visconfig.hpp" + +using namespace testing; + +/*********************************************************************************************************************** + * Suite + **********************************************************************************************************************/ + +class VISConfigTest : public Test { +protected: + Poco::JSON::Object mConfig; + + void SetUp() override + { + mConfig.set("visServer", "vis-server"); + mConfig.set("caCertFile", "ca-file"); + mConfig.set("webSocketTimeout", "10s"); + } +}; + +/*********************************************************************************************************************** + * Tests + **********************************************************************************************************************/ + +TEST_F(VISConfigTest, InitSucceeds) +{ + VISConfig visConfig; + + auto err = visConfig.Init(mConfig); + ASSERT_TRUE(err.IsNone()) << err.Message(); +} + +TEST_F(VISConfigTest, InitFailsVisServerKeyNotFound) +{ + VISConfig visConfig; + + mConfig.remove("visServer"); + + auto err = visConfig.Init(mConfig); + ASSERT_TRUE(err.Is(aos::ErrorEnum::eNotFound)) << err.Message(); +} + +TEST_F(VISConfigTest, InitFailsVisServerInvalidFormat) +{ + VISConfig visConfig; + + mConfig.set("visServer", 100); + + auto err = visConfig.Init(mConfig); + ASSERT_TRUE(err.Is(aos::ErrorEnum::eInvalidArgument)) << err.Message(); +} + +TEST_F(VISConfigTest, InitFailsInvalidWebSocketTimeout) +{ + VISConfig visConfig; + + mConfig.set("webSocketTimeout", "invalid format"); + + auto err = visConfig.Init(mConfig); + ASSERT_TRUE(err.Is(aos::ErrorEnum::eInvalidArgument)) << err.Message(); +} + +TEST_F(VISConfigTest, ToJSON) +{ + VISConfig visConfig; + + auto err = visConfig.Init(mConfig); + ASSERT_TRUE(err.IsNone()) << err.Message(); + + ASSERT_FALSE(visConfig.ToJSON().isEmpty()); +} + +TEST_F(VISConfigTest, ToString) +{ + VISConfig visConfig; + + auto err = visConfig.Init(mConfig); + ASSERT_TRUE(err.IsNone()) << err.Message(); + + ASSERT_FALSE(visConfig.ToString().empty()); +} diff --git a/tests/visidentifier/visidentifier_test.cpp b/tests/visidentifier/visidentifier_test.cpp new file mode 100644 index 00000000..76d904cf --- /dev/null +++ b/tests/visidentifier/visidentifier_test.cpp @@ -0,0 +1,446 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024s EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "logger/logger.hpp" +#include "mocks/vissubjectsobservermock.hpp" +#include "mocks/wsclientmock.hpp" +#include "visidentifier/pocowsclient.hpp" +#include "visidentifier/visidentifier.hpp" +#include "visidentifier/vismessage.hpp" +#include "visidentifier/wsexception.hpp" + +using namespace testing; + +/*********************************************************************************************************************** + * Static + **********************************************************************************************************************/ + +class TestVISIdentifier : public VISIdentifier { + +public: + void SetWSClient(WSClientItfPtr wsClient) { VISIdentifier::SetWSClient(wsClient); } + WSClientItfPtr GetWSClient() { return VISIdentifier::GetWSClient(); } + void HandleSubscription(const std::string& message) { return VISIdentifier::HandleSubscription(message); } + void WaitUntilConnected() { VISIdentifier::WaitUntilConnected(); } + + MOCK_METHOD(aos::Error, InitWSClient, (const Config&), (override)); +}; + +/*********************************************************************************************************************** + * Suite + **********************************************************************************************************************/ + +class VisidentifierTest : public testing::Test { +protected: + const std::string cTestSubscriptionId {"1234-4321"}; + const VISConfig cVISConfig {"vis-service", "ca-path", UtilsTime::Duration(1)}; + + WSClientEvent mWSClientEvent; + VisSubjectsObserverMockPtr mVisSubjectsObserverMockPtr {std::make_shared>()}; + WSClientMockPtr mWSClientItfMockPtr {std::make_shared>()}; + TestVISIdentifier mVisIdentifier; + Config mConfig; + + // This method is called before any test cases in the test suite + static void SetUpTestSuite() + { + static Logger mLogger; + + mLogger.SetBackend(Logger::Backend::eStdIO); + mLogger.SetLogLevel(aos::LogLevelEnum::eDebug); + mLogger.Init(); + } + + void SetUp() override + { + mConfig.mIdentifier.mParams = cVISConfig.ToJSON(); + + mVisIdentifier.SetWSClient(mWSClientItfMockPtr); + } + + void TearDown() override + { + if (mVisIdentifier.GetWSClient() != nullptr) { + ExpectUnsubscribeAllIsSent(); + + // ws closed + EXPECT_CALL(*mWSClientItfMockPtr, Close).WillOnce(Invoke([this] { + mWSClientEvent.Set(WSClientEvent::EventEnum::CLOSED, "mock closed"); + })); + } + } + + void ExpectSubscribeSucceeded() + { + EXPECT_CALL(*mWSClientItfMockPtr, GenerateRequestID).Times(1); + EXPECT_CALL(*mWSClientItfMockPtr, SendRequest) + .Times(1) + .WillOnce( + Invoke([this](const std::string&, const WSClientItf::ByteArray& message) -> WSClientItf::ByteArray { + try { + const VISMessage request(std::string {message.cbegin(), message.cend()}); + + EXPECT_TRUE(request.Is(VISAction::EnumType::eSubscribe)) << request.ToString(); + + VISMessage subscribeResponse(VISActionEnum::eSubscribe); + + subscribeResponse.SetKeyValue("requestId", "request-id"); + subscribeResponse.SetKeyValue("subscriptionId", cTestSubscriptionId); + + const auto str = subscribeResponse.ToString(); + + return {str.cbegin(), str.cend()}; + } catch (...) { + return {}; + } + })); + } + + void ExpectInitSucceeded() + { + mVisIdentifier.SetWSClient(mWSClientItfMockPtr); + + ExpectSubscribeSucceeded(); + EXPECT_CALL(*mWSClientItfMockPtr, Connect).Times(1); + EXPECT_CALL(mVisIdentifier, InitWSClient).WillOnce(Return(aos::ErrorEnum::eNone)); + EXPECT_CALL(*mWSClientItfMockPtr, WaitForEvent).WillOnce(Invoke([this]() { return mWSClientEvent.Wait(); })); + + const auto err = mVisIdentifier.Init(mConfig, mVisSubjectsObserverMockPtr); + ASSERT_TRUE(err.IsNone()) << err.Message(); + + mVisIdentifier.WaitUntilConnected(); + } + + void ExpectUnsubscribeAllIsSent() + { + EXPECT_CALL(*mWSClientItfMockPtr, GenerateRequestID).Times(1); + EXPECT_CALL(*mWSClientItfMockPtr, AsyncSendMessage) + .Times(1) + .WillOnce(Invoke([&](const WSClientItf::ByteArray& message) { + try { + VISMessage visMessage(std::string {message.cbegin(), message.cend()}); + + ASSERT_TRUE(visMessage.Is(VISAction::EnumType::eUnsubscribeAll)); + } catch (...) { + FAIL() << "exception was not expected"; + } + })); + } +}; + +/*********************************************************************************************************************** + * Tests + **********************************************************************************************************************/ + +TEST_F(VisidentifierTest, InitFailsOnEmptyConfig) +{ + VISIdentifier identifier; + + const auto err = identifier.Init(Config {}, mVisSubjectsObserverMockPtr); + ASSERT_FALSE(err.IsNone()) << err.Message(); +} + +TEST_F(VisidentifierTest, InitSubjectsObserverNotAccessible) +{ + mVisSubjectsObserverMockPtr.reset(); + + EXPECT_CALL(mVisIdentifier, InitWSClient).Times(0); + + const auto err = mVisIdentifier.Init(mConfig, mVisSubjectsObserverMockPtr); + ASSERT_TRUE(err.Is(aos::ErrorEnum::eInvalidArgument)) << err.Message(); +} + +TEST_F(VisidentifierTest, SubscriptionNotificationReceivedAndObserverIsNotified) +{ + ExpectInitSucceeded(); + + aos::StaticArray, 3> subjects; + + EXPECT_CALL(*mVisSubjectsObserverMockPtr, SubjectsChanged) + .Times(1) + .WillOnce(Invoke([&subjects](const auto& newSubjects) { + subjects = newSubjects; + + return aos::ErrorEnum::eNone; + })); + + const std::string kSubscriptionNofiticationJson + = R"({"action":"subscription","subscriptionId":"1234-4321","value":[11,12,13], "timestamp": 0})"; + + mVisIdentifier.HandleSubscription(kSubscriptionNofiticationJson); + + EXPECT_EQ(subjects.Size(), 3); + + // Observer is notified only if subsription json contains new value + for (size_t i {0}; i < 3; ++i) { + EXPECT_CALL(*mVisSubjectsObserverMockPtr, SubjectsChanged).Times(0); + mVisIdentifier.HandleSubscription(kSubscriptionNofiticationJson); + } +} + +TEST_F(VisidentifierTest, SubscriptionNotificationNestedJsonReceivedAndObserverIsNotified) +{ + ExpectInitSucceeded(); + + aos::StaticArray, 3> subjects; + + EXPECT_CALL(*mVisSubjectsObserverMockPtr, SubjectsChanged) + .Times(1) + .WillOnce(Invoke([&subjects](const auto& newSubjects) { + subjects = newSubjects; + + return aos::ErrorEnum::eNone; + })); + + const std::string kSubscriptionNofiticationJson + = R"({"action":"subscription","subscriptionId":"1234-4321","value":{"Attribute.Aos.Subjects": [11,12,13]}, "timestamp": 0})"; + + mVisIdentifier.HandleSubscription(kSubscriptionNofiticationJson); + + EXPECT_EQ(subjects.Size(), 3); + + // Observer is notified only if subsription json contains new value + for (size_t i {0}; i < 3; ++i) { + EXPECT_CALL(*mVisSubjectsObserverMockPtr, SubjectsChanged).Times(0); + mVisIdentifier.HandleSubscription(kSubscriptionNofiticationJson); + } +} + +TEST_F(VisidentifierTest, SubscriptionNotificationReceivedUnknownSubscriptionId) +{ + ExpectInitSucceeded(); + + EXPECT_CALL(*mVisSubjectsObserverMockPtr, SubjectsChanged).Times(0); + + mVisIdentifier.HandleSubscription( + R"({"action":"subscription","subscriptionId":"unknown-subscriptionId","value":[11,12,13], "timestamp": 0})"); +} + +TEST_F(VisidentifierTest, SubscriptionNotificationReceivedInvalidPayload) +{ + ExpectInitSucceeded(); + + EXPECT_CALL(*mVisSubjectsObserverMockPtr, SubjectsChanged).Times(0); + + ASSERT_NO_THROW(mVisIdentifier.HandleSubscription(R"({cActionTagName})")); +} + +TEST_F(VisidentifierTest, SubscriptionNotificationValueExceedsMaxLimit) +{ + ExpectInitSucceeded(); + + EXPECT_CALL(*mVisSubjectsObserverMockPtr, SubjectsChanged).Times(0); + + Poco::JSON::Object notification; + + notification.set("action", "subscription"); + notification.set("timestamp", 0); + notification.set("subscriptionId", cTestSubscriptionId); + notification.set("value", std::vector(aos::cMaxSubjectIDSize + 1, "test")); + + std::ostringstream jsonStream; + Poco::JSON::Stringifier::stringify(notification, jsonStream); + + ASSERT_NO_THROW(mVisIdentifier.HandleSubscription(jsonStream.str())); +} + +TEST_F(VisidentifierTest, ReconnectOnFailSendFrame) +{ + EXPECT_CALL(mVisIdentifier, InitWSClient).WillRepeatedly(Return(aos::ErrorEnum::eNone)); + EXPECT_CALL(*mWSClientItfMockPtr, Disconnect).Times(1); + EXPECT_CALL(*mWSClientItfMockPtr, Connect).Times(2); + + EXPECT_CALL(*mWSClientItfMockPtr, WaitForEvent).WillOnce(Invoke([this]() { return mWSClientEvent.Wait(); })); + + EXPECT_CALL(*mWSClientItfMockPtr, GenerateRequestID).Times(2); + EXPECT_CALL(*mWSClientItfMockPtr, SendRequest) + .Times(2) + .WillOnce(Invoke([](const std::string&, const WSClientItf::ByteArray&) -> WSClientItf::ByteArray { + throw WSException("mock"); + })) + .WillOnce(Invoke([this](const std::string&, const WSClientItf::ByteArray&) -> WSClientItf::ByteArray { + VISMessage message(VISActionEnum::eSubscribe); + + message.SetKeyValue("requestId", "id"); + message.SetKeyValue("subscriptionId", cTestSubscriptionId); + message.SetKeyValue("path", "p"); + + const auto str = message.ToString(); + + return {str.cbegin(), str.cend()}; + })); + + const auto err = mVisIdentifier.Init(mConfig, mVisSubjectsObserverMockPtr); + ASSERT_TRUE(err.IsNone()) << err.Message(); + + mVisIdentifier.WaitUntilConnected(); +} + +TEST_F(VisidentifierTest, GetSystemIDSucceds) +{ + ExpectInitSucceeded(); + + const std::string cExpectedSystemId {"expectedSystemId"}; + + EXPECT_CALL(*mWSClientItfMockPtr, GenerateRequestID).Times(1); + EXPECT_CALL(*mWSClientItfMockPtr, SendRequest) + .WillOnce(Invoke([&](const std::string&, const WSClientItf::ByteArray&) -> WSClientItf::ByteArray { + Poco::JSON::Object response; + + response.set("action", "get"); + response.set("requestId", "requestId"); + response.set("timestamp", 0); + response.set("value", cExpectedSystemId); + + std::ostringstream jsonStream; + Poco::JSON::Stringifier::stringify(response, jsonStream); + + const auto str = jsonStream.str(); + + return {str.cbegin(), str.cend()}; + })); + + aos::StaticString systemId; + aos::Error err; + + Tie(systemId, err) = mVisIdentifier.GetSystemID(); + EXPECT_TRUE(err.IsNone()) << err.Message(); + EXPECT_STREQ(systemId.CStr(), cExpectedSystemId.c_str()); +} + +TEST_F(VisidentifierTest, GetSystemIDNestedValueTagSucceds) +{ + ExpectInitSucceeded(); + + const std::string cExpectedSystemId {"expectedSystemId"}; + + EXPECT_CALL(*mWSClientItfMockPtr, GenerateRequestID).Times(1); + EXPECT_CALL(*mWSClientItfMockPtr, SendRequest) + .WillOnce(Invoke([&](const std::string&, const WSClientItf::ByteArray&) -> WSClientItf::ByteArray { + Poco::JSON::Object valueTag; + valueTag.set("Attribute.Vehicle.VehicleIdentification.VIN", cExpectedSystemId); + + Poco::JSON::Object response; + + response.set("action", "get"); + response.set("requestId", "requestId"); + response.set("timestamp", 0); + response.set("value", valueTag); + + std::ostringstream jsonStream; + Poco::JSON::Stringifier::stringify(response, jsonStream); + + const auto str = jsonStream.str(); + + return {str.cbegin(), str.cend()}; + })); + + aos::StaticString systemId; + aos::Error err; + + Tie(systemId, err) = mVisIdentifier.GetSystemID(); + EXPECT_TRUE(err.IsNone()) << err.Message(); + EXPECT_STREQ(systemId.CStr(), cExpectedSystemId.c_str()); +} + +TEST_F(VisidentifierTest, GetSystemIDExceedsMaxSize) +{ + ExpectInitSucceeded(); + + EXPECT_CALL(*mWSClientItfMockPtr, GenerateRequestID).Times(1); + EXPECT_CALL(*mWSClientItfMockPtr, SendRequest) + .WillOnce(Invoke([](const std::string&, const WSClientItf::ByteArray&) -> WSClientItf::ByteArray { + Poco::JSON::Object response; + + response.set("action", "get"); + response.set("requestId", "requestId"); + response.set("timestamp", 0); + response.set("value", std::string(aos::cSystemIDLen + 1, '1')); + + std::ostringstream jsonStream; + Poco::JSON::Stringifier::stringify(response, jsonStream); + + const auto str = jsonStream.str(); + + return {str.cbegin(), str.cend()}; + })); + + const auto err = mVisIdentifier.GetSystemID(); + EXPECT_TRUE(err.mError.Is(aos::ErrorEnum::eNoMemory)) << err.mError.Message(); +} + +TEST_F(VisidentifierTest, GetSystemIDRequestFailed) +{ + ExpectInitSucceeded(); + + EXPECT_CALL(*mWSClientItfMockPtr, GenerateRequestID).Times(1); + EXPECT_CALL(*mWSClientItfMockPtr, SendRequest) + .WillOnce(Invoke([](const std::string&, const WSClientItf::ByteArray&) -> WSClientItf::ByteArray { + throw WSException("mock"); + })); + + const auto err = mVisIdentifier.GetSystemID(); + EXPECT_TRUE(err.mError.Is(aos::ErrorEnum::eFailed)) << err.mError.Message(); +} + +TEST_F(VisidentifierTest, GetUnitModelExceedsMaxSize) +{ + ExpectInitSucceeded(); + + EXPECT_CALL(*mWSClientItfMockPtr, GenerateRequestID).Times(1); + EXPECT_CALL(*mWSClientItfMockPtr, SendRequest) + .WillOnce(Invoke([](const std::string&, const WSClientItf::ByteArray&) -> WSClientItf::ByteArray { + Poco::JSON::Object response; + + response.set("action", "get"); + response.set("requestId", "test-requestId"); + response.set("timestamp", 0); + response.set("value", std::string(aos::cUnitModelLen + 1, '1')); + + std::ostringstream jsonStream; + Poco::JSON::Stringifier::stringify(response, jsonStream); + + const auto str = jsonStream.str(); + + return {str.cbegin(), str.cend()}; + })); + + const auto err = mVisIdentifier.GetUnitModel(); + EXPECT_TRUE(err.mError.Is(aos::ErrorEnum::eNoMemory)) << err.mError.Message(); +} + +TEST_F(VisidentifierTest, GetUnitModelRequestFailed) +{ + ExpectInitSucceeded(); + + EXPECT_CALL(*mWSClientItfMockPtr, GenerateRequestID).Times(1); + EXPECT_CALL(*mWSClientItfMockPtr, SendRequest) + .WillOnce(Invoke([](const std::string&, const WSClientItf::ByteArray&) -> WSClientItf::ByteArray { + throw WSException("mock"); + })); + + const auto err = mVisIdentifier.GetUnitModel(); + EXPECT_TRUE(err.mError.Is(aos::ErrorEnum::eFailed)) << err.mError.Message(); +} + +TEST_F(VisidentifierTest, GetSubjectsRequestFailed) +{ + ExpectInitSucceeded(); + + EXPECT_CALL(*mWSClientItfMockPtr, GenerateRequestID).Times(1); + EXPECT_CALL(*mWSClientItfMockPtr, SendRequest) + .WillOnce(Invoke([](const std::string&, const WSClientItf::ByteArray&) -> WSClientItf::ByteArray { + throw WSException("mock"); + })); + + aos::StaticArray, aos::cMaxSubjectIDSize> subjects; + const auto err = mVisIdentifier.GetSubjects(subjects); + EXPECT_TRUE(err.Is(aos::ErrorEnum::eFailed)); + EXPECT_TRUE(subjects.IsEmpty()); +} diff --git a/tests/visidentifier/vismessage_test.cpp b/tests/visidentifier/vismessage_test.cpp new file mode 100644 index 00000000..51161f03 --- /dev/null +++ b/tests/visidentifier/vismessage_test.cpp @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024s EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "visidentifier/vismessage.hpp" + +/*********************************************************************************************************************** + * Static + **********************************************************************************************************************/ + +class VISMessageTest : public testing::Test { }; + +/*********************************************************************************************************************** + * Tests + **********************************************************************************************************************/ + +TEST_F(VISMessageTest, ConstructFromJson) +{ + Poco::JSON::Object json; + + json.set("action", "get"); + json.set("path", "test-path"); + json.set("requestId", "test-request-id"); + + std::ostringstream jsonStream; + Poco::JSON::Stringifier::stringify(json, jsonStream); + + VISMessage message(jsonStream.str()); + + EXPECT_EQ(json.size(), message.GetJSON().size()); + + EXPECT_EQ(message.GetValue(VISMessage::cActionTagName), "get"); + EXPECT_EQ(message.GetValue(VISMessage::cPathTagName), "test-path"); + EXPECT_EQ(message.GetValue(VISMessage::cRequestIdTagName), "test-request-id"); +} + +TEST_F(VISMessageTest, IsSucceeds) +{ + const VISMessage message(VISActionEnum::eGet); + + EXPECT_TRUE(message.Is(VISActionEnum::eGet)); +} + +TEST_F(VISMessageTest, IsFails) +{ + const VISMessage message(VISActionEnum::eGet); + + EXPECT_FALSE(message.Is(VISActionEnum::eSubscribe)); +} + +TEST_F(VISMessageTest, ConstructorSetsAction) +{ + const VISMessage message(VISActionEnum::eGet); + + std::string actionTagValue; + + ASSERT_NO_THROW(actionTagValue = message.GetValue(VISMessage::cActionTagName)); + EXPECT_EQ(actionTagValue, "get"); +} + +TEST_F(VISMessageTest, SetKeyValue) +{ + VISMessage message(VISActionEnum::eGet); + + message.SetKeyValue("key", "value"); + EXPECT_EQ(message.GetValue("key"), "value"); + + message.SetKeyValue("key", "value1"); + EXPECT_EQ(message.GetValue("key"), "value1"); + + message.SetKeyValue("key", 10); + EXPECT_EQ(message.GetValue("key"), 10); +} + +TEST_F(VISMessageTest, GetValueThrowsOnKeyNotFound) +{ + VISMessage message(VISActionEnum::eGet); + + ASSERT_THROW(message.GetValue("key-not-found"), Poco::Exception); +} + +TEST_F(VISMessageTest, GetValueThrowsOnInvalidGetType) +{ + VISMessage message(VISActionEnum::eGet); + + message.SetKeyValue("key", "str10"); + + ASSERT_THROW(message.GetValue("key"), Poco::Exception); +} diff --git a/tests/visidentifier/visserver.cpp b/tests/visidentifier/visserver.cpp new file mode 100644 index 00000000..3c935382 --- /dev/null +++ b/tests/visidentifier/visserver.cpp @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024s EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "logger/logger.hpp" +#include "visidentifier/vismessage.hpp" +#include "visserver.hpp" + +/*********************************************************************************************************************** + * Static + **********************************************************************************************************************/ + +#define LOG_DBG() LOG_MODULE_DBG(AosLogModule(LogModuleEnum::eNumModules)) +#define LOG_INF() LOG_MODULE_INF(AosLogModule(LogModuleEnum::eNumModules)) +#define LOG_WRN() LOG_MODULE_WRN(AosLogModule(LogModuleEnum::eNumModules)) +#define LOG_ERR() LOG_MODULE_ERR(AosLogModule(LogModuleEnum::eNumModules)) + +static const std::string cLogPrefix = "[Test VIS Service]"; + +/*********************************************************************************************************************** + * VISParams + **********************************************************************************************************************/ + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +void VISParams::Set(const std::string& key, const std::string& value) +{ + std::lock_guard lock(mMutex); + + mMap[key] = {value}; +} + +void VISParams::Set(const std::string& key, const std::vector& values) +{ + std::lock_guard lock(mMutex); + + mMap[key] = values; +} + +std::vector VISParams::Get(const std::string& key) +{ + std::lock_guard lock(mMutex); + + if (const auto it = mMap.find(key); it != mMap.end()) { + return it->second; + } + + throw std::runtime_error("key not found"); +} + +VISParams& VISParams::Instance() +{ + static VISParams instance; + return instance; +} + +/*********************************************************************************************************************** + * WebSocketRequestHandler + **********************************************************************************************************************/ + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +void WebSocketRequestHandler::handleRequest( + Poco::Net::HTTPServerRequest& request, Poco::Net::HTTPServerResponse& response) +{ + try { + Poco::Net::WebSocket ws(request, response); + + LOG_INF() << Poco::format("%s Web Socket has been connection established.", cLogPrefix).c_str(); + + int flags; + int n; + Poco::Buffer buffer(0); + + do { + n = ws.receiveFrame(buffer, flags); + + if (n == 0) { + continue; + } else if ((flags & Poco::Net::WebSocket::FRAME_OP_BITMASK) == Poco::Net::WebSocket::FRAME_OP_CLOSE) { + ws.sendFrame(nullptr, 0, flags); + break; + } + + const std::string frameStr(buffer.begin(), buffer.end()); + + buffer.resize(0); + + LOG_DBG() << Poco::format( + "%s Frame received (val=%s, length=%d, flags=0x%x).", cLogPrefix, frameStr, n, unsigned(flags)) + .c_str(); + + const auto responseFrame = handleFrame(frameStr); + + ws.sendFrame(responseFrame.c_str(), responseFrame.length(), flags); + } while (n > 0 && (flags & Poco::Net::WebSocket::FRAME_OP_BITMASK) != Poco::Net::WebSocket::FRAME_OP_CLOSE); + + LOG_DBG() << Poco::format("%s Web Socket has been closed.", cLogPrefix).c_str(); + + } catch (const Poco::Net::WebSocketException& exc) { + LOG_ERR() << Poco::format("%s Caught exception: messag = %s.", cLogPrefix, exc.what()).c_str(); + + switch (exc.code()) { + case Poco::Net::WebSocket::WS_ERR_HANDSHAKE_UNSUPPORTED_VERSION: + response.set("Sec-WebSocket-Version", Poco::Net::WebSocket::WEBSOCKET_VERSION); + // fallthrough + case Poco::Net::WebSocket::WS_ERR_NO_HANDSHAKE: + case Poco::Net::WebSocket::WS_ERR_HANDSHAKE_NO_VERSION: + case Poco::Net::WebSocket::WS_ERR_HANDSHAKE_NO_KEY: + response.setStatusAndReason(Poco::Net::HTTPResponse::HTTP_BAD_REQUEST); + response.setContentLength(0); + response.send(); + + break; + } + } +} + +/*********************************************************************************************************************** + * Private + **********************************************************************************************************************/ + +std::string WebSocketRequestHandler::handleGetRequest(const VISMessage& request) +{ + VISMessage response(VISAction::EnumType::eGet, request.GetValue(VISMessage::cRequestIdTagName), ""); + + Poco::JSON::Array valueArray; + + for (const auto& value : VISParams::Instance().Get(request.GetValue(VISMessage::cPathTagName))) { + valueArray.add(value); + } + + if (valueArray.size() > 1) { + response.SetKeyValue("value", valueArray); + } else { + response.SetKeyValue("value", valueArray.empty() ? "" : valueArray.begin()->extract()); + } + + return response.ToString(); +} + +std::string WebSocketRequestHandler::handleSubscribeRequest(const VISMessage& request) +{ + static uint32_t lastSubscribeId {0}; + + const auto requestId = request.GetValue(VISMessage::cRequestIdTagName); + const auto subscriptionId = std::to_string(lastSubscribeId++); + + VISMessage response(VISAction::EnumType::eSubscribe); + + response.SetKeyValue(VISMessage::cRequestIdTagName, requestId); + response.SetKeyValue(VISMessage::cSubscriptionIdTagName, subscriptionId); + + return response.ToString(); +} + +std::string WebSocketRequestHandler::handleUnsubscribeAllRequest(const VISMessage& request) +{ + return request.ToString(); +} + +std::string WebSocketRequestHandler::handleFrame(const std::string& frame) +{ + const VISMessage request(frame); + + if (request.Is(VISActionEnum::eGet)) { + return handleGetRequest(frame); + } else if (request.Is(VISActionEnum::eSubscribe)) { + return handleSubscribeRequest(frame); + } else if (request.Is(VISActionEnum::eUnsubscribeAll)) { + return handleUnsubscribeAllRequest(frame); + } + + return frame; +} + +/*********************************************************************************************************************** + * RequestHandlerFactory + **********************************************************************************************************************/ + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +Poco::Net::HTTPRequestHandler* RequestHandlerFactory::createRequestHandler(const Poco::Net::HTTPServerRequest&) +{ + return new WebSocketRequestHandler; +} + +/*********************************************************************************************************************** + * VISWebSocketServer + **********************************************************************************************************************/ + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +VISWebSocketServer& VISWebSocketServer::Instance() +{ + static VISWebSocketServer wsServer; + + return wsServer; +} + +void VISWebSocketServer::Start(const std::string& keyPath, const std::string& certPath, const std::string& uriStr) +{ + std::lock_guard lock(mMutex); + + Stop(); + + mThread = std::thread(&VISWebSocketServer::RunServiceThreadF, this, keyPath, certPath, uriStr); +} + +void VISWebSocketServer::Stop() +{ + std::lock_guard lock(mMutex); + + if (mThread.joinable()) { + mStopEvent.set(); + + mThread.join(); + } +} + +bool VISWebSocketServer::TryWaitServiceStart(const long timeout) +{ + return mStartEvent.tryWait(timeout); +} + +/*********************************************************************************************************************** + * Private + **********************************************************************************************************************/ + +void VISWebSocketServer::RunServiceThreadF( + const std::string keyPath, const std::string certPath, const std::string uriStr) +{ + try { + Poco::SharedPtr cert = new Poco::Net::AcceptCertificateHandler(false); + + Poco::Net::Context::Ptr context = new Poco::Net::Context(Poco::Net::Context::SERVER_USE, keyPath, certPath, "", + Poco::Net::Context::VERIFY_NONE, 9, false, "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"); + + Poco::Net::SSLManager::instance().initializeClient(0, cert, context); + + const auto port = Poco::URI(uriStr).getPort(); + Poco::Net::SecureServerSocket svs(port, 64, context); + Poco::Net::HTTPServer srv(new RequestHandlerFactory, svs, new Poco::Net::HTTPServerParams); + + srv.start(); + + mStartEvent.set(); + + mStopEvent.wait(); + + srv.stop(); + } catch (const Poco::Exception& e) { + LOG_ERR() << Poco::format("%s RunServiceThreadF caught exception: messag = %s.", cLogPrefix, e.what()).c_str(); + } +} diff --git a/tests/visidentifier/visserver.hpp b/tests/visidentifier/visserver.hpp new file mode 100644 index 00000000..1db34f67 --- /dev/null +++ b/tests/visidentifier/visserver.hpp @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 Renesas Electronics Corporation. + * Copyright (C) 2024s EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef VISSERVER_HPP_ +#define VISSERVER_HPP_ + +#include +#include +#include + +#include +#include +#include +#include + +#include "visidentifier/vismessage.hpp" + +class VISParams { +public: + void Set(const std::string& key, const std::string& value); + void Set(const std::string& key, const std::vector& values); + std::vector Get(const std::string& key); + static VISParams& Instance(); + +private: + VISParams() = default; + + std::mutex mMutex; + std::map> mMap; +}; + +class WebSocketRequestHandler : public Poco::Net::HTTPRequestHandler { +public: + void handleRequest(Poco::Net::HTTPServerRequest& request, Poco::Net::HTTPServerResponse& response) override; + +private: + std::string handleGetRequest(const VISMessage& request); + std::string handleSubscribeRequest(const VISMessage& request); + std::string handleUnsubscribeAllRequest(const VISMessage& request); + std::string handleFrame(const std::string& frame); +}; + +class RequestHandlerFactory : public Poco::Net::HTTPRequestHandlerFactory { +public: + Poco::Net::HTTPRequestHandler* createRequestHandler(const Poco::Net::HTTPServerRequest&) override; +}; + +class VISWebSocketServer { +public: + static VISWebSocketServer& Instance(); + void Start(const std::string& keyPath, const std::string& certPath, const std::string& uriStr); + void Stop(); + bool TryWaitServiceStart(const long timeout = 2000); + +private: + VISWebSocketServer() = default; + void RunServiceThreadF(const std::string keyPath, const std::string certPath, const std::string uriStr); + + std::recursive_mutex mMutex; + std::thread mThread; + Poco::Event mStopEvent; + Poco::Event mStartEvent; +}; + +#endif