Skip to content

InfinotedPluginDevelopment

Armin Burgmeier edited this page Aug 23, 2014 · 3 revisions

Infinoted Plugin Development

It is possible to develop third-party plugins for infinoted. Infinoted plugins need to be written in C or C++. A infinoted plugin basically consists of a shared object file (.so on Linux or .dll on Windows) with a specified entry point. Infinoted picks up all plugins in $PREFIX/lib/infinoted-0.6/plugins/, where $PREFIX is the prefix that infinoted has been installed in, typically /usr or /usr/local. Custom plugins therefore need to be installed in this directory.

The Basics

Infinoted plugins need to link against libinfinoted-plugin-manager-0.6, which ships with libinfinity. This is a small library which provides access to the state of the infinoted server to plugins.

Every plugin needs to export a symbol called INFINOTED_PLUGIN, of type [InfinotedPlugin](http://infinote.0x539.de/libinfinity/API/libinfinoted-plugin-manager/InfinotedPluginManager.html#InfinotedPlugin). The InfinotedPlugin is declared in infinoted/infinoted-plugin-manager.h, and contains basic information about the plugin, such as its name and its entry point. Below is how the structure looks like, and a description for each field.

struct _InfinotedPlugin {
  const gchar* name;
  const gchar* description;
  const InfinotedParameterInfo* options;

  gsize info_size;
  gsize connection_info_size;
  gsize session_info_size;
  const gchar* session_type;

  void(*on_info_initialize)(gpointer plugin_info);

  gboolean(*on_initialize)(InfinotedPluginManager* manager,
                           gpointer plugin_info,
                           GError** error);

  void(*on_deinitialize)(gpointer plugin_info);

  void(*on_connection_added)(InfXmlConnection* connection,
                             gpointer plugin_info,
                             gpointer connection_info);

  void(*on_connection_removed)(InfXmlConnection* connection,
                               gpointer plugin_info,
                               gpointer connection_info);

  void(*on_session_added)(const InfBrowserIter* iter,
                          InfSessionProxy* proxy,
                          gpointer plugin_info,
                          gpointer session_info);

  void(*on_session_removed)(const InfBrowserIter* iter,
                            InfSessionProxy* proxy,
                            gpointer plugin_info,
                            gpointer session_info);
};

The meaning of these fields in detail:

  • name: This is the name of the plugin. When the shared object file is called libinfinoted-plugin-X.so, then this should be set to X.
  • description: A human-readable description of what the plugin does.
  • options: A 0-terminated list of input options. See below for a detailed description of how options are passed to plugins.
  • info_size: The size, in bytes, that should be allocated for the plugin when it is instantiated. This is typically given by the structure where plugin-specific data is stored. This should be greater than 0.
  • connection_info_size: For each client connection, this many bytes are allocated and made available to the plugin to store connection-specific data. This is allowed to be 0 if the plugin does not have any connection-specific data.
  • session_info_size: For each active session on the server, this many bytes are allocated and made available to the plugin to store session-specific data. This is allowed to be 0 if the plugin does not have any connection-specific data.
  • session_type: If this is NULL, a session_info structure is created for every session on the server, no matter its type. If it is non-%NULL, a structure is only created for sessions of the given type. This can be used for example by plugins which only operate on text documents and not on chat sessions. In this case, this should be set to "InfTextSession".
  • on_info_initialize: This function is called after the plugin structure has been instantiated. It should initialize the structure to sane default values.
  • on_initialize: This is the primary initialization function for the plugin. This is called after the options for the plugin have been parsed, i.e. it can do all initialization based on the options provided to the plugin. It can also return FALSE and set the error parameter, in which case the server is being shutdown and an error is shown to the user. This can be used for example if some resources that the plugin needs are not available, or if the provided options are not valid.
  • on_deinitialize: This is called when the plugin is unloaded, and should free all resources allocated to the plugin. If on_initialize fails with an error, this function is also called to clean up the partly-initialized plugin.
  • on_connection_added: This function is called whenever there is a new client connection to the server, and also for all existing connections at the time the plugin is loaded.
  • on_connection_removed: This function is called whenever a connection to a client is closed, and also for all existing connections at the time the plugin is unloaded.
  • on_session_added: This function is called when a new session becomes active on the server, and also for all active sessions at the time the plugin is loaded.
  • on_session_removed: This function is called when a session becomes inactive on the server and is removed from RAM, and for all active sessions at the time the plugin is unloaded.

Note that the connection-specific and session-specific data and callbacks could also be implemented without being part of the plugin infrastructure. The plugin would just have to install appropriate signal handlers to signals of the InfdDirectory object. However, since most plugins need at such functionality, it is handled centrally by the plugin manager. Below is an example of how this structure can look like in an actual infinoted plugin:

const InfinotedPlugin INFINOTED_PLUGIN = {
  "example",
  "An example plugin that writes a greeting message into the log for every user that joins a session.",
  INFINOTED_PLUGIN_EXAMPLE_OPTIONS,
  sizeof(InfinotedPluginExample),
  0,
  0,
  NULL,
  infinoted_plugin_example_info_initialize,
  infinoted_plugin_example_initialize,
  infinoted_plugin_example_deinitialize,
  NULL,
  NULL,
  infinoted_plugin_example_session_added,
  infinoted_plugin_example_session_removed,
};

Plugin options

Options that can be provided to the plugin are declared with the [InfinotedParameterInfo](http://infinote.0x539.de/libinfinity/API/libinfinoted-plugin-manager/libinfinoted-plugin-manager-06-infinoted-parameter.html#InfinotedParameterInfo) structure. One such structure declares one parameter. An array of parameters is then given to the plugin in its options field. The InfinotedParameterInfo structure has the following fields:

struct _InfinotedParameterInfo {
  const char* name;
  InfinotedParameterType type;
  InfinotedParameterFlags flags;
  size_t offset;
  InfinotedParameterConvertFunc convert;
  char short_name;
  const char* description;
  const char* arg_description;
};

The fields have the following meanings.

  • name: The name of the option. In the infinoted configuration file, it can be set with name=XXX, where XXX will be its input value.
  • type: Specifies the type of the parameter input value. This can be one of INFINOTED_PARAMETER_BOOLEAN, INFINOTED_PARAMETER_INT, INFINOTED_PARAMETER_STRING', or INFINOTED_PARAMETER_STRING_LIST`.
  • flags: Either 0 or INFINOTED_PARAMETER_REQUIRED. If set to INFINOTED_PARAMETER_REQUIRED, the parameter must be given in the configuration file or an error is generated when the plugin is loaded.
  • offset: Offset in bytes into the plugin instance structure where the parameter output value will be stored. This is typically created with a macro such as offsetof or G_STRUCT_OFFSET.
  • convert: A conversion function, which converts the input value read from the configuration to an internal representation used by the plugin. It's signature is gboolean(*)(gpointer, gpointer, GError**), where the first pointer points to the output value, and the second pointer to the input value. If the function generates an error, the plugin is not loaded. See Conversion Functions below for more details.
  • short_name: A short one-character name for the parameter, like for short command line options. This is unused at the moment, but might be used at some point for command line option parsing.
  • description: A human-readable description of the parameter as it would appear in the --help output for command line options.
  • arg_description: A description for the argument of the parameter, if any, or NULL.

When a plugin is loaded, the initialization sequence is the following: First, on_info_initialize is called on the plugin. Then options are read from the configuration file and their values stored in the plugin instance. If one of the conversion function produces an error, the on_deinitialize is called and the plugin is not loaded. Otherwise, on_initialize is called on the plugin. If this produces an error, again on_deinitialize is called and the plugin unloaded. Otherwise, the plugin is fully initialized.

Due to the sequence outlined above, default values for parameters can be assigned in on_info_initialize, and then they might be overwritten by the parameter parsing.

Below is an example for a parameter list declaration. Note that the list needs to be 0-terminated.

static const InfinotedParameterInfo INFINOTED_PLUGIN_EXAMPLE_OPTIONS[] = {
  {
    "greeting-text",
    INFINOTED_PARAMETER_STRING,
    0, /* no flags, if parameter is not given default is used */
    G_STRUCT_OFFSET(InfinotedPluginExample, greeting_text),
    infinoted_parameter_convert_string,
    0,
    "Text that is written into the log for each user",
    "TEXT"
  }, {
    NULL,
    0,
    0,
    0,
    NULL
  }
};

Conversion functions

Conversion functions are used to convert the raw value read from the configuration file, which is one of gboolean, int, gchar* or gchar** (depending on the type field in the InfinotedParameterInfo structure) into the internal form consumed by the plugin. It also validates the parameter, for example it can make sure that for time intervals only positive values are allowed. While all sorts of conversion functions can be written by plugin developers, libinfinoted-plugin-manager provides a set of basic functions described in the following:

  • infinoted_parameter_convert_string (for INFINOTED_PARAMETER_STRING parameters): The input value is a pointer to a gchar*, and the output value is a pointer to a gchar*. Takes the input string as the output string. If the output string exists already, it is freed. This means that if you want to set default string values in on_info_initialize for parameters using this conversion functions, they should be dynamically allocated, for example with g_strdup.
  • infinoted_parameter_convert_string_list (for INFINOTED_PARAMETER_STRING_LIST parameters): The input value is a pointer to a gchar**, and the output value is a pointer to a gchar**. Takes the input string list as the output string list. If the output string list exists already, it is freed.
  • infinoted_parameter_convert_filename (for INFINOTED_PARAMETER_STRING parameters): The input value is a pointer to a gchar*, and the output value is a pointer to a gchar*. Converts the UTF-8 input string to the GLib filename encoding, so that it can be used when using it as a filename to open files.
  • infinoted_parameter_convert_boolean (for INFINOTED_PARAMETER_BOOLEAN parameters): The input value is a pointer to a gboolean, and the output value is a pointer to a gboolean. Takes the input value as output value.
  • infinoted_parameter_convert_port (for INFINOTED_PARAMETER_INTEGER parameters): The input value is a pointer to a gint, and the output value is a pointer to a guint. Takes the input value as output value if it is between 1 and 65535. Otherwise, an error is generated. This should be used for parameters that represent TCP and UDP port numbers.
  • infinoted_parameter_convert_nonnegative (for INFINOTED_PARAMETER_INTEGER parameters): The input value is a pointer to a gint, and the output value is a pointer to a guint. Takes the input value as output value if it is greater or equal than 0. Otherwise, an error is generated.
  • infinoted_parameter_convert_positive (for INFINOTED_PARAMETER_INTEGER parameters): The input value is a pointer to a gint, and the output value is a pointer to a guint. Takes the input value as output value if it is greater than 0. Otherwise, an error is generated.
  • infinoted_parameter_convert_security_policy (for INFINOTED_PARAMETER_STRING parameters): The input value is a pointer to a gchar*, and the output value is a pointer to a InfXmppConnectionSecurityPolicy. The only allowed input values are "no-tls", "allow-tls" and "require-tls", and they map to the values INF_XMPP_CONNECTION_SECURITY_ONLY_UNSECURED, INF_XMPP_CONNECTION_SECURITY_BOTH_PREFER_TLS, and INF_XMPP_CONNECTION_SECURITY_ONLY_TLS, respectively.

The plugin manager API

The InfinotedPluginManager instance that is passed to the on_initialize function can be used to access the state of the server. A pointer to it is therefore typically saved in the plugin instance structure. Typically, what a plugin would do, is to wait for various events by installing signal handlers for various signals of objects, such as when text is changed in a document, or a user joins a document.

The reference documentation for the available API is available online.

Tips & Tricks

The libinfinity API is quite extensive, but at some places a bit counter-intuitive to use. In the following, a few tips are gathered together to get you started.

  • The primary server object is InfdDirectory, which represents the document tree of the server. However, most operations on it are performed using the API of the InfBrowser interface, which InfdDirectory implements.

  • For navigating inside the directory, a InfBrowserIter is used. Such an iterator points to a single item in the tree. The iter argument to the on_session_added and on_session_removed callbacks of a plugin represents the document which is being edited within the session.

  • From the directory, a session can be obtained for each document. In this case, a InfdSessionProxy object is returned, which again implements the InfSessionProxy interface. The InfSessionProxy can be used to join a user into the session, which is required before being able to make changes to it. The actual session object, InfSession, is obtained from the session proxy with this code:

    InfSession* session; g_object_get(G_OBJECT(proxy), "session", &session, NULL); /* do something with session */ g_object_unref(session);

  • The InfSession object represents the editing session. It is used to send message between participants of the session.

  • The session object can be used to obtain the user table, with inf_session_get_user_table(), which allows to access the participants of the session, and be notified when users join or leave.

  • The session object can be used to obtain the buffer, with inf_session_get_buffer(), which, for text sessions, can be cast to InfTextBuffer, which allows to access the text of the document, and to be notified when the text changes.

An Example Plugin

A full example for an infinoted plugin is available here on github: infinoted-plugin-example. It is available under an ISC license. It works fully out-of-the-box, including an autotools setup that installs the plugin in the correct path for infinoted to pick it up, and the code for a small plugin. The plugin writes a short log message into the server log for every user that joins a session.

For further reference, the source code of the available plugins that ship with infinoted can be studied.