Translate

Image of Linux Kernel Development (3rd Edition)
Image of Android Wireless Application Development
Image of RHCE Red Hat Certified Engineer Linux Study Guide (Exam RH302) (Certification Press)
Image of Advanced Programming in the UNIX Environment, Second Edition (Addison-Wesley Professional Computing Series)

D-Bus, Cinnamon and the GNOME Shell

Many people are unaware of the fact that both the GNOME Shell and Cinnamon provide a D-Bus ((Desktop Bus) interface. D-Bus is a form of inter-process communications (IPC) which provides a mechanism for applications running on a single platform to “talk” to each other. In this post, I will show you how to enumerate the available methods and properties and how to enable and disable extensions via the session D-Bus using a command line tool.

D-Bus is a system for low-latency, low-overhead IPC using messages rather than byte streams. The core protocol is a 2-way, asynchronous, binary protocol. A message consists of a header and a body. The header is used figure out where to send the message and how to interpret it. The message body is interpreted by the recipient. Both the message header and message body use the same type system and format for serializing the data. Each type of value has a on-the-wire format. Converting a value from some other representation into the on-the-wire format is called marshaling and converting it back from the wire format is unmarshaling. If you have done any RPC programming, you will be familiar with the concept of marshaling and unmarshaling parameters.

There are actually multiple D-Bus buses running on your average Linux platform at any one time. These can be divided into two types:

  • A single system bus (privileged channel) is used for system-wide communications on a platform.
  • A session bus (private channel) is created for each user session. It allows applications within that user session to communicate.

The GNONE Shell and Cinnamon, as do most D-Bus message sources and sinks, communicate with other applications via a user’s session bus.

Currently, both shells expose the same D-Bus interfaces but, obviously, this may not be the case in the future. In the GNOME Shell, see ../js/ui/shellDBus.js. For Cinnamon, see ../js/ui/cinnamonDBus.js. Here is the relevant code from ../js/ui/cinnamonDBus.js:

const CinnamonIface = {
    name: 'org.Cinnamon',
    methods: [{ name: 'Eval',
                inSignature: 's',
                outSignature: 'bs'
              },
              { name: 'ListExtensions',
                inSignature: '',
                outSignature: 'a{sa{sv}}'
              },
              { name: 'GetExtensionInfo',
                inSignature: 's',
                outSignature: 'a{sv}'
              },
              { name: 'GetExtensionErrors',
                inSignature: 's',
                outSignature: 'as'
              },
              { name: 'ScreenshotArea',
                inSignature: 'iiiis',
                outSignature: 'b'
              },
              { name: 'ScreenshotWindow',
                inSignature: 'bs',
                outSignature: 'b'
              },
              { name: 'Screenshot',
                inSignature: 's',
                outSignature: 'b'
              },
              { name: 'EnableExtension',
                inSignature: 's',
                outSignature: ''
              },
              { name: 'DisableExtension',
                inSignature: 's',
                outSignature: ''
              },
              { name: 'InstallRemoteExtension',
                inSignature: 'ss',
                outSignature: ''
              },
              { name: 'UninstallExtension',
                inSignature: 's',
                outSignature: 'b'
              }
             ],
    signals: [{ name: 'ExtensionStatusChanged',
                inSignature: 'sis' }],
    properties: [{ name: 'OverviewActive',
                   signature: 'b',
                   access: 'readwrite' },
                 { name: 'ApiVersion',
                   signature: 'i',
                   access: 'read' },
                 { name: 'CinnamonVersion',
                   signature: 's',
                   access: 'read' }]
};

So how can you retrieve details of an object’s methods, properties and signals without examining the source code? For this purpose, D-Bus supports the concept of object introspection. There are a number of utilities which can be used retrieve such information. Probably the most common is dbus-send. In the following example, I use dbus-send to introspect Cinnamon and list out the available methods, signals and properties.

$ dbus-send --session --type=method_call --print-reply \
  --dest=org.Cinnamon /org/Cinnamon \
  org.freedesktop.DBus.Introspectable.Introspect

method return sender=:1.34 -> dest=:1.123 reply_serial=2
string "

<node>
  <interface name="org.Cinnamon"
    <method name="Eval">
      <arg type="s" direction="in"/>
      <arg type="b" direction="out"/>
      <arg type="s" direction="out"/>
    </method>
    <method name="ListExtensions">
      <arg type="a{sa{sv}}" direction="out"/>
    </method>
    <method name="GetExtensionInfo">
      <arg type="s" direction="in"/>
      <arg type="a{sv}" direction="out"/>
    </method>
    <method name="GetExtensionErrors">
      <arg type="s" direction="in"/>
      <arg type="as" direction="out"/>
    </method>
    <method name="ScreenshotArea">
      <arg type="i" direction="in"/>
      <arg type="i" direction="in"/>
      <arg type="i" direction="in"/>
      <arg type="i" direction="in"/>
      <arg type="s" direction="in"/>
      <arg type="b" direction="out"/>
    </method>
    <method name="ScreenshotWindow">
      <arg type="b" direction="in"/>
      <arg type="s" direction="in"/>
      <arg type="b" direction="out"/>
    </method>
    <method name="Screenshot">
      <arg type="s" direction="in"/>
      <arg type="b" direction="out"/>
    </method>
    <method name="EnableExtension">
      <arg type="s" direction="in"/>
    </method>
    <method name="DisableExtension">
      <arg type="s" direction="in"/>
    </method>
    <method name="InstallRemoteExtension">
      <arg type="s" direction="in"/>
      <arg type="s" direction="in"/>
    </method>
    <method name="UninstallExtension">
      <arg type="s" direction="in"/>
      <arg type="b" direction="out"/>
    </method>
    <signal name="ExtensionStatusChanged">
      <arg type="s"/>
      <arg type="i"/>
      <arg type="s"/>
    </signal>
    <property name="OverviewActive" type="b" access="readwrite"/>
    <property name="ApiVersion" type="i" access="read"/>
    <property name="CinnamonVersion" type="s" access="read"/>
  </interface>
  <interface name="org.freedesktop.DBus.Introspectable">
    <method name="Introspect">
      <arg type="s" direction="out"/>
    </method>
  </interface>
  <interface name="org.freedesktop.DBus.Properties">
    <method name="Get">
      <arg type="s" direction="in"/>
      <arg type="s" direction="in"/>
      <arg type="v" direction="out"/>
    </method>
    <method name="Set">
      <arg type="s" direction="in"/>
      <arg type="s" direction="in"/>
      <arg type="v" direction="in"/>
    </method>
    <method name="GetAll">
      <arg type="s" direction="in"/>
      <arg type="a{sv}" direction="out"/>
    </method>
  </interface>
</node>
"


To enable D-Bus messages to specify their destination object, the system needs a way to identify and address an object. Each object must support at least one interface. Think of an interface as a named group of methods and signals, just as it is in Java. D-Bus defines a name for each object interface, For Cinnamon the name is org.Cinnamon and for the GNOME Shell the name is org.gnome.Shell. An object path is a name used to refer to a particular object instance. For Cinnamon the object path is /org/Cinnamon and for the GNOME Shell the name is /org/gnome/Shell.

Objects may be introspected at runtime, returning an XML string that describes the object, using the org.freedesktop.DBus.Introspectable interface which has one method Introspect. This method is used to return the XML description of the specified object, including its methods, properties and signals, and objects below it in the object path tree.

If you just want the object properties (AKA attributes), you can retrieve them using org.freedesktop.DBus.Properties.

$ dbus-send --session --type=method_call --print-reply \
  --dest=org.Cinnamon /org/Cinnamon \
  org.freedesktop.DBus.Properties.GetAll string:org.Cinnamon

method return sender=:1.166 -> dest=:1.173 reply_serial=2
   array [
      dict entry(
         string "OverviewActive"
         variant             boolean false
      )
      dict entry(
         string "ApiVersion"
         variant             int32 1
      )
      dict entry(
         string "CinnamonVersion"
         variant             string "1.2.0"
      )
   ]

Another useful tool for inspecting D-Bus objects is gdbus. In this example, I use gdbus to introspect Cinnamon using the session bus.

$ gdbus introspect --session --dest org.Cinnamon --object-path /org/Cinnamon

node /org/Cinnamon {
  interface org.Cinnamon {
    methods:
      Eval(in  s arg_0,
           out b arg_1,
           out s arg_2);
      ListExtensions(out a{sa{sv}} arg_0);
      GetExtensionInfo(in  s arg_0,
                       out a{sv} arg_1);
      GetExtensionErrors(in  s arg_0,
                         out as arg_1);
      ScreenshotArea(in  i arg_0,
                     in  i arg_1,
                     in  i arg_2,
                     in  i arg_3,
                     in  s arg_4,
                     out b arg_5);
      ScreenshotWindow(in  b arg_0,
                       in  s arg_1,
                       out b arg_2);
      Screenshot(in  s arg_0,
                 out b arg_1);
      EnableExtension(in  s arg_0);
      DisableExtension(in  s arg_0);
      DisableAllExtensions();
      InstallRemoteExtension(in  s arg_0,
                             in  s arg_1);
      UninstallExtension(in  s arg_0,
                         out b arg_1);
    signals:
      ExtensionStatusChanged(s arg_0,
                             i arg_1,
                             s arg_2);
    properties:
      readwrite b OverviewActive = false;
      readonly i ApiVersion = 1;
      readonly s CinnamonVersion = '1.2.0';
  };
  interface org.freedesktop.DBus.Introspectable {
    methods:
      Introspect(out s arg_0);
    signals:
    properties:
  };
  interface org.freedesktop.DBus.Properties {
    methods:
      Get(in  s arg_0,
          in  s arg_1,
          out v arg_2);
      Set(in  s arg_0,
          in  s arg_1,
          in  v arg_2);
      GetAll(in  s arg_0,
             out a{sv} arg_1);
    signals:
    properties:
  };
};


What do the various argument signatures such as v,s and in mean?

By design, the D-Bus protocol does not include type tags in the marshaled data. Instead a block of marshaled values has a type signature. The type signature is made up of one or more type codes. A type code is an ASCII character representing the type of a value. Because ASCII characters are used, the type signature will always form a valid ASCII string.

The following table summarizes the D-Bus value types:

Conventional NameCodeDescription
INVALID0 (ASCII NUL)Not a valid type code, used to terminate signatures
BYTE121 (ASCII 'y')8-bit unsigned integer
BOOLEAN98 (ASCII 'b')Boolean value, 0 is FALSE and 1 is TRUE. Everything else is invalid.
INT16110 (ASCII 'n')16-bit signed integer
UINT16113 (ASCII 'q')16-bit unsigned integer
INT32105 (ASCII 'i')32-bit signed integer
UINT32117 (ASCII 'u')32-bit unsigned integer
INT64120 (ASCII 'x')64-bit signed integer
UINT64116 (ASCII 't')64-bit unsigned integer
DOUBLE100 (ASCII 'd')IEEE 754 double
STRING115 (ASCII 's')UTF-8 string must be valid null-terminated UTF-8
OBJECT_PATH111 (ASCII 'o')Name of an object instance
SIGNATURE103 (ASCII 'g')A type signature
ARRAY97 (ASCII 'a')Array
STRUCT114 (ASCII 'r'), 40 (ASCII '('), 41 (ASCII ')')Struct
VARIANT118 (ASCII 'v')Variant type (the type of the value is part of the value itself)
DICT_ENTRY101 (ASCII 'e'), 123 (ASCII '{'), 125 (ASCII '}')Entry in a dict or map (array of key-value pairs)
UNIX_FD104 (ASCII 'h')Unix file descriptor

There are also a number of message types. Each of the message types (METHOD_CALL, METHOD_RETURN, ERROR and SIGNAL) has its own expected usage conventions and header fields. Messages that invoke an operation on a remote object are called method call messages and have a type tag of METHOD_CALL. When an application handles a METHOD_CALL message, it is required to return a reply. The reply is identified by a REPLY_SERIAL header field indicating the serial number of the METHOD_CALL being replied to.

The reply can only be one of two types; either METHOD_RETURN or ERROR. If the reply has type METHOD_RETURN, the arguments to the reply message are the return value(s) of the method call. If the reply has type ERROR, then an exception has been thrown, the call had failed and no return value is provided. Even if a method call has no return values, a METHOD_RETURN reply is required, so the caller will know the method was successfully processed.

APIs for D-Bus may map method calls to a method call in a specific programming language, such as Python orC++, or may map a method call written in an IDL to a D-Bus message. In APIs of this nature, arguments to a method are often termed "in", i.e. sent in the METHOD_CALL), or "out", i.e. returned in METHOD_RETURN. See the D-Bus specification for more information.

