Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f6c1d8a
Implement StateMachine for plugin lifecycle management
bramoosterhuis May 12, 2026
ca970cd
Merge branch 'master' into development/plugin-server-service-state-ma…
bramoosterhuis May 13, 2026
1eb1ac3
Use RAII to safely yield transition ownership in Evaluate()
bramoosterhuis May 13, 2026
9311f22
Merge branch 'master' into development/plugin-server-service-state-ma…
bramoosterhuis Jun 9, 2026
fb28577
refactor(pluginserver): replace unsafe TransitionYield with RAII Tran…
bramoosterhuis Jun 9, 2026
3376a75
refactor: integrate Suspend and Resume fully into StateMachine
bramoosterhuis Jun 9, 2026
78c9f80
refactor(server): add DestroyedState tombstone to fix lifecycle race
bramoosterhuis Jun 9, 2026
b23c936
use memory_order_acquire for _current load in DeactivatedState::Resume
bramoosterhuis Jun 10, 2026
1e91cf0
Add ASSERT to Tombstone() enforcing DEACTIVATED precondition
bramoosterhuis Jun 10, 2026
66014ae
Update StateMachine documentation to reflect current design
bramoosterhuis Jun 10, 2026
fa15ec5
Remove stale state guard and State(ACTIVATED) from Wakeup()
bramoosterhuis Jun 10, 2026
608f2c7
Make StateMachine re-entrancy guard portable and active in all builds
bramoosterhuis Jun 10, 2026
4890997
Replace dead hibernate state guards with lock-free invariant checks
bramoosterhuis Jun 10, 2026
1128a5b
Tighten StateMachine visibility
bramoosterhuis Jun 10, 2026
15f8485
Refactor formatting in PluginServer to improve code readability
bramoosterhuis Jun 10, 2026
2913049
ci: Enable coredump and backtrace collection for smoke test debugging
bramoosterhuis Jun 10, 2026
ed8904c
fix: Correct core pattern configuration for core dumps in smoke test
bramoosterhuis Jun 10, 2026
d54e7af
fix: Update core pattern configuration for core dumps in smoke test
bramoosterhuis Jun 10, 2026
b40e3f7
Fix SIGABRT in Server::Notification during shutdown deactivation
bramoosterhuis Jun 10, 2026
3c88af2
Remove || true workaround from smoke test step
bramoosterhuis Jun 10, 2026
8d17011
fix: Set state to Deactivated in activation path for PluginServer
bramoosterhuis Jun 15, 2026
e9e4505
feat: Add ReentrantControl header for state machine testing
bramoosterhuis Jun 15, 2026
14d5b7a
fix regression: Implement QueryInterface for DeactivationState and re…
bramoosterhuis Jun 22, 2026
89c97a6
fix regression: evaluate both conditions on every Reevaluate, not jus…
bramoosterhuis Jun 22, 2026
3d43fed
Disable all hibernation logic if not enabled
bramoosterhuis Jun 22, 2026
d425116
Disable all suspend and resume functionality in state machine.
bramoosterhuis Jun 22, 2026
f568c6d
Tests: add StateMachine lifecycle characterization suite
bramoosterhuis Jun 22, 2026
3c89704
refactor the cmake coverage logic to modern standards
bramoosterhuis Jun 22, 2026
bd615b8
Merge remote-tracking branch 'RDKCentral/master' into development/plu…
bramoosterhuis Jun 23, 2026
74198b8
Merge remote-tracking branch 'origin/master' into development/plugin-…
bramoosterhuis Jul 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,35 @@ if(APPLE)
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
endif()

if (ENABLE_CODE_COVERAGE)
include (CodeCoverage)
append_coverage_compiler_flags()
set(COVERAGE_EXCLUDES
"${CMAKE_CURRENT_BINARY_DIR}/Tests/unit*"
"${CMAKE_CURRENT_BINARY_DIR}/Tests/unit/core/*")
setup_target_for_coverage_gcovr_html(NAME "coverage")
setup_target_for_coverage_gcovr_xml(NAME "coverage-report")
add_definitions(-DWITH_CODE_COVERAGE=1)
include(CMakeDependentOption)

cmake_dependent_option(ENABLE_CODE_COVERAGE "Enable gcov/gcovr code coverage instrumentation" OFF
"CMAKE_BUILD_TYPE STREQUAL Debug" OFF)

