Table of Contents:

This guide describes creating an agent using the Centerbury legacy application framework. This framework is deprecated and therefore not recommended for new development.

An agent is a non graphical program which runs in the background. Agents can be used for playing music or computing expensive operations like indexing large databases, for example.

Following is a step-by-step guide for creating an agent. This guide is based on the agent sample application available in the Apertis repos. It will be handy to have the sample application code open while reading through this guide.

Our sample application is composed of two classes which will be explained in more detail below:

  • HlwAgent: A wrapper class that manages the process itself and the D-Bus object
  • HlwTickBoard: A child class that implements the D-Bus interface

Let's get started!

Agents are GApplications

The GApplication is the recommended base class for agents, just as it is for graphical applications. One of the advantages of being derived from GApplication is that most of the work of registering with D-Bus is handled by the class, so you need only override a few virtual functions in order to get a running agent.

The HlwAgent class is the entry point for running the agent. This class is defined as:

G_DEFINE_TYPE (HlwAgent, hlw_agent, G_TYPE_APPLICATION)

Create and launch an HlwAgent:

  app = G_APPLICATION (g_object_new (HLW_TYPE_AGENT,
      "application-id", "org.apertis.HelloWorld.Agent",
      "flags", G_APPLICATION_IS_SERVICE,
      NULL));

  g_application_run (app, argc, argv);

The application_id property of the GApplication is used to provide the D-Bus bus name. This id should be unique so it is recommend that you use @BUDNLE_ID@.Agent. GApplicationFlags should be set to G_APPLICATION_IS_SERVICE. See the description of flags for more information.

In the HlwAgent class, the dbus_register and dbus_unregister virtual functions should be overridden to catch whether the requested D-Bus name is available to use, andactivate should be overridden as well, which is a mandatory virtual function for all applications and agents.

static void
hlw_agent_class_init (HlwAgentClass *klass)
{
  GApplicationClass *app_class = G_APPLICATION_CLASS (klass);

  ...

  app_class->activate = hlw_agent_activate;
  app_class->dbus_register = hlw_agent_dbus_register;
  app_class->dbus_unregister = hlw_agent_dbus_unregister;

  ...
}

To cause HlwAgent to run in the background and prevent termination by the ending of the main function, the reference count should be held and released properly. In a graphical application, it's clear that g_application_hold () should be called when the window appears and g_application_release () when the window disappears, but unlike a graphical program, it's a bit less clear when to manage reference count in an agent. Fortunately, in the dbus_register function, we can assume that the agent is ready to export extra objects on the bus, making it a good place to take the reference with g_application_hold (), and in dbus_unregister we can release the reference count with g_application_release ().

static gboolean
hlw_agent_dbus_register (GApplication    *app,
                         GDBusConnection *connection,
                         const gchar     *object_path,
                         GError         **error)
{
  ...

  g_application_hold (app);

  ...
}

static void
hlw_agent_dbus_unregister (GApplication    *app,
                           GDBusConnection *connection,
                           const gchar     *object_path)
{
  ...

  g_application_release (app);

  ...
}

Once the activate virtual function is overridden, we will have a runnable agent skeleton.

D-Bus Interface

Using the GApplication class allows our agent to run as stand-alone non-graphical program, but it isn't yet able to interact with the outside world. It is recommended that D-Bus be used to communicate with other processes on the system. D-Bus code and documentation can be easily created by gdbus-codegen. For more details of the usages of the generator, refer to gdbus-codegen.

D-Bus XML Schema

To make our agent application D-Bus aware, let's introduce a simple XML D-Bus interface.

Our interface will contain the following:

  • method: ToggleTick
  • property: CurrentTick

By calling ToggleTick, an auto-incremented tick count function is enabled or disabled. Once the function is enabled, the property, CurrentTick, is increased by 1 every second.

<node name="/org/apertis/helloworld/agent/tickboard">
  <interface name="org.apertis.HelloWorld.Agent.TickBoard">
    <method name="ToggleTick"/>
    <property name="CurrentTick" type="i" access="read"/>
  </interface>
