D-Bus and Python: asynchronous method implementation

This is a quick demonstration of how to implement a D-Bus method in Python using asynchronous callbacks.

I recently added support in system-config-printer for determining the best driver to use for a particular printer.  This is an expensive operation, largely because of the time it takes to get a list of available drivers from CUPS, and the Python program providing the D-Bus service also provides other services.  I wanted the program to be able to deal with other callers while the CUPS operation was in progress.  Here’s how that was done.

Firstly, I already had a class for asynchronous communication with CUPS.  When using PolicyKit to talk to CUPS, the calls use D-Bus which provides an asynchronous API for method calls anyway.  For calls not provided that way the fallback is to queue requests for a worker thread to deal with.

Asynchronous D-Bus calls are made in Python by supplying “reply_handler” and “error_handler” named options in the method call, like this:

self._cupsconn.getPPDs2 (reply_handler=self._cups_getppds_reply,

That call returns right away, but the getPPDs2 operation continues in the background.  When it is done, my reply_handler function is called.  Alternatively, if there is an error, the error_handler function is called.  One or the other will be called, at which point the operation is finished with.

So far so good from the client side, but the question is how to implement a D-Bus service in an asynchronous manner.  Here is a reminder of what a synchronous D-Bus service implementation looks like.

import gobject
import dbus.service
import time

START_TIME=time.time ()

class Timer(dbus.service.Object):
    def __init__ (self):
        self.bus = dbus.SessionBus ()
        bus_name = dbus.service.BusName (BUS, bus=self.bus)
        dbus.service.Object.__init__ (self, bus_name, PATH)

    def Delay (self, seconds):
        print "Sleeping for %ds" % seconds
        time.sleep (seconds)
        return seconds

def heartbeat():
    print "Still alive at", time.time () - START_TIME
    return True

from dbus.glib import DBusGMainLoop
DBusGMainLoop (set_as_default=True)
loop = gobject.MainLoop ()
# Start the heartbeat
handle = gobject.timeout_add_seconds (1, heartbeat)
# Start the D-Bus service
timer = Timer ()
loop.run ()

The Timer class is the D-Bus service.  It has a single method, Delay, which delays for a number of seconds and returns that same number.  The program sets a repeating 1-second timer to print “Still alive”, and the D-Bus calls into the Timer service are handled by the D-Bus main loop.

How does this program behave?  Here is its output.  While it was running I used D-Feet to call Delay(3).

Still alive at 1.02512407303
Still alive at 2.0262401104
Still alive at 3.02633500099
Still alive at 4.02575397491
Sleeping for 3s
Still alive at 7.0756611824
Still alive at 8.02573609352
Still alive at 9.02583909035
Still alive at 10.0259339809
^CTraceback (most recent call last):
  File "/tmp/demo.py", line 36, in <module>
    loop.run ()

As you can see, while it was handling the call to the Delay method it could not do anything else.  Here is a new version of the Delay function, this time implemented using asynchronous callbacks.

    def Delay (self, seconds, reply_handler, error_handler):
        print "Sleeping for %ds" % seconds
        gobject.timeout_add_seconds (seconds,
                                     lambda: reply_handler (seconds))

You’ll notice that the D-Bus function decorator now contains an async_callbacks keyword.  This keyword declares the method keywords that the function uses for reply and error handlers.  Here, I’ve stuck with the usual “reply_handler” and “error_handler” names, and added those same names to the definition of the Delay function on the next line.

This time, when the D-Bus main loop calls the Delay method it will also provide the reply and error callbacks.  When the Delay method returns, its return value is ignored.  The D-Bus call is only ended by calling one of the callbacks.  In this very simple implementation, I’ve arranged for that to happen by setting a timeout.

How does the program behave now?

Still alive at 1.07246303558
Still alive at 2.07272696495
Still alive at 3.07237696648
Still alive at 4.07276511192
Sleeping for 3s
Still alive at 5.07263112068
Still alive at 6.07277011871
Still alive at 7.07274699211
Still alive at 8.07311701775
Still alive at 9.07332015038
Still alive at 10.0734181404
Still alive at 11.0735199451
^CTraceback (most recent call last):
  File "/tmp/demo-async.py", line 38, in <module>
    loop.run ()

Much better.