if(ENABLE_CODE_COVERAGE)
message(STATUS "Code coverage instrumentation enabled")

include(CodeCoverage)

append_coverage_compiler_flags()

set(COVERAGE_EXCLUDES
"${CMAKE_CURRENT_BINARY_DIR}/Tests/unit*"
"${CMAKE_CURRENT_BINARY_DIR}/Tests/unit/core/*"
"${CMAKE_CURRENT_BINARY_DIR}/Tests/statemachine/*"
)
setup_target_for_coverage_gcovr_html(
NAME "coverage-html-report"
GCOVR_ARGS --gcov-ignore-parse-errors=negative_hits.warn_once_per_file
EXCLUDE_THROW_BRANCHES ON
EXCLUDE_UNREACHABLE_BRANCHES ON
)
setup_target_for_coverage_gcovr_xml(
NAME "coverage-xml-report"
GCOVR_ARGS --gcov-ignore-parse-errors=negative_hits.warn_once_per_file
EXCLUDE_THROW_BRANCHES ON
EXCLUDE_UNREACHABLE_BRANCHES ON
)
endif()

if(BUILD_TESTS)
Expand Down
927 changes: 523 additions & 404 deletions Source/Thunder/PluginServer.cpp

Large diffs are not rendered by default.

724 changes: 659 additions & 65 deletions Source/Thunder/PluginServer.h

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions Tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ option(MESSAGEBUFFER_TEST "Test message buffer" OFF)
option(UNRAVELLER "reveal thread details" OFF)
option(STREAMJSON_GARBAGE_TEST "Reproducer for issue #1963: infinite loop on garbage data in StreamJSONType::ReceiveData()" OFF)
option(ENABLE_TEST_RUNTIME "Build Thunder test support library for plugin integration tests" OFF)
option(STATEMACHINE_TEST "Test for the state machine plugin" OFF)

if(BUILD_TESTS)
add_subdirectory(unit)
Expand Down Expand Up @@ -44,6 +45,10 @@ if(UNRAVELLER)
add_subdirectory(unraveller)
endif()

if(STATEMACHINE_TEST)
add_subdirectory(statemachine)
endif()

if(STREAMJSON_GARBAGE_TEST)
add_subdirectory(streamjson-garbage)
endif()
Expand Down
37 changes: 37 additions & 0 deletions Tests/statemachine/BravePlugin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#include "Module.h"

namespace Thunder {
namespace Plugin {

// Minimal in-process plugin: loads, initializes and tears down cleanly.
// Empty Locator in the config routes instantiation through the in-process
// service chain, matched on the bare class name "BravePlugin".
class BravePlugin : public PluginHost::IPlugin {
public:
BravePlugin() = default;
~BravePlugin() override = default;

BravePlugin(const BravePlugin&) = delete;
BravePlugin& operator=(const BravePlugin&) = delete;

BEGIN_INTERFACE_MAP(BravePlugin)
INTERFACE_ENTRY(PluginHost::IPlugin)
END_INTERFACE_MAP

const string Initialize(PluginHost::IShell* /* shell */) override
{
return string(); // empty == success
}
void Deinitialize(PluginHost::IShell* /* shell */) override
{
}
string Information() const override
{
return string();
}
};

SERVICE_REGISTRATION(BravePlugin, 1, 0)

} // namespace Plugin
} // namespace Thunder
59 changes: 59 additions & 0 deletions Tests/statemachine/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
set(TARGET thunder_test_statemachine)

find_package(GTest REQUIRED)

add_executable(${TARGET}
StateMachineTest.cpp
BravePlugin.cpp
FailInitPlugin.cpp
ReentrantPlugin.cpp
ObserverPlugin.cpp
StateControlPlugin.cpp
Module.cpp
)

target_compile_definitions(${TARGET}
PRIVATE
MODULE_NAME=StateMachineTest
HOSTING_COMPROCESS=ThunderPlugin
THREADPOOL_COUNT=4
)

if(BUILD_REFERENCE)
target_compile_definitions(${TARGET} PRIVATE BUILD_REFERENCE=${BUILD_REFERENCE})
endif()

set(PLUGIN_DIR "${CMAKE_CURRENT_BINARY_DIR}/data/plugins" CACHE PATH "Directory where the test plugins are located")



target_link_libraries(${TARGET}
PRIVATE
CompileSettings::CompileSettings
CompileSettingsDebug::CompileSettingsDebug
${NAMESPACE}Plugins::${NAMESPACE}Plugins
thunder_test_support
GTest::gtest
GTest::gtest_main
)