</node>

To generate the D-Bus code at make time, we introduce a simple rule to the Automake Makefile.am.

helloworld-agent/%.c: helloworld-agent/%.xml Makefile
	$(AM_V_GEN)$(GDBUS_CODEGEN) \
		--c-namespace=HlwDBus \
		--interface-prefix=org.apertis.HelloWorld.Agent \
		--generate-c-code helloworld-agent/$* $<

Note that --interface-prefix should be the same as the application-id of the agent.

D-Bus function implementation

The D-Bus skeleton is generated by gdbus-codegen according to the XML schema. Now we need to create actual behaviors for when the D-Bus APIs are called. In our example, the HlwDBusTickBoardSkeleton object and the HlwDBusTickBoard interface are created. Although there are various approaches to implementing this interface, being a child of the D-Bus skeleton object will help show how the generated virtual functions should be filled in.

G_DEFINE_TYPE_WITH_CODE (HlwTickBoard, hlw_tick_board,
                         HLW_DBUS_TYPE_TICK_BOARD_SKELETON,
                         G_IMPLEMENT_INTERFACE (HLW_DBUS_TYPE_TICK_BOARD,
                                                hlw_tick_board_tick_board_iface_init))

Next, handle_toggle_tick of HlwDbusTickBoardIface should be overridden.

static gboolean
hlw_tick_board_handle_toggle_tick (HlwDBusTickBoard *object,
                                   GDBusMethodInvocation *invocation)
{
  ...

  hlw_dbus_tick_board_complete_toggle_tick (object, invocation);

  return TRUE;
}

static void
hlw_tick_board_tick_board_iface_init (HlwDBusTickBoardIface *iface)
{
  iface->handle_toggle_tick = hlw_tick_board_handle_toggle_tick;
}

Exporting the interface to D-Bus

Once the implementation of the D-Bus interface is completed, it needs to be exported on the bus. HlwAgent is already registered on the bus so the HlwTickBoard, which is a child object of the D-Bus skeleton, can be easily exported in the dbus_register function.

static gboolean
hlw_agent_dbus_register (GApplication    *app,
                         GDBusConnection *connection,
                         const gchar     *object_path,
                         GError         **error)
{
  gboolean ret;

  ...

  /* chain up */
  ret = G_APPLICATION_CLASS (hlw_agent_parent_class)->dbus_register (
      app,
      connection,
      object_path,
      error);

  if (ret &&
      !g_dbus_interface_skeleton_export (
          G_DBUS_INTERFACE_SKELETON (self->tick_board),
          connection,
          "/org/apertis/HelloWorld/Agent/TickBoard",
          error))
    {
      g_warning ("Failed to export TickBoard D-Bus interface (reason: %s)",
          (*error)->message);
    }

  return ret;
}

Note that it is recommended that an application registers as above before exporting any other interfaces to D-Bus.

Apparmor profile

An agent must be granted different Apparmor permissions than those granted to standard application bundles. Several additional rules are required to allow an agent to access the bus.

/Applications/@BUNDLE_ID@/** {
  #include <abstractions/chaiwala-base>
  #include <abstractions/dbus-session-strict>

  /Applications/@BUNDLE_ID@/{bin,libexec}/* pix,
  /Applications/@BUNDLE_ID@/{bin,lib,libexec}/{,**} mr,
  /Applications/@BUNDLE_ID@/share/{,**} r,

  owner /var/Applications/@BUNDLE_ID@/users/** rwk,

  dbus send
       bus=session
       path=/org/freedesktop/DBus
       interface=org.freedesktop.DBus
       member="RequestName"
       peer=(name=org.freedesktop.DBus),

  dbus bind
       bus=session
       name="@BUNDLE_ID@",

  dbus bind
       bus=session
       name="@BUNDLE_ID@.Agent",

  dbus (send, receive)
       bus=session
       peer=(label=/Applications/@BUNDLE_ID@/**),

  signal receive peer=/usr/bin/canterbury,
}