Skip to content

Concepts

Plugins and control

A plugin is a class that runs in the process of a device. It can read and set properties, follow the state of the device and drive the state machine. Each device that starts with runDevice.h has two built-in plugins: config reads the channel configuration, and control moves the device through its states.

Class Task
fair::mq::Plugin The base class of all plugins. It holds the name, the version, the maintainer and the homepage of the plugin, and it gives the plugin API.
fair::mq::PluginManager Finds, loads, creates and owns the plugins.
fair::mq::PluginServices Connects the plugins to the device and to its properties. All plugins of a device use the same object.
fair::mq::DeviceRunner Starts the device with its properties and its plugins. runDevice.h uses it.

A plugin does not use the device directly. Each call goes through PluginServices:

plugin ──► PluginServices ──► Device (state, transitions)
└─► ProgOptions (properties)

DeviceRunner::Run() loads and creates the plugins in this sequence (DeviceRunner.cxx):

  1. The constructor of DeviceRunner makes the plugin manager. The plugin manager reads -S and -P and loads the plugins of -P, in their sequence.
  2. Run() loads the built-in plugin config.
  3. The runner hook hooks::LoadPlugins runs. Your code can load more plugins here.
  4. Run() loads the built-in plugin control.
  5. The runner hook hooks::SetCustomCmdLineOptions runs. runDevice.h adds the options of addCustomOptions() here.
  6. The runner adds the options of all plugins to the command-line options. The runner hook hooks::ModifyRawCmdLineArgs runs. Then the runner parses the command line. With --help or --print-options, Run() prints the options and returns 0.
  7. The runner hook hooks::InstantiateDevice makes the device.
  8. The plugin manager makes the PluginServices object. Then it creates the plugins, in the load sequence.
  9. The device runs its state machine until the state EXITING.
  10. The runner waits until no plugin has the controller role. Then Run() returns 0.

The constructor of a plugin runs after the parsing of the command line, so a plugin can read its options there. The control plugin comes last. A plugin that takes the controller role in its constructor gets it before the control plugin.

To load plugins without DeviceRunner, see Load plugins without DeviceRunner.

Give the name of a plugin to -P (--plugin), or to PluginManager::LoadPlugin() in a runner hook. A prefix selects the mechanism (PluginManager.cxx):

Name Mechanism Where the plugin manager looks
name or d:name Dynamic In a shared library file. See Plugin files and search paths.
p:name Prelinked In the program. The executable links the library of the plugin at build time.
s:name Static In the program: the built-in plugins config and control, or a plugin that the executable contains.

Give -P once for each plugin, for example -P first -P second. The plugin manager does not split a name at commas. It loads a dynamic or a prelinked plugin only once, also when you give its name twice.

For the dynamic plugin name, the plugin manager looks for two file names: libfairmq-plugin-name.so and then libFairMQPlugin_name.so (the suffix is the one of the platform, .so on Linux). It looks in these places, in this sequence:

  1. Each directory of -S (--plugin-search-path).
  2. The system lookup of the dynamic linker: the system library directories, RUNPATH and LD_LIBRARY_PATH.

-S takes one or more directories. The paths without a prefix make the list. Then the plugin manager puts each path with the prefix < at the start of the list. It puts each path with the prefix > at the end. Without -S, the list is empty. The test PluginManager.Factory gives this example:

Terminal window
-S '>/lib' '</home/user/lib' /usr/local/lib /usr/lib

The search paths are then /home/user/lib, /usr/local/lib, /usr/lib and /lib. Put quotes around paths with < or >, because the shell uses these characters for redirection. The plugin manager ignores a directory that does not exist. A path to a file, or an empty path, gives the error PluginManager::BadSearchPath.

When no file loads, the plugin manager throws PluginManager::PluginLoadError:

Could not load dynamic plugin name from configured search paths (-S) nor with system lookup (system libdirs, RUNPATH, $LD_LIBRARY_PATH).

The main() function of runDevice.h catches the exception, logs it and returns 1. A file that exists but does not load, for example because of a missing symbol, gives the same error.

The functions of fair::mq::Plugin call PluginServices. They give the name of the plugin as the ID of the subscriber and of the controller.