target_compile_definitions(${TARGET} PRIVATE
TEST_PLUGIN_DIR="${PLUGIN_DIR}"
)

add_custom_command(TARGET ${TARGET} PRE_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "${PLUGIN_DIR}"
)

add_subdirectory(provider)
target_compile_definitions(${TARGET} PRIVATE
TEST_PLUGIN_LOCATOR="$<TARGET_FILE_NAME:ThunderTestStreamingProvider>"
)
add_dependencies(${TARGET} ThunderTestStreamingProvider)

add_subdirectory(statecontrol_oop)
target_compile_definitions(${TARGET} PRIVATE
STATECTRL_OOP_LOCATOR="$<TARGET_FILE_NAME:ThunderTestStateControlOOP>")
add_dependencies(${TARGET} ThunderTestStateControlOOP)

add_test(NAME ${TARGET} COMMAND ${TARGET})
36 changes: 36 additions & 0 deletions Tests/statemachine/FailInitPlugin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#include "Module.h"

namespace Thunder {
namespace Plugin {

// In-process plugin whose Initialize() reports failure, to drive the
// INITIALIZATION_FAILED branch (ends back in DEACTIVATED).
class FailInitPlugin : public PluginHost::IPlugin {
public:
FailInitPlugin() = default;
~FailInitPlugin() override = default;

FailInitPlugin(const FailInitPlugin&) = delete;
FailInitPlugin& operator=(const FailInitPlugin&) = delete;

BEGIN_INTERFACE_MAP(FailInitPlugin)
INTERFACE_ENTRY(PluginHost::IPlugin)
END_INTERFACE_MAP

const string Initialize(PluginHost::IShell* /* shell */) override
{
return _T("init refused"); // non-empty == failure
}
void Deinitialize(PluginHost::IShell* /* shell */) override
{
}
string Information() const override
{
return string();
}
};

SERVICE_REGISTRATION(FailInitPlugin, 1, 0)

} // namespace Plugin
} // namespace Thunder
3 changes: 3 additions & 0 deletions Tests/statemachine/Module.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#include "Module.h"

MODULE_NAME_DECLARATION(BUILD_REFERENCE)
10 changes: 10 additions & 0 deletions Tests/statemachine/Module.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#pragma once

#ifndef MODULE_NAME
#define MODULE_NAME StateMachineTest
#endif

#include <plugins/plugins.h>

#undef EXTERNAL
#define EXTERNAL
11 changes: 11 additions & 0 deletions Tests/statemachine/ObserverControl.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#pragma once
#include <string>

namespace Thunder {
namespace TestSupport {
extern std::string g_observedCallsign; // set by the test before deactivating
extern bool g_deactivatedFired;
extern bool g_shellQiWorked; // QI<IShell> during the Deactivated notification
extern bool g_handlerQiResolved; // QI<IPlugin> during it (forwards to live handler -> non-null)
}
}
64 changes: 64 additions & 0 deletions Tests/statemachine/ObserverPlugin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#include "Module.h"
#include "ObserverControl.h"

namespace Thunder {

namespace TestSupport {
std::string g_observedCallsign;
bool g_deactivatedFired = false;
bool g_shellQiWorked = false;
bool g_handlerQiResolved = false;
}

namespace Plugin {

// Observes plugin lifecycle notifications. On the observed plugin's
// Deactivated() it probes QueryInterface to pin the notification-ordering
// contract: fired in DEACTIVATION state, with _handler still alive, so both
// IShell and the handler interface resolve.
class ObserverPlugin : public PluginHost::IPlugin, public PluginHost::IPlugin::INotification {
public:
ObserverPlugin() = default;
~ObserverPlugin() override = default;
ObserverPlugin(const ObserverPlugin&) = delete;
ObserverPlugin& operator=(const ObserverPlugin&) = delete;

BEGIN_INTERFACE_MAP(ObserverPlugin)
INTERFACE_ENTRY(PluginHost::IPlugin)
INTERFACE_ENTRY(PluginHost::IPlugin::INotification)
END_INTERFACE_MAP

const string Initialize(PluginHost::IShell* shell) override
{
shell->Register(static_cast<PluginHost::IPlugin::INotification*>(this));
return string();
}
void Deinitialize(PluginHost::IShell* shell) override
{
shell->Unregister(static_cast<PluginHost::IPlugin::INotification*>(this));
}
string Information() const override { return string(); }

void Activated(const string&, PluginHost::IShell*) override {}
void Unavailable(const string&, PluginHost::IShell*) override {}
void Deactivated(const string& callsign, PluginHost::IShell* plugin) override
{
if (callsign != TestSupport::g_observedCallsign) {
return;
}
TestSupport::g_deactivatedFired = true;

PluginHost::IShell* asShell = plugin->QueryInterface<PluginHost::IShell>();
TestSupport::g_shellQiWorked = (asShell != nullptr);
if (asShell != nullptr) { asShell->Release(); }

PluginHost::IPlugin* asPlugin = plugin->QueryInterface<PluginHost::IPlugin>();
TestSupport::g_handlerQiResolved = (asPlugin != nullptr);
if (asPlugin != nullptr) { asPlugin->Release(); }
}
};

SERVICE_REGISTRATION(ObserverPlugin, 1, 0)

} // namespace Plugin
} // namespace Thunder
19 changes: 19 additions & 0 deletions Tests/statemachine/ReentrantControl.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once

