Earlier this year I wrote a number of posts about monitoring and interacting with D-Bus using shell scripts. In this post I use Ruby to monitor and interact with D-Bus enabled applications. If you are unfamiliar with D-Bus, a good starting point is this Freedesktop.org tutorial by the authors of the D-Bus specification.
I used the standard out-of-the-box version of Ruby which comes with Fedora 11.
$ ruby --version ruby 1.8.6 (2009-06-08 patchlevel 369) [x86_64-linux]
Those readers who are familiar with Ruby will recognize that this version of Ruby is quite old by Ruby standards and is in maintenance mode with Kirk Haines of Engine Yard being the lead maintainer. Hopefully a 1.9 version of Ruby will be included in the official repositories for Fedora 12.
I also installed the latest version (0.2.9 “I’m not dead”) of ruby-dbus which comes as a compressed tarball rather than as a gem. This project was recently taken over by Martin Vidner, who works for Novell in their Prague development group, after a long period of no activity by the original developers. An introduction to and a tutorial on ruby-dbus is available here. It needs to be updated and expanded to include all the interfaces and methods but it is a good introduction to the general concepts.
Introspection is core to D-Bus scripting. Here is one way to introspect the D-Bus system bus and output the resulting introspection data in XML format. The introspection data format is specified by the D-Bus Specification. The introspect_data method does all the heavy lifting.
#!/usr/bin/ruby require 'dbus' bus = DBus::SystemBus.instance xml = bus.introspect_data("org.freedesktop.DBus", "/org/freedesktop/DBus/Introspectable") puts xml
Here is the output from my computer which is running Fedora 11:
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> <node> <interface name="org.freedesktop.DBus.Introspectable"> <method name="Introspect"> <arg name="data" direction="out" type="s"/> </method> </interface> <interface name="org.freedesktop.DBus"> <method name="Hello"> <arg direction="out" type="s"/> </method> <method name="RequestName"> <arg direction="in" type="s"/> <arg direction="in" type="u"/> <arg direction="out" type="u"/> </method> <method name="ReleaseName"> <arg direction="in" type="s"/> <arg direction="out" type="u"/> </method> <method name="StartServiceByName"> <arg direction="in" type="s"/> <arg direction="in" type="u"/> <arg direction="out" type="u"/> </method> <method name="UpdateActivationEnvironment"> <arg direction="in" type="a{ss}"/> </method> <method name="NameHasOwner"> <arg direction="in" type="s"/> <arg direction="out" type="b"/> </method> <method name="ListNames"> <arg direction="out" type="as"/> </method> <method name="ListActivatableNames"> <arg direction="out" type="as"/> </method> <method name="AddMatch"> <arg direction="in" type="s"/> </method> <method name="RemoveMatch"> <arg direction="in" type="s"/> </method> <method name="GetNameOwner"> <arg direction="in" type="s"/> <arg direction="out" type="s"/> </method> <method name="ListQueuedOwners"> <arg direction="in" type="s"/> <arg direction="out" type="as"/> </method> <method name="GetConnectionUnixUser"> <arg direction="in" type="s"/> <arg direction="out" type="u"/> </method> <method name="GetConnectionUnixProcessID"> <arg direction="in" type="s"/> <arg direction="out" type="u"/> </method> <method name="GetAdtAuditSessionData"> <arg direction="in" type="s"/> <arg direction="out" type="ay"/> </method> <method name="GetConnectionSELinuxSecurityContext"> <arg direction="in" type="s"/> <arg direction="out" type="ay"/> </method> <method name="ReloadConfig"> </method> <method name="GetId"> <arg direction="out" type="s"/> </method> <signal name="NameOwnerChanged"> <arg type="s"/> <arg type="s"/> <arg type="s"/> </signal> <signal name="NameLost"> <arg type="s"/> </signal> <signal name="NameAcquired"> <arg type="s"/> </signal> </interface> </node>
The following example uses the introspect method instead of the introspect_data method to enumerate a list of the available services using the ListActivatableNames method. I am going to assume that you are familiar with the Ruby language.so concepts such as looping and arrays do not need explanation.
require 'dbus' bus = DBus::SystemBus.instance proxy = bus.introspect("org.freedesktop.DBus", "/org/freedesktop/DBus/ListActivatableNames") bus.proxy.ListActivatableNames[0].each do |service| puts "#{service}" end
This is the output:
org.freedesktop.DBus org.freedesktop.DeviceKit.Disks org.fedoraproject.Setroubleshootd com.hp.hplip org.fedoraproject.Config.Services org.freedesktop.ConsoleKit org.gnome.CPUFreqSelector net.reactivated.Fprint org.freedesktop.PackageKit org.freedesktop.DeviceKit org.freedesktop.NetworkManagerSystemSettings org.gnome.ClockApplet.Mechanism org.kerneloops.submit org.freedesktop.PolicyKit org.freedesktop.Gypsy org.gnome.GConf.Defaults fi.epitest.hostap.WPASupplicant org.gnome.SystemMonitor.Mechanism org.freedesktop.DeviceKit.Power org.freedesktop.nm_dispatcher org.opensuse.CupsPkHelper.Mechanism
You can query devices via the D-Bus interface to HAL as the following examples demonstrate.
#!/usr/bin/ruby require 'dbus' bus = DBus::SystemBus.instance xml = bus.introspect_data("org.freedesktop.Hal", "/org/freedesktop/Hal/devices/computer") puts xml
Here is what is outputted when this script is executed:
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> <node> <interface name="org.freedesktop.Hal.Device"> <method name="GetAllProperties"> <arg name="properties" direction="out" type="a{sv}"/> </method> <method name="SetMultipleProperties"> <arg name="properties" direction="in" type="a{sv}"/> </method> <method name="GetProperty"> <arg name="key" direction="in" type="s"/> <arg name="value" direction="out" type="v"/> </method> ...... <signal name="InterfaceLockAcquired"> <arg name="interface_name" type="s"/> <arg name="lock_holder" type="s"/> <arg name="num_locks" type="i"/> </signal> <signal name="InterfaceLockReleased"> <arg name="interface_name" type="s"/> <arg name="lock_holder" type="s"/> <arg name="num_locks" type="i"/> </signal> </interface> ....... <interface name="org.freedesktop.Hal.Device.CPUFreq"> <method name="SetCPUFreqGovernor"> <arg name="governor_string" direction="in" type="s"/> </method> <method name="SetCPUFreqPerformance"> <arg name="value" direction="in" type="i"/> </method> <method name="SetCPUFreqConsiderNice"> <arg name="value" direction="in" type="b"/> </method> <method name="GetCPUFreqGovernor"> <arg name="return_code" direction="out" type="s"/> </method> <method name="GetCPUFreqPerformance"> <arg name="return_code" direction="out" type="i"/> </method> <method name="GetCPUFreqConsiderNice"> <arg name="return_code" direction="out" type="b"/> </method> <method name="GetCPUFreqAvailableGovernors"> <arg name="return_code" direction="out" type="as"/> </method> </interface> </node>
Here is an example of how to output the previous data in a more readable form using the interfaces method..
#!/usr/bin/ruby require 'dbus' bus = DBus::SystemBus.instance proxy = bus.introspect("org.freedesktop.Hal", "/org/freedesktop/Hal/devices/computer") proxy.interfaces.each do |interface| puts "Interface: #{interface}" proxy[interface].methods.each do |key,value| puts " Method: #{key}" end proxy[interface].signals.each do |key,value| puts " Signal: #{key}" end
Here is an portion of the output: from executing this script.
Interface: org.freedesktop.Hal.Device.CPUFreq Method: GetCPUFreqConsiderNice Method: GetCPUFreqGovernor Method: SetCPUFreqPerformance Method: GetCPUFreqAvailableGovernors Method: SetCPUFreqGovernor Method: GetCPUFreqPerformance Method: SetCPUFreqConsiderNice Interface: org.freedesktop.Hal.Device.SystemPowerManagement Method: Shutdown Method: SuspendHybrid Method: SetPowerSave Method: Hibernate Method: Reboot Method: Suspend Interface: org.freedesktop.Hal.Device Method: ReleaseInterfaceLock Method: ClaimInterface Method: Reprobe ........ Method: SetPropertyBoolean Signal: InterfaceLockAcquired Signal: InterfaceLockReleased Signal: PropertyModified Signal: Condition Interface: org.freedesktop.DBus.Introspectable Method: Introspect
You can easily drill down to individual interfaces and methods. For example, here is one way to list the set of available CPU frequency governors.
#!/usr/bin/ruby # # <interface name="org.freedesktop.Hal.Device.CPUFreq"> # <method name="SetCPUFreqGovernor"> # <arg name="governor_string" direction="in" type="s"/> # </method> # <method name="SetCPUFreqPerformance"> # <arg name="value" direction="in" type="i"/> # </method> # <method name="SetCPUFreqConsiderNice"> # <arg name="value" direction="in" type="b"/> # </method> # <method name="GetCPUFreqGovernor"> # <arg name="return_code" direction="out" type="s"/> # </method> # <method name="GetCPUFreqPerformance"> # <arg name="return_code" direction="out" type="i"/> # </method> # <method name="GetCPUFreqConsiderNice"> # <arg name="return_code" direction="out" type="b"/> # </method> # <method name="GetCPUFreqAvailableGovernors"> # <arg name="return_code" direction="out" type="as"/> # </method> # </interface> # require 'dbus' bus = DBus::SystemBus.instance proxy = bus.introspect("org.freedesktop.Hal", "/org/freedesktop/Hal/devices/computer") interface = proxy["org.freedesktop.Hal.Device.CPUFreq"] governors = interface.GetCPUFreqAvailableGovernors puts "Available CPU Freq Governors:" governors[0].each do |gov| puts " #{gov}" end
which produces the following output:
Available CPU Freq Governors: ondemand userspace performance
Lots of useful information can be be obtained from the HAL manager interface. The following script lists all the methods supported by the HAL manager interface and then uses the GetAllDevices method to enumerate the devices on my computer.
require 'dbus' bus = DBus::SystemBus.instance proxy = bus.introspect("org.freedesktop.Hal", "/org/freedesktop/Hal/Manager") puts "HAL manager reports following methods:" proxy.interfaces[0].each do |interface| proxy[interface].methods.each do |key, value| puts " #{key}" end end interface = proxy["org.freedesktop.Hal.Manager"] devices = interface.GetAllDevices puts "\nHAL manager reports following devices:" devices[0].each do |device| puts " #{device}" end
Here is a portion of the output when this script is executed
HAL manager reports following methods: AcquireGlobalInterfaceLock DeviceExists CommitToGdl GetAllDevicesWithProperties FindDeviceByCapability NewDevice SingletonAddonIsReady FindDeviceStringMatch GetAllDevices ReleaseGlobalInterfaceLock Remove HAL manager reports following devices: /org/freedesktop/Hal/devices/net_ba_cf_03_4e_14_ca /org/freedesktop/Hal/devices/volume_part7_size_115326976 /org/freedesktop/Hal/devices/computer /org/freedesktop/Hal/devices/storage_model_DVD_Writer_1070d ............... /org/freedesktop/Hal/devices/acpi_CPU0 /org/freedesktop/Hal/devices/acpi_CPU1 /org/freedesktop/Hal/devices/acpi_CPU2 /org/freedesktop/Hal/devices/acpi_CPU3 /org/freedesktop/Hal/devices/pci_8086_2940 /org/freedesktop/Hal/devices/pci_8086_293e /org/freedesktop/Hal/devices/usb_device_1d6b_2_0000_00_1a_7_if0 /org/freedesktop/Hal/devices/usb_device_1d6b_2_0000_00_1a_7 /org/freedesktop/Hal/devices/pci_8086_293c /org/freedesktop/Hal/devices/usb_device_1d6b_1_0000_00_1a_2_if0 /org/freedesktop/Hal/devices/usb_device_1d6b_1_0000_00_1a_2 /org/freedesktop/Hal/devices/pci_8086_2939 /org/freedesktop/Hal/devices/usb_device_1d6b_1_0000_00_1a_1_if0 /org/freedesktop/Hal/devices/usb_device_1d6b_1_0000_00_1a_1 /org/freedesktop/Hal/devices/pci_8086_2938 /org/freedesktop/Hal/devices/usb_device_1d6b_1_0000_00_1a_0_if0 /org/freedesktop/Hal/devices/usb_device_1d6b_1_0000_00_1a_0 /org/freedesktop/Hal/devices/pci_8086_2937 /org/freedesktop/Hal/devices/pci_8086_294c /org/freedesktop/Hal/devices/pci_10de_640 /org/freedesktop/Hal/devices/pci_8086_29e1 /org/freedesktop/Hal/devices/pci_8086_29e0
The next example uses the D-Bus interface to Tomboy, a desktop note-taking application, to create a simple note, display it for 5 seconds and then delete the note.
#!/usr/bin/ruby require 'dbus' bus = DBus::SessionBus.instance service = bus.service("org.gnome.Tomboy") tomboy = service.object("/org/gnome/Tomboy/RemoteControl") tomboy.introspect tomboy.default_iface = "org.gnome.Tomboy.RemoteControl" note = tomboy.CreateNamedNote("My Note")[0] tomboy.SetNoteContents(note, "Hello World") tomboy.DisplayNote(note) sleep 5 tomboy.DeleteNote(note)
Note the need to introspect the tomboy object and set the default interface using the default_iface method.
You can also add one or more tags to a Tomboy note and extract comprehensive metadata in XML format as shown in the following example.
#!/usr/bin/ruby require 'dbus' bus = DBus::SessionBus.instance service = bus.service("org.gnome.Tomboy") tomboy = service.object("/org/gnome/Tomboy/RemoteControl") tomboy.introspect tomboy.default_iface = "org.gnome.Tomboy.RemoteControl" note = tomboy.CreateNamedNote("My Note")[0] tomboy.SetNoteContents(note, "Hello World") tomboy.AddTagToNote(note, "blog example") tomboy.DisplayNote(note) sleep 5 xml = tomboy.GetNoteCompleteXml(note) tomboy.DeleteNote(note) puts xml
Here is the XML that is outputted when this script is executed:
<?xml version="1.0" encoding="utf-16"?> <note version="0.3" xmlns:link="http://beatniksoftware.com/tomboy/link" xmlns:size="http://beatniksoftware.com/tomboy/size" xmlns="http://beatniksoftware.com/tomboy"> <title>My Note</title> <text xml:space="preserve"><note-content version="0.1">Hello World</note-content></text> <last-change-date>2009-09-07T15:44:10.2178650-04:00</last-change-date> <last-metadata-change-date>2009-09-07T15:44:10.2212100-04:00</last-metadata-change-date> <create-date>2009-09-07T15:44:10.2142940-04:00</create-date> <cursor-position>37</cursor-position> <width>450</width> <height>360</height> <x>1440</x> <y>0</y> <tags> <tag>blog example</tag> </tags> <open-on-startup>False</open-on-startup> </note>
The next example show you how to monitor D-Bus messages and act upon certain messages. You can do this using D-Bus signals. Suppose, for example, you want to monitor activity relating to Tomboy notes such as when Tomboy notes are created. First of all you need to find out what D-Bus signals Tomboy supports. This is easy to do using introspection as shown in the following script.
#!/usr/bin/ruby require 'dbus' bus = DBus::SessionBus.instance service = bus.service("org.gnome.Tomboy") tomboy = service.object("/org/gnome/Tomboy/RemoteControl") tomboy.introspect tomboy.default_iface = "org.gnome.Tomboy.RemoteControl" puts "List of supported signals:" tomboy.signals.each do |key,value| puts " #{key}" end
Here is the output:
List of supported signals: NoteSaved NoteAdded NoteDeleted
As you can see the version of Tomboy on my computer emits three D-Bus signals – NoteAdded when a note is added, NoteSaved when a note is saved, and NoteDeleted when a note is deleted.
The following script outputs a message each time a Tomboy note is created, saved or deleted.
#!/usr/bin/ruby require 'dbus' bus = DBus::SessionBus.instance match = DBus::MatchRule.new match.type = "signal" match.interface = "org.gnome.Tomboy.RemoteControl" match.path = "/org/gnome/Tomboy/RemoteControl" bus.add_match(match) do |msg, misc| puts "#{msg.member} #{msg.params[0]}" end main = DBus::Main.new main << bus main.run
Here is what is outputted when a single Tomboy note is created, saved and then deleted.
NoteAdded note://tomboy/56587548-7535-4e21-a4b3-3ee85ffa7756 NoteSaved note://tomboy/56587548-7535-4e21-a4b3-3ee85ffa7756 NoteDeleted note://tomboy/56587548-7535-4e21-a4b3-3ee85ffa7756
Well that's all for now. There is a lot more that you can do using ruby-dbus but I will leave it up to you to go explore the possibilities. The best place to discover what is possible is to study the ruby-dbus source code. It is fairly compact and terse but logically laid out.