Area Functions
State GetCurrentDeviceState(), SubscribeToDeviceStateChange(), UnsubscribeFromDeviceStateChange(), ToDeviceState(), ToDeviceStateTransition(), ToStr()
Control TakeDeviceControl(), StealDeviceControl(), ReleaseDeviceControl(), ChangeDeviceState()
Read properties PropertyExists(), GetProperty<T>(), GetPropertyAsString(), GetProperties(), GetPropertiesStartingWith(), GetPropertiesAsString(), GetPropertiesAsStringStartingWith(), GetPropertyKeys(), GetChannelInfo()
Change properties SetProperty<T>(), SetProperties(), UpdateProperty<T>(), UpdateProperties(), DeleteProperty()
Property changes SubscribeToPropertyChange<T>(), UnsubscribeFromPropertyChange<T>(), SubscribeToPropertyChangeAsString(), UnsubscribeFromPropertyChangeAsString()
Logging CycleLogConsoleSeverityUp(), CycleLogConsoleSeverityDown(), CycleLogVerbosityUp(), CycleLogVerbosityDown()
Metadata GetName(), GetVersion(), GetMaintainer(), GetHomepage()

The state machine calls the state callback at the start of each new state, in the thread of the state machine. The device starts the handler of the state after all callbacks return, so keep the callbacks short. A callback for a property change runs in the thread that changes the property.

GetNumberOfConnectedPeers() exists for tests. The code marks it for removal.

Only one plugin at a time can change the state of the device. This plugin is the controller.

Function Effect
TakeDeviceControl() The plugin becomes the controller, if no plugin has the role. If the plugin has the role, nothing changes. If another plugin has the role, the function throws PluginServices::DeviceControlError.
ChangeDeviceState(transition) Requests a transition. If no plugin has the role, the plugin takes it first. If another plugin has the role, the function throws DeviceControlError. It returns false when the device cannot do the transition now.
StealDeviceControl() The plugin becomes the controller, also when another plugin has the role. The control plugin uses it after a signal.
ReleaseDeviceControl() The plugin gives the role back, if it has the role.

DeviceRunner waits after the end of the state machine until no plugin has the role. Thus a controller must release the role, normally in the state EXITING.

The device changes the state when the handler of the current state returns. A requested transition interrupts blocking transfers, and NewStatePending() then returns true, so that the handler can return. See Device states.

The built-in plugin config adds the device options, for example --id, --transport, --session, --rate, --mq-config, --channel-config and the options of the shmem transport. At the start of the state INITIALIZING DEVICE, it reads the channel configuration and sets the channel properties:

fairmq/plugins/config/Config.cxx
SubscribeToDeviceStateChange([&](DeviceState newState) {
if (newState == DeviceState::InitializingDevice) {
string idForParser;
if (PropertyExists("config-key")) {
idForParser = GetProperty<string>("config-key");
} else if (PropertyExists("id")) {
idForParser = GetProperty<string>("id");
}
if (!idForParser.empty()) {
try {
if (PropertyExists("mq-config")) {
LOG(debug) << "mq-config: Using default JSON parser";
SetProperties(JSONParser(GetProperty<string>("mq-config"), idForParser));
} else if (PropertyExists("channel-config")) {
LOG(debug) << "channel-config: Parsing channel configuration";
SetProperties(SuboptParser(GetProperty<vector<string>>("channel-config"), idForParser));
} else {
LOG(info) << "fair::mq::plugins::Config: no channels configuration provided via --mq-config or --channel-config";
}
} catch (exception& e) {
LOG(error) << e.what();
}
} else {
LOG(warn) << "No device ID or config-key provided for the configuration parser.";
}
}
});
fairmq/plugins/config/Config.cxx:25–53

The key of the device in the configuration is --config-key, or --id if --config-key is not set. --mq-config has priority over --channel-config. When the parser fails, the plugin logs the error and the device continues. See Configuration and properties.

The built-in plugin control has two options: --control (default dynamic) and --catch-signals (default 1). Its constructor takes the controller role, unless --control is none. If another plugin has the role already, the control plugin logs the reason at the severity debug and starts no controller. Its signal handling does not depend on the controller role.

