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 Name | Code | Description |
---|---|---|
INVALID | 0 (ASCII NUL) | Not a valid type code, used to terminate signatures |
BYTE | 121 (ASCII 'y') | 8-bit unsigned integer |
BOOLEAN | 98 (ASCII 'b') | Boolean value, 0 is FALSE and 1 is TRUE. Everything else is invalid. |
INT16 | 110 (ASCII 'n') | 16-bit signed integer |
UINT16 | 113 (ASCII 'q') | 16-bit unsigned integer |
INT32 | 105 (ASCII 'i') | 32-bit signed integer |
UINT32 | 117 (ASCII 'u') | 32-bit unsigned integer |
INT64 | 120 (ASCII 'x') | 64-bit signed integer |
UINT64 | 116 (ASCII 't') | 64-bit unsigned integer |
DOUBLE | 100 (ASCII 'd') | IEEE 754 double |
STRING | 115 (ASCII 's') | UTF-8 string must be valid null-terminated UTF-8 |
OBJECT_PATH | 111 (ASCII 'o') | Name of an object instance |
SIGNATURE | 103 (ASCII 'g') | A type signature |
ARRAY | 97 (ASCII 'a') | Array |
STRUCT | 114 (ASCII 'r'), 40 (ASCII '('), 41 (ASCII ')') | Struct |
VARIANT | 118 (ASCII 'v') | Variant type (the type of the value is part of the value itself) |
DICT_ENTRY | 101 (ASCII 'e'), 123 (ASCII '{'), 125 (ASCII '}') | Entry in a dict or map (array of key-value pairs) |
UNIX_FD | 104 (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.