Plugin Lifecycle
A key responsibility of the Thunder framework is managing the lifecycle of its plugins. All plugins are subject to the same rules regarding lifecycle, which is designed to ensure consistent behaviour. The Controller plugin is responsible for managing plugin lifecycle. Some lifecycle features such as suspend/resume require plugins to implement specific interfaces.
Overview
Each plugin goes through a sequence of states when activated or deactivated. Plugins can be configured to activate automatically when Thunder is started or can be activated manually using the Controller plugin.
Unless otherwise stated in the config, a plugin will default to the Deactivated state when Thunder is started.
Plugins can be in one of the following states at a given time:
State | Description |
---|---|
Deactivated | The plugin library has been loaded but the plugin has not been initialised and is not running. In this state, it is not possible to call any methods on the plugin |
Deactivation | The plugin is currently being deactivated |
Activated | The plugin library has been loaded and the plugin is initialised. The plugin is running and will respond to function calls. |
Activation | The plugin is currently being activated |
Unavailable | An administrative state used to indicate the plugin is known but not available (e.g. it might be downloaded later) to prevent accidental activation. The plugin must be explicitly moved to Deactivated before it can be activated. |
Precondition | The plugin is currently waiting on preconditions to be met before it will activate. Once the preconditions are met, it will move to activated |
Suspended | Only available if the plugin implements IStateControl A sub-state of Activated . The plugin is activated but has been placed into a suspended state. The exact behaviour of a suspended plugin will depend on the implementation of the IStateControl interface. |
Resumed | Only available if the plugin implements IStateControl A sub-state of Activated . The plugin is activated and not in a suspended state. The plugin has been initialised, is running and will respond to function calls. |
Hibernated | Only available if Thunder is built with Hibernation support The plugin has been placed into a hibernated state where the contents of its memory have been flushed to disk. The plugin is not running and will not respond to requests. |
Activation & Deactivation
Activation and deactivation are the core lifecycle events in Thunder.
During plugin activation, the library is loaded, constructed and the Initialize()
method is called. Once the initialise method returns, activation is considered complete and the plugin moves to the Activated state.
When a plugin is deactivated the reverse happens; the Deinitialize()
method is called, the plugin moves to the Deactivated state and the library is destructed and unloaded. As a result, once a plugin is deactivated it is safe to replace the library file (perhaps to upgrade to a new version) without needing to restart the framework.
Note
All plugin libraries will be quickly loaded & unloaded once during Thunder startup regardless of plugin start mode to retrieve the version information.
Every plugin must implement the Initialize()
and Deinitialize()
methods from the IPlugin
interface, which are called during plugin activation and deactivation respectively. After Initialize()
completes, the plugin must be in a state where it is ready to respond to incoming method calls.
The Initialize()
method takes a parameter containing a pointer to the plugin's IShell
interface. This allows access to information about the plugin instance, such as the loaded configuration. Plugins may want to store a reference to the shell for later access.
If there is a fault during initialisation, Initialize()
should return a non-empty string that contains the description of the error. The plugin will then move back to a deactivated state. If it returns an empty string, this indicates a successful initialisation and the plugin will move into the Activated state.
Danger
A plugin should do all setup and teardown work in the Initialize()
/Deinitialize()
methods, not in the plugin constructor/destructor. Failure to do so may cause stability issues or crashes. The constructor should only be used for simple variable/memory initialization
const string TestPlugin::Initialize(PluginHost::IShell* service)
{
ASSERT(_service == nullptr);
TRACE(Trace::Initialisation, (_T("Initializing TestPlugin")));
_service = service;
_service->AddRef();
// Success, return an empty string
return std::string();
}
void TestPlugin::Deinitialize(PluginHost::IShell* service)
{
ASSERT(_service == service);
TRACE(Trace::Initialisation, (_T("Deinitializing TestPlugin")));
_service->Release();
_service = nullptr;
}
Preconditions
Plugins can define pre-conditions either in their config file or in their metadata, which are Thunder subsystems that must be active for the plugin to move to an activated state.
If an attempt is made to activate the plugin whilst the preconditions are not met, then the plugin will be placed in a Preconditions state and wait. As soon as the preconditions are met, then the plugin will automatically move to the activated state.
Reasons
When a plugin is activated or deactivated, a reason must be provided to explain why the state change occurred.
virtual Core::hresult Activate(const reason) = 0;
virtual Core::hresult Deactivate(const reason) = 0;
The following reasons are available as defined in IShell
Reason | Description |
---|---|
Requested | The state change was intentionally requested - either by Thunder itself or by a client application. This is the default reason |
Automatic | The state change occurred automatically (e.g. the Monitor plugin might restart a plugin that crashed and would specify the Automatic reason to indicate this was not a manual decision) |
Failure | A generic error occurred and the plugin changed state to reflect this. An example scenario that would trigger this reason would be the out-of-process component of a plugin unexpectedly crashing |
Memory Exceeded | The plugin exceeded a given memory limit. Used by the Monitor plugin |
Startup | The state changed due to the plugin being automatically activated when Thunder was started |
Shutdown | The state changed due to Thunder shutting down |
Conditions | The state changed due to plugin preconditions no longer being satisfied |
Watchdog Expired | The state changed due to a watchdog expiring. For example, used by the WebKitBrowser plugin to provide a hang-detection mechanism that deactivates the plugin if the browser stops responding. |
Initialization Failed | The Initialize() method of the plugin returned an error and the plugin could not transition to the Activated state |
Certain reasons (as defined in the exitreasons
value in the main Thunder configuration) may trigger the post-mortem handler for easier debugging.
Suspend & Resume
Some plugins may wish to implement the ability for clients to suspend or resume their plugin. This can be useful for plugins that may need to free up resources when not in active use, or to pause background jobs without needing to deactivate the entire plugin.
Since the behaviour of suspend and resume is specific to a particular plugin, Thunder does not enforce suspend/resume support. Instead, if a plugin wishes to allow suspend/resume behaviour, then it must implement the PluginHost::IStateControl
interface. It is then the plugins responsibility to take suitable actions when moving in or out of a suspended state.
IStateControl Interface
The IStateControl
interface (Source/plugins/IStateControl.h
) requires the plugin to provide implementations for a number of pure virtual methods in order to support suspend/resume.
Warning
The Configure()
method is deprecated but kept for backwards compatibility. It has been replaced with the IConfiguration
interface. For new plugins, inherit from IConfiguration
if a configuration method is required. Otherwise the method should be a stub and just return Core::ERROR_NONE
.
// Deprecated
virtual Core::hresult Configure(PluginHost::IShell* framework) = 0;
// Return the current state of the plugin
virtual state State() const = 0;
// This method is called whenever a state change is requested for the plugin
// The plugin should take whatever action is required to change its state
// Return an error code if the plugin cannot transition state
virtual Core::hresult Request(const command state) = 0;
// Allow clients to register/unregister for state change notifications
virtual void Register(IStateControl::INotification* notification) = 0;
virtual void Unregister(IStateControl::INotification* notification) = 0;
The interface also defines a notification the plugin should raise on state change to allow COM-RPC clients to subscribe to state change notifications for the plugin
struct INotification : virtual public Core::IUnknown {
enum {
ID = RPC::ID_STATECONTROL_NOTIFICATION
};
virtual void StateChange(const IStateControl::state state) = 0;
};
A plugin should inherit from the interface class, add it to the plugin interface map and add overrides for those methods. It should also maintain a local variable with its current state - which should be initialised to PluginHost::IStateControl::UNINITIALIZED
.
The important method is Request(...)
which is called whenever a state change is requested for the plugin. The plugin should compare the requested state to the current state, and if required different take whatever actions are necessary to transition itself to the new state before updating its state internally. If the state changes, it should then raise the StateChange
notification with the new state.
If the requested state change is invalid (e.g. resuming an already resumed plugin) then return Core::ERROR_ILLEGAL_STATE
.
Core::hresult TestPlugin::Configure(PluginHost::IShell* service)
{
// Stub out
return Core::ERROR_NONE;
}
/**
* @brief Return the current plugin state
*/
PluginHost::IStateControl::state TestPlugin::State() const
{
return _currentState;
}
/**
* @brief Called when a request is made to change the plugin state.
*/
Core::hresult TestPlugin::Request(const PluginHost::IStateControl::command state)
{
Core::hresult result = Core::ERROR_ILLEGAL_STATE;
_adminLock.Lock();
TRACE(Trace::Information, (_T("Received state change request from %s to %s"),
IStateControl::ToString(_currentState), IStateControl::ToString(state)));
if (_currentState == PluginHost::IStateControl::state::RESUMED &&
state == PluginHost::IStateControl::command::SUSPEND) {
// Request to move from resumed -> suspended
// Do whatever action is necessary to suspend the plugin
_currentState = PluginHost::IStateControl::state::SUSPENDED;
result = Core::ERROR_NONE;
} else if (_currentState == PluginHost::IStateControl::state::SUSPENDED &&
state == PluginHost::IStateControl::command::RESUME) {
// Request to move from suspended -> resumed
// Do whatever action is necessary to resume the plugin
_currentState = PluginHost::IStateControl::state::RESUMED;
result = Core::ERROR_NONE;
} else {
// Trying to move from/to the same state
TRACE(Trace::Warning, (_T("Illegal state change")));
}
// Fire off a notification to subscribed clients if we changed state successfully
if (result == Core::ERROR_NONE) {
for (const auto& client : _stateChangeClients) {
client->StateChange(_currentState);
}
}
_adminLock.Unlock();
return result;
}
/**
* @brief Called by COM-RPC clients to subscribe to state change notifications
*/
void TestPlugin::Register(IStateControl::INotification* notification)
{
_adminLock.Lock();
// Make sure a sink is not registered multiple times.
if (std::find(_stateChangeClients.begin(), _stateChangeClients.end(), notification) == _stateChangeClients.end()) {
_stateChangeClients.push_back(notification);
notification->AddRef();
}
_adminLock.Unlock();
}
/**
* @brief Called by COM-RPC clients to unsubscribe from state change notifications
*/
void TestPlugin::Unregister(IStateControl::INotification* notification)
{
_adminLock.Lock();
auto index = std::find(_stateChangeClients.begin(), _stateChangeClients.end(), notification);
if (index != _stateChangeClients.end()) {
(*index)->Release();
_stateChangeClients.erase(index);
}
_adminLock.Unlock();
}
Hibernate
Note
Hibernated
state is not available by default. To enable it proper option must be switch on while building Thunder.
Hibernate is a state where plugin is not running and will not respond to requests. Memory of the plugin is flushed to disk and released. When a plugin goes into Suspended
state it slows down the CPU usage but it still occupies memory. Hibernate allows memory recovery of plugins that are not currently in use. This can come in handy when there is very little available and we need to be careful not to use it all. As the memory must first be written to and then read from disk, this increases the recovery time of the plugin.
To put plugin in Hibernated
state three conditions must be met:
- Plugin must be running
Out of process
- Plugin must be
Activated
- Plugin must be
Suspended
Fulfilment of these two conditions allows the Controller to save the contents of the plugin memory to disk and release it for further use by the system. To Hibernate
plugin Hibernate()
function needs to be called. For every process (parent and children) HibernateProcess()
is called.
uint32_t HibernateProcess(const uint32_t timeout, const pid_t pid, const char data_dir[], const char volatile_dir[], void** storage)
{
assert(*storage == NULL);
CheckpointMetaData* metaData = (CheckpointMetaData*) malloc(sizeof(CheckpointMetaData));
assert(metaData);
metaData->pid = pid;
*storage = (void*)(metaData);
return HIBERNATE_ERROR_NONE;
}
Suspended
state. In case of success, HIBERNATE_ERROR_NONE
is returned.
Resuming Plugin
To resume plugin Wakeup()
needs to be called. It is done automatically when you invoke the Activate()
method. Controller will read the flushed memory from the disk and make the plugin running and responding to requests again. For every process (parent and children) WakeupProcess()
is called.
uint32_t WakeupProcess(const uint32_t timeout, const pid_t pid, const char data_dir[], const char volatile_dir[], void** storage)
{
assert(*storage != NULL);
CheckpointMetaData* metaData = (CheckpointMetaData*)(*storage);
assert(metaData->pid == pid);
free(metaData);
*storage = NULL;
return HIBERNATE_ERROR_NONE;
}
In case of success, similar to HibernateProcess()
, HIBERNATE_ERROR_NONE
is returned
Enabling Hibernate
To enable hibernate you need to build Thunder with cmake
option HIBERNATE_CHECKPOINTLIB=ON
. You can do this with this command:
cmake -DHIBERNATE_CHECKPOINTLIB=ON
Warning
To enable HIBERNATE_CHECKPOINTLIB
, you must have the Memcr
library available in your project. Link to Memcr lib. Make sure the library is correctly installed and that CMake can find it using the find_package command.
Unavailable Plugins
If required, it is possible to move a plugin to the Unavailable state. This is a purely administrative state that behaves almost identically to the Deactivated state. The only difference is the allowed state transitions in/out of the state - it is not possible to activate an unavailable plugin without first moving it to a deactivated state.
This state was added to make it easier to distinguish between a plugin that is deactivated and a plugin that might not actually be installed on the platform. However, it is not a requirement to use, since plugin libraries are unloaded once the plugin is deactivated.
By default, a plugin will start in the deactivated state. Using the startmode
option in the plugin config file, it is possible to change this so a plugin starts in the unavailable state.
Clients: Changing Plugin State
The Controller plugin is responsible for managing plugin lifecycle in Thunder as well as general information and configuration tasks. Controller implements the IController
interface (Source/plugins/IController.h
) and exposes both JSON and COM-RPC interfaces.
JSON-RPC
To change the state of a plugin, call the appropriate method on the Controller plugin. E.G
Activate Plugin
Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "Controller.1.activate",
"params": {
"callsign": "TestPlugin"
}
}
Response
{
"jsonrpc": "2.0",
"id": 1,
"result": null
}
If you attempt to transition a plugin to an invalid state (e.g. trying to move from Unavailable directly to Activated), then an ERROR_ILLEGAL_STATE error will be returned
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": 5,
"message": "The service is in an illegal state!!!."
}
}
Check State
The status
property on Controller will show the current state of the plugin
Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "Controller.1.status@TestPlugin",
"params": {}
}
Response
{
"jsonrpc": "2.0",
"id": 1,
"result": [
{
// ...
"state": "resumed",
// ...
}
]
}
COM-RPC
There are multiple options for controlling plugin state over COM-RPC:
- Request the IController interface from Thunder and invoke methods on this to change any plugin state
- Request the IShell interface of a specific plugin and control the state of that particular plugin
- Note: IShell will allow interface traversal to other plugins using
QueryInterfaceByCallsign(...)
- If the plugin implements
IStateControl
, then query for that interface then call theRequest()
method on that interface to trigger a state change
Controller
#include <com/com.h>
#include <core/core.h>
#include <plugins/plugins.h>
using namespace Thunder;
int main(int argc, char const* argv[])
{
{
// Controller's ILifeTime interface is responsible for plugin activation/deactivation
RPC::SmartInterfaceType<Exchange::IController::ILifeTime> controllerLink;
auto success = controllerLink.Open(RPC::CommunicationTimeOut, controllerLink.Connector(), "Controller");
if (success == Core::ERROR_NONE && controllerLink.IsOperational()) {
auto controller = controllerLink.Interface();
if (controller) {
// Controller will automatically provide the "Requested" reason
auto activationSuccess = controller->Activate("TestPlugin");
if (activationSuccess == Core::ERROR_NONE) {
printf("Successfully activated TestPlugin\n");
} else {
printf("Failed to activate TestPlugin with error %d (%s)\n", activationSuccess,
Core::ErrorToString(activationSuccess));
}
controller->Release();
}
}
controllerLink.Close(Core::infinite);
}
Core::Singleton::Dispose();
return 0;
}
IShell
#include <com/com.h>
#include <core/core.h>
#include <plugins/plugins.h>
using namespace Thunder;
int main(int argc, char const* argv[])
{
{
auto engine = Core::ProxyType<RPC::InvokeServerType<4, 0, 1>>::Create();
auto client = Core::ProxyType<RPC::CommunicatorClient>::Create(Core::NodeId(_T("/tmp/communicator")),
Core::ProxyType<Core::IIPCServer>(engine));
if (client.IsValid()) {
// Open the TestPlugin IShell
auto shell = client->Open<PluginHost::IShell>("TestPlugin", ~0, RPC::CommunicationTimeOut);
if (shell) {
auto success = shell->Activate(PluginHost::IShell::REQUESTED);
if (success == Core::ERROR_NONE) {
printf("Successfully activated TestPlugin\n");
} else {
printf("Failed to activate TestPlugin with error %d (%s)\n", success,
Core::ErrorToString(success));
}
shell->Release();
}
if (client->IsOpen()) {
client->Close(RPC::CommunicationTimeOut);
}
}
client.Release();
}
Core::Singleton::Dispose();
return 0;
}
IStateControl
#include <com/com.h>
#include <core/core.h>
#include <plugins/plugins.h>
using namespace Thunder;
int main(int argc, char const* argv[])
{
{
auto engine = Core::ProxyType<RPC::InvokeServerType<4, 0, 1>>::Create();
auto client = Core::ProxyType<RPC::CommunicatorClient>::Create(Core::NodeId(_T("/tmp/communicator")),
Core::ProxyType<Core::IIPCServer>(engine));
if (client.IsValid()) {
// Check if the plugin implements IStateControl
auto stateControl = client->Open<PluginHost::IStateControl>("TestPlugin", ~0, RPC::CommunicationTimeOut);
if (!stateControl) {
printf("Plugin does not support IStateControl so cannot be suspended\n");
} else {
// Suspend the plugin
auto success = stateControl->Request(PluginHost::IStateControl::command::SUSPEND);
if (success == Core::ERROR_NONE) {
printf("Successfully suspended TestPlugin\n");
} else {
printf("Failed to suspend TestPlugin with error %d (%s)\n", success,
Core::ErrorToString(success));
}
stateControl->Release();
}
if (client->IsOpen()) {
client->Close(RPC::CommunicationTimeOut);
}
}
client.Release();
}
Core::Singleton::Dispose();
return 0;
}
State Change Notifications
When a plugin changes state, Thunder will send out a notification to interested subscribers. This allows client applications to take action on plugin state changes.
JSON-RPC
The Controller plugin will emit state change notifications over JSON-RPC to any websocket client who is subscribed. To receive notifications, subscribe to the statechanged
event
Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "Controller.1.register",
"params": {
"event": "statechange",
"id": "sampleClient"
}
}
Then, whenever a state change occurs a message will be sent over the websocket connection containing:
- The callsign of the plugin that changed state
- The new state of the plugin
- The reason the state changed
Event
{
"jsonrpc": "2.0",
"method": "sampleClient.statechange",
"params": {
"callsign": "TestPlugin",
"state": "Deactivated",
"reason": "Requested"
}
}
COM-RPC
Controller's ILifeTime
interface provides a notification that can be subscribed to for state changes.
For the sake of an example, the below code will subscribe to state change notifications then wait for 10 seconds. If a state change occurs, it will print the plugin that changed state, the state it changed to and the reason.
#include <com/com.h>
#include <core/core.h>
#include <plugins/plugins.h>
using namespace Thunder;
class StateChangeNotification : public Exchange::IController::ILifeTime::INotification {
public:
StateChangeNotification() = default;
~StateChangeNotification() = default;
void StateChange(const string& callsign, const PluginHost::IShell::state& state, const PluginHost::IShell::reason& reason) override
{
// Use EnumerateType to convert enum to human-readable string
printf("Plugin %s has changed state to %s due to %s\n", callsign.c_str(),
Core::EnumerateType<PluginHost::IShell::state>(state).Data(),
Core::EnumerateType<PluginHost::IShell::reason>(reason).Data());
}
BEGIN_INTERFACE_MAP(StateChangeNotification)
INTERFACE_ENTRY(Exchange::IController::ILifeTime::INotification)
END_INTERFACE_MAP
};
int main(int argc, char const* argv[])
{
{
// Controller's ILifeTime interface is responsible for plugin activation/deactivation
RPC::SmartInterfaceType<Exchange::IController::ILifeTime> controllerLink;
auto success = controllerLink.Open(RPC::CommunicationTimeOut, controllerLink.Connector(), "Controller");
if (success == Core::ERROR_NONE && controllerLink.IsOperational()) {
auto controller = controllerLink.Interface();
if (controller) {
Core::Sink<StateChangeNotification> stateChangeNotification;
controller->Register(&stateChangeNotification);
printf("Waiting for state change notifications\n");
SleepS(10);
printf("Finished\n");
controller->Unregister(&stateChangeNotification);
controller->Release();
}
}
controllerLink.Close(Core::infinite);
}
Core::Singleton::Dispose();
return 0;
}
/* Output
Waiting for state change notifications
Plugin TestPlugin has changed state to Activated due to Requested
Finished
*/