#include <cstdint>

namespace Thunder {
namespace TestSupport {

enum class ReentrantTrigger : uint8_t {
Activate,
Deactivate,
Unavailable,
Hibernate
};

extern ReentrantTrigger g_reentrantTrigger; // set by the test before activation
extern uint32_t g_reentrantResult; // captured result of the re-entrant call

} // namespace TestSupport
} // namespace Thunder
56 changes: 56 additions & 0 deletions Tests/statemachine/ReentrantPlugin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#include "Module.h"
#include "ReentrantControl.h"

namespace Thunder {

namespace TestSupport {
ReentrantTrigger g_reentrantTrigger = ReentrantTrigger::Deactivate;
uint32_t g_reentrantResult = Core::ERROR_NONE;
}

namespace Plugin {

// Calls a lifecycle trigger on its OWN shell from within Initialize, i.e. on
// the transition thread. The re-entrancy guard must reject every such call
// with ERROR_ILLEGAL_STATE without deadlocking or corrupting state.
class ReentrantPlugin : public PluginHost::IPlugin {
public:
ReentrantPlugin() = default;
~ReentrantPlugin() override = default;
ReentrantPlugin(const ReentrantPlugin&) = delete;
ReentrantPlugin& operator=(const ReentrantPlugin&) = delete;

BEGIN_INTERFACE_MAP(ReentrantPlugin)
INTERFACE_ENTRY(PluginHost::IPlugin)
END_INTERFACE_MAP

const string Initialize(PluginHost::IShell* shell) override
{
using Trigger = TestSupport::ReentrantTrigger;
const auto why = PluginHost::IShell::reason::REQUESTED;

switch (TestSupport::g_reentrantTrigger) {
case Trigger::Activate:
TestSupport::g_reentrantResult = shell->Activate(why);
break;
case Trigger::Deactivate:
TestSupport::g_reentrantResult = shell->Deactivate(why);
break;
case Trigger::Unavailable:
TestSupport::g_reentrantResult = shell->Unavailable(why);
break;
case Trigger::Hibernate:
TestSupport::g_reentrantResult = shell->Hibernate(0);
break;
}

return string(); // Initialize itself succeeds; the re-entry was rejected.
}
void Deinitialize(PluginHost::IShell* /* shell */) override {}
string Information() const override { return string(); }
};

SERVICE_REGISTRATION(ReentrantPlugin, 1, 0)

} // namespace Plugin
} // namespace Thunder
21 changes: 21 additions & 0 deletions Tests/statemachine/StateControlControl.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once
#include <string>
#include <vector>

namespace Thunder {
namespace TestSupport {
// Observation surface for the in-process StateControl mock. Because the mock
// runs in the test process, the test reads these directly. (An OOP variant
// cannot, which is why these tests do not transfer to OOP.)
extern std::vector<int> g_stateControlRequests; // command per Request() call: SUSPEND=0, RESUME=1
extern int g_stateControlSinkCount; // currently registered INotification sinks
extern int g_stateControlNotifyCount; // StateChange notifications emitted to sinks

inline void ResetStateControlObservation()
{
g_stateControlRequests.clear();
g_stateControlSinkCount = 0;
g_stateControlNotifyCount = 0;
}
}
}
Loading
Loading