From 112b03cf1cad54a437d2d76be4de98701d0ccfc6 Mon Sep 17 00:00:00 2001 From: Denis Erokhin Date: Fri, 15 May 2026 15:59:16 +0200 Subject: [PATCH 1/6] init --- .../include/opcuaserver/opcuaserver.h | 10 +++++- .../opcua/opcuaserver/src/CMakeLists.txt | 5 +-- .../opcua/opcuaserver/src/opcuaserver.cpp | 34 ++++++++++++++++++- .../opcuaserver/tests/test_opcuaserver.cpp | 4 +-- .../opcuatms_server/src/tms_server.cpp | 8 ++--- .../test_tms_integration.cpp | 29 ++++++++++++++++ 6 files changed, 80 insertions(+), 10 deletions(-) diff --git a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h index 63a0068d..9f0ca647 100644 --- a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h +++ b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h @@ -16,6 +16,7 @@ #pragma once +#include #include #include @@ -36,7 +37,14 @@ BEGIN_NAMESPACE_OPENDAQ_OPCUA class OpcUaServer final : public daq::utils::ThreadEx { public: - using OnClientConnectedCallback = std::function; + struct ClientConnectionInfo + { + std::string clientId; + std::string address; + std::string hostname; + }; + + using OnClientConnectedCallback = std::function; using OnClientDisconnectedCallback = std::function; using OnAllowBrowsingNodeCallback = UA_Boolean (*)(UA_Server* server, UA_AccessControl* ac, diff --git a/shared/libraries/opcua/opcuaserver/src/CMakeLists.txt b/shared/libraries/opcua/opcuaserver/src/CMakeLists.txt index 6b73eb4f..f5d4e886 100644 --- a/shared/libraries/opcua/opcuaserver/src/CMakeLists.txt +++ b/shared/libraries/opcua/opcuaserver/src/CMakeLists.txt @@ -39,8 +39,9 @@ set(SOURCE_FILES ${ALL_SOURCES}) add_library(${MODULE_NAME} STATIC ${ALL_SOURCES}) add_library(${OPENDAQ_SDK_TARGET_NAMESPACE}::${MODULE_NAME} ALIAS ${MODULE_NAME}) -target_include_directories(${MODULE_NAME} PUBLIC $ - $ +target_include_directories(${MODULE_NAME} PUBLIC $ + $ + PRIVATE $ ) target_link_libraries(${MODULE_NAME} PUBLIC ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcuashared diff --git a/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp b/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp index 8a2909b4..5263b9c5 100644 --- a/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp +++ b/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp @@ -7,6 +7,14 @@ #include #include #include +#include "server/ua_server_internal.h" +#ifdef _WIN32 + #include + #include +#else + #include + #include +#endif BEGIN_NAMESPACE_OPENDAQ_OPCUA @@ -620,7 +628,31 @@ UA_StatusCode OpcUaServer::activateSession(UA_Server* server, { serverInstance->createSession(*sessionId, authorizedUser, sessionContext); if (serverInstance->clientConnectedHandler) - serverInstance->clientConnectedHandler(OpcUaNodeId::getIdentifier(*sessionId)); + { + OpcUaServer::ClientConnectionInfo info; + info.clientId = OpcUaNodeId::getIdentifier(*sessionId); + + UA_Session* session = UA_Server_getSessionById(server, sessionId); + if (session && session->header.channel && session->header.channel->connection) + { + UA_SOCKET sockfd = session->header.channel->connection->sockfd; + struct sockaddr_storage addr{}; + socklen_t addrLen = sizeof(addr); + if (getpeername(sockfd, reinterpret_cast(&addr), &addrLen) == 0) + { + char ipBuf[NI_MAXHOST] = {}; + char hostBuf[NI_MAXHOST] = {}; + getnameinfo(reinterpret_cast(&addr), addrLen, + ipBuf, sizeof(ipBuf), nullptr, 0, NI_NUMERICHOST); + getnameinfo(reinterpret_cast(&addr), addrLen, + hostBuf, sizeof(hostBuf), nullptr, 0, 0); + info.address = ipBuf; + info.hostname = hostBuf; + } + } + + serverInstance->clientConnectedHandler(info); + } } return status; diff --git a/shared/libraries/opcua/opcuaserver/tests/test_opcuaserver.cpp b/shared/libraries/opcua/opcuaserver/tests/test_opcuaserver.cpp index 9c1b77a9..9fca3ad0 100644 --- a/shared/libraries/opcua/opcuaserver/tests/test_opcuaserver.cpp +++ b/shared/libraries/opcua/opcuaserver/tests/test_opcuaserver.cpp @@ -230,10 +230,10 @@ TEST_F(OpcUaServerTest, ClientConnectDisconnectCallbacks) bool clientConnected{false}; bool clientDisconnected{false}; server.setClientConnectedHandler( - [&clientId, &clientConnected](const std::string& id) + [&clientId, &clientConnected](const OpcUaServer::ClientConnectionInfo& info) { clientConnected = true; - clientId = id; + clientId = info.clientId; } ); server.setClientDisconnectedHandler( diff --git a/shared/libraries/opcuatms/opcuatms_server/src/tms_server.cpp b/shared/libraries/opcuatms/opcuatms_server/src/tms_server.cpp index c257416d..663edeb3 100644 --- a/shared/libraries/opcuatms/opcuatms_server/src/tms_server.cpp +++ b/shared/libraries/opcuatms/opcuatms_server/src/tms_server.cpp @@ -53,18 +53,18 @@ void TmsServer::start() server->setPort(opcUaPort); server->setAuthenticationProvider(context.getAuthenticationProvider()); server->setClientConnectedHandler( - [this](const std::string& clientId) + [this](const OpcUaServer::ClientConnectionInfo& connInfo) { const auto loggerComponent = context.getLogger().getOrAddComponent("TmsServer"); - LOG_I("New client connected, ID: {}", clientId); + LOG_I("New client connected, ID: {}, address: {}", connInfo.clientId, connInfo.address); SizeT clientNumber = 0; if (device.assigned() && !device.isRemoved()) { device.getInfo().asPtr(true).addConnectedClient( &clientNumber, - ConnectedClientInfo("", ProtocolType::Configuration, "OpenDAQOPCUA", "", "")); + ConnectedClientInfo(connInfo.address, ProtocolType::Configuration, "OpenDAQOPCUA", "Control", connInfo.hostname)); } - registeredClientIds.insert({clientId, clientNumber}); + registeredClientIds.insert({connInfo.clientId, clientNumber}); } ); server->setClientDisconnectedHandler( diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_integration.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_integration.cpp index 433c3364..bdfe9fe0 100644 --- a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_integration.cpp +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_integration.cpp @@ -14,6 +14,8 @@ #include #include #include +#include +#include using namespace daq; using namespace daq::opcua; @@ -635,3 +637,30 @@ TEST_F(TmsIntegrationTest, GetDaqServers) ASSERT_NO_THROW(servers = clientDevice.getServers()); ASSERT_EQ(servers.getCount(), device.getServers().getCount()); } + +TEST_F(TmsIntegrationTest, ConnectedClientInfoHasAddressAndHostname) +{ + InstancePtr device = createDevice(); + + TmsServer tmsServer(device); + tmsServer.start(); + + TmsClient tmsClient(device.getContext(), nullptr, OPC_URL); + ASSERT_NO_THROW(tmsClient.connect()); + + const auto clients = device.getInfo().getConnectedClientsInfo(); + + ConnectedClientInfoPtr opcuaClient; + for (const auto& client : clients) + { + if (client.getProtocolName() == "OpenDAQOPCUA") + { + opcuaClient = client; + break; + } + } + + ASSERT_TRUE(opcuaClient.assigned()) << "No connected client with protocolName 'OpenDAQOPCUA' found"; + ASSERT_FALSE(opcuaClient.getAddress().getLength() == 0) << "Client address must not be empty"; + ASSERT_FALSE(opcuaClient.getHostName().getLength() == 0) << "Client hostname must not be empty"; +} From 65440b8e83d74e056b71c4bf7cdacd75af5af5b6 Mon Sep 17 00:00:00 2001 From: Denis Erokhin Date: Fri, 15 May 2026 17:49:54 +0200 Subject: [PATCH 2/6] fix linux --- shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp b/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp index 5263b9c5..98f916b8 100644 --- a/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp +++ b/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp @@ -632,10 +632,18 @@ UA_StatusCode OpcUaServer::activateSession(UA_Server* server, OpcUaServer::ClientConnectionInfo info; info.clientId = OpcUaNodeId::getIdentifier(*sessionId); + // UA_Server_getSessionById requires the server mutex to be held, + // but activateSession is called with it released — reacquire briefly + // just to read the socket fd, then drop it before the blocking DNS call. + UA_SOCKET sockfd = UA_INVALID_SOCKET; + UA_LOCK(&server->serviceMutex); UA_Session* session = UA_Server_getSessionById(server, sessionId); if (session && session->header.channel && session->header.channel->connection) + sockfd = session->header.channel->connection->sockfd; + UA_UNLOCK(&server->serviceMutex); + + if (sockfd != UA_INVALID_SOCKET) { - UA_SOCKET sockfd = session->header.channel->connection->sockfd; struct sockaddr_storage addr{}; socklen_t addrLen = sizeof(addr); if (getpeername(sockfd, reinterpret_cast(&addr), &addrLen) == 0) From 72c8029b6c21f8363f59cf402756c7602ead3c0d Mon Sep 17 00:00:00 2001 From: Denis Erokhin Date: Wed, 27 May 2026 18:54:59 +0200 Subject: [PATCH 3/6] make setting connectio client async --- .../include/opcuaserver/opcuaserver.h | 15 +++- .../opcua/opcuaserver/src/opcuaserver.cpp | 75 +++++++++++++++---- .../opcuaserver/tests/test_opcuaserver.cpp | 4 +- .../include/opcuatms_server/tms_server.h | 4 + .../opcuatms_server/src/tms_server.cpp | 60 +++++++++++---- 5 files changed, 127 insertions(+), 31 deletions(-) diff --git a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h index 9f0ca647..1b9002d5 100644 --- a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h +++ b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h @@ -16,7 +16,11 @@ #pragma once +#include +#include +#include #include +#include #include #include @@ -44,7 +48,8 @@ class OpcUaServer final : public daq::utils::ThreadEx std::string hostname; }; - using OnClientConnectedCallback = std::function; + using OnClientConnectedCallback = std::function; + using OnSetClientInfoCallback = std::function; using OnClientDisconnectedCallback = std::function; using OnAllowBrowsingNodeCallback = UA_Boolean (*)(UA_Server* server, UA_AccessControl* ac, @@ -81,6 +86,7 @@ class OpcUaServer final : public daq::utils::ThreadEx void setPort(uint16_t port); void setAuthenticationProvider(const AuthenticationProviderPtr& authenticationProvider); void setClientConnectedHandler(const OnClientConnectedCallback& callback); + void setClientInfoHandler(const OnSetClientInfoCallback& callback); void setClientDisconnectedHandler(const OnClientDisconnectedCallback& callback); void setAllowBrowsingNodeCallback(const OnAllowBrowsingNodeCallback& callback); void setGetUserRightsMaskCallback(const OnGetUserRightsMaskCallback& callback); @@ -174,6 +180,10 @@ class OpcUaServer final : public daq::utils::ThreadEx bool isUsernameIdentityTokenValid(const UA_UserNameIdentityToken* token, UserPtr& authorizedUser); bool isAnonymousIdentityTokenValid(const UA_AnonymousIdentityToken* token, UserPtr& authorizedUser); void createSession(const OpcUaNodeId& sessionId, const UserPtr authorizedUser, void** sessionContext); + void scheduleClientInfoAsync(ClientConnectionInfo info, + const sockaddr_storage& addr, + socklen_t addrLen); + void waitForPendingClientInfoFutures(); static UA_StatusCode activateSession(UA_Server* server, UA_AccessControl* ac, @@ -208,7 +218,10 @@ class OpcUaServer final : public daq::utils::ThreadEx ServerEventManagerPtr eventManager; AuthenticationProviderPtr authenticationProvider; OnClientConnectedCallback clientConnectedHandler; + OnSetClientInfoCallback clientInfoHandler; OnClientDisconnectedCallback clientDisconnectedHandler; + std::vector> pendingClientInfoFutures; + std::mutex pendingClientInfoFuturesMutex; }; END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp b/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp index 98f916b8..35881d91 100644 --- a/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp +++ b/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp @@ -4,7 +4,10 @@ #include #include #include +#include #include +#include +#include #include #include #include "server/ua_server_internal.h" @@ -67,6 +70,11 @@ void OpcUaServer::setClientConnectedHandler(const OnClientConnectedCallback& cal this->clientConnectedHandler = callback; } +void OpcUaServer::setClientInfoHandler(const OnSetClientInfoCallback& callback) +{ + this->clientInfoHandler = callback; +} + void OpcUaServer::setClientDisconnectedHandler(const OnClientDisconnectedCallback& callback) { this->clientDisconnectedHandler = callback; @@ -119,9 +127,53 @@ void OpcUaServer::start() void OpcUaServer::stop() { ThreadEx::stop(); + waitForPendingClientInfoFutures(); shutdownServer(); } +void OpcUaServer::waitForPendingClientInfoFutures() +{ + std::lock_guard lock(pendingClientInfoFuturesMutex); + for (auto& future : pendingClientInfoFutures) + { + if (future.valid()) + future.wait(); + } + pendingClientInfoFutures.clear(); +} + +void OpcUaServer::scheduleClientInfoAsync(ClientConnectionInfo info, + const sockaddr_storage& addr, + socklen_t addrLen) +{ + const OnSetClientInfoCallback handler = clientInfoHandler; + if (!handler) + return; + + std::lock_guard lock(pendingClientInfoFuturesMutex); + + pendingClientInfoFutures.erase( + std::remove_if(pendingClientInfoFutures.begin(), + pendingClientInfoFutures.end(), + [](std::future& f) { + return f.wait_for(std::chrono::seconds(0)) == std::future_status::ready; + }), + pendingClientInfoFutures.end()); + + pendingClientInfoFutures.push_back( + std::async(std::launch::async, [handler, info = std::move(info), addr, addrLen]() mutable + { + const auto* sockAddr = reinterpret_cast(&addr); + char ipBuf[NI_MAXHOST] = {}; + char hostBuf[NI_MAXHOST] = {}; + if (getnameinfo(sockAddr, addrLen, ipBuf, sizeof(ipBuf), nullptr, 0, NI_NUMERICHOST) == 0) + info.address = ipBuf; + if (getnameinfo(sockAddr, addrLen, hostBuf, sizeof(hostBuf), nullptr, 0, 0) == 0) + info.hostname = hostBuf; + handler(info); + })); +} + void OpcUaServer::prepare() { try @@ -627,14 +679,16 @@ UA_StatusCode OpcUaServer::activateSession(UA_Server* server, if (status == UA_STATUSCODE_GOOD) { serverInstance->createSession(*sessionId, authorizedUser, sessionContext); + + const std::string clientId = OpcUaNodeId::getIdentifier(*sessionId); if (serverInstance->clientConnectedHandler) - { - OpcUaServer::ClientConnectionInfo info; - info.clientId = OpcUaNodeId::getIdentifier(*sessionId); + serverInstance->clientConnectedHandler(clientId); + if (serverInstance->clientInfoHandler) + { // UA_Server_getSessionById requires the server mutex to be held, // but activateSession is called with it released — reacquire briefly - // just to read the socket fd, then drop it before the blocking DNS call. + // to read the socket fd and peer address (reverse DNS is done asynchronously). UA_SOCKET sockfd = UA_INVALID_SOCKET; UA_LOCK(&server->serviceMutex); UA_Session* session = UA_Server_getSessionById(server, sessionId); @@ -648,18 +702,11 @@ UA_StatusCode OpcUaServer::activateSession(UA_Server* server, socklen_t addrLen = sizeof(addr); if (getpeername(sockfd, reinterpret_cast(&addr), &addrLen) == 0) { - char ipBuf[NI_MAXHOST] = {}; - char hostBuf[NI_MAXHOST] = {}; - getnameinfo(reinterpret_cast(&addr), addrLen, - ipBuf, sizeof(ipBuf), nullptr, 0, NI_NUMERICHOST); - getnameinfo(reinterpret_cast(&addr), addrLen, - hostBuf, sizeof(hostBuf), nullptr, 0, 0); - info.address = ipBuf; - info.hostname = hostBuf; + ClientConnectionInfo connInfo; + connInfo.clientId = clientId; + serverInstance->scheduleClientInfoAsync(std::move(connInfo), addr, addrLen); } } - - serverInstance->clientConnectedHandler(info); } } diff --git a/shared/libraries/opcua/opcuaserver/tests/test_opcuaserver.cpp b/shared/libraries/opcua/opcuaserver/tests/test_opcuaserver.cpp index 9fca3ad0..9c1b77a9 100644 --- a/shared/libraries/opcua/opcuaserver/tests/test_opcuaserver.cpp +++ b/shared/libraries/opcua/opcuaserver/tests/test_opcuaserver.cpp @@ -230,10 +230,10 @@ TEST_F(OpcUaServerTest, ClientConnectDisconnectCallbacks) bool clientConnected{false}; bool clientDisconnected{false}; server.setClientConnectedHandler( - [&clientId, &clientConnected](const OpcUaServer::ClientConnectionInfo& info) + [&clientId, &clientConnected](const std::string& id) { clientConnected = true; - clientId = info.clientId; + clientId = id; } ); server.setClientDisconnectedHandler( diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/tms_server.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/tms_server.h index 9435921c..82ad3768 100644 --- a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/tms_server.h +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/tms_server.h @@ -21,6 +21,8 @@ #include #include #include +#include +#include BEGIN_NAMESPACE_OPENDAQ_OPCUA @@ -45,6 +47,8 @@ class TmsServer uint16_t opcUaPort = 4840; std::string opcUaPath = "/"; std::unordered_map registeredClientIds; + std::mutex connectedClientsMutex; + std::atomic running{false}; }; END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcuatms/opcuatms_server/src/tms_server.cpp b/shared/libraries/opcuatms/opcuatms_server/src/tms_server.cpp index 4cc0f92e..20e50150 100644 --- a/shared/libraries/opcuatms/opcuatms_server/src/tms_server.cpp +++ b/shared/libraries/opcuatms/opcuatms_server/src/tms_server.cpp @@ -53,23 +53,49 @@ void TmsServer::start() server->setPort(opcUaPort); server->setAuthenticationProvider(context.getAuthenticationProvider()); server->setClientConnectedHandler( + [this](const std::string& clientId) + { + const auto loggerComponent = context.getLogger().getOrAddComponent("TmsServer"); + LOG_I("New client connected, ID: {}", clientId); + std::lock_guard lock(connectedClientsMutex); + registeredClientIds.insert({clientId, 0}); + } + ); + server->setClientInfoHandler( [this](const OpcUaServer::ClientConnectionInfo& connInfo) { + if (!running.load()) + return; + + std::lock_guard lock(connectedClientsMutex); + const auto it = registeredClientIds.find(connInfo.clientId); + if (it == registeredClientIds.end()) + return; + const auto loggerComponent = context.getLogger().getOrAddComponent("TmsServer"); - LOG_I("New client connected, ID: {}, address: {}", connInfo.clientId, connInfo.address); + LOG_I("Client address resolved, ID: {}, address: {}, hostname: {}", + connInfo.clientId, + connInfo.address, + connInfo.hostname); + SizeT clientNumber = 0; if (device.assigned() && !device.isRemoved()) { - device.getInfo().asPtr(true).addConnectedClient( + device.getInfo().asPtr().addConnectedClient( &clientNumber, - ConnectedClientInfo(connInfo.address, ProtocolType::Configuration, "OpenDAQOPCUA", "Control", connInfo.hostname)); + ConnectedClientInfo(connInfo.address, + ProtocolType::Configuration, + "OpenDAQOPCUA", + "Control", + connInfo.hostname)); } - registeredClientIds.insert({connInfo.clientId, clientNumber}); + it->second = clientNumber; } ); server->setClientDisconnectedHandler( [this](const std::string& clientId) { + std::lock_guard lock(connectedClientsMutex); if (auto it = registeredClientIds.find(clientId); it != registeredClientIds.end()) { const auto loggerComponent = context.getLogger().getOrAddComponent("TmsServer"); @@ -101,31 +127,37 @@ void TmsServer::start() tmsDevice->registerOpcUaNode(OpcUaNodeId(NAMESPACE_DI, UA_DIID_DEVICESET)); tmsDevice->createNonhierarchicalReferences(); + running = true; server->start(); } void TmsServer::stop() { - if (device.assigned() && !device.isRemoved()) + running = false; + { - const auto info = device.getInfo(); - const auto infoInternal = info.asPtr(); - if (info.hasServerCapability("OpenDAQOPCUAConfiguration")) - infoInternal.removeServerCapability("OpenDAQOPCUAConfiguration"); - for (const auto& [_, clientNumber] : registeredClientIds) + std::lock_guard lock(connectedClientsMutex); + if (device.assigned() && !device.isRemoved()) { - if (clientNumber != 0) - infoInternal.removeConnectedClient(clientNumber); + const auto info = device.getInfo(); + const auto infoInternal = info.asPtr(); + if (info.hasServerCapability("OpenDAQOPCUAConfiguration")) + infoInternal.removeServerCapability("OpenDAQOPCUAConfiguration"); + for (const auto& [_, clientNumber] : registeredClientIds) + { + if (clientNumber != 0) + infoInternal.removeConnectedClient(clientNumber); + } } + registeredClientIds.clear(); } - registeredClientIds.clear(); tmsDevice.reset(); tmsContext.reset(); if (server) server->stop(); - + server.reset(); } From b0fdcaed240bd005614d2c3a01b81fe7ab31bcd4 Mon Sep 17 00:00:00 2001 From: Denis Erokhin Date: Thu, 28 May 2026 10:04:10 +0200 Subject: [PATCH 4/6] use raii --- .../libraries/opcua/opcuaserver/src/opcuaserver.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp b/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp index 35881d91..0ba5f923 100644 --- a/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp +++ b/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp @@ -690,11 +690,14 @@ UA_StatusCode OpcUaServer::activateSession(UA_Server* server, // but activateSession is called with it released — reacquire briefly // to read the socket fd and peer address (reverse DNS is done asynchronously). UA_SOCKET sockfd = UA_INVALID_SOCKET; - UA_LOCK(&server->serviceMutex); - UA_Session* session = UA_Server_getSessionById(server, sessionId); - if (session && session->header.channel && session->header.channel->connection) - sockfd = session->header.channel->connection->sockfd; - UA_UNLOCK(&server->serviceMutex); + { + UA_LOCK(&server->serviceMutex); + Finally finallyUnlock([&]() { UA_UNLOCK(&server->serviceMutex); }); + + UA_Session* session = UA_Server_getSessionById(server, sessionId); + if (session && session->header.channel && session->header.channel->connection) + sockfd = session->header.channel->connection->sockfd; + } if (sockfd != UA_INVALID_SOCKET) { From 6209d2f7cba3c358f850f613c6d944870f994e02 Mon Sep 17 00:00:00 2001 From: Denis Erokhin Date: Fri, 29 May 2026 11:00:46 +0200 Subject: [PATCH 5/6] use not more than one thread for resolving the client address --- .../include/opcuaserver/opcuaserver.h | 10 +- .../opcua/opcuaserver/src/opcuaserver.cpp | 87 ++++++++++------ .../objects/tms_server_object.h | 10 +- .../include/opcuatms_server/tms_server.h | 4 + .../src/objects/tms_server_object.cpp | 22 ++--- .../opcuatms_server/src/tms_server.cpp | 99 ++++++++++--------- .../tests/test_tms_user_access.cpp | 8 +- .../test_tms_integration.cpp | 51 +++++++++- .../tms_object_integration_test.cpp | 8 +- 9 files changed, 194 insertions(+), 105 deletions(-) diff --git a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h index 1b9002d5..6acc4869 100644 --- a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h +++ b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h @@ -16,11 +16,11 @@ #pragma once +#include #include #include #include #include -#include #include #include @@ -88,6 +88,7 @@ class OpcUaServer final : public daq::utils::ThreadEx void setClientConnectedHandler(const OnClientConnectedCallback& callback); void setClientInfoHandler(const OnSetClientInfoCallback& callback); void setClientDisconnectedHandler(const OnClientDisconnectedCallback& callback); + void scheduleClientInfoChainTask(std::function task); void setAllowBrowsingNodeCallback(const OnAllowBrowsingNodeCallback& callback); void setGetUserRightsMaskCallback(const OnGetUserRightsMaskCallback& callback); void setGetUserAccessLevelCallback(const OnGetUserAccessLevelCallback& callback); @@ -184,6 +185,8 @@ class OpcUaServer final : public daq::utils::ThreadEx const sockaddr_storage& addr, socklen_t addrLen); void waitForPendingClientInfoFutures(); + void processClientInfo(ClientConnectionInfo& info, const sockaddr_storage& addr, socklen_t addrLen); + void continueClientInfoChain(); static UA_StatusCode activateSession(UA_Server* server, UA_AccessControl* ac, @@ -220,8 +223,9 @@ class OpcUaServer final : public daq::utils::ThreadEx OnClientConnectedCallback clientConnectedHandler; OnSetClientInfoCallback clientInfoHandler; OnClientDisconnectedCallback clientDisconnectedHandler; - std::vector> pendingClientInfoFutures; - std::mutex pendingClientInfoFuturesMutex; + std::future clientInfoChain; + std::deque> clientInfoChainWaiting; + std::mutex clientInfoChainMutex; }; END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp b/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp index 0ba5f923..e6400a49 100644 --- a/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp +++ b/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp @@ -4,9 +4,7 @@ #include #include #include -#include #include -#include #include #include #include @@ -133,45 +131,72 @@ void OpcUaServer::stop() void OpcUaServer::waitForPendingClientInfoFutures() { - std::lock_guard lock(pendingClientInfoFuturesMutex); - for (auto& future : pendingClientInfoFutures) + std::future chain; { - if (future.valid()) - future.wait(); + std::lock_guard lock(clientInfoChainMutex); + clientInfoChainWaiting.clear(); + chain = std::move(clientInfoChain); } - pendingClientInfoFutures.clear(); + if (chain.valid()) + chain.wait(); } -void OpcUaServer::scheduleClientInfoAsync(ClientConnectionInfo info, - const sockaddr_storage& addr, - socklen_t addrLen) +void OpcUaServer::processClientInfo(ClientConnectionInfo& info, + const sockaddr_storage& addr, + socklen_t addrLen) { const OnSetClientInfoCallback handler = clientInfoHandler; if (!handler) return; - std::lock_guard lock(pendingClientInfoFuturesMutex); - - pendingClientInfoFutures.erase( - std::remove_if(pendingClientInfoFutures.begin(), - pendingClientInfoFutures.end(), - [](std::future& f) { - return f.wait_for(std::chrono::seconds(0)) == std::future_status::ready; - }), - pendingClientInfoFutures.end()); - - pendingClientInfoFutures.push_back( - std::async(std::launch::async, [handler, info = std::move(info), addr, addrLen]() mutable + const auto* sockAddr = reinterpret_cast(&addr); + char ipBuf[NI_MAXHOST] = {}; + char hostBuf[NI_MAXHOST] = {}; + if (getnameinfo(sockAddr, addrLen, ipBuf, sizeof(ipBuf), nullptr, 0, NI_NUMERICHOST) == 0) + info.address = ipBuf; + if (getnameinfo(sockAddr, addrLen, hostBuf, sizeof(hostBuf), nullptr, 0, 0) == 0) + info.hostname = hostBuf; + handler(info); +} + +void OpcUaServer::continueClientInfoChain() +{ + while (true) + { + std::function task; { - const auto* sockAddr = reinterpret_cast(&addr); - char ipBuf[NI_MAXHOST] = {}; - char hostBuf[NI_MAXHOST] = {}; - if (getnameinfo(sockAddr, addrLen, ipBuf, sizeof(ipBuf), nullptr, 0, NI_NUMERICHOST) == 0) - info.address = ipBuf; - if (getnameinfo(sockAddr, addrLen, hostBuf, sizeof(hostBuf), nullptr, 0, 0) == 0) - info.hostname = hostBuf; - handler(info); - })); + std::lock_guard lock(clientInfoChainMutex); + if (clientInfoChainWaiting.empty()) + return; + task = std::move(clientInfoChainWaiting.front()); + clientInfoChainWaiting.pop_front(); + } + task(); + } +} + +void OpcUaServer::scheduleClientInfoChainTask(std::function task) +{ + std::lock_guard lock(clientInfoChainMutex); + const bool chainRunning = !clientInfoChainWaiting.empty() || + (clientInfoChain.valid() && + clientInfoChain.wait_for(std::chrono::seconds(0)) != std::future_status::ready); + clientInfoChainWaiting.push_back(std::move(task)); + if (!chainRunning) + clientInfoChain = std::async(std::launch::async, [this]() { continueClientInfoChain(); }); +} + +void OpcUaServer::scheduleClientInfoAsync(ClientConnectionInfo info, + const sockaddr_storage& addr, + socklen_t addrLen) +{ + if (!clientInfoHandler) + return; + + scheduleClientInfoChainTask([this, info = std::move(info), addr, addrLen]() mutable + { + processClientInfo(info, addr, addrLen); + }); } void OpcUaServer::prepare() diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_object.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_object.h index 5d52eefa..314d2af1 100644 --- a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_object.h +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_object.h @@ -72,27 +72,27 @@ class TmsServerObject : public std::enable_shared_from_this virtual void createNonhierarchicalReferences(); virtual void onCoreEvent(const CoreEventArgsPtr& eventArgs); - static UA_Boolean allowBrowsingNodeCallback(UA_Server* server, + static UA_Boolean AllowBrowsingNodeCallback(UA_Server* server, UA_AccessControl* ac, const UA_NodeId* sessionId, void* sessionContext, const UA_NodeId* nodeId, void* nodeContext); - static UA_UInt32 getUserRightsMaskCallback(UA_Server* server, + static UA_UInt32 GetUserRightsMaskCallback(UA_Server* server, UA_AccessControl* ac, const UA_NodeId* sessionId, void* sessionContext, const UA_NodeId* nodeId, void* nodeContext); - static UA_Byte getUserAccessLevelCallback(UA_Server* server, + static UA_Byte GetUserAccessLevelCallback(UA_Server* server, UA_AccessControl* ac, const UA_NodeId* sessionId, void* sessionContext, const UA_NodeId* nodeId, void* nodeContext); - static UA_Boolean getUserExecutableCallback(UA_Server* server, + static UA_Boolean GetUserExecutableCallback(UA_Server* server, UA_AccessControl* ac, const UA_NodeId* sessionId, void* sessionContext, @@ -110,7 +110,7 @@ class TmsServerObject : public std::enable_shared_from_this return ptr; } - static bool checkPermission(const Permission permission, const UA_NodeId* const nodeId, void* const sessionContext, void* const nodeContext); + static bool CheckPermission(const Permission permission, const UA_NodeId* const nodeId, void* const sessionContext, void* const nodeContext); virtual bool checkPermission(const Permission permission, const UA_NodeId* const nodeId, const OpcUaSession* const sessionContext); std::string readBrowseName(const opcua::OpcUaNodeId& nodeId); diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/tms_server.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/tms_server.h index 82ad3768..f3aca1ba 100644 --- a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/tms_server.h +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/tms_server.h @@ -38,6 +38,10 @@ class TmsServer void start(); void stop(); +private: + void addConnectedClientInfo(const OpcUaServer::ClientConnectionInfo& connInfo); + void removeConnectedClientInfo(const std::string& clientId); + protected: DevicePtr device; ContextPtr context; diff --git a/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_object.cpp b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_object.cpp index 6b9ae245..09553ca6 100644 --- a/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_object.cpp +++ b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_object.cpp @@ -99,43 +99,43 @@ void TmsServerObject::onCoreEvent(const CoreEventArgsPtr& /*eventArgs*/) { } -UA_Boolean TmsServerObject::allowBrowsingNodeCallback(UA_Server* server, +UA_Boolean TmsServerObject::AllowBrowsingNodeCallback(UA_Server* server, UA_AccessControl* ac, const UA_NodeId* sessionId, void* sessionContext, const UA_NodeId* nodeId, void* nodeContext) { - return checkPermission(Permission::Read, nodeId, sessionContext, nodeContext); + return CheckPermission(Permission::Read, nodeId, sessionContext, nodeContext); } -UA_UInt32 TmsServerObject::getUserRightsMaskCallback(UA_Server *server, UA_AccessControl *ac, +UA_UInt32 TmsServerObject::GetUserRightsMaskCallback(UA_Server *server, UA_AccessControl *ac, const UA_NodeId *sessionId, void *sessionContext, const UA_NodeId *nodeId, void *nodeContext) { - return checkPermission(Permission::Write, nodeId, sessionContext, nodeContext) ? 0xFFFFFFFF : 0; + return CheckPermission(Permission::Write, nodeId, sessionContext, nodeContext) ? 0xFFFFFFFF : 0; } -UA_Byte TmsServerObject::getUserAccessLevelCallback( +UA_Byte TmsServerObject::GetUserAccessLevelCallback( UA_Server* server, UA_AccessControl* ac, const UA_NodeId* sessionId, void* sessionContext, const UA_NodeId* nodeId, void* nodeContext) { constexpr UA_Byte readMask = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_HISTORYREAD; constexpr UA_Byte writeMask = UA_ACCESSLEVELMASK_WRITE | UA_ACCESSLEVELMASK_HISTORYWRITE | UA_ACCESSLEVELMASK_SEMANTICCHANGE | UA_ACCESSLEVELMASK_STATUSWRITE | UA_ACCESSLEVELMASK_TIMESTAMPWRITE; UA_Byte mask = 0xFF; - mask = checkPermission(Permission::Read, nodeId, sessionContext, nodeContext) ? (mask | readMask) : (mask & ~readMask); - mask = checkPermission(Permission::Write, nodeId, sessionContext, nodeContext) ? (mask | writeMask) : (mask & ~writeMask); + mask = CheckPermission(Permission::Read, nodeId, sessionContext, nodeContext) ? (mask | readMask) : (mask & ~readMask); + mask = CheckPermission(Permission::Write, nodeId, sessionContext, nodeContext) ? (mask | writeMask) : (mask & ~writeMask); return mask; } -UA_Boolean TmsServerObject::getUserExecutableCallback(UA_Server* server, +UA_Boolean TmsServerObject::GetUserExecutableCallback(UA_Server* server, UA_AccessControl* ac, const UA_NodeId* sessionId, void* sessionContext, const UA_NodeId* methodId, void* methodContext) { - return checkPermission(Permission::Execute, methodId, sessionContext, methodContext); + return CheckPermission(Permission::Execute, methodId, sessionContext, methodContext); } NodeEventManagerPtr TmsServerObject::addEvent(const StringPtr& nodeName) @@ -206,14 +206,14 @@ bool TmsServerObject::hasChildNode(const std::string& nodeName) const return references.count(nodeName) != 0; } -bool TmsServerObject::checkPermission(const Permission permission, +bool TmsServerObject::CheckPermission(const Permission permission, const UA_NodeId* const nodeId, void* const sessionContext, void* const nodeContext) { if (nodeContext == nullptr || sessionContext == nullptr) return true; - return static_cast(nodeContext)->checkPermission(permission, nodeId, static_cast(sessionContext));; + return static_cast(nodeContext)->checkPermission(permission, nodeId, static_cast(sessionContext)); } bool TmsServerObject::checkPermission(const Permission permission, const UA_NodeId* const nodeId, const OpcUaSession* const sessionContext) diff --git a/shared/libraries/opcuatms/opcuatms_server/src/tms_server.cpp b/shared/libraries/opcuatms/opcuatms_server/src/tms_server.cpp index 20e50150..0674b43a 100644 --- a/shared/libraries/opcuatms/opcuatms_server/src/tms_server.cpp +++ b/shared/libraries/opcuatms/opcuatms_server/src/tms_server.cpp @@ -61,57 +61,17 @@ void TmsServer::start() registeredClientIds.insert({clientId, 0}); } ); - server->setClientInfoHandler( - [this](const OpcUaServer::ClientConnectionInfo& connInfo) - { - if (!running.load()) - return; - - std::lock_guard lock(connectedClientsMutex); - const auto it = registeredClientIds.find(connInfo.clientId); - if (it == registeredClientIds.end()) - return; - - const auto loggerComponent = context.getLogger().getOrAddComponent("TmsServer"); - LOG_I("Client address resolved, ID: {}, address: {}, hostname: {}", - connInfo.clientId, - connInfo.address, - connInfo.hostname); - - SizeT clientNumber = 0; - if (device.assigned() && !device.isRemoved()) - { - device.getInfo().asPtr().addConnectedClient( - &clientNumber, - ConnectedClientInfo(connInfo.address, - ProtocolType::Configuration, - "OpenDAQOPCUA", - "Control", - connInfo.hostname)); - } - it->second = clientNumber; - } - ); + server->setClientInfoHandler([this](const OpcUaServer::ClientConnectionInfo& connInfo) { addConnectedClientInfo(connInfo); }); server->setClientDisconnectedHandler( [this](const std::string& clientId) { - std::lock_guard lock(connectedClientsMutex); - if (auto it = registeredClientIds.find(clientId); it != registeredClientIds.end()) - { - const auto loggerComponent = context.getLogger().getOrAddComponent("TmsServer"); - LOG_I("Client disconnected, ID: {}", clientId); - if (device.assigned() && !device.isRemoved() && it->second != 0) - { - device.getInfo().asPtr(true).removeConnectedClient(it->second); - } - registeredClientIds.erase(it); - } + server->scheduleClientInfoChainTask([this, clientId]() { removeConnectedClientInfo(clientId); }); } ); - server->setAllowBrowsingNodeCallback(TmsServerObject::allowBrowsingNodeCallback); - server->setGetUserAccessLevelCallback(TmsServerObject::getUserAccessLevelCallback); - server->setGetUserRightsMaskCallback(TmsServerObject::getUserRightsMaskCallback); - server->setGetUserExecutableCallback(TmsServerObject::getUserExecutableCallback); + server->setAllowBrowsingNodeCallback(TmsServerObject::AllowBrowsingNodeCallback); + server->setGetUserAccessLevelCallback(TmsServerObject::GetUserAccessLevelCallback); + server->setGetUserRightsMaskCallback(TmsServerObject::GetUserRightsMaskCallback); + server->setGetUserExecutableCallback(TmsServerObject::GetUserExecutableCallback); server->prepare(); tmsContext = std::make_shared(context, device); @@ -131,6 +91,53 @@ void TmsServer::start() server->start(); } +void TmsServer::addConnectedClientInfo(const OpcUaServer::ClientConnectionInfo& connInfo) +{ + if (!running.load()) + return; + + std::lock_guard lock(connectedClientsMutex); + const auto it = registeredClientIds.find(connInfo.clientId); + if (it == registeredClientIds.end()) + return; + + const auto loggerComponent = context.getLogger().getOrAddComponent("TmsServer"); + LOG_I("Client address resolved, ID: {}, address: {}, hostname: {}", + connInfo.clientId, + connInfo.address, + connInfo.hostname); + + SizeT clientNumber = 0; + if (device.assigned() && !device.isRemoved()) + { + device.getInfo().asPtr().addConnectedClient( + &clientNumber, + ConnectedClientInfo(connInfo.address, + ProtocolType::Configuration, + "OpenDAQOPCUA", + "Control", + connInfo.hostname)); + } + it->second = clientNumber; +} + +void TmsServer::removeConnectedClientInfo(const std::string& clientId) +{ + if (!running.load()) + return; + + std::lock_guard lock(connectedClientsMutex); + const auto it = registeredClientIds.find(clientId); + if (it == registeredClientIds.end()) + return; + + const auto loggerComponent = context.getLogger().getOrAddComponent("TmsServer"); + LOG_I("Client disconnected, ID: {}", clientId); + if (device.assigned() && !device.isRemoved() && it->second != 0) + device.getInfo().asPtr(true).removeConnectedClient(it->second); + registeredClientIds.erase(it); +} + void TmsServer::stop() { running = false; diff --git a/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_user_access.cpp b/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_user_access.cpp index 2dc0d54e..6aedd0ea 100644 --- a/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_user_access.cpp +++ b/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_user_access.cpp @@ -33,10 +33,10 @@ class TmsServerObjectTestWithParameterizedServer : public TmsServerObjectTest using namespace daq::opcua::tms; server = std::make_shared(); server->setPort(4840); - server->setAllowBrowsingNodeCallback(TmsServerObject::allowBrowsingNodeCallback); - server->setGetUserAccessLevelCallback(TmsServerObject::getUserAccessLevelCallback); - server->setGetUserRightsMaskCallback(TmsServerObject::getUserRightsMaskCallback); - server->setGetUserExecutableCallback(TmsServerObject::getUserExecutableCallback); + server->setAllowBrowsingNodeCallback(TmsServerObject::AllowBrowsingNodeCallback); + server->setGetUserAccessLevelCallback(TmsServerObject::GetUserAccessLevelCallback); + server->setGetUserRightsMaskCallback(TmsServerObject::GetUserRightsMaskCallback); + server->setGetUserExecutableCallback(TmsServerObject::GetUserExecutableCallback); server->setAuthenticationProvider(StaticAuthenticationProvider(true, test_helpers::CreateUsers())); server->start(); diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_integration.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_integration.cpp index bdfe9fe0..e068707e 100644 --- a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_integration.cpp +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_integration.cpp @@ -5,8 +5,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -664,3 +664,52 @@ TEST_F(TmsIntegrationTest, ConnectedClientInfoHasAddressAndHostname) ASSERT_FALSE(opcuaClient.getAddress().getLength() == 0) << "Client address must not be empty"; ASSERT_FALSE(opcuaClient.getHostName().getLength() == 0) << "Client hostname must not be empty"; } + +TEST_F(TmsIntegrationTest, RapidMultiThreadClientConnectDisconnectLeavesNoClientInfo) +{ + InstancePtr device = createDevice(); + + TmsServer tmsServer(device); + tmsServer.start(); + + const std::string opcUrl = OPC_URL; + constexpr int threadCount = 10; + constexpr int connectsPerThread = 2; + + std::vector threads; + threads.reserve(threadCount); + for (int t = 0; t < threadCount; ++t) + { + threads.emplace_back([opcUrl]() + { + const auto moduleManager = ModuleManager("[[none]]"); + auto logger = Logger(); + auto context = Context(Scheduler(logger, 1), logger, TypeManager(), moduleManager, nullptr); + + for (int i = 0; i < connectsPerThread; ++i) + { + try + { + TmsClient tmsClient(context, nullptr, opcUrl); + const DevicePtr clientDevice = tmsClient.connect(); + } + catch (...) + { + return; + } + } + }); + } + + for (auto& thread : threads) + thread.join(); + + size_t opcuaClientCount = 0; + for (const auto& client : device.getInfo().getConnectedClientsInfo()) + { + if (client.getProtocolName() == "OpenDAQOPCUA") + ++opcuaClientCount; + } + + ASSERT_EQ(opcuaClientCount, 0u) << "Connected OpenDAQOPCUA client info must be empty after all clients disconnected"; +} diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/tms_object_integration_test.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/tms_object_integration_test.cpp index 8ebad4c7..ef5e174b 100644 --- a/shared/libraries/opcuatms/tests/opcuatms_integration/tms_object_integration_test.cpp +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/tms_object_integration_test.cpp @@ -18,10 +18,10 @@ void TmsObjectIntegrationTest::Init() { server = std::make_shared(); server->setPort(4840); - server->setAllowBrowsingNodeCallback(TmsServerObject::allowBrowsingNodeCallback); - server->setGetUserAccessLevelCallback(TmsServerObject::getUserAccessLevelCallback); - server->setGetUserRightsMaskCallback(TmsServerObject::getUserRightsMaskCallback); - server->setGetUserExecutableCallback(TmsServerObject::getUserExecutableCallback); + server->setAllowBrowsingNodeCallback(TmsServerObject::AllowBrowsingNodeCallback); + server->setGetUserAccessLevelCallback(TmsServerObject::GetUserAccessLevelCallback); + server->setGetUserRightsMaskCallback(TmsServerObject::GetUserRightsMaskCallback); + server->setGetUserExecutableCallback(TmsServerObject::GetUserExecutableCallback); server->setAuthenticationProvider(StaticAuthenticationProvider(true, test_helpers::CreateUsers())); server->start(); client = CreateAndConnectTestClient(); From 2c5b39be740e6e901fcfd4baebb8fdf74c5c6287 Mon Sep 17 00:00:00 2001 From: Denis Erokhin Date: Fri, 29 May 2026 11:49:36 +0200 Subject: [PATCH 6/6] fix lambda capture --- .../tests/opcuatms_integration/test_tms_integration.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_integration.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_integration.cpp index e068707e..08ef73fa 100644 --- a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_integration.cpp +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_integration.cpp @@ -680,7 +680,7 @@ TEST_F(TmsIntegrationTest, RapidMultiThreadClientConnectDisconnectLeavesNoClient threads.reserve(threadCount); for (int t = 0; t < threadCount; ++t) { - threads.emplace_back([opcUrl]() + threads.emplace_back([opcUrl, connectsPerThread]() { const auto moduleManager = ModuleManager("[[none]]"); auto logger = Logger();