From dc8d26e60d885b6d167394376729f4209e51a415 Mon Sep 17 00:00:00 2001 From: Volkan Date: Wed, 29 Oct 2025 15:12:44 +0100 Subject: [PATCH 01/64] Revert "Development/metrol -1164 (#1962)" This reverts commit bd232108a35ffa86ad2f46fbd09f64fbcc5c85f2. --- Source/core/FileSystem.h | 3 +- Tests/unit/core/test_metrol1164.cpp | 89 ----------------------------- 2 files changed, 1 insertion(+), 91 deletions(-) delete mode 100644 Tests/unit/core/test_metrol1164.cpp diff --git a/Source/core/FileSystem.h b/Source/core/FileSystem.h index f1c5c2a02d..ab4ad80497 100644 --- a/Source/core/FileSystem.h +++ b/Source/core/FileSystem.h @@ -444,8 +444,7 @@ namespace Core { Close(); #ifdef __POSIX__ - int errval{0}; - result = ((errval = remove(_name.c_str())) == 0) || (errval == -1 && (errno == ENOENT)); + result = (remove(_name.c_str()) == 0); #endif #ifdef __WINDOWS__ result = (::DeleteFile(_name.c_str()) != FALSE); diff --git a/Tests/unit/core/test_metrol1164.cpp b/Tests/unit/core/test_metrol1164.cpp deleted file mode 100644 index 9e2cbd11ee..0000000000 --- a/Tests/unit/core/test_metrol1164.cpp +++ /dev/null @@ -1,89 +0,0 @@ -/* - * If not stated otherwise in this file or this component's LICENSE file the - * following copyright and licenses apply: - * - * Copyright 2020 Metrological - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifdef __APPLE__ -#include -#endif - -#include - -#ifndef MODULE_NAME -#include "../Module.h" -#endif - -#include - -#include "../IPTestAdministrator.h" - -namespace Thunder { -namespace Tests { -namespace Core { - - // Both processes close the used buffer and hence the underlying mapped file - // Commit d41b8db695e8fead30b872535a9da9d2bf7b8bf7 prevents an assert at - // CyclicBuffer.cpp:222 due to the return value of FilseSystem.h at 449 - // remove() returning -1 and errno set to ENOENT for the second call. This - // test should not fail with the patch applied. - - TEST(METROL_1164, CreateDestroy) - { - constexpr uint32_t initHandshakeValue = 0, maxWaitTime = 4, VARIABLE_IS_NOT_USED maxWaitTimeMs = 4000, VARIABLE_IS_NOT_USED maxInitTime = 2000; - constexpr uint8_t maxRetries = 100; - - const std::string bufferName {"/tmp/CyclicBuffer-metrol1164"}; - - constexpr uint32_t bufferSize = 1024 * 1024; - - constexpr uint32_t bufferMode = ::Thunder::Core::File::Mode::USER_READ - | ::Thunder::Core::File::Mode::USER_WRITE - | ::Thunder::Core::File::Mode::SHAREABLE - ; - - IPTestAdministrator::Callback callback_child = [&](VARIABLE_IS_NOT_USED IPTestAdministrator& testAdmin) { - SleepMs(maxInitTime); - - ::Thunder::Core::CyclicBuffer buffer{ bufferName.c_str(), bufferMode, 0, false }; - - EXPECT_TRUE(buffer.Open()); - - buffer.Close(); - - ASSERT_EQ(testAdmin.Signal(initHandshakeValue, maxRetries), ::Thunder::Core::ERROR_NONE); - }; - - IPTestAdministrator::Callback callback_parent = [&](VARIABLE_IS_NOT_USED IPTestAdministrator& testAdmin) { - ::Thunder::Core::CyclicBuffer buffer{ bufferName.c_str(), bufferMode, bufferSize, false }; - - EXPECT_TRUE(buffer.Open()); - - ASSERT_EQ(testAdmin.Wait(initHandshakeValue), ::Thunder::Core::ERROR_NONE); - - buffer.Close(); - }; - - IPTestAdministrator testAdmin(callback_parent, callback_child, initHandshakeValue, maxWaitTime); - - // Code after this line is executed by both parent and child - - ::Thunder::Core::Singleton::Dispose(); - } - -} // Core -} // Tests -} // Thunder From 8251e4612dc787ccfe1ef5e63ed00d37139f6e40 Mon Sep 17 00:00:00 2001 From: Volkan Date: Wed, 29 Oct 2025 15:14:21 +0100 Subject: [PATCH 02/64] Revert "Development/metrol 1190 (#1968)" This reverts commit fdea42fb401cd098ada6583df5c1be8f22351569. --- Source/core/CyclicBuffer.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Source/core/CyclicBuffer.cpp b/Source/core/CyclicBuffer.cpp index 5b3af2a27e..b0acff0a08 100644 --- a/Source/core/CyclicBuffer.cpp +++ b/Source/core/CyclicBuffer.cpp @@ -65,10 +65,7 @@ namespace Core { ret = pthread_condattr_setpshared(&cond_attr, PTHREAD_PROCESS_SHARED); ASSERT(ret == 0); DEBUG_VARIABLE(ret); -#ifndef __APPLE__ - ret = pthread_condattr_setclock(&cond_attr, CLOCK_MONOTONIC); - ASSERT(ret == 0); DEBUG_VARIABLE(ret); -#endif + ret = pthread_cond_init(&(_administration->_signal), &cond_attr); ASSERT(ret == 0); DEBUG_VARIABLE(ret); @@ -159,11 +156,6 @@ namespace Core { ret = pthread_cond_init(&(_administration->_signal), &cond_attr); ASSERT(ret == 0); DEBUG_VARIABLE(ret); -#ifndef __APPLE__ - ret = pthread_condattr_setclock(&cond_attr, CLOCK_MONOTONIC); - ASSERT(ret == 0); DEBUG_VARIABLE(ret); -#endif - pthread_mutexattr_t mutex_attr; // default values From 36864d64b0040d7e4525ff00772b1964d12c8187 Mon Sep 17 00:00:00 2001 From: Pierre Wielders Date: Thu, 30 Oct 2025 01:08:59 +0100 Subject: [PATCH 03/64] [FIX] for issue https://github.com/rdkcentral/Thunder/issues/1948 (#1976) Co-authored-by: MFransen69 <39826971+MFransen69@users.noreply.github.com> --- Source/core/JSON.h | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/Source/core/JSON.h b/Source/core/JSON.h index 0609aee0f2..2509b17685 100644 --- a/Source/core/JSON.h +++ b/Source/core/JSON.h @@ -2020,7 +2020,7 @@ namespace Core { // We are assumed to be opaque, but all quoted string stuff is enclosed between quotes // and should be considered for scope counting. // Check if we are entering or leaving a quoted area in the opaque object - if ((current == '\"') && ((_value.empty() == true) || IsEscaped(_value))) { + if ((current == '\"') && (IsEscaped(_value) == false)) { // This is not an "escaped" quote, so it should be considered a real quote. It means // we are now entering or leaving a quoted area within the opaque struct... _flagsAndCounters ^= QuotedAreaBit; @@ -2219,17 +2219,28 @@ namespace Core { private: bool IsEscaped(const string& value) const { - // This code determines if a lot of back slashes to esscape the backslash - // Is odd or even, so does it escape the last character.. - // e.g. 'Test \\\\\\\\\\"' is not the escaping of the quote (") - // 'Test \\\\\\\\\" continued"' is the escaping of th quote.. - // 'Test \" and \" and than \\\"' are all escaped quotes - uint32_t index = static_cast(value.length() - 1); - uint32_t start = index; - while ( (index != static_cast(~0)) && (value[index] == '\\') ) { - index--; - } - return (((start - index) % 2) == 0); + bool escaped(false); + + if (_value.empty() == false) { + // This code determines if a lot of back slashes to esscape the backslash + // Is odd or even, so does it escape the last character.. + // e.g. 'Test \\\\\\\\\\"' is not the escaping of the quote (") + // 'Test \\\\\\\\\" continued"' is the escaping of th quote.. + // 'Test \" and \" and than \\\"' are all escaped quotes + // Subtracting 2 here, because the length is always counted by + // starting @1, so to a zero based buffer, i need to substract 1 + // however that will give us the last character. This is the character + // for which we would like to know if is is escaped, so I need to go + // even one position before that one, hence -2 + uint32_t index = static_cast(value.length() - 2); + uint32_t count = 0; + while ((index != static_cast(~0)) && (value[index] == '\\')) { + index--; + count++; + } + escaped = ((count % 2) != 0); + } + return (escaped); } bool InScope(const ScopeBracket mode) { bool added = false; From eabfb219cb825be470e57f5b5536d27b3171bdf6 Mon Sep 17 00:00:00 2001 From: Pierre Wielders Date: Thu, 30 Oct 2025 00:34:58 +0100 Subject: [PATCH 04/64] [LIMIT] Limit handing out interfaces of Plugins *only* if the plugin is active! (#1977) --- Source/Thunder/PluginServer.cpp | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/Source/Thunder/PluginServer.cpp b/Source/Thunder/PluginServer.cpp index 7a0ff74fc1..56c15ca624 100644 --- a/Source/Thunder/PluginServer.cpp +++ b/Source/Thunder/PluginServer.cpp @@ -288,22 +288,19 @@ namespace PluginHost { AddRef(); result = static_cast(this); } - else if (id == PluginHost::IDispatcher::ID) { - _pluginHandling.Lock(); - if (_jsonrpc != nullptr) { - _jsonrpc->AddRef(); - result = _jsonrpc; - } - _pluginHandling.Unlock(); - } else { _pluginHandling.Lock(); - - if (_handler != nullptr) { - - result = _handler->QueryInterface(id); + if (State() == ACTIVATED) { + if (id == PluginHost::IDispatcher::ID) { + if (_jsonrpc != nullptr) { + _jsonrpc->AddRef(); + result = _jsonrpc; + } + } + else if (_handler != nullptr) { + result = _handler->QueryInterface(id); + } } - _pluginHandling.Unlock(); } From fc4ce5fe44862196dd42fe42e629be5fe33eaff9 Mon Sep 17 00:00:00 2001 From: MFransen69 <39826971+MFransen69@users.noreply.github.com> Date: Thu, 30 Oct 2025 14:06:23 +0100 Subject: [PATCH 05/64] [Core] Add build option for Forgiving JSON RPC method handling (#1978) --- Source/CMakeLists.txt | 2 ++ Source/core/CMakeLists.txt | 5 +++++ Source/core/JSONRPC.h | 4 +++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 1ce83c3921..31d4956300 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -47,6 +47,8 @@ option(DEADLOCK_DETECTION "Enable deadlock detection tooling." OFF) option(DISABLE_USE_COMPLEMENTARY_CODE_SET "Disable the complementary code set" OFF) +option(ENABLE_JSONRPC_FORGIVING_METHOD_CASE_HANDLING + "Enable json rpc forgiving camel and pascal case method handling" OFF) if(HIDE_NON_EXTERNAL_SYMBOLS) diff --git a/Source/core/CMakeLists.txt b/Source/core/CMakeLists.txt index f8f27ccd3d..d83fa03598 100644 --- a/Source/core/CMakeLists.txt +++ b/Source/core/CMakeLists.txt @@ -207,6 +207,11 @@ if (INSTANCE_ID_BITS) message(STATUS "COMRPC instanceid defined as ${INSTANCE_ID_BITS} bits.") endif() +if (ENABLE_JSONRPC_FORGIVING_METHOD_CASE_HANDLING) + target_compile_definitions(${TARGET} PUBLIC __ENABLE_JSONRPC_FORGIVING_METHOD_CASE_HANDLING__=${ENABLE_JSONRPC_FORGIVING_METHOD_CASE_HANDLING}) + message(STATUS "json rpc forgiving method handling enabled") +endif() + if(BLUETOOTH_SUPPORT) target_compile_definitions(${TARGET} PUBLIC __CORE_BLUETOOTH_SUPPORT__) diff --git a/Source/core/JSONRPC.h b/Source/core/JSONRPC.h index f129a16fbb..19a6e389ab 100644 --- a/Source/core/JSONRPC.h +++ b/Source/core/JSONRPC.h @@ -406,11 +406,13 @@ namespace Core { } return (pos == string::npos ? EMPTY_STRING : designator.substr(0, pos)); } - static void Formalize(string& method) + static void Formalize(VARIABLE_IS_NOT_USED string& method) { +#ifdef __ENABLE_JSONRPC_FORGIVING_METHOD_CASE_HANDLING__ PUSH_WARNING(DISABLE_WARNING_DEPRECATED_USE) // Support pascal casing during the transition period ToCamelCase(method); POP_WARNING() +#endif } static string Formalize(const string& method) { From e880cbd701b7b096bed664816956f708eb2d451a Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Thu, 30 Oct 2025 17:44:59 +0100 Subject: [PATCH 06/64] [FIX] Update WorkerPool initialization to use correct thread count and disable assertion temporarily (#1979) --- Source/ThunderPlugin/Process.cpp | 2 +- Source/core/ThreadPool.h | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/ThunderPlugin/Process.cpp b/Source/ThunderPlugin/Process.cpp index 80e96d22a2..1cf5e21d9d 100644 --- a/Source/ThunderPlugin/Process.cpp +++ b/Source/ThunderPlugin/Process.cpp @@ -112,7 +112,7 @@ POP_WARNING() PUSH_WARNING(DISABLE_WARNING_THIS_IN_MEMBER_INITIALIZER_LIST) WorkerPoolImplementation(const uint8_t threads, const uint32_t stackSize, const uint32_t queueSize, const string& callsign) - : WorkerPool(threads - 1, stackSize, queueSize, &_dispatcher, this, (threads > 2 ? (threads - 1) : 1), (threads > 2 ? (threads - 1) : 1)) + : WorkerPool(threads - 1, stackSize, queueSize, &_dispatcher, this, threads, threads) , _dispatcher(callsign) , _sink(*this) { diff --git a/Source/core/ThreadPool.h b/Source/core/ThreadPool.h index e37f7ebb90..f2ad15570b 100644 --- a/Source/core/ThreadPool.h +++ b/Source/core/ThreadPool.h @@ -579,7 +579,8 @@ namespace Core { , _callback(callback) , _unitsSet() { - ASSERT(((lowPriorityThreadCount <= count) && (mediumPriorityThreadCount <= count)) || (count == 0)); + // FIXME!!! + // ASSERT(((lowPriorityThreadCount <= count) && (mediumPriorityThreadCount <= count)) || (count == 0)); const TCHAR* name = _T("WorkerPool::Thread"); for (uint8_t index = 0; index < count; index++) { From b338461beff67cf6bf23835a995237b7d82e70fa Mon Sep 17 00:00:00 2001 From: sebaszm <45654185+sebaszm@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:22:26 +0100 Subject: [PATCH 07/64] [Controller] Correct error code/msg for Suspend (#1983) --- Source/Thunder/Controller.cpp | 2 +- Source/core/JSONRPC.h | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/Thunder/Controller.cpp b/Source/Thunder/Controller.cpp index e2def643f9..f022b38918 100644 --- a/Source/Thunder/Controller.cpp +++ b/Source/Thunder/Controller.cpp @@ -1073,7 +1073,7 @@ namespace Plugin { PluginHost::IStateControl* stateControl = service->QueryInterface(); if (stateControl == nullptr) { - result = Core::ERROR_UNAVAILABLE; + result = Core::ERROR_NOT_SUPPORTED; } else { result = stateControl->Request(PluginHost::IStateControl::command::SUSPEND); diff --git a/Source/core/JSONRPC.h b/Source/core/JSONRPC.h index 19a6e389ab..2d51d53dfb 100644 --- a/Source/core/JSONRPC.h +++ b/Source/core/JSONRPC.h @@ -180,6 +180,10 @@ namespace Core { Code = ApplicationErrorCodeBase - Core::ERROR_UNAVAILABLE; Text = _T("The service is not active."); break; + case Core::ERROR_NOT_SUPPORTED: + Code = ApplicationErrorCodeBase - Core::ERROR_NOT_SUPPORTED; + Text = _T("The operation is not supported."); + break; default: if ((frameworkError & 0x80000000) == 0) { Code = ApplicationErrorCodeBase - static_cast(frameworkError); From b2c687533d6ffbf53a7653509c5d3e911716457c Mon Sep 17 00:00:00 2001 From: MFransen69 <39826971+MFransen69@users.noreply.github.com> Date: Mon, 3 Nov 2025 21:21:22 +0100 Subject: [PATCH 08/64] [Core] fix frame int_cast for 24 bit types (#1985) * [Core] fix frame int_cast for 24 bit types * [Core] fix gcc warning? (sometimes I hate this language :) ) * [Core] removing space * [Core] Oopsie * [Core] Make the assert in Frame actually trigger when needed --- Source/Thunder/PluginServer.h | 3 +++ Source/core/Frame.h | 46 +++++++++++++++++++++++++++++++---- Source/core/Portability.h | 4 +++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/Source/Thunder/PluginServer.h b/Source/Thunder/PluginServer.h index 3244adc256..d2b6a1a249 100644 --- a/Source/Thunder/PluginServer.h +++ b/Source/Thunder/PluginServer.h @@ -4316,6 +4316,9 @@ namespace PluginHost { } break; } + case Request::INCOMPLETE: + // nothing to do for now, jira ticket created... + break; default: { // I think we handled every possible situation ASSERT(false); diff --git a/Source/core/Frame.h b/Source/core/Frame.h index b241a8cff9..022dbd8ef3 100644 --- a/Source/core/Frame.h +++ b/Source/core/Frame.h @@ -31,6 +31,9 @@ namespace Core { public: static constexpr uint8_t SizeOf = 3; static constexpr uint32_t Max = 0x7FFFFF; + static constexpr int32_t Min = 0xFF800000; + + using InternalType = int32_t; SInt24() : _value(0) @@ -66,6 +69,9 @@ namespace Core { public: static constexpr uint8_t SizeOf = 3; static constexpr uint32_t Max = 0xFFFFFF; + static constexpr uint32_t Min = 0; + + using InternalType = uint32_t; UInt24() : _value(0) @@ -107,13 +113,44 @@ namespace Core { } template ::value, int>::type = 0> - static constexpr uint32_t Max() { + static constexpr T Max() { return (std::numeric_limits::max()); } - template ::type = 0> - static constexpr uint32_t Max() { + + template ::type = 0> + static constexpr typename T::InternalType Max() + { return (T::Max); } + + template ::value, int>::type = 0> + static constexpr T Min() + { + return (std::numeric_limits::min()); + } + + template ::type = 0> + static constexpr typename T::InternalType Min() + { + return (T::Min); + } + + template + NEW_TYPE buffer_length_cast(const ORIGINAL_TYPE& input) + { + ASSERT(input <= Frame::Max()); + ASSERT(input >= Frame::Min()); + + // in release in case the length does not fit we do not want to send data at all, then it is more obvious to the recipient something is wrong instead of only sending partial data + ORIGINAL_TYPE length = (input <= Frame::Max() ? input : 0); + + PUSH_WARNING(DISABLE_WARNING_CONVERSION_POSSIBLE_LOSS_OF_DATA) + + return (static_cast(length)); + + POP_WARNING() + } + } template @@ -655,8 +692,7 @@ namespace Core { SIZE_CONTEXT SetText(const SIZE_CONTEXT offset, const string& value) { std::string convertedText(Core::ToString(value)); - ASSERT(convertedText.length() <= Frame::Max()); - return (SetBuffer(offset, int_cast(convertedText.length() <= Frame::Max() ? convertedText.length() : 0), reinterpret_cast(convertedText.c_str()))); + return (SetBuffer(offset, Frame::buffer_length_cast(convertedText.length()), reinterpret_cast(convertedText.c_str()))); } SIZE_CONTEXT SetNullTerminatedText(const SIZE_CONTEXT offset, const string& value, const SIZE_CONTEXT maxLength) diff --git a/Source/core/Portability.h b/Source/core/Portability.h index 92e90d79ca..efc1ef2fa1 100644 --- a/Source/core/Portability.h +++ b/Source/core/Portability.h @@ -182,6 +182,8 @@ #define DISABLE_WARNING_UNUSED_PARAMETERS PUSH_WARNING_ARG_(4100) // W4 - 'function': unreferenced function with internal linkage has been removed #define DISABLE_WARNING_UNUSED_FUNCTIONS PUSH_WARNING_ARG_(5242) +// W3 - 'argument': conversion from 'type' to 'type', possible loss of data +#define DISABLE_WARNING_CONVERSION_POSSIBLE_LOSS_OF_DATA PUSH_WARNING_ARG_(4267) #define DISABLE_WARNING_DEPRECATED_COPY #define DISABLE_WARNING_NON_VIRTUAL_DESTRUCTOR #define DISABLE_WARNING_UNUSED_RESULT @@ -230,6 +232,8 @@ #define DISABLE_WARNING_FREE_NONHEAP_OBJECT PUSH_WARNING_ARG_("-Wfree-nonheap-object") #define DISABLE_WARNING_ARRAY_BOUNDS PUSH_WARNING_ARG_("-Warray-bounds") #define DISABLE_WARNING_IMPLICIT_FALLTHROUGH PUSH_WARNING_ARG_("-Wimplicit-fallthrough") +#define DISABLE_WARNING_CONVERSION_POSSIBLE_LOSS_OF_DATA PUSH_WARNING_ARG_("-Wconversion") + #endif From 9e2d3f94dd0fd15df4912ab7c92db65b078c4cf2 Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Thu, 30 Oct 2025 20:41:25 +0100 Subject: [PATCH 09/64] Development/threadpool stop lock (#1980) * [FIX] Update WorkerPool initialization to use correct thread count and disable assertion temporarily * [FIX] Replace Thread::Wait with Thread::Stop in ThreadPool to improve stopping mechanism --- Source/core/ThreadPool.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/core/ThreadPool.h b/Source/core/ThreadPool.h index f2ad15570b..9b51341f0d 100644 --- a/Source/core/ThreadPool.h +++ b/Source/core/ThreadPool.h @@ -541,7 +541,7 @@ namespace Core { Thread::Run(); } void Stop () { - Thread::Wait(Thread::STOPPED|Thread::BLOCKED, infinite); + Thread::Stop(); } Minion& Me() { return (_minion); From 5ed8aef8a9d4a7a539ae5cd65719dcf16d41fc5e Mon Sep 17 00:00:00 2001 From: sebaszm <45654185+sebaszm@users.noreply.github.com> Date: Tue, 4 Nov 2025 12:48:34 +0100 Subject: [PATCH 10/64] [JSON-RPC] Facilitate event indexes (#1990) * [JSON-RPC] Facilitate event indexes * Update IController notifications * Remove unnecessary lambda capture in state change --- Source/Thunder/Controller.cpp | 61 +++++--- Source/Thunder/Controller.h | 12 +- Source/Thunder/doc/ControllerPlugin.md | 38 +++-- Source/plugins/IController.h | 10 +- Source/plugins/IDispatcher.h | 6 +- Source/plugins/JSONRPC.h | 205 ++++++++++++++++--------- 6 files changed, 211 insertions(+), 121 deletions(-) diff --git a/Source/Thunder/Controller.cpp b/Source/Thunder/Controller.cpp index f022b38918..7c86aacc47 100644 --- a/Source/Thunder/Controller.cpp +++ b/Source/Thunder/Controller.cpp @@ -1460,12 +1460,18 @@ namespace Plugin { } _adminLock.Unlock(); + // also notify the JSON RPC listeners (if any) - Exchange::Controller::JLifeTime::Event::StateChange(*this, callsign, state, reason, - [&callsign](const string& designator) { - const size_t dot = designator.find('.'); - return (dot == string::npos) || (designator.compare(0, dot, callsign) == 0); - }); + + // First notify observers that registered for all (notification will include the callsign) + Exchange::Controller::JLifeTime::Event::StateChange(*this, {}, callsign, state, reason, + [](const string&, const string& index) { + // Custom sendif lambda to only catch broadcast observers. + return (index.empty() == true); + }); + + // ... then the specific observers (notification will not inlcude a callsign) + Exchange::Controller::JLifeTime::Event::StateChange(*this, callsign, {}, state, reason); } void Controller::NotifyStateControlStateChange(const string& callsign, const Exchange::Controller::ILifeTime::state& state) @@ -1479,20 +1485,21 @@ namespace Plugin { } _adminLock.Unlock(); + // also notify the JSON RPC listeners (if any) - Exchange::Controller::JLifeTime::Event::StateControlStateChange(*this, callsign, state, - [&callsign](const string& designator) { - const size_t dot = designator.find('.'); - return (dot == string::npos) || (designator.compare(0, dot, callsign) == 0); - }); + Exchange::Controller::JLifeTime::Event::StateControlStateChange(*this, {}, callsign, state, + [](const string&, const string& index) { + return (index.empty() == true); + }); + + Exchange::Controller::JLifeTime::Event::StateControlStateChange(*this, callsign, {}, state); } - void Controller::SendInitialStateSnapshot(const string& client) + void Controller::SendInitialStateSnapshot(const string& client, const string& callsign) { - const size_t dot = client.find('.'); - - if (dot == string::npos) { + if (callsign.empty() == true) { _adminLock.Lock(); + ASSERT(_pluginServer != nullptr); auto it = _pluginServer->Services().Services(); @@ -1501,27 +1508,35 @@ namespace Plugin { if (service->State() == PluginHost::IShell::state::ACTIVATED) { const string serviceCallsign = service->Callsign(); - Exchange::Controller::JLifeTime::Event::StateChange(*this, serviceCallsign, service->State(), service->Reason(), client); + + Exchange::Controller::JLifeTime::Event::StateChange(*this, {}, serviceCallsign, service->State(), service->Reason(), + [&client, &serviceCallsign](const string& designator, const string& index) { + // Custom sendif lambda to also catch broadcast observers. + return ((designator == client) && ((index.empty() == true) || (index == serviceCallsign))); + }); } } + _adminLock.Unlock(); } else { - const string callsign(client.substr(0, dot)); Core::ProxyType service = FromIdentifier(callsign); if ((service.IsValid() == true) && (service->State() == PluginHost::IShell::state::ACTIVATED)) { - Exchange::Controller::JLifeTime::Event::StateChange(*this, service->Callsign(), service->State(), service->Reason(), client); + const string serviceCallsign = service->Callsign(); + + Exchange::Controller::JLifeTime::Event::StateChange(*this, {}, {}, service->State(), service->Reason(), + [&client, &serviceCallsign](const string& designator, const string& index) { + // Custom sendif lambda to also catch broadcast observers. + return ((designator == client) && ((index.empty() == true) || (index == serviceCallsign))); + }); } } } - void Controller::SendInitialStateControlSnapshot(const string& client) + void Controller::SendInitialStateControlSnapshot(const string& client, const string& callsign) { - const size_t dot = client.find('.'); - - if (dot != string::npos) { - const string callsign(client.substr(0, dot)); + if (callsign.empty() == false) { Core::ProxyType service = FromIdentifier(callsign); if (service.IsValid() == true) { @@ -1529,7 +1544,7 @@ namespace Plugin { if (control != nullptr) { const Exchange::Controller::ILifeTime::state ltState = ToLifeTimeState(control->State()); - Exchange::Controller::JLifeTime::Event::StateControlStateChange(*this, service->Callsign(), ltState, client); + Exchange::Controller::JLifeTime::Event::StateControlStateChange(*this, service->Callsign(), {}, ltState, client); control->Release(); } } diff --git a/Source/Thunder/Controller.h b/Source/Thunder/Controller.h index 9402951c72..ba4af8f2cc 100644 --- a/Source/Thunder/Controller.h +++ b/Source/Thunder/Controller.h @@ -329,16 +329,16 @@ namespace Plugin { Core::hresult Subsystems(ISubsystems::ISubsystemsIterator*& outSubsystems) const override; // JSONRPCSupportsEventStatus overrides - void OnStateChangeEventRegistration(const string& client, const PluginHost::JSONRPCSupportsEventStatus::Status status) override + void OnStateChangeEventRegistration(const string& client, const string& index, const PluginHost::JSONRPCSupportsEventStatus::Status status) override { if (status == PluginHost::JSONRPCSupportsEventStatus::Status::registered) { - SendInitialStateSnapshot(client); + SendInitialStateSnapshot(client, index); } } - void OnStateControlStateChangeEventRegistration(const string& client, const PluginHost::JSONRPCSupportsEventStatus::Status status) override + void OnStateControlStateChangeEventRegistration(const string& client, const string& index, const PluginHost::JSONRPCSupportsEventStatus::Status status) override { if (status == PluginHost::JSONRPCSupportsEventStatus::Status::registered) { - SendInitialStateControlSnapshot(client); + SendInitialStateControlSnapshot(client, index); } } @@ -397,8 +397,8 @@ namespace Plugin { Core::ProxyType DeleteMethod(Core::TextSegmentIterator& index, const Web::Request& request); void StartupResume(const string& callsign, PluginHost::IShell* plugin); void NotifyStateChange(const string& callsign, const PluginHost::IShell::state& state, const PluginHost::IShell::reason& reason); - void SendInitialStateSnapshot(const string& client); - void SendInitialStateControlSnapshot(const string& client); + void SendInitialStateSnapshot(const string& client, const string& callsign); + void SendInitialStateControlSnapshot(const string& client, const string& callsign); private: Core::CriticalSection _adminLock; diff --git a/Source/Thunder/doc/ControllerPlugin.md b/Source/Thunder/doc/ControllerPlugin.md index a10fee5181..3bddd6c55a 100644 --- a/Source/Thunder/doc/ControllerPlugin.md +++ b/Source/Thunder/doc/ControllerPlugin.md @@ -184,7 +184,7 @@ This method takes no parameters. "id": 42, "result": [ { - "name": "JController", + "name": "JMyInterface", "major": 1, "minor": 0, "patch": 0 @@ -1625,14 +1625,22 @@ Controller Events interface events: Notifies of a plugin state change. +### Description + +If registered for empty callsign, notifications for all services will be sent. + > This notification may also be triggered by client registration. +### Parameters + +> The *callsign* parameter shall be passed as index to the ``register`` call, i.e. ``register@``. + ### Notification Parameters | Name | Type | M/O | Description | | :-------- | :-------- | :-------- | :-------- | | params | object | mandatory | *...* | -| params.callsign | string | mandatory | Plugin callsign | +| params?.callsign | string | optional | Plugin callsign | | params.state | string | mandatory | New state of the plugin (must be one of the following: *Activated, Deactivated, Unavailable*) | | params.reason | string | mandatory | Reason for state change (must be one of the following: *Automatic, Conditions, Failure, InitializationFailed, MemoryExceeded, Requested, Shutdown, Startup, WatchdogExpired*) | @@ -1644,7 +1652,7 @@ Notifies of a plugin state change. { "jsonrpc": "2.0", "id": 42, - "method": "Controller.1.register", + "method": "Controller.1.register@Messenger", "params": { "event": "statechange", "id": "myid" @@ -1657,30 +1665,38 @@ Notifies of a plugin state change. ```json { "jsonrpc": "2.0", - "method": "myid.statechange", + "method": "myid.statechange@Messenger", "params": { - "callsign": "...", + "callsign": "Messenger", "state": "Deactivated", "reason": "Automatic" } } ``` -> The *client ID* parameter is passed within the notification designator, i.e. ``.statechange``. +> The *client ID* parameter is passed within the notification designator, i.e. ``.statechange@``. ## *statecontrolstatechange [notification](#head_Notifications)* Notifies of a plugin state change controlled by IStateControl. +### Description + +If registered for empty callsign, notifications for all services will be sent. + > This notification may also be triggered by client registration. +### Parameters + +> The *callsign* parameter shall be passed as index to the ``register`` call, i.e. ``register@``. + ### Notification Parameters | Name | Type | M/O | Description | | :-------- | :-------- | :-------- | :-------- | | params | object | mandatory | *...* | -| params.callsign | string | mandatory | Plugin callsign | +| params?.callsign | string | optional | Plugin callsign | | params.state | string | mandatory | New state of the plugin (must be one of the following: *Resumed, Suspended, Unknown*) | ### Example @@ -1691,7 +1707,7 @@ Notifies of a plugin state change controlled by IStateControl. { "jsonrpc": "2.0", "id": 42, - "method": "Controller.1.register", + "method": "Controller.1.register@Messenger", "params": { "event": "statecontrolstatechange", "id": "myid" @@ -1704,15 +1720,15 @@ Notifies of a plugin state change controlled by IStateControl. ```json { "jsonrpc": "2.0", - "method": "myid.statecontrolstatechange", + "method": "myid.statecontrolstatechange@Messenger", "params": { - "callsign": "...", + "callsign": "Messenger", "state": "Suspended" } } ``` -> The *client ID* parameter is passed within the notification designator, i.e. ``.statecontrolstatechange``. +> The *client ID* parameter is passed within the notification designator, i.e. ``.statecontrolstatechange@``. ## *subsystemchange [notification](#head_Notifications)* diff --git a/Source/plugins/IController.h b/Source/plugins/IController.h index cc4573a014..54d59e8e33 100644 --- a/Source/plugins/IController.h +++ b/Source/plugins/IController.h @@ -112,17 +112,19 @@ namespace Controller { // @statuslistener // @brief Notifies of a plugin state change - // @param callsign: Plugin callsign + // @details If registered for empty callsign, notifications for all services will be sent. + // @param callsign: Plugin callsign (e.g. Messenger) // @param state: New state of the plugin // @param reason: Reason for state change - virtual void StateChange(const string& callsign, const PluginHost::IShell::state& state, const PluginHost::IShell::reason& reason) = 0; + virtual void StateChange(const Core::OptionalType& callsign /* @index */, const PluginHost::IShell::state& state, const PluginHost::IShell::reason& reason) = 0; // @statuslistener // @brief Notifies of a plugin state change controlled by IStateControl - // @param callsign: Plugin callsign + // @details If registered for empty callsign, notifications for all services will be sent. + // @param callsign: Plugin callsign (e.g. Messenger) // @param state: New state of the plugin // @param reason: Reason for state change - virtual void StateControlStateChange(const string& callsign, const state& state) = 0; + virtual void StateControlStateChange(const Core::OptionalType& callsign /* @index */, const state& state) = 0; }; virtual Core::hresult Register(INotification* sink, const Core::OptionalType& callsign) = 0; diff --git a/Source/plugins/IDispatcher.h b/Source/plugins/IDispatcher.h index 139069ccbb..06bb90c125 100644 --- a/Source/plugins/IDispatcher.h +++ b/Source/plugins/IDispatcher.h @@ -38,13 +38,13 @@ namespace Thunder { enum { ID = RPC::ID_DISPATCHER_CALLBACK }; - virtual Core::hresult Event(const string& event, const string& designator, const string& parameters /* @restrict:(4M-1) */) = 0; + virtual Core::hresult Event(const string& event, const string& designator, const string& index, const string& parameters /* @restrict:(4M-1) */) = 0; }; virtual uint32_t Invoke(const uint32_t channelid, const uint32_t id, const string& token, const string& method, const string& parameters, string& response /* @out */) = 0; - virtual Core::hresult Subscribe(ICallback* callback, const string& event, const string& designator) = 0; - virtual Core::hresult Unsubscribe(ICallback* callback, const string& event, const string& designator) = 0; + virtual Core::hresult Subscribe(ICallback* callback, const string& event, const string& designator, const string& index) = 0; + virtual Core::hresult Unsubscribe(ICallback* callback, const string& event, const string& designator, const string& index) = 0; // Lifetime managment of the IDispatcher. // Attach is to be called prior to receiving JSONRPC requests! diff --git a/Source/plugins/JSONRPC.h b/Source/plugins/JSONRPC.h index a452d440a1..139756bc56 100644 --- a/Source/plugins/JSONRPC.h +++ b/Source/plugins/JSONRPC.h @@ -55,6 +55,7 @@ namespace PluginHost { class EXTERNAL JSONRPC : public IDispatcher { public: using SendIfMethod = std::function; + using SendIfMethodIndexed = std::function; private: class Notification : public IShell::IConnectionServer::INotification { @@ -98,12 +99,31 @@ namespace PluginHost { : _callback(nullptr) , _channelId(channelId) , _designator(designator) + , _index() + , _oneShot(oneShot) { + } + Destination(uint32_t channelId, const string& designator, const string& index, const bool oneShot = false) + : _callback(nullptr) + , _channelId(channelId) + , _designator(designator) + , _index(index) , _oneShot(oneShot) { } Destination(IDispatcher::ICallback* callback, const string& designator) : _callback(callback) , _channelId(~0) , _designator(designator) + , _index() + , _oneShot(false) { + if (_callback != nullptr) { + _callback->AddRef(); + } + } + Destination(IDispatcher::ICallback* callback, const string& designator, const string& index) + : _callback(callback) + , _channelId(~0) + , _designator(designator) + , _index(index) , _oneShot(false) { if (_callback != nullptr) { _callback->AddRef(); @@ -113,6 +133,7 @@ namespace PluginHost { : _callback(move._callback) , _channelId(move._channelId) , _designator(std::move(move._designator)) + , _index(std::move(move._index)) , _oneShot(move._oneShot) { move._callback = nullptr; move._channelId = ~0; @@ -121,6 +142,7 @@ namespace PluginHost { : _callback(copy._callback) , _channelId(copy._channelId) , _designator(copy._designator) + , _index(copy._index) , _oneShot(copy._oneShot) { if (_callback != nullptr) { _callback->AddRef(); @@ -140,6 +162,7 @@ namespace PluginHost { _callback = move._callback; _channelId = move._channelId; _designator = std::move(move._designator); + _index = std::move(move._index); move._callback = nullptr; move._channelId = ~0; return (*this); @@ -152,6 +175,7 @@ namespace PluginHost { _callback = copy._callback; _channelId = copy._channelId; _designator = copy._designator; + _index = copy._index; _oneShot = copy._oneShot; if (_callback != nullptr) { _callback->AddRef(); @@ -169,6 +193,9 @@ namespace PluginHost { inline const string& Designator() const { return (_designator); } + inline const string& Index() const { + return (_index); + } inline bool IsOneShot() const { return (_oneShot); } @@ -177,6 +204,7 @@ namespace PluginHost { IDispatcher::ICallback* _callback; uint32_t _channelId; string _designator; + string _index; bool _oneShot; }; using Destinations = std::vector; @@ -200,16 +228,16 @@ namespace PluginHost { bool IsEmpty() const { return ( _designators.empty() ); } - uint32_t Subscribe(const uint32_t id, const string& designator, const bool oneShot) { + uint32_t Subscribe(const uint32_t id, const string& designator, const string& index, const bool oneShot) { uint32_t result = Core::ERROR_NONE; - Destinations::iterator index(_designators.begin()); - while ((index != _designators.end()) && ((index->ChannelId() != id) || (index->Designator() != designator))) { - index++; + Destinations::iterator it(_designators.begin()); + while ((it != _designators.end()) && ((it->ChannelId() != id) || (it->Designator() != designator) || (it->Index() != index))) { + it++; } - if (index == _designators.end()) { - _designators.emplace_back(id, designator, oneShot); + if (it == _designators.end()) { + _designators.emplace_back(id, designator, index, oneShot); } else { result = Core::ERROR_DUPLICATE_KEY; @@ -217,16 +245,16 @@ namespace PluginHost { return (result); } - uint32_t Unsubscribe(const uint32_t id, const string& designator) { + uint32_t Unsubscribe(const uint32_t id, const string& designator, const string& index) { uint32_t result = Core::ERROR_NONE; - Destinations::iterator index(_designators.begin()); - while ((index != _designators.end()) && ((index->ChannelId() != id) || (index->Designator() != designator))) { - index++; + Destinations::iterator it(_designators.begin()); + while ((it != _designators.end()) && ((it->ChannelId() != id) || (it->Designator() != designator) || (it->Index() != index))) { + it++; } - if (index != _designators.end()) { - _designators.erase(index); + if (it != _designators.end()) { + _designators.erase(it); } else { result = Core::ERROR_BAD_REQUEST; @@ -234,16 +262,16 @@ namespace PluginHost { return (result); } - uint32_t Subscribe(IDispatcher::ICallback* callback, const string& designator) { + uint32_t Subscribe(IDispatcher::ICallback* callback, const string& designator, const string& index) { uint32_t result = Core::ERROR_NONE; - Destinations::iterator index(_designators.begin()); - while ((index != _designators.end()) && ((index->Designator() != designator) || (index->Callback() == callback))) { - index++; + Destinations::iterator it(_designators.begin()); + while ((it != _designators.end()) && ((it->Designator() != designator) || (it->Index() != index) || (it->Callback() == callback))) { + it++; } - if (index == _designators.end()) { - _designators.emplace_back(callback, designator); + if (it == _designators.end()) { + _designators.emplace_back(callback, designator, index); } else { result = Core::ERROR_DUPLICATE_KEY; @@ -251,16 +279,16 @@ namespace PluginHost { return (result); } - uint32_t Unsubscribe(IDispatcher::ICallback* callback, const string& designator) { + uint32_t Unsubscribe(IDispatcher::ICallback* callback, const string& designator, const string& index) { uint32_t result = Core::ERROR_NONE; - Destinations::iterator index(_designators.begin()); - while ((index != _designators.end()) && ((index->Designator() != designator) || (index->Callback() == callback))) { - index++; + Destinations::iterator it(_designators.begin()); + while ((it != _designators.end()) && ((it->Designator() != designator) || (it->Index() != index) || (it->Callback() == callback))) { + it++; } - if (index != _designators.end()) { - _designators.erase(index); + if (it != _designators.end()) { + _designators.erase(it); } else { result = Core::ERROR_BAD_REQUEST; @@ -284,7 +312,7 @@ namespace PluginHost { Destinations::iterator index = _designators.begin(); while (index != _designators.end()) { if ( (index->ChannelId() == channelId) && (index->Callback() == nullptr) ) { - unregistered(index->Designator()); + unregistered(index->Designator(), index->Index()); index = _designators.erase(index); } else { @@ -300,10 +328,37 @@ namespace PluginHost { if (!sendifmethod || sendifmethod(entry.Designator())) { if (entry.Callback() == nullptr) { - parent.Notify(entry.ChannelId(), entry.Designator() + '.' + event, parameter); + parent.Notify(entry.ChannelId(), (entry.Designator() + '.' + event), parameter); } else { - entry.Callback()->Event(event, entry.Designator(), parameter); + entry.Callback()->Event(event, entry.Designator(), _T(""), parameter); + } + } + + if (entry.IsOneShot() == true) { + index = _designators.erase(index); + } + else { + ++index; + } + } + } + void Event(JSONRPC& parent, const string event, const string& parameter, const SendIfMethodIndexed& sendifmethod) { + Destinations::iterator index(_designators.begin()); + + while (index != _designators.end()) { + Destination& entry = (*index); + + if (!sendifmethod || sendifmethod(entry.Designator(), entry.Index())) { + if (entry.Callback() == nullptr) { + string joined = (entry.Designator() + '.' + event); + if (entry.Index().empty() == false) { + joined += "@" + entry.Index(); + } + parent.Notify(entry.ChannelId(), joined, parameter); + } + else { + entry.Callback()->Event(event, entry.Designator(), entry.Index(), parameter); } } @@ -632,14 +687,14 @@ namespace PluginHost { { return (InternalNotify(event, _T(""))); } - template ::value, int>::type = 0> + template ::value && !std::is_convertible::value, int>::type = 0> uint32_t Notify(const string& event, const JSONOBJECT& parameters) const { string subject; parameters.ToString(subject); return (InternalNotify(event, subject)); } - template ::value, int>::type = 0> + template ::value || std::is_convertible::value, int>::type = 0> uint32_t Notify(const string& event, SENDIFMETHOD method) const { return InternalNotify(event, _T(""), std::move(method)); @@ -675,8 +730,9 @@ namespace PluginHost { string prefix; string instanceId; string methodName; + string index; - Core::JSONRPC::Message::Split(method, &callsign, nullptr, &prefix, &instanceId, &methodName, nullptr); + Core::JSONRPC::Message::Split(method, &callsign, nullptr, &prefix, &instanceId, &methodName, &index); const string realMethod = Core::JSONRPC::Message::Join(prefix, methodName); @@ -741,7 +797,7 @@ namespace PluginHost { result = Core::ERROR_INVALID_PARAMETER; } else { - result = Subscribe(channelId, Core::JSONRPC::Message::Join(prefix, instanceId, info.Event.Value()), info.Id.Value()); + result = Subscribe(channelId, Core::JSONRPC::Message::Join(prefix, instanceId, info.Event.Value()), info.Id.Value(), index); if (result != Core::ERROR_NONE) { result = Core::ERROR_FAILED_REGISTERED; } @@ -754,7 +810,7 @@ namespace PluginHost { result = Core::ERROR_INVALID_PARAMETER; } else { - result = Unsubscribe(channelId, Core::JSONRPC::Message::Join(prefix, instanceId, info.Event.Value()), info.Id.Value()); + result = Unsubscribe(channelId, Core::JSONRPC::Message::Join(prefix, instanceId, info.Event.Value()), info.Id.Value(), index); if (result != Core::ERROR_NONE) { result = Core::ERROR_FAILED_REGISTERED; } @@ -770,43 +826,43 @@ namespace PluginHost { return (result); } - Core::hresult Subscribe(ICallback* callback, const string& eventId, const string& designator) override + Core::hresult Subscribe(ICallback* callback, const string& eventId, const string& designator, const string& index) override { uint32_t result; _adminLock.Lock(); - ObserverMap::iterator index = _observers.find(eventId); + ObserverMap::iterator it = _observers.find(eventId); - if (index == _observers.end()) { - index = _observers.emplace(std::piecewise_construct, + if (it == _observers.end()) { + it = _observers.emplace(std::piecewise_construct, std::forward_as_tuple(eventId), std::forward_as_tuple()).first; } - result = index->second.Subscribe(callback, designator); + result = it->second.Subscribe(callback, designator, index); - if ((result != Core::ERROR_NONE) && (index->second.IsEmpty() == true)) { - _observers.erase(index); + if ((result != Core::ERROR_NONE) && (it->second.IsEmpty() == true)) { + _observers.erase(it); } _adminLock.Unlock(); return (result); } - Core::hresult Unsubscribe(ICallback* callback, const string& eventId, const string& designator) override + Core::hresult Unsubscribe(ICallback* callback, const string& eventId, const string& designator, const string& index) override { uint32_t result = Core::ERROR_UNKNOWN_KEY; _adminLock.Lock(); - ObserverMap::iterator index = _observers.find(eventId); + ObserverMap::iterator it = _observers.find(eventId); - if (index != _observers.end()) { - result = index->second.Unsubscribe(callback, designator); + if (it != _observers.end()) { + result = it->second.Unsubscribe(callback, designator, index); - if ((result == Core::ERROR_NONE) && (index->second.IsEmpty() == true)) { - _observers.erase(index); + if ((result == Core::ERROR_NONE) && (it->second.IsEmpty() == true)) { + _observers.erase(it); } } _adminLock.Unlock(); @@ -904,30 +960,30 @@ namespace PluginHost { } public: - uint32_t Subscribe(const uint32_t channelId, const string& eventId, const string& designator, const bool oneShot = false) + uint32_t Subscribe(const uint32_t channelId, const string& eventId, const string& designator, const string& index, const bool oneShot = false) { - uint32_t result = ProcessSubscribe(channelId, eventId, designator, oneShot); + uint32_t result = ProcessSubscribe(channelId, eventId, designator, index, oneShot); return (result); } - uint32_t Unsubscribe(const uint32_t channelId, const string& eventId, const string& designator) + uint32_t Unsubscribe(const uint32_t channelId, const string& eventId, const string& designator, const string& index) { uint32_t result = Core::ERROR_UNKNOWN_KEY; _adminLock.Lock(); - ObserverMap::iterator index = _observers.find(eventId); + ObserverMap::iterator it = _observers.find(eventId); - if (index != _observers.end()) { - result = index->second.Unsubscribe(channelId, designator); + if (it != _observers.end()) { + result = it->second.Unsubscribe(channelId, designator, index); if (result == Core::ERROR_NONE) { - ProcessUnsubscribed(channelId, eventId, designator); + ProcessUnsubscribed(channelId, eventId, designator, index); - if (index->second.IsEmpty() == true) { - _observers.erase(index); + if (it->second.IsEmpty() == true) { + _observers.erase(it); } } } @@ -938,23 +994,23 @@ namespace PluginHost { } protected: - uint32_t DoSubscribe(const uint32_t channelId, const string& eventId, const string& designator, const bool oneShot) + uint32_t DoSubscribe(const uint32_t channelId, const string& eventId, const string& designator, const string& index, const bool oneShot) { _adminLock.Lock(); - ObserverMap::iterator index = _observers.find(eventId); + ObserverMap::iterator it = _observers.find(eventId); - if (index == _observers.end()) { - index = _observers.emplace(std::piecewise_construct, + if (it == _observers.end()) { + it = _observers.emplace(std::piecewise_construct, std::forward_as_tuple(eventId), std::forward_as_tuple()) .first; } - uint32_t result = index->second.Subscribe(channelId, designator, oneShot); + uint32_t result = it->second.Subscribe(channelId, designator, index, oneShot); - if ((result != Core::ERROR_NONE) && (index->second.IsEmpty() == true)) { - _observers.erase(index); + if ((result != Core::ERROR_NONE) && (it->second.IsEmpty() == true)) { + _observers.erase(it); } _adminLock.Unlock(); @@ -963,12 +1019,12 @@ namespace PluginHost { } private: - virtual uint32_t ProcessSubscribe(const uint32_t channelId, const string& eventId, const string& designator, const bool oneShot) + virtual uint32_t ProcessSubscribe(const uint32_t channelId, const string& eventId, const string& designator, const string& index, const bool oneShot) { - return DoSubscribe(channelId, eventId, designator, oneShot); + return DoSubscribe(channelId, eventId, designator, index, oneShot); } - virtual void ProcessUnsubscribed(const uint32_t channelId VARIABLE_IS_NOT_USED, const string& eventId VARIABLE_IS_NOT_USED, const string& designator VARIABLE_IS_NOT_USED) + virtual void ProcessUnsubscribed(const uint32_t channelId VARIABLE_IS_NOT_USED, const string& eventId VARIABLE_IS_NOT_USED, const string& designator VARIABLE_IS_NOT_USED, const string& index VARIABLE_IS_NOT_USED) { } @@ -980,7 +1036,7 @@ namespace PluginHost { while (index != _observers.end()) { const string& eventId = index->first; - index->second.Dropped(channelId, [this, channelId, &eventId](const string& designator) { ProcessUnsubscribed(channelId, eventId, designator); }); + index->second.Dropped(channelId, [this, channelId, &eventId](const string& designator, const string& index) { ProcessUnsubscribed(channelId, eventId, designator, index); }); if (index->second.IsEmpty() == true) { index = _observers.erase(index); @@ -994,7 +1050,8 @@ namespace PluginHost { } private: - uint32_t InternalNotify(const string& event, const string& parameters, const SendIfMethod& sendifmethod = nullptr) const + template + uint32_t InternalNotify(const string& event, const string& parameters, SENDIFMETHOD sendifmethod = nullptr) const { uint32_t result = Core::ERROR_UNKNOWN_KEY; @@ -1139,7 +1196,7 @@ namespace PluginHost { } private: - uint32_t ProcessSubscribe(const uint32_t channel, const string& designator, const string& clientId, const bool oneShot) override + uint32_t ProcessSubscribe(const uint32_t channel, const string& designator, const string& clientId, const string& index, const bool oneShot) override { Core::hresult result = Core::ERROR_PRIVILIGED_REQUEST; @@ -1153,10 +1210,10 @@ namespace PluginHost { if ((_subscribeAssessor == nullptr) || (_subscribeAssessor(channel, prefix, instanceId, event, clientId) == true)) { - result = JSONRPC::DoSubscribe(channel, designator, clientId, oneShot); + result = JSONRPC::DoSubscribe(channel, designator, clientId, index, oneShot); if (result == Core::ERROR_NONE) { - NotifyObservers(channel, Core::JSONRPC::Message::Join(prefix, event), instanceId, clientId, Status::registered); + NotifyObservers(channel, Core::JSONRPC::Message::Join(prefix, event), instanceId, clientId, index, Status::registered); } } @@ -1164,7 +1221,7 @@ namespace PluginHost { return (result); } - void ProcessUnsubscribed(const uint32_t channel, const string& designator, const string& clientId) override + void ProcessUnsubscribed(const uint32_t channel, const string& designator, const string& clientId, const string& index) override { string prefix; string instanceId; @@ -1174,22 +1231,22 @@ namespace PluginHost { _adminLock.Lock(); - NotifyObservers(channel, Core::JSONRPC::Message::Join(prefix, event), instanceId, clientId, Status::unregistered); + NotifyObservers(channel, Core::JSONRPC::Message::Join(prefix, event), instanceId, clientId, index, Status::unregistered); _adminLock.Unlock(); } private: - void NotifyObservers(const uint32_t channel, const string event, const string& instanceId, const string& client, const Status status) const + void NotifyObservers(const uint32_t channel, const string event, const string& instanceId, const string& client, const string& index, const Status status) const { StatusCallbackMap::const_iterator it = _observers.find(event); if (it != _observers.cend()) { - it->second(channel, instanceId, client, status); + it->second(channel, instanceId, client, index, status); } } private: - using EventStatusCallback = std::function; + using EventStatusCallback = std::function; using SubscribeCallback = std::function; using StatusCallbackMap = std::map; From 64d1862511b505d0fa989eb5951bb36818b93633 Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Tue, 4 Nov 2025 14:09:49 +0100 Subject: [PATCH 11/64] [Core] Add wait for thread pool to stop in InvokeServerType destructor (#1991) --- Source/com/Administrator.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/com/Administrator.h b/Source/com/Administrator.h index f2a2c54547..3931063761 100644 --- a/Source/com/Administrator.h +++ b/Source/com/Administrator.h @@ -554,6 +554,7 @@ POP_WARNING() ~InvokeServerType() override { _threadPoolEngine.Stop(); + _threadPoolEngine.WaitForStop(); } void Submit(const Core::ProxyType& job) override { _threadPoolEngine.Submit(job, Core::infinite); @@ -566,7 +567,7 @@ POP_WARNING() } void Stop() { - _threadPoolEngine.Stop(); + _threadPoolEngine.Stop(); } private: From 1988cb16466166ca8b336d1331a88529259e2fe3 Mon Sep 17 00:00:00 2001 From: Mateusz Daniluk <121170681+VeithMetro@users.noreply.github.com> Date: Tue, 4 Nov 2025 14:24:52 +0100 Subject: [PATCH 12/64] [Priority Queue] Distinguish the source of ThreadPool to bring the ASSERT back (#1987) * Make it possible to distinguish between ThreadPool created in Thunder and ThunderPlugin, bring the ASSERT back * Rename the new debug param * Adjust thread count in WorkerPool constructor --------- Co-authored-by: MFransen69 <39826971+MFransen69@users.noreply.github.com> --- Source/ThunderPlugin/Process.cpp | 4 ++-- Source/core/ThreadPool.h | 6 +++--- Source/core/WorkerPool.h | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Source/ThunderPlugin/Process.cpp b/Source/ThunderPlugin/Process.cpp index 1cf5e21d9d..03498ab9d6 100644 --- a/Source/ThunderPlugin/Process.cpp +++ b/Source/ThunderPlugin/Process.cpp @@ -111,8 +111,8 @@ POP_WARNING() WorkerPoolImplementation& operator=(const WorkerPoolImplementation&) = delete; PUSH_WARNING(DISABLE_WARNING_THIS_IN_MEMBER_INITIALIZER_LIST) - WorkerPoolImplementation(const uint8_t threads, const uint32_t stackSize, const uint32_t queueSize, const string& callsign) - : WorkerPool(threads - 1, stackSize, queueSize, &_dispatcher, this, threads, threads) + WorkerPoolImplementation(const uint8_t threads, const uint32_t stackSize, const uint32_t queueSize, const string& callsign, const uint8_t additionalThreads = 1) + : WorkerPool((threads - additionalThreads), stackSize, queueSize, &_dispatcher, this, (threads > 2 ? (threads - 1) : threads), (threads > 2 ? (threads - 1) : threads), additionalThreads) , _dispatcher(callsign) , _sink(*this) { diff --git a/Source/core/ThreadPool.h b/Source/core/ThreadPool.h index 9b51341f0d..a2947aee83 100644 --- a/Source/core/ThreadPool.h +++ b/Source/core/ThreadPool.h @@ -565,7 +565,7 @@ namespace Core { ThreadPool& operator=(ThreadPool&&) = delete; ThreadPool& operator=(const ThreadPool&) = delete; - ThreadPool(const uint8_t count, const uint32_t stackSize, const uint32_t queueSize, IDispatcher* dispatcher, IScheduler* scheduler, Minion* external, ICallback* callback, const uint16_t lowPriorityThreadCount = 0, const uint16_t mediumPriorityThreadCount = 0) + ThreadPool(const uint8_t count, const uint32_t stackSize, const uint32_t queueSize, IDispatcher* dispatcher, IScheduler* scheduler, Minion* external, ICallback* callback, const uint16_t lowPriorityThreadCount = 0, const uint16_t mediumPriorityThreadCount = 0, const uint8_t additionalThreads = 0) #if defined(__JOB_QUEUE_STATIC_PRIORITY__) || defined(__JOB_QUEUE_DYNAMIC_PRIORITY__) : _queue(lowPriorityThreadCount, mediumPriorityThreadCount, queueSize) #else @@ -579,8 +579,8 @@ namespace Core { , _callback(callback) , _unitsSet() { - // FIXME!!! - // ASSERT(((lowPriorityThreadCount <= count) && (mediumPriorityThreadCount <= count)) || (count == 0)); + DEBUG_VARIABLE(additionalThreads); + ASSERT(((lowPriorityThreadCount <= (count + additionalThreads)) && (mediumPriorityThreadCount <= (count + additionalThreads)))); const TCHAR* name = _T("WorkerPool::Thread"); for (uint8_t index = 0; index < count; index++) { diff --git a/Source/core/WorkerPool.h b/Source/core/WorkerPool.h index 7c8ab162c7..0bef4904d2 100644 --- a/Source/core/WorkerPool.h +++ b/Source/core/WorkerPool.h @@ -315,9 +315,9 @@ namespace Core { WorkerPool& operator=(const WorkerPool&) = delete; PUSH_WARNING(DISABLE_WARNING_THIS_IN_MEMBER_INITIALIZER_LIST) - WorkerPool(const uint8_t threadCount, const uint32_t stackSize, const uint32_t queueSize, ThreadPool::IDispatcher* dispatcher, ThreadPool::ICallback* callback = nullptr, const uint16_t lowPriorityThreadCount = 0, const uint16_t mediumPriorityThreadCount = 0) + WorkerPool(const uint8_t threadCount, const uint32_t stackSize, const uint32_t queueSize, ThreadPool::IDispatcher* dispatcher, ThreadPool::ICallback* callback = nullptr, const uint16_t lowPriorityThreadCount = 0, const uint16_t mediumPriorityThreadCount = 0, const uint8_t additionalThreads = 0) : _scheduler(this, _timer) - , _threadPool(threadCount, stackSize, queueSize, dispatcher, &_scheduler, &_external, callback, lowPriorityThreadCount, mediumPriorityThreadCount) + , _threadPool(threadCount, stackSize, queueSize, dispatcher, &_scheduler, &_external, callback, lowPriorityThreadCount, mediumPriorityThreadCount, additionalThreads) , _external(_threadPool, dispatcher) , _timer(1024 * 1024, _T("WorkerPoolType::Timer")) , _metadata() From 8da778df7a574b8e6872ef31d1c2c458689bb0f5 Mon Sep 17 00:00:00 2001 From: Mateusz Daniluk <121170681+VeithMetro@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:15:22 +0100 Subject: [PATCH 13/64] [COM-RPC][Logging] Add logging in case of a possible COM-RPC deadlock (#1982) * Change a SYSLOG to TRACE_1 to avoid flooding the system, add a new SYSLOG in cases of a possible COM-RPC deadlock * Refactor IPC communication error logging * Improve deadlock assertion and logging messages --------- Co-authored-by: MFransen69 <39826971+MFransen69@users.noreply.github.com> --- Source/com/Administrator.h | 4 ++-- Source/com/IUnknown.h | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Source/com/Administrator.h b/Source/com/Administrator.h index 3931063761..c04df125dd 100644 --- a/Source/com/Administrator.h +++ b/Source/com/Administrator.h @@ -500,7 +500,7 @@ POP_WARNING() if (source.InProgress() == true) { // If this is on an already occupied channel, it has an outgoing COM-RPC call, raise // the priority as we might be causing a deadlock if the workerpool would be stuffed. - SYSLOG(Logging::Notification, (_T("COM-RPC: second call in the same direction detected; raising priority to High"))); + TRACE_L1("COM-RPC: channel is already occupied, as it has an outgoing COM-RPC call; raising priority to High"); _threadPoolEngine.Submit(Core::ProxyType(job), Core::ThreadPool::Priority::High); } else { @@ -586,7 +586,7 @@ POP_WARNING() if (source.InProgress() == true) { // If this is on an already occupied channel, it has an outgoing COM-RPC call, raise // the priority as we might be causing a deadlock if the workerpool would be stuffed. - SYSLOG(Logging::Notification, (_T("COM-RPC: second call in the same direction detected; raising priority to High"))); + TRACE_L1("COM-RPC: channel is already occupied, as it has an outgoing COM-RPC call; raising priority to High"); _threadPoolEngine.Submit(Core::ProxyType(job), Core::infinite, Core::ThreadPool::Priority::High); } else { _threadPoolEngine.Submit(Core::ProxyType(job), Core::infinite, Core::ThreadPool::Priority::Low); diff --git a/Source/com/IUnknown.h b/Source/com/IUnknown.h index 5f45b7cee7..830b47950b 100644 --- a/Source/com/IUnknown.h +++ b/Source/com/IUnknown.h @@ -327,6 +327,11 @@ namespace ProxyStub { if (channel.IsValid() == true) { + if (channel->InProgress() == true) { + ASSERT(false && "IPC in progress detected on this channel. Possible deadlock!"); + SYSLOG(Logging::Error, (_T("IPC in progress detected on this channel for Interface [0x%X], Method ID [0x%X]. Possible deadlock!"), message->Parameters().InterfaceId(), message->Parameters().MethodId())); + } + result = channel->Invoke(message, waitTime); if (result != Core::ERROR_NONE) { From cafc8872fe9e1979273f6b26582470b030127eb1 Mon Sep 17 00:00:00 2001 From: MFransen69 <39826971+MFransen69@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:27:48 +0100 Subject: [PATCH 14/64] [Core] disable webrequest incomplete assert for now (#1992) --- Source/Thunder/PluginServer.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Thunder/PluginServer.h b/Source/Thunder/PluginServer.h index d2b6a1a249..9074ff62fb 100644 --- a/Source/Thunder/PluginServer.h +++ b/Source/Thunder/PluginServer.h @@ -4177,7 +4177,8 @@ namespace PluginHost { request->Set(status, Core::ProxyType(service), callType); - ASSERT(request->State() != Request::INCOMPLETE); + // for now disable the assert as it is trigger probably when it shouldn't. But we should investigate how to enable it again so it fires at the right time: jira issue METROL-1211 created + //ASSERT(request->State() != Request::INCOMPLETE); if (request->State() == Request::COMPLETE) { From 1aba4626f1814608713cd756fb5fe480520ea793 Mon Sep 17 00:00:00 2001 From: msieben <4319079+msieben@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:55:46 +0100 Subject: [PATCH 15/64] [core / Tests/unit/core] : remove any reference to 'TriState' (#1994) Co-authored-by: MFransen69 <39826971+MFransen69@users.noreply.github.com> --- Source/core/CMakeLists.txt | 1 - Source/core/TriState.h | 155 ------------------------------ Source/core/core.h | 1 - Source/core/core.vcxproj | 3 +- Source/core/core.vcxproj.filters | 5 +- Tests/unit/core/CMakeLists.txt | 2 - Tests/unit/core/test_tristate.cpp | 52 ---------- 7 files changed, 2 insertions(+), 217 deletions(-) delete mode 100644 Source/core/TriState.h delete mode 100644 Tests/unit/core/test_tristate.cpp diff --git a/Source/core/CMakeLists.txt b/Source/core/CMakeLists.txt index d83fa03598..f86f2d8891 100644 --- a/Source/core/CMakeLists.txt +++ b/Source/core/CMakeLists.txt @@ -140,7 +140,6 @@ set(PUBLIC_HEADERS Time.h Timer.h Trace.h - TriState.h TypeTraits.h ValueRecorder.h XGetopt.h diff --git a/Source/core/TriState.h b/Source/core/TriState.h deleted file mode 100644 index 010d8623db..0000000000 --- a/Source/core/TriState.h +++ /dev/null @@ -1,155 +0,0 @@ - /* - * If not stated otherwise in this file or this component's LICENSE file the - * following copyright and licenses apply: - * - * Copyright 2020 Metrological - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef __TRISTATE_H -#define __TRISTATE_H - -#include "Module.h" -#include "Optional.h" - -namespace Thunder { -namespace Core { - - class TriState { - public: - enum EnumState { - True, - False, - Unknown - }; - - public: - TriState() - : m_State(Unknown) - { - } - explicit TriState(const TCHAR* text, const uint32_t length = NUMBER_MAX_UNSIGNED(uint32_t)) - : m_State(Unknown) - { - // Time to see if there is a value - if ((length == 1) || (text[1] == '\0')) { - // Its a single character flag, t/T or f/F - if (toupper(text[0]) == 'F') { - m_State = False; - } else if (toupper(text[0]) == 'T') { - m_State = True; - } - } else if ((length == 4) || (toupper(text[4]) == '\0')) { - uint8_t index = 0; - TCHAR value[] = _T("TRUE\0"); - - while ((index < 4) && (toupper(text[index]) == value[index])) { - index++; - } - - if (index == 4) { - m_State = True; - } - } else if ((length == 5) || (toupper(text[5]) == '\0')) { - uint8_t index = 0; - TCHAR value[] = _T("FALSE\0"); - - while ((index < 5) && (toupper(text[index]) == value[index])) { - index++; - } - - if (index == 5) { - m_State = False; - } - } - } - explicit TriState(EnumState state) - : m_State(state) - { - } - TriState(const TriState& copy) - : m_State(copy.m_State) - { - } - TriState(TriState&& move) noexcept - : m_State(std::move(move.m_State)) - { - } - ~TriState() = default; - - TriState& operator=(const TriState& rhs) - { - m_State = rhs.m_State; - return (*this); - } - - TriState& operator=(TriState&& move) noexcept - { - if (this != &move) { - m_State = std::move(move.m_State); - } - return (*this); - } - - public: - inline TriState& operator=(const bool rhs) - { - m_State = rhs ? True : False; - return (*this); - } - inline TriState& operator=(const EnumState rhs) - { - m_State = rhs; - return (*this); - } - inline bool operator==(bool other) const - { - return (other == true && m_State == True) || (other == false && m_State == False); - } - inline bool operator!=(bool other) const - { - return (!operator==(other)); - } - operator OptionalType() - { - return (m_State == Unknown ? OptionalType() : OptionalType(m_State == True)); - } - inline EnumState Get() const - { - return (m_State); - } - - // Set returns TRUE if changed - inline bool Set(bool value) - { - EnumState oldState = m_State; - m_State = value ? True : False; - return m_State != oldState; - } - - // Set returns TRUE if changed - inline bool Set(EnumState value) - { - EnumState oldState = m_State; - m_State = value; - return m_State != oldState; - } - - private: - EnumState m_State; - }; -} -} // namespace Core - -#endif // __TRISTATE_H diff --git a/Source/core/core.h b/Source/core/core.h index 9ed6710ac0..4c1170e595 100644 --- a/Source/core/core.h +++ b/Source/core/core.h @@ -101,7 +101,6 @@ #include "Timer.h" #include "TokenizedStringList.h" #include "Trace.h" -#include "TriState.h" #include "TypeTraits.h" #include "ValueRecorder.h" #include "XGetopt.h" diff --git a/Source/core/core.vcxproj b/Source/core/core.vcxproj index 659a41e7bf..356b393d19 100644 --- a/Source/core/core.vcxproj +++ b/Source/core/core.vcxproj @@ -97,7 +97,6 @@ - @@ -311,4 +310,4 @@ - \ No newline at end of file + diff --git a/Source/core/core.vcxproj.filters b/Source/core/core.vcxproj.filters index 1b661fbfcf..a5d7a74337 100644 --- a/Source/core/core.vcxproj.filters +++ b/Source/core/core.vcxproj.filters @@ -210,9 +210,6 @@ Header Files - - Header Files - Header Files @@ -387,4 +384,4 @@ Source Files - \ No newline at end of file + diff --git a/Tests/unit/core/CMakeLists.txt b/Tests/unit/core/CMakeLists.txt index eb8aa8826e..f36ec0cb2f 100644 --- a/Tests/unit/core/CMakeLists.txt +++ b/Tests/unit/core/CMakeLists.txt @@ -73,7 +73,6 @@ add_executable(${TEST_RUNNER_NAME} test_threadpool.cpp test_time.cpp test_timer.cpp - test_tristate.cpp #test_valuerecorder.cpp test_weblinkjson.cpp test_weblinktext.cpp @@ -131,7 +130,6 @@ add_executable(${TEST_RUNNER_NAME} test_threadpool.cpp test_time.cpp test_timer.cpp - test_tristate.cpp #test_valuerecorder.cpp test_workerpool.cpp test_xgetopt.cpp diff --git a/Tests/unit/core/test_tristate.cpp b/Tests/unit/core/test_tristate.cpp deleted file mode 100644 index 6ac6d2476f..0000000000 --- a/Tests/unit/core/test_tristate.cpp +++ /dev/null @@ -1,52 +0,0 @@ -/* - * If not stated otherwise in this file or this component's LICENSE file the - * following copyright and licenses apply: - * - * Copyright 2020 Metrological - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include - -#ifndef MODULE_NAME -#include "../Module.h" -#endif - -#include - -namespace Thunder { -namespace Tests { -namespace Core { - - TEST(test_TriState, simple_TriState) - { - ::Thunder::Core::TriState(); - ::Thunder::Core::TriState tristate1("F"); - ::Thunder::Core::TriState tristate2("T"); - ::Thunder::Core::TriState tristate3("True"); - ::Thunder::Core::TriState tristate4("False"); - ::Thunder::Core::TriState tristate5(tristate1); - ::Thunder::Core::TriState tristate6 =tristate1; - ::Thunder::Core::TriState tristate7(::Thunder::Core::TriState::EnumState::True); - - EXPECT_EQ(tristate3.Get(),::Thunder::Core::TriState::EnumState::True); - tristate3.Set(false); - EXPECT_EQ(tristate3.Get(),::Thunder::Core::TriState::EnumState::False); - tristate3.Set(::Thunder::Core::TriState::EnumState::Unknown); - EXPECT_EQ(tristate3.Get(),::Thunder::Core::TriState::EnumState::Unknown); - } - -} // Core -} // Tests -} // Thunder From 0c5f46a75adb6e3df8525fc5156d41cd85cf935d Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Tue, 4 Nov 2025 11:34:09 +0100 Subject: [PATCH 16/64] [TESTS] Use composition instead of inheritance for ThreadPoolTester and MinionTester and WorkerPoolTester (#1984) * [TESTS] Use composition instead of inheritance for ThreadPoolTester and MinionTester * [TESTS] Use composition instead of inheritance for WorkerPoolTester * [FIX] Ensure proper cleanup and adjust member initialization order * [FEATURE] Add WaitForStop method to ThreadPool and assert in Stop method * [REFINE] Improve ThreadPoolTester and MinionTester shutdown process * [FEATURE] Add WaitForStop method to WorkerPool for graceful shutdown * [REFINE] Adjust WorkerPoolTester initialization to handle thread count correctly * [REFINE] Add assertion in Stop method to ensure WorkerPool stops within timeout --------- Co-authored-by: MFransen69 <39826971+MFransen69@users.noreply.github.com> --- Source/core/ThreadPool.h | 14 +++ Source/core/WorkerPool.h | 5 +- Tests/unit/core/test_threadpool.cpp | 182 +++++++++++++++------------- Tests/unit/core/test_workerpool.cpp | 82 +++++++------ 4 files changed, 161 insertions(+), 122 deletions(-) diff --git a/Source/core/ThreadPool.h b/Source/core/ThreadPool.h index a2947aee83..238b30f61e 100644 --- a/Source/core/ThreadPool.h +++ b/Source/core/ThreadPool.h @@ -743,6 +743,20 @@ namespace Core { index++; } } + bool WaitForStop(uint32_t timeout = Core::infinite) + { + std::list::iterator index = _units.begin(); + bool allStopped = true; + + while (index != _units.end()) { + if (!index->Wait(Thread::STOPPED, timeout)) { + allStopped = false; + } + index++; + } + + return allStopped; + } bool HasThreadID(const thread_id id) const { return (_unitsSet.find(id) != _unitsSet.end()); diff --git a/Source/core/WorkerPool.h b/Source/core/WorkerPool.h index 0bef4904d2..42a897afd0 100644 --- a/Source/core/WorkerPool.h +++ b/Source/core/WorkerPool.h @@ -436,7 +436,10 @@ POP_WARNING() #endif _threadPool.Stop(); } - + bool WaitForStop(uint32_t timeout = Core::infinite) + { + return _threadPool.WaitForStop(timeout); + } private: Scheduler _scheduler; ThreadPool _threadPool; diff --git a/Tests/unit/core/test_threadpool.cpp b/Tests/unit/core/test_threadpool.cpp index d9c11884d3..db1880ce26 100644 --- a/Tests/unit/core/test_threadpool.cpp +++ b/Tests/unit/core/test_threadpool.cpp @@ -302,22 +302,23 @@ namespace Core { } }; - class ThreadPoolTester : public EventControl, public JobControl, public ::Thunder::Core::ThreadPool { + class ThreadPoolTester : public EventControl, public JobControl { private: public: ThreadPoolTester() = delete; ThreadPoolTester(const ThreadPoolTester&) = delete; ThreadPoolTester& operator=(const ThreadPoolTester&) = delete; ThreadPoolTester(const uint8_t count, const uint32_t stackSize, const uint32_t queueSize) - : JobControl(*this) - , ThreadPool(count, stackSize, queueSize, &_dispatcher, &_scheduler, nullptr, nullptr, (count > 2 ? (count - 1) : 1), (count > 2 ? (count - 1) : 1)) + : JobControl(*this) , _queueSize(queueSize) , _dispatcher() - , _scheduler(*this) + , _pool(count, stackSize, queueSize, &_dispatcher, &_scheduler, nullptr, nullptr, (count > 2 ? (count - 1) : 1), (count > 2 ? (count - 1) : 1)) + , _scheduler(_pool) { } ~ThreadPoolTester() { + Stop(); } public: @@ -327,25 +328,35 @@ namespace Core { } bool QueueIsFull() { - return (ThreadPool::Pending() >= _queueSize); + return (_pool.Pending() >= _queueSize); } bool QueueIsEmpty() { - return (ThreadPool::Pending() == 0); + return (_pool.Pending() == 0); } void Stop() { - ThreadPool::Stop(); + _pool.Stop(); + assert(_pool.WaitForStop(5000) == true); JobControl::Stop(); } + void Submit(const ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>& job, const uint32_t waitTime = 0) + { + _pool.Submit(job, waitTime); + } + + ::Thunder::Core::ThreadPool& Pool() { return _pool; } + const ::Thunder::Core::ThreadPool& Pool() const { return _pool; } + private: uint32_t _queueSize; Dispatcher _dispatcher; + ::Thunder::Core::ThreadPool _pool; Scheduler _scheduler; }; - class MinionTester : public ::Thunder::Core::Thread, public JobControl, public ::Thunder::Core::ThreadPool::Minion { + class MinionTester : public ::Thunder::Core::Thread, public JobControl { public: MinionTester() = delete; @@ -353,38 +364,39 @@ namespace Core { MinionTester& operator=(const MinionTester&) = delete; MinionTester(ThreadPoolTester& threadPool, const uint32_t queueSize) : JobControl(*this) - , ::Thunder::Core::ThreadPool::Minion(threadPool, &_dispatcher) , _queueSize(queueSize) , _threadPool(threadPool) , _dispatcher() + , _minion(_threadPool.Pool(), &_dispatcher) { } ~MinionTester() { - Stop(); + Shutdown(); } public: void Submit(const ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>& job, const uint32_t waitTime = 0) { - _threadPool.Submit(job, waitTime); + _threadPool.Pool().Submit(job, waitTime); } void RunThreadPool() { - _threadPool.Run(); + _threadPool.Pool().Run(); } void Shutdown() { _threadPool.Stop(); + Stop(); } void Revoke(const ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>& job, const uint32_t waitTime = 0) { - _threadPool.Revoke(job, waitTime); + _threadPool.Pool().Revoke(job, waitTime); Completed(job, waitTime); } uint32_t Completed(const ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>& job, const uint32_t waitTime) { - uint32_t result = ::Thunder::Core::ThreadPool::Minion::Completed(job, waitTime); + uint32_t result = _minion.Completed(job, waitTime); TestJob& testJob = static_cast&>(*job); const_cast&>(testJob).Cancelled(); @@ -393,7 +405,7 @@ namespace Core { virtual uint32_t Worker() override { if (IsRunning()) { - ::Thunder::Core::ThreadPool::Minion::Process(); + _minion.Process(); } ::Thunder::Core::Thread::Block(); return (::Thunder::Core::infinite); @@ -410,17 +422,21 @@ namespace Core { } bool QueueIsFull() { - return (_threadPool.Pending() >= _queueSize); + return (_threadPool.Pool().Pending() >= _queueSize); } bool QueueIsEmpty() { - return (_threadPool.Pending() == 0); + return (_threadPool.Pool().Pending() == 0); } + + ::Thunder::Core::ThreadPool::Minion& Minion() { return _minion; } + const ::Thunder::Core::ThreadPool::Minion& Minion() const { return _minion; } private: uint32_t _queueSize; ThreadPoolTester& _threadPool; Dispatcher _dispatcher; + ::Thunder::Core::ThreadPool::Minion _minion; }; TEST(Core_ThreadPool, CheckMinion_ProcessJob) { @@ -439,7 +455,7 @@ namespace Core { EXPECT_EQ(static_cast&>(*job).GetStatus(), TestJob::COMPLETED); ::Thunder::Core::ThreadPool::Metadata info; - minion.Info(info); + minion.Minion().Info(info); EXPECT_EQ(info.Runs, 1u); job.Release(); @@ -452,9 +468,9 @@ namespace Core { ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch> job = ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>(::Thunder::Core::ProxyType>::Create(minion, TestJob::INITIATED, 500, false)); EXPECT_EQ(static_cast&>(*job).GetStatus(), TestJob::INITIATED); - EXPECT_EQ(minion.IsActive(), false); + EXPECT_EQ(minion.Minion().IsActive(), false); minion.SubmitUsingSelfWorker(job); - EXPECT_EQ(minion.IsActive(), false); + EXPECT_EQ(minion.Minion().IsActive(), false); minion.Run(); volatile bool queueIsEmpty = false; @@ -462,7 +478,7 @@ namespace Core { __asm__ volatile("nop"); } - EXPECT_EQ(minion.IsActive(), true); + EXPECT_EQ(minion.Minion().IsActive(), true); EXPECT_EQ(minion.WaitForJobEvent(job, MaxJobWaitTime * 3), ::Thunder::Core::ERROR_NONE); minion.Shutdown(); @@ -470,7 +486,7 @@ namespace Core { EXPECT_EQ(static_cast&>(*job).GetStatus(), TestJob::COMPLETED); ::Thunder::Core::ThreadPool::Metadata info; - minion.Info(info); + minion.Minion().Info(info); EXPECT_EQ(info.Runs, 1u); job.Release(); @@ -483,15 +499,15 @@ namespace Core { ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch> job = ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>(::Thunder::Core::ProxyType>::Create(minion, TestJob::INITIATED)); EXPECT_EQ(static_cast&>(*job).GetStatus(), TestJob::INITIATED); - EXPECT_EQ(minion.IsActive(), false); + EXPECT_EQ(minion.Minion().IsActive(), false); minion.Submit(job); - EXPECT_EQ(minion.IsActive(), false); + EXPECT_EQ(minion.Minion().IsActive(), false); minion.Revoke(job); - EXPECT_EQ(minion.IsActive(), false); + EXPECT_EQ(minion.Minion().IsActive(), false); EXPECT_EQ(static_cast&>(*job).GetStatus(), TestJob::CANCELED); ::Thunder::Core::ThreadPool::Metadata info; - minion.Info(info); + minion.Minion().Info(info); EXPECT_EQ(info.Runs, 0u); job.Release(); @@ -504,9 +520,9 @@ namespace Core { ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch> job = ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>(::Thunder::Core::ProxyType>::Create(minion, TestJob::INITIATED, 500)); EXPECT_EQ(static_cast&>(*job).GetStatus(), TestJob::INITIATED); - EXPECT_EQ(minion.IsActive(), false); + EXPECT_EQ(minion.Minion().IsActive(), false); minion.Submit(job); - EXPECT_EQ(minion.IsActive(), false); + EXPECT_EQ(minion.Minion().IsActive(), false); minion.Run(); volatile bool queueIsEmpty = false; @@ -516,11 +532,11 @@ namespace Core { minion.Revoke(job); EXPECT_EQ(minion.WaitForJobEvent(job, MaxJobWaitTime * 3), ::Thunder::Core::ERROR_NONE); minion.Shutdown(); - EXPECT_EQ(minion.IsActive(), false); + EXPECT_EQ(minion.Minion().IsActive(), false); EXPECT_EQ(static_cast&>(*job).GetStatus(), TestJob::COMPLETED); ::Thunder::Core::ThreadPool::Metadata info; - minion.Info(info); + minion.Minion().Info(info); EXPECT_EQ(info.Runs, 1u); job.Release(); @@ -532,9 +548,9 @@ namespace Core { MinionTester minion(threadPool, queueSize); ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch> job = ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>(::Thunder::Core::ProxyType>::Create(minion, TestJob::INITIATED, 500, false)); EXPECT_EQ(static_cast&>(*job).GetStatus(), TestJob::INITIATED); - EXPECT_EQ(minion.IsActive(), false); + EXPECT_EQ(minion.Minion().IsActive(), false); minion.SubmitUsingSelfWorker(job); - EXPECT_EQ(minion.IsActive(), false); + EXPECT_EQ(minion.Minion().IsActive(), false); minion.Run(); volatile bool queueIsEmpty = false; @@ -542,20 +558,20 @@ namespace Core { __asm__ volatile("nop"); } - EXPECT_EQ(minion.IsActive(), true); + EXPECT_EQ(minion.Minion().IsActive(), true); minion.Revoke(job); - EXPECT_EQ(minion.IsActive(), true); + EXPECT_EQ(minion.Minion().IsActive(), true); EXPECT_EQ(minion.WaitForJobEvent(job, MaxJobWaitTime * 3), ::Thunder::Core::ERROR_NONE); EXPECT_EQ(static_cast&>(*job).GetStatus(), TestJob::COMPLETED); ::Thunder::Core::ThreadPool::Metadata info; - minion.Info(info); + minion.Minion().Info(info); EXPECT_EQ(info.Runs, 1u); minion.Shutdown(); - EXPECT_EQ(minion.IsActive(), false); + EXPECT_EQ(minion.Minion().IsActive(), false); job.Release(); } TEST(Core_ThreadPool, CheckMinion_ProcessMultipleJobs) @@ -592,7 +608,7 @@ namespace Core { } ::Thunder::Core::ThreadPool::Metadata info; - minion.Info(info); + minion.Minion().Info(info); EXPECT_EQ(info.Runs, 6u); for (auto& job: jobs) { @@ -646,7 +662,7 @@ namespace Core { minion.Shutdown(); ::Thunder::Core::ThreadPool::Metadata info; - minion.Info(info); + minion.Minion().Info(info); EXPECT_EQ(info.Runs, queueSize); for (auto& job: jobs) { @@ -662,15 +678,15 @@ namespace Core { ThreadPoolTester threadPool(threadCount, 0, queueSize); ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch> job = ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>(::Thunder::Core::ProxyType>::Create(threadPool, TestJob::INITIATED, 500)); EXPECT_EQ(static_cast&>(*job).GetStatus(), TestJob::INITIATED); - threadPool.Submit(job, 0); - EXPECT_EQ(threadPool.Pending(), queueSize); + threadPool.Pool().Submit(job, 0); + EXPECT_EQ(threadPool.Pool().Pending(), queueSize); EXPECT_EQ(threadPool.QueueIsEmpty(), false); - threadPool.Run(); + threadPool.Pool().Run(); while(threadPool.QueueIsEmpty() != true) { __asm__ volatile("nop"); } EXPECT_EQ(threadPool.WaitForJobEvent(job, MaxJobWaitTime), ::Thunder::Core::ERROR_NONE); - EXPECT_EQ(threadPool.Pending(), 0u); + EXPECT_EQ(threadPool.Pool().Pending(), 0u); threadPool.Stop(); EXPECT_EQ(static_cast&>(*job).GetStatus(), TestJob::COMPLETED); @@ -685,12 +701,12 @@ namespace Core { ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch> job = ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>(::Thunder::Core::ProxyType>::Create(threadPool, TestJob::INITIATED)); EXPECT_EQ(static_cast&>(*job).GetStatus(), TestJob::INITIATED); - threadPool.Submit(job, 0); - EXPECT_EQ(threadPool.Pending(), queueSize); - threadPool.Revoke(job, 0); - EXPECT_EQ(threadPool.Pending(), 0u); + threadPool.Pool().Submit(job, 0); + EXPECT_EQ(threadPool.Pool().Pending(), queueSize); + threadPool.Pool().Revoke(job, 0); + EXPECT_EQ(threadPool.Pool().Pending(), 0u); EXPECT_EQ(threadPool.QueueIsEmpty(), true); - threadPool.Run(); + threadPool.Pool().Run(); threadPool.Stop(); EXPECT_EQ(static_cast&>(*job).GetStatus(), TestJob::INITIATED); @@ -704,16 +720,16 @@ namespace Core { ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch> job = ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>(::Thunder::Core::ProxyType>::Create(threadPool, TestJob::INITIATED, 1000)); EXPECT_EQ(static_cast&>(*job).GetStatus(), TestJob::INITIATED); - threadPool.Submit(job, 0); - EXPECT_EQ(threadPool.Pending(), queueSize); + threadPool.Pool().Submit(job, 0); + EXPECT_EQ(threadPool.Pool().Pending(), queueSize); EXPECT_EQ(threadPool.QueueIsEmpty(), false); - threadPool.Run(); + threadPool.Pool().Run(); EXPECT_EQ(threadPool.QueueIsEmpty(), false); while(threadPool.QueueIsEmpty() != true) { __asm__ volatile("nop"); } - threadPool.Revoke(job, 0); - EXPECT_EQ(threadPool.Pending(), 0u); + threadPool.Pool().Revoke(job, 0); + EXPECT_EQ(threadPool.Pool().Pending(), 0u); EXPECT_EQ(threadPool.WaitForJobEvent(job, MaxJobWaitTime * 3), ::Thunder::Core::ERROR_NONE); threadPool.Stop(); @@ -744,9 +760,9 @@ namespace Core { EXPECT_EQ(threadPool.QueueIsFull(), true); EXPECT_EQ(threadPool.QueueIsEmpty(), false); - EXPECT_GE(threadPool.Pending(), queueSize); + EXPECT_GE(threadPool.Pool().Pending(), queueSize); - threadPool.Run(); + threadPool.Pool().Run(); for (auto& job: jobs) { EXPECT_EQ(threadPool.WaitForJobEvent(job, MaxJobWaitTime * 3), ::Thunder::Core::ERROR_NONE); } @@ -760,7 +776,7 @@ namespace Core { ::Thunder::Core::ThreadPool::Metadata* info = new ::Thunder::Core::ThreadPool::Metadata[threadCount]; std::vector jobsStrings; - threadPool.Snapshot(threadCount, info, jobsStrings); + threadPool.Pool().Snapshot(threadCount, info, jobsStrings); for (uint8_t i = 0; i < threadCount; ++i) { totalRuns += info[i].Runs; @@ -794,7 +810,7 @@ namespace Core { uint8_t additionalJobs = 2; uint8_t threadCount = 1; ThreadPoolTester threadPool(threadCount, 0, queueSize); - EXPECT_EQ(threadPool.Count(), threadCount); + EXPECT_EQ(threadPool.Pool().Count(), threadCount); std::vector<::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>> jobs; // Create Jobs with more than Queue size. i.e, queueSize + additionalJobs @@ -813,11 +829,11 @@ namespace Core { } EXPECT_EQ(threadPool.QueueIsFull(), true); EXPECT_EQ(threadPool.QueueIsEmpty(), false); - EXPECT_GE(threadPool.Pending(), queueSize); + EXPECT_GE(threadPool.Pool().Pending(), queueSize); - threadPool.Run(); - threadPool.Revoke(jobs[3], 0); - threadPool.Revoke(jobs[4], 0); + threadPool.Pool().Run(); + threadPool.Pool().Revoke(jobs[3], 0); + threadPool.Pool().Revoke(jobs[4], 0); ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch> newJob = ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>(::Thunder::Core::ProxyType>::Create(threadPool, TestJob::INITIATED)); EXPECT_EQ(static_cast&>(*newJob).GetStatus(), TestJob::INITIATED); @@ -845,7 +861,7 @@ namespace Core { ::Thunder::Core::ThreadPool::Metadata* info = new ::Thunder::Core::ThreadPool::Metadata[threadCount]; std::vector jobsStrings; - threadPool.Snapshot(threadCount, info, jobsStrings); + threadPool.Pool().Snapshot(threadCount, info, jobsStrings); for (uint8_t i = 0; i < threadCount; ++i) { totalRuns += info[i].Runs; @@ -864,7 +880,7 @@ namespace Core { void CheckThreadPool_ProcessMultipleJobs_CancelInBetween_WithMultiplePool(const uint8_t threadCount, const uint8_t queueSize, const uint8_t additionalJobs) { ThreadPoolTester threadPool(threadCount, 0, queueSize); - EXPECT_EQ(threadPool.Count(), threadCount); + EXPECT_EQ(threadPool.Pool().Count(), threadCount); std::vector<::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>> jobs; // Create Jobs with more than Queue size. i.e, queueSize + additionalJobs @@ -882,15 +898,15 @@ namespace Core { } } EXPECT_EQ(threadPool.QueueIsEmpty(), false); - EXPECT_GE(threadPool.Pending(), queueSize); + EXPECT_GE(threadPool.Pool().Pending(), queueSize); - threadPool.Run(); + threadPool.Pool().Run(); usleep(100); for (uint8_t i = 0; i < jobs.size(); ++i) { EXPECT_EQ(threadPool.WaitForJobEvent(jobs[i], MaxJobWaitTime * 5), ::Thunder::Core::ERROR_NONE); if ((i == 3) || (i == 4)) { - threadPool.Revoke(jobs[i], 0); + threadPool.Pool().Revoke(jobs[i], 0); } } @@ -903,7 +919,7 @@ namespace Core { ::Thunder::Core::ThreadPool::Metadata* info = new ::Thunder::Core::ThreadPool::Metadata[threadCount]; std::vector jobsStrings; - threadPool.Snapshot(threadCount, info, jobsStrings); + threadPool.Pool().Snapshot(threadCount, info, jobsStrings); for (uint8_t i = 0; i < threadCount; ++i) { totalRuns += info[i].Runs; @@ -987,7 +1003,7 @@ namespace Core { void CheckThreadPool_JobType_Submit_Using_Idle(const uint8_t threadCount, const uint8_t queueSize, const uint8_t additionalJobs, const uint8_t cancelJobsCount, const uint8_t* cancelJobsId) { ThreadPoolTester threadPool(threadCount, 0, queueSize); - EXPECT_EQ(threadPool.Count(), threadCount); + EXPECT_EQ(threadPool.Pool().Count(), threadCount); std::vector<::Thunder::Core::ProxyType> jobs; // Create Jobs with more than Queue size. i.e, queueSize + additionalJobs @@ -1030,9 +1046,9 @@ namespace Core { } } } - EXPECT_EQ(threadPool.Pending(), static_cast(queueSize - cancelJobsCount)); + EXPECT_EQ(threadPool.Pool().Pending(), static_cast(queueSize - cancelJobsCount)); - threadPool.Run(); + threadPool.Pool().Run(); usleep(100); for (uint8_t i = 0; i < jobs.size(); ++i) { @@ -1058,7 +1074,7 @@ namespace Core { ::Thunder::Core::ThreadPool::Metadata* info = new ::Thunder::Core::ThreadPool::Metadata[threadCount]; std::vector jobsStrings; - threadPool.Snapshot(threadCount, info, jobsStrings); + threadPool.Pool().Snapshot(threadCount, info, jobsStrings); for (uint8_t i = 0; i < threadCount; ++i) { totalRuns += info[i].Runs; @@ -1107,7 +1123,7 @@ namespace Core { void CheckThreadPool_JobType_Submit_Using_Submit(const uint8_t threadCount, const uint8_t queueSize, const uint8_t additionalJobs, const uint8_t cancelJobsCount, const uint8_t* cancelJobsId) { ThreadPoolTester threadPool(threadCount, 0, queueSize); - EXPECT_EQ(threadPool.Count(), threadCount); + EXPECT_EQ(threadPool.Pool().Count(), threadCount); std::vector<::Thunder::Core::ProxyType> jobs; // Create Jobs with more than Queue size. i.e, queueSize + additionalJobs @@ -1149,9 +1165,9 @@ namespace Core { } } } - EXPECT_EQ(threadPool.Pending(), static_cast(queueSize - cancelJobsCount)); + EXPECT_EQ(threadPool.Pool().Pending(), static_cast(queueSize - cancelJobsCount)); - threadPool.Run(); + threadPool.Pool().Run(); usleep(100); for (uint8_t i = 0; i < jobs.size(); ++i) { @@ -1177,7 +1193,7 @@ namespace Core { ::Thunder::Core::ThreadPool::Metadata* info = new ::Thunder::Core::ThreadPool::Metadata[threadCount]; std::vector jobsStrings; - threadPool.Snapshot(threadCount, info, jobsStrings); + threadPool.Pool().Snapshot(threadCount, info, jobsStrings); for (uint8_t i = 0; i < threadCount; ++i) { totalRuns += info[i].Runs; @@ -1225,8 +1241,8 @@ namespace Core { void CheckThreadPool_JobType_Submit_Using_Reschedule(const uint8_t threadCount, const uint8_t queueSize, const uint8_t additionalJobs, const uint8_t cancelJobsCount, const uint8_t* cancelJobsId, const uint16_t scheduledTimes[]) { ThreadPoolTester threadPool(threadCount, 0, queueSize); - EXPECT_EQ(threadPool.Count(), threadCount); - threadPool.Run(); + EXPECT_EQ(threadPool.Pool().Count(), threadCount); + threadPool.Pool().Run(); std::vector<::Thunder::Core::ProxyType> jobs; // Create Jobs with more than Queue size. i.e, queueSize + additionalJobs @@ -1290,7 +1306,7 @@ namespace Core { ::Thunder::Core::ThreadPool::Metadata* info = new ::Thunder::Core::ThreadPool::Metadata[threadCount]; std::vector jobsStrings; - threadPool.Snapshot(threadCount, info, jobsStrings); + threadPool.Pool().Snapshot(threadCount, info, jobsStrings); for (uint8_t i = 0; i < threadCount; ++i) { totalRuns += info[i].Runs; @@ -1348,8 +1364,8 @@ namespace Core { uint8_t threadCount = 5; const uint16_t scheduledTimes[] = {2000, 2000, 3000, 1000, 2000, 1000}; ThreadPoolTester threadPool(threadCount, 0, queueSize); - EXPECT_EQ(threadPool.Count(), threadCount); - threadPool.Run(); + EXPECT_EQ(threadPool.Pool().Count(), threadCount); + threadPool.Pool().Run(); std::vector<::Thunder::Core::ProxyType> jobs; // Create Jobs with more than Queue size. i.e, queueSize + additionalJobs @@ -1389,7 +1405,7 @@ namespace Core { ::Thunder::Core::ThreadPool::Metadata* info = new ::Thunder::Core::ThreadPool::Metadata[threadCount]; std::vector jobsStrings; - threadPool.Snapshot(threadCount, info, jobsStrings); + threadPool.Pool().Snapshot(threadCount, info, jobsStrings); for (uint8_t i = 0; i < threadCount; ++i) { totalRuns += info[i].Runs; @@ -1413,8 +1429,8 @@ namespace Core { uint8_t threadCount = 5; const uint16_t scheduledTimes[] = {1000, 2000, 3000, 1000, 4000, 1000}; ThreadPoolTester threadPool(threadCount, 0, queueSize); - EXPECT_EQ(threadPool.Count(), threadCount); - threadPool.Run(); + EXPECT_EQ(threadPool.Pool().Count(), threadCount); + threadPool.Pool().Run(); std::vector<::Thunder::Core::ProxyType> jobs; // Create Jobs with more than Queue size. i.e, queueSize + additionalJobs @@ -1454,7 +1470,7 @@ namespace Core { ::Thunder::Core::ThreadPool::Metadata* info = new ::Thunder::Core::ThreadPool::Metadata[threadCount]; std::vector jobsStrings; - threadPool.Snapshot(threadCount, info, jobsStrings); + threadPool.Pool().Snapshot(threadCount, info, jobsStrings); for (uint8_t i = 0; i < threadCount; ++i) { totalRuns += info[i].Runs; diff --git a/Tests/unit/core/test_workerpool.cpp b/Tests/unit/core/test_workerpool.cpp index 5a6db4bf9a..78f01b160d 100644 --- a/Tests/unit/core/test_workerpool.cpp +++ b/Tests/unit/core/test_workerpool.cpp @@ -237,7 +237,7 @@ namespace Core { void SubmitUsingSelfWorker(::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>& job) { InsertJobData(job, 0); - _parent.Submit(job); + _parent.Pool().Submit(job); } void SubmitUsingExternalWorker(::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>& job) { @@ -249,12 +249,12 @@ namespace Core { void ScheduleJobs(::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>& job, const uint16_t scheduledTime) { InsertJobData(job, scheduledTime); - _parent.Schedule(::Thunder::Core::Time::Now().Add(scheduledTime), job); + _parent.Pool().Schedule(::Thunder::Core::Time::Now().Add(scheduledTime), job); } void RescheduleJobs(::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>& job, const uint16_t scheduledTime) { InsertJobData(job, scheduledTime); - _parent.Reschedule(::Thunder::Core::Time::Now().Add(scheduledTime), job); + _parent.Pool().Reschedule(::Thunder::Core::Time::Now().Add(scheduledTime), job); } void ValidateId() { @@ -313,15 +313,16 @@ namespace Core { std::vector*> _external; }; - class WorkerPoolTester : public ::Thunder::Core::WorkerPool, public JobControl, public ::Thunder::Core::Thread { + class WorkerPoolTester : public JobControl, public ::Thunder::Core::Thread { public: WorkerPoolTester() = delete; WorkerPoolTester(const WorkerPoolTester&) = delete; WorkerPoolTester& operator=(const WorkerPoolTester&) = delete; WorkerPoolTester(const uint8_t threads, const uint32_t stackSize, const uint32_t queueSize) - : WorkerPool(threads, stackSize, queueSize, &_dispatcher, nullptr, (threads > 2 ? (threads - 1) : 1), (threads > 2 ? (threads - 1) : 1)) - , JobControl(*this, threads) + : JobControl(*this, threads) + , _dispatcher() + , _pool(threads, stackSize, queueSize, &_dispatcher, nullptr, (threads > 2 ? (threads - 1) : threads), (threads > 2 ? (threads - 1) : threads)) { } @@ -335,7 +336,8 @@ namespace Core { public: void Stop() { - ::Thunder::Core::WorkerPool::Stop(); + _pool.Stop(); + assert(_pool.WaitForStop(5000) == true); ::Thunder::Core::Thread::Wait(::Thunder::Core::Thread::STOPPED|::Thunder::Core::Thread::BLOCKED, ::Thunder::Core::infinite); } @@ -345,12 +347,12 @@ namespace Core { } void SubmitJob(const ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>& job) { - Submit(job); + _pool.Submit(job); } virtual uint32_t Worker() override { if (IsRunning()) { - Join(); + _pool.Join(); } ::Thunder::Core::Thread::Block(); return (::Thunder::Core::infinite); @@ -361,9 +363,12 @@ namespace Core { } void RunThreadPool() { - static_cast(this)->Run(); + _pool.Run(); } + ::Thunder::Core::WorkerPool& Pool() { return _pool; } + const ::Thunder::Core::WorkerPool& Pool() const { return _pool; } + private: class Dispatcher : public ::Thunder::Core::ThreadPool::IDispatcher { public: @@ -381,6 +386,7 @@ namespace Core { }; Dispatcher _dispatcher; + ::Thunder::Core::WorkerPool _pool; }; TEST(Core_WorkerPool, CheckWorkerStaticMethods) { @@ -390,8 +396,8 @@ namespace Core { EXPECT_EQ(::Thunder::Core::WorkerPool::IsAvailable(), false); - ::Thunder::Core::WorkerPool::Assign(&workerPool); - EXPECT_EQ(&::Thunder::Core::WorkerPool::Instance(), &workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); + EXPECT_EQ(&::Thunder::Core::WorkerPool::Instance(), &workerPool.Pool()); EXPECT_EQ(::Thunder::Core::WorkerPool::IsAvailable(), true); ::Thunder::Core::WorkerPool::Assign(nullptr); @@ -402,11 +408,11 @@ namespace Core { uint8_t queueSize = 5; uint8_t threadCount = 1; WorkerPoolTester workerPool(threadCount, 0, queueSize); - ::Thunder::Core::WorkerPool::Assign(&workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch> job = ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>(::Thunder::Core::ProxyType>::Create(workerPool, TestJob::INITIATED, 500)); EXPECT_EQ(static_cast&>(*job).GetStatus(), TestJob::INITIATED); - workerPool.Submit(job); + workerPool.Pool().Submit(job); workerPool.RunThreadPool(); EXPECT_EQ(workerPool.WaitForJobEvent(job, MaxJobWaitTime), ::Thunder::Core::ERROR_NONE); workerPool.Stop(); @@ -420,12 +426,12 @@ namespace Core { uint8_t queueSize = 5; uint8_t threadCount = 1; WorkerPoolTester workerPool(threadCount, 0, queueSize); - ::Thunder::Core::WorkerPool::Assign(&workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch> job = ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>(::Thunder::Core::ProxyType>::Create(workerPool, TestJob::INITIATED)); EXPECT_EQ(static_cast&>(*job).GetStatus(), TestJob::INITIATED); - workerPool.Submit(job); - workerPool.Revoke(job); + workerPool.Pool().Submit(job); + workerPool.Pool().Revoke(job); workerPool.RunThreadPool(); EXPECT_EQ(workerPool.WaitForJobEvent(job, MaxJobWaitTime), ::Thunder::Core::ERROR_TIMEDOUT); workerPool.Stop(); @@ -439,11 +445,11 @@ namespace Core { uint8_t queueSize = 5; uint8_t threadCount = 1; WorkerPoolTester workerPool(threadCount, 0, queueSize); - ::Thunder::Core::WorkerPool::Assign(&workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch> job = ::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>(::Thunder::Core::ProxyType>::Create(workerPool, TestJob::INITIATED, 1000, true)); EXPECT_EQ(static_cast&>(*job).GetStatus(), TestJob::INITIATED); - workerPool.Submit(job); + workerPool.Pool().Submit(job); workerPool.RunThreadPool(); // Wait for job to actually start executing (wait for first notification) @@ -451,7 +457,7 @@ namespace Core { // Now job is waiting in WaitForReady - it's definitely running // Try to revoke while job is running (should fail to cancel) - workerPool.Revoke(job); + workerPool.Pool().Revoke(job); // Now notify it to continue and complete workerPool.NotifyReady(job); @@ -466,7 +472,7 @@ namespace Core { void CheckWorkerPool_MultipleJobs(const uint8_t threadCount, const uint8_t queueSize, const uint8_t additionalJobs, const bool runExternal = false) { WorkerPoolTester workerPool(threadCount, 0, queueSize); - ::Thunder::Core::WorkerPool::Assign(&workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); std::vector<::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>> jobs; // Create Jobs with more than Queue size. i.e, queueSize + additionalJobs @@ -540,7 +546,7 @@ namespace Core { void CheckWorkerPool_MultipleJobs_CancelJobs_InBetween(const uint8_t threadCount, const uint8_t queueSize, const uint8_t additionalJobs, const uint8_t cancelJobsCount, const uint8_t cancelJobsId[]) { WorkerPoolTester workerPool(threadCount, 0, queueSize); - ::Thunder::Core::WorkerPool::Assign(&workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); std::vector<::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>> jobs; // Create Jobs with more than Queue size. i.e, queueSize + additionalJobs @@ -563,7 +569,7 @@ namespace Core { // it just have to wait for processing jobs completion. // Hence revoking before starting the job. Just to ensure the status meets for (uint8_t index = 0; index < cancelJobsCount; index++) { - workerPool.Revoke(jobs[cancelJobsId[index]], 0); + workerPool.Pool().Revoke(jobs[cancelJobsId[index]], 0); } for (uint8_t index = 0; index < jobs.size(); index++) { @@ -627,7 +633,7 @@ namespace Core { void CheckWorkerPool_ScheduleJobs(const uint8_t threadCount, const uint8_t queueSize, const uint8_t additionalJobs, const uint8_t cancelJobsCount, const uint8_t* cancelJobsId, const uint16_t scheduledTimes[]) { WorkerPoolTester workerPool(threadCount, 0, queueSize); - ::Thunder::Core::WorkerPool::Assign(&workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); workerPool.RunThreadPool(); workerPool.RunExternal(); @@ -647,7 +653,7 @@ namespace Core { } for (uint8_t index = 0; index < cancelJobsCount; index++) { - workerPool.Revoke(jobs[cancelJobsId[index]], 0); + workerPool.Pool().Revoke(jobs[cancelJobsId[index]], 0); } for (uint8_t index = 0; index < jobs.size(); index++) { @@ -781,7 +787,7 @@ namespace Core { void CheckWorkerPool_RescheduleJobs(const uint8_t threadCount, const uint8_t queueSize, const uint8_t additionalJobs, const uint8_t cancelJobsCount, const uint8_t* cancelJobsId, const uint16_t scheduledTimes[], const uint16_t rescheduledTimes[]) { WorkerPoolTester workerPool(threadCount, 0, queueSize); - ::Thunder::Core::WorkerPool::Assign(&workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); workerPool.RunThreadPool(); std::vector<::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>> jobs; @@ -803,7 +809,7 @@ namespace Core { workerPool.RescheduleJobs(jobs[i], rescheduledTimes[i]); } for (uint8_t index = 0; index < cancelJobsCount; index++) { - workerPool.Revoke(jobs[cancelJobsId[index]], 0); + workerPool.Pool().Revoke(jobs[cancelJobsId[index]], 0); } for (uint8_t index = 0; index < jobs.size(); index++) { @@ -966,7 +972,7 @@ namespace Core { void CheckWorkerPool_MetaData(uint8_t threadCount, uint8_t queueSize, uint8_t additionalJobs) { WorkerPoolTester workerPool(threadCount, 0, queueSize); - ::Thunder::Core::WorkerPool::Assign(&workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); std::vector<::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>> jobs; // Create Jobs with more than Queue size. i.e, queueSize + additionalJobs @@ -1029,7 +1035,7 @@ namespace Core { void CheckWorkerPool_Ids(uint8_t threadCount, uint8_t queueSize, uint8_t additionalJobs) { WorkerPoolTester workerPool(threadCount, 0, queueSize); - ::Thunder::Core::WorkerPool::Assign(&workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); workerPool.RunThreadPool(); std::vector<::Thunder::Core::ProxyType<::Thunder::Core::IDispatch>> jobs; @@ -1150,7 +1156,7 @@ namespace Core { TEST(Core_WorkerPool, Check_JobType_Submit) { WorkerPoolTester workerPool(1, 0, 1); - ::Thunder::Core::WorkerPool::Assign(&workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); workerPool.RunThreadPool(); { WorkerJobTester jobTester(0); @@ -1163,7 +1169,7 @@ namespace Core { TEST(Core_WorkerPool, Check_JobType_Submit_Revoke) { WorkerPoolTester workerPool(4, 0, 1); - ::Thunder::Core::WorkerPool::Assign(&workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); { WorkerJobTester jobTester(0); EXPECT_EQ(jobTester.Submit(), true); @@ -1177,7 +1183,7 @@ namespace Core { TEST(Core_WorkerPool, Check_JobType_Schedule) { WorkerPoolTester workerPool(4, 0, 1); - ::Thunder::Core::WorkerPool::Assign(&workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); workerPool.RunThreadPool(); { WorkerJobTester jobTester(0); @@ -1190,7 +1196,7 @@ namespace Core { TEST(Core_WorkerPool, Check_JobType_Schedule_Revoke) { WorkerPoolTester workerPool(4, 0, 1); - ::Thunder::Core::WorkerPool::Assign(&workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); workerPool.RunThreadPool(); { WorkerJobTester jobTester(0); @@ -1205,7 +1211,7 @@ namespace Core { void CheckJobType_Reschedule(const uint16_t scheduleTime, const uint16_t rescheduleTime) { WorkerPoolTester workerPool(4, 0, 1); - ::Thunder::Core::WorkerPool::Assign(&workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); workerPool.RunThreadPool(); { WorkerJobTester jobTester(0); @@ -1225,7 +1231,7 @@ namespace Core { TEST(Core_WorkerPool, Check_JobType_Reschedule_Revoke) { WorkerPoolTester workerPool(4, 0, 1); - ::Thunder::Core::WorkerPool::Assign(&workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); workerPool.RunThreadPool(); { WorkerJobTester jobTester(0); @@ -1242,7 +1248,7 @@ namespace Core { void CheckJobType_RescheduleJobs(const uint8_t threadCount, const uint8_t queueSize, const uint8_t additionalJobs, const uint8_t cancelJobsCount, const uint8_t* cancelJobsId, const uint16_t scheduledTimes[], const uint16_t rescheduledTimes[]) { WorkerPoolTester workerPool(threadCount, 0, queueSize); - ::Thunder::Core::WorkerPool::Assign(&workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); workerPool.RunThreadPool(); { std::vector<::Thunder::Core::ProxyType> jobs; @@ -1316,7 +1322,7 @@ namespace Core { TEST(Core_WorkerPool, Check_JobType_Reschedule_WhileRunning) { WorkerPoolTester workerPool(4, 0, 1); - ::Thunder::Core::WorkerPool::Assign(&workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); workerPool.RunThreadPool(); { WorkerJobTester jobTester(1000); @@ -1420,7 +1426,7 @@ namespace Core { void CheckWorkerPool_ReschduledTimedJob(const uint8_t threadCount, const uint8_t queueSize, const uint16_t timedWait, const uint16_t jobWait, const uint8_t times) { WorkerPoolTester workerPool(threadCount, 0, queueSize); - ::Thunder::Core::WorkerPool::Assign(&workerPool); + ::Thunder::Core::WorkerPool::Assign(&workerPool.Pool()); workerPool.RunThreadPool(); { // Schedule the Job every timedWait (eg: 10 Seconds) to run once.. From 06c633faceab10e2fdd0b1f6e1a618eec0e40a27 Mon Sep 17 00:00:00 2001 From: sebaszm <45654185+sebaszm@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:09:46 +0100 Subject: [PATCH 17/64] Fix Activate reason (#1997) * Fix Activate reason * Exit precondition state on error * Add INSTANTIATION_FAILED reason --------- Co-authored-by: MFransen69 <39826971+MFransen69@users.noreply.github.com> --- Source/Thunder/PluginServer.cpp | 5 ++++- Source/plugins/IShell.h | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Source/Thunder/PluginServer.cpp b/Source/Thunder/PluginServer.cpp index 56c15ca624..b529f0a857 100644 --- a/Source/Thunder/PluginServer.cpp +++ b/Source/Thunder/PluginServer.cpp @@ -343,6 +343,8 @@ namespace PluginHost { Unlock(); } else if ((currentState == IShell::state::DEACTIVATED) || (currentState == IShell::state::PRECONDITION)) { + _reason = why; + // Load the interfaces, If we did not load them yet... if (_handler == nullptr) { AcquireInterfaces(); @@ -354,6 +356,8 @@ namespace PluginHost { if (_handler == nullptr) { SYSLOG(Logging::Startup, (_T("Loading of plugin [%s]:[%s], failed. Error [%s]"), className.c_str(), callSign.c_str(), ErrorMessage().c_str())); result = Core::ERROR_UNAVAILABLE; + _reason = reason::INSTANTIATION_FAILED; + State(DEACTIVATED); Unlock(); @@ -361,7 +365,6 @@ namespace PluginHost { } else if (_precondition.IsMet() == false) { SYSLOG(Logging::Startup, (_T("Activation of plugin [%s]:[%s], postponed, preconditions have not been met, yet."), className.c_str(), callSign.c_str())); result = Core::ERROR_PENDING_CONDITIONS; - _reason = why; State(PRECONDITION); if (Thunder::Messaging::LocalLifetimeType::IsEnabled() == true) { diff --git a/Source/plugins/IShell.h b/Source/plugins/IShell.h index cc2fb33575..fda613bc1c 100644 --- a/Source/plugins/IShell.h +++ b/Source/plugins/IShell.h @@ -106,7 +106,8 @@ namespace PluginHost { SHUTDOWN, CONDITIONS, WATCHDOG_EXPIRED, - INITIALIZATION_FAILED + INITIALIZATION_FAILED, + INSTANTIATION_FAILED }; /* @stubgen:omit */ From 56e4cc4724a3f9758061d20da076c07182574f89 Mon Sep 17 00:00:00 2001 From: sramani-metro <71630728+sramani-metro@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:03:09 +0000 Subject: [PATCH 18/64] Break dependency of UnknownProxy::Invoke (#1999) Co-authored-by: MFransen69 <39826971+MFransen69@users.noreply.github.com> --- Source/com/IUnknown.cpp | 33 +++++++++++++++++++++++++++++++++ Source/com/IUnknown.h | 31 +------------------------------ 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/Source/com/IUnknown.cpp b/Source/com/IUnknown.cpp index d98d55b163..7bfe227e35 100644 --- a/Source/com/IUnknown.cpp +++ b/Source/com/IUnknown.cpp @@ -99,6 +99,39 @@ namespace ProxyStub { // ------------------------------------------------------------------------------------------- // PROXY // ------------------------------------------------------------------------------------------- + + uint32_t UnknownProxy::Invoke(Core::ProxyType& message, const uint32_t waitTime) const + { + uint32_t result = Core::ERROR_UNAVAILABLE | COM_ERROR; + + _adminLock.Lock(); + Core::ProxyType channel (_channel); + _adminLock.Unlock(); + + if (channel.IsValid() == true) { + + if (channel->InProgress() == true) { + ASSERT(false && "IPC in progress detected on this channel. Possible deadlock!"); + SYSLOG(Logging::Error, (_T("IPC in progress detected on this channel for Interface [0x%X], Method ID [0x%X]. Possible deadlock!"), message->Parameters().InterfaceId(), message->Parameters().MethodId())); + } + + result = channel->Invoke(message, waitTime); + if (result != Core::ERROR_NONE) { + + if (result == Core::ERROR_TIMEDOUT) { + Shutdown(); + } + + result |= COM_ERROR; + + // Oops something failed on the communication. Report it. + TRACE_L1("IPC method invocation failed for 0x%X, Method ID 0x%X error: %d", message->Parameters().InterfaceId(), message->Parameters().MethodId(), result); + } + } + + return (result); + } + uint32_t UnknownProxy::Id() const { uint32_t id = 0; diff --git a/Source/com/IUnknown.h b/Source/com/IUnknown.h index 830b47950b..10b240227e 100644 --- a/Source/com/IUnknown.h +++ b/Source/com/IUnknown.h @@ -317,38 +317,9 @@ namespace ProxyStub { return (message); } - inline uint32_t Invoke(Core::ProxyType& message, const uint32_t waitTime = RPC::CommunicationTimeOut) const - { - uint32_t result = Core::ERROR_UNAVAILABLE | COM_ERROR; - - _adminLock.Lock(); - Core::ProxyType channel (_channel); - _adminLock.Unlock(); - - if (channel.IsValid() == true) { - - if (channel->InProgress() == true) { - ASSERT(false && "IPC in progress detected on this channel. Possible deadlock!"); - SYSLOG(Logging::Error, (_T("IPC in progress detected on this channel for Interface [0x%X], Method ID [0x%X]. Possible deadlock!"), message->Parameters().InterfaceId(), message->Parameters().MethodId())); - } - - result = channel->Invoke(message, waitTime); - if (result != Core::ERROR_NONE) { + uint32_t Invoke(Core::ProxyType& message, const uint32_t waitTime = RPC::CommunicationTimeOut) const; - if (result == Core::ERROR_TIMEDOUT) { - Shutdown(); - } - - result |= COM_ERROR; - - // Oops something failed on the communication. Report it. - TRACE_L1("IPC method invocation failed for 0x%X, Method ID 0x%X error: %d", message->Parameters().InterfaceId(), message->Parameters().MethodId(), result); - } - } - - return (result); - } inline void Complete(RPC::Data::Setup& response) { uint32_t result = Release(); From e8f4c1df11afe54341defe10761954421ae1a43d Mon Sep 17 00:00:00 2001 From: Mateusz Daniluk <121170681+VeithMetro@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:22:37 +0100 Subject: [PATCH 19/64] [Doc] Update the config docs with the Priority Queue related settings (#1995) * Update the config doc with the Priority Queue related settings * Update docs/introduction/config.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: MFransen69 <39826971+MFransen69@users.noreply.github.com> --- docs/introduction/config.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/introduction/config.md b/docs/introduction/config.md index 6a36f6b0e1..b3f0f52b68 100644 --- a/docs/introduction/config.md +++ b/docs/introduction/config.md @@ -25,27 +25,30 @@ This section documents the available options for Thunder. This is different from | interface | The network interface Thunder will bind on. If empty, will pick the first appropriate interface. | string | - | `eth0` | | prefix | URL prefix for the REST/HTTP endpoint | string | Service | | | jsonrpc | URL prefix for the JSON-RPC endpoint | string | jsonrpc | | -| persistentpath | Directory to store persistent data in.

Each plugin will have an associated directory underneath this corresponding to the callsign of the plugin. | string | - | /opt/thunder/ | -| datapath | Read-only directory plugins can read data from.

Each plugin will have an associated directory underneath this corresponding to the callsign of the plugin. | string | - | usr/share/thunder | -| systempath | Directory plugin libraries are installed and available in | string | - | /usr/lib/thunder/ | +| persistentpath | Directory to store persistent data in.

Each plugin will have an associated directory underneath this corresponding to the callsign of the plugin. | string | - | /opt/thunder/ | +| datapath | Read-only directory plugins can read data from.

Each plugin will have an associated directory underneath this corresponding to the callsign of the plugin. | string | - | usr/share/thunder | +| systempath | Directory plugin libraries are installed and available in | string | - | /usr/lib/thunder/ | | volatilepath | Directory to store volatile temporary data.

Each plugin will have an associated directory underneath this corresponding to the callsign of the plugin | string | /tmp | /tmp/ | -| proxystubpath | Directory to search for the generated proxy stub libraries | string | - | /usr/lib/thunder/proxystubs | +| proxystubpath | Directory to search for the generated proxy stub libraries | string | - | /usr/lib/thunder/proxystubs | | postmortempath | Directory to store debugging info (worker pool information, debug data) in the event of a plugin or server crash.

If breakpad is found during build, will store breakpad mindumps here | string | /opt/minidumps | /opt/minidumps | | communicator | Socket to listen for COM-RPC messages. Can be a filesystem path on Linux for a Unix domain socket, or a TCP socket.

For unix sockets, the file permissions can be specified by adding a `|` followed by the numeric permissions | string | /tmp/communicator\|0777 | 127.0.0.1:4000 | -| redirect | Redirect incoming HTTP requests to the root Thunder URL to this address (please note it must contain the resource that is required e.g. index.html ) | string | http://127.0.0.1/Service/Controller/UI/index.html | http://127.0.0.1/Service/Controller/UI/index.html | +| redirect | Redirect incoming HTTP requests to the root Thunder URL to this address (please note it must contain the resource that is required e.g. index.html ) | string | http://127.0.0.1/Service/Controller/UI/index.html | http://127.0.0.1/Service/Controller/UI/index.html | | idletime | Amount of time (in seconds) to wait before closing and cleaning up idle client connections. If no activity occurs over a connection for this time Thunder will close it. | integer | 180 | 180 | | softkillcheckwaittime | When killing an out-of-process plugin, the amount of time to wait after sending a SIGTERM signal to the process before checking & trying again | integer | 3 | 3 | | hardkillcheckwaittime | When killing an out-of-process plugin, the amount of time to wait after sending a SIGKILL signal to the process before trying again | integer | 10 | 10 | | legacyinitalize | Enables legacy Plugin initialization behaviour where the Deinitialize() method is not called on if Initialize() fails. For backwards compatibility | bool | false | false | | defaultmessagingcategories | See "Messaging configuration" below | object | - | - | | defaultwarningreportingcategories | See "Warning Reporting Configuration" below | array | - | - | -| process.user | The Linux user the Thunder process runs as | string | - | myusr | -| process.group | The Linux group the Thunder process runs under | string | - | mygrp | -| process.priority | The nice priority of the Thunder process | integer | - | 0 | +| process.user | The Linux user the Thunder process runs as | string | - | myusr | +| process.group | The Linux group the Thunder process runs under | string | - | mygrp | +| process.priority | The nice priority of the Thunder process | integer | - | 0 | | process.policy | The linux scheduling priority of the Thunder process. Valid values are: `Batch`, `FIFO`, `Idle`, `RoundRobin`, `Other` | string | - | OTHER | | process.oomadjust | The OOM killer score (see [here](https://lwn.net/Articles/317814/) for more info) | integer | - | 0 | | process.stacksize | The default stack size in bytes for spawned threads. If not set or 0, will use to Linux defaults | integer | - | 4096 | -| process.umask | Set the Thunder umask value | integer | - | 077 | +| process.umask | The umask value for the Thunder process | integer | - | 077 | +| process.threadpoolcount | Total amount of available threads | integer | 4 | 4 | +| process.lowprioritythreadcount | Maximum amount of low priority jobs executed in parallel | integer | 3 | 3 | +| process.mediumprioritythreadcount | Maximum amount of medium priority jobs executed in parallel | integer | 3 | 3 | | input.locator | If using Thunder input handling. Socket to receive key events over | string | /tmp/keyhandler\|0766 | - | | input.type | If using Thunder input handling. Input device type (either `device` (`/dev/uinput`) or `virtual` (json-rpc api) | string | Virtual | Device | | input.output | If using Thunder input handling. Whether input events should be re-output for forwarding | bool | true | - | @@ -57,6 +60,6 @@ This section documents the available options for Thunder. This is different from | messagingport | By default, the messaging engine sends log/trace messages over a unix socket. Provide a TCP port here to use that port instead if desired | int | - | 3000 | | processcontainers.logging | Path for container logs if using process container. Behaviour will vary depending on container backend | string | - | - | | linkerpluginpaths | Array of additional directories to search for .so files | array | - | - | -| observe.proxystubpath | Directory to monitor for new proxy stub libraries. If libraries are added during runtime, Thunder will load these new proxystubs | string | - | /root/thunder/dynamic/proxystubs | -| observe.configpath | Directory to monitor for new plugin configuration files. If config files are added during runtime, Thunder will load them | string | - | /root/thunder/dynamic/config | +| observe.proxystubpath | Directory to monitor for new proxy stub libraries. If libraries are added during runtime, Thunder will load these new proxystubs | string | - | /root/thunder/dynamic/proxystubs | +| observe.configpath | Directory to monitor for new plugin configuration files. If config files are added during runtime, Thunder will load them | string | - | /root/thunder/dynamic/config | | hibernate.locator | Configuration for the process hibernation feature (alpha) | string | - | - | From 0af0dfab66b650905697c0586543ff70e4e8aca1 Mon Sep 17 00:00:00 2001 From: sramani-metro <71630728+sramani-metro@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:07:11 +0000 Subject: [PATCH 20/64] Remove ::template keyword to make clang happy (#1998) --- Source/core/Proxy.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/core/Proxy.h b/Source/core/Proxy.h index 1d865a7091..27d279bbcd 100644 --- a/Source/core/Proxy.h +++ b/Source/core/Proxy.h @@ -1479,7 +1479,7 @@ POP_WARNING() for (uint32_t index = 0; index < initialQueueSize; index++) { Core::ProxyType newElement; - Core::ProxyType::template CreateMove(newElement, 0, *this, std::forward(args)...); + Core::ProxyType::CreateMove(newElement, 0, *this, std::forward(args)...); _queue.emplace_back(std::move(newElement)); ASSERT(_queue.back().IsValid() == true); } @@ -1531,7 +1531,7 @@ POP_WARNING() _lock.Unlock(); - Core::ProxyType::template CreateMove(element, 0, *this, std::forward(args)...); + Core::ProxyType::CreateMove(element, 0, *this, std::forward(args)...); result = Core::ProxyType(element); } @@ -1649,7 +1649,7 @@ POP_WARNING() if (index == _map.end()) { // Oops we do not have such an element, create it... Core::ProxyType newItem; - Core::ProxyType::template CreateMove(newItem, 0, *this, std::forward(args)...); + Core::ProxyType::CreateMove(newItem, 0, *this, std::forward(args)...); if (newItem.IsValid() == true) { @@ -1810,7 +1810,7 @@ POP_WARNING() Core::ProxyType result; Core::ProxyType newItem; - Core::ProxyType::template CreateMove(newItem, 0, *this, std::forward(args)...); + Core::ProxyType::CreateMove(newItem, 0, *this, std::forward(args)...); if (newItem.IsValid() == true) { From 8ce639d58c80ab9050f78c2209a46fd560e44d41 Mon Sep 17 00:00:00 2001 From: Karthick Somasundaresan Date: Wed, 12 Nov 2025 15:53:42 +0530 Subject: [PATCH 21/64] Starting COM after Controller is initialized (#2001) --- Source/Thunder/PluginServer.cpp | 1 + Source/Thunder/PluginServer.h | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Source/Thunder/PluginServer.cpp b/Source/Thunder/PluginServer.cpp index b529f0a857..237600a79f 100644 --- a/Source/Thunder/PluginServer.cpp +++ b/Source/Thunder/PluginServer.cpp @@ -921,6 +921,7 @@ namespace PluginHost { // class Server::ServiceMap // ----------------------------------------------------------------------------------------------------------------------------------- void Server::ServiceMap::Open(std::vector& externallyControlled) { + _processAdministrator.Open(); // Load the metadata for the subsystem information.. if (Configuration().MetadataDiscovery() == false) { SYSLOG(Logging::Startup, (_T("Automatic metadata discovery and plugin versioning is DISABLED!!!"))); diff --git a/Source/Thunder/PluginServer.h b/Source/Thunder/PluginServer.h index 9074ff62fb..b5ff3afdce 100644 --- a/Source/Thunder/PluginServer.h +++ b/Source/Thunder/PluginServer.h @@ -2287,14 +2287,7 @@ namespace PluginHost { // STRONG RECOMMENDATION TO HAVE THIS ACTIVE (TRUE)!!! RPC::Administrator::Instance().DelegatedReleases(delegatedReleases); - if (RPC::Communicator::Open(RPC::CommunicationTimeOut) != Core::ERROR_NONE) { - TRACE_L1("We can not open the RPC server. No out-of-process communication available. %d", __LINE__); - } else { - // We need to pass the communication channel NodeId via an environment variable, for process, - // not being started by the rpcprocess... - Core::SystemInfo::SetEnvironment(string(CommunicatorConnector), RPC::Communicator::Connector()); - RPC::Communicator::ForcedDestructionTimes(softKillCheckWaitTime, hardKillCheckWaitTime); - } + RPC::Communicator::ForcedDestructionTimes(softKillCheckWaitTime, hardKillCheckWaitTime); if (observableProxyStubPath.empty() == true) { SYSLOG(Logging::Startup, (_T("Dynamic COMRPC disabled."))); @@ -2453,6 +2446,15 @@ namespace PluginHost { _deadProxiesProtection.Unlock(); } + void Open() { + if (RPC::Communicator::Open(RPC::CommunicationTimeOut) != Core::ERROR_NONE) { + TRACE_L1("We can not open the RPC server. No out-of-process communication available. %d", __LINE__); + } else { + // We need to pass the communication channel NodeId via an environment variable, for process, + // not being started by the rpcprocess... + Core::SystemInfo::SetEnvironment(string(CommunicatorConnector), RPC::Communicator::Connector()); + } + } private: void Reload(const string& path) { TRACE(Activity, (Core::Format(_T("Reloading ProxyStubs from %s."), path.c_str()))); From afc0fe39a511264a5693a257d6eea9beef9a157f Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Thu, 13 Nov 2025 08:14:08 +0100 Subject: [PATCH 22/64] Development/workerpool fixes (#2003) --- Source/core/Thread.cpp | 4 + Source/core/ThreadPool.h | 25 + Source/core/WorkerPool.h | 8 +- Tests/unit/CMakeLists.txt | 1 + Tests/unit/workerpool/CMakeLists.txt | 29 + .../test_WorkerPool_SelfResubmit.cpp | 538 ++++++++++++++++++ 6 files changed, 604 insertions(+), 1 deletion(-) create mode 100644 Tests/unit/workerpool/CMakeLists.txt create mode 100644 Tests/unit/workerpool/test_WorkerPool_SelfResubmit.cpp diff --git a/Source/core/Thread.cpp b/Source/core/Thread.cpp index b5251b3102..fd33ed99d6 100644 --- a/Source/core/Thread.cpp +++ b/Source/core/Thread.cpp @@ -371,6 +371,8 @@ POP_WARNING() { bool blOK = false; + m_enumState.Lock(); + switch (m_enumState) { case INITIALIZED: blOK = ((enumNewState == RUNNING) || (enumNewState == BLOCKED) || (enumNewState == STOPPED) || (enumNewState == STOPPING)); @@ -419,6 +421,8 @@ POP_WARNING() } } + m_enumState.Unlock(); + return (blOK); } diff --git a/Source/core/ThreadPool.h b/Source/core/ThreadPool.h index 238b30f61e..5b24fa5a46 100644 --- a/Source/core/ThreadPool.h +++ b/Source/core/ThreadPool.h @@ -262,6 +262,31 @@ namespace Core { return (result); } + /** + * @brief Attempt to submit the associated job to the thread pool. + * + * This function inspects and updates the internal atomic _state to decide + * how to handle the submission: + * - If the pool state is IDLE, it atomically transitions IDLE -> SUBMITTED + * and returns a ProxyType that represents the newly submitted job. + * - If the pool state is EXECUTING or SCHEDULE, it atomically sets the state + * to RESUBMIT (EXECUTING -> RESUBMIT or SCHEDULE -> RESUBMIT) to request + * a re-run, but does not return a dispatch proxy. + * - In any other state (including already RESUBMIT), no state change is made + * and an empty proxy is returned. + * + * The implementation uses compare_exchange_strong on a shared atomic state + * and relies on short-circuit evaluation to perform at most one successful + * transition. Callers should inspect the returned ProxyType to + * determine whether this call actually enqueued the job (non-empty) or + * merely marked it for resubmission / left it unchanged (empty). + * + * Thread-safety: safe to call concurrently; state transitions are performed + * atomically. No exception guarantees beyond those of ProxyType construction. + * + * @return ProxyType Non-empty when the job was transitioned from + * IDLE to SUBMITTED (i.e., submission succeeded). Empty otherwise. + */ ProxyType Submit() { state executing = EXECUTING; diff --git a/Source/core/WorkerPool.h b/Source/core/WorkerPool.h index 42a897afd0..225168a631 100644 --- a/Source/core/WorkerPool.h +++ b/Source/core/WorkerPool.h @@ -314,8 +314,13 @@ namespace Core { WorkerPool(const WorkerPool&) = delete; WorkerPool& operator=(const WorkerPool&) = delete; + WorkerPool(const uint8_t threadCount, const uint32_t stackSize, const uint32_t queueSize, ThreadPool::IDispatcher* dispatcher, ThreadPool::ICallback* callback = nullptr) + : WorkerPool(threadCount, stackSize, queueSize, dispatcher, callback, threadCount, threadCount, 1) + { + } + PUSH_WARNING(DISABLE_WARNING_THIS_IN_MEMBER_INITIALIZER_LIST) - WorkerPool(const uint8_t threadCount, const uint32_t stackSize, const uint32_t queueSize, ThreadPool::IDispatcher* dispatcher, ThreadPool::ICallback* callback = nullptr, const uint16_t lowPriorityThreadCount = 0, const uint16_t mediumPriorityThreadCount = 0, const uint8_t additionalThreads = 0) + WorkerPool(const uint8_t threadCount, const uint32_t stackSize, const uint32_t queueSize, ThreadPool::IDispatcher* dispatcher, ThreadPool::ICallback* callback, const uint16_t lowPriorityThreadCount, const uint16_t mediumPriorityThreadCount, const uint8_t additionalThreads = 0) : _scheduler(this, _timer) , _threadPool(threadCount, stackSize, queueSize, dispatcher, &_scheduler, &_external, callback, lowPriorityThreadCount, mediumPriorityThreadCount, additionalThreads) , _external(_threadPool, dispatcher) @@ -334,6 +339,7 @@ POP_WARNING() ~WorkerPool() { _threadPool.Stop(); + _threadPool.WaitForStop(); delete[] _metadata.Slot; } diff --git a/Tests/unit/CMakeLists.txt b/Tests/unit/CMakeLists.txt index b56086ae79..ce68b39681 100644 --- a/Tests/unit/CMakeLists.txt +++ b/Tests/unit/CMakeLists.txt @@ -39,3 +39,4 @@ enable_testing() add_subdirectory(core) add_subdirectory(tests) +add_subdirectory(workerpool) \ No newline at end of file diff --git a/Tests/unit/workerpool/CMakeLists.txt b/Tests/unit/workerpool/CMakeLists.txt new file mode 100644 index 0000000000..1a34cbdecd --- /dev/null +++ b/Tests/unit/workerpool/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.10) +project(WorkerPoolSelfResubmitTest) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Find GTest +find_package(GTest REQUIRED) +include_directories(${GTEST_INCLUDE_DIRS}) + +# Add the test executable +add_executable(test_WorkerPool_SelfResubmit + test_WorkerPool_SelfResubmit.cpp +) + +# Link against Thunder and GTest +target_link_libraries(test_WorkerPool_SelfResubmit + ${GTEST_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} + ${NAMESPACE}Core::${NAMESPACE}Core +) + +install( + TARGETS test_WorkerPool_SelfResubmit + DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT ${NAMESPACE}_Test) + +# Add the test to CTest +enable_testing() +add_test(NAME WorkerPoolSelfResubmit COMMAND test_WorkerPool_SelfResubmit) diff --git a/Tests/unit/workerpool/test_WorkerPool_SelfResubmit.cpp b/Tests/unit/workerpool/test_WorkerPool_SelfResubmit.cpp new file mode 100644 index 0000000000..3f5493481f --- /dev/null +++ b/Tests/unit/workerpool/test_WorkerPool_SelfResubmit.cpp @@ -0,0 +1,538 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2020 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "core/WorkerPool.h" +#include "core/Sync.h" +#include +#include + +using namespace Thunder; +using namespace Thunder::Core; + +/** + * Test to verify that a WorkerPool job can safely submit itself again + * from within its own Dispatch() method. + */ +class WorkerPoolSelfResubmitTest : public ::testing::Test { +protected: + void SetUp() override + { + // Create a WorkerPool with multiple threads + _dispatcher = new WorkerPoolDispatcher(); + _workerPool = new WorkerPool( + 1, // threadCount + Core::Thread::DefaultStackSize(), // stackSize (0 = default) + 2, // queueSize + _dispatcher, + nullptr + ); + + IWorkerPool::Assign(_workerPool); + _workerPool->Run(); + } + + void TearDown() override + { + if (_workerPool != nullptr) { + _workerPool->Stop(); + IWorkerPool::Assign(nullptr); + delete _workerPool; + _workerPool = nullptr; + } + + if (_dispatcher != nullptr) { + delete _dispatcher; + _dispatcher = nullptr; + } + } + + // Simple dispatcher for testing + class WorkerPoolDispatcher : public ThreadPool::IDispatcher { + public: + WorkerPoolDispatcher() = default; + ~WorkerPoolDispatcher() override = default; + + void Initialize() override {} + void Deinitialize() override {} + void Dispatch(IDispatch* job) override + { + job->Dispatch(); + } + }; + + WorkerPoolDispatcher* _dispatcher; + WorkerPool* _workerPool; +}; + +/** + * Test Case 1: Basic self-resubmission + * Verify that a job can call Submit() on itself from within Dispatch() + */ +TEST_F(WorkerPoolSelfResubmitTest, BasicSelfResubmit) +{ + class SelfResubmittingJob { + private: + Core::WorkerPool::JobType _job; + std::atomic _executionCount; + std::atomic _maxExecutions; + Core::Event _completed; + + public: + SelfResubmittingJob(uint32_t maxExecutions) + : _job(*this) + , _executionCount(0) + , _maxExecutions(maxExecutions) + , _completed(false, true) + { + } + + ~SelfResubmittingJob() + { + _job.Revoke(); + } + + void Start() + { + _job.Submit(); + } + + bool WaitForCompletion(uint32_t timeoutMs) + { + return (_completed.Lock(timeoutMs) == ERROR_NONE); + } + + uint32_t GetExecutionCount() const + { + return _executionCount.load(); + } + + void Dispatch() + { + uint32_t currentCount = ++_executionCount; + + // Self-resubmit if we haven't reached max executions + if (currentCount < _maxExecutions) { + ASSERT_TRUE(_job.Submit()); + } else { + _completed.SetEvent(); + } + } + }; + + // Create a job that will resubmit itself 5 times + SelfResubmittingJob job(5); + job.Start(); + + // Wait for completion with timeout + ASSERT_TRUE(job.WaitForCompletion(Core::infinite)) << "Job did not complete within timeout"; + EXPECT_EQ(job.GetExecutionCount(), 5u) << "Job should have executed exactly 5 times"; +} + +/** + * Test Case 2: Rapid self-resubmission stress test + * Verify stability under rapid resubmissions + */ +TEST_F(WorkerPoolSelfResubmitTest, RapidSelfResubmitStressTest) +{ + class RapidResubmittingJob { + private: + Core::WorkerPool::JobType _job; + std::atomic _executionCount; + std::atomic _maxExecutions; + Core::Event _completed; + + public: + RapidResubmittingJob(uint32_t maxExecutions) + : _job(*this) + , _executionCount(0) + , _maxExecutions(maxExecutions) + , _completed(false, true) + { + } + + ~RapidResubmittingJob() + { + _job.Revoke(); + } + + void Start() + { + _job.Submit(); + } + + bool WaitForCompletion(uint32_t timeoutMs) + { + return (_completed.Lock(timeoutMs) == ERROR_NONE); + } + + uint32_t GetExecutionCount() const + { + return _executionCount.load(); + } + + void Dispatch() + { + uint32_t currentCount = ++_executionCount; + + if (currentCount < _maxExecutions) { + // Immediately resubmit without any delay + _job.Submit(); + } else { + _completed.SetEvent(); + } + } + }; + + // Stress test with 100 rapid resubmissions + RapidResubmittingJob job(100); + job.Start(); + + ASSERT_TRUE(job.WaitForCompletion(10000)) << "Job did not complete within timeout"; + EXPECT_EQ(job.GetExecutionCount(), 100u) << "Job should have executed exactly 100 times"; +} + +/** + * Test Case 3: Multiple concurrent self-resubmitting jobs + * Verify that multiple jobs can self-resubmit without interfering with each other + */ +TEST_F(WorkerPoolSelfResubmitTest, MultipleConcurrentSelfResubmittingJobs) +{ + class ConcurrentResubmittingJob { + private: + Core::WorkerPool::JobType _job; + std::atomic _executionCount; + std::atomic _maxExecutions; + Core::Event _completed; + uint32_t _jobId; + + public: + ConcurrentResubmittingJob(uint32_t jobId, uint32_t maxExecutions) + : _job(*this) + , _executionCount(0) + , _maxExecutions(maxExecutions) + , _completed(false, true) + , _jobId(jobId) + { + } + + ~ConcurrentResubmittingJob() + { + _job.Revoke(); + } + + void Start() + { + _job.Submit(); + } + + bool WaitForCompletion(uint32_t timeoutMs) + { + return (_completed.Lock(timeoutMs) == ERROR_NONE); + } + + uint32_t GetExecutionCount() const + { + return _executionCount.load(); + } + + void Dispatch() + { + uint32_t currentCount = ++_executionCount; + + if (currentCount < _maxExecutions) { + _job.Submit(); + } else { + _completed.SetEvent(); + } + } + }; + + // Create multiple jobs that will run concurrently + constexpr uint32_t NUM_JOBS = 5; + constexpr uint32_t EXECUTIONS_PER_JOB = 20; + + std::vector> jobs; + + for (uint32_t i = 0; i < NUM_JOBS; ++i) { + jobs.push_back(std::make_unique(i, EXECUTIONS_PER_JOB)); + } + + // Start all jobs + for (auto& job : jobs) { + job->Start(); + } + + // Wait for all jobs to complete + for (auto& job : jobs) { + ASSERT_TRUE(job->WaitForCompletion(10000)) << "Job did not complete within timeout"; + EXPECT_EQ(job->GetExecutionCount(), EXECUTIONS_PER_JOB) + << "Job should have executed exactly " << EXECUTIONS_PER_JOB << " times"; + } +} + +/** + * Test Case 4: Self-resubmission with conditional logic + * Verify that jobs can make decisions about resubmission + */ +TEST_F(WorkerPoolSelfResubmitTest, ConditionalSelfResubmit) +{ + class ConditionalResubmittingJob { + private: + Core::WorkerPool::JobType _job; + std::atomic _executionCount; + std::atomic _successCount; + Core::Event _completed; + + public: + ConditionalResubmittingJob() + : _job(*this) + , _executionCount(0) + , _successCount(0) + , _completed(false, true) + { + } + + ~ConditionalResubmittingJob() + { + _job.Revoke(); + } + + void Start() + { + _job.Submit(); + } + + bool WaitForCompletion(uint32_t timeoutMs) + { + return (_completed.Lock(timeoutMs) == ERROR_NONE); + } + + uint32_t GetExecutionCount() const + { + return _executionCount.load(); + } + + uint32_t GetSuccessCount() const + { + return _successCount.load(); + } + + void Dispatch() + { + uint32_t currentCount = ++_executionCount; + + // Simulate some work that might succeed or fail + bool success = (currentCount % 3) != 0; // Fails every 3rd execution + + if (success) { + _successCount++; + } + + // Keep retrying until we have 5 successes + if (_successCount < 5) { + _job.Submit(); + } else { + _completed.SetEvent(); + } + } + }; + + ConditionalResubmittingJob job; + job.Start(); + + ASSERT_TRUE(job.WaitForCompletion(5000)) << "Job did not complete within timeout"; + EXPECT_EQ(job.GetSuccessCount(), 5u) << "Job should have achieved 5 successes"; + EXPECT_GE(job.GetExecutionCount(), 5u) << "Job should have executed at least 5 times"; +} + +/** + * Test Case 5: Verify no deadlock when pool is under heavy load + * This is critical - ensure self-resubmission doesn't cause deadlocks + */ +TEST_F(WorkerPoolSelfResubmitTest, NoDeadlockUnderLoad) +{ + class LoadTestJob { + private: + Core::WorkerPool::JobType _job; + std::atomic _executionCount; + std::atomic _maxExecutions; + Core::Event _completed; + + public: + LoadTestJob(uint32_t maxExecutions) + : _job(*this) + , _executionCount(0) + , _maxExecutions(maxExecutions) + , _completed(false, true) + { + } + + ~LoadTestJob() + { + _job.Revoke(); + } + + void Start() + { + _job.Submit(); + } + + bool WaitForCompletion(uint32_t timeoutMs) + { + return (_completed.Lock(timeoutMs) == ERROR_NONE); + } + + uint32_t GetExecutionCount() const + { + return _executionCount.load(); + } + + void Dispatch() + { + // Simulate some work + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + + uint32_t currentCount = ++_executionCount; + + if (currentCount < _maxExecutions) { + _job.Submit(); + } else { + _completed.SetEvent(); + } + } + }; + + // Create many jobs that will run concurrently and self-resubmit + // This stresses the pool and tests for deadlock conditions + constexpr uint32_t NUM_JOBS = 10; + constexpr uint32_t EXECUTIONS_PER_JOB = 10; + + std::vector> jobs; + + for (uint32_t i = 0; i < NUM_JOBS; ++i) { + jobs.push_back(std::make_unique(EXECUTIONS_PER_JOB)); + } + + // Start all jobs simultaneously + for (auto& job : jobs) { + job->Start(); + } + + // All jobs should complete without deadlock + for (auto& job : jobs) { + ASSERT_TRUE(job->WaitForCompletion(30000)) + << "Job did not complete - possible deadlock detected"; + EXPECT_EQ(job->GetExecutionCount(), EXECUTIONS_PER_JOB); + } +} + +/** + * Test Case 6: Self-resubmission after revoke should not execute + * Verify that revoked jobs don't continue to resubmit + */ +TEST_F(WorkerPoolSelfResubmitTest, RevokeStopsSelfResubmission) +{ + class RevocableResubmittingJob { + private: + Core::WorkerPool::JobType _job; + std::atomic _executionCount; + std::atomic _shouldStop; + Core::Event _started; + + public: + RevocableResubmittingJob() + : _job(*this) + , _executionCount(0) + , _shouldStop(false) + , _started(false, true) + { + } + + ~RevocableResubmittingJob() + { + _shouldStop = true; + _job.Revoke(); + } + + void Start() + { + _job.Submit(); + } + + void Stop() + { + _shouldStop = true; + _job.Revoke(); + } + + bool WaitForStart(uint32_t timeoutMs) + { + return (_started.Lock(timeoutMs) == ERROR_NONE); + } + + uint32_t GetExecutionCount() const + { + return _executionCount.load(); + } + + void Dispatch() + { + _executionCount++; + _started.SetEvent(); + + // Keep resubmitting unless stopped + if (!_shouldStop) { + // Small delay to allow revoke to happen + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + if (!_shouldStop) { + _job.Submit(); + } + } + } + }; + + RevocableResubmittingJob job; + job.Start(); + + // Wait for job to start + ASSERT_TRUE(job.WaitForStart(1000)) << "Job did not start"; + + // Let it run for a bit + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Stop the job + uint32_t countBeforeStop = job.GetExecutionCount(); + job.Stop(); + + // Wait a bit and verify it stopped resubmitting + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + uint32_t countAfterStop = job.GetExecutionCount(); + + // The count should not increase significantly after stop + EXPECT_LE(countAfterStop - countBeforeStop, 2u) + << "Job continued to execute after revoke"; +} + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From c6fc9ef611fa74133647095420a0baa14fdba948 Mon Sep 17 00:00:00 2001 From: Pierre Wielders Date: Fri, 14 Nov 2025 13:43:14 +0100 Subject: [PATCH 23/64] If the Closed() method, was run, it would set the m_State = 0, which meant that the SocketPort class was (#2004) closed and eligable for destruction. iThe CLosed() is always running on the ResourceMonitor thread. In a race condition, this setting of the m_State = 0 allowed a server to "cleanup" the children (Clients). In the unlucky case that after the setting of the m_State =0, the kernel would preempt the ResourceMonitor thread for later continuation, the Server cleanup thread migh have killed the whole SocketPort object. Effectively any operation following the closed() on the ResourceMonitor threas that was preempted and continues would happen on a dead object! [ReourceMonitorThread] SocketPort::Closed() { ... m_State -> 0 [preempted] [Server Cleanup Thread] Delete all Clients that are Closed() [preemted] [ResourceMonitorThread] m_State &= ~SocketPort::MONITOR; CRASH! ... } This PR prevents this race condition from happening. As this is in the core of Thunder, the biggest risk is the Lock in the destructor. It might casue (if the socket is used incorrectly) to ABBA locks. Request thorough testing ! Also the ASSERT to validate if the CLosed() is indeed only run on the resource monitor thread is a risk! --- Source/core/SocketPort.cpp | 51 ++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/Source/core/SocketPort.cpp b/Source/core/SocketPort.cpp index e921359ea5..1c14aa3996 100644 --- a/Source/core/SocketPort.cpp +++ b/Source/core/SocketPort.cpp @@ -423,6 +423,14 @@ namespace Thunder { { TRACE_L5("Destructor SocketPort <%p>", (this)); + // The Closed(), internal method, run on the ResourceMonitor thread, + // will lock and clear the m_State if applicable. We want to make sure + // that we can take the lock again, to make sure that the Closed() + // released it. All methods, calling the Closed(), will nolonger refer + // to any members or methods in this class, so it is safe to destruct + // after we have aquired the lock! + m_syncAdmin.Lock(); + // Make sure the socket is closed before you destruct. Otherwise // the virtuals might be called, which are destructed at this point !!!! ASSERT((m_Socket == INVALID_SOCKET) || (IsClosed())); @@ -430,6 +438,7 @@ namespace Thunder { if (m_Socket != INVALID_SOCKET) { DestroySocket(m_Socket); } + m_syncAdmin.Unlock(); ::free(m_SendBuffer); } @@ -1065,8 +1074,11 @@ namespace Thunder { } if ((IsForcedClosing() == true) && (Closed() == true)) { + // In the Closed() method, which is only run on the Resouce Monitor Thread, the unregister + // happens. After the Unregister the last bit in the _state is cleared which means that + // there is nolonger a guarantee that the socket is alive anymore. Do not execute *any* + // operation on the socket members anymore!!! result = 0; - m_State &= ~SocketPort::MONITOR; } else { @@ -1092,6 +1104,10 @@ namespace Thunder { #ifdef __WINDOWS__ if ((flagsSet & FD_CLOSE) != 0) { + // In the Closed() method, which is only run on the Resouce Monitor Thread, the unregister + // happens. After the Unregister the last bit in the _state is cleared which means that + // there is nolonger a guarantee that the socket is alive anymore. Do not execute *any* + // operation on the socket members anymore!!! Closed(); } else if (IsListening()) { @@ -1114,6 +1130,10 @@ namespace Thunder { #else if ((flagsSet & POLLHUP) != 0) { TRACE_L3("HUP event received on socket %u", static_cast(m_Socket)); + // In the Closed() method, which is only run on the Resouce Monitor Thread, the unregister + // happens. After the Unregister the last bit in the _state is cleared which means that + // there is nolonger a guarantee that the socket is alive anymore. Do not execute *any* + // operation on the socket members anymore!!! Closed(); } else if ((flagsSet & POLLRDHUP) != 0) { @@ -1286,6 +1306,7 @@ namespace Thunder { bool result = true; ASSERT(m_Socket != INVALID_SOCKET); + ASSERT(Core::Thread::ThreadId() == ResourceMonitor::Instance().Id()); m_syncAdmin.Lock(); @@ -1295,27 +1316,25 @@ namespace Thunder { StateChange(); - m_State &= (~SHUTDOWN); + DestroySocket(m_Socket); + ResourceMonitor::Instance().Unregister(*this); - if (m_State != 0) { - result = false; - } - else { - DestroySocket(m_Socket); - ResourceMonitor::Instance().Unregister(*this); - // Remove socket descriptor for UNIX domain datagram socket. - if ((m_LocalNode.Type() == NodeId::TYPE_DOMAIN) && - ((m_SocketType == SocketPort::LISTEN) || (SocketMode() != SOCK_STREAM)) && - !m_SystemdSocket) { - TRACE_L1("CLOSED: Remove socket descriptor %s", m_LocalNode.HostName().c_str()); + // Remove socket descriptor for UNIX domain datagram socket. + if ((m_LocalNode.Type() == NodeId::TYPE_DOMAIN) && + ((m_SocketType == SocketPort::LISTEN) || (SocketMode() != SOCK_STREAM)) && + !m_SystemdSocket) { + TRACE_L1("CLOSED: Remove socket descriptor %s", m_LocalNode.HostName().c_str()); #ifdef __WINDOWS__ - _unlink(m_LocalNode.HostName().c_str()); + _unlink(m_LocalNode.HostName().c_str()); #else - unlink(m_LocalNode.HostName().c_str()); + unlink(m_LocalNode.HostName().c_str()); #endif - } } + m_State &= (~SHUTDOWN); + + ASSERT (m_State == 0); + m_syncAdmin.Unlock(); return (result); From 552513fd143f00b395ee7f6c87c93f3957a6818b Mon Sep 17 00:00:00 2001 From: MFransen69 <39826971+MFransen69@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:48:25 +0100 Subject: [PATCH 24/64] Remove some more risky changes --- Source/core/SocketPort.cpp | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Source/core/SocketPort.cpp b/Source/core/SocketPort.cpp index 1c14aa3996..84de021a43 100644 --- a/Source/core/SocketPort.cpp +++ b/Source/core/SocketPort.cpp @@ -423,14 +423,6 @@ namespace Thunder { { TRACE_L5("Destructor SocketPort <%p>", (this)); - // The Closed(), internal method, run on the ResourceMonitor thread, - // will lock and clear the m_State if applicable. We want to make sure - // that we can take the lock again, to make sure that the Closed() - // released it. All methods, calling the Closed(), will nolonger refer - // to any members or methods in this class, so it is safe to destruct - // after we have aquired the lock! - m_syncAdmin.Lock(); - // Make sure the socket is closed before you destruct. Otherwise // the virtuals might be called, which are destructed at this point !!!! ASSERT((m_Socket == INVALID_SOCKET) || (IsClosed())); @@ -438,7 +430,6 @@ namespace Thunder { if (m_Socket != INVALID_SOCKET) { DestroySocket(m_Socket); } - m_syncAdmin.Unlock(); ::free(m_SendBuffer); } @@ -1306,7 +1297,6 @@ namespace Thunder { bool result = true; ASSERT(m_Socket != INVALID_SOCKET); - ASSERT(Core::Thread::ThreadId() == ResourceMonitor::Instance().Id()); m_syncAdmin.Lock(); From e8f0f84a7c7dd7abfdf70d9e83259db8336261e2 Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Tue, 18 Nov 2025 12:05:24 +0100 Subject: [PATCH 25/64] Development/patch bluez remove uint24 t (#2007) * cmake: Add ApplyPatch function to apply patches using git or patch utility * cmake: Add option to remove uint24_t from bluez5 headers and apply patch --------- Co-authored-by: Mateusz Daniluk <121170681+VeithMetro@users.noreply.github.com> --- cmake/common/ApplyPatch.cmake | 58 +++++++++++++++++++++++++++++ cmake/common/GetBluez5Headers.cmake | 39 ++++++++++++++++++- 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 cmake/common/ApplyPatch.cmake diff --git a/cmake/common/ApplyPatch.cmake b/cmake/common/ApplyPatch.cmake new file mode 100644 index 0000000000..aa7d1e8150 --- /dev/null +++ b/cmake/common/ApplyPatch.cmake @@ -0,0 +1,58 @@ +function(ApplyPatch SOURCE_DIR PATCH_FILE) + # Find git + find_program(GIT_EXECUTABLE git) + + if(GIT_EXECUTABLE) + message(STATUS "Applying patch using git apply: ${PATCH_FILE}") + + execute_process( + COMMAND "${GIT_EXECUTABLE}" apply "${PATCH_FILE}" + WORKING_DIRECTORY "${SOURCE_DIR}" + RESULT_VARIABLE GIT_APPLY_RESULT + OUTPUT_VARIABLE GIT_APPLY_OUT + ERROR_VARIABLE GIT_APPLY_ERR + ) + + if(GIT_APPLY_RESULT EQUAL 0) + message(STATUS "Patch applied successfully using git.") + return() + else() + message(WARNING + "git apply failed (exit ${GIT_APPLY_RESULT}). " + "Falling back to 'patch -p1'.\n" + "git output:\n${GIT_APPLY_ERR}" + ) + endif() + else() + message(STATUS "git not found — falling back to patch -p1") + endif() + + # Find patch tool + find_program(PATCH_EXECUTABLE patch) + + if(NOT PATCH_EXECUTABLE) + message(FATAL_ERROR + "Neither git nor patch utilities are available. " + "Cannot apply patch: ${PATCH_FILE}" + ) + endif() + + message(STATUS "Applying patch using patch -p1: ${PATCH_FILE}") + + execute_process( + COMMAND "${PATCH_EXECUTABLE}" -p1 -i "${PATCH_FILE}" + WORKING_DIRECTORY "${SOURCE_DIR}" + RESULT_VARIABLE PATCH_RESULT + OUTPUT_VARIABLE PATCH_OUT + ERROR_VARIABLE PATCH_ERR + ) + + if(NOT PATCH_RESULT EQUAL 0) + message(FATAL_ERROR + "Failed to apply patch using patch -p1: ${PATCH_FILE}\n" + "Error:\n${PATCH_ERR}" + ) + endif() + + message(STATUS "Patch applied successfully using patch.") +endfunction() diff --git a/cmake/common/GetBluez5Headers.cmake b/cmake/common/GetBluez5Headers.cmake index 259f242ddc..f1918fcb4a 100644 --- a/cmake/common/GetBluez5Headers.cmake +++ b/cmake/common/GetBluez5Headers.cmake @@ -1,9 +1,10 @@ option(DOWNLOAD_BLUEZ_UTIL_HEADERS "Download bluez5 headers" OFF) +option(BLUEZ_REMOVE_UINT24T "Removes the uint24_t form the bluez5 headers" ON) + set(DOWNLOAD_BLUEZ_UTIL_HEADERS_VERSION "5.78" CACHE STRING "version of the bluez5 headers to download...") set(DOWNLOAD_BLUEZ_UTIL_HEADERS_REPO "https://github.com/bluez/bluez.git" CACHE STRING "Repo where to get the bluez5 headers...") set(BLUEZ_LOCAL_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/core/bluez5") - include(CreateLink) if(DOWNLOAD_BLUEZ_UTIL_HEADERS) @@ -11,13 +12,47 @@ if(DOWNLOAD_BLUEZ_UTIL_HEADERS) include(GetExternalCode) + set(BLUEZ_SRC "${CMAKE_BINARY_DIR}/bluez5-${DOWNLOAD_BLUEZ_UTIL_HEADERS_VERSION}") GetExternalCode( GIT_REPOSITORY "${DOWNLOAD_BLUEZ_UTIL_HEADERS_REPO}" GIT_VERSION "${DOWNLOAD_BLUEZ_UTIL_HEADERS_VERSION}" - SOURCE_DIR "${CMAKE_BINARY_DIR}/bluez5-${DOWNLOAD_BLUEZ_UTIL_HEADERS_VERSION}" + SOURCE_DIR "${BLUEZ_SRC}" ) + if(BLUEZ_REMOVE_UINT24T) + include(ApplyPatch) + + set(BLUEZ_REMOVE_UINT24T_PATCH_CONTENT " +diff --git a/lib/bluetooth.h b/lib/bluetooth.h +index 1286aa7..d403b53 100644 +--- a/lib/bluetooth.h ++++ b/lib/bluetooth.h +@@ -443,10 +443,6 @@ void bt_free(void *ptr); + int bt_error(uint16_t code); + const char *bt_compidtostr(int id); + +-typedef struct { +- uint8_t data[3]; +-} uint24_t; +- + typedef struct { + uint8_t data[16]; + } uint128_t; +-- +2.43.0") + + set(BLUEZ_REMOVE_UINT24T_PATCH_FILE "${CMAKE_BINARY_DIR}/0001-remove-int24.patch") + + file(WRITE "${BLUEZ_REMOVE_UINT24T_PATCH_FILE}" "${BLUEZ_REMOVE_UINT24T_PATCH_CONTENT}") + + if(NOT EXISTS "${BLUEZ_REMOVE_UINT24T_PATCH_FILE}") + message(FATAL_ERROR "Failed to create patch file: ${BLUEZ_REMOVE_UINT24T_PATCH_FILE}") + endif() + + ApplyPatch("${BLUEZ_SRC}" "${BLUEZ_REMOVE_UINT24T_PATCH_FILE}") + endif() + CreateLink( LINK "${BLUEZ_LOCAL_INCLUDE_DIR}" TARGET "${CMAKE_BINARY_DIR}/bluez5-${DOWNLOAD_BLUEZ_UTIL_HEADERS_VERSION}/lib" From a6fd86bcd309d478c3bb4adb492077123632bee6 Mon Sep 17 00:00:00 2001 From: MFransen69 <39826971+MFransen69@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:48:17 +0100 Subject: [PATCH 26/64] Add CustomCode feature (#2005) * [Core] Phase 1 move code from Frame to NUmber and add numeric_limits * [Core] custom code bit functions added * [Core] Custom error code to string handling (draft) * [CustomCode] Handle in PluginServer and condig * [CustomCode] some small fixes * [CusttomCode] Make it work on Linux * [CustomCode] some minor fixes * [CustomCode] small improvement * Bump Actions --------- Co-authored-by: Mateusz Daniluk <121170681+VeithMetro@users.noreply.github.com> --- Source/Thunder/Config.h | 24 ++++ Source/Thunder/PluginHost.cpp | 74 +++++++++++ Source/core/CMakeLists.txt | 1 + Source/core/Frame.h | 193 ++++++--------------------- Source/core/ICustomErrorCode.h | 53 ++++++++ Source/core/JSONRPC.h | 17 ++- Source/core/Number.h | 232 ++++++++++++++++++++++++++++++++- Source/core/Portability.cpp | 109 ++++++++++++++++ Source/core/Portability.h | 25 +++- 9 files changed, 567 insertions(+), 161 deletions(-) create mode 100644 Source/core/ICustomErrorCode.h diff --git a/Source/Thunder/Config.h b/Source/Thunder/Config.h index 2c29c70d8b..771e0a3b1a 100644 --- a/Source/Thunder/Config.h +++ b/Source/Thunder/Config.h @@ -424,6 +424,9 @@ namespace PluginHost { , Observe() #ifdef HIBERNATE_SUPPORT_ENABLED , Hibernate() +#endif +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + , CustomCodeLibrary() #endif { // No IdleTime @@ -468,6 +471,9 @@ namespace PluginHost { Add(_T("observe"), &Observe); #ifdef HIBERNATE_SUPPORT_ENABLED Add(_T("hibernate"), &Hibernate); +#endif +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + Add(_T("customcodelibrary"), &CustomCodeLibrary); #endif } ~JSONConfig() override = default; @@ -516,6 +522,9 @@ namespace PluginHost { Observables Observe; #ifdef HIBERNATE_SUPPORT_ENABLED HibernateConfig Hibernate; +#endif +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + Core::JSON::String CustomCodeLibrary; #endif }; @@ -687,6 +696,9 @@ namespace PluginHost { , _linkerPluginPaths() #ifdef HIBERNATE_SUPPORT_ENABLED , _hibernateLocator() +#endif +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + , _customCodeLibrary() #endif { JSONConfig config; @@ -702,6 +714,9 @@ namespace PluginHost { #endif #ifdef HIBERNATE_SUPPORT_ENABLED _hibernateLocator = config.Hibernate.Locator.Value(); +#endif +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + _customCodeLibrary = config.CustomCodeLibrary.Value(); #endif _volatilePath = Core::Directory::Normalize(config.VolatilePath.Value()); _persistentPath = Core::Directory::Normalize(config.PersistentPath.Value()); @@ -841,6 +856,12 @@ namespace PluginHost { inline const string& HibernateLocator() const { return (_hibernateLocator); } +#endif +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + inline const string& CustomCodeLibrary() const + { + return (_customCodeLibrary); + } #endif inline const string& VolatilePath() const { @@ -1157,6 +1178,9 @@ namespace PluginHost { std::vector _linkerPluginPaths; #ifdef HIBERNATE_SUPPORT_ENABLED string _hibernateLocator; +#endif +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + string _customCodeLibrary; #endif }; } diff --git a/Source/Thunder/PluginHost.cpp b/Source/Thunder/PluginHost.cpp index 9647c90fbb..483374b572 100644 --- a/Source/Thunder/PluginHost.cpp +++ b/Source/Thunder/PluginHost.cpp @@ -136,6 +136,66 @@ POP_WARNING() Core::AdapterObserver _observer; }; +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + + class CustomCodeLibrary { + public: + + CustomCodeLibrary(const CustomCodeLibrary&) = delete; + CustomCodeLibrary& operator=(const CustomCodeLibrary&) = delete; + CustomCodeLibrary(CustomCodeLibrary&&) = delete; + CustomCodeLibrary& operator=(CustomCodeLibrary&&) = delete; + + CustomCodeLibrary() + : _customCodeLibrary() + , _customCodeToStringHandler(nullptr) + { + } + ~CustomCodeLibrary() + { + ASSERT(_customCodeToStringHandler == nullptr); + ASSERT(_customCodeLibrary.IsLoaded() == false); + } + + void Load(const string& libraryPath) + { + ASSERT(_customCodeToStringHandler == nullptr); + ASSERT(_customCodeLibrary.IsLoaded() == false); + + _customCodeLibrary = Core::Library(libraryPath.c_str()); + if (_customCodeLibrary.IsLoaded() == true) { + _customCodeToStringHandler = reinterpret_cast(_customCodeLibrary.LoadFunction(CustomCodeToStingName)); + if (_customCodeToStringHandler != nullptr) { + Core::SetCustomCodeToStringHandler(_customCodeToStringHandler); + } else { + SYSLOG(Logging::Error, (_T("Could not find CustomCodeToSting function in Custom Error Code library"))); + _customCodeLibrary = Core::Library(); + } + } else { + SYSLOG(Logging::Error, (_T("Could not load Custom Error Code library"))); + } + } + + void Release() + { + if (_customCodeToStringHandler != nullptr) { + Core::SetCustomCodeToStringHandler(nullptr); + _customCodeToStringHandler = nullptr; + } + if (_customCodeLibrary.IsLoaded() == true) { + _customCodeLibrary = Core::Library(); + } + } + + private: + static constexpr const TCHAR* CustomCodeToStingName{ _T("CustomCodeToSting") }; + + Core::Library _customCodeLibrary; + Core::CustomCodeToStringHandler _customCodeToStringHandler; + }; + +#endif + class ExitHandler : public Core::Thread { private: ExitHandler() = delete; @@ -596,6 +656,10 @@ POP_WARNING() } } +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + CustomCodeLibrary customcodelibraryhandler; +#endif + if (_config != nullptr) { if (_config->Process().IsSet() == true) { @@ -646,6 +710,12 @@ POP_WARNING() postMortemPath.CreatePath(); } +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + if (_config->CustomCodeLibrary().empty() == false) { + customcodelibraryhandler.Load(_config->CustomCodeLibrary()); + } +#endif + MessagingInitialization(options.configFile, options.flushMode); SYSLOG(Logging::Startup, (_T(EXPAND_AND_QUOTE(APPLICATION_NAME)))); @@ -1031,6 +1101,10 @@ POP_WARNING() fprintf(stderr, EXPAND_AND_QUOTE(APPLICATION_NAME) " shutting down due to a 'Q' press in the terminal. Regular shutdown\n"); fflush(stderr); } + +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + customcodelibraryhandler.Release(); +#endif ExitHandler::Destruct(); std::set_terminate(nullptr); diff --git a/Source/core/CMakeLists.txt b/Source/core/CMakeLists.txt index f86f2d8891..0f784cdbc2 100644 --- a/Source/core/CMakeLists.txt +++ b/Source/core/CMakeLists.txt @@ -150,6 +150,7 @@ set(PUBLIC_HEADERS CallsignTLS.h TokenizedStringList.h MessageStore.h + ICustomErrorCode.h ${CMAKE_CURRENT_BINARY_DIR}/generated/core/Version.h ) diff --git a/Source/core/Frame.h b/Source/core/Frame.h index 022dbd8ef3..cce80a5265 100644 --- a/Source/core/Frame.h +++ b/Source/core/Frame.h @@ -2,7 +2,7 @@ * If not stated otherwise in this file or this component's LICENSE file the * following copyright and licenses apply: * - * Copyright 2020 Metrological + * Copyright 2020 Metrological * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,128 +27,17 @@ namespace Core { namespace Frame { - class SInt24 { - public: - static constexpr uint8_t SizeOf = 3; - static constexpr uint32_t Max = 0x7FFFFF; - static constexpr int32_t Min = 0xFF800000; - - using InternalType = int32_t; - - SInt24() - : _value(0) - { - } - SInt24(const int32_t value) - : _value(value | (value & 0x800000? 0xFF000000 : 0)) - { - ASSERT(((static_cast(value) >> 24) == 0) || ((static_cast(value) >> 24) == 0xFF)); - } - SInt24(const SInt24&) = default; - SInt24(SInt24&&) = default; - ~SInt24() = default; - - public: - SInt24& operator=(const SInt24& value) = default; - SInt24& operator=(SInt24&& value) = default; - SInt24& operator=(const int32_t value) - { - ASSERT(((static_cast(value) >> 24) == 0) || ((static_cast(value) >> 24) == 0xFF)); - _value = (value | (value & 0x800000? 0xFF000000 : 0)); - return (*this); - } - operator int32_t() const { - return (_value); - } - - private: - int32_t _value; - }; - - class UInt24 { - public: - static constexpr uint8_t SizeOf = 3; - static constexpr uint32_t Max = 0xFFFFFF; - static constexpr uint32_t Min = 0; - - using InternalType = uint32_t; - - UInt24() - : _value(0) - { - } - UInt24(const UInt24& value) = default; - UInt24(UInt24&& value) = default; - UInt24(const uint32_t value) - : _value(value) - { - ASSERT((value >> 24) == 0); - } - ~UInt24() = default; - - public: - UInt24& operator=(const UInt24& copy) = default; - UInt24& operator=(UInt24&& move) = default; - UInt24& operator=(const uint32_t value) - { - ASSERT((value >> 24) == 0); - _value = value; - return (*this); - } - operator uint32_t() const { - return (_value); - } - - private: - uint32_t _value; - }; - - template ::value, int>::type = 0> - static constexpr uint8_t RealSize() { - return (sizeof(T)); - } - template ::type = 0> - static constexpr uint8_t RealSize() { - return (T::SizeOf); - } - - template ::value, int>::type = 0> - static constexpr T Max() { - return (std::numeric_limits::max()); - } - - template ::type = 0> - static constexpr typename T::InternalType Max() - { - return (T::Max); - } - - template ::value, int>::type = 0> - static constexpr T Min() - { - return (std::numeric_limits::min()); - } - - template ::type = 0> - static constexpr typename T::InternalType Min() - { - return (T::Min); - } - template NEW_TYPE buffer_length_cast(const ORIGINAL_TYPE& input) { - ASSERT(input <= Frame::Max()); - ASSERT(input >= Frame::Min()); - // in release in case the length does not fit we do not want to send data at all, then it is more obvious to the recipient something is wrong instead of only sending partial data - ORIGINAL_TYPE length = (input <= Frame::Max() ? input : 0); + NEW_TYPE length = 0; - PUSH_WARNING(DISABLE_WARNING_CONVERSION_POSSIBLE_LOSS_OF_DATA) - - return (static_cast(length)); + if (Core::check_and_cast(input, length) == false) { + length = 0; + } - POP_WARNING() + return (length); } } @@ -347,7 +236,7 @@ namespace Core { result = _container->GetBuffer(_offset, maxLength, buffer); _offset += result; - return (static_cast(result - Frame::RealSize())); + return (static_cast(result - Core::RealSize())); } void Copy(const SIZE_CONTEXT length, uint8_t buffer[]) const { @@ -654,9 +543,9 @@ namespace Core { template uint32_t SetBuffer(const SIZE_CONTEXT offset, const TYPENAME length, const uint8_t buffer[]) { - SIZE_CONTEXT requiredLength(static_cast(Frame::RealSize() + length)); + SIZE_CONTEXT requiredLength(static_cast(Core::RealSize() + length)); - static_assert(Frame::RealSize() <= sizeof(SIZE_CONTEXT), "Make sure the logic can handle the size (enlarge the SIZE_CONTEXT)"); + static_assert(Core::RealSize() <= sizeof(SIZE_CONTEXT), "Make sure the logic can handle the size (enlarge the SIZE_CONTEXT)"); if ((offset + requiredLength) >= _size) { Size(offset + requiredLength); @@ -666,7 +555,7 @@ namespace Core { if (length != 0) { ASSERT(buffer != nullptr); - ::memcpy(&(_data[offset + Frame::RealSize()]), buffer, length); + ::memcpy(&(_data[offset + Core::RealSize()]), buffer, length); } return (requiredLength); @@ -714,42 +603,42 @@ namespace Core { { TYPENAME textLength; - ASSERT((offset + Frame::RealSize()) <= _size); - static_assert(Frame::RealSize() <= sizeof(SIZE_CONTEXT), "Make sure the logic can handle the size (enlarge the SIZE_CONTEXT)"); + ASSERT((offset + Core::RealSize()) <= _size); + static_assert(Core::RealSize() <= sizeof(SIZE_CONTEXT), "Make sure the logic can handle the size (enlarge the SIZE_CONTEXT)"); GetNumber(offset, textLength); - ASSERT((textLength + offset + Frame::RealSize()) <= _size); + ASSERT((textLength + offset + Core::RealSize()) <= _size); - if ((textLength + offset + Frame::RealSize()) > _size) { - textLength = (_size - (offset + Frame::RealSize())); + if ((textLength + offset + Core::RealSize()) > _size) { + textLength = (_size - (offset + Core::RealSize())); } - memcpy(buffer, &(_data[offset + Frame::RealSize()]), (textLength > length ? length : textLength)); + memcpy(buffer, &(_data[offset + Core::RealSize()]), (textLength > length ? length : textLength)); - return (static_cast(Frame::RealSize() + textLength)); + return (static_cast(Core::RealSize() + textLength)); } template SIZE_CONTEXT GetText(const SIZE_CONTEXT offset, string& result) const { TYPENAME textLength; - ASSERT((offset + Frame::RealSize()) <= _size); - static_assert(Frame::RealSize() <= sizeof(SIZE_CONTEXT), "Make sure the logic can handle the size (enlarge the SIZE_CONTEXT)"); + ASSERT((offset + Core::RealSize()) <= _size); + static_assert(Core::RealSize() <= sizeof(SIZE_CONTEXT), "Make sure the logic can handle the size (enlarge the SIZE_CONTEXT)"); GetNumber(offset, textLength); - ASSERT((textLength + offset + Frame::RealSize()) <= _size); + ASSERT((textLength + offset + Core::RealSize()) <= _size); - if (textLength + offset + Frame::RealSize() > _size) { - textLength = static_cast(_size - (offset + Frame::RealSize())); + if (textLength + offset + Core::RealSize() > _size) { + textLength = static_cast(_size - (offset + Core::RealSize())); } - std::string convertedText(reinterpret_cast(&(_data[offset + Frame::RealSize()])), textLength); + std::string convertedText(reinterpret_cast(&(_data[offset + Core::RealSize()])), textLength); result = Core::ToString(convertedText); - return (static_cast(Frame::RealSize() + textLength)); + return (static_cast(Core::RealSize() + textLength)); } SIZE_CONTEXT GetNullTerminatedText(const SIZE_CONTEXT offset, string& result) const @@ -785,7 +674,7 @@ namespace Core { uint8_t index = 0; TYPENAME value = number; - static_assert(Frame::RealSize() <= ((sizeof(bytes) * 7) / 8), "Make sure the size is not too large (not much bigger than uint64_t)"); + static_assert(Core::RealSize() <= ((sizeof(bytes) * 7) / 8), "Make sure the size is not too large (not much bigger than uint64_t)"); do { bytes[index++] = ( static_cast(value % 128) | 0x80 ); @@ -796,7 +685,7 @@ namespace Core { bytes[index - 1] ^= 0x80; if ((offset + index) >= _size) { - Size(offset + Frame::RealSize()); + Size(offset + Core::RealSize()); } if ( (BIG_ENDIAN_ORDERING == true) && (index > 1) ) { @@ -847,7 +736,7 @@ namespace Core { index++; } - ASSERT(((index * 7) / 8) <= Frame::RealSize()); + ASSERT(((index * 7) / 8) <= Core::RealSize()); ++index; @@ -868,13 +757,13 @@ namespace Core { template inline SIZE_CONTEXT SetNumber(const SIZE_CONTEXT offset, const TYPENAME number) { - return (SetNumber(offset, number, TemplateIntToType() == 1>())); + return (SetNumber(offset, number, TemplateIntToType() == 1>())); } template inline SIZE_CONTEXT GetNumber(const SIZE_CONTEXT offset, TYPENAME& number) const { - return (GetNumber(offset, number, TemplateIntToType() == 1>())); + return (GetNumber(offset, number, TemplateIntToType() == 1>())); } #ifdef __DEBUG__ @@ -912,9 +801,9 @@ namespace Core { template void SetNumberLittleEndianPlatform(const SIZE_CONTEXT offset, const TYPENAME number) { const uint8_t* source = reinterpret_cast(&number); - uint8_t* destination = &(_data[offset + Frame::RealSize() - 1]); + uint8_t* destination = &(_data[offset + Core::RealSize() - 1]); - for (uint8_t index = 0; index < Frame::RealSize(); index++) { + for (uint8_t index = 0; index < Core::RealSize(); index++) { *destination-- = *source++; } } @@ -924,7 +813,7 @@ namespace Core { const uint8_t* source = reinterpret_cast(&number); uint8_t* destination = &(_data[offset]); - for (uint8_t index = 0; index < Frame::RealSize(); index++) { + for (uint8_t index = 0; index < Core::RealSize(); index++) { *destination++ = *source++; } } @@ -933,8 +822,8 @@ namespace Core { template SIZE_CONTEXT SetNumber(const SIZE_CONTEXT offset, const TYPENAME number, const TemplateIntToType&) { - if ((offset + Frame::RealSize()) >= _size) { - Size(offset + Frame::RealSize()); + if ((offset + Core::RealSize()) >= _size) { + Size(offset + Core::RealSize()); } if (BIG_ENDIAN_ORDERING == true) { @@ -952,14 +841,14 @@ namespace Core { #endif } - return (Frame::RealSize()); + return (Core::RealSize()); } template SIZE_CONTEXT GetNumber(const SIZE_CONTEXT offset, TYPENAME& number, const TemplateIntToType&) const { // Only on package level allowed to pass the boundaries!!! - ASSERT((offset + Frame::RealSize()) <= _size); + ASSERT((offset + Core::RealSize()) <= _size); number = static_cast(_data[offset]); @@ -971,9 +860,9 @@ namespace Core { { TYPENAME result = static_cast(0); const uint8_t* source = &(_data[offset]); - uint8_t* destination = &(reinterpret_cast(&result)[Frame::RealSize() - 1]); + uint8_t* destination = &(reinterpret_cast(&result)[Core::RealSize() - 1]); - for (uint8_t index = 0; index < Frame::RealSize(); index++) { + for (uint8_t index = 0; index < Core::RealSize(); index++) { *destination-- = *source++; } @@ -989,7 +878,7 @@ namespace Core { const uint8_t* source = &(_data[offset]); uint8_t* destination = reinterpret_cast(&result); - for (uint8_t index = 0; index < Frame::RealSize(); index++) { + for (uint8_t index = 0; index < Core::RealSize(); index++) { *destination++ = *source++; } @@ -999,7 +888,7 @@ namespace Core { template inline SIZE_CONTEXT GetNumber(const SIZE_CONTEXT offset, TYPENAME& value, const TemplateIntToType&) const { - if ((offset + Frame::RealSize()) > _size) { + if ((offset + Core::RealSize()) > _size) { value = static_cast(0); } else if (BIG_ENDIAN_ORDERING == true) { @@ -1017,7 +906,7 @@ namespace Core { #endif } - return (Frame::RealSize()); + return (Core::RealSize()); } private: diff --git a/Source/core/ICustomErrorCode.h b/Source/core/ICustomErrorCode.h new file mode 100644 index 0000000000..a6f6376f9a --- /dev/null +++ b/Source/core/ICustomErrorCode.h @@ -0,0 +1,53 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + This file contains the interface that a library can implement in case the "custom error codes" feature is used in Thunder and code to string comversion is desired +*/ + +#pragma once + +#undef EXTERNAL + +#if defined(WIN32) || defined(_WINDOWS) || defined(__CYGWIN__) || defined(_WIN64) +#define EXTERNAL __declspec(dllexport) +#else +#define EXTERNAL __attribute__((visibility("default"))) +#endif + +#ifndef TCHAR +#ifdef _UNICODE +#define TCHAR wchar_t +#else +#define TCHAR char +#endif +#endif + +#ifdef __cplusplus +#include +extern "C" { +#else +#include +#endif + +EXTERNAL const TCHAR* CustomCodeToSting(const int32_t code); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/Source/core/JSONRPC.h b/Source/core/JSONRPC.h index 2d51d53dfb..5b89b6b055 100644 --- a/Source/core/JSONRPC.h +++ b/Source/core/JSONRPC.h @@ -185,14 +185,23 @@ namespace Core { Text = _T("The operation is not supported."); break; default: - if ((frameworkError & 0x80000000) == 0) { - Code = ApplicationErrorCodeBase - static_cast(frameworkError); - } else { + if ((frameworkError & COM_ERROR) != 0) { Code = ApplicationErrorCodeBase - static_cast(frameworkError & 0x7FFFFFFF) - 500; + } else { +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + int32_t customcode = IsCustomCode(frameworkError); + if (customcode != 0) { + Code = (customcode == std::numeric_limits::min() ? 0 : customcode); + } else { +#endif + Code = ApplicationErrorCodeBase - static_cast(frameworkError); +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + } +#endif } if (Text.IsSet() == false) { - Text = Core::ErrorToString(frameworkError); + Text = Core::ErrorToStringExtended(frameworkError); } break; } diff --git a/Source/core/Number.h b/Source/core/Number.h index b98f75a1c0..3d9b1ced74 100644 --- a/Source/core/Number.h +++ b/Source/core/Number.h @@ -25,6 +25,8 @@ #include "TextFragment.h" #include "TypeTraits.h" +#include + namespace Thunder { namespace Core { extern "C" { @@ -1049,15 +1051,243 @@ namespace Core { uint8_t _size; }; -} + class SInt24 { + public: + static constexpr uint8_t SizeOf = 3; + static constexpr uint32_t Max = 0x7FFFFF; + static constexpr int32_t Min = 0xFF800000; + + using InternalType = int32_t; + + SInt24() + : _value(0) + { + } + SInt24(const int32_t value) + : _value(value | (value & 0x800000 ? 0xFF000000 : 0)) + { + ASSERT(((static_cast(value) >> 24) == 0) || ((static_cast(value) >> 24) == 0xFF)); + } + SInt24(const SInt24&) = default; + SInt24(SInt24&&) = default; + ~SInt24() = default; + + public: + SInt24& operator=(const SInt24& value) = default; + SInt24& operator=(SInt24&& value) = default; + SInt24& operator=(const int32_t value) + { + ASSERT(((static_cast(value) >> 24) == 0) || ((static_cast(value) >> 24) == 0xFF)); + _value = (value | (value & 0x800000 ? 0xFF000000 : 0)); + return (*this); + } + operator int32_t() const + { + return (_value); + } + + int32_t AsSInt24() const + { + return (_value & 0xFFFFFF); + } + + private: + int32_t _value; + }; + + class UInt24 { + public: + static constexpr uint8_t SizeOf = 3; + static constexpr uint32_t Max = 0xFFFFFF; + static constexpr uint32_t Min = 0; + + using InternalType = uint32_t; + + UInt24() + : _value(0) + { + } + UInt24(const UInt24& value) = default; + UInt24(UInt24&& value) = default; + UInt24(const uint32_t value) + : _value(value) + { + ASSERT((value >> 24) == 0); + } + ~UInt24() = default; + + public: + UInt24& operator=(const UInt24& copy) = default; + UInt24& operator=(UInt24&& move) = default; + UInt24& operator=(const uint32_t value) + { + ASSERT((value >> 24) == 0); + _value = value; + return (*this); + } + operator uint32_t() const + { + return (_value); + } + + uint32_t AsUInt24() const // just to be consistent with SInt24 + { + return (_value & 0xFFFFFF); // in debug assert should already have fired on assignment + } + + private: + uint32_t _value; + }; + + template ::value, int>::type = 0> + static constexpr uint8_t RealSize() + { + return (sizeof(T)); + } + template ::type = 0> + static constexpr uint8_t RealSize() + { + return (T::SizeOf); + } + + template + bool check_and_cast(const ORIGINAL_TYPE& input, NEW_TYPE& output) + { + ASSERT(input <= std::numeric_limits::max()); + ASSERT(input >= std::numeric_limits::min()); + + bool success = false; + + if ((input <= std::numeric_limits::max()) && (input >= std::numeric_limits::min())) { + + success = true; + + PUSH_WARNING(DISABLE_WARNING_CONVERSION_POSSIBLE_LOSS_OF_DATA) + + output = static_cast(input); + + POP_WARNING() + } + + return success; + + } + } // namespace Core +} // namespace Thunder + +namespace std { // seems to be allowed/mandatory to specialize inside the std namespace + +template <> +class numeric_limits { + +public: + static constexpr bool is_specialized = true; + + static constexpr Thunder::Core::SInt24::InternalType min() noexcept { return Thunder::Core::SInt24::Min; } + static constexpr Thunder::Core::SInt24::InternalType max() noexcept { return Thunder::Core::SInt24::Max; } + static constexpr Thunder::Core::SInt24::InternalType lowest() noexcept { return min(); } + + static constexpr int digits = 23; + static constexpr int digits10 = 6; + static constexpr int max_digits10 = 0; + + static constexpr bool is_signed = true; + static constexpr bool is_integer = true; + static constexpr bool is_exact = true; + static constexpr int radix = 2; + + static constexpr Thunder::Core::SInt24::InternalType epsilon() noexcept { return 0; } + static constexpr Thunder::Core::SInt24::InternalType round_error() noexcept { return 0; } + + static constexpr int min_exponent = 0; + static constexpr int min_exponent10 = 0; + static constexpr int max_exponent = 0; + static constexpr int max_exponent10 = 0; + + static constexpr bool has_infinity = false; + static constexpr bool has_quiet_NaN = false; + static constexpr bool has_signaling_NaN = false; + static constexpr std::float_denorm_style has_denorm = std::denorm_absent; + static constexpr bool has_denorm_loss = false; + + static constexpr Thunder::Core::SInt24::InternalType infinity() noexcept { return static_cast(0); } + static constexpr Thunder::Core::SInt24::InternalType quiet_NaN() noexcept { return static_cast(0); } + static constexpr Thunder::Core::SInt24::InternalType signaling_NaN() noexcept { return static_cast(0); } + static constexpr Thunder::Core::SInt24::InternalType denorm_min() noexcept { return static_cast(0); } + + static constexpr bool is_iec559 = false; + static constexpr bool is_bounded = true; + static constexpr bool is_modulo = false; + + static constexpr bool traps = true; + static constexpr bool tinyness_before = false; + static constexpr std::float_round_style round_style = std::round_toward_zero; +}; + +template <> +class numeric_limits { + +public: + static constexpr bool is_specialized = true; + + static constexpr Thunder::Core::UInt24::InternalType min() noexcept { return Thunder::Core::UInt24::Min; } + static constexpr Thunder::Core::UInt24::InternalType max() noexcept { return Thunder::Core::UInt24::Max; } + static constexpr Thunder::Core::UInt24::InternalType lowest() noexcept { return min(); } + + static constexpr int digits = 24; + static constexpr int digits10 = 7; + static constexpr int max_digits10 = 0; + + static constexpr bool is_signed = false; + static constexpr bool is_integer = true; + static constexpr bool is_exact = true; + static constexpr int radix = 2; + + static constexpr Thunder::Core::UInt24::InternalType epsilon() noexcept { return 0; } + static constexpr Thunder::Core::UInt24::InternalType round_error() noexcept { return 0; } + + static constexpr int min_exponent = 0; + static constexpr int min_exponent10 = 0; + static constexpr int max_exponent = 0; + static constexpr int max_exponent10 = 0; + + static constexpr bool has_infinity = false; + static constexpr bool has_quiet_NaN = false; + static constexpr bool has_signaling_NaN = false; + static constexpr std::float_denorm_style has_denorm = std::denorm_absent; + static constexpr bool has_denorm_loss = false; + + static constexpr Thunder::Core::UInt24::InternalType infinity() noexcept { return static_cast(0); } + static constexpr Thunder::Core::UInt24::InternalType quiet_NaN() noexcept { return static_cast(0); } + static constexpr Thunder::Core::UInt24::InternalType signaling_NaN() noexcept { return static_cast(0); } + static constexpr Thunder::Core::UInt24::InternalType denorm_min() noexcept { return static_cast(0); } + + static constexpr bool is_iec559 = false; + static constexpr bool is_bounded = true; + static constexpr bool is_modulo = true; + + static constexpr bool traps = true; + static constexpr bool tinyness_before = false; + static constexpr std::float_round_style round_style = std::round_toward_zero; +}; + +} // namespace std + +using uint24_t = Thunder::Core::UInt24; +using int24_t = Thunder::Core::SInt24; template NEW_TYPE int_cast(const ORIGINAL_TYPE& input) { ASSERT(input <= std::numeric_limits::max()); ASSERT(input >= std::numeric_limits::min()); + + PUSH_WARNING(DISABLE_WARNING_CONVERSION_POSSIBLE_LOSS_OF_DATA) + return (static_cast(input)); + + POP_WARNING() } #endif // __NUMBER_H diff --git a/Source/core/Portability.cpp b/Source/core/Portability.cpp index fad90a909c..482a7812ab 100644 --- a/Source/core/Portability.cpp +++ b/Source/core/Portability.cpp @@ -25,6 +25,7 @@ #include "Sync.h" #include "SystemInfo.h" #include "Serialization.h" +#include "Number.h" #ifdef __LINUX__ #include @@ -457,5 +458,113 @@ namespace Core { return (lastIndex < (index - 1) ? TextFragment(result, lastIndex + 1, result.Length() - (lastIndex + 1)) : result); } + +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + +namespace { + + static CustomCodeToStringHandler customerrorcodehandler = nullptr; + + const TCHAR* HandleCustomErrorCodeToString(const int32_t customcode) + { + const TCHAR* text = nullptr; + + if (customcode == std::numeric_limits::min()) { + text = _T("Invalid Custom ErrorCode set"); + } else if ((customerrorcodehandler == nullptr) || ((text = customerrorcodehandler(customcode)) == nullptr)) { + text = _T("Undefined Custom Error"); + } + + return text; + } + + string HandleCustomErrorCodeToStringExtended(const int32_t customcode) + { + string result; + + const TCHAR* text = nullptr; + if (customcode == std::numeric_limits::min()) { + result = _T("Invalid Custom ErrorCode set"); + } else if ((customerrorcodehandler == nullptr) || ((text = customerrorcodehandler(customcode)) == nullptr)) { + result = _T("Undefined Custom Error: ") + Core::NumberType(customcode).Text(); + } else { + result = text; + } + + return result; + } + +} + void SetCustomCodeToStringHandler(CustomCodeToStringHandler handler) { + customerrorcodehandler = handler; + } + + hresult CustomCode(const int32_t customCode) { + + static_assert(CUSTOM_ERROR == 0x1000000, "Code below assumes 25th bit used for CUSTOM_ERROR"); + + hresult result = Core::ERROR_NONE; + + if (customCode != 0) { + int24_t code; + if (Core::check_and_cast(customCode, code) == true) { + result = static_cast(code.AsSInt24()); + } else { + result = 0; // set invalid customCode result; + } + result |= CUSTOM_ERROR; // set custom code bit + } + + return result; + } + + int32_t IsCustomCode(const Core::hresult code) { + static_assert(CUSTOM_ERROR == 0x1000000, "Code below assumes 25th bit used for CUSTOM_ERROR"); + + int32_t result = 0; + + if ((code & CUSTOM_ERROR) != 0) { + int24_t custumcode(code & 0xFFFFFF); // remove custom error bit before assigning + result = custumcode; + if (result == 0) { + result = std::numeric_limits::min(); + } + } + + return result; + } + +#endif + + const TCHAR* ErrorToString(const Core::hresult code) + { +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + int32_t customcode = IsCustomCode(code); + + if (customcode != 0) { + return HandleCustomErrorCodeToString(customcode); + } +#endif + return _bogus_ErrorToString<>(code & (~COM_ERROR)); + } + + string ErrorToStringExtended(const Core::hresult code) + { +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + int32_t customcode = IsCustomCode(code); + + if (customcode != 0) { + return HandleCustomErrorCodeToStringExtended(customcode); + } +#endif + string result = _bogus_ErrorToString<>(code & (~COM_ERROR)); + + if (result.empty() == true) { + result = _T("Undefined Thunder error code: ") + Core::NumberType(code).Text(); + } + return result; + } + + } // namespace Core } // namespace Thunder diff --git a/Source/core/Portability.h b/Source/core/Portability.h index efc1ef2fa1..61680cf032 100644 --- a/Source/core/Portability.h +++ b/Source/core/Portability.h @@ -926,6 +926,16 @@ namespace Core { } #define COM_ERROR (0x80000000) + #define CUSTOM_ERROR (0x1000000) + +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + + // transform a custum code into an hresult + EXTERNAL Core::hresult CustomCode(const int32_t customCode); + // query if the hresult is a custom code and if so extract the value, returns 0 if the hresult was not a custom code + EXTERNAL int32_t IsCustomCode(const Core::hresult code); + +#endif #define ERROR_CODES \ ERROR_CODE(ERROR_NONE, 0) \ @@ -1022,10 +1032,17 @@ namespace Core { return (code == 0? _Err2Str<0u>() : _Err2Str<~0u>()); }; - inline const TCHAR* ErrorToString(Core::hresult code) - { - return _bogus_ErrorToString<>(code & (~COM_ERROR)); - } + EXTERNAL const TCHAR* ErrorToString(const Core::hresult code); + EXTERNAL string ErrorToStringExtended(const Core::hresult code); + +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + + using CustomCodeToStringHandler = const TCHAR* (*)(const int32_t code); + + // can only set one, not multithreaded safe + EXTERNAL void SetCustomCodeToStringHandler(CustomCodeToStringHandler handler); + +#endif #undef ERROR_CODE From 97bc3ab32b3f519e0e11250dba1e7441784e0bd3 Mon Sep 17 00:00:00 2001 From: MFransen69 <39826971+MFransen69@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:44:36 +0100 Subject: [PATCH 27/64] Development/custom codes2 (#2008) * [Core] Add uint24_t (poor mans solution) * [Core] uint24_t and int24_t as full supported types * [Core] add new files to CMakeList.txt --- Source/core/AccessControl.h | 1 + Source/core/CMakeLists.txt | 3 + Source/core/Errors.cpp | 135 ++++++++++ Source/core/Errors.h | 152 ++++++++++++ Source/core/ExtraNumberDefinitions.h | 270 ++++++++++++++++++++ Source/core/ICustomErrorCode.h | 3 + Source/core/JSONRPC.h | 6 +- Source/core/NetworkInfo.h | 1 - Source/core/Number.h | 210 ++-------------- Source/core/Portability.cpp | 108 +------- Source/core/Portability.h | 358 +++++++++------------------ Source/core/ProcessInfo.cpp | 1 + Source/core/Proxy.h | 1 + Source/core/Sync.cpp | 1 + Source/core/core.h | 3 + Source/core/core.vcxproj | 6 +- 16 files changed, 719 insertions(+), 540 deletions(-) create mode 100644 Source/core/Errors.cpp create mode 100644 Source/core/Errors.h create mode 100644 Source/core/ExtraNumberDefinitions.h diff --git a/Source/core/AccessControl.h b/Source/core/AccessControl.h index 71d40acdf0..2651adab30 100644 --- a/Source/core/AccessControl.h +++ b/Source/core/AccessControl.h @@ -22,6 +22,7 @@ #include "Module.h" #include "NodeId.h" #include "Portability.h" +#include "Errors.h" #ifdef __POSIX__ #include diff --git a/Source/core/CMakeLists.txt b/Source/core/CMakeLists.txt index 0f784cdbc2..371264a65f 100644 --- a/Source/core/CMakeLists.txt +++ b/Source/core/CMakeLists.txt @@ -58,6 +58,7 @@ add_library(${TARGET} WorkerPool.cpp XGetopt.cpp ResourceMonitor.cpp + Errors.cpp ) #TODO: Remove all non public headers from this list, @@ -151,6 +152,8 @@ set(PUBLIC_HEADERS TokenizedStringList.h MessageStore.h ICustomErrorCode.h + Errors.h + ExtraNumberDefinitions.h ${CMAKE_CURRENT_BINARY_DIR}/generated/core/Version.h ) diff --git a/Source/core/Errors.cpp b/Source/core/Errors.cpp new file mode 100644 index 0000000000..8731bb6bd9 --- /dev/null +++ b/Source/core/Errors.cpp @@ -0,0 +1,135 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Module.h" +#include "Errors.h" +#include "Number.h" + +namespace Thunder { + +namespace Core { + +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + +namespace { + + static CustomCodeToStringHandler customerrorcodehandler = nullptr; + + const TCHAR* HandleCustomErrorCodeToString(const int32_t customcode) + { + const TCHAR* text = nullptr; + + if (customcode == std::numeric_limits::max()) { + text = _T("Invalid Custom ErrorCode set"); + } else if ((customerrorcodehandler == nullptr) || ((text = customerrorcodehandler(customcode)) == nullptr)) { + text = _T("Undefined Custom Error"); + } + + return text; + } + + string HandleCustomErrorCodeToStringExtended(const int32_t customcode) + { + string result; + + const TCHAR* text = nullptr; + if (customcode == std::numeric_limits::max()) { + result = _T("Invalid Custom ErrorCode set"); + } else if ((customerrorcodehandler == nullptr) || ((text = customerrorcodehandler(customcode)) == nullptr)) { + result = _T("Undefined Custom Error: ") + Core::NumberType(customcode).Text(); + } else { + result = text; + } + + return result; + } + +} + void SetCustomCodeToStringHandler(CustomCodeToStringHandler handler) { + customerrorcodehandler = handler; + } + + hresult CustomCode(const int24_t customCode) + { + static_assert(CUSTOM_ERROR == 0x1000000, "Code below assumes 25th bit used for CUSTOM_ERROR"); + + hresult result = Core::ERROR_NONE; + + if (customCode != 0) { + if (Core::Overflowed(customCode) == false) { + result = static_cast(customCode.AsSInt24()); + } else { + result = 0; // set invalid customCode result; + } + result |= CUSTOM_ERROR; // set custom code bit + } + + return result; + } + + int24_t IsCustomCode(const Core::hresult code) + { + static_assert(CUSTOM_ERROR == 0x1000000, "Code below assumes 25th bit used for CUSTOM_ERROR"); + + int24_t result = 0; + + if ((code & CUSTOM_ERROR) != 0) { + result = static_cast(code & 0xFFFFFF); // remove custom error bit before assigning + if (result == 0) { + result = std::numeric_limits::max(); // this will assert in debug, but if that happens one managed to fill an hresult with an overflowed core result, that should have asserted already when using CustomCode to fill it, os this is probably caused by either manually incorrectly filling the hresult or memory corruption + } + } + + return result; + } + +#endif + + const TCHAR* ErrorToString(const Core::hresult code) + { +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + int24_t customcode = IsCustomCode(code); + + if (customcode != 0) { + return HandleCustomErrorCodeToString(customcode); + } +#endif + return _bogus_ErrorToString<>(code & (~COM_ERROR)); + } + + string ErrorToStringExtended(const Core::hresult code) + { +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + int24_t customcode = IsCustomCode(code); + + if (customcode != 0) { + return HandleCustomErrorCodeToStringExtended(customcode); + } +#endif + string result = _bogus_ErrorToString<>(code & (~COM_ERROR)); + + if (result.empty() == true) { + result = _T("Undefined Thunder error code: ") + Core::NumberType(code).Text(); + } + return result; + } + + +} // namespace Core +} // namespace Thunder diff --git a/Source/core/Errors.h b/Source/core/Errors.h new file mode 100644 index 0000000000..a6858cc8b6 --- /dev/null +++ b/Source/core/Errors.h @@ -0,0 +1,152 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "Module.h" +#include "ExtraNumberDefinitions.h" + +#define COM_ERROR (0x80000000) +#define CUSTOM_ERROR (0x1000000) + +namespace Thunder { +namespace Core { + +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + + // transform a custum code into an hresult + EXTERNAL Core::hresult CustomCode(const int24_t customCode); + // query if the hresult is a custom code and if so extract the value, returns 0 if the hresult was not a custom code + EXTERNAL int24_t IsCustomCode(const Core::hresult code); + +#endif + + #define ERROR_CODES \ + ERROR_CODE(ERROR_NONE, 0) \ + ERROR_CODE(ERROR_GENERAL, 1) \ + ERROR_CODE(ERROR_UNAVAILABLE, 2) \ + ERROR_CODE(ERROR_ASYNC_FAILED, 3) \ + ERROR_CODE(ERROR_ASYNC_ABORTED, 4) \ + ERROR_CODE(ERROR_ILLEGAL_STATE, 5) \ + ERROR_CODE(ERROR_OPENING_FAILED, 6) \ + ERROR_CODE(ERROR_ACCEPT_FAILED, 7) \ + ERROR_CODE(ERROR_PENDING_SHUTDOWN, 8) \ + ERROR_CODE(ERROR_ALREADY_CONNECTED, 9) \ + ERROR_CODE(ERROR_CONNECTION_CLOSED, 10) \ + ERROR_CODE(ERROR_TIMEDOUT, 11) \ + ERROR_CODE(ERROR_INPROGRESS, 12) \ + ERROR_CODE(ERROR_COULD_NOT_SET_ADDRESS, 13) \ + ERROR_CODE(ERROR_INCORRECT_HASH, 14) \ + ERROR_CODE(ERROR_INCORRECT_URL, 15) \ + ERROR_CODE(ERROR_INVALID_INPUT_LENGTH, 16) \ + ERROR_CODE(ERROR_DESTRUCTION_SUCCEEDED, 17) \ + ERROR_CODE(ERROR_DESTRUCTION_FAILED, 18) \ + ERROR_CODE(ERROR_CLOSING_FAILED, 19) \ + ERROR_CODE(ERROR_PROCESS_TERMINATED, 20) \ + ERROR_CODE(ERROR_PROCESS_KILLED, 21) \ + ERROR_CODE(ERROR_UNKNOWN_KEY, 22) \ + ERROR_CODE(ERROR_INCOMPLETE_CONFIG, 23) \ + ERROR_CODE(ERROR_PRIVILIGED_REQUEST, 24) \ + ERROR_CODE(ERROR_RPC_CALL_FAILED, 25) \ + ERROR_CODE(ERROR_UNREACHABLE_NETWORK, 26) \ + ERROR_CODE(ERROR_REQUEST_SUBMITTED, 27) \ + ERROR_CODE(ERROR_UNKNOWN_TABLE, 28) \ + ERROR_CODE(ERROR_DUPLICATE_KEY, 29) \ + ERROR_CODE(ERROR_BAD_REQUEST, 30) \ + ERROR_CODE(ERROR_PENDING_CONDITIONS, 31) \ + ERROR_CODE(ERROR_SURFACE_UNAVAILABLE, 32) \ + ERROR_CODE(ERROR_PLAYER_UNAVAILABLE, 33) \ + ERROR_CODE(ERROR_FIRST_RESOURCE_NOT_FOUND, 34) \ + ERROR_CODE(ERROR_SECOND_RESOURCE_NOT_FOUND, 35) \ + ERROR_CODE(ERROR_ALREADY_RELEASED, 36) \ + ERROR_CODE(ERROR_NEGATIVE_ACKNOWLEDGE, 37) \ + ERROR_CODE(ERROR_INVALID_SIGNATURE, 38) \ + ERROR_CODE(ERROR_READ_ERROR, 39) \ + ERROR_CODE(ERROR_WRITE_ERROR, 40) \ + ERROR_CODE(ERROR_INVALID_DESIGNATOR, 41) \ + ERROR_CODE(ERROR_UNAUTHENTICATED, 42) \ + ERROR_CODE(ERROR_NOT_EXIST, 43) \ + ERROR_CODE(ERROR_NOT_SUPPORTED, 44) \ + ERROR_CODE(ERROR_INVALID_RANGE, 45) \ + ERROR_CODE(ERROR_HIBERNATED, 46) \ + ERROR_CODE(ERROR_INPROC, 47) \ + ERROR_CODE(ERROR_FAILED_REGISTERED, 48) \ + ERROR_CODE(ERROR_FAILED_UNREGISTERED, 49) \ + ERROR_CODE(ERROR_PARSE_FAILURE, 50) \ + ERROR_CODE(ERROR_PRIVILIGED_DEFERRED, 51) \ + ERROR_CODE(ERROR_INVALID_ENVELOPPE, 52) \ + ERROR_CODE(ERROR_UNKNOWN_METHOD, 53) \ + ERROR_CODE(ERROR_INVALID_PARAMETER, 54) \ + ERROR_CODE(ERROR_INTERNAL_JSONRPC, 55) \ + ERROR_CODE(ERROR_PARSING_ENVELOPPE, 56) \ + ERROR_CODE(ERROR_COMPOSIT_OBJECT, 57) \ + ERROR_CODE(ERROR_ABORTED, 58) + + #define ERROR_CODE(CODE, VALUE) CODE = VALUE, + + enum ErrorCodes { + ERROR_CODES + ERROR_COUNT + }; + + #undef ERROR_CODE + + // Convert error enumerations to string + + template + inline const TCHAR* _Err2Str() + { + return _T(""); + }; + + #define ERROR_CODE(CODE, VALUE) \ + template<> inline const TCHAR* _Err2Str() { return _T(#CODE); } + + ERROR_CODES; + + #undef ERROR_CODE + + template + inline const TCHAR* _bogus_ErrorToString(uint32_t code) + { + return (code == N? _Err2Str() : _bogus_ErrorToString(code)); + }; + + template<> + inline const TCHAR* _bogus_ErrorToString<0u>(uint32_t code) + { + return (code == 0? _Err2Str<0u>() : _Err2Str<~0u>()); + }; + + EXTERNAL const TCHAR* ErrorToString(const Core::hresult code); + EXTERNAL string ErrorToStringExtended(const Core::hresult code); + +#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ + + using CustomCodeToStringHandler = const TCHAR* (*)(const int32_t code); + + // can only set one, not multithreaded safe + EXTERNAL void SetCustomCodeToStringHandler(CustomCodeToStringHandler handler); + +#endif + + +} +} + diff --git a/Source/core/ExtraNumberDefinitions.h b/Source/core/ExtraNumberDefinitions.h new file mode 100644 index 0000000000..ce6ed3c8cf --- /dev/null +++ b/Source/core/ExtraNumberDefinitions.h @@ -0,0 +1,270 @@ + /* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2020 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +// note does not include Module.h for now as that also would drag in Portability.h (and we do not need Module.h here for now) + +#include "Module.h" +#include "Trace.h" + +#include + +namespace Thunder { +namespace Core { + + class SInt24 { + public: + static constexpr uint8_t SizeOf = 3; + static constexpr uint32_t Max = 0x7FFFFF; + static constexpr int32_t Min = 0xFF800000; + + using InternalType = int32_t; + + SInt24() + : _value(0) + { + } + SInt24(const int32_t value) + : _value(value) + { + bool overflow = ((static_cast(value) >> 23) != 0) && ((static_cast(value) >> 23) != 0x1FF); + ASSERT(overflow == false); + if (overflow == true) { + _value = std::numeric_limits::max(); + } + } + // if it is an uint32_t it considered a 24 bit only filled value + SInt24(const uint32_t value) + : _value(value | (value & 0x800000 ? 0xFF000000 : 0)) + { + bool overflow = ((static_cast(value) >> 23) != 0) && ((static_cast(value) >> 23) != 0x1); + ASSERT(overflow == false); + if (overflow == true) { + _value = std::numeric_limits::max(); + } + } + SInt24(const SInt24&) = default; + SInt24(SInt24&&) = default; + ~SInt24() = default; + + public: + SInt24& operator=(const SInt24& value) = default; + SInt24& operator=(SInt24&& value) = default; + SInt24& operator=(const int32_t value) + { + _value = value; + bool overflow = ((static_cast(value) >> 23) != 0) && ((static_cast(value) >> 23) != 0x1FF); + ASSERT(overflow == false); + if (overflow == true) { + _value = std::numeric_limits::max(); + } + return (*this); + } + // if it is an uint32_t it considered a 24 bit only filled value + SInt24& operator=(const uint32_t value) + { + _value = (value | (value & 0x800000 ? 0xFF000000 : 0)); + bool overflow = ((static_cast(value) >> 23) != 0) && ((static_cast(value) >> 23) != 0x1); + ASSERT(overflow == false); + if (overflow == true) { + _value = std::numeric_limits::max(); + } + return (*this); + } + operator int32_t() const + { + return (_value); + } + + int32_t AsSInt24() const + { + return (_value & 0xFFFFFF); + } + + bool Overflowed() const { + return (_value == std::numeric_limits::max()); + } + + private: + int32_t _value; + }; + + class UInt24 { + public: + static constexpr uint8_t SizeOf = 3; + static constexpr uint32_t Max = 0xFFFFFF; + static constexpr uint32_t Min = 0; + + using InternalType = uint32_t; + + UInt24() + : _value(0) + { + } + UInt24(const UInt24& value) = default; + UInt24(UInt24&& value) = default; + UInt24(const uint32_t value) + : _value(value) + { + bool overflow = ((value >> 24) != 0); + ASSERT(overflow == false); + if (overflow == true) { + _value = std::numeric_limits::max(); + } + } + ~UInt24() = default; + + public: + UInt24& operator=(const UInt24& copy) = default; + UInt24& operator=(UInt24&& move) = default; + UInt24& operator=(const uint32_t value) + { + bool overflow = ((value >> 24) != 0); + ASSERT(overflow == false); + if (overflow == false) { + _value = value; + } else { + _value = std::numeric_limits::max(); + } + return (*this); + } + operator uint32_t() const + { + return (_value); + } + + uint32_t AsUInt24() const // just to be consistent with SInt24 + { + return (_value & 0xFFFFFF); // in debug assert should already have fired on assignment + } + + bool Overflowed() const + { + return (_value == std::numeric_limits::max()); + } + + private: + uint32_t _value; + }; + +} // namespace Core +} // namespace Thunder + +namespace std { // seems to be allowed/mandatory to specialize inside the std namespace + +template <> +class numeric_limits { + +public: + static constexpr bool is_specialized = true; + + static constexpr Thunder::Core::SInt24::InternalType min() noexcept { return Thunder::Core::SInt24::Min; } + static constexpr Thunder::Core::SInt24::InternalType max() noexcept { return Thunder::Core::SInt24::Max; } + static constexpr Thunder::Core::SInt24::InternalType lowest() noexcept { return min(); } + + static constexpr int digits = 23; + static constexpr int digits10 = 6; + static constexpr int max_digits10 = 0; + + static constexpr bool is_signed = true; + static constexpr bool is_integer = true; + static constexpr bool is_exact = true; + static constexpr int radix = 2; + + static constexpr Thunder::Core::SInt24::InternalType epsilon() noexcept { return 0; } + static constexpr Thunder::Core::SInt24::InternalType round_error() noexcept { return 0; } + + static constexpr int min_exponent = 0; + static constexpr int min_exponent10 = 0; + static constexpr int max_exponent = 0; + static constexpr int max_exponent10 = 0; + + static constexpr bool has_infinity = false; + static constexpr bool has_quiet_NaN = false; + static constexpr bool has_signaling_NaN = false; + static constexpr std::float_denorm_style has_denorm = std::denorm_absent; + static constexpr bool has_denorm_loss = false; + + static constexpr Thunder::Core::SInt24::InternalType infinity() noexcept { return static_cast(0); } + static constexpr Thunder::Core::SInt24::InternalType quiet_NaN() noexcept { return static_cast(0); } + static constexpr Thunder::Core::SInt24::InternalType signaling_NaN() noexcept { return static_cast(0); } + static constexpr Thunder::Core::SInt24::InternalType denorm_min() noexcept { return static_cast(0); } + + static constexpr bool is_iec559 = false; + static constexpr bool is_bounded = true; + static constexpr bool is_modulo = false; + + static constexpr bool traps = true; + static constexpr bool tinyness_before = false; + static constexpr std::float_round_style round_style = std::round_toward_zero; +}; + +template <> +class numeric_limits { + +public: + static constexpr bool is_specialized = true; + + static constexpr Thunder::Core::UInt24::InternalType min() noexcept { return Thunder::Core::UInt24::Min; } + static constexpr Thunder::Core::UInt24::InternalType max() noexcept { return Thunder::Core::UInt24::Max; } + static constexpr Thunder::Core::UInt24::InternalType lowest() noexcept { return min(); } + + static constexpr int digits = 24; + static constexpr int digits10 = 7; + static constexpr int max_digits10 = 0; + + static constexpr bool is_signed = false; + static constexpr bool is_integer = true; + static constexpr bool is_exact = true; + static constexpr int radix = 2; + + static constexpr Thunder::Core::UInt24::InternalType epsilon() noexcept { return 0; } + static constexpr Thunder::Core::UInt24::InternalType round_error() noexcept { return 0; } + + static constexpr int min_exponent = 0; + static constexpr int min_exponent10 = 0; + static constexpr int max_exponent = 0; + static constexpr int max_exponent10 = 0; + + static constexpr bool has_infinity = false; + static constexpr bool has_quiet_NaN = false; + static constexpr bool has_signaling_NaN = false; + static constexpr std::float_denorm_style has_denorm = std::denorm_absent; + static constexpr bool has_denorm_loss = false; + + static constexpr Thunder::Core::UInt24::InternalType infinity() noexcept { return static_cast(0); } + static constexpr Thunder::Core::UInt24::InternalType quiet_NaN() noexcept { return static_cast(0); } + static constexpr Thunder::Core::UInt24::InternalType signaling_NaN() noexcept { return static_cast(0); } + static constexpr Thunder::Core::UInt24::InternalType denorm_min() noexcept { return static_cast(0); } + + static constexpr bool is_iec559 = false; + static constexpr bool is_bounded = true; + static constexpr bool is_modulo = true; + + static constexpr bool traps = true; + static constexpr bool tinyness_before = false; + static constexpr std::float_round_style round_style = std::round_toward_zero; +}; + +} // namespace std + +using uint24_t = Thunder::Core::UInt24; +using int24_t = Thunder::Core::SInt24; + diff --git a/Source/core/ICustomErrorCode.h b/Source/core/ICustomErrorCode.h index a6f6376f9a..23afe5e3d0 100644 --- a/Source/core/ICustomErrorCode.h +++ b/Source/core/ICustomErrorCode.h @@ -46,6 +46,9 @@ extern "C" { #include #endif +// called from within Thunder to get the string representation for a custom code +// note parameter code is the pure (signed) Custom Code passed to Thunder. no additional bits set (so for signed numbers 32th bit used as sign bit). +// in case no special string representation is needed return nullptr (NULL), in that case Thunder will convert the Custom Code to a generic message itself EXTERNAL const TCHAR* CustomCodeToSting(const int32_t code); #ifdef __cplusplus diff --git a/Source/core/JSONRPC.h b/Source/core/JSONRPC.h index 5b89b6b055..7b8dad18cb 100644 --- a/Source/core/JSONRPC.h +++ b/Source/core/JSONRPC.h @@ -22,6 +22,8 @@ #include "JSON.h" #include "Module.h" #include "TypeTraits.h" +#include "Errors.h" +#include "Number.h" #include #include @@ -189,9 +191,9 @@ namespace Core { Code = ApplicationErrorCodeBase - static_cast(frameworkError & 0x7FFFFFFF) - 500; } else { #ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ - int32_t customcode = IsCustomCode(frameworkError); + int24_t customcode = IsCustomCode(frameworkError); if (customcode != 0) { - Code = (customcode == std::numeric_limits::min() ? 0 : customcode); + Code = (Core::Overflowed(customcode) == true ? 0 : static_cast(customcode)); } else { #endif Code = ApplicationErrorCodeBase - static_cast(frameworkError); diff --git a/Source/core/NetworkInfo.h b/Source/core/NetworkInfo.h index 683ce67a51..0e4422b303 100644 --- a/Source/core/NetworkInfo.h +++ b/Source/core/NetworkInfo.h @@ -23,7 +23,6 @@ #include "Portability.h" #include "Netlink.h" #include "NodeId.h" -#include "Portability.h" #include "SocketPort.h" namespace Thunder { diff --git a/Source/core/Number.h b/Source/core/Number.h index 3d9b1ced74..9fddd7b653 100644 --- a/Source/core/Number.h +++ b/Source/core/Number.h @@ -25,8 +25,6 @@ #include "TextFragment.h" #include "TypeTraits.h" -#include - namespace Thunder { namespace Core { extern "C" { @@ -1051,94 +1049,6 @@ namespace Core { uint8_t _size; }; - class SInt24 { - public: - static constexpr uint8_t SizeOf = 3; - static constexpr uint32_t Max = 0x7FFFFF; - static constexpr int32_t Min = 0xFF800000; - - using InternalType = int32_t; - - SInt24() - : _value(0) - { - } - SInt24(const int32_t value) - : _value(value | (value & 0x800000 ? 0xFF000000 : 0)) - { - ASSERT(((static_cast(value) >> 24) == 0) || ((static_cast(value) >> 24) == 0xFF)); - } - SInt24(const SInt24&) = default; - SInt24(SInt24&&) = default; - ~SInt24() = default; - - public: - SInt24& operator=(const SInt24& value) = default; - SInt24& operator=(SInt24&& value) = default; - SInt24& operator=(const int32_t value) - { - ASSERT(((static_cast(value) >> 24) == 0) || ((static_cast(value) >> 24) == 0xFF)); - _value = (value | (value & 0x800000 ? 0xFF000000 : 0)); - return (*this); - } - operator int32_t() const - { - return (_value); - } - - int32_t AsSInt24() const - { - return (_value & 0xFFFFFF); - } - - private: - int32_t _value; - }; - - class UInt24 { - public: - static constexpr uint8_t SizeOf = 3; - static constexpr uint32_t Max = 0xFFFFFF; - static constexpr uint32_t Min = 0; - - using InternalType = uint32_t; - - UInt24() - : _value(0) - { - } - UInt24(const UInt24& value) = default; - UInt24(UInt24&& value) = default; - UInt24(const uint32_t value) - : _value(value) - { - ASSERT((value >> 24) == 0); - } - ~UInt24() = default; - - public: - UInt24& operator=(const UInt24& copy) = default; - UInt24& operator=(UInt24&& move) = default; - UInt24& operator=(const uint32_t value) - { - ASSERT((value >> 24) == 0); - _value = value; - return (*this); - } - operator uint32_t() const - { - return (_value); - } - - uint32_t AsUInt24() const // just to be consistent with SInt24 - { - return (_value & 0xFFFFFF); // in debug assert should already have fired on assignment - } - - private: - uint32_t _value; - }; - template ::value, int>::type = 0> static constexpr uint8_t RealSize() { @@ -1149,6 +1059,25 @@ namespace Core { { return (T::SizeOf); } + + // ----------------------------------------------------- + // Check for Overflowed member available on number type + // ----------------------------------------------------- + IS_MEMBER_AVAILABLE(Overflowed, hasOverflowed); + + template + inline typename Core::TypeTraits::enable_if::value, bool>::type + Overflowed(TYPE t) + { + return t.Overflowed(); + } + + template + inline typename Core::TypeTraits::enable_if::value, bool>::type + Overflowed(TYPE) + { + return false; + } template bool check_and_cast(const ORIGINAL_TYPE& input, NEW_TYPE& output) @@ -1176,107 +1105,6 @@ namespace Core { } // namespace Core } // namespace Thunder -namespace std { // seems to be allowed/mandatory to specialize inside the std namespace - -template <> -class numeric_limits { - -public: - static constexpr bool is_specialized = true; - - static constexpr Thunder::Core::SInt24::InternalType min() noexcept { return Thunder::Core::SInt24::Min; } - static constexpr Thunder::Core::SInt24::InternalType max() noexcept { return Thunder::Core::SInt24::Max; } - static constexpr Thunder::Core::SInt24::InternalType lowest() noexcept { return min(); } - - static constexpr int digits = 23; - static constexpr int digits10 = 6; - static constexpr int max_digits10 = 0; - - static constexpr bool is_signed = true; - static constexpr bool is_integer = true; - static constexpr bool is_exact = true; - static constexpr int radix = 2; - - static constexpr Thunder::Core::SInt24::InternalType epsilon() noexcept { return 0; } - static constexpr Thunder::Core::SInt24::InternalType round_error() noexcept { return 0; } - - static constexpr int min_exponent = 0; - static constexpr int min_exponent10 = 0; - static constexpr int max_exponent = 0; - static constexpr int max_exponent10 = 0; - - static constexpr bool has_infinity = false; - static constexpr bool has_quiet_NaN = false; - static constexpr bool has_signaling_NaN = false; - static constexpr std::float_denorm_style has_denorm = std::denorm_absent; - static constexpr bool has_denorm_loss = false; - - static constexpr Thunder::Core::SInt24::InternalType infinity() noexcept { return static_cast(0); } - static constexpr Thunder::Core::SInt24::InternalType quiet_NaN() noexcept { return static_cast(0); } - static constexpr Thunder::Core::SInt24::InternalType signaling_NaN() noexcept { return static_cast(0); } - static constexpr Thunder::Core::SInt24::InternalType denorm_min() noexcept { return static_cast(0); } - - static constexpr bool is_iec559 = false; - static constexpr bool is_bounded = true; - static constexpr bool is_modulo = false; - - static constexpr bool traps = true; - static constexpr bool tinyness_before = false; - static constexpr std::float_round_style round_style = std::round_toward_zero; -}; - -template <> -class numeric_limits { - -public: - static constexpr bool is_specialized = true; - - static constexpr Thunder::Core::UInt24::InternalType min() noexcept { return Thunder::Core::UInt24::Min; } - static constexpr Thunder::Core::UInt24::InternalType max() noexcept { return Thunder::Core::UInt24::Max; } - static constexpr Thunder::Core::UInt24::InternalType lowest() noexcept { return min(); } - - static constexpr int digits = 24; - static constexpr int digits10 = 7; - static constexpr int max_digits10 = 0; - - static constexpr bool is_signed = false; - static constexpr bool is_integer = true; - static constexpr bool is_exact = true; - static constexpr int radix = 2; - - static constexpr Thunder::Core::UInt24::InternalType epsilon() noexcept { return 0; } - static constexpr Thunder::Core::UInt24::InternalType round_error() noexcept { return 0; } - - static constexpr int min_exponent = 0; - static constexpr int min_exponent10 = 0; - static constexpr int max_exponent = 0; - static constexpr int max_exponent10 = 0; - - static constexpr bool has_infinity = false; - static constexpr bool has_quiet_NaN = false; - static constexpr bool has_signaling_NaN = false; - static constexpr std::float_denorm_style has_denorm = std::denorm_absent; - static constexpr bool has_denorm_loss = false; - - static constexpr Thunder::Core::UInt24::InternalType infinity() noexcept { return static_cast(0); } - static constexpr Thunder::Core::UInt24::InternalType quiet_NaN() noexcept { return static_cast(0); } - static constexpr Thunder::Core::UInt24::InternalType signaling_NaN() noexcept { return static_cast(0); } - static constexpr Thunder::Core::UInt24::InternalType denorm_min() noexcept { return static_cast(0); } - - static constexpr bool is_iec559 = false; - static constexpr bool is_bounded = true; - static constexpr bool is_modulo = true; - - static constexpr bool traps = true; - static constexpr bool tinyness_before = false; - static constexpr std::float_round_style round_style = std::round_toward_zero; -}; - -} // namespace std - -using uint24_t = Thunder::Core::UInt24; -using int24_t = Thunder::Core::SInt24; - template NEW_TYPE int_cast(const ORIGINAL_TYPE& input) { diff --git a/Source/core/Portability.cpp b/Source/core/Portability.cpp index 482a7812ab..e7165e991d 100644 --- a/Source/core/Portability.cpp +++ b/Source/core/Portability.cpp @@ -25,7 +25,7 @@ #include "Sync.h" #include "SystemInfo.h" #include "Serialization.h" -#include "Number.h" +//#include "Number.h" #ifdef __LINUX__ #include @@ -459,112 +459,6 @@ namespace Core { return (lastIndex < (index - 1) ? TextFragment(result, lastIndex + 1, result.Length() - (lastIndex + 1)) : result); } -#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ - -namespace { - - static CustomCodeToStringHandler customerrorcodehandler = nullptr; - - const TCHAR* HandleCustomErrorCodeToString(const int32_t customcode) - { - const TCHAR* text = nullptr; - - if (customcode == std::numeric_limits::min()) { - text = _T("Invalid Custom ErrorCode set"); - } else if ((customerrorcodehandler == nullptr) || ((text = customerrorcodehandler(customcode)) == nullptr)) { - text = _T("Undefined Custom Error"); - } - - return text; - } - - string HandleCustomErrorCodeToStringExtended(const int32_t customcode) - { - string result; - - const TCHAR* text = nullptr; - if (customcode == std::numeric_limits::min()) { - result = _T("Invalid Custom ErrorCode set"); - } else if ((customerrorcodehandler == nullptr) || ((text = customerrorcodehandler(customcode)) == nullptr)) { - result = _T("Undefined Custom Error: ") + Core::NumberType(customcode).Text(); - } else { - result = text; - } - - return result; - } - -} - void SetCustomCodeToStringHandler(CustomCodeToStringHandler handler) { - customerrorcodehandler = handler; - } - - hresult CustomCode(const int32_t customCode) { - - static_assert(CUSTOM_ERROR == 0x1000000, "Code below assumes 25th bit used for CUSTOM_ERROR"); - - hresult result = Core::ERROR_NONE; - - if (customCode != 0) { - int24_t code; - if (Core::check_and_cast(customCode, code) == true) { - result = static_cast(code.AsSInt24()); - } else { - result = 0; // set invalid customCode result; - } - result |= CUSTOM_ERROR; // set custom code bit - } - - return result; - } - - int32_t IsCustomCode(const Core::hresult code) { - static_assert(CUSTOM_ERROR == 0x1000000, "Code below assumes 25th bit used for CUSTOM_ERROR"); - - int32_t result = 0; - - if ((code & CUSTOM_ERROR) != 0) { - int24_t custumcode(code & 0xFFFFFF); // remove custom error bit before assigning - result = custumcode; - if (result == 0) { - result = std::numeric_limits::min(); - } - } - - return result; - } - -#endif - - const TCHAR* ErrorToString(const Core::hresult code) - { -#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ - int32_t customcode = IsCustomCode(code); - - if (customcode != 0) { - return HandleCustomErrorCodeToString(customcode); - } -#endif - return _bogus_ErrorToString<>(code & (~COM_ERROR)); - } - - string ErrorToStringExtended(const Core::hresult code) - { -#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ - int32_t customcode = IsCustomCode(code); - - if (customcode != 0) { - return HandleCustomErrorCodeToStringExtended(customcode); - } -#endif - string result = _bogus_ErrorToString<>(code & (~COM_ERROR)); - - if (result.empty() == true) { - result = _T("Undefined Thunder error code: ") + Core::NumberType(code).Text(); - } - return result; - } - } // namespace Core } // namespace Thunder diff --git a/Source/core/Portability.h b/Source/core/Portability.h index 61680cf032..af7f7de23b 100644 --- a/Source/core/Portability.h +++ b/Source/core/Portability.h @@ -753,298 +753,178 @@ typedef std::string string; #endif namespace Thunder { -namespace Core { + namespace Core { - class TextFragment; + class TextFragment; - #if defined(__CORE_INSTANCE_BITS__) && (__CORE_INSTANCE_BITS__ != 0) - #if __CORE_INSTANCE_BITS__ <= 8 - typedef uint8_t instance_id; - #elif __CORE_INSTANCE_BITS__ <= 16 - typedef uint16_t instance_id; - #elif __CORE_INSTANCE_BITS__ <= 32 - typedef uint32_t instance_id; - #elif __CORE_INSTANCE_BITS__ <= 64 - typedef uint64_t instance_id; - #endif - #else - #if defined(__SIZEOF_POINTER__) && (__SIZEOF_POINTER__ == 8) +#if defined(__CORE_INSTANCE_BITS__) && (__CORE_INSTANCE_BITS__ != 0) +#if __CORE_INSTANCE_BITS__ <= 8 + typedef uint8_t instance_id; +#elif __CORE_INSTANCE_BITS__ <= 16 + typedef uint16_t instance_id; +#elif __CORE_INSTANCE_BITS__ <= 32 + typedef uint32_t instance_id; +#elif __CORE_INSTANCE_BITS__ <= 64 + typedef uint64_t instance_id; +#endif +#else +#if defined(__SIZEOF_POINTER__) && (__SIZEOF_POINTER__ == 8) typedef uint64_t instance_id; - #else +#else typedef uint32_t instance_id; - #endif - #endif +#endif +#endif - #ifdef __LINUX__ - typedef pthread_t thread_id; - #else +#ifdef __LINUX__ + typedef pthread_t thread_id; +#else typedef DWORD thread_id; - #endif +#endif - typedef uint32_t hresult; + typedef uint32_t hresult; - struct callstack_info { - void* address; - string module; - string function; - uint32_t line; - }; + struct callstack_info { + void* address; + string module; + string function; + uint32_t line; + }; - inline void* Alignment(size_t alignment, void* incoming) - { - const auto basePtr = reinterpret_cast(incoming); - return reinterpret_cast((basePtr - 1u + alignment) & ~(alignment - 1)); - } + inline void* Alignment(size_t alignment, void* incoming) + { + const auto basePtr = reinterpret_cast(incoming); + return reinterpret_cast((basePtr - 1u + alignment) & ~(alignment - 1)); + } - inline uint8_t* PointerAlign(uint8_t* pointer) - { - uintptr_t addr = reinterpret_cast(pointer); - addr = (addr + (sizeof(void*) - 1)) & ~(sizeof(void*) - 1); // Round up to align-byte boundary - return reinterpret_cast(addr); - } + inline uint8_t* PointerAlign(uint8_t* pointer) + { + uintptr_t addr = reinterpret_cast(pointer); + addr = (addr + (sizeof(void*) - 1)) & ~(sizeof(void*) - 1); // Round up to align-byte boundary + return reinterpret_cast(addr); + } - inline const uint8_t* PointerAlign(const uint8_t* pointer) - { - uintptr_t addr = reinterpret_cast(pointer); - addr = (addr + (sizeof(void*) - 1)) & ~(sizeof(void*) - 1); // Round up to align-byte boundary - return reinterpret_cast(addr); - } + inline const uint8_t* PointerAlign(const uint8_t* pointer) + { + uintptr_t addr = reinterpret_cast(pointer); + addr = (addr + (sizeof(void*) - 1)) & ~(sizeof(void*) - 1); // Round up to align-byte boundary + return reinterpret_cast(addr); + } #ifdef _UNICODE - typedef std::wstring string; + typedef std::wstring string; #endif #ifndef _UNICODE - typedef std::string string; + typedef std::string string; #endif - inline void ToUpper(const string& input, string& output) - { - // Copy string to output, so we know memory is allocated. - output = input; + inline void ToUpper(const string& input, string& output) + { + // Copy string to output, so we know memory is allocated. + output = input; - std::transform(input.begin(), input.end(), output.begin(), ::toupper); - } + std::transform(input.begin(), input.end(), output.begin(), ::toupper); + } - inline void ToUpper(string& inplace) - { - std::transform(inplace.begin(), inplace.end(), inplace.begin(), ::toupper); - } + inline void ToUpper(string& inplace) + { + std::transform(inplace.begin(), inplace.end(), inplace.begin(), ::toupper); + } - inline void ToLower(const string& input, string& output) - { - output = input; + inline void ToLower(const string& input, string& output) + { + output = input; - std::transform(input.begin(), input.end(), output.begin(), ::tolower); - } + std::transform(input.begin(), input.end(), output.begin(), ::tolower); + } - inline void ToLower(string& inplace) - { - std::transform(inplace.begin(), inplace.end(), inplace.begin(), ::tolower); - } + inline void ToLower(string& inplace) + { + std::transform(inplace.begin(), inplace.end(), inplace.begin(), ::tolower); + } - EXTERNAL extern string Format(const TCHAR formatter[], ...) PRINTF_FORMAT(1, 2); - EXTERNAL extern void Format(string& dst, const TCHAR format[], ...) PRINTF_FORMAT(2, 3); - EXTERNAL extern void Format(string& dst, const TCHAR format[], va_list ap); + EXTERNAL extern string Format(const TCHAR formatter[], ...) PRINTF_FORMAT(1, 2); + EXTERNAL extern void Format(string& dst, const TCHAR format[], ...) PRINTF_FORMAT(2, 3); + EXTERNAL extern void Format(string& dst, const TCHAR format[], va_list ap); - const uint32_t infinite = -1; - static const string emptyString; + const uint32_t infinite = -1; + static const string emptyString; - class Void { - public: - template - inline Void(Args&&...) {} - inline Void(const Void&) = default; - inline Void(Void&&) = default; - inline ~Void() = default; + class Void { + public: + template + inline Void(Args&&...) {} + inline Void(const Void&) = default; + inline Void(Void&&) = default; + inline ~Void() = default; - inline Void& operator=(const Void&) = default; - }; + inline Void& operator=(const Void&) = default; + }; - struct EXTERNAL IReferenceCounted { - virtual ~IReferenceCounted() = default; - virtual uint32_t AddRef() const = 0; - virtual uint32_t Release() const = 0; - }; + struct EXTERNAL IReferenceCounted { + virtual ~IReferenceCounted() = default; + virtual uint32_t AddRef() const = 0; + virtual uint32_t Release() const = 0; + }; struct EXTERNAL IUnknown : public IReferenceCounted { - enum : uint32_t { + enum : uint32_t { ID_OFFSET_INTERNAL = 0x00000000, ID_OFFSET_PUBLIC = 0x00000040, ID_OFFSET_CUSTOM = 0x80000000 - }; + }; - enum { ID = (ID_OFFSET_INTERNAL + 0x0000) }; + enum { ID = (ID_OFFSET_INTERNAL + 0x0000) }; - ~IUnknown() override = default; + ~IUnknown() override = default; - virtual void* QueryInterface(const uint32_t interfaceNumber) = 0; + virtual void* QueryInterface(const uint32_t interfaceNumber) = 0; - template - REQUESTEDINTERFACE* QueryInterface() - { - void* baseInterface(QueryInterface(REQUESTEDINTERFACE::ID)); + template + REQUESTEDINTERFACE* QueryInterface() + { + void* baseInterface(QueryInterface(REQUESTEDINTERFACE::ID)); + + if (baseInterface != nullptr) { + return (reinterpret_cast(baseInterface)); + } - if (baseInterface != nullptr) { - return (reinterpret_cast(baseInterface)); + return (nullptr); } - return (nullptr); - } + template + const REQUESTEDINTERFACE* QueryInterface() const + { + const void* baseInterface(const_cast(this)->QueryInterface(REQUESTEDINTERFACE::ID)); - template - const REQUESTEDINTERFACE* QueryInterface() const - { - const void* baseInterface(const_cast(this)->QueryInterface(REQUESTEDINTERFACE::ID)); + if (baseInterface != nullptr) { + return (reinterpret_cast(baseInterface)); + } - if (baseInterface != nullptr) { - return (reinterpret_cast(baseInterface)); + return (nullptr); } + }; - return (nullptr); - } - }; - - namespace memory_order { - #ifdef __WINDOWS__ - static constexpr std::memory_order memory_order_relaxed = std::memory_order::memory_order_relaxed; - static constexpr std::memory_order memory_order_consume = std::memory_order::memory_order_seq_cst; - static constexpr std::memory_order memory_order_acquire = std::memory_order::memory_order_seq_cst; - static constexpr std::memory_order memory_order_release = std::memory_order::memory_order_release; - static constexpr std::memory_order memory_order_acq_rel = std::memory_order::memory_order_seq_cst; - static constexpr std::memory_order memory_order_seq_cst = std::memory_order::memory_order_seq_cst; - #else + namespace memory_order { +#ifdef __WINDOWS__ + static constexpr std::memory_order memory_order_relaxed = std::memory_order::memory_order_relaxed; + static constexpr std::memory_order memory_order_consume = std::memory_order::memory_order_seq_cst; + static constexpr std::memory_order memory_order_acquire = std::memory_order::memory_order_seq_cst; + static constexpr std::memory_order memory_order_release = std::memory_order::memory_order_release; + static constexpr std::memory_order memory_order_acq_rel = std::memory_order::memory_order_seq_cst; + static constexpr std::memory_order memory_order_seq_cst = std::memory_order::memory_order_seq_cst; +#else static constexpr std::memory_order memory_order_relaxed = std::memory_order::memory_order_relaxed; static constexpr std::memory_order memory_order_consume = std::memory_order::memory_order_consume; static constexpr std::memory_order memory_order_acquire = std::memory_order::memory_order_acquire; static constexpr std::memory_order memory_order_release = std::memory_order::memory_order_release; static constexpr std::memory_order memory_order_acq_rel = std::memory_order::memory_order_acq_rel; static constexpr std::memory_order memory_order_seq_cst = std::memory_order::memory_order_seq_cst; - #endif - } - - #define COM_ERROR (0x80000000) - #define CUSTOM_ERROR (0x1000000) - -#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ - - // transform a custum code into an hresult - EXTERNAL Core::hresult CustomCode(const int32_t customCode); - // query if the hresult is a custom code and if so extract the value, returns 0 if the hresult was not a custom code - EXTERNAL int32_t IsCustomCode(const Core::hresult code); - -#endif - - #define ERROR_CODES \ - ERROR_CODE(ERROR_NONE, 0) \ - ERROR_CODE(ERROR_GENERAL, 1) \ - ERROR_CODE(ERROR_UNAVAILABLE, 2) \ - ERROR_CODE(ERROR_ASYNC_FAILED, 3) \ - ERROR_CODE(ERROR_ASYNC_ABORTED, 4) \ - ERROR_CODE(ERROR_ILLEGAL_STATE, 5) \ - ERROR_CODE(ERROR_OPENING_FAILED, 6) \ - ERROR_CODE(ERROR_ACCEPT_FAILED, 7) \ - ERROR_CODE(ERROR_PENDING_SHUTDOWN, 8) \ - ERROR_CODE(ERROR_ALREADY_CONNECTED, 9) \ - ERROR_CODE(ERROR_CONNECTION_CLOSED, 10) \ - ERROR_CODE(ERROR_TIMEDOUT, 11) \ - ERROR_CODE(ERROR_INPROGRESS, 12) \ - ERROR_CODE(ERROR_COULD_NOT_SET_ADDRESS, 13) \ - ERROR_CODE(ERROR_INCORRECT_HASH, 14) \ - ERROR_CODE(ERROR_INCORRECT_URL, 15) \ - ERROR_CODE(ERROR_INVALID_INPUT_LENGTH, 16) \ - ERROR_CODE(ERROR_DESTRUCTION_SUCCEEDED, 17) \ - ERROR_CODE(ERROR_DESTRUCTION_FAILED, 18) \ - ERROR_CODE(ERROR_CLOSING_FAILED, 19) \ - ERROR_CODE(ERROR_PROCESS_TERMINATED, 20) \ - ERROR_CODE(ERROR_PROCESS_KILLED, 21) \ - ERROR_CODE(ERROR_UNKNOWN_KEY, 22) \ - ERROR_CODE(ERROR_INCOMPLETE_CONFIG, 23) \ - ERROR_CODE(ERROR_PRIVILIGED_REQUEST, 24) \ - ERROR_CODE(ERROR_RPC_CALL_FAILED, 25) \ - ERROR_CODE(ERROR_UNREACHABLE_NETWORK, 26) \ - ERROR_CODE(ERROR_REQUEST_SUBMITTED, 27) \ - ERROR_CODE(ERROR_UNKNOWN_TABLE, 28) \ - ERROR_CODE(ERROR_DUPLICATE_KEY, 29) \ - ERROR_CODE(ERROR_BAD_REQUEST, 30) \ - ERROR_CODE(ERROR_PENDING_CONDITIONS, 31) \ - ERROR_CODE(ERROR_SURFACE_UNAVAILABLE, 32) \ - ERROR_CODE(ERROR_PLAYER_UNAVAILABLE, 33) \ - ERROR_CODE(ERROR_FIRST_RESOURCE_NOT_FOUND, 34) \ - ERROR_CODE(ERROR_SECOND_RESOURCE_NOT_FOUND, 35) \ - ERROR_CODE(ERROR_ALREADY_RELEASED, 36) \ - ERROR_CODE(ERROR_NEGATIVE_ACKNOWLEDGE, 37) \ - ERROR_CODE(ERROR_INVALID_SIGNATURE, 38) \ - ERROR_CODE(ERROR_READ_ERROR, 39) \ - ERROR_CODE(ERROR_WRITE_ERROR, 40) \ - ERROR_CODE(ERROR_INVALID_DESIGNATOR, 41) \ - ERROR_CODE(ERROR_UNAUTHENTICATED, 42) \ - ERROR_CODE(ERROR_NOT_EXIST, 43) \ - ERROR_CODE(ERROR_NOT_SUPPORTED, 44) \ - ERROR_CODE(ERROR_INVALID_RANGE, 45) \ - ERROR_CODE(ERROR_HIBERNATED, 46) \ - ERROR_CODE(ERROR_INPROC, 47) \ - ERROR_CODE(ERROR_FAILED_REGISTERED, 48) \ - ERROR_CODE(ERROR_FAILED_UNREGISTERED, 49) \ - ERROR_CODE(ERROR_PARSE_FAILURE, 50) \ - ERROR_CODE(ERROR_PRIVILIGED_DEFERRED, 51) \ - ERROR_CODE(ERROR_INVALID_ENVELOPPE, 52) \ - ERROR_CODE(ERROR_UNKNOWN_METHOD, 53) \ - ERROR_CODE(ERROR_INVALID_PARAMETER, 54) \ - ERROR_CODE(ERROR_INTERNAL_JSONRPC, 55) \ - ERROR_CODE(ERROR_PARSING_ENVELOPPE, 56) \ - ERROR_CODE(ERROR_COMPOSIT_OBJECT, 57) \ - ERROR_CODE(ERROR_ABORTED, 58) - - #define ERROR_CODE(CODE, VALUE) CODE = VALUE, - - enum ErrorCodes { - ERROR_CODES - ERROR_COUNT - }; - - #undef ERROR_CODE - - // Convert error enumerations to string - - template - inline const TCHAR* _Err2Str() - { - return _T(""); - }; - - #define ERROR_CODE(CODE, VALUE) \ - template<> inline const TCHAR* _Err2Str() { return _T(#CODE); } - - ERROR_CODES; - - template - inline const TCHAR* _bogus_ErrorToString(uint32_t code) - { - return (code == N? _Err2Str() : _bogus_ErrorToString(code)); - }; - - template<> - inline const TCHAR* _bogus_ErrorToString<0u>(uint32_t code) - { - return (code == 0? _Err2Str<0u>() : _Err2Str<~0u>()); - }; - - EXTERNAL const TCHAR* ErrorToString(const Core::hresult code); - EXTERNAL string ErrorToStringExtended(const Core::hresult code); - -#ifndef __DISABLE_USE_COMPLEMENTARY_CODE_SET__ - - using CustomCodeToStringHandler = const TCHAR* (*)(const int32_t code); - - // can only set one, not multithreaded safe - EXTERNAL void SetCustomCodeToStringHandler(CustomCodeToStringHandler handler); - #endif + } - #undef ERROR_CODE EXTERNAL TextFragment Demangled(const char name[]); EXTERNAL TextFragment ClassName(const char name[]); @@ -1053,6 +933,8 @@ namespace Core { } } +// code to make (to some extend) old code whioch still uses the WPEFramework instead of Thunder still compile + namespace WPEFramework { using namespace Thunder; } diff --git a/Source/core/ProcessInfo.cpp b/Source/core/ProcessInfo.cpp index a0aa74caf5..233cad552b 100644 --- a/Source/core/ProcessInfo.cpp +++ b/Source/core/ProcessInfo.cpp @@ -17,6 +17,7 @@ * limitations under the License. */ +#include "Errors.h" #include "ProcessInfo.h" #include "FileSystem.h" #include "SystemInfo.h" diff --git a/Source/core/Proxy.h b/Source/core/Proxy.h index 27d279bbcd..46e39b687e 100644 --- a/Source/core/Proxy.h +++ b/Source/core/Proxy.h @@ -26,6 +26,7 @@ // ---- Include local include files ---- #include "Portability.h" +#include "Errors.h" #include "StateTrigger.h" #include "Sync.h" #include "TypeTraits.h" diff --git a/Source/core/Sync.cpp b/Source/core/Sync.cpp index 6864df15d0..370badb528 100644 --- a/Source/core/Sync.cpp +++ b/Source/core/Sync.cpp @@ -37,6 +37,7 @@ #include "Sync.h" #include "ProcessInfo.h" #include "Trace.h" +#include "Errors.h" #ifdef __CORE_CRITICAL_SECTION_LOG__ #include "Thread.h" diff --git a/Source/core/core.h b/Source/core/core.h index 4c1170e595..0ca826b8ed 100644 --- a/Source/core/core.h +++ b/Source/core/core.h @@ -107,6 +107,9 @@ #include "WorkerPool.h" #include "WarningReportingControl.h" #include "WarningReportingCategories.h" +#include "Number.h" +#include "ExtraNumberDefinitions.h" +#include "Errors.h" #ifdef __WINDOWS__ #pragma comment(lib, "core.lib") diff --git a/Source/core/core.vcxproj b/Source/core/core.vcxproj index 356b393d19..b41ec03a2d 100644 --- a/Source/core/core.vcxproj +++ b/Source/core/core.vcxproj @@ -24,6 +24,9 @@ + + + @@ -112,6 +115,7 @@ + @@ -310,4 +314,4 @@ - + \ No newline at end of file From b48ddab2cbd850b7ef77290d68315322743f36ea Mon Sep 17 00:00:00 2001 From: MFransen69 <39826971+MFransen69@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:04:07 +0100 Subject: [PATCH 28/64] [CustomCode] oops r was missing (#2010) --- Source/Thunder/PluginHost.cpp | 6 +++--- Source/core/ICustomErrorCode.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Thunder/PluginHost.cpp b/Source/Thunder/PluginHost.cpp index 483374b572..6e72ed8d4a 100644 --- a/Source/Thunder/PluginHost.cpp +++ b/Source/Thunder/PluginHost.cpp @@ -164,11 +164,11 @@ POP_WARNING() _customCodeLibrary = Core::Library(libraryPath.c_str()); if (_customCodeLibrary.IsLoaded() == true) { - _customCodeToStringHandler = reinterpret_cast(_customCodeLibrary.LoadFunction(CustomCodeToStingName)); + _customCodeToStringHandler = reinterpret_cast(_customCodeLibrary.LoadFunction(CustomCodeToStringName)); if (_customCodeToStringHandler != nullptr) { Core::SetCustomCodeToStringHandler(_customCodeToStringHandler); } else { - SYSLOG(Logging::Error, (_T("Could not find CustomCodeToSting function in Custom Error Code library"))); + SYSLOG(Logging::Error, (_T("Could not find CustomCodeToString function in Custom Error Code library"))); _customCodeLibrary = Core::Library(); } } else { @@ -188,7 +188,7 @@ POP_WARNING() } private: - static constexpr const TCHAR* CustomCodeToStingName{ _T("CustomCodeToSting") }; + static constexpr const TCHAR* CustomCodeToStringName{ _T("CustomCodeToString") }; Core::Library _customCodeLibrary; Core::CustomCodeToStringHandler _customCodeToStringHandler; diff --git a/Source/core/ICustomErrorCode.h b/Source/core/ICustomErrorCode.h index 23afe5e3d0..9c2b0cbb9a 100644 --- a/Source/core/ICustomErrorCode.h +++ b/Source/core/ICustomErrorCode.h @@ -49,7 +49,7 @@ extern "C" { // called from within Thunder to get the string representation for a custom code // note parameter code is the pure (signed) Custom Code passed to Thunder. no additional bits set (so for signed numbers 32th bit used as sign bit). // in case no special string representation is needed return nullptr (NULL), in that case Thunder will convert the Custom Code to a generic message itself -EXTERNAL const TCHAR* CustomCodeToSting(const int32_t code); +EXTERNAL const TCHAR* CustomCodeToString(const int32_t code); #ifdef __cplusplus } // extern "C" From 53d88b894b358791821fea06da88ecce224b0eb0 Mon Sep 17 00:00:00 2001 From: sebaszm <45654185+sebaszm@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:36:17 +0100 Subject: [PATCH 29/64] [IController] Move event index to registration (#2012) * [IController] Move event index to registration * update docs * Use stock sendif lambdas * Also update StateControlStateChange event * kick actions Add assertion to ensure callsign is not empty. * kick actions --------- Co-authored-by: Pierre Wielders --- Source/Thunder/Controller.cpp | 44 ++++++++++---------------- Source/Thunder/Controller.h | 8 ++--- Source/Thunder/doc/ControllerPlugin.md | 2 +- Source/plugins/IController.h | 8 ++--- 4 files changed, 25 insertions(+), 37 deletions(-) diff --git a/Source/Thunder/Controller.cpp b/Source/Thunder/Controller.cpp index 7c86aacc47..e4cbb24fa8 100644 --- a/Source/Thunder/Controller.cpp +++ b/Source/Thunder/Controller.cpp @@ -1451,6 +1451,8 @@ namespace Plugin { } void Controller::NotifyStateChange(const string& callsign, const PluginHost::IShell::state& state, const PluginHost::IShell::reason& reason) { + ASSERT(callsign.empty() == false); + _adminLock.Lock(); for (const auto& entry : _lifeTimeObservers) { @@ -1461,14 +1463,10 @@ namespace Plugin { _adminLock.Unlock(); - // also notify the JSON RPC listeners (if any) + // also notify the JSON RPC listeners (if any...) // First notify observers that registered for all (notification will include the callsign) - Exchange::Controller::JLifeTime::Event::StateChange(*this, {}, callsign, state, reason, - [](const string&, const string& index) { - // Custom sendif lambda to only catch broadcast observers. - return (index.empty() == true); - }); + Exchange::Controller::JLifeTime::Event::StateChange(*this, {}, callsign, state, reason); // ... then the specific observers (notification will not inlcude a callsign) Exchange::Controller::JLifeTime::Event::StateChange(*this, callsign, {}, state, reason); @@ -1476,6 +1474,8 @@ namespace Plugin { void Controller::NotifyStateControlStateChange(const string& callsign, const Exchange::Controller::ILifeTime::state& state) { + ASSERT(callsign.empty() == false); + _adminLock.Lock(); for (const auto& entry : _lifeTimeObservers) { @@ -1487,17 +1487,13 @@ namespace Plugin { _adminLock.Unlock(); // also notify the JSON RPC listeners (if any) - Exchange::Controller::JLifeTime::Event::StateControlStateChange(*this, {}, callsign, state, - [](const string&, const string& index) { - return (index.empty() == true); - }); - + Exchange::Controller::JLifeTime::Event::StateControlStateChange(*this, {}, callsign, state); Exchange::Controller::JLifeTime::Event::StateControlStateChange(*this, callsign, {}, state); } - void Controller::SendInitialStateSnapshot(const string& client, const string& callsign) + void Controller::SendInitialStateSnapshot(const string& client, const Core::OptionalType& callsign) { - if (callsign.empty() == true) { + if (callsign.IsSet() == false) { _adminLock.Lock(); ASSERT(_pluginServer != nullptr); @@ -1509,35 +1505,27 @@ namespace Plugin { if (service->State() == PluginHost::IShell::state::ACTIVATED) { const string serviceCallsign = service->Callsign(); - Exchange::Controller::JLifeTime::Event::StateChange(*this, {}, serviceCallsign, service->State(), service->Reason(), - [&client, &serviceCallsign](const string& designator, const string& index) { - // Custom sendif lambda to also catch broadcast observers. - return ((designator == client) && ((index.empty() == true) || (index == serviceCallsign))); - }); + Exchange::Controller::JLifeTime::Event::StateChange(*this, {}, serviceCallsign, service->State(), service->Reason(), client); } } _adminLock.Unlock(); } - else { - Core::ProxyType service = FromIdentifier(callsign); + else if (callsign.Value().empty() == false) { + Core::ProxyType service = FromIdentifier(callsign.Value()); if ((service.IsValid() == true) && (service->State() == PluginHost::IShell::state::ACTIVATED)) { const string serviceCallsign = service->Callsign(); - Exchange::Controller::JLifeTime::Event::StateChange(*this, {}, {}, service->State(), service->Reason(), - [&client, &serviceCallsign](const string& designator, const string& index) { - // Custom sendif lambda to also catch broadcast observers. - return ((designator == client) && ((index.empty() == true) || (index == serviceCallsign))); - }); + Exchange::Controller::JLifeTime::Event::StateChange(*this, serviceCallsign, {}, service->State(), service->Reason(), client); } } } - void Controller::SendInitialStateControlSnapshot(const string& client, const string& callsign) + void Controller::SendInitialStateControlSnapshot(const string& client, const Core::OptionalType& callsign) { - if (callsign.empty() == false) { - Core::ProxyType service = FromIdentifier(callsign); + if ((callsign.IsSet() == true) && (callsign.Value().empty() == false)) { + Core::ProxyType service = FromIdentifier(callsign.Value()); if (service.IsValid() == true) { PluginHost::IStateControl* control = service->QueryInterface(); diff --git a/Source/Thunder/Controller.h b/Source/Thunder/Controller.h index ba4af8f2cc..30f6476def 100644 --- a/Source/Thunder/Controller.h +++ b/Source/Thunder/Controller.h @@ -329,13 +329,13 @@ namespace Plugin { Core::hresult Subsystems(ISubsystems::ISubsystemsIterator*& outSubsystems) const override; // JSONRPCSupportsEventStatus overrides - void OnStateChangeEventRegistration(const string& client, const string& index, const PluginHost::JSONRPCSupportsEventStatus::Status status) override + void OnStateChangeEventRegistration(const string& client, const Core::OptionalType& index, const PluginHost::JSONRPCSupportsEventStatus::Status status) override { if (status == PluginHost::JSONRPCSupportsEventStatus::Status::registered) { SendInitialStateSnapshot(client, index); } } - void OnStateControlStateChangeEventRegistration(const string& client, const string& index, const PluginHost::JSONRPCSupportsEventStatus::Status status) override + void OnStateControlStateChangeEventRegistration(const string& client, const Core::OptionalType& index, const PluginHost::JSONRPCSupportsEventStatus::Status status) override { if (status == PluginHost::JSONRPCSupportsEventStatus::Status::registered) { SendInitialStateControlSnapshot(client, index); @@ -397,8 +397,8 @@ namespace Plugin { Core::ProxyType DeleteMethod(Core::TextSegmentIterator& index, const Web::Request& request); void StartupResume(const string& callsign, PluginHost::IShell* plugin); void NotifyStateChange(const string& callsign, const PluginHost::IShell::state& state, const PluginHost::IShell::reason& reason); - void SendInitialStateSnapshot(const string& client, const string& callsign); - void SendInitialStateControlSnapshot(const string& client, const string& callsign); + void SendInitialStateSnapshot(const string& client, const Core::OptionalType& callsign); + void SendInitialStateControlSnapshot(const string& client, const Core::OptionalType& callsign); private: Core::CriticalSection _adminLock; diff --git a/Source/Thunder/doc/ControllerPlugin.md b/Source/Thunder/doc/ControllerPlugin.md index 3bddd6c55a..af97c316e6 100644 --- a/Source/Thunder/doc/ControllerPlugin.md +++ b/Source/Thunder/doc/ControllerPlugin.md @@ -1642,7 +1642,7 @@ If registered for empty callsign, notifications for all services will be sent. | params | object | mandatory | *...* | | params?.callsign | string | optional | Plugin callsign | | params.state | string | mandatory | New state of the plugin (must be one of the following: *Activated, Deactivated, Unavailable*) | -| params.reason | string | mandatory | Reason for state change (must be one of the following: *Automatic, Conditions, Failure, InitializationFailed, MemoryExceeded, Requested, Shutdown, Startup, WatchdogExpired*) | +| params.reason | string | mandatory | Reason for state change (must be one of the following: *Automatic, Conditions, Failure, InitializationFailed, InstantiationFailed, MemoryExceeded, Requested, Shutdown, Startup, WatchdogExpired*) | ### Example diff --git a/Source/plugins/IController.h b/Source/plugins/IController.h index 54d59e8e33..a2c65de52b 100644 --- a/Source/plugins/IController.h +++ b/Source/plugins/IController.h @@ -116,7 +116,7 @@ namespace Controller { // @param callsign: Plugin callsign (e.g. Messenger) // @param state: New state of the plugin // @param reason: Reason for state change - virtual void StateChange(const Core::OptionalType& callsign /* @index */, const PluginHost::IShell::state& state, const PluginHost::IShell::reason& reason) = 0; + virtual void StateChange(const string& callsign, const PluginHost::IShell::state& state, const PluginHost::IShell::reason& reason) = 0; // @statuslistener // @brief Notifies of a plugin state change controlled by IStateControl @@ -124,11 +124,11 @@ namespace Controller { // @param callsign: Plugin callsign (e.g. Messenger) // @param state: New state of the plugin // @param reason: Reason for state change - virtual void StateControlStateChange(const Core::OptionalType& callsign /* @index */, const state& state) = 0; + virtual void StateControlStateChange(const string& callsign, const state& state) = 0; }; - virtual Core::hresult Register(INotification* sink, const Core::OptionalType& callsign) = 0; - virtual Core::hresult Unregister(INotification* sink, const Core::OptionalType& callsign) = 0; + virtual Core::hresult Register(INotification* sink, const Core::OptionalType& callsign /* @index */) = 0; + virtual Core::hresult Unregister(INotification* sink, const Core::OptionalType& callsign /* @index */) = 0; // @brief Activates a plugin // @details Use this method to activate a plugin, i.e. move from Deactivated, via Activating to Activated state. From e2d628b43a9aacd2c5a44cf9c230555b551ff196 Mon Sep 17 00:00:00 2001 From: MFransen69 <39826971+MFransen69@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:27:53 +0100 Subject: [PATCH 30/64] [Core] process_t is not deprecated (#2011) --- Source/core/ProcessInfo.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/core/ProcessInfo.h b/Source/core/ProcessInfo.h index 7f862c42c3..7b06c0fb53 100644 --- a/Source/core/ProcessInfo.h +++ b/Source/core/ProcessInfo.h @@ -26,7 +26,7 @@ namespace Thunder { namespace Core { - typedef DEPRECATED pid_t process_t; + typedef pid_t process_t; class EXTERNAL ProcessInfo { public: From 28e71c596f00a4b4149f887202dc29ae57c1ec58 Mon Sep 17 00:00:00 2001 From: Karthick Somasundaresan Date: Fri, 21 Nov 2025 19:26:45 +0530 Subject: [PATCH 31/64] Reverting to original Closed logic (#2013) We are reverting to the original logic in Closed as we missed a scenario where the socket might be entering listening state during state change. Co-authored-by: Pierre Wielders --- Source/core/SocketPort.cpp | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/Source/core/SocketPort.cpp b/Source/core/SocketPort.cpp index 84de021a43..a12fd1bead 100644 --- a/Source/core/SocketPort.cpp +++ b/Source/core/SocketPort.cpp @@ -1306,22 +1306,29 @@ namespace Thunder { StateChange(); - DestroySocket(m_Socket); - ResourceMonitor::Instance().Unregister(*this); + m_State &= (~SHUTDOWN); - // Remove socket descriptor for UNIX domain datagram socket. - if ((m_LocalNode.Type() == NodeId::TYPE_DOMAIN) && - ((m_SocketType == SocketPort::LISTEN) || (SocketMode() != SOCK_STREAM)) && - !m_SystemdSocket) { - TRACE_L1("CLOSED: Remove socket descriptor %s", m_LocalNode.HostName().c_str()); -#ifdef __WINDOWS__ - _unlink(m_LocalNode.HostName().c_str()); -#else - unlink(m_LocalNode.HostName().c_str()); -#endif + // In StateChange, the socket may get destroyed and recreated and moved to + // listening state. In such scenario, m_State will not be 0. + if (m_State != 0) { + result = false; + } else { + DestroySocket(m_Socket); + ResourceMonitor::Instance().Unregister(*this); + + // Remove socket descriptor for UNIX domain datagram socket. + if ((m_LocalNode.Type() == NodeId::TYPE_DOMAIN) && + ((m_SocketType == SocketPort::LISTEN) || (SocketMode() != SOCK_STREAM)) && + !m_SystemdSocket) { + TRACE_L1("CLOSED: Remove socket descriptor %s", m_LocalNode.HostName().c_str()); + #ifdef __WINDOWS__ + _unlink(m_LocalNode.HostName().c_str()); + #else + unlink(m_LocalNode.HostName().c_str()); + #endif + } } - m_State &= (~SHUTDOWN); ASSERT (m_State == 0); From d57c0345bb14f2c0c1a845786f6e349bf7a9da47 Mon Sep 17 00:00:00 2001 From: MFransen69 <39826971+MFransen69@users.noreply.github.com> Date: Sat, 22 Nov 2025 16:02:42 +0100 Subject: [PATCH 32/64] [Controller] Correct resume error message (#2014) --- Source/Thunder/Controller.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Thunder/Controller.cpp b/Source/Thunder/Controller.cpp index e4cbb24fa8..2485d66f10 100644 --- a/Source/Thunder/Controller.cpp +++ b/Source/Thunder/Controller.cpp @@ -1103,7 +1103,7 @@ namespace Plugin { PluginHost::IStateControl* stateControl = service->QueryInterface(); if (stateControl == nullptr) { - result = Core::ERROR_UNAVAILABLE; + result = Core::ERROR_NOT_SUPPORTED; } else { result = stateControl->Request(PluginHost::IStateControl::command::RESUME); From f4a59f2138f40182ae610fa7e4890c2c2e437ebb Mon Sep 17 00:00:00 2001 From: MFransen69 <39826971+MFransen69@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:19:38 +0100 Subject: [PATCH 33/64] [Core] Type in comment (#2021) --- Source/core/ICustomErrorCode.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/core/ICustomErrorCode.h b/Source/core/ICustomErrorCode.h index 9c2b0cbb9a..72b306a8ef 100644 --- a/Source/core/ICustomErrorCode.h +++ b/Source/core/ICustomErrorCode.h @@ -18,7 +18,7 @@ */ /* - This file contains the interface that a library can implement in case the "custom error codes" feature is used in Thunder and code to string comversion is desired + This file contains the interface that a library can implement in case the "custom error codes" feature is used in Thunder and code to string conversion is desired */ #pragma once From 44f84d0a418d3095d3b7a017c5bd6ec07952c425 Mon Sep 17 00:00:00 2001 From: Pierre Wielders Date: Tue, 25 Nov 2025 15:57:01 +0100 Subject: [PATCH 34/64] Revert "[IPCConnector] Make sure the connection failure is properly detected (#1909)" This reverts commit 0f614835ce252384995dc7e030096c1ae4bc7376. --- Source/core/IPCConnector.h | 70 ++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/Source/core/IPCConnector.h b/Source/core/IPCConnector.h index b8883b7ad4..b38306522d 100644 --- a/Source/core/IPCConnector.h +++ b/Source/core/IPCConnector.h @@ -591,10 +591,8 @@ POP_WARNING() return (result); } - inline bool Flush() + void Flush() { - bool result = false; - _lock.Lock(); TRACE_L1("Flushing the IPC mechanims. %d", __LINE__); @@ -603,16 +601,12 @@ POP_WARNING() if (_outbound.IsValid() == true) { _outbound.Release(); - result = true; } if (_inbound.IsValid() == true) { _inbound.Release(); - result = true; } _lock.Unlock(); - - return (result); } inline ProxyType ReceivedMessage(const Core::ProxyType& rhs, Core::ProxyType& inbound) @@ -668,16 +662,29 @@ POP_WARNING() _lock.Unlock(); } - inline void Abort() + bool AbortOutbound() { + bool result = false; + _lock.Lock(); - if (_callback != nullptr) { - _callback->Dispatch(*_outbound); - _callback = nullptr; + if (_outbound.IsValid() == true) { + + result = true; + + if (_callback != nullptr) { + _callback->Dispatch(*_outbound); + _callback = nullptr; + } + + _outbound.Release(); + } else { + ASSERT(_callback == nullptr); } _lock.Unlock(); + + return (result); } private: @@ -725,6 +732,10 @@ POP_WARNING() _administration.Unregister(id); } + void Abort() + { + _administration.AbortOutbound(); + } template inline uint32_t Invoke(const ProxyType& command, IDispatchType* completed) { @@ -824,7 +835,8 @@ POP_WARNING() { if (_parent.Source().IsOpen() == false) { // Whatever s hapening, Flush what we were doing.. - _factory.Abort(); + _parent.Abort(); + _factory.Flush(); } _parent.StateChange(); @@ -854,10 +866,10 @@ POP_WARNING() // Now we wait for ever, to get a signal that we are done :-) if (_signal.Lock(waitTime) != Core::ERROR_NONE) { - _administration.Flush(); + _administration.AbortOutbound(); result = Core::ERROR_TIMEDOUT; - } else if (_administration.Flush() == true) { + } else if (_administration.AbortOutbound() == true) { result = Core::ERROR_ASYNC_FAILED; } @@ -993,21 +1005,15 @@ POP_WARNING() if (_administration.InProgress() == true) { success = Core::ERROR_INPROGRESS; - } - else { + } else if (_link.IsOpen() == true) { // We need to accept a CONST object to avoid an additional object creation // proxy casted objects. _administration.SetOutbound(command, completed); - if (_link.IsOpen() == true) { - _link.Submit(command->IParameters()); - - success = Core::ERROR_NONE; - } else { - _administration.Flush(); + // Send out the + _link.Submit(command->IParameters()); - success = Core::ERROR_CONNECTION_CLOSED; - } + success = Core::ERROR_NONE; } _serialize.Unlock(); @@ -1020,22 +1026,18 @@ POP_WARNING() _serialize.Lock(); - IPCTrigger sink(_administration); + if (_link.IsOpen() == true) { + IPCTrigger sink(_administration); - // We need to accept a CONST object to avoid an additional object creation - // proxy casted objects. - _administration.SetOutbound(command, &sink); + // We need to accept a CONST object to avoid an additional object creation + // proxy casted objects. + _administration.SetOutbound(command, &sink); - if (_link.IsOpen() == true) { + // Send out the _link.Submit(command->IParameters()); success = sink.Wait(waitTime); } - else { - _administration.Flush(); - - success = Core::ERROR_CONNECTION_CLOSED; - } _serialize.Unlock(); From 08dedbc2315e487ece7a4ae475ea7616a24e8648 Mon Sep 17 00:00:00 2001 From: nxtum <94901881+nxtum@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:19:18 +0100 Subject: [PATCH 35/64] PSG Documentation for 5.3 (#2017) * PSG documentation * fix grammar --------- Co-authored-by: MFransen69 <39826971+MFransen69@users.noreply.github.com> --- .../devtools/pluginskeletongenerator.md | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/docs/plugin/devtools/pluginskeletongenerator.md b/docs/plugin/devtools/pluginskeletongenerator.md index 65de07e11e..c2763fa6fc 100644 --- a/docs/plugin/devtools/pluginskeletongenerator.md +++ b/docs/plugin/devtools/pluginskeletongenerator.md @@ -4,24 +4,22 @@ Please note that the tool is in beta release at the moment. Although it was very Some remarks: -* Although it already supports quite a number of different plugin scenario’s/configurations it will not support everything you can think of in a plugin, that it will never do. So if you cannot do it with the PSG does not mean Thunder cannot do it, you just probably need to add/change code by hand. On the other hand the PSG will be extended and will support more common plugin options in the future. +* Although it already supports quite a number of different plugin scenario’s/configurations it will not support everything you can think of in a plugin, that it will never do. So if you cannot do it with the PSG, it does not mean Thunder cannot do it, you just probably need to add/change code by hand. On the other hand the PSG will be extended and will support more common plugin options in the future. * At the moment the PSG will generate Thunder 5 compliant code. * Start the PSG in the folder where you want a subfolder to be created for your new plugin (so do not start it from the PSG folder itself). * The PSG does already support a plugin implementing more than one interface. -* At the moment the PSG does not yet parse the header file for the interface(s) you want your plugin to implement, this is planned for a future release. So it will not generate the correct methods etc, but for now only some example methods and you will have to update manually to reflect your interface(s). +* The generator now has the ability to parse the header file for the interface(s) you want your plugin to implement. +* The parser checks for tags such as @json to generate the correct code required to make the JSONRPC interface work automatically (add required libraries, interface, registration code, notification listeners of events are used etc.). If the interface is found to be JSONRPC, the PSG will also generate the code for COMRPC (Register and Unregister) if appropriate. * The PSG will generate a default text for the License (Apache License 2.0) in all the code files with a placeholder for the organization. Please update this after generating the code. How to use the Plugin Skeleton Generator: -* Start the PSG from the folder in which you want your plugin to be generated (it will be created in its own subfolder of course) or start it from the ThuderDevTools menu. +* Start the PSG from the folder in which you want your plugin to be generated (it will be created in its own subfolder of course) or start it from the ThunderDevTools menu. * You first will be asked how you want to name your plugin (this will be used for the subfolder, classnames, callsign etc.). * Then you will be asked if the plugin needs to be able to run OOP, select the desired option. -* Now you will be given the option to define the interface(s) your plugin will implement, the PSG already supports generating a skeleton for a plugin that does implement more then one. It will first ask if you want do define an interface at all. The PSG does support a plugin that does not implement an interface at all (except IPlugin of course), but this is logically only available for an in process plugin. Press enter to start defining interfaces, press q if you don't want to define an interface at all. -* Following it will ask in which subfolder of the include path the interfaces your plugin will implement are located (default is interfaces, ) so in that case you just can press ENTER or otherwise add the path you want to use in the generated code. Please note that this will only influence the path the PSG will use in the generated code to include the interface from, it might be needed to adjust the plugin CMake file if your interfaces are not located in ThunderInterfaces. -* For an interface first it will ask for the header file of the interface the plugin will implement. This is on purpose because if we in a future version of the PSG actually scan the file we do no longer have to ask a number of the question that will follow and also the generated code will match the interface better (as we then know exactly what methods the interface expects to be overridden. But at the moment it will not parse the header file yet). -* Then it will guess the name of the Interface the header file provides (at the moment only one interface per header file supported) but you can override it if it is different. -* Then it will ask if the interface does also have notifications (note not only relevant for JSONRPC it will also generate code for COMRPC, e.g. register and unregister code, and required containers). But if the answer is yes it will assume the JSONRPC interface will have an @event for this notification and the PSG will generate the code for that as well -* Following that it will ask if this interface also exposes a JSONRPC interface (so if it has a @json tag). Of course if applicable it will generate all code required to make the JSONRPC interface work automatically (add required libraries, interface, registration code, notification listeners of events are used etc.). -* After that it will ask for more subsequent interfaces, just press 'q' to stop adding interfaces. -* Finally it will ask if the plugin has plugin specific configuration. If answered positive (example)code will be added on how that can be provided to the plugin configuration file from the build and also how it can be parsed and handled in the plugin itself (in an OOP plugin it is assumed it need to be dealt with in the OOP part as that is the most complex case). -* Now an overview of the desired plugin options is generated and the plugin code generated. +* Next, it will ask if the plugin has plugin specific configuration. If answered positive, example code will be added on how that can be provided to the plugin configuration file from the build, and also how it can be parsed and handled in the plugin itself (in an OOP plugin it is assumed it needs to be dealt with in the OOP part as that is the most complex case). +* Now you will be given the option to define the interface(s) your plugin will implement, the PSG already supports generating a skeleton for a plugin that does implement more than one. The PSG also supports a plugin that does not implement an interface at all (except IPlugin of course), but this is logically only available for an in process plugin. Add the full path to your interface and press enter to define an interface. Press ENTER on an empty string to stop defining interfaces. +* If your file contains more than one interface at the root level, you will be asked to specify which interface(s) you would like to use. +* Following this, it will ask whether your plugin relies on Thunder subsystems (Preconditions, Terminations, Controls). +* Afterwards, it will ask in which subfolder of the include path the interfaces your plugin will implement are located (default is interfaces, ) so in that case you just can press ENTER or otherwise include the custom location you want to use in the generated code. Please note that this will only influence the path the PSG will use in the generated code to include the interface from, it might be needed to adjust the plugin CMake file if your interfaces are not located in ThunderInterfaces. +* Now an overview of the desired plugin options is displayed, and the plugin code is generated. \ No newline at end of file From 2f208a9c15d1234a0afb84b6fe0a3551532f7b3b Mon Sep 17 00:00:00 2001 From: sebaszm <45654185+sebaszm@users.noreply.github.com> Date: Tue, 25 Nov 2025 00:10:59 +0100 Subject: [PATCH 36/64] [Controller] Update docs (#2022) --- Source/Thunder/doc/ControllerPlugin.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Source/Thunder/doc/ControllerPlugin.md b/Source/Thunder/doc/ControllerPlugin.md index af97c316e6..c91c427cbb 100644 --- a/Source/Thunder/doc/ControllerPlugin.md +++ b/Source/Thunder/doc/ControllerPlugin.md @@ -225,7 +225,7 @@ This method will return *True* for the following methods/properties: *environmen "id": 42, "method": "Controller.1.exists", "params": { - "method": "environment" + "method": "methodName" } } ``` @@ -279,7 +279,7 @@ This method supports the following event names: *[statechange](#notification_sta "id": 42, "method": "Controller.1.register", "params": { - "event": "statechange", + "event": "eventName", "id": "myapp" } } @@ -334,7 +334,7 @@ This method supports the following event names: *[statechange](#notification_sta "id": 42, "method": "Controller.1.unregister", "params": { - "event": "statechange", + "event": "eventName", "id": "myapp" } } @@ -1633,7 +1633,7 @@ If registered for empty callsign, notifications for all services will be sent. ### Parameters -> The *callsign* parameter shall be passed as index to the ``register`` call, i.e. ``register@``. +> The *callsign* parameter is optional. If set it shall be passed as index to the ``register`` call, i.e. ``register@``. ### Notification Parameters @@ -1676,6 +1676,8 @@ If registered for empty callsign, notifications for all services will be sent. > The *client ID* parameter is passed within the notification designator, i.e. ``.statechange@``. +> The *callsign* parameter is optionally passed as index within the notification designator, i.e. ``.statechange@``. + ## *statecontrolstatechange [notification](#head_Notifications)* @@ -1689,7 +1691,7 @@ If registered for empty callsign, notifications for all services will be sent. ### Parameters -> The *callsign* parameter shall be passed as index to the ``register`` call, i.e. ``register@``. +> The *callsign* parameter is optional. If set it shall be passed as index to the ``register`` call, i.e. ``register@``. ### Notification Parameters @@ -1730,6 +1732,8 @@ If registered for empty callsign, notifications for all services will be sent. > The *client ID* parameter is passed within the notification designator, i.e. ``.statecontrolstatechange@``. +> The *callsign* parameter is optionally passed as index within the notification designator, i.e. ``.statecontrolstatechange@``. + ## *subsystemchange [notification](#head_Notifications)* From 78c1ad7a00ba0a7d9da587a693c7a7751735e85a Mon Sep 17 00:00:00 2001 From: MFransen69 <39826971+MFransen69@users.noreply.github.com> Date: Tue, 25 Nov 2025 00:27:22 +0100 Subject: [PATCH 37/64] [Core] another typo in the header file... (#2024) --- Source/core/ICustomErrorCode.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/core/ICustomErrorCode.h b/Source/core/ICustomErrorCode.h index 72b306a8ef..db5b427f83 100644 --- a/Source/core/ICustomErrorCode.h +++ b/Source/core/ICustomErrorCode.h @@ -47,7 +47,7 @@ extern "C" { #endif // called from within Thunder to get the string representation for a custom code -// note parameter code is the pure (signed) Custom Code passed to Thunder. no additional bits set (so for signed numbers 32th bit used as sign bit). +// note parameter code is the pure (signed) Custom Code passed to Thunder. no additional bits set (so for signed numbers 32nd bit used as sign bit). // in case no special string representation is needed return nullptr (NULL), in that case Thunder will convert the Custom Code to a generic message itself EXTERNAL const TCHAR* CustomCodeToString(const int32_t code); From 892d4fc8e102f07873782239cb18290ba2f55b88 Mon Sep 17 00:00:00 2001 From: Karthick Somasundaresan Date: Tue, 25 Nov 2025 18:47:30 +0530 Subject: [PATCH 38/64] Removing unwanted assert (#2025) --- Source/core/SocketPort.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/Source/core/SocketPort.cpp b/Source/core/SocketPort.cpp index a12fd1bead..1bad691130 100644 --- a/Source/core/SocketPort.cpp +++ b/Source/core/SocketPort.cpp @@ -1330,8 +1330,6 @@ namespace Thunder { } - ASSERT (m_State == 0); - m_syncAdmin.Unlock(); return (result); From 7e4b93c6761bda3bf2b6b77ed1fdc691de9b30dd Mon Sep 17 00:00:00 2001 From: MFransen69 <39826971+MFransen69@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:41:51 +0100 Subject: [PATCH 39/64] Development/53releasenotes (#2015) * [Doc] 5.3 Release notes first draft * [ReleaseNotes] 5.3 update * [Docs] update * [Docs] more release documentation * [Docs] More release info * [Docs] add versios function * [Docs] even more documentation * [Docs] add Custom Codes * [Docs] more updates to release notes * Update permissions for documentation preview workflow * Change permissions to read for contents * Fix spelling issue in config.md * [Docs] review comments incorporated * [Docs] update for 5.3 releasae * [Docs] some small changes * [Docs] update indexed events with own lambda override --------- Co-authored-by: Mateusz Daniluk <121170681+VeithMetro@users.noreply.github.com> --- ReleaseNotes/ThunderReleaseNotes_R5.3.md | 378 +++++++++++++++++++++++ docs/introduction/config.md | 3 + docs/plugin/config.md | 1 + docs/plugin/interfaces/interfaces.md | 319 ++++++++++++++++++- docs/plugin/interfaces/tags.md | 97 +++++- docs/utils/customcodes.md | 224 ++++++++++++++ mkdocs.yml | 1 + 7 files changed, 1013 insertions(+), 10 deletions(-) create mode 100644 ReleaseNotes/ThunderReleaseNotes_R5.3.md create mode 100644 docs/utils/customcodes.md diff --git a/ReleaseNotes/ThunderReleaseNotes_R5.3.md b/ReleaseNotes/ThunderReleaseNotes_R5.3.md new file mode 100644 index 0000000000..1ed573da46 --- /dev/null +++ b/ReleaseNotes/ThunderReleaseNotes_R5.3.md @@ -0,0 +1,378 @@ +# Thunder Release Notes R5.3 + +## Introduction + +This document describes the new features and changes introduced in Thunder R5.3 (compared to the latest R5.2 release). +See [here](https://github.com/rdkcentral/Thunder/blob/master/ReleaseNotes/ThunderReleaseNotes_R5.2.md) for the release notes of Thunder R5.2. +This document describes the changes in Thunder, ThunderTools and ThunderInterfaces as they are related. + +# Thunder + +## Process Changes and new Features + +### Feature: Windows build artifacts cleanup + +The Windows build (Visual Studio Projects) now fully cleans up the build artifacts when the solution is cleaned guaranteeing a complete fresh build after cleaning. + +### Feature: Scripts before Thunder start in CMake + +A feature was added to the Thunder CMake files to enable the inclusion of scripts to be executed before Thunder is started by the (Linux) startup script. These scripts should be placed [here](https://github.com/rdkcentral/Thunder/tree/master/Source/Thunder/scripts) and see there as well for some examples. + +### Feature: Actions added that build with Performance Monitoring feature on + +The GitHub Actions workflow now includes a build with the Performance Monitoring feature enabled (this adds code to Thunder that enables a Performance Monitor plugin to measures JSON-RPC performance) so pull requests are checked for regressions in this area. + +### Feature: Copilot scanning enabled and issues fixed + +Copilot is now enabled for all Thunder repositories alongside Coverity. The few remarks found when by Copilot have been addressed. + +## Changes and new Features + +## Feature: WorkerPool PriorityQueue + +The Thunder WorkerPool (and therefore the ThreadPool as well) now has the ability to use priorities for its jobs. Three priorities are available, high medium and low. +In the Thunder config the total number of WorkerPool threads used by Thunder itself, as well as the thresholds of maximum parallel scheduled jobs for medium and low priority jobs can be configured (note if not overridden here the already existing THREADPOOL_COUNT is still taken into account). +See more info [here](https://rdkcentral.github.io/Thunder/introduction/config/). + +The scheduling algorithm used by the Thunder WorkerPool/ThreadPool can be compile time configured to be either static or dynamic. With static only the maximum number of jobs for that priority will be scheduled in parallel, with dynamic additionally the total load of the system is also taken into account. +When both static and dynamic are disabled (at compile time) the previous non priority solution will be used. + +Currently, only low and high priority jobs are used in Thunder. If a COM-RPC request is received from a channel that already has an outgoing request (IPC, not in-process), the job to handle the incoming request is submitted with high priority. This mitigates deadlocks seen previously when the thread pool is filled with COM-RPC jobs that require callbacks into Thunder before they complete. Note: if returning COM-RPC calls keep spawning out-of-process calls, you may still exhaust WorkerPool threads, depending on the configured thread count. + +For this reason, it is advised to configure the low-priority threshold to be at least one less than the total number of worker pool threads to ensure at least one thread is always available for high-priority jobs. + +In case of an incorrect configuration, e.g. medium and/or low thresholds higher than the total amount of threads will trigger an ASSERT. + +As this is a relatively new but potentially complex feature, the Thunder documentation will be extended with a more detailed description than the above. + +### Feature: Custom (Error) Codes. + +Before Thunder 5.3 error codes used in Thunder were predefined, which sufficed up to now. These error codes (due to the "JSON-RPC in terms of COM-RPC" feature) are also translated into JSON-RPC error codes and there was a request to support a more flexible error scheme (mainly to have more flexibility in the error codes reported in the JSON-RPC error object). +So with this new Custom Codes feature a mechanism was introduced to have custom error codes that are consistent for both COM-RPC and JSON-RPC (or any other future protocol that will be implemented in terms of COM-RPC), allow for custom (not hardcoded inside Thunder) code to string translation and allow for direct influence on the error code returned by JSON-RPC. + +For more info see [here]() in the Thunder documentation. + +NOTE self: add link above + +### Feature: Warning reporting for WebSocket open and close + +We now have added a WarningReporting message when the opening and/or closing of a WebSocket takes longer than an (configurable as always with WarningReporting) minimum amount of time. + +### Change: Open COM-RPC port later + +We moved the opening of the COM-RPC port to later in the startup of Thunder to prevent issues from clients trying to access it too soon. + +### Feature: make Thunder uClibc compatible + +Changes were made to the Thunder code to make it uClibc compatible. + +### Feature: JSON-RPC FlowControl configurable + +The JSON-RPC FlowControl feature (see the the Thunder 5.1 release notes for more info) is now configurable from the Thunder configuration file and plugin configuration file. + +With "channel_throttle" in the Thunder configuration file one can configure the maximum number of parallel JSON-RPC requests allowed for any channel. +With "throttle" in the Thunder configuration file one can configure the maximum number of parallel JSON-RPC requests allowed to a particular plugin. +The default for both is half the number of workerpool threads (with a minimum of one). + +With the option "throttle" in the plugin configuration file one can for this specific plugin override the generic value set in the Thunder configuration file for the maximum number of parallel JSON-RPC requests allowed. + +### Feature: WarningReporting for JSON-RPC FlowControl + +We now have added a WarningReporting message when the FlowControl feature (see above and in the Thunder 5.1 release notes) leads to longer than configured handling times. + +### Change: COMRPCTerminator thread only started when needed + +The COM-RPC terminator thread (a cleanup thread used within Thunder) is now only started when required, leading to one less thread in processes that do not need it (like ThunderPlugin) + +### Feature: allow per callsign registration IShell + +When registering for plugin state notifications on the IShell interface over COM-RPC, it is now also possible to subscribe to state changes for a single plugin by optionally providing its callsign, next to subscribing to state changes for all plugins. +This change was made in a way that is backwards compatible with existing code. + +Note the applicable SmartInterfaceTypes (which use the IPlugin::INotification internally) have been adopted to use this feature reducing COM-RPC traffic and overhead. + +### Feature: allow per callsign state change notifications in IController and improvements + +The IController state notifications have been improved (so this also affects the JSON-RPC plugin state notifications). + +It is now possible to register only to be updated for the state changes of a specific plugin instead of state changes of all plugins, while the latter of course is also still possible, by optionally providing the callsign of the plugin for which one wants to receive the notifications. +This also applies to the JSON-RPC interface, for details here best to consult the generated documentation for the IController interface available in the Thunder repository. + +StateChange (which notifies of changes to plugin state that do not includes Plugin::IStateControl) will, when registering, send a notification for all plugins already activated at that moment (when registering to be notified for all plugin state changes). If registering for a specific plugin it will send a Activated notification in case that specific plugin was already activated at the moment of registering. + +StateControlStateChange (which notifies of changes to plugin state from Plugin::IStateControl, so suspended and resumed) will when registering for notifications for a specific plugin immediately notify about the current state for that plugin and send no current state notification when registering for all plugins. + +### Feature: MAC Address class + +A class was added to Thunder core to represent a MAC Address (and as noted below is also fully supported by the Thunder code generators) +The class can be found [here](https://github.com/rdkcentral/Thunder/blob/d57c0345bb14f2c0c1a845786f6e349bf7a9da47/Source/core/MACAddress.h#L29). + +### Feature: Enable to reach remote plugin via the Composite plugin feature + +Thunder now fully supports accessing a Plugin living in a remote Thunder instance to be made accessible in the local Thunder instance using the Composite plugin feature (see also the new BridgeLink plugin in the ThunderNanoServicesRDK repository, read the release notes for that here NOTE hier nog link toevoegen) + +The ThunderUI has also been updated to support this feature and will now also show the remote plugins now. + +### Feature: version handling for distributed plugins + +The plugin version handling now takes the Composite plugin into account. One can now use versioned methods also for the "composited" plugin by putting the version after the composite plugin delimiter. + +### Feature: metadata loading configurable + +One can now disable the loading of Plugin metadata (plugin version, subsystem being controlled and dependencies) with the Thunder configuration flag "discovery". With this flag set to true (default) means loading the metadata is enabled. +When disabled this means this information will not be loaded before the plugin is actually activated making some features of Thunder not work correctly. +This feature was added on request because, on some devices, loading the library to read metadata took much longer than normal. So only set this flag to false in special circumstances. + +### Feature: Forgiving JSON-RPC method Pascal/CamelCase handling + +A build option was introduced to enable "forgiving" JSON-RPC method casing handling in Thunder. When turned on (disabled by default) Thunder will accept the method name both as camelCase and PascalCase (to be more forgiving as method name casing was not always done consistently in interfaces). +The build option is called ENABLE_JSONRPC_FORGIVING_METHOD_CASE_HANDLING. + +### Change: ICOMLink::INotification Revoked notification + +When in the past the ICOMLink::INotification Revoked method was renamed to Dangling it was not removed or made deprecated and now might also confuse people with the Revoked notification when an offered interface is revoked from COM-RPC communicator. +It is now made deprecated and should in this context not be used anymore (it cannot yet be removed as that would break plugins that implement ICOMLink::INotifcation). When used make sure only Dangling is handled from this notification, Revoked will be removed in Thunder 6. + +### Feature: Syslog on COM-RPC timeout + +When a COM-RPC call is canceled because it takes longer than the configured COM-RPC timeout this is now logged as a Syslog message to help the investigation of situations where this happens. +Note: in this case we now also close the connection as can be read above. + +### Change: Improvements to Library and Service Discovery handling + +The way Thunder loads the available services from Plugin libraries was improved in general (preventing deadlocks for example which were seen in rare cases by our QA department). +This now also makes it possible to have services with a duplicate name which previously would have led to issues. + +### Change: Activate reason + +The reporting of the failure for a plugin to activate has been improved and an additional failure state was added called "INSTANTIATION_FAILED" if instantiating the plugin before the IPlugin::Initialize is called failed (which would be a very rare error). + +### Feature: Logging second invoke on COM-RPC channel + +Now a message is printed to Syslog when a second COM-RPC invocation is made to a channel warning about this possible deadlock (so in an IPC situation, not in process). This to help in the investigation of these plugin issues. + +### Feature: SinkType now has WaitReleased + +The SinkType now has a WaitReleased method. This can be used in the special situation when a Service is put into the SinkType but there can be a possible race condition between closing the channel(s) from which the Service is used and receiving the actual Release call(s) from the using side(s). +With WaitReleased the server side can now wait for the Release() to be received from the client side(s), with timeout. +See for a scenario in which this is useful [here](https://github.com/rdkcentral/ThunderPluginActivator/blob/89f789624305f4473fb5b605e8ae3cc4fa39fd15/source/COMRPCStarter.cpp#L94) and for the actual call to WaitReleased [here](https://github.com/rdkcentral/ThunderPluginActivator/blob/89f789624305f4473fb5b605e8ae3cc4fa39fd15/source/COMRPCStarter.cpp#L152). + +### Change: Improved dead proxies handling + +Previously calling a COM-RPC method (over an IPC channel) from the (PluginServer) ICOMLink::INotification Dangling notification could lead to a possible deadlock this has been fixed now. +This means that all Dangling handling code (so both in PluginServer as well as CommunicatorServer) is now safe for this usage pattern. + +### Change: THUNDER_PERFORMANCE code made to work again + +The Thunder code using the THUNDER_PERFORMANCE has been corrected so it is functional again (this enables code in Thunder that can be used by a special plugin developed in the past to do JSON-RPC performance measurements) +The flag has a generic name so that future possible performance measurement code can also be put inside this flag. + +### Change: Connector timeout can be specified. + +It is now possible to set the timeout to be used in the SmartLinkType connect and disconnect (before it was always Core::infinite so it would wait indefinitely for the event to happen) + +### Change: General bug fixes + +A number of issues found in Thunder 5.2 have been corrected in Thunder 5.3. + +## Breaking Changes + +In principle Thunder 5.3 does not contain breaking changes. Some changes alter previous behavior but are not expected to cause any backward compatibility issues; they are listed below. + +### Feature: close channel on COM-RPC timeout + +To prevent issue as a result of "collateral damage" after a COM-RPC timeout has happened we now close the channel on which the COM-RPC timeout occurred automatically. + +### Change: Error message changed for suspend and resumed when not supported + +When a Suspend or Resume request for a Plugin was received via the Thunder Controller (both COM-RPC and JSON-RPC) and the plugin does not support the PluginHost::IStateControl interface previously the error ERROR_UNAVAILABLE was reported, this has been changed to ERROR_NOT_SUPPORTED. + +### Change: improved handling when a COM-RPC string is too big + +If a COM-RPC string transferred over IPC is too big for the maximum size that was reserved with @restrict no longer the string will be capped but now this will assert when asserts are enabled in the build and otherwise no data will be sent at all to better indicate to the receiver not the full data was transferred. + +### Change: Controller Version change + +There are some small changes to the Controller interface regarding the Version method: + +The Controller Version method (to retrieve the Thunder version) has been renamed Framework in COM-RPC to prevent any confusion. For JSON-RPC "version" will still work but is made deprecated and will be removed in Thunder 6. + +### Change: Exists and Register/Unregister changes + +There are some small changes to the following generic JSON-RPC functions for a plugin: + +#### Exists + +The "exists" function (to inquire if a certain function is available on the JSON-RPC interface) is made consistent and can now (also) be used using "compliant" parameters format: +```json +params{ "name" : ""} +``` +This will now correctly return "result" : true or false. + +Note the previous format can still be used: + +```json +params : ""} +``` + +which will return a JSON-RPC error object ERROR_UNKNOWN_KEY (code 22) or "result" : null to be fully Thunder 4.4 backwards compatible. + +#### Register/Unregister + +The generic plugin JSON-RPC "register" and "unregister" functions to register for a JSON-RPC Notification have been changed to now return "result" : null if the registration/un-registration was successful and no longer return 0. +On error nothing changed (except of course when compared to Thunder 4.4 a change in the error numbers used as can be read in the Thunder 5.0 Release notes) + +### Change: TriState class removed + +The Thunder Core class TriState has been removed. It was suboptimal and nowadays alternatives exist like OptionalType. +We do not expect this to be used anywhere nor was it used in Thunder internally. + +# Thunder Tools + +## Changes and new Features + +### Feature: event indexes improvement + +Using indexes for events has been improved. +The indexes now use @< index > at the end of designator in the JSON-RPC interface. +This had to be changed as indexes could contain dots itself (e.g. if the index is a callsign) so having it before the first dot in the clientid would not suffice. +Although the old dot format was in use only for a short time in Thunder 5 the @index:deprecated can be used with an event parameter to indicate it is an index for that event and the deprecated dot format should be used. + +Indexes now also support OptionalType indexes to indicate subscribing to the index or all is in principle optional. For this to work it is now also allowed to put the @index keyword at the Register and the event is then looked up by name of the indexed parameter. +(it is also allowed and advised to also put the @index then at the Unregister) +Please see this section in the Thunder documentation that has been added to describe indexed events: NOTE: add link + +### Change: auto object lookup now requires @encode:autolookup + +The JSON-RPC auto object lookup feature now requires the @encode:autolookup tag, this for improved detection and consistency. +See more [here]() NOTE self: add link to autolookup documentation section. +Note the auto object lookup feature was introduced in Thunder 5.2 without needing @encode:autolookup to be specified (it should not be breaking as Thunder 5.2 was not used to define new object based JSON-RPC interfaces with auto object lookup). + +A new feature for the auto object lookup is that it is now possible to register callbacks to be called when objects are acquired or relinquished in case special handling is needed for JSON-RPC (generic code can be put into the COM-RPC code for Acquire and Relinquish as that is called for both cases). + +The (updated) documentation can be found [here]() NOTE: update link + +### Feature: support custom object lookup + +Next to auto object lookup the Thunder Tooling now also support custom object lookup for JSON-RPC interfaces. +Where auto lookup takes care of the creation of object ID's and linking them to the objects for you, custom lookup can be used when the objects already have a unique ID internally (e.g a unique name or number ID) that can be used. +This makes it easier for clients as the now can use a more meaningful ID instead of the abstract ID created by autolookup. +Custom lookup is indicated by specifying the @encode:lookup with the interface for the object type. + +See the Thunder documentation for more info [here]() + +NOTE: link to be included + +### Feature: support status listeners for lookup objects + +Object lookup (see above) now also supports status listeners to be used for session object creation and destruction. See the object look up section and the code generator examples referred to from there for more information on this. + +### Feature: StatusListener unregistered also on channel closed event + +When the @statuslistener is used for an event in an interface the status listener is also called with state Status::unregistered when a client explicitly unregisters from the event. +Starting 5.3 this is also done when the unregister is a result of the channel from which the client registered closing without the client calling unregister explicitly before that (note the internal cleanup inside Thunder did already happen, there were no leaks, just the statuslistener was not called when this happened). + +### Beta Feature: PSG with interface parsing + +The Plugin Skeleton Generator has been greatly extended to now parse the IDL interface file(s) you want the generated plugin code to implement and therefore generates code already completely providing all methods for the Plugin, only the implementation needs to be added! +It does generate code for both COM-RPC and JSON-RPC interfaces just by checking if the interface IDL header file indicates also specifies a JSON-RPC interface should be generated. +It has also been extended with example code for dangling proxies (if applicable for the interface it implements) and support for subsystem handling code generation. + +Note due to all the permutations possible with the interfaces it can encounter the PSG remains in beta. If you see issues or have doubts on the code it generates please contact us. + +Note: see [here](https://rdkcentral.github.io/Thunder/plugin/devtools/pluginskeletongenerator/) for the (updated) Thunder documentation on the Plugin Skeleton Generator + +### Feature: wrapped format + +The newly added wrapped tag will for a single output parameter also add the parameter name to the result, making it always a JSON object. It can also be used for arrays, std::vector, iterator etc. (see for more info [here]()) NOTE add link after documentation published +Of course it is preferable to keep the JSON-RPC interface as whole consistent but this was added as there are interface where workarounds are used to achieve the wrapped effect so having this tag will make it easier to achieve the wrapped format. + +See here for more info; + +NOTE: add link to documentation. + +### Feature: new buffer encoding options + +There is a new encoding tag supported with @encode (next to the already existing base64): + +@encode:hex will encode/decode the buffer as hex value into/from the JSON-RPC string (so works for both input and out parameters). Buffer can be an array, std::vector or buffer+len parameter with base type uint8_t or char. + +All encodings can now also be used in events. + +### Feature: New supported type: class MACAddress + +The newly added Thunder type Core::MACAddress (to hold MAC addresses) is now fully supported in both COM-RPC and JSON-RPC from the IDL header file by the code generators. + +### Feature: serialize empty non optional arrays and containers + +Previously empty arrays (and also std::vectors and iterators) and containers (if all members are optional and not set) output parameters are not serialized to the JSON-RPC output at all. +Starting 5.3 they will be included by default when empty (as [] and {}) (this on request of the Comcast Thunder user base). + +When it is desired they are not included when empty they can be made optional (with OptionalType< >(var)) in the interface, then when not set they will not be included. + +Note this is not considered breaking as both are valid JSON (and it is on specific request). + +### Feature: Allow inclusion of enum and POD from other IDL header file + +It is now possible to use an enum or POD (struct with data members) defined in one IDL header file in another by including the other header file with @insert + +### Feature: Enable enum <-> string conversion for non JSON-RPC interfaces + +Previously the enum to string (and vice versa) conversion tables were only generated when the enum was used in an interface used in JSON-RPC generation (so with @json tag). +Starting 5.3 they can be generated for COM-RPC only interfaces as well. +For this use the @encode:text tag with the enum in the COM-RPC interface. + +### Feature: support optional iterators: + +Iterators in the IDL header file can now also be of OptionalType<>. + +### Feature: support restrict for iterators + +The @restrict tag can now, next to strings, arrays and std::vector, also be used to set the minimum and maximum allowed size for iterator types in the IDL header file. + +### Feature: allow only lower bound restrict for strings + +When an input string in a method in the IDL is not allowed to be empty (but it is not desirable to set a maximum length, if that is the case the @restrict:x..y tag can be used) it can be flagged with the @restrict:nonempty tag. +If the string is empty this will already generate an error when validating the input in the generated proxy stub code. + +### Feature: collated iterators + +Iterators in the past always got their values one by one via the COM-RPC interface leading to overhead in an IPC situation as multiple COM-RPC calls are needed while most of the times the iterators were used to in the end get all the values. +It is now possible to make all the iterators get all the values in one go and after that the COM-RPC calls to get the values will only be local calls. +To enable this mode for all iterators you can pass the flag --collated-iterators to the proxy stub generator. +With this mode enabled one can by specifying the tag @interface with an iterator or the iterator typedef make that iterator work like it did in the past and get the values one by one. + +Note in Thunder 6 this mode might become the default, that has not been done yet to give the opportunity to put the @interface tag at the iterators that potentially deal with huge amounts of data where this might pose a too big of a memory penalty. + +### Feature: build in methods in documentation: + +The JSON-RPC API documentation generation now for every plugin also includes the Thunder predefined functions ("versions", "exists", "register" and "unregister") so that a complete API overview is created. +These methods are now also described in the Thunder documentation at the relevant places. + +### Change: General bug fixes + +A number of fixes and improvements were made to make the generated code more robust and compliant with higher warning levels and compiler versions + +# Thunder Interfaces + +## Changes and new Features + +### Change: + +### Feature: there is now an example section for interfaces + +The Thunder interfaces now contain a special section for example interfaces that are not meant to be used in production, see [here](https://github.com/rdkcentral/ThunderInterfaces/tree/master/example_interfaces) + +This also includes new example interfaces for features added in Thunder 5.3. + +### Change: interfaces updated for lower case legacy + +A number of interfaces being used by contributed plugins still used the JSON meta file solution. +These have been changed in to full IDL header files so the legacy-lowercase feature could be used to make the interfaces backwards compatible in JSON-RPC casing now the default for this has changed. +(among these: timesync, webkit, opencdm, memorymonitor, locationsync, security agent, device-id) + +### Feature: IPluginAsyncStateControl interface added + +The [IPluginAsyncStateControl.h](https://github.com/rdkcentral/ThunderInterfaces/blob/R5_3/interfaces/IPluginAsyncStateControl.h) interface was added which can be used by services that want to implement plugin state control functionality. + + \ No newline at end of file diff --git a/docs/introduction/config.md b/docs/introduction/config.md index b3f0f52b68..83e1d5e73c 100644 --- a/docs/introduction/config.md +++ b/docs/introduction/config.md @@ -34,6 +34,9 @@ This section documents the available options for Thunder. This is different from | communicator | Socket to listen for COM-RPC messages. Can be a filesystem path on Linux for a Unix domain socket, or a TCP socket.

For unix sockets, the file permissions can be specified by adding a `|` followed by the numeric permissions | string | /tmp/communicator\|0777 | 127.0.0.1:4000 | | redirect | Redirect incoming HTTP requests to the root Thunder URL to this address (please note it must contain the resource that is required e.g. index.html ) | string | http://127.0.0.1/Service/Controller/UI/index.html | http://127.0.0.1/Service/Controller/UI/index.html | | idletime | Amount of time (in seconds) to wait before closing and cleaning up idle client connections. If no activity occurs over a connection for this time Thunder will close it. | integer | 180 | 180 | +| discovery | enable loading of the plugin metadata on Thunder startup, when disabled metadata will only be available after plugin activation (only turn this feature off when loading the metadata does not work in special circumstances) | bool | true | +| channel_throttle | maximum number of JSON-RPC requests allowed in parallel per channel (0 is no limit) | integer | half the number of available workerpool threads | 3 | +| throttle | maximum number of JSON-RPC requests allowed in parallel to a particular plugin, can be overridden for a specific plugin in the plugin configuration (0 is no limit) | integer | half the number of available workerpool threads | 3 | | softkillcheckwaittime | When killing an out-of-process plugin, the amount of time to wait after sending a SIGTERM signal to the process before checking & trying again | integer | 3 | 3 | | hardkillcheckwaittime | When killing an out-of-process plugin, the amount of time to wait after sending a SIGKILL signal to the process before trying again | integer | 10 | 10 | | legacyinitalize | Enables legacy Plugin initialization behaviour where the Deinitialize() method is not called on if Initialize() fails. For backwards compatibility | bool | false | false | diff --git a/docs/plugin/config.md b/docs/plugin/config.md index 8614a34f25..0165011e6f 100644 --- a/docs/plugin/config.md +++ b/docs/plugin/config.md @@ -40,6 +40,7 @@ These are the options applicable to all plugins | configuration.root.remoteaddress | If running in distributed mode, the address of the COM-RPC socket on the network to connect to | string | - | - | | persistentpathpostfix | Instead of using the plugin callsign, use this as the persistent path postfix. Useful if you are cloning plugins and want them to use the same persistent directory | string | - | sharedPersistentDirectory | | volatilepathpostfix | Instead of using the plugin callsign, use this as the volatile path postfix. Useful if you are cloning plugins and want them to use the same volatile directory | string | - | sharedVolatileDirectory | +| throttle | maximum number of JSON-RPC requests allowed in parallel to this plugin | int | - | 3 | | systemrootpath | Custom directory to search for the plugin .so files | string | - | | | startuporder | A simple mechanism for prioritising autostart plugins. Plugins will be started based on their startup order value - e.g. lower values will cause plugins to be started earlier than plugins with higher values | int | 50 | 10 | diff --git a/docs/plugin/interfaces/interfaces.md b/docs/plugin/interfaces/interfaces.md index 5805dfb4c3..3a82efc604 100644 --- a/docs/plugin/interfaces/interfaces.md +++ b/docs/plugin/interfaces/interfaces.md @@ -97,6 +97,8 @@ In older Thunder versions (` allows a member to be optional (this superseded @optional), and must be used if an attribute is expected to be optional on JSON-RPC. In COM-RPC the OptionalType can be used to see if a value was set, and in JSON-RPC it is then allowed to omit this parameter. * A @default tag can be used to provide a value, in the case T is not set. See more [here](../tags/#default). @@ -111,9 +113,35 @@ In older Thunder versions ( +#### Checking if a function exists -
+With the generically available "exists" function a client can query if a specific function is available on a given plugin JSON-RPC interface. Call it like + +```json +{ + "jsonrpc": "2.0", + "id": 42, + "method": "MyPlugin.1.exists", + "params": { + "method": "methodname" + } +} +``` + +it will return true or false to indicate whether the method was available or not: + +```json +{ + "jsonrpc": "2.0", + "id": 42, + "result": true +} +``` #### Preventing Memory leaks @@ -133,13 +161,243 @@ Examples: View [Messenger.h](https://github.com/WebPlatformForEmbedded/ThunderNanoServicesRDK/blob/master/Messenger/Messenger.h#L254-L255) to see how ```Core::JSONRPC::Context``` is used. +Note in newer Thunder versions session support was added which in a lot of cases automates this and leaks are prevented automatically, for more info see the [object lookup section](#object-lookup) and the [custom object lookup](#custom-object-lookup) +
-#### Notification Registration +#### Notifications Notification registration is a way of tracking updates on a notification. +When a notification is tagged with the @event keyword it means code will be generated to make it easy to send this notification to registered clients from the C++ code that implements the interface. +Registering for a JSON-RPC event can be done by the client by calling the "register" function which is generically available for all plugins (if documentation is generated for an interface or plugin using that interface containing events, examples for registration and deregistration and the notification itself will be included in the documentation): + +```json +{ + "jsonrpc": "2.0", + "id": 42, + "method": "MyPlugin.1.register", + "params": { + "event": "eventname", + "id": "myapp" + } +} +``` + +if the registration is successful the following return can be expected (and an error response in case the registration failed) : + +```json +{ + "jsonrpc": "2.0", + "id": 42, + "result": null +} +``` + +Unregistering from an event can be done by the client by calling the "unregister" function, which like the register is generically available for all plugins: + +```json +{ + "jsonrpc": "2.0", + "id": 42, + "method": "MyPlugin.1.unregister", + "params": { + "event": "eventname", + "id": "myapp" + } +} +``` + +if the deregistration is successful the following return can be expected (and an error response in case the deregistration failed) : + +```json +{ + "jsonrpc": "2.0", + "id": 42, + "result": null +} +``` + +In the generated J< interface >.h file static helper functions will be generated that one can call in the C++ code to send the notifications to all registered clients. +For example suppose we would have this interface: +```cpp + // @json 1.0.0 + struct EXTERNAL ITest : virtual public Core::IUnknown { + + enum { ID = ID_TEST }; + + // @event + struct EXTERNAL INotification : virtual public Core::IUnknown { + + enum { ID = RPC::ID_TEST_NOTIFICATION }; + + virtual void StateChange(const string& callsign, const bool test2) = 0; + }; + + virtual Core::hresult Register(INotification* const sink) = 0; + virtual Core::hresult Unregister(const INotification* const sink) = 0; + + virtual Core::hresult Test() const = 0; + }; +``` + +After including the generated helper file one could notify all registered clients on a StateNotification class by calling the following code: + +```cpp + Exchange::JTest::Event::StateChange(*this, "test", true); +``` + +Note depending on the interface more than one helper function is generated (e.g. also one where you can use the generated params class), check the generated J< interface >.h file for all the options. + +##### indexed events + +With the above and the generated event notification functions the notification will be sent to all registered clients. It is possible to only notify some clients register for an event. +If one wants to send some notifications only to some registered clients there must be a parameter in the event that a client uses to indicate if it wants to only receive the notification when that parameter has a certain value. +That parameter can be flagged with the @index tag to indicate it is used to discriminate registered clients. + +Let's discuss with an example: + +Suppose we have this interface: + +```cpp + // @json 1.0.0 + struct EXTERNAL ITest : virtual public Core::IUnknown { + + enum { ID = ID_TEST }; + + // @event + struct EXTERNAL INotification : virtual public Core::IUnknown { + + enum { ID = RPC::ID_TEST_NOTIFICATION }; + + virtual void StateChange(const string& callsign, const bool test2) = 0; + }; + + virtual Core::hresult Register(INotification* const sink, const string& callsign /* index */) = 0; + virtual Core::hresult Unregister(const INotification* const sink, const string& callsign /* index */) = 0; + + virtual Core::hresult Test() const = 0; + }; +``` + +From the COM-RPC interface it is clear one can subscribe to be notified on changes for a specific callsign. The @index is put at the COM-RPC Register and Unregister methods, the code generators will do a name lookup, so in this case "callsign" to find the connected event. +(the event can have the @index tag as well, as long as the index parameter name matches). + +A client can subscribe via JSON-RPC to be notified on a specific callsign like this: + +```json +{ + "jsonrpc": "2.0", + "id": 42, + "method": ".1.register@", + "params": { + "event": "stateChange", + "id": "myid" + } +} +``` + +where < callsign > should have the specific callsign this client wants to be notified about. + +The notification sent will then be like this: + +```json +{ + "jsonrpc": "2.0", + "method": "myid.stateChange@", + "params": { + "marcel": false + } +} +``` + +As with a non indexed event if the documentation is generated for the JSON-RPC interface examples for registering, deregistering and the notification will be included in the document. + +Again static helpers are created in the J< interface >.h file that allow one to sent the notifications to all registered clients: + +```cpp + Exchange::JTest::Event::StateChange(*this, "test", true); +``` + +This will only sent a notification to the clients whom registered for the stateChange event with client "test" by adding @test to the registration designator. + +This generated static helper uses a lambda internally which will send the notification to all clients who registered for the index passed to the helper function. It is also possible to pass a lambda yourself as last parameter to the static helper function when you want different behaviour. + +It is also possible to use Core::OptionalType to indicate the index is not mandatory (and if not set one registers for all values of the index). + +```cpp + // @json 1.0.0 + struct EXTERNAL ITest : virtual public Core::IUnknown { + + enum { ID = ID_TEST }; + + // @event + struct EXTERNAL INotification : virtual public Core::IUnknown { + + enum { ID = RPC::ID_TEST_NOTIFICATION }; + + virtual void StateChange(const string& callsign, const bool test2) = 0; + }; + + virtual Core::hresult Register(INotification* sink, const Core::OptionalType& callsign /* @index */) = 0; + virtual Core::hresult Unregister(INotification* sink, const Core::OptionalType& callsign /* @index */) = 0; + + virtual Core::hresult Test() const = 0; + }; +``` + +for the JSON-RPC registration that does not influence much, one uses: + +```json +{ + "jsonrpc": "2.0", + "id": 42, + "method": ".1.register@", + "params": { + "event": "stateChange", + "id": "myid" + } +} +``` + +to register only to be notified for a specific callsign and: + +```json +{ + "jsonrpc": "2.0", + "id": 42, + "method": ".1.register", + "params": { + "event": "stateChange", + "id": "myid" + } +} +``` + +to register for all callsigns no matter what the value of callsign is. + +The notification is now a little different however: + +```json +{ + "jsonrpc": "2.0", + "method": "myid.stateChange@abc", + "params": { + "callsign": "...", + "marcel": false + } +} +``` +It will now always include the callsign as parameter (as if the event was for a client that registered for all callsigns the @ part will not be in the event so the callsign must be included as parameter to indicate for which callsign this event is). + +As always these examples will also be included in the generated documentation for this interface + +And again static helper functions will be included in the generated J< interface >.h file for sending the notifications. + +##### statuslisteners + Tagging a notification with @statuslistener will emit additional code that will allow you to be notified when a JSON-RPC client has registered (or unregistered) from this notification. As a result, an additional IHandler interface is generated, providing the callbacks. +Note: the 'unregistered' notification is also triggered if the client disconnects (channel closed) without explicitly calling Unregister beforehand. Examples: @@ -204,9 +462,11 @@ For a more detailed view, visit [Messenger.h](https://github.com/WebPlatformForE Object lookup defines the ability to create a JSON-RPC interface to access dynamically created objects (or sessions). This object interface is brought into JSON-RPC scope with a object ID. This translates the Object Oriented domain (used in COM-RPC interfaces) to the functional domain (JSON-RPC). -Object lookup will happen automatically by the generator when a method is found on the COM-RPC interface that returns an COM-RPC Interface also tagged as JSON-RPC interface as an out parameter and it is expected also a method that takes the same interface as input parameter is available to be able to destroy the created object. +Object lookup will happen automatically when the @encode:autolookup tag is added to the interface that is to be the session object. The generator will look for methods return the session type as an out parameter as COM-RPC Interface (where the interface must in the same file and of course also must be tagged with @json) and it is expected that there is also a method that takes the same interface as input parameter to be available as well to be able to destroy the created object. -The generated JSON-RPC interface will then automatically associate the method with the interface out parameter as a creation function for an object that implements the interface of the out parameter and will return an object ID for JSON-RPC to identify the created object. This object ID can then be used in subsequent calls on methods available on the type of the interface to indicate the object you want the function to be called upon. the JSON-RPC generator associates the COM-RPC method with the input interface pointer as the method that will destroy the created object, on JSON-RPC level the object ID to destroy is expected as an input parameter. +The generated JSON-RPC interface will then automatically (therefore the lookup method also described as auto object lookup) associate the method with the interface out parameter as a creation function for an object that implements the interface of the out parameter and will return an object ID for JSON-RPC to identify the created object. This object ID can then be used in subsequent calls on methods available on the type of the interface to indicate the object you want the function to be called upon. the JSON-RPC generator associates the COM-RPC method with the input interface pointer as the method that will destroy the created object, on JSON-RPC level the object ID to destroy is expected as an input parameter. +In the JSON-RPC interface the object-ID will then for the function that creates the object be returned as output parameter and for functions to be called on an object the method for the JSON-RPC call should be the name of the interface (to prevent name clashes) and after that add "#< device is >::< object method name >" to specify the method to be called on the object and the object ID on which the method should be called on. +But easier is to just generate the documentation for this interface with the document generator as that will include examples for all methods. Note this also works when the interface contains an event. You will then be able to register specifically for the events of a specific object ID and only receive the ones generated for that object ID. @@ -215,11 +475,15 @@ To prevent any object leaks a call must be made into the generated code to relea Meaning to be able to use this COM-RPC interface in JSON-RPC no additional code needs to be written, it is enough to implement the COM-RPC interface and connect the JSON-RPC interface to it as you would normally do, the code generator will take care of the full JSON-RPC interface. +It is also possible to register callbacks to be called when objects are acquired or relinquished in case special handling is needed for JSON-RPC (generic code can be put into the COM-RPC code for Acquire and Relinquish as that is called for both cases). + +Note that for the above to work the plugin should derive from the PluginHost::JSONRPCSupportsAutoObjectLookup class instead of the PluginHost::JSONRPC class for its PluginHost::IDispatcher implementation. + ##### Example [here](https://github.com/rdkcentral/ThunderInterfaces/blob/master/example_interfaces/ISimpleInstanceObjects.h#L29) you will see an example of an interface that uses automatic object lookup. -The [Acquire](https://github.com/rdkcentral/ThunderInterfaces/blob/master/example_interfaces/ISimpleInstanceObjects.h#L73) method on COM-RPC creates an object of type [IDevice](https://github.com/rdkcentral/ThunderInterfaces/blob/master/example_interfaces/ISimpleInstanceObjects.h#L39). +The [Acquire](https://github.com/rdkcentral/ThunderInterfaces/blob/master/example_interfaces/ISimpleInstanceObjects.h#L73) method on COM-RPC creates an object of type [IDevice](https://github.com/rdkcentral/ThunderInterfaces/blob/master/example_interfaces/ISimpleInstanceObjects.h#L39) which has the @encode:autolookup tag specified. With the [Relinquish](https://github.com/rdkcentral/ThunderInterfaces/blob/master/example_interfaces/ISimpleInstanceObjects.h#L78) method the object is destroyed again. If you look into the generated documentation for this interface which can be found [here](https://github.com/rdkcentral/ThunderNanoServices/blob/master/examples/GeneratorShowcase/doc/GeneratorShowcasePlugin.md) @@ -236,8 +500,53 @@ To register for an event for a specific Device, so object ID in the JSON-RPC wor [Here](https://github.com/rdkcentral/ThunderNanoServices/blob/master/examples/GeneratorShowcase/GeneratorShowcase.h#L673) you can see how to call the generated Closed method to do cleanup in case the channel closed without all devices being Relinquished. As mentioned it is also possible to pass a callback to the Closed method to get notified on all devices being released here (not demonstrated). +[Here](https://github.com/rdkcentral/ThunderNanoServices/blob/fd91c5f012bfecac1ee4f9fa40cf7db6e7fb90ec/examples/GeneratorShowcase/GeneratorShowcase.h#L1534-L1586) is an example of callbacks registered to be called when a JSON-RPC acquire or relinquish request is being handled. +
+#### Custom Object lookup + +As an alternative to "auto object lookup" where the object-id creation and linking to an object is automatically handled for you under the hood there is also a "custom object lookup" feature. +This can be used when an object already has a logical instance-id itself that can uniquely be used to identify the object (e.g. each object has a unique name of number id). +Now that logical id can also be exposed to the JSON-RPC world to connect a request to a certain object. One is now self responsible for handing out the id's and connecting them to the object as well as the object lifetime. + +To enable custom lookup add the @encode:lookup to the interface representing the object. The code generator will now look for functions or properties where the object interface is returned as output parameter and assume that function is used to retrieve the object by its id (which should be the other parameter of the function or property). +Two lambdas should be registered in the plugin code that provide the translation from object to object ID and vice versa. + +Like for autolookup (indexed) events are fully supported for custom lookup. + +Handling of the object ID's in the JSON-RPC interface is exactly the same as for autolookup, but again easiest is to just generate the documentation for this interface with the document generator as that will include examples for all methods. + +Lifetime for the objects must be handled in the implementation of the interface (this can now not be automatic as the generated code does not have any internal storage for the objects). +If the objects do have a static lifetime (meaning their lifetime is equal to the plugin) this is easy and no "relinquish" code or leakage prevention is needed. +If the objects lifetime is dynamic (meaning they are created and destroyed on the fly) this is more complicated. The COM-RPC method that returns the object by name can now create the object, a "relinquish" method must be added as well, just like for autlookup, as one can not use the COM-RPC "Release" method as that is not exposed to JSON-RPC. And one should probably also want to register for PluginHost::IShell::IConnectionServer::INotification notifications to be able to cleanup objects that were not explicitly relinquished before the channel closed. + +If the objects can only be used on a certain channel is up to the implementation. If they are static most likely not, if they are dynamic they most probably will. But this can be completely handled in the implementation as the object ID <-> object lambdas have full access to the JSON-RPC context. + +Note that for the above to work the plugin should derive from the PluginHost::JSONRPCSupportsObjectLookup class instead of the PluginHost::JSONRPC class for its PluginHost::IDispatcher implementation. + +##### Example + +This example uses static objects, therefore no special lifetime handling is required. Also the usage of the objects is not restricted to specific channels. + +[here](https://github.com/rdkcentral/ThunderInterfaces/blob/master/example_interfaces/ISimpleCustomObjects.h) you will see an example of an interface that uses custom object lookup. + +With the @encode:lookup on the [IAccessory](https://github.com/rdkcentral/ThunderInterfaces/blob/8de7db327a585f27c6a7e1e4eb944aa29ccef070/example_interfaces/ISimpleCustomObjects.h#L33) interface we indicated this interface is used as an object. + +The [Accessory](https://github.com/rdkcentral/ThunderInterfaces/blob/8de7db327a585f27c6a7e1e4eb944aa29ccef070/example_interfaces/ISimpleCustomObjects.h#L86) property on the ISimpleCustomObjects interface allows access to a specific Accessory object by specifying the accessory name. + +The Accessory functions or properties like [Pin](https://github.com/rdkcentral/ThunderInterfaces/blob/8de7db327a585f27c6a7e1e4eb944aa29ccef070/example_interfaces/ISimpleCustomObjects.h#L60) now can be accessed when using JSON-RPC requests passing the object ID retrieved with the Accessory property. + +The generated documentation for this interface can be found [here](https://github.com/rdkcentral/ThunderNanoServices/blob/master/examples/GeneratorShowcase/doc/GeneratorShowcasePlugin.md) +The [Accessory](https://github.com/rdkcentral/ThunderNanoServices/blob/master/examples/GeneratorShowcase/doc/GeneratorShowcasePlugin.md#property_accessory) documentation shows that one can retrieve the object ID for an accessory by passing it as index to the property. +The property will return the object ID as return value. +Calling for example [Pin](https://github.com/rdkcentral/ThunderNanoServices/blob/master/examples/GeneratorShowcase/doc/GeneratorShowcasePlugin.md#property_accessory__pin) can be done by calling the "accessory" method followed by #< object ID>::pin, and as the pin is an indexed property the index follows after that with a @ delimiter. + +The plugin code can be found [here](https://github.com/rdkcentral/ThunderNanoServices/tree/master/examples/GeneratorShowcase) + +[Here](https://github.com/rdkcentral/ThunderNanoServices/blob/fd91c5f012bfecac1ee4f9fa40cf7db6e7fb90ec/examples/GeneratorShowcase/GeneratorShowcase.h#L40) we see the plugin derives from PluginHost::JSONRPCSupportsObjectLookup. +And [here](https://github.com/rdkcentral/ThunderNanoServices/blob/fd91c5f012bfecac1ee4f9fa40cf7db6e7fb90ec/examples/GeneratorShowcase/GeneratorShowcase.h#L1504-L1524) we see the lambdas being registered that take care of the object ID to object conversion and vice versa. + #### Asynchronous Functions When an action triggered by a method on a COM-RPC interface which takes some time to complete this method will be made asynchronous, meaning it will return when the action is started and it will expect a callback interface to be passed as input parameter to the method so a method on the callback interface will be called when the action that was started is finished or failed (it it mandatory that the started action always results in a method to be called so it is clear when the action is over). diff --git a/docs/plugin/interfaces/tags.md b/docs/plugin/interfaces/tags.md index 69a99ac739..f4cff5c985 100644 --- a/docs/plugin/interfaces/tags.md +++ b/docs/plugin/interfaces/tags.md @@ -103,7 +103,7 @@ Ddefines a literal as a known identifier (equivalent of `#define` in C++ code) |[@out](#out)|Marks an output parameter | | Yes|Yes|Method Parameter| |[@inout](#inout)|Marks as input and output parameter (equivalent to `@in @out`) | | Yes|Yes| Method Parameter| |[@restrict](#restrict)|Specifies valid range for a parameter | | Yes |Yes| Method Parameter | -|[@interface](#interface)| Specifies a parameter holding interface ID value for void* interface passing | | Yes | No |Method paramter| +|[@interface](#interface)| Specifies a parameter holding interface ID value for void* interface passing or indicates iterator should not be collated | | Yes | No |Method paramter| |[@length](#length)|Specifies the expression to evaluate length of an array parameter (can be other parameter name, or constant, or math expression)| | No | Yes | Method Parameter| |[@maxlength](#maxlength)|Specifies a maximum buffer length value | | No | Yes |Method parameter| |[@default](#default)|Provides a default value for an unset optional type | | Yes | Yes |Method parameter| @@ -143,12 +143,13 @@ When the function returns, the parameter will have the modified length value. Th #### @restrict -Specifies a valid range for a parameter (e.g. for buffers and strings it could specify a valid size). Ranges are inclusive. +Specifies a valid range for a parameter (e.g. it can indicate a valid size for buffers, strings, arrays, std::vector, or iterators). Ranges are inclusive. If a parameter is outside the valid range, then there are two possibilities: * If running a debug build, an ASSERT will be triggered if the value is outside the allowed range * If the stub generator is invoked with the `--secure` flag, then the range will be checked on all builds and an error (`ERROR_INVALID_RANGE`) will be returned if the value is outside the range +* If one just wants to indicate a string must contain at least one character, empty would not be valid, the tag @restrict:nonempty can be used with that parameter. ##### Example @@ -163,6 +164,10 @@ This tag specifies a parameter holding interface ID value for `void*` interface Functions like [Acquire](https://github.com/rdkcentral/Thunder/blob/master/Source/com/ICOM.h#L45) will return the pointer to the queried interface. For such functions, this tag will specify which field to look for to get the corresponding interface id. +A second usage for this tag is to indicate an iterator should not be collated. Iterators can be collated in Thunder (meaning all their values will be transferred in one go and all follow up Next calls will be local instead of remote). +If this is not desired (e.g. because the iterator holds a huge number of items making this a too big of memory overhead or because you only need a few items from the iterator instead of most of them) one can add the interface with the iterator parameter or with the iterator typedef. +At the moment the default behavior for the code generators is to have all iterators not collated. By passing the the flag --collated-iterators to the proxy stub generator all iterators will become collated, expect of the course the ones that have the tag @interface. + ##### Example In [ICOM.h](https://github.com/rdkcentral/Thunder/blob/master/Source/com/ICOM.h#L45) specifies parameter 3 interfaceId holds the interface id for the returned interface. @@ -269,6 +274,7 @@ In [IController.h](https://github.com/rdkcentral/Thunder/blob/master/Source/plug |[@statuslistener](#statuslistener)| Notifies when a JSON-RPC client registers/unregisters from an notification | | No | Yes | Method | |[@async](#async)| Indicates a method is asynchronous for the JSON-RPC interface | | No | Yes | Method | |[@encode](#encode)|Encodes data into a different format | | Yes | Yes |Method parameter| +|[@wrapped](#wrapped)|Encodes a single out parameter in a wrapped format | | No | Yes |Class and Method parameter| #### @json This tag helps to generate JSON-RPC files for the given Class/Struct/enum. @@ -399,7 +405,11 @@ Indicates that enumerator lists should be packed into into a bit mask. #### @index Used in conjunction with @property. Allows a property list to be accessed at a given index. -Index should be the first parameter in the function. +Index should be the first parameter in the function. It is allowed to make the index a Core::OptionalType<>. + +The @index tag can also be used to mark an event parameter to be indexed, so clients can subscribe to the event for a specific value of that indexed parameter: for more info see [here](../interfaces/#indexed-events) + +As for a short amount of time the @index for events generated code for the JSON-RPC interface that expected a dot (.) as a separator for the index in the clientid instead of a '@' in the designator, @index:deprecated can be put at the indexed event parameter (so instead of just @index) to indicate this indexed event should generate code that uses the old deprecated index format (see the generated documentation for what the interface would be like). Only use this for backwards compatibility reasons. ##### Example [IController.h](https://github.com/rdkcentral/Thunder/blob/6fa31a946314fdbad05792a216b33891584fb4b5/Source/plugins/IController.h#L125) sets the `@index` tag on the `index` parameter. @@ -564,9 +574,27 @@ For more details, click [here](../interfaces/#asynchronous-functions) #### @encode -This tag encodes data into an alternate format. +This tag encodes or decodes (if an input parameter) data into/from an alternate format. + +* `@encode:base64` encodes/decodes arrays (or std::vector or ptr+len buffers) as base64 JSON-RPC string, on the condition that the array base is type `uint8_t` or `char`. +* `@encode:hex` encodes/decodes arrays (or std::vector or ptr+len buffers) as a Hex JSON-RPC string, on the condition that the array base is type `uint8_t` or `char`. + +@encode:autolookup is another form of encode: it indicates this interface is used as an object in another interface. See for more info [here](../interfaces/#object-lookup) -* `@encode:base64` encodes arrays as base64 JSON-RPC arrays, on the condition that the array base is type `uint8_t`. +An alternative to "autolookup" is custom lookup where one can keep track of how an object-id is connected to an object in a custom manor (in autolookup this is automatic and arranged for you under the hood). +Custom lookup is indicated with the @encode:lookup tag. +See for more info on custom object lookup [here](../interfaces/#custom-object-lookup) + +For COM-RPC `@encode:text` can be used to have conversion code created for an enum (enum to string and vice versa), this can useful for example in case the enum must be added to a Trace or message as string. +Example: +```cpp + // @encode:text + enum state : uint16_t { + PLAYING = 0x0001, + STOPPED = 0x0002, + SUSPENDING = 0x0004 + }; +``` ##### Example @@ -611,6 +639,65 @@ Example list:
+#### Wrapped + +This tag can be placed at class or method level, where at class level is by far preferable as it prevents inconsistencies in JSON-RPC function handling. +Wrapped will for a single output parameter also add the parameter name to the result, making it always a JSON object. +Note can also be used for array, std::vector, iterator etc. single output parameter. As for a POD it does not immediately make sense to have it wrapped, it becomes a JSON object inside an object, wrapped will be ignored for POD when the wrapped is put on class level. If put on method level however the POD is wrapped as that than is the clear expectation of the interface designer. +Wrapped cannot be used for properties as that does not make sense. +Incorrect or inconsistent usage will lead to an error raised by the code generators. Of course the documentation generators do take the wrapped tag into account. + +Remark: of course it is preferable to keep the JSON-RPC interface as whole consistent so for that reason be hesitant when using this tag (it was added as there are interface where workarounds are used to achieve the wrapped effect). + +#### Example + +Without the wrapped tag in case of a single output parameter the result will be as short as possible to make it as efficient as possible: + +e.g. with this interface (without wrapped): + +```cpp + // @json 1.0.0 + struct EXTERNAL ITest : virtual public Core::IUnknown { + + enum { ID = ID_TEST }; + + virtual Core::hresult Test(uint8_t& test /* @out */ ) const = 0; + }; +``` +this is the returned JSON-RPC result for the Test function + +```json +{ + "jsonrpc": "2.0", + "id": 42, + "result": 0 +} +``` + +if we enable the wrapped tag for this interface: + +```cpp + // @json 1.0.0 @wrapped + struct EXTERNAL ITest : virtual public Core::IUnknown { + + enum { ID = ID_TEST }; + + virtual Core::hresult Test(uint8_t& test /* @out */ ) const = 0; + }; +``` + +the returned JSON-RPC result for the Test function has changed to this: + +```json +{ + "jsonrpc": "2.0", + "id": 42, + "result": { + "test": 0 + } +} +``` + ### JSON-RPC Documentation Related Tags | Tag|Short Description|Deprecated|StubGen|JsonGen|Scope| diff --git a/docs/utils/customcodes.md b/docs/utils/customcodes.md new file mode 100644 index 0000000000..3d75f517e9 --- /dev/null +++ b/docs/utils/customcodes.md @@ -0,0 +1,224 @@ +# Thunder Custom (Error) Codes + +## Introduction + +Error codes used in Thunder are normally predefined, which suffices up to now. These error codes (due to the "JSON-RPC in terms of COM-RPC" feature) are also translated into JSON-RPC error codes. The custom code feature supports a more flexible error scheme to be used (mainly to have more flexibility in the error codes reported in the JSON-RPC error object). +There is already a feature for "custom JSON-RPC error messages" which allows for context dependent JSON-RPC messages but also allows to override the error code returned in some form. +This however leads to inconsistent error codes between COM-RPC and JSON-RPC for the same handler and is not designed to be used on a large scale but for exceptional cases only. +So with Custom Codes we support a way to have custom error codes that are consistent for both COM-RPC and JSON-RPC (or any other future protocol that will be implemented in terms of COM-RPC), allow for custom (not hardcoded inside Thunder) code to string translation and allow for direct influence on the error code returned by JSON-RPC. +(the "custom JSON-RPC error messages" feature will continue to be supported for context dependent JSON-RPC messages). + +## Custom Error Code hresults solution and range provided + +Every COM-RPC method returns a Core::hresult error code. This is an unsigned double word value. +Custom Codes uses the 25th bit of the hresult to indicate the hresult contains a "custom error code" that is not one of the Thunder predefined error codes and should be treated differently. +When this bit is set the 24th bit will be considered a sign bit so that the custom error codes can also be negative. +This leads to an available range of -8.388.608 to 8.388.607 for the custom error codes. +One custom code will however have special meaning, code 0, (so with "custom code bit" set, hresult code 0x1000000) will mean an invalid custom error code was set, more on this later. + +## Thunder Core Code support + +The Thunder Core header file (Errors.h) adds the following helper function: + +### Core::CustomCode + +```cpp + Core::hresult CustomCode(const int24_t customCode); +``` + +This function can be used to transform a custom code into a Core::hresult where the correct bit is set to indicate this. + +If the customCode input for CustomCode is Core::ERROR_NONE it will create a Core::hresult of Core::ERROR_NONE (so no Custom Code bit set). This as existing (Thunder and plugin) code do at certain locations check if a call failed with: + +```cpp + Core::hresult error = DoSomething(); + + if (error != Core::ERROR_NONE) { + //handle the error situation + } +``` + +and with the above this code will continue to work without adopting (which would also cause additional overhead and it will be hard to identify all locations where code might be checking like this). Note that this is why returning 0x1000000 directly is not recommended (custom error bit set with code 0) as that will cause issues (and would mean you are returning the error code "invalid custom error code" not "no error") + +And with this behaviour of the CustomCode the following code will work correctly and not lead to unexpected behaviour: + +```cpp +Core::hresult A() +{ + int24_t error = Core::ERROR_NONE; + + if(something_bad_happened == true) { + error = -12345; // set a custom code error + } + + return Core::CustomCode(error); +} +``` + +or if preferred this alternative will work as well of course: + +```cpp +Core::hresult A() +{ + Core::hresult error = Core::ERROR_NONE; + + if(something_bad_happened == true) { + error = Core::CustomCode(-12345); // set a custom code error + } + + return error; +} +``` + +In case an error code is passed to CustomCode that would overflow the reserved range this will ASSERT in debug and in Release (or any situations asserts are off) to a flexible error code of 0 (so custom code bit set with code 0) so this info is still carried in the hresult and can be extracted later. + +Some examples: + +```cpp +{ + + Core::hresult error = Core::CustomCode(-12345); // will return an hresult with the "custom code bit" set and the custom code will be -12345 + + Core::hresult error = Core::CustomCode(Core::ERROR_NONE); // will set the hresult to Core::ERROR_NONE ("custom code bit" not set) + + Core::hresult error = Core::CustomCode(9000000); // as this overflows the allowed range it will return an hresult with the "custom code bit" set and the flex error code will be 0, so 0x1000000 (or an assert in debug) + +} +``` + +### Core::IsCustomCode + +With IsCustomCode one can find out if a Core::hresult is a custom code and what the code is: + +```cpp + int24_t IsCustomCode(const Core::hresult code); +``` + + This will return the custom error code as signed 24 bit number when the "custom error bit" was set and 0 if the hresult was not a flexible error. + (if the hresult code would have the value 0x1000000 ("custom code bit" set with code 0) IsCustomCode will return an int24_t with overflow value set which can be checked with Core::Overflowed, see the example below. + +```cpp +{ + + ASSERT(Core::IsCustomCode(Core::CustomCode(-12345)) == -12345); + + ASSERT(Core::IsCustomCode(Core::CustomCode(Core::ERROR_NONE)) == 0); + + Core::hresult result = Core::ERROR_GENERAL; + ASSERT(Core::IsCustomCode(result) == 0); + + if (Core::Overflowed(Core::IsCustomCode(Core::CustomCode(9000000))) == true) { printf("this will be printed"); } // note will when ASSERTS are enabled in the build already ASSERT on assigning the value to CustomCode + +} +``` +In example the above the ASSERTS will not fire and "this will be printed" will be printed (when ASSERTS are not turned on in the build) + +### Core::ErrorToString (existing) + +```cpp + const TCHAR* ErrorToString(const Core::hresult code); +``` + +The existing Core::ErrorToString will also (so next to the string representation for non Custom Code hresult values) return the correct string representation for the custom error code (also see the string representation section below) although the message will not include the actual set Custom Code, see ErrorToStringExtended below +So you can feed any hresult to this function, whether it is a Custom Code hresult or not. + +### Core::ErrorToStringExtended (new) + +```cpp + string ErrorToStringExtended(const Core::hresult code); +``` + +This function will return a correct string representation for any hresult value, whether it is a Custom Code hresult or not. If it is a Custom Code and the string cannot be retrieved from the conversion library the actual Custom Code value will be in the message. + +As the full desired functionality with Core::ErrorToString can not be achieved, a second, ErrorToStringExtended, is available for this. +As the range for the custom code is too big to have pre created static texts for all of them, it is desirable to create a dynamic string representation when a static one is not provided by the conversion library (or the conversion library feature is not used at all) to include the actual custom code in the message. +This cannot be done in the ErrorToString as it returns a pointer to the text, therefore an extended version is created that will include the custom code number. +(changing the ErrorToString was not desirable as that would break backwards compatibility unnecessarily) + +## String representation + +To be able to provide for a custom error code to string conversion (e.g. used for the JSON-RPC error text in the error object or to return a correct string representation when Core::ErrorToString is called) the following infrastructure will be added to Thunder: + +In Thunder sources/core a header file "ICustomErrorCodes.h" provides the interface to be implemented by an external library to support the custom error codes used in the plugin code for that Thunder instance. + +For now there is only one function in this interface called "CustomCodeToString": + +```cpp + +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + This file contains the interface that a library can implement in case the "custom error codes" feature is used in Thunder and code to string conversion is desired +*/ + +#pragma once + +#undef EXTERNAL + +#if defined(WIN32) || defined(_WINDOWS) || defined(__CYGWIN__) || defined(_WIN64) +#define EXTERNAL __declspec(dllexport) +#else +#define EXTERNAL __attribute__((visibility("default"))) +#endif + +#ifndef TCHAR +#ifdef _UNICODE +#define TCHAR wchar_t +#else +#define TCHAR char +#endif +#endif + +#ifdef __cplusplus +#include +extern "C" { +#else +#include +#endif + +// called from within Thunder to get the string representation for a custom code +// note parameter code is the pure (signed) Custom Code passed to Thunder. no additional bits set (so for signed numbers 32nd bit used as sign bit). +// in case no special string representation is needed return nullptr (NULL), in that case Thunder will convert the Custom Code to a generic message itself +EXTERNAL const TCHAR* CustomCodeToString(const int32_t code); + +#ifdef __cplusplus +} // extern "C" +#endif + +``` + +It should suffice to have static strings which prevents unnecessary string copying and keeps the memory management simple. + +There is a Thunder config option called "customcodelibrary" that allows you to point Thunder to the library implementing the above interface. +If it is not provided, or in case CustomCodeToString returns a nullptr for the particular code and a string representation for a custom error code is required the error code will be translated into "Undefined Custom Error: XXXX" where XXXX is the custom error code. +Note the existing ErrorToString will return a string without the number included when the string is not provided by the library as that cannot be supported without breaking backwards compatibility, ErrorToStringExtended will however (Thunder will of course use the new ErrorToStringExtended version to get the string that will be added to the JSON-RPC error text tag). +Note the custom error code 0 will not be routed through CustomCodeToString but always translate into "Invalid Custom ErrorCode set". + +### JSON-RPC specifics for custom error codes + +In case JSON-RPC is called in terms of COM-RPC (so an IDL C++ header file with a COM-RPC interface where the @json tag is used to generate the JSON-RPC interface) and the COM-RPC method returns a custom error code it will be dealt with in the following way: + +* The custom error code will be placed as is in the JSON RPC error object "code" tag. (so code 0 means an invalid custom code (too big) was passed). Note it is the responsibility of developers setting the codes not to overlap with JSON RPC specification reserved codes or Thunder codes (1 to 100 in Thunder 4.4 or -31000 to -31999 in Thunder 5 and above) this on explicit request so that codes can be used for API backwards compatibility (a TRACE will be added to warn for these cases though) +* the JSON-RPC error object "message" tag will contain the string returned by the CustomCodeToString call from the conversion library if configured and does return a text for that specific code, otherwise it will be set to "Undefined Custom Error: XXXX" where XXXX will be the Custom Code set. (of course when the "custom JSON-RPC error messages" feature was used for this call that will override this default behaviour) +* if the value set with CustomCode() overflowed (too big for a 24bit number) and ASSERTS are not enabled in the build the error code will be set to 0 and the object "message" tag will be set to "Invalid Custom ErrorCode set" + +### Example + +An example on how the above can be used in a plugin, including creating a custom code to string library, can be found [here](https://github.com/rdkcentral/ThunderNanoServices/tree/R5_3/examples/CustomErrorCodes) \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index b1921fea9a..e78ccb5595 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -65,6 +65,7 @@ nav: - Processes: utils/processes.md - Protocols: - WebSocket: rfc/websocket.md + - Custom Codes: utils/customcodes.md - Client Development: - Introduction: client/intro.md - JSON-RPC App: client/json-rpc.md From 569b9ba9d5cb484920f945c027404ed72a95332e Mon Sep 17 00:00:00 2001 From: MFransen69 <39826971+MFransen69@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:24:11 +0100 Subject: [PATCH 40/64] [Docs] add correct links to release notes for 5.3 (#2027) --- ReleaseNotes/ThunderReleaseNotes_R5.3.md | 21 +++++++-------------- docs/plugin/interfaces/tags.md | 2 +- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/ReleaseNotes/ThunderReleaseNotes_R5.3.md b/ReleaseNotes/ThunderReleaseNotes_R5.3.md index 1ed573da46..2e0a43f571 100644 --- a/ReleaseNotes/ThunderReleaseNotes_R5.3.md +++ b/ReleaseNotes/ThunderReleaseNotes_R5.3.md @@ -50,9 +50,7 @@ As this is a relatively new but potentially complex feature, the Thunder documen Before Thunder 5.3 error codes used in Thunder were predefined, which sufficed up to now. These error codes (due to the "JSON-RPC in terms of COM-RPC" feature) are also translated into JSON-RPC error codes and there was a request to support a more flexible error scheme (mainly to have more flexibility in the error codes reported in the JSON-RPC error object). So with this new Custom Codes feature a mechanism was introduced to have custom error codes that are consistent for both COM-RPC and JSON-RPC (or any other future protocol that will be implemented in terms of COM-RPC), allow for custom (not hardcoded inside Thunder) code to string translation and allow for direct influence on the error code returned by JSON-RPC. -For more info see [here]() in the Thunder documentation. - -NOTE self: add link above +For more info see [here](https://rdkcentral.github.io/Thunder/utils/customcodes/) in the Thunder documentation. ### Feature: Warning reporting for WebSocket open and close @@ -109,7 +107,7 @@ The class can be found [here](https://github.com/rdkcentral/Thunder/blob/d57c034 ### Feature: Enable to reach remote plugin via the Composite plugin feature -Thunder now fully supports accessing a Plugin living in a remote Thunder instance to be made accessible in the local Thunder instance using the Composite plugin feature (see also the new BridgeLink plugin in the ThunderNanoServicesRDK repository, read the release notes for that here NOTE hier nog link toevoegen) +Thunder now fully supports accessing a Plugin living in a remote Thunder instance to be made accessible in the local Thunder instance using the Composite plugin feature (see also the new BridgeLink plugin in the ThunderNanoServicesRDK repository, read the release notes for that [here](https://github.com/WebPlatformForEmbedded/ThunderNanoServicesRDK/blob/master/ReleaseNotes/ThunderReleaseNotes_R5.3_rdkservices.md) ) The ThunderUI has also been updated to support this feature and will now also show the remote plugins now. @@ -240,17 +238,16 @@ Although the old dot format was in use only for a short time in Thunder 5 the @i Indexes now also support OptionalType indexes to indicate subscribing to the index or all is in principle optional. For this to work it is now also allowed to put the @index keyword at the Register and the event is then looked up by name of the indexed parameter. (it is also allowed and advised to also put the @index then at the Unregister) -Please see this section in the Thunder documentation that has been added to describe indexed events: NOTE: add link +Please see [this section](https://rdkcentral.github.io/Thunder/plugin/interfaces/interfaces/#indexed-events) in the Thunder documentation that has been added to describe indexed events. ### Change: auto object lookup now requires @encode:autolookup The JSON-RPC auto object lookup feature now requires the @encode:autolookup tag, this for improved detection and consistency. -See more [here]() NOTE self: add link to autolookup documentation section. Note the auto object lookup feature was introduced in Thunder 5.2 without needing @encode:autolookup to be specified (it should not be breaking as Thunder 5.2 was not used to define new object based JSON-RPC interfaces with auto object lookup). A new feature for the auto object lookup is that it is now possible to register callbacks to be called when objects are acquired or relinquished in case special handling is needed for JSON-RPC (generic code can be put into the COM-RPC code for Acquire and Relinquish as that is called for both cases). -The (updated) documentation can be found [here]() NOTE: update link +The (updated) documentation can be found [here](https://rdkcentral.github.io/Thunder/plugin/interfaces/interfaces/#object-lookup) ### Feature: support custom object lookup @@ -259,9 +256,7 @@ Where auto lookup takes care of the creation of object ID's and linking them to This makes it easier for clients as the now can use a more meaningful ID instead of the abstract ID created by autolookup. Custom lookup is indicated by specifying the @encode:lookup with the interface for the object type. -See the Thunder documentation for more info [here]() - -NOTE: link to be included +See the Thunder documentation for more info [here](https://rdkcentral.github.io/Thunder/plugin/interfaces/interfaces/#custom-object-lookup) ### Feature: support status listeners for lookup objects @@ -284,12 +279,10 @@ Note: see [here](https://rdkcentral.github.io/Thunder/plugin/devtools/pluginskel ### Feature: wrapped format -The newly added wrapped tag will for a single output parameter also add the parameter name to the result, making it always a JSON object. It can also be used for arrays, std::vector, iterator etc. (see for more info [here]()) NOTE add link after documentation published +The newly added wrapped tag will for a single output parameter also add the parameter name to the result, making it always a JSON object. It can also be used for arrays, std::vector, iterator etc. Of course it is preferable to keep the JSON-RPC interface as whole consistent but this was added as there are interface where workarounds are used to achieve the wrapped effect so having this tag will make it easier to achieve the wrapped format. -See here for more info; - -NOTE: add link to documentation. +See [here](https://rdkcentral.github.io/Thunder/plugin/interfaces/tags/#wrapped) for more info. ### Feature: new buffer encoding options diff --git a/docs/plugin/interfaces/tags.md b/docs/plugin/interfaces/tags.md index f4cff5c985..86276c7981 100644 --- a/docs/plugin/interfaces/tags.md +++ b/docs/plugin/interfaces/tags.md @@ -639,7 +639,7 @@ Example list:
-#### Wrapped +#### @wrapped This tag can be placed at class or method level, where at class level is by far preferable as it prevents inconsistencies in JSON-RPC function handling. Wrapped will for a single output parameter also add the parameter name to the result, making it always a JSON object. From 6149b2f0092c1dfc441c5b461f55fcf7bea98e1c Mon Sep 17 00:00:00 2001 From: MFransen Date: Wed, 26 Nov 2025 10:48:20 +0100 Subject: [PATCH 41/64] [com] remove deadlock check for now as it will lead to false positives --- Source/com/IUnknown.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Source/com/IUnknown.cpp b/Source/com/IUnknown.cpp index 7bfe227e35..7ff65fda20 100644 --- a/Source/com/IUnknown.cpp +++ b/Source/com/IUnknown.cpp @@ -110,11 +110,6 @@ namespace ProxyStub { if (channel.IsValid() == true) { - if (channel->InProgress() == true) { - ASSERT(false && "IPC in progress detected on this channel. Possible deadlock!"); - SYSLOG(Logging::Error, (_T("IPC in progress detected on this channel for Interface [0x%X], Method ID [0x%X]. Possible deadlock!"), message->Parameters().InterfaceId(), message->Parameters().MethodId())); - } - result = channel->Invoke(message, waitTime); if (result != Core::ERROR_NONE) { From ca7ad203398b3f065f9f494b89bc12038cd24f95 Mon Sep 17 00:00:00 2001 From: Volkan Date: Wed, 26 Nov 2025 11:45:09 +0100 Subject: [PATCH 42/64] Semantic versioning update --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3bf521ae9f..dcb447dd27 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,7 +18,7 @@ cmake_minimum_required(VERSION 3.15) project(Thunder - VERSION 5.0.0 + VERSION 5.3.0 DESCRIPTION "Thunder framework" HOMEPAGE_URL "https://rdkcentral.github.io/Thunder/") From e9fbfad0a4c2dd5e9763ff291fd1c5a524644276 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Thu, 16 Apr 2026 14:51:44 +0530 Subject: [PATCH 43/64] WIP: test support library --- .../workflows/Test_Thunder_Test_Support.yml | 112 +++++ Tests/CMakeLists.txt | 5 + Tests/test_support/CMakeLists.txt | 98 ++++ Tests/test_support/Module.cpp | 9 + Tests/test_support/ThunderTestRuntime.cpp | 455 ++++++++++++++++++ Tests/test_support/ThunderTestRuntime.h | 182 +++++++ Tests/test_support/tests/CMakeLists.txt | 23 + Tests/test_support/tests/Module.cpp | 5 + Tests/test_support/tests/SmokeTest.cpp | 123 +++++ 9 files changed, 1012 insertions(+) create mode 100644 .github/workflows/Test_Thunder_Test_Support.yml create mode 100644 Tests/test_support/CMakeLists.txt create mode 100644 Tests/test_support/Module.cpp create mode 100644 Tests/test_support/ThunderTestRuntime.cpp create mode 100644 Tests/test_support/ThunderTestRuntime.h create mode 100644 Tests/test_support/tests/CMakeLists.txt create mode 100644 Tests/test_support/tests/Module.cpp create mode 100644 Tests/test_support/tests/SmokeTest.cpp diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml new file mode 100644 index 0000000000..a624b00932 --- /dev/null +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -0,0 +1,112 @@ +name: Test Thunder Test Support Library + +permissions: + contents: read + +on: + workflow_dispatch: + push: + branches: ["R5.3"] + paths: + - 'Tests/test_support/**' + - 'Source/Thunder/**' + - 'Source/core/**' + - 'Source/plugins/**' + pull_request: + branches: ["R5.3"] + paths: + - 'Tests/test_support/**' + - 'Source/Thunder/**' + - 'Source/core/**' + - 'Source/plugins/**' + +jobs: + SmokeTest: + runs-on: ubuntu-24.04 + + strategy: + matrix: + build_type: [Debug, Release] + + name: Smoke Test - ${{matrix.build_type}} + + steps: + - name: Install necessary packages + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 10 + command: | + sudo gem install apt-spy2 + sudo apt-spy2 fix --commit --launchpad --country=US + sudo apt-get update + sudo apt-get install -y python3-pip build-essential cmake ninja-build libusb-1.0-0-dev zlib1g-dev libssl-dev libgtest-dev + python3 -m venv venv + source venv/bin/activate + pip install jsonref + +# ----- Checkout ----- + - name: Checkout Thunder + uses: actions/checkout@v4 + with: + path: Thunder + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Checkout ThunderTools - default + if: ${{ !contains(github.event.pull_request.body, '[DependsOn=ThunderTools:') }} + uses: actions/checkout@v4 + with: + path: ThunderTools + repository: rdkcentral/ThunderTools + + - name: Regex ThunderTools + if: ${{ contains(github.event.pull_request.body, '[DependsOn=ThunderTools:') }} + id: tools + uses: AsasInnab/regex-action@v1 + with: + regex_pattern: '(?<=\[DependsOn=ThunderTools:).*(?=\])' + regex_flags: 'gim' + search_string: ${{github.event.pull_request.body}} + + - name: Checkout ThunderTools - ${{steps.tools.outputs.first_match}} + if: ${{ contains(github.event.pull_request.body, '[DependsOn=ThunderTools:') }} + uses: actions/checkout@v4 + with: + path: ThunderTools + repository: rdkcentral/ThunderTools + ref: ${{steps.tools.outputs.first_match}} + +# ----- Build ----- + - name: Build ThunderTools + run: | + source venv/bin/activate + cmake -G Ninja -S ThunderTools -B ${{matrix.build_type}}/build/ThunderTools \ + -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" + cmake --build ${{matrix.build_type}}/build/ThunderTools --target install + + - name: Build Thunder with test support + run: | + source venv/bin/activate + cmake -G Ninja -S Thunder -B ${{matrix.build_type}}/build/Thunder \ + -DBINDING="127.0.0.1" \ + -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ + -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ + -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/WPEFramework/Modules" \ + -DPORT="0" \ + -DENABLE_CXX17=OFF \ + -DENABLE_TEST_RUNTIME=ON + cmake --build ${{matrix.build_type}}/build/Thunder --target install + +# ----- Run smoke test ----- + - name: Run smoke test + run: | + LD_LIBRARY_PATH="${{matrix.build_type}}/install/usr/lib:$LD_LIBRARY_PATH" \ + ${{matrix.build_type}}/build/Thunder/Tests/test_support/tests/thunder_test_runtime_smoke \ + --gtest_output="xml:smoke-test-results.xml" \ + --gtest_color=yes + + - name: Upload test results + uses: actions/upload-artifact@v4 + with: + name: smoke-test-results-${{matrix.build_type}} + path: smoke-test-results.xml diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 79a328ea9c..220f102291 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -5,6 +5,7 @@ option(FILE_UNLINK_TEST "File unlink test" OFF) option(REDIRECT_TEST "Test stream redirection" OFF) option(MESSAGEBUFFER_TEST "Test message buffer" OFF) option(UNRAVELLER "reveal thread details" OFF) +option(ENABLE_TEST_RUNTIME "Build the in-process Thunder test runtime library" OFF) if(BUILD_TESTS) add_subdirectory(unit) @@ -40,4 +41,8 @@ endif() if(UNRAVELLER) add_subdirectory(unraveller) +endif() + +if(ENABLE_TEST_RUNTIME) + add_subdirectory(test_support) endif() \ No newline at end of file diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt new file mode 100644 index 0000000000..e2c483d351 --- /dev/null +++ b/Tests/test_support/CMakeLists.txt @@ -0,0 +1,98 @@ +# ========================================================================== +# thunder_test_support — static library for in-process Thunder plugin testing +# +# Compiles the test runtime together with the subset of Source/Thunder +# objects needed to host an embedded PluginHost::Server. The resulting +# archive is linked via whole-archive so that MODULE_NAME_DECLARATION +# statics are not dropped by the linker. +# ========================================================================== + +set(TARGET thunder_test_support) + +add_library(${TARGET} STATIC + ThunderTestRuntime.cpp + Module.cpp + ${CMAKE_SOURCE_DIR}/Source/Thunder/PluginServer.cpp + ${CMAKE_SOURCE_DIR}/Source/Thunder/Controller.cpp + ${CMAKE_SOURCE_DIR}/Source/Thunder/SystemInfo.cpp + ${CMAKE_SOURCE_DIR}/Source/Thunder/PostMortem.cpp + ${CMAKE_SOURCE_DIR}/Source/Thunder/Probe.cpp +) + +target_include_directories(${TARGET} + PUBLIC + $ + PRIVATE + ${CMAKE_SOURCE_DIR}/Source/Thunder +) + +target_compile_definitions(${TARGET} + PUBLIC + NAMESPACE=${NAMESPACE} + APPLICATION_NAME=ThunderTestRuntime + MODULE_NAME=ThunderTestRuntime + THREADPOOL_COUNT=${THREADPOOL_COUNT} + PRIVATE + DEFAULT_SYSTEM_PATH="${SYSTEM_PATH}" + DEFAULT_PROXYSTUB_PATH="${PROXYSTUB_PATH}" +) + +target_link_libraries(${TARGET} + PUBLIC + ${NAMESPACE}Core::${NAMESPACE}Core + ${NAMESPACE}Cryptalgo::${NAMESPACE}Cryptalgo + ${NAMESPACE}COM::${NAMESPACE}COM + ${NAMESPACE}Messaging::${NAMESPACE}Messaging + ${NAMESPACE}WebSocket::${NAMESPACE}WebSocket + ${NAMESPACE}Plugins::${NAMESPACE}Plugins + CompileSettingsDebug::CompileSettingsDebug + COMProcess::COMProcess + Threads::Threads +) + +# ------------------------------------------------------------------ +# Whole-archive link. Ensures that the MODULE_NAME_DECLARATION +# constructors from the Server objects are not discarded. +# ------------------------------------------------------------------ +if(APPLE) + target_link_options(${TARGET} INTERFACE + "SHELL:-Wl,-force_load,$" + ) +else() + target_link_options(${TARGET} INTERFACE + "SHELL:-Wl,--whole-archive $ -Wl,--no-whole-archive" + ) +endif() + +# ------------------------------------------------------------------ +# Optional features – only link when enabled in the main build. +# ------------------------------------------------------------------ +if(WARNING_REPORTING) + target_link_libraries(${TARGET} + PRIVATE + ${NAMESPACE}WarningReporting::${NAMESPACE}WarningReporting + ) + target_sources(${TARGET} + PRIVATE + ${CMAKE_SOURCE_DIR}/Source/Thunder/WarningReportingCategories.cpp + ) +endif() + +if(PROCESSCONTAINERS) + target_link_libraries(${TARGET} + PRIVATE + ${NAMESPACE}ProcessContainers::${NAMESPACE}ProcessContainers + ) +endif() + +if(HIBERNATESUPPORT) + target_link_libraries(${TARGET} + PRIVATE + ${NAMESPACE}Hibernate::${NAMESPACE}Hibernate + ) +endif() + +# ------------------------------------------------------------------ +# Smoke test sub-directory +# ------------------------------------------------------------------ +add_subdirectory(tests) diff --git a/Tests/test_support/Module.cpp b/Tests/test_support/Module.cpp new file mode 100644 index 0000000000..4202ae8bf2 --- /dev/null +++ b/Tests/test_support/Module.cpp @@ -0,0 +1,9 @@ +// Module definition for the thunder_test_support static library. +// +// MODULE_NAME is supplied by the target compile definitions so the embedded +// Source/Thunder objects and this archive-level declaration use the same +// module name. + +#include + +MODULE_NAME_ARCHIVE_DECLARATION diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp new file mode 100644 index 0000000000..34884e2512 --- /dev/null +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -0,0 +1,455 @@ +#include "ThunderTestRuntime.h" + +#include +#include +#include +#include + +// ========================================================================== +// ThunderTestRuntime implementation +// +// Lifecycle: Initialize() -> [run tests] -> Deinitialize() +// +// Initialize creates a unique temp directory tree under the platform temp root, +// writes a minimal Thunder config.json, parses it into PluginHost::Config, +// constructs PluginHost::Server, and calls Server::Open() which boots +// the controller and activates configured plugins. +// +// Deinitialize reverses the process: Server::Close(), cleanup temp files. +// ========================================================================== + +namespace Thunder { +namespace TestCore { + + // ================================================================== + // JSONRPCLink implementation + // ================================================================== + + ThunderTestRuntime::JSONRPCLink::JSONRPCLink(ThunderTestRuntime& runtime, const string& callsign) + : _runtime(runtime) + , _callsign(callsign) + , _dispatcher(nullptr) + { + Core::ProxyType shell = _runtime.GetShell(_callsign); + if (shell.IsValid()) { + _dispatcher = shell->QueryInterface(); + } + } + + ThunderTestRuntime::JSONRPCLink::~JSONRPCLink() + { + // Unsubscribe from all active events + if (_dispatcher != nullptr) { + std::lock_guard guard(_lock); + for (const auto& entry : _handlers) { + _dispatcher->Unsubscribe(this, entry.first, _callsign, string()); + } + _handlers.clear(); + _dispatcher->Release(); + } + } + + uint32_t ThunderTestRuntime::JSONRPCLink::Invoke(const string& method, + const string& params, + string& response) + { + uint32_t result = Core::ERROR_UNAVAILABLE; + + if (_dispatcher != nullptr) { + + string fullMethod = _callsign + '.' + method; + + if (_runtime.MethodExists(_dispatcher, _callsign, method) == false) { + result = Core::ERROR_UNKNOWN_KEY; + } else { + result = _dispatcher->Invoke(0, 0, string(), fullMethod, params, response); + } + } + + return result; + } + + uint32_t ThunderTestRuntime::JSONRPCLink::Subscribe(const string& event, EventHandler handler) + { + uint32_t result = Core::ERROR_UNAVAILABLE; + + if (_dispatcher != nullptr) { + + result = _dispatcher->Subscribe(this, event, _callsign, string()); + + if (result == Core::ERROR_NONE) { + std::lock_guard guard(_lock); + _handlers[event] = std::move(handler); + } + } + + return result; + } + + uint32_t ThunderTestRuntime::JSONRPCLink::Unsubscribe(const string& event) + { + uint32_t result = Core::ERROR_UNAVAILABLE; + + if (_dispatcher != nullptr) { + + result = _dispatcher->Unsubscribe(this, event, _callsign, string()); + + std::lock_guard guard(_lock); + _handlers.erase(event); + } + + return result; + } + + Core::hresult ThunderTestRuntime::JSONRPCLink::Event(const string& event, + const string& /* designator */, + const string& /* index */, + const string& parameters) + { + std::lock_guard guard(_lock); + + auto it = _handlers.find(event); + if (it != _handlers.end()) { + it->second(parameters); + } + + return Core::ERROR_NONE; + } + + // ================================================================== + // ThunderTestRuntime implementation + // ================================================================== + + ThunderTestRuntime::~ThunderTestRuntime() + { + Deinitialize(); + } + + bool ThunderTestRuntime::CreateDirectories() const + { + const string persistentPath = _tempDir + "persistent/"; + const string volatilePath = _tempDir + "volatile/"; + const string dataPath = _tempDir + "data/"; + + const bool created = + Core::Directory(persistentPath.c_str()).Create() && + Core::Directory(volatilePath.c_str()).Create() && + Core::Directory(dataPath.c_str()).Create(); + + if (created == false) { + TRACE_L1("ThunderTestRuntime: Failed to create temp directory tree at '%s'", _tempDir.c_str()); + } + + return created; + } + + void ThunderTestRuntime::CleanupDirectories() const + { + if (_tempDir.empty() == false) { + Core::Directory(_tempDir.c_str()).Destroy(); + + // Core::Directory::Destroy() does not remove the directory if the path + // ends with a trailing separator. Strip it before the final call. + string path = _tempDir; + while ((path.empty() == false) && (path.back() == '/' || path.back() == '\\')) { + path.pop_back(); + } + if (path.empty() == false) { + Core::Directory(path.c_str()).Destroy(); + } + } + } + + static string TemporaryRootPath() + { + string path; + static const char* variables[] = { "TMPDIR", "TEMP", "TMP" }; + + for (const char* variable : variables) { + if ((Core::SystemInfo::GetEnvironment(variable, path) == true) && (path.empty() == false)) { + return Core::Directory::Normalize(path); + } + } + +#ifdef __WINDOWS__ + return Core::Directory::Normalize("c:/temp"); +#else + return Core::Directory::Normalize("/tmp"); +#endif + } + + static bool CreateUniqueTemporaryDirectory(string& path) + { + const string root = TemporaryRootPath(); + + if (root.empty() == true) { + return false; + } + + Core::Directory rootDirectory(root.c_str()); + if ((rootDirectory.Exists() == false) && (rootDirectory.CreatePath() == false)) { + TRACE_L1("ThunderTestRuntime: Failed to create temporary root '%s'", root.c_str()); + return false; + } + + const string processId = Core::NumberType(static_cast(Core::ProcessInfo().Id())).Text(); + const string ticks = Core::NumberType(Core::Time::Now().Ticks()).Text(); + + for (uint8_t attempt = 0; attempt < 32; ++attempt) { + const string candidate = root + "thunder_test_" + processId + '_' + ticks + '_' + Core::NumberType(attempt).Text(); + Core::Directory directory(candidate.c_str()); + + if ((directory.Exists() == false) && (directory.Create() == true)) { + path = Core::Directory::Normalize(candidate); + return true; + } + } + + TRACE_L1("ThunderTestRuntime: Failed to create unique temporary directory under '%s'", root.c_str()); + return false; + } + + string ThunderTestRuntime::BuildConfigJSON(const std::vector& plugins, + const string& systemPath, + const string& proxyStubPath) const + { + const string communicatorPath = _tempDir + "communicator|0777"; + + JsonObject config; + JsonArray pluginList; + + config["port"] = 0; + config["binding"] = "127.0.0.1"; + config["idletime"] = 180; + config["persistentpath"] = _tempDir + "persistent/"; + config["volatilepath"] = _tempDir + "volatile/"; + config["datapath"] = _tempDir + "data/"; + config["systempath"] = systemPath; + config["proxystubpath"] = proxyStubPath; + config["communicator"] = communicatorPath; + + for (const auto& plugin : plugins) { + + string serializedPluginConfig; + JsonValue pluginValue; + Core::OptionalType error; + + plugin.ToString(serializedPluginConfig); + + if ((pluginValue.FromString(serializedPluginConfig, error) == false) || (pluginValue.IsValid() == false)) { + TRACE_L1("ThunderTestRuntime: Failed to serialize configuration for plugin '%s'", plugin.Callsign.Value().c_str()); + return string(); + } + + pluginList.Add(pluginValue); + } + + config["plugins"] = pluginList; + + string json; + config.ToString(json); + + return json; + } + + uint32_t ThunderTestRuntime::Initialize(const std::vector& plugins, + const string& systemPath, + const string& proxyStubPath) + { + if (_server != nullptr) { + return Core::ERROR_ALREADY_CONNECTED; + } + + // Create a unique temp directory for this test run using Thunder Core helpers. + if (CreateUniqueTemporaryDirectory(_tempDir) == false) { + return Core::ERROR_GENERAL; + } + + if (CreateDirectories() == false) { + CleanupDirectories(); + _tempDir.clear(); + return Core::ERROR_OPENING_FAILED; + } + + // Determine system path for plugin .so files + string sysPath = systemPath.empty() + ? Core::Directory::Normalize(DEFAULT_SYSTEM_PATH) + : Core::Directory::Normalize(systemPath); + + // Determine proxy stub path + _proxyStubPath = proxyStubPath.empty() + ? Core::Directory::Normalize(DEFAULT_PROXYSTUB_PATH) + : Core::Directory::Normalize(proxyStubPath); + + // Build and write config to temp file + string configJSON = BuildConfigJSON(plugins, sysPath, _proxyStubPath); + if (configJSON.empty()) { + CleanupDirectories(); + _tempDir.clear(); + return Core::ERROR_INCOMPLETE_CONFIG; + } + _configFilePath = _tempDir + "config.json"; + + { + std::ofstream configFile(_configFilePath); + if (!configFile.is_open()) { + CleanupDirectories(); + return Core::ERROR_OPENING_FAILED; + } + configFile << configJSON; + } + + // Parse config + Core::File configFile(_configFilePath); + if (configFile.Open(true) == false) { + CleanupDirectories(); + return Core::ERROR_OPENING_FAILED; + } + + Core::OptionalType error; + _config = new PluginHost::Config(configFile, false, error); + configFile.Close(); + + if (error.IsSet()) { + delete _config; + _config = nullptr; + CleanupDirectories(); + return Core::ERROR_INCOMPLETE_CONFIG; + } + + // Initialize the messaging subsystem (must happen before Server creation, + // mirrors the MessagingInitialization() call in the real PluginHost main()). + Messaging::MessageUnit::Settings::Config messagingConfig; + uint32_t messagingResult = Messaging::MessageUnit::Instance().Open( + _config->VolatilePath(), messagingConfig, false, Messaging::MessageUnit::flush::OFF); + + if (messagingResult != Core::ERROR_NONE) { + TRACE_L1("ThunderTestRuntime: Failed to initialize messaging subsystem (0x%08X)", messagingResult); + } + + // Create and start the server + _server = new PluginHost::Server(*_config, false); + _server->Open(); + + return Core::ERROR_NONE; + } + + ThunderTestRuntime::JSONRPCLink* ThunderTestRuntime::JSONRPCLink(const string& callsign) + { + return new class JSONRPCLink(*this, callsign); + } + + // Invoke a JSON-RPC method synchronously via the in-process dispatcher. + // Bypasses HTTP/WebSocket — calls IDispatcher::Invoke() directly. + // Callsign and method are parsed using Core::JSONRPC::Message helpers. + uint32_t ThunderTestRuntime::Invoke(const string& method, + const string& params, string& response) + { + uint32_t result = Core::ERROR_ILLEGAL_STATE; + + if (_server != nullptr) { + + string callsign = Core::JSONRPC::Message::Callsign(method); + string methodName = Core::JSONRPC::Message::Method(method); + + if (callsign.empty() == true) { + result = Core::ERROR_INVALID_SIGNATURE; + } else { + + Core::ProxyType shell; + result = _server->Services().FromIdentifier(callsign, shell); + + if (result == Core::ERROR_NONE) { + + PluginHost::IDispatcher* dispatcher = shell->QueryInterface(); + + if (dispatcher == nullptr) { + result = Core::ERROR_UNAVAILABLE; + } else { + + if (MethodExists(dispatcher, callsign, methodName) == false) { + result = Core::ERROR_UNKNOWN_KEY; + } else { + result = dispatcher->Invoke(0, 0, string(), method, params, response); + } + + dispatcher->Release(); + } + } + } + } + + return result; + } + + bool ThunderTestRuntime::MethodExists(PluginHost::IDispatcher* dispatcher, + const string& callsign, + const string& methodName) const + { + bool found = false; + + JsonObject existsParams; + existsParams["method"] = methodName; + + string serializedParams; + existsParams.ToString(serializedParams); + + string existsResponse; + dispatcher->Invoke(0, 0, string(), callsign + ".exists", serializedParams, existsResponse); + + Core::JSON::Boolean available; + available.FromString(existsResponse); + found = available.Value(); + + return found; + } + + Core::ProxyType ThunderTestRuntime::GetShell(const string& callsign) + { + Core::ProxyType shell; + if (_server != nullptr) { + _server->Services().FromIdentifier(callsign, shell); + } + return shell; + } + + PluginHost::Server& ThunderTestRuntime::Server() + { + ASSERT(_server != nullptr); + return *_server; + } + + string ThunderTestRuntime::CommunicatorPath() const + { + if (_config != nullptr) { + return _config->Communicator().HostName(); + } + return string(); + } + + void ThunderTestRuntime::Deinitialize() + { + if (_server != nullptr) { + _server->Close(); + delete _server; + _server = nullptr; + } + + if (_config != nullptr) { + delete _config; + _config = nullptr; + } + + Messaging::MessageUnit::Instance().Close(); + + if (_configFilePath.empty() == false) { + Core::File(_configFilePath).Destroy(); + _configFilePath.clear(); + } + + CleanupDirectories(); + _tempDir.clear(); + } + +} // namespace TestCore +} // namespace Thunder diff --git a/Tests/test_support/ThunderTestRuntime.h b/Tests/test_support/ThunderTestRuntime.h new file mode 100644 index 0000000000..e5447d32f9 --- /dev/null +++ b/Tests/test_support/ThunderTestRuntime.h @@ -0,0 +1,182 @@ +#pragma once + +// ========================================================================== +// ThunderTestRuntime — Public API for in-process Thunder plugin testing +// +// Provides a lightweight wrapper around PluginHost::Server that: +// - Creates an isolated temp directory per test run +// - Generates a minimal Thunder config (JSON) on the fly +// - Boots the embedded server and activates the controller +// - Exposes helpers for JSON-RPC invocation, event handling, +// and COM-RPC queries +// - Tears everything down cleanly on Deinitialize() +// +// Typical usage in a GTest fixture: +// +// static ThunderTestRuntime _runtime; +// +// static void SetUpTestSuite() { +// std::vector plugins = { ... }; +// _runtime.Initialize(plugins, pluginPath, proxyStubPath); +// } +// static void TearDownTestSuite() { _runtime.Deinitialize(); } +// +// TEST_F(MyTest, JsonRpc) { +// string resp; +// auto link = _runtime.JSONRPCLink("MyPlugin"); +// link.Invoke("someMethod", R"({"param":1})", resp); +// } +// +// TEST_F(MyTest, Events) { +// auto link = _runtime.JSONRPCLink("MyPlugin"); +// link.Subscribe("onSomethingChanged", +// [](const string& params) { /* handle event */ }); +// // ... trigger event ... +// link.Unsubscribe("onSomethingChanged"); +// } +// +// TEST_F(MyTest, ComRpc) { +// auto* iface = _runtime.GetInterface("MyPlugin"); +// } +// ========================================================================== + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Thunder { + +namespace PluginHost { + class Server; + class Config; +} + +namespace TestCore { + + class ThunderTestRuntime { + public: + // Reuse the real Thunder plugin configuration type. + // Key fields: Callsign, Locator, ClassName, StartupOrder, StartMode, Configuration. + using PluginConfig = Plugin::Config; + + // ------------------------------------------------------------------ + // JSONRPCLink — callsign-bound helper for JSON-RPC calls and events + // ------------------------------------------------------------------ + class JSONRPCLink : public PluginHost::IDispatcher::ICallback { + public: + using EventHandler = std::function; + + JSONRPCLink(ThunderTestRuntime& runtime, const string& callsign); + ~JSONRPCLink() override; + + JSONRPCLink(const JSONRPCLink&) = delete; + JSONRPCLink& operator=(const JSONRPCLink&) = delete; + + // JSON-RPC method invocation (callsign is implicit). + // Method is the bare method name (e.g. "getLogLevel"), not "Callsign.getLogLevel". + uint32_t Invoke(const string& method, const string& params, string& response); + + // Subscribe to a JSON-RPC event with a callback. + uint32_t Subscribe(const string& event, EventHandler handler); + + // Unsubscribe from a previously subscribed event. + uint32_t Unsubscribe(const string& event); + + const string& Callsign() const { return _callsign; } + + // IDispatcher::ICallback + void AddRef() const override {} + uint32_t Release() const override { return Core::ERROR_NONE; } + + private: + // IDispatcher::ICallback + Core::hresult Event(const string& event, const string& designator, + const string& index, const string& parameters) override; + + BEGIN_INTERFACE_MAP(JSONRPCLink) + INTERFACE_ENTRY(PluginHost::IDispatcher::ICallback) + END_INTERFACE_MAP + + ThunderTestRuntime& _runtime; + string _callsign; + PluginHost::IDispatcher* _dispatcher; + + mutable std::mutex _lock; + std::unordered_map _handlers; + }; + + ThunderTestRuntime() = default; + ~ThunderTestRuntime(); + + ThunderTestRuntime(const ThunderTestRuntime&) = delete; + ThunderTestRuntime& operator=(const ThunderTestRuntime&) = delete; + + // Boot the embedded Thunder server with the given plugins. + // Creates temp directories, generates config, parses it, and calls Server::Open(). + // systemPath — directory containing plugin .so files + // proxyStubPath — directory containing proxy stub .so files + // Returns Core::ERROR_NONE on success. + uint32_t Initialize(const std::vector& plugins, + const string& systemPath = "", + const string& proxyStubPath = ""); + + // Create a callsign-bound JSON-RPC link for invoke and event operations. + // Caller owns the returned object. + JSONRPCLink* JSONRPCLink(const string& callsign); + + // Invoke a JSON-RPC method on a loaded plugin (full designator form). + // Method format: "Callsign.method" (e.g. "MyPlugin.doSomething") + // Returns Core::ERROR_UNAVAILABLE if the JSON-RPC endpoint is not available. + uint32_t Invoke(const string& method, const string& params, string& response); + + // Obtain a COM-RPC interface from a loaded plugin. + // Caller must Release() the returned pointer when done. + template + INTERFACE* GetInterface(const string& callsign) + { + INTERFACE* result = nullptr; + Core::ProxyType shell = GetShell(callsign); + if (shell.IsValid()) { + result = shell->QueryInterface(); + } + return result; + } + + // Get the IShell proxy for a plugin (for activation/deactivation control). + Core::ProxyType GetShell(const string& callsign); + + // Direct access to the underlying PluginHost::Server. + PluginHost::Server& Server(); + + // Returns the domain socket path used by the communicator. + string CommunicatorPath() const; + + // Stop the server, release config, and clean up temp directories. + void Deinitialize(); + + private: + string BuildConfigJSON(const std::vector& plugins, + const string& systemPath, + const string& proxyStubPath) const; + bool MethodExists(PluginHost::IDispatcher* dispatcher, + const string& callsign, + const string& methodName) const; + bool CreateDirectories() const; + void CleanupDirectories() const; + + PluginHost::Config* _config = nullptr; + PluginHost::Server* _server = nullptr; + string _tempDir; + string _configFilePath; + string _proxyStubPath; + }; + +} // namespace TestCore +} // namespace Thunder diff --git a/Tests/test_support/tests/CMakeLists.txt b/Tests/test_support/tests/CMakeLists.txt new file mode 100644 index 0000000000..5e6463e706 --- /dev/null +++ b/Tests/test_support/tests/CMakeLists.txt @@ -0,0 +1,23 @@ +set(TARGET thunder_test_runtime_smoke) + +find_package(GTest REQUIRED) + +add_executable(${TARGET} + SmokeTest.cpp + Module.cpp +) + +target_compile_definitions(${TARGET} + PRIVATE + MODULE_NAME=SmokeTest + BUILD_REFERENCE=${BUILD_REFERENCE} +) + +target_link_libraries(${TARGET} + PRIVATE + thunder_test_support + GTest::GTest + GTest::Main +) + +add_test(NAME ${TARGET} COMMAND ${TARGET}) diff --git a/Tests/test_support/tests/Module.cpp b/Tests/test_support/tests/Module.cpp new file mode 100644 index 0000000000..5f7e111ab5 --- /dev/null +++ b/Tests/test_support/tests/Module.cpp @@ -0,0 +1,5 @@ +#define MODULE_NAME SmokeTest + +#include + +MODULE_NAME_DECLARATION(BUILD_REFERENCE) diff --git a/Tests/test_support/tests/SmokeTest.cpp b/Tests/test_support/tests/SmokeTest.cpp new file mode 100644 index 0000000000..27332b9ee3 --- /dev/null +++ b/Tests/test_support/tests/SmokeTest.cpp @@ -0,0 +1,123 @@ +// ========================================================================== +// SmokeTest — validates the ThunderTestRuntime boots and tears down cleanly. +// +// These tests exercise: +// 1. Invoke() with the full "Callsign.method" form +// 2. JSONRPCLink (callsign-bound) invocation +// 3. GetShell() COM-RPC path +// ========================================================================== + +#include "ThunderTestRuntime.h" +#include +#include +#include + +namespace Thunder { +namespace TestCore { +namespace Tests { + + class SmokeTest : public ::testing::Test { + protected: + static ThunderTestRuntime _runtime; + + static void SetUpTestSuite() + { + std::vector plugins; + + uint32_t result = _runtime.Initialize(plugins); + ASSERT_EQ(result, Core::ERROR_NONE) << "Failed to initialize Thunder runtime"; + } + + static void TearDownTestSuite() + { + _runtime.Deinitialize(); + } + }; + + ThunderTestRuntime SmokeTest::_runtime; + + // ------------------------------------------------------------------ + // Full-designator Invoke: "Controller.status" + // ------------------------------------------------------------------ + TEST_F(SmokeTest, ControllerStatusViaFullDesignator) + { + string response; + uint32_t result = _runtime.Invoke("Controller.status", "{}", response); + EXPECT_EQ(result, Core::ERROR_NONE) << "Controller.status returned: " << result; + EXPECT_FALSE(response.empty()); + } + + // ------------------------------------------------------------------ + // Full-designator Invoke: "Controller.subsystems" + // ------------------------------------------------------------------ + TEST_F(SmokeTest, ControllerSubsystemsViaFullDesignator) + { + string response; + uint32_t result = _runtime.Invoke("Controller.subsystems", "{}", response); + EXPECT_EQ(result, Core::ERROR_NONE) << "Controller.subsystems returned: " << result; + EXPECT_FALSE(response.empty()); + } + + // ------------------------------------------------------------------ + // JSONRPCLink (callsign-bound): invoke without repeating callsign + // ------------------------------------------------------------------ + TEST_F(SmokeTest, ControllerStatusViaJSONRPCLink) + { + auto* controller = _runtime.JSONRPCLink("Controller"); + ASSERT_NE(controller, nullptr); + + string response; + uint32_t result = controller->Invoke("status", "{}", response); + EXPECT_EQ(result, Core::ERROR_NONE) << "status via link returned: " << result; + EXPECT_FALSE(response.empty()); + + delete controller; + } + + // ------------------------------------------------------------------ + // GetShell: obtain the IShell for the Controller + // ------------------------------------------------------------------ + TEST_F(SmokeTest, GetControllerShell) + { + auto shell = _runtime.GetShell("Controller"); + EXPECT_TRUE(shell.IsValid()) << "Controller IShell must be available"; + } + + // ------------------------------------------------------------------ + // Unknown method returns ERROR_UNKNOWN_KEY + // ------------------------------------------------------------------ + TEST_F(SmokeTest, UnknownMethodReturnsError) + { + string response; + uint32_t result = _runtime.Invoke("Controller.thisMethodDoesNotExist", "{}", response); + EXPECT_EQ(result, Core::ERROR_UNKNOWN_KEY); + } + + // ------------------------------------------------------------------ + // Unknown method via JSONRPCLink + // ------------------------------------------------------------------ + TEST_F(SmokeTest, UnknownMethodViaJSONRPCLinkReturnsError) + { + auto* controller = _runtime.JSONRPCLink("Controller"); + ASSERT_NE(controller, nullptr); + + string response; + uint32_t result = controller->Invoke("thisMethodDoesNotExist", "{}", response); + EXPECT_EQ(result, Core::ERROR_UNKNOWN_KEY); + + delete controller; + } + + // ------------------------------------------------------------------ + // Missing callsign in full-designator form returns error + // ------------------------------------------------------------------ + TEST_F(SmokeTest, MissingCallsignReturnsError) + { + string response; + uint32_t result = _runtime.Invoke("noCallsignDot", "{}", response); + EXPECT_NE(result, Core::ERROR_NONE); + } + +} // namespace Tests +} // namespace TestCore +} // namespace Thunder From 5f0f65c64f45c6228a1057463e54de02398861ca Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Fri, 17 Apr 2026 10:16:39 +0530 Subject: [PATCH 44/64] Resolve compilation issues --- .../workflows/Test_Thunder_Test_Support.yml | 3 +- Tests/test_support/CMakeLists.txt | 63 +++++++++++++------ Tests/test_support/ThunderTestRuntime.cpp | 2 +- Tests/test_support/ThunderTestRuntime.h | 4 +- Tests/test_support/tests/SmokeTest.cpp | 4 +- 5 files changed, 51 insertions(+), 25 deletions(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index a624b00932..8d8f05f21b 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -50,7 +50,7 @@ jobs: uses: actions/checkout@v4 with: path: Thunder - ref: ${{ github.event.pull_request.head.sha || github.sha }} + # ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Checkout ThunderTools - default if: ${{ !contains(github.event.pull_request.body, '[DependsOn=ThunderTools:') }} @@ -93,7 +93,6 @@ jobs: -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/WPEFramework/Modules" \ -DPORT="0" \ - -DENABLE_CXX17=OFF \ -DENABLE_TEST_RUNTIME=ON cmake --build ${{matrix.build_type}}/build/Thunder --target install diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt index e2c483d351..01a8edfee9 100644 --- a/Tests/test_support/CMakeLists.txt +++ b/Tests/test_support/CMakeLists.txt @@ -7,36 +7,60 @@ # statics are not dropped by the linker. # ========================================================================== +find_package(Threads REQUIRED) + +set(THUNDER_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../Source/Thunder") + +set(THREADPOOL_COUNT "4" CACHE STRING "The number of threads in the thread pool for test runtime") + set(TARGET thunder_test_support) add_library(${TARGET} STATIC ThunderTestRuntime.cpp Module.cpp - ${CMAKE_SOURCE_DIR}/Source/Thunder/PluginServer.cpp - ${CMAKE_SOURCE_DIR}/Source/Thunder/Controller.cpp - ${CMAKE_SOURCE_DIR}/Source/Thunder/SystemInfo.cpp - ${CMAKE_SOURCE_DIR}/Source/Thunder/PostMortem.cpp - ${CMAKE_SOURCE_DIR}/Source/Thunder/Probe.cpp + ${THUNDER_SOURCE_DIR}/PluginServer.cpp + ${THUNDER_SOURCE_DIR}/Controller.cpp + ${THUNDER_SOURCE_DIR}/SystemInfo.cpp + ${THUNDER_SOURCE_DIR}/PostMortem.cpp + ${THUNDER_SOURCE_DIR}/Probe.cpp ) target_include_directories(${TARGET} PUBLIC - $ + ${CMAKE_CURRENT_SOURCE_DIR} PRIVATE - ${CMAKE_SOURCE_DIR}/Source/Thunder + ${THUNDER_SOURCE_DIR} + $ + $ ) target_compile_definitions(${TARGET} - PUBLIC + PRIVATE NAMESPACE=${NAMESPACE} APPLICATION_NAME=ThunderTestRuntime MODULE_NAME=ThunderTestRuntime THREADPOOL_COUNT=${THREADPOOL_COUNT} - PRIVATE DEFAULT_SYSTEM_PATH="${SYSTEM_PATH}" DEFAULT_PROXYSTUB_PATH="${PROXYSTUB_PATH}" ) +target_compile_options(${TARGET} PRIVATE -Wno-psabi) + +set_target_properties(${TARGET} PROPERTIES + CXX_STANDARD 11 + CXX_STANDARD_REQUIRED YES +) + +target_link_libraries(${TARGET} + PRIVATE + CompileSettings::CompileSettings + CompileSettingsDebug::CompileSettingsDebug +) + +if(EXCEPTION_CATCHING) + set_source_files_properties(${THUNDER_SOURCE_DIR}/PluginServer.cpp PROPERTIES COMPILE_FLAGS "-fexceptions") +endif() + target_link_libraries(${TARGET} PUBLIC ${NAMESPACE}Core::${NAMESPACE}Core @@ -45,8 +69,7 @@ target_link_libraries(${TARGET} ${NAMESPACE}Messaging::${NAMESPACE}Messaging ${NAMESPACE}WebSocket::${NAMESPACE}WebSocket ${NAMESPACE}Plugins::${NAMESPACE}Plugins - CompileSettingsDebug::CompileSettingsDebug - COMProcess::COMProcess + ${NAMESPACE}COMProcess::${NAMESPACE}COMProcess Threads::Threads ) @@ -68,28 +91,32 @@ endif() # Optional features – only link when enabled in the main build. # ------------------------------------------------------------------ if(WARNING_REPORTING) - target_link_libraries(${TARGET} - PRIVATE - ${NAMESPACE}WarningReporting::${NAMESPACE}WarningReporting - ) target_sources(${TARGET} PRIVATE - ${CMAKE_SOURCE_DIR}/Source/Thunder/WarningReportingCategories.cpp + ${THUNDER_SOURCE_DIR}/WarningReportingCategories.cpp ) endif() if(PROCESSCONTAINERS) target_link_libraries(${TARGET} - PRIVATE + PUBLIC ${NAMESPACE}ProcessContainers::${NAMESPACE}ProcessContainers ) + target_compile_definitions(${TARGET} + PUBLIC + PROCESSCONTAINERS_ENABLED=1 + ) endif() if(HIBERNATESUPPORT) target_link_libraries(${TARGET} - PRIVATE + PUBLIC ${NAMESPACE}Hibernate::${NAMESPACE}Hibernate ) + target_compile_definitions(${TARGET} + PUBLIC + HIBERNATE_SUPPORT_ENABLED=1 + ) endif() # ------------------------------------------------------------------ diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp index 34884e2512..5623582eb1 100644 --- a/Tests/test_support/ThunderTestRuntime.cpp +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -334,7 +334,7 @@ namespace TestCore { return Core::ERROR_NONE; } - ThunderTestRuntime::JSONRPCLink* ThunderTestRuntime::JSONRPCLink(const string& callsign) + ThunderTestRuntime::JSONRPCLink* ThunderTestRuntime::CreateJSONRPCLink(const string& callsign) { return new class JSONRPCLink(*this, callsign); } diff --git a/Tests/test_support/ThunderTestRuntime.h b/Tests/test_support/ThunderTestRuntime.h index e5447d32f9..0c38e3800c 100644 --- a/Tests/test_support/ThunderTestRuntime.h +++ b/Tests/test_support/ThunderTestRuntime.h @@ -92,7 +92,7 @@ namespace TestCore { const string& Callsign() const { return _callsign; } // IDispatcher::ICallback - void AddRef() const override {} + uint32_t AddRef() const override { return Core::ERROR_NONE; } uint32_t Release() const override { return Core::ERROR_NONE; } private: @@ -129,7 +129,7 @@ namespace TestCore { // Create a callsign-bound JSON-RPC link for invoke and event operations. // Caller owns the returned object. - JSONRPCLink* JSONRPCLink(const string& callsign); + class JSONRPCLink* CreateJSONRPCLink(const string& callsign); // Invoke a JSON-RPC method on a loaded plugin (full designator form). // Method format: "Callsign.method" (e.g. "MyPlugin.doSomething") diff --git a/Tests/test_support/tests/SmokeTest.cpp b/Tests/test_support/tests/SmokeTest.cpp index 27332b9ee3..5ed057a14a 100644 --- a/Tests/test_support/tests/SmokeTest.cpp +++ b/Tests/test_support/tests/SmokeTest.cpp @@ -63,7 +63,7 @@ namespace Tests { // ------------------------------------------------------------------ TEST_F(SmokeTest, ControllerStatusViaJSONRPCLink) { - auto* controller = _runtime.JSONRPCLink("Controller"); + auto* controller = _runtime.CreateJSONRPCLink("Controller"); ASSERT_NE(controller, nullptr); string response; @@ -98,7 +98,7 @@ namespace Tests { // ------------------------------------------------------------------ TEST_F(SmokeTest, UnknownMethodViaJSONRPCLinkReturnsError) { - auto* controller = _runtime.JSONRPCLink("Controller"); + auto* controller = _runtime.CreateJSONRPCLink("Controller"); ASSERT_NE(controller, nullptr); string response; From e03f7a2cb784f19b567f92b2d00ae5b8099b7c1d Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Mon, 20 Apr 2026 13:55:21 +0530 Subject: [PATCH 45/64] Add Test plugin test to workflow --- .../workflows/Test_Thunder_Test_Support.yml | 91 ++++++++- Tests/test_support/Module.cpp | 8 +- Tests/test_support/Module.h | 10 + Tests/test_support/ThunderTestRuntime.cpp | 2 +- Tests/test_support/tests/CMakeLists.txt | 33 +++ Tests/test_support/tests/Module.cpp | 4 +- Tests/test_support/tests/Module.h | 10 + Tests/test_support/tests/SmokeTest.cpp | 2 +- Tests/test_support/tests/TestPluginTest.cpp | 190 ++++++++++++++++++ 9 files changed, 334 insertions(+), 16 deletions(-) create mode 100644 Tests/test_support/Module.h create mode 100644 Tests/test_support/tests/Module.h create mode 100644 Tests/test_support/tests/TestPluginTest.cpp diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index 8d8f05f21b..2340e320b8 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -76,6 +76,58 @@ jobs: repository: rdkcentral/ThunderTools ref: ${{steps.tools.outputs.first_match}} + - name: Checkout ThunderInterfaces - default + if: ${{ !contains(github.event.pull_request.body, '[DependsOn=ThunderInterfaces:') }} + uses: actions/checkout@v4 + with: + path: ThunderInterfaces + repository: rdkcentral/ThunderInterfaces + # TODO: Remove ref once qa_interfaces changes are merged to master + ref: development/test-support + + - name: Regex ThunderInterfaces + if: ${{ contains(github.event.pull_request.body, '[DependsOn=ThunderInterfaces:') }} + id: interfaces + uses: AsasInnab/regex-action@v1 + with: + regex_pattern: '(?<=\[DependsOn=ThunderInterfaces:).*(?=\])' + regex_flags: 'gim' + search_string: ${{github.event.pull_request.body}} + + - name: Checkout ThunderInterfaces - ${{steps.interfaces.outputs.first_match}} + if: ${{ contains(github.event.pull_request.body, '[DependsOn=ThunderInterfaces:') }} + uses: actions/checkout@v4 + with: + path: ThunderInterfaces + repository: rdkcentral/ThunderInterfaces + ref: ${{steps.interfaces.outputs.first_match}} + + - name: Checkout ThunderNanoServices - default + if: ${{ !contains(github.event.pull_request.body, '[DependsOn=ThunderNanoServices:') }} + uses: actions/checkout@v4 + with: + path: ThunderNanoServices + repository: rdkcentral/ThunderNanoServices + # TODO: Remove ref once TestPlugin changes are merged to master + ref: development/test-support + + - name: Regex ThunderNanoServices + if: ${{ contains(github.event.pull_request.body, '[DependsOn=ThunderNanoServices:') }} + id: nanoservices + uses: AsasInnab/regex-action@v1 + with: + regex_pattern: '(?<=\[DependsOn=ThunderNanoServices:).*(?=\])' + regex_flags: 'gim' + search_string: ${{github.event.pull_request.body}} + + - name: Checkout ThunderNanoServices - ${{steps.nanoservices.outputs.first_match}} + if: ${{ contains(github.event.pull_request.body, '[DependsOn=ThunderNanoServices:') }} + uses: actions/checkout@v4 + with: + path: ThunderNanoServices + repository: rdkcentral/ThunderNanoServices + ref: ${{steps.nanoservices.outputs.first_match}} + # ----- Build ----- - name: Build ThunderTools run: | @@ -91,11 +143,32 @@ jobs: -DBINDING="127.0.0.1" \ -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ - -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/WPEFramework/Modules" \ + -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/Thunder/Modules" \ -DPORT="0" \ - -DENABLE_TEST_RUNTIME=ON + -DENABLE_CXX17=OFF \ + -DENABLE_TEST_RUNTIME=ON \ + -DTEST_PLUGIN_PATH="${PWD}/${{matrix.build_type}}/install/usr/lib/thunder/plugins" cmake --build ${{matrix.build_type}}/build/Thunder --target install + - name: Build ThunderInterfaces + run: | + source venv/bin/activate + cmake -G Ninja -S ThunderInterfaces -B ${{matrix.build_type}}/build/ThunderInterfaces \ + -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ + -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ + -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/Thunder/Modules" + cmake --build ${{matrix.build_type}}/build/ThunderInterfaces --target install + + - name: Build ThunderNanoServices TestPlugin + run: | + source venv/bin/activate + cmake -G Ninja -S ThunderNanoServices/tests -B ${{matrix.build_type}}/build/ThunderNanoServicesTests \ + -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ + -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ + -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/Thunder/Modules" \ + -DPLUGIN_TESTPLUGIN=ON + cmake --build ${{matrix.build_type}}/build/ThunderNanoServicesTests --target install + # ----- Run smoke test ----- - name: Run smoke test run: | @@ -104,8 +177,18 @@ jobs: --gtest_output="xml:smoke-test-results.xml" \ --gtest_color=yes +# ----- Run plugin test ----- + - name: Run plugin test (COM-RPC + JSON-RPC) + run: | + LD_LIBRARY_PATH="${{matrix.build_type}}/install/usr/lib:${PWD}/${{matrix.build_type}}/install/usr/lib/thunder/plugins:$LD_LIBRARY_PATH" \ + ${{matrix.build_type}}/build/Thunder/Tests/test_support/tests/thunder_test_runtime_plugin \ + --gtest_output="xml:plugin-test-results.xml" \ + --gtest_color=yes + - name: Upload test results uses: actions/upload-artifact@v4 with: - name: smoke-test-results-${{matrix.build_type}} - path: smoke-test-results.xml + name: test-results-${{matrix.build_type}} + path: | + smoke-test-results.xml + plugin-test-results.xml diff --git a/Tests/test_support/Module.cpp b/Tests/test_support/Module.cpp index 4202ae8bf2..e328ce1e4d 100644 --- a/Tests/test_support/Module.cpp +++ b/Tests/test_support/Module.cpp @@ -1,9 +1,3 @@ -// Module definition for the thunder_test_support static library. -// -// MODULE_NAME is supplied by the target compile definitions so the embedded -// Source/Thunder objects and this archive-level declaration use the same -// module name. - -#include +#include "Module.h" MODULE_NAME_ARCHIVE_DECLARATION diff --git a/Tests/test_support/Module.h b/Tests/test_support/Module.h new file mode 100644 index 0000000000..a54bdb5637 --- /dev/null +++ b/Tests/test_support/Module.h @@ -0,0 +1,10 @@ +#pragma once + +#ifndef MODULE_NAME +#define MODULE_NAME ThunderTestRuntime +#endif + +#include + +#undef EXTERNAL +#define EXTERNAL diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp index 5623582eb1..c8eb660c2f 100644 --- a/Tests/test_support/ThunderTestRuntime.cpp +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -1,7 +1,7 @@ +#include "Module.h" #include "ThunderTestRuntime.h" #include -#include #include #include diff --git a/Tests/test_support/tests/CMakeLists.txt b/Tests/test_support/tests/CMakeLists.txt index 5e6463e706..67b7f32e44 100644 --- a/Tests/test_support/tests/CMakeLists.txt +++ b/Tests/test_support/tests/CMakeLists.txt @@ -21,3 +21,36 @@ target_link_libraries(${TARGET} ) add_test(NAME ${TARGET} COMMAND ${TARGET}) + +# ------------------------------------------------------------------ +# TestPlugin integration test +# +# Requires: +# - ThunderInterfaces qa_interfaces headers installed (for ITestPlugin.h) +# - ThunderNanoServices TestPlugin .so built and path passed via +# TEST_PLUGIN_PATH (defaults to the Thunder install plugins dir) +# ------------------------------------------------------------------ +set(PLUGIN_TEST_TARGET thunder_test_runtime_plugin) + +set(TEST_PLUGIN_PATH "" CACHE PATH "Directory containing libThunderTestPlugin.so") + +add_executable(${PLUGIN_TEST_TARGET} + TestPluginTest.cpp + Module.cpp +) + +target_compile_definitions(${PLUGIN_TEST_TARGET} + PRIVATE + MODULE_NAME=TestPluginTest + BUILD_REFERENCE=${BUILD_REFERENCE} + TEST_PLUGIN_PATH="${TEST_PLUGIN_PATH}" +) + +target_link_libraries(${PLUGIN_TEST_TARGET} + PRIVATE + thunder_test_support + GTest::GTest + GTest::Main +) + +add_test(NAME ${PLUGIN_TEST_TARGET} COMMAND ${PLUGIN_TEST_TARGET}) diff --git a/Tests/test_support/tests/Module.cpp b/Tests/test_support/tests/Module.cpp index 5f7e111ab5..2d85ed902b 100644 --- a/Tests/test_support/tests/Module.cpp +++ b/Tests/test_support/tests/Module.cpp @@ -1,5 +1,3 @@ -#define MODULE_NAME SmokeTest - -#include +#include "Module.h" MODULE_NAME_DECLARATION(BUILD_REFERENCE) diff --git a/Tests/test_support/tests/Module.h b/Tests/test_support/tests/Module.h new file mode 100644 index 0000000000..c8b64442c0 --- /dev/null +++ b/Tests/test_support/tests/Module.h @@ -0,0 +1,10 @@ +#pragma once + +#ifndef MODULE_NAME +#define MODULE_NAME SmokeTest +#endif + +#include + +#undef EXTERNAL +#define EXTERNAL diff --git a/Tests/test_support/tests/SmokeTest.cpp b/Tests/test_support/tests/SmokeTest.cpp index 5ed057a14a..ef75e81797 100644 --- a/Tests/test_support/tests/SmokeTest.cpp +++ b/Tests/test_support/tests/SmokeTest.cpp @@ -7,9 +7,9 @@ // 3. GetShell() COM-RPC path // ========================================================================== +#include "Module.h" #include "ThunderTestRuntime.h" #include -#include #include namespace Thunder { diff --git a/Tests/test_support/tests/TestPluginTest.cpp b/Tests/test_support/tests/TestPluginTest.cpp new file mode 100644 index 0000000000..3104e550fa --- /dev/null +++ b/Tests/test_support/tests/TestPluginTest.cpp @@ -0,0 +1,190 @@ +// ========================================================================== +// TestPluginTest — validates that the test runtime can load a plugin +// and interact with it via both COM-RPC and JSON-RPC. +// +// COM-RPC tests use QueryInterface to call methods directly. +// JSON-RPC tests use ThunderTestRuntime::Invoke() and JSONRPCLink. +// +// The TestPlugin is built as a shared library and placed in +// ${CMAKE_BINARY_DIR}/test_plugins/. The test passes that directory as +// the systemPath so the embedded server can dlopen it. +// ========================================================================== + +#include "Module.h" +#include "ThunderTestRuntime.h" +#include +#include +#include + +#ifndef TEST_PLUGIN_PATH +#error "TEST_PLUGIN_PATH must be defined by CMake" +#endif + +namespace Thunder { +namespace TestCore { +namespace Tests { + + class TestPluginTest : public ::testing::Test { + protected: + static ThunderTestRuntime _runtime; + + static void SetUpTestSuite() + { + ThunderTestRuntime::PluginConfig dummyConfig; + dummyConfig.Callsign = "TestPlugin"; + dummyConfig.ClassName = "TestPlugin"; + dummyConfig.Locator = "libThunderTestPlugin.so"; + dummyConfig.StartMode = PluginHost::IShell::startmode::ACTIVATED; + + std::vector plugins; + plugins.push_back(dummyConfig); + + uint32_t result = _runtime.Initialize(plugins, TEST_PLUGIN_PATH); + ASSERT_EQ(result, Core::ERROR_NONE) << "Failed to initialize runtime with TestPlugin"; + } + + static void TearDownTestSuite() + { + _runtime.Deinitialize(); + } + }; + + ThunderTestRuntime TestPluginTest::_runtime; + + // ================================================================== + // Plugin lifecycle + // ================================================================== + + TEST_F(TestPluginTest, PluginIsActivated) + { + auto shell = _runtime.GetShell("TestPlugin"); + EXPECT_TRUE(shell.IsValid()) << "TestPlugin IShell must be available"; + if (shell.IsValid()) { + EXPECT_EQ(shell->State(), PluginHost::IShell::state::ACTIVATED); + } + } + + // ================================================================== + // COM-RPC path (QueryInterface) + // ================================================================== + + TEST_F(TestPluginTest, COMRPC_QueryInterfaceSucceeds) + { + auto* iface = _runtime.GetInterface("TestPlugin"); + ASSERT_NE(iface, nullptr) << "QueryInterface must succeed"; + iface->Release(); + } + + TEST_F(TestPluginTest, COMRPC_EchoReturnsInput) + { + auto* iface = _runtime.GetInterface("TestPlugin"); + ASSERT_NE(iface, nullptr); + + string output; + uint32_t result = iface->Echo("hello", output); + EXPECT_EQ(result, Core::ERROR_NONE); + EXPECT_EQ(output, "hello"); + + iface->Release(); + } + + TEST_F(TestPluginTest, COMRPC_GreetReturnsMessage) + { + auto* iface = _runtime.GetInterface("TestPlugin"); + ASSERT_NE(iface, nullptr); + + string message; + uint32_t result = iface->Greet("Thunder", message); + EXPECT_EQ(result, Core::ERROR_NONE); + EXPECT_EQ(message, "Hello, Thunder!"); + + iface->Release(); + } + + TEST_F(TestPluginTest, COMRPC_GreetDefaultsToWorld) + { + auto* iface = _runtime.GetInterface("TestPlugin"); + ASSERT_NE(iface, nullptr); + + string message; + uint32_t result = iface->Greet("", message); + EXPECT_EQ(result, Core::ERROR_NONE); + EXPECT_EQ(message, "Hello, World!"); + + iface->Release(); + } + + TEST_F(TestPluginTest, COMRPC_EchoEmptyString) + { + auto* iface = _runtime.GetInterface("TestPlugin"); + ASSERT_NE(iface, nullptr); + + string output; + uint32_t result = iface->Echo("", output); + EXPECT_EQ(result, Core::ERROR_NONE); + EXPECT_TRUE(output.empty()); + + iface->Release(); + } + + // ================================================================== + // JSON-RPC path (full designator via Invoke) + // ================================================================== + + TEST_F(TestPluginTest, JSONRPC_EchoReturnsInput) + { + string response; + uint32_t result = _runtime.Invoke("TestPlugin.echo", R"({"input":"hello"})", response); + EXPECT_EQ(result, Core::ERROR_NONE) << "echo returned: " << result; + EXPECT_FALSE(response.empty()); + EXPECT_NE(response.find("hello"), string::npos) << "response: " << response; + } + + TEST_F(TestPluginTest, JSONRPC_GreetReturnsMessage) + { + string response; + uint32_t result = _runtime.Invoke("TestPlugin.greet", R"({"name":"Thunder"})", response); + EXPECT_EQ(result, Core::ERROR_NONE) << "greet returned: " << result; + EXPECT_NE(response.find("Hello, Thunder!"), string::npos) << "response: " << response; + } + + TEST_F(TestPluginTest, JSONRPC_UnknownMethodReturnsError) + { + string response; + uint32_t result = _runtime.Invoke("TestPlugin.nonexistent", "{}", response); + EXPECT_EQ(result, Core::ERROR_UNKNOWN_KEY); + } + + // ================================================================== + // JSON-RPC path (JSONRPCLink — callsign-bound) + // ================================================================== + + TEST_F(TestPluginTest, JSONRPC_EchoViaLink) + { + auto* link = _runtime.CreateJSONRPCLink("TestPlugin"); + ASSERT_NE(link, nullptr); + + string response; + uint32_t result = link->Invoke("echo", R"({"input":"linked"})", response); + EXPECT_EQ(result, Core::ERROR_NONE); + EXPECT_NE(response.find("linked"), string::npos) << "response: " << response; + + delete link; + } + + TEST_F(TestPluginTest, JSONRPC_GreetViaLink) + { + auto* link = _runtime.CreateJSONRPCLink("TestPlugin"); + ASSERT_NE(link, nullptr); + + string response; + uint32_t result = link->Invoke("greet", R"({"name":"Link"})", response); + EXPECT_EQ(result, Core::ERROR_NONE); + EXPECT_NE(response.find("Hello, Link!"), string::npos) << "response: " << response; + + delete link; + } + +} // namespace Tests +} // namespace TestCore +} // namespace Thunder From 68f35e93c14a717bafe62bee7ee7b83de5fcd833 Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Mon, 20 Apr 2026 14:31:52 +0530 Subject: [PATCH 46/64] Update Test_Thunder_Test_Support.yml --- .github/workflows/Test_Thunder_Test_Support.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index 2340e320b8..73f7326c6e 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -6,7 +6,7 @@ permissions: on: workflow_dispatch: push: - branches: ["R5.3"] + branches: ["R5.3", "development/test-support"] paths: - 'Tests/test_support/**' - 'Source/Thunder/**' From deeda11b800afb00df61c0e8413a54d62398d190 Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Mon, 20 Apr 2026 14:33:01 +0530 Subject: [PATCH 47/64] Update Test_Thunder_Test_Support.yml From 9be539e0c8c70e73c9305ef360b1bed71db77b0d Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Mon, 20 Apr 2026 14:58:19 +0530 Subject: [PATCH 48/64] Update Test_Thunder_Test_Support.yml --- .github/workflows/Test_Thunder_Test_Support.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index 73f7326c6e..9c38731ef3 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -7,18 +7,9 @@ on: workflow_dispatch: push: branches: ["R5.3", "development/test-support"] - paths: - - 'Tests/test_support/**' - - 'Source/Thunder/**' - - 'Source/core/**' - - 'Source/plugins/**' + pull_request: branches: ["R5.3"] - paths: - - 'Tests/test_support/**' - - 'Source/Thunder/**' - - 'Source/core/**' - - 'Source/plugins/**' jobs: SmokeTest: From 689d4c873939b915028e563d05160a651e703576 Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Mon, 20 Apr 2026 14:58:37 +0530 Subject: [PATCH 49/64] Update Test_Thunder_Test_Support.yml --- .github/workflows/Test_Thunder_Test_Support.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index 9c38731ef3..f10c749f01 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -6,10 +6,10 @@ permissions: on: workflow_dispatch: push: - branches: ["R5.3", "development/test-support"] + branches: ["R5.3"] pull_request: - branches: ["R5.3"] + branches: ["R5.3", "development/test-support"] jobs: SmokeTest: From 8912b88b5f070fe699abbfcabc9277142487e646 Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Mon, 20 Apr 2026 15:00:36 +0530 Subject: [PATCH 50/64] Update Test_Thunder_Test_Support.yml --- .github/workflows/Test_Thunder_Test_Support.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index f10c749f01..0ce4c8e1cb 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -6,7 +6,7 @@ permissions: on: workflow_dispatch: push: - branches: ["R5.3"] + branches: ["R5.3", "development/test-support"] pull_request: branches: ["R5.3", "development/test-support"] From 30bab109f3592293a4df521195564ed94cd5569d Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Mon, 20 Apr 2026 16:19:29 +0530 Subject: [PATCH 51/64] Update Test_Thunder_Test_Support.yml --- .github/workflows/Test_Thunder_Test_Support.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index 0ce4c8e1cb..885052ae20 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -6,10 +6,10 @@ permissions: on: workflow_dispatch: push: - branches: ["R5.3", "development/test-support"] + branches: ["R5_3", "development/test-support"] pull_request: - branches: ["R5.3", "development/test-support"] + branches: ["R5_3", "development/test-support"] jobs: SmokeTest: From 4c7ce48a0665cf92d29bd6bb1f703130bafd4b86 Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Mon, 20 Apr 2026 16:19:54 +0530 Subject: [PATCH 52/64] Update TestPluginTest.cpp --- Tests/test_support/tests/TestPluginTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_support/tests/TestPluginTest.cpp b/Tests/test_support/tests/TestPluginTest.cpp index 3104e550fa..366e474e64 100644 --- a/Tests/test_support/tests/TestPluginTest.cpp +++ b/Tests/test_support/tests/TestPluginTest.cpp @@ -12,7 +12,7 @@ #include "Module.h" #include "ThunderTestRuntime.h" -#include +#include #include #include From 8468c7dc10cd1d815e2b1bab6f005a4e99ebeb44 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Mon, 20 Apr 2026 16:27:06 +0530 Subject: [PATCH 53/64] Resolve header include error --- Tests/test_support/tests/CMakeLists.txt | 5 +++++ Tests/test_support/tests/TestPluginTest.cpp | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Tests/test_support/tests/CMakeLists.txt b/Tests/test_support/tests/CMakeLists.txt index 67b7f32e44..d25dac524b 100644 --- a/Tests/test_support/tests/CMakeLists.txt +++ b/Tests/test_support/tests/CMakeLists.txt @@ -46,6 +46,11 @@ target_compile_definitions(${PLUGIN_TEST_TARGET} TEST_PLUGIN_PATH="${TEST_PLUGIN_PATH}" ) +target_include_directories(${PLUGIN_TEST_TARGET} + PRIVATE + ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}/${NAMESPACE} +) + target_link_libraries(${PLUGIN_TEST_TARGET} PRIVATE thunder_test_support diff --git a/Tests/test_support/tests/TestPluginTest.cpp b/Tests/test_support/tests/TestPluginTest.cpp index 366e474e64..3104e550fa 100644 --- a/Tests/test_support/tests/TestPluginTest.cpp +++ b/Tests/test_support/tests/TestPluginTest.cpp @@ -12,7 +12,7 @@ #include "Module.h" #include "ThunderTestRuntime.h" -#include +#include #include #include From 3f4d329df4c00db753177558d37bfd0470c36638 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Mon, 20 Apr 2026 16:35:13 +0530 Subject: [PATCH 54/64] Resolve workflow issue --- .../workflows/Test_Thunder_Test_Support.yml | 14 +++-- Tests/test_support/tests/CMakeLists.txt | 56 ++++++++++--------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index 885052ae20..cfed396c5e 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -5,9 +5,7 @@ permissions: on: workflow_dispatch: - push: - branches: ["R5_3", "development/test-support"] - + pull_request: branches: ["R5_3", "development/test-support"] @@ -137,8 +135,7 @@ jobs: -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/Thunder/Modules" \ -DPORT="0" \ -DENABLE_CXX17=OFF \ - -DENABLE_TEST_RUNTIME=ON \ - -DTEST_PLUGIN_PATH="${PWD}/${{matrix.build_type}}/install/usr/lib/thunder/plugins" + -DENABLE_TEST_RUNTIME=ON cmake --build ${{matrix.build_type}}/build/Thunder --target install - name: Build ThunderInterfaces @@ -160,6 +157,13 @@ jobs: -DPLUGIN_TESTPLUGIN=ON cmake --build ${{matrix.build_type}}/build/ThunderNanoServicesTests --target install + - name: Build Thunder plugin test + run: | + source venv/bin/activate + cmake -G Ninja -S Thunder -B ${{matrix.build_type}}/build/Thunder \ + -DTEST_PLUGIN_PATH="${PWD}/${{matrix.build_type}}/install/usr/lib/thunder/plugins" + cmake --build ${{matrix.build_type}}/build/Thunder --target thunder_test_runtime_plugin + # ----- Run smoke test ----- - name: Run smoke test run: | diff --git a/Tests/test_support/tests/CMakeLists.txt b/Tests/test_support/tests/CMakeLists.txt index d25dac524b..0059158c1a 100644 --- a/Tests/test_support/tests/CMakeLists.txt +++ b/Tests/test_support/tests/CMakeLists.txt @@ -30,32 +30,34 @@ add_test(NAME ${TARGET} COMMAND ${TARGET}) # - ThunderNanoServices TestPlugin .so built and path passed via # TEST_PLUGIN_PATH (defaults to the Thunder install plugins dir) # ------------------------------------------------------------------ -set(PLUGIN_TEST_TARGET thunder_test_runtime_plugin) - set(TEST_PLUGIN_PATH "" CACHE PATH "Directory containing libThunderTestPlugin.so") -add_executable(${PLUGIN_TEST_TARGET} - TestPluginTest.cpp - Module.cpp -) - -target_compile_definitions(${PLUGIN_TEST_TARGET} - PRIVATE - MODULE_NAME=TestPluginTest - BUILD_REFERENCE=${BUILD_REFERENCE} - TEST_PLUGIN_PATH="${TEST_PLUGIN_PATH}" -) - -target_include_directories(${PLUGIN_TEST_TARGET} - PRIVATE - ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}/${NAMESPACE} -) - -target_link_libraries(${PLUGIN_TEST_TARGET} - PRIVATE - thunder_test_support - GTest::GTest - GTest::Main -) - -add_test(NAME ${PLUGIN_TEST_TARGET} COMMAND ${PLUGIN_TEST_TARGET}) +if(TEST_PLUGIN_PATH) + set(PLUGIN_TEST_TARGET thunder_test_runtime_plugin) + + add_executable(${PLUGIN_TEST_TARGET} + TestPluginTest.cpp + Module.cpp + ) + + target_compile_definitions(${PLUGIN_TEST_TARGET} + PRIVATE + MODULE_NAME=TestPluginTest + BUILD_REFERENCE=${BUILD_REFERENCE} + TEST_PLUGIN_PATH="${TEST_PLUGIN_PATH}" + ) + + target_include_directories(${PLUGIN_TEST_TARGET} + PRIVATE + ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}/${NAMESPACE} + ) + + target_link_libraries(${PLUGIN_TEST_TARGET} + PRIVATE + thunder_test_support + GTest::GTest + GTest::Main + ) + + add_test(NAME ${PLUGIN_TEST_TARGET} COMMAND ${PLUGIN_TEST_TARGET}) +endif() From df16a4712c6e43080df6d17df568fa981c8e9842 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Mon, 20 Apr 2026 16:44:00 +0530 Subject: [PATCH 55/64] Resolve workflow issue --- .github/workflows/Test_Thunder_Test_Support.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index cfed396c5e..ecc9efefca 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -144,7 +144,8 @@ jobs: cmake -G Ninja -S ThunderInterfaces -B ${{matrix.build_type}}/build/ThunderInterfaces \ -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ - -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/Thunder/Modules" + -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/Thunder/Modules" \ + -DCMAKE_PREFIX_PATH="${PWD}/${{matrix.build_type}}/install/usr" cmake --build ${{matrix.build_type}}/build/ThunderInterfaces --target install - name: Build ThunderNanoServices TestPlugin @@ -154,6 +155,7 @@ jobs: -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/Thunder/Modules" \ + -DCMAKE_PREFIX_PATH="${PWD}/${{matrix.build_type}}/install/usr" \ -DPLUGIN_TESTPLUGIN=ON cmake --build ${{matrix.build_type}}/build/ThunderNanoServicesTests --target install From 57f70075aa6079fe14a5fc354f45a110171aebef Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Mon, 20 Apr 2026 17:26:25 +0530 Subject: [PATCH 56/64] Update Test_Thunder_Test_Support.yml --- .github/workflows/Test_Thunder_Test_Support.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index ecc9efefca..505fe5f361 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -47,6 +47,7 @@ jobs: with: path: ThunderTools repository: rdkcentral/ThunderTools + ref: R5.3.0 - name: Regex ThunderTools if: ${{ contains(github.event.pull_request.body, '[DependsOn=ThunderTools:') }} From f801490127ab32c8ec5e624e8e8bc8b906ee59e6 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Tue, 21 Apr 2026 10:33:16 +0530 Subject: [PATCH 57/64] Move out TestPlugin Test file --- .../workflows/Test_Thunder_Test_Support.yml | 16 +- Tests/test_support/CMakeLists.txt | 18 ++ Tests/test_support/tests/CMakeLists.txt | 40 ---- Tests/test_support/tests/TestPluginTest.cpp | 190 ------------------ 4 files changed, 23 insertions(+), 241 deletions(-) delete mode 100644 Tests/test_support/tests/TestPluginTest.cpp diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index 505fe5f361..ba980ef081 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -149,7 +149,7 @@ jobs: -DCMAKE_PREFIX_PATH="${PWD}/${{matrix.build_type}}/install/usr" cmake --build ${{matrix.build_type}}/build/ThunderInterfaces --target install - - name: Build ThunderNanoServices TestPlugin + - name: Build ThunderNanoServices TestPlugin + test run: | source venv/bin/activate cmake -G Ninja -S ThunderNanoServices/tests -B ${{matrix.build_type}}/build/ThunderNanoServicesTests \ @@ -157,15 +157,9 @@ jobs: -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/Thunder/Modules" \ -DCMAKE_PREFIX_PATH="${PWD}/${{matrix.build_type}}/install/usr" \ - -DPLUGIN_TESTPLUGIN=ON - cmake --build ${{matrix.build_type}}/build/ThunderNanoServicesTests --target install - - - name: Build Thunder plugin test - run: | - source venv/bin/activate - cmake -G Ninja -S Thunder -B ${{matrix.build_type}}/build/Thunder \ + -DPLUGIN_TESTPLUGIN=ON \ -DTEST_PLUGIN_PATH="${PWD}/${{matrix.build_type}}/install/usr/lib/thunder/plugins" - cmake --build ${{matrix.build_type}}/build/Thunder --target thunder_test_runtime_plugin + cmake --build ${{matrix.build_type}}/build/ThunderNanoServicesTests --target install # ----- Run smoke test ----- - name: Run smoke test @@ -176,10 +170,10 @@ jobs: --gtest_color=yes # ----- Run plugin test ----- - - name: Run plugin test (COM-RPC + JSON-RPC) + - name: Run plugin test (COM-RPC + JSON-RPC + events) run: | LD_LIBRARY_PATH="${{matrix.build_type}}/install/usr/lib:${PWD}/${{matrix.build_type}}/install/usr/lib/thunder/plugins:$LD_LIBRARY_PATH" \ - ${{matrix.build_type}}/build/Thunder/Tests/test_support/tests/thunder_test_runtime_plugin \ + ${{matrix.build_type}}/build/ThunderNanoServicesTests/TestPlugin/test/thunder_testplugin_test \ --gtest_output="xml:plugin-test-results.xml" \ --gtest_color=yes diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt index 01a8edfee9..16a3e5aaa0 100644 --- a/Tests/test_support/CMakeLists.txt +++ b/Tests/test_support/CMakeLists.txt @@ -119,6 +119,24 @@ if(HIBERNATESUPPORT) ) endif() +# ------------------------------------------------------------------ +# Install the library and header so external projects can use it +# via find_package(thunder_test_support) +# ------------------------------------------------------------------ +install( + FILES ThunderTestRuntime.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${NAMESPACE}/test_support + COMPONENT ${NAMESPACE}_Development +) + +install( + TARGETS ${TARGET} EXPORT ${TARGET}Targets + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT ${NAMESPACE}_Development + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${NAMESPACE} +) + +InstallCMakeConfig(TARGETS ${TARGET}) + # ------------------------------------------------------------------ # Smoke test sub-directory # ------------------------------------------------------------------ diff --git a/Tests/test_support/tests/CMakeLists.txt b/Tests/test_support/tests/CMakeLists.txt index 0059158c1a..5e6463e706 100644 --- a/Tests/test_support/tests/CMakeLists.txt +++ b/Tests/test_support/tests/CMakeLists.txt @@ -21,43 +21,3 @@ target_link_libraries(${TARGET} ) add_test(NAME ${TARGET} COMMAND ${TARGET}) - -# ------------------------------------------------------------------ -# TestPlugin integration test -# -# Requires: -# - ThunderInterfaces qa_interfaces headers installed (for ITestPlugin.h) -# - ThunderNanoServices TestPlugin .so built and path passed via -# TEST_PLUGIN_PATH (defaults to the Thunder install plugins dir) -# ------------------------------------------------------------------ -set(TEST_PLUGIN_PATH "" CACHE PATH "Directory containing libThunderTestPlugin.so") - -if(TEST_PLUGIN_PATH) - set(PLUGIN_TEST_TARGET thunder_test_runtime_plugin) - - add_executable(${PLUGIN_TEST_TARGET} - TestPluginTest.cpp - Module.cpp - ) - - target_compile_definitions(${PLUGIN_TEST_TARGET} - PRIVATE - MODULE_NAME=TestPluginTest - BUILD_REFERENCE=${BUILD_REFERENCE} - TEST_PLUGIN_PATH="${TEST_PLUGIN_PATH}" - ) - - target_include_directories(${PLUGIN_TEST_TARGET} - PRIVATE - ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}/${NAMESPACE} - ) - - target_link_libraries(${PLUGIN_TEST_TARGET} - PRIVATE - thunder_test_support - GTest::GTest - GTest::Main - ) - - add_test(NAME ${PLUGIN_TEST_TARGET} COMMAND ${PLUGIN_TEST_TARGET}) -endif() diff --git a/Tests/test_support/tests/TestPluginTest.cpp b/Tests/test_support/tests/TestPluginTest.cpp deleted file mode 100644 index 3104e550fa..0000000000 --- a/Tests/test_support/tests/TestPluginTest.cpp +++ /dev/null @@ -1,190 +0,0 @@ -// ========================================================================== -// TestPluginTest — validates that the test runtime can load a plugin -// and interact with it via both COM-RPC and JSON-RPC. -// -// COM-RPC tests use QueryInterface to call methods directly. -// JSON-RPC tests use ThunderTestRuntime::Invoke() and JSONRPCLink. -// -// The TestPlugin is built as a shared library and placed in -// ${CMAKE_BINARY_DIR}/test_plugins/. The test passes that directory as -// the systemPath so the embedded server can dlopen it. -// ========================================================================== - -#include "Module.h" -#include "ThunderTestRuntime.h" -#include -#include -#include - -#ifndef TEST_PLUGIN_PATH -#error "TEST_PLUGIN_PATH must be defined by CMake" -#endif - -namespace Thunder { -namespace TestCore { -namespace Tests { - - class TestPluginTest : public ::testing::Test { - protected: - static ThunderTestRuntime _runtime; - - static void SetUpTestSuite() - { - ThunderTestRuntime::PluginConfig dummyConfig; - dummyConfig.Callsign = "TestPlugin"; - dummyConfig.ClassName = "TestPlugin"; - dummyConfig.Locator = "libThunderTestPlugin.so"; - dummyConfig.StartMode = PluginHost::IShell::startmode::ACTIVATED; - - std::vector plugins; - plugins.push_back(dummyConfig); - - uint32_t result = _runtime.Initialize(plugins, TEST_PLUGIN_PATH); - ASSERT_EQ(result, Core::ERROR_NONE) << "Failed to initialize runtime with TestPlugin"; - } - - static void TearDownTestSuite() - { - _runtime.Deinitialize(); - } - }; - - ThunderTestRuntime TestPluginTest::_runtime; - - // ================================================================== - // Plugin lifecycle - // ================================================================== - - TEST_F(TestPluginTest, PluginIsActivated) - { - auto shell = _runtime.GetShell("TestPlugin"); - EXPECT_TRUE(shell.IsValid()) << "TestPlugin IShell must be available"; - if (shell.IsValid()) { - EXPECT_EQ(shell->State(), PluginHost::IShell::state::ACTIVATED); - } - } - - // ================================================================== - // COM-RPC path (QueryInterface) - // ================================================================== - - TEST_F(TestPluginTest, COMRPC_QueryInterfaceSucceeds) - { - auto* iface = _runtime.GetInterface("TestPlugin"); - ASSERT_NE(iface, nullptr) << "QueryInterface must succeed"; - iface->Release(); - } - - TEST_F(TestPluginTest, COMRPC_EchoReturnsInput) - { - auto* iface = _runtime.GetInterface("TestPlugin"); - ASSERT_NE(iface, nullptr); - - string output; - uint32_t result = iface->Echo("hello", output); - EXPECT_EQ(result, Core::ERROR_NONE); - EXPECT_EQ(output, "hello"); - - iface->Release(); - } - - TEST_F(TestPluginTest, COMRPC_GreetReturnsMessage) - { - auto* iface = _runtime.GetInterface("TestPlugin"); - ASSERT_NE(iface, nullptr); - - string message; - uint32_t result = iface->Greet("Thunder", message); - EXPECT_EQ(result, Core::ERROR_NONE); - EXPECT_EQ(message, "Hello, Thunder!"); - - iface->Release(); - } - - TEST_F(TestPluginTest, COMRPC_GreetDefaultsToWorld) - { - auto* iface = _runtime.GetInterface("TestPlugin"); - ASSERT_NE(iface, nullptr); - - string message; - uint32_t result = iface->Greet("", message); - EXPECT_EQ(result, Core::ERROR_NONE); - EXPECT_EQ(message, "Hello, World!"); - - iface->Release(); - } - - TEST_F(TestPluginTest, COMRPC_EchoEmptyString) - { - auto* iface = _runtime.GetInterface("TestPlugin"); - ASSERT_NE(iface, nullptr); - - string output; - uint32_t result = iface->Echo("", output); - EXPECT_EQ(result, Core::ERROR_NONE); - EXPECT_TRUE(output.empty()); - - iface->Release(); - } - - // ================================================================== - // JSON-RPC path (full designator via Invoke) - // ================================================================== - - TEST_F(TestPluginTest, JSONRPC_EchoReturnsInput) - { - string response; - uint32_t result = _runtime.Invoke("TestPlugin.echo", R"({"input":"hello"})", response); - EXPECT_EQ(result, Core::ERROR_NONE) << "echo returned: " << result; - EXPECT_FALSE(response.empty()); - EXPECT_NE(response.find("hello"), string::npos) << "response: " << response; - } - - TEST_F(TestPluginTest, JSONRPC_GreetReturnsMessage) - { - string response; - uint32_t result = _runtime.Invoke("TestPlugin.greet", R"({"name":"Thunder"})", response); - EXPECT_EQ(result, Core::ERROR_NONE) << "greet returned: " << result; - EXPECT_NE(response.find("Hello, Thunder!"), string::npos) << "response: " << response; - } - - TEST_F(TestPluginTest, JSONRPC_UnknownMethodReturnsError) - { - string response; - uint32_t result = _runtime.Invoke("TestPlugin.nonexistent", "{}", response); - EXPECT_EQ(result, Core::ERROR_UNKNOWN_KEY); - } - - // ================================================================== - // JSON-RPC path (JSONRPCLink — callsign-bound) - // ================================================================== - - TEST_F(TestPluginTest, JSONRPC_EchoViaLink) - { - auto* link = _runtime.CreateJSONRPCLink("TestPlugin"); - ASSERT_NE(link, nullptr); - - string response; - uint32_t result = link->Invoke("echo", R"({"input":"linked"})", response); - EXPECT_EQ(result, Core::ERROR_NONE); - EXPECT_NE(response.find("linked"), string::npos) << "response: " << response; - - delete link; - } - - TEST_F(TestPluginTest, JSONRPC_GreetViaLink) - { - auto* link = _runtime.CreateJSONRPCLink("TestPlugin"); - ASSERT_NE(link, nullptr); - - string response; - uint32_t result = link->Invoke("greet", R"({"name":"Link"})", response); - EXPECT_EQ(result, Core::ERROR_NONE); - EXPECT_NE(response.find("Hello, Link!"), string::npos) << "response: " << response; - - delete link; - } - -} // namespace Tests -} // namespace TestCore -} // namespace Thunder From bfcdfcee48a1c204e2ca7d0c05adad5afb22b208 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Tue, 21 Apr 2026 10:40:14 +0530 Subject: [PATCH 58/64] Resolve workflow failures --- .github/workflows/Test_Thunder_Test_Support.yml | 1 - Tests/test_support/CMakeLists.txt | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index ba980ef081..98e7722cb6 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -135,7 +135,6 @@ jobs: -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/Thunder/Modules" \ -DPORT="0" \ - -DENABLE_CXX17=OFF \ -DENABLE_TEST_RUNTIME=ON cmake --build ${{matrix.build_type}}/build/Thunder --target install diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt index 16a3e5aaa0..2f7d807cae 100644 --- a/Tests/test_support/CMakeLists.txt +++ b/Tests/test_support/CMakeLists.txt @@ -27,7 +27,8 @@ add_library(${TARGET} STATIC target_include_directories(${TARGET} PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR} + $ + $ PRIVATE ${THUNDER_SOURCE_DIR} $ @@ -69,8 +70,9 @@ target_link_libraries(${TARGET} ${NAMESPACE}Messaging::${NAMESPACE}Messaging ${NAMESPACE}WebSocket::${NAMESPACE}WebSocket ${NAMESPACE}Plugins::${NAMESPACE}Plugins - ${NAMESPACE}COMProcess::${NAMESPACE}COMProcess Threads::Threads + PRIVATE + ${NAMESPACE}COMProcess::${NAMESPACE}COMProcess ) # ------------------------------------------------------------------ From ed48229fa747a8c6f50b7dd6b8d40353e9920556 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Tue, 21 Apr 2026 10:43:32 +0530 Subject: [PATCH 59/64] Resolve workflow failures --- Tests/test_support/CMakeLists.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt index 2f7d807cae..0f0b2700a8 100644 --- a/Tests/test_support/CMakeLists.txt +++ b/Tests/test_support/CMakeLists.txt @@ -71,10 +71,12 @@ target_link_libraries(${TARGET} ${NAMESPACE}WebSocket::${NAMESPACE}WebSocket ${NAMESPACE}Plugins::${NAMESPACE}Plugins Threads::Threads - PRIVATE - ${NAMESPACE}COMProcess::${NAMESPACE}COMProcess ) +# COMProcess is an INTERFACE library carrying only a compile definition. +# It is not in any export set, so we absorb its effect directly. +target_compile_definitions(${TARGET} PRIVATE HOSTING_COMPROCESS=ThunderPlugin) + # ------------------------------------------------------------------ # Whole-archive link. Ensures that the MODULE_NAME_DECLARATION # constructors from the Server objects are not discarded. From 5f0e7b662f6f4081b83b7e03a73bd13cca10fa09 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Tue, 21 Apr 2026 10:55:29 +0530 Subject: [PATCH 60/64] Resolve workflow failures --- Tests/test_support/CMakeLists.txt | 11 +++++++--- .../thunder_test_supportConfig.cmake.in | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 Tests/test_support/thunder_test_supportConfig.cmake.in diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt index 0f0b2700a8..2f91a64bc1 100644 --- a/Tests/test_support/CMakeLists.txt +++ b/Tests/test_support/CMakeLists.txt @@ -80,14 +80,16 @@ target_compile_definitions(${TARGET} PRIVATE HOSTING_COMPROCESS=ThunderPlugin) # ------------------------------------------------------------------ # Whole-archive link. Ensures that the MODULE_NAME_DECLARATION # constructors from the Server objects are not discarded. +# BUILD_INTERFACE only — installed consumers get this via the +# custom Config.cmake.in template instead. # ------------------------------------------------------------------ if(APPLE) target_link_options(${TARGET} INTERFACE - "SHELL:-Wl,-force_load,$" + "$>" ) else() target_link_options(${TARGET} INTERFACE - "SHELL:-Wl,--whole-archive $ -Wl,--no-whole-archive" + "$ -Wl,--no-whole-archive>" ) endif() @@ -139,7 +141,10 @@ install( INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${NAMESPACE} ) -InstallCMakeConfig(TARGETS ${TARGET}) +InstallCMakeConfig( + TARGETS ${TARGET} + TEMPLATE ${CMAKE_CURRENT_SOURCE_DIR}/thunder_test_supportConfig.cmake.in +) # ------------------------------------------------------------------ # Smoke test sub-directory diff --git a/Tests/test_support/thunder_test_supportConfig.cmake.in b/Tests/test_support/thunder_test_supportConfig.cmake.in new file mode 100644 index 0000000000..ae044d95b3 --- /dev/null +++ b/Tests/test_support/thunder_test_supportConfig.cmake.in @@ -0,0 +1,20 @@ +set(dependencies @dependencies@) + +foreach(dependency ${dependencies}) + find_package(${dependency} REQUIRED) +endforeach() + +get_filename_component(_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH) +include(${_DIR}/@_name@Targets.cmake) + +# Whole-archive linking ensures MODULE_NAME_DECLARATION static constructors +# from the embedded PluginHost::Server objects are not discarded by the linker. +if(TARGET @_name@::@_name@) + if(APPLE) + set_property(TARGET @_name@::@_name@ APPEND PROPERTY + INTERFACE_LINK_OPTIONS "SHELL:-Wl,-force_load,$") + else() + set_property(TARGET @_name@::@_name@ APPEND PROPERTY + INTERFACE_LINK_OPTIONS "SHELL:-Wl,--whole-archive $ -Wl,--no-whole-archive") + endif() +endif() From b45292ea19ee6ebd63b00433c9c2e33738da3057 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Tue, 21 Apr 2026 11:33:50 +0530 Subject: [PATCH 61/64] Add conditional gaurd to initialize and deinitialize --- Tests/test_support/ThunderTestRuntime.cpp | 7 ++++++- Tests/test_support/ThunderTestRuntime.h | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp index c8eb660c2f..56dbf6803b 100644 --- a/Tests/test_support/ThunderTestRuntime.cpp +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -331,6 +331,8 @@ namespace TestCore { _server = new PluginHost::Server(*_config, false); _server->Open(); + _initialized = true; + return Core::ERROR_NONE; } @@ -440,7 +442,10 @@ namespace TestCore { _config = nullptr; } - Messaging::MessageUnit::Instance().Close(); + if (_initialized == true) { + Messaging::MessageUnit::Instance().Close(); + _initialized = false; + } if (_configFilePath.empty() == false) { Core::File(_configFilePath).Destroy(); diff --git a/Tests/test_support/ThunderTestRuntime.h b/Tests/test_support/ThunderTestRuntime.h index 0c38e3800c..ac7433cd98 100644 --- a/Tests/test_support/ThunderTestRuntime.h +++ b/Tests/test_support/ThunderTestRuntime.h @@ -173,6 +173,7 @@ namespace TestCore { PluginHost::Config* _config = nullptr; PluginHost::Server* _server = nullptr; + bool _initialized = false; string _tempDir; string _configFilePath; string _proxyStubPath; From 5689025bb5de85722961de8d4a388c0237cd0d87 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Tue, 21 Apr 2026 11:57:31 +0530 Subject: [PATCH 62/64] Update ThunderTestRuntime.cpp --- Tests/test_support/ThunderTestRuntime.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp index 56dbf6803b..94913385db 100644 --- a/Tests/test_support/ThunderTestRuntime.cpp +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -444,6 +444,7 @@ namespace TestCore { if (_initialized == true) { Messaging::MessageUnit::Instance().Close(); + Core::Singleton::Dispose(); _initialized = false; } From 8e977abbed0014c6c54c93490d7b87a757e8165a Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Tue, 21 Apr 2026 12:27:35 +0530 Subject: [PATCH 63/64] Add debug comments --- Tests/test_support/ThunderTestRuntime.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp index 94913385db..11aac53021 100644 --- a/Tests/test_support/ThunderTestRuntime.cpp +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -122,7 +122,9 @@ namespace TestCore { ThunderTestRuntime::~ThunderTestRuntime() { + fprintf(stderr, "[TestRuntime] ~ThunderTestRuntime() - begin\n"); fflush(stderr); Deinitialize(); + fprintf(stderr, "[TestRuntime] ~ThunderTestRuntime() - done\n"); fflush(stderr); } bool ThunderTestRuntime::CreateDirectories() const @@ -431,20 +433,29 @@ namespace TestCore { void ThunderTestRuntime::Deinitialize() { + fprintf(stderr, "[TestRuntime] Deinitialize - begin\n"); fflush(stderr); + if (_server != nullptr) { + fprintf(stderr, "[TestRuntime] Server::Close() - begin\n"); fflush(stderr); _server->Close(); + fprintf(stderr, "[TestRuntime] Server::Close() - done\n"); fflush(stderr); delete _server; + fprintf(stderr, "[TestRuntime] delete _server - done\n"); fflush(stderr); _server = nullptr; } if (_config != nullptr) { delete _config; + fprintf(stderr, "[TestRuntime] delete _config - done\n"); fflush(stderr); _config = nullptr; } if (_initialized == true) { + fprintf(stderr, "[TestRuntime] MessageUnit::Close() - begin\n"); fflush(stderr); Messaging::MessageUnit::Instance().Close(); + fprintf(stderr, "[TestRuntime] Singleton::Dispose() - begin\n"); fflush(stderr); Core::Singleton::Dispose(); + fprintf(stderr, "[TestRuntime] Singleton::Dispose() - done\n"); fflush(stderr); _initialized = false; } @@ -455,6 +466,7 @@ namespace TestCore { CleanupDirectories(); _tempDir.clear(); + fprintf(stderr, "[TestRuntime] Deinitialize - done\n"); fflush(stderr); } } // namespace TestCore From 4770e550e45e0e2fbdeafd52cb7ba661e4564834 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Tue, 21 Apr 2026 15:06:08 +0530 Subject: [PATCH 64/64] Add thunder_test_main static library providing a GTest main() that calls _Exit() after RUN_ALL_TESTS() to avoid static destruction order crashes when external plugin .so files are loaded. --- Tests/test_support/CMakeLists.txt | 32 +++++++++++++++++++++++ Tests/test_support/ThunderTestMain.cpp | 28 ++++++++++++++++++++ Tests/test_support/ThunderTestRuntime.cpp | 13 --------- 3 files changed, 60 insertions(+), 13 deletions(-) create mode 100644 Tests/test_support/ThunderTestMain.cpp diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt index 2f91a64bc1..5e6a5149b6 100644 --- a/Tests/test_support/CMakeLists.txt +++ b/Tests/test_support/CMakeLists.txt @@ -141,11 +141,43 @@ install( INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${NAMESPACE} ) +# ------------------------------------------------------------------ +# thunder_test_main — GTest main() that calls _Exit() after +# RUN_ALL_TESTS() to avoid static destruction order crashes when +# external plugin .so files are loaded. +# +# Plugin tests link this instead of GTest::Main. +# ------------------------------------------------------------------ +set(MAIN_TARGET thunder_test_main) + +add_library(${MAIN_TARGET} STATIC + ThunderTestMain.cpp +) + +target_link_libraries(${MAIN_TARGET} + PUBLIC + GTest::GTest +) + +set_target_properties(${MAIN_TARGET} PROPERTIES + CXX_STANDARD 11 + CXX_STANDARD_REQUIRED YES +) + +install( + TARGETS ${MAIN_TARGET} EXPORT ${MAIN_TARGET}Targets + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT ${NAMESPACE}_Development +) + InstallCMakeConfig( TARGETS ${TARGET} TEMPLATE ${CMAKE_CURRENT_SOURCE_DIR}/thunder_test_supportConfig.cmake.in ) +InstallCMakeConfig( + TARGETS ${MAIN_TARGET} +) + # ------------------------------------------------------------------ # Smoke test sub-directory # ------------------------------------------------------------------ diff --git a/Tests/test_support/ThunderTestMain.cpp b/Tests/test_support/ThunderTestMain.cpp new file mode 100644 index 0000000000..b9ba67adea --- /dev/null +++ b/Tests/test_support/ThunderTestMain.cpp @@ -0,0 +1,28 @@ +// ========================================================================== +// ThunderTestMain — GTest main() for plugin integration tests. +// +// When a test binary loads external plugin shared libraries (.so) via the +// ThunderTestRuntime, those libraries may register static objects whose +// destruction order at process exit conflicts with Thunder's own +// singletons (WorkerPool, ResourceMonitor, etc.). +// +// This main() calls _Exit() after RUN_ALL_TESTS() to skip static +// destruction entirely, matching the real Thunder daemon which calls +// exit(0) after its CloseDown() sequence. +// +// Link against thunder_test_main instead of GTest::Main: +// target_link_libraries(my_test PRIVATE +// thunder_test_support::thunder_test_support +// thunder_test_main::thunder_test_main +// GTest::GTest) +// ========================================================================== + +#include +#include + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + int result = RUN_ALL_TESTS(); + _Exit(result); +} diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp index 11aac53021..56dbf6803b 100644 --- a/Tests/test_support/ThunderTestRuntime.cpp +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -122,9 +122,7 @@ namespace TestCore { ThunderTestRuntime::~ThunderTestRuntime() { - fprintf(stderr, "[TestRuntime] ~ThunderTestRuntime() - begin\n"); fflush(stderr); Deinitialize(); - fprintf(stderr, "[TestRuntime] ~ThunderTestRuntime() - done\n"); fflush(stderr); } bool ThunderTestRuntime::CreateDirectories() const @@ -433,29 +431,19 @@ namespace TestCore { void ThunderTestRuntime::Deinitialize() { - fprintf(stderr, "[TestRuntime] Deinitialize - begin\n"); fflush(stderr); - if (_server != nullptr) { - fprintf(stderr, "[TestRuntime] Server::Close() - begin\n"); fflush(stderr); _server->Close(); - fprintf(stderr, "[TestRuntime] Server::Close() - done\n"); fflush(stderr); delete _server; - fprintf(stderr, "[TestRuntime] delete _server - done\n"); fflush(stderr); _server = nullptr; } if (_config != nullptr) { delete _config; - fprintf(stderr, "[TestRuntime] delete _config - done\n"); fflush(stderr); _config = nullptr; } if (_initialized == true) { - fprintf(stderr, "[TestRuntime] MessageUnit::Close() - begin\n"); fflush(stderr); Messaging::MessageUnit::Instance().Close(); - fprintf(stderr, "[TestRuntime] Singleton::Dispose() - begin\n"); fflush(stderr); - Core::Singleton::Dispose(); - fprintf(stderr, "[TestRuntime] Singleton::Dispose() - done\n"); fflush(stderr); _initialized = false; } @@ -466,7 +454,6 @@ namespace TestCore { CleanupDirectories(); _tempDir.clear(); - fprintf(stderr, "[TestRuntime] Deinitialize - done\n"); fflush(stderr); } } // namespace TestCore