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, error_handler=self._cups_error)
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.
#!/usr/bin/python import gobject import dbus.service import time BUS='com.example.Timer' PATH='/com/example/Timer' IFACE='com.example.Timer' 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) @dbus.service.method(dbus_interface=IFACE, in_signature='i', out_signature='i') 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 () KeyboardInterrupt
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.
@dbus.service.method(dbus_interface=IFACE, in_signature='i', out_signature='i', async_callbacks=('reply_handler', 'error_handler')) 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 () KeyboardInterrupt