D-Bus also supports a form of signals. Unlike method calls, signal emissions have no replies. A signal emission is simply a single message of type SIGNAL. It must have three header fields, i.e. PATH giving the object the signal was emitted from, plus INTERFACE and MEMBER giving the fully-qualified name of the signal.

Along with methods and signals, D-Bus also supports the concept of object properties or attributes. These can be exposed via the org.freedesktop.DBus.Properties interface. The available properties and whether they are writable can be determined by using the org.freedesktop.DBus.Introspectable interface.

The following is a simple Python utility which I wrote to demonstrate how to use the Cinnamon D-Bus interface to control Cinnamon extensions. I am not going to explain the script in detail as I assume you have a modicum of knowledge w.r.t the Python language. The script uses gobject introspection (GI) to interface with GLib and GIO.

#!/usr/bin/python
#
#   Copyright (c) Finnbarr P. Murphy 2012.  All rights reserved.
#
#      Name: cinnamon-tool
#
#   Version: 1.0 (02/16/2012)
#
#   License: Attribution Assurance License (see www.opensource.org/licenses)
#

try:
    import os
    import sys
    import argparse
except ImportError, detail:
    print detail
    sys.exit(1)

from gi.repository import Gio, GLib

CINNAMON_PATH   = '/org/Cinnamon'
CINNAMON_IFACE  = 'org.Cinnamon'