--control What the control plugin does
dynamic (default), interactive, external It runs the startup sequence. Then it reads keys from the terminal until you press q, and runs the shutdown sequence.
static It runs the startup sequence. When the device goes back to READY, for example because ConditionalRun() returns false, it runs the shutdown sequence.
gui It runs the startup sequence. Then it waits for a signal or for the state ERROR.
none It does not take the controller role. Another plugin or your code must change the states.
Another value It logs Unrecognized control mode at the severity error and uses static.

Use --control static in scripts. This mode needs no terminal, and it ends the device when the work in RUNNING is done. The test scripts of the examples use it.

For the effect of each mode on the end of a run, see What the controllers do.

The startup sequence requests the transitions INIT DEVICE, COMPLETE INIT, BIND, CONNECT, INIT TASK and RUN. Before each request, the plugin waits for the state that the previous transition gives. At the end, the device is in RUNNING.

The shutdown sequence starts from the current state and repeats until EXITING. Then the plugin releases the controller role.

Current state Transition
RUNNING STOP
READY RESET TASK
DEVICE READY, BOUND, INITIALIZED RESET DEVICE
INITIALIZING DEVICE COMPLETE INIT
IDLE END

In the modes dynamic, interactive and external, a key works without Enter, and the terminal does not show it.

Key Action
h Print the help and the state machine with the keys.
c Log the current state, at the severity state.
p Log the number of connected peers of each sub-channel.
i Request INIT DEVICE, wait for INITIALIZING DEVICE, then request COMPLETE INIT.
b Request BIND.
x Request CONNECT.
j Request INIT TASK.
r Request RUN.
s Request STOP.
t Request RESET TASK.
d Request RESET DEVICE.
k, l Move the console severity one step up (fewer lines) or down (more lines).
n, m Move the log verbosity one step up or down.
q Stop reading keys and run the shutdown sequence.
Another key Print Invalid input: […]. and the help.

A key that the current state does not accept changes nothing. The state machine logs No transition from state … on transition … at the severity state.

With --catch-signals greater than 0, the control plugin handles SIGINT and SIGTERM (Control.cxx):

  1. The first signal starts a graceful shutdown. The plugin logs Received device shutdown request (signal …) and Waiting for graceful device shutdown. Hit Ctrl-C again to abort immediately.
  2. The plugin stops its controller.
  3. If its shutdown sequence has not run yet, the plugin takes the controller role with StealDeviceControl(), also from another plugin. Then it runs the shutdown sequence.
  4. A SIGINT after an earlier signal ends the process at once with std::abort(). A second SIGTERM has no effect.

With --catch-signals 0, the plugin logs the warning Signal handling (e.g. Ctrl-C) has been deactivated. A signal then has its default effect, and the device does not run the shutdown sequence.

An exception in a hook, or the transition ERROR FOUND, moves the device to ERROR. No transition leaves this state.

  1. The state machine stops, and RunStateMachine() throws: the exception of the hook, or StateMachine::ErrorStateException.
  2. The control plugin stops its controller. It does not run the shutdown sequence.
  3. The main() function of runDevice.h catches the exception, logs Uncaught exception reached the top of main: … and returns 1.

DeviceRunner::RunWithExceptionHandlers() also returns 1. It logs Uncaught exception reached the top of DeviceRunner: …. The tests in test/device/_exceptions.cxx check the exit code 1 for the modes static and interactive.

OptionDefaultDescription
--controlstringdynamicController of the state machine: static, dynamic (also interactive and external), gui or none. An unknown value gives static, with an error in the log.control plugin
--catch-signalsint1Handle SIGINT and SIGTERM: 1 (yes) or 0 (no). The first signal starts the shutdown sequence. A second SIGINT aborts the process.control plugin
-S, --plugin-search-pathvector<string>, several values—Directories to search for plugins, in this order. Put < before a path to put it at the start of the list, or > to put it at the end. If a plugin is not in these directories, the device tries the system lookup of libraries (system directories, RUNPATH, LD_LIBRARY_PATH).PluginManager
-P, --pluginvector<string>—Load these plugins, in this sequence. For the file libfairmq-plugin-example.so, the name is example. Put p: before the name of a prelinked plugin.PluginManager