class Cinnamon:
    def __init__(self):
        try:
            self.bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
            self.proxy  = Gio.DBusProxy.new_sync( self.bus, Gio.DBusProxyFlags.NONE, None,
                              CINNAMON_IFACE, CINNAMON_PATH, CINNAMON_IFACE, None)
            self.proxyp = Gio.DBusProxy.new_sync( self.bus, Gio.DBusProxyFlags.NONE, None,
                              CINNAMON_IFACE, CINNAMON_PATH, 'org.freedesktop.DBus.Properties', None)
        except:
            print "Exception: %s" % sys.exec_info()[1]

    def list_extensions(self):
        output = self.proxy.ListExtensions()
        return output

    def enable_extension(self, uuid):
        self.proxy.EnableExtension('(s)', uuid)
        return

    def disable_extension(self, uuid):
        self.proxy.DisableExtension('(s)', uuid)
        return

    def disable_all_extensions(self):
        self.proxy.DisableAllExtensions()
        return

    def get_cinnamon_version(self):
        output = self.proxyp.Get('(ss)', CINNAMON_IFACE, 'CinnamonVersion')
        return output

    def get_api_version(self):
        output = self.proxyp.Get('(ss)', CINNAMON_IFACE, 'ApiVersion')
        return output


def enable_extension(uuid):
    s = Cinnamon()
    s.enable_extension(uuid)

def disable_extension(uuid):
    s = Cinnamon()
    s.disable_extension(uuid)

def disable_all_extensions():
    s = Cinnamon()
    s.disable_all_extensions()

def get_versions():
    s = Cinnamon()
    c = s.get_cinnamon_version()
    a = s.get_api_version()
    print 'Versions - Cinnamon: %s   API: %s' % (c, a)

def list_extensions():
    state = { 1:"enabled", 2:"disabled", 3:"error", 4:"out of date", 5:"downloading"}
    type  = { 1:"system", 2:"per user"}
    s = Cinnamon()
    l = s.list_extensions()
    for k, v in l.iteritems():
        print '%s: %s, %s' % (v["uuid"], state[v["state"]], type[v["type"]])
    print

def main():
    parser = argparse.ArgumentParser(description="Cinnamon extension tool")
    group = parser.add_mutually_exclusive_group()
    group.add_argument("-c", "--CinnamonVersion", dest="cversion",
                       action="store_true", help="get Cinnamon version")
    group.add_argument("-d", "--disable", nargs=1, action="store", dest="disable",
                       metavar="uuid", help="disable a Cinnamon extension")
    group.add_argument("-D", "--disable-all", dest="disableall",
                       action="store_true", help="disable all Cinnamon extensions")
    group.add_argument("-e", "--enable", nargs=1, action="store", dest="enable",
                       metavar="uuid", help="enable a Cinnamon extension")
    group.add_argument("-l", "--list", dest="list",
                       action="store_true", help="list Cinnamon extensions")
    group.add_argument('-v', '--version', action='version', version='%(prog)s 1.0')

    args = parser.parse_args()

    if args.cversion:
        get_versions()
    elif args.disable:
        disable_extension("".join(args.disable))
    elif args.disableall:
        disable_all_extensions()
    elif args.enable:
        enable_extension("".join(args.enable))
    elif args.list:
        list_extensions()
    else:
        parser.print_usage()
    sys.exit(0)

if __name__ == "__main__":
    main()


By the way, the Python interfaces (PyGObject) to GLib and GIO are poorly documented -like a lot of the GNOME libraries! While the above utility is specific to Cinnamon, it can be easily modified to work with GNOME Shell. Rather than providing details of the required modifications, I will leave it up to you to as a learning exercise.

Note that I modified cinnamonDBus.js to add support for the DisableAllExtensions method as I wanted cinnamon-tool to be able to disable all Cinnamon extensions via one command. Here are the relevant diffs.

$ diff cinnamonDBus.js.org cinnamonDBus.js.new
47a48,51
>               { name: 'DisableAllExtensions',
>                 inSignature: '',
>                 outSignature: ''
>               },
179a184,189
> 
>      DisableAllExtensions: function() {
>         let enabledExtensions = global.settings.get_strv(ExtensionSystem.ENABLED_EXTENSIONS_KEY);
>         enabledExtensions.length = 0; 
>         global.settings.set_strv(ExtensionSystem.ENABLED_EXTENSIONS_KEY, enabledExtensions);
>      },


Note that D-Bus is not just used for managing shell extensions and screen snapshots within GNOME Shell or Cinnamon. In fact, it is used in a growing number of places within these shells including the screen keyboard, the screen magnifier and the calendar server. To date, however, I have not come across much posted information on the use of D-Bus within Cinnamon or the Gnome Shell.

In my next post, I will demonstrate how write an extension that uses D-Bus to enable the behavior of the extension to be modified via a command line tool.

Comments are closed.