http://www.documentroot.net - Tipps, Tricks, Tutorials, Wissenswertes
28. September 2010

Tutorial: Interprocess Communication mit D-Bus und Python

Quelle: http://www.documentroot.net/linux/python-dbus-tutorial

In letzter Zeit habe ich mich viel mit DBus und dessen Python-Library herumschlagen müssen. Da mich das sehr viel Zeit und vor allem Nerven gekostet hat, will ich nun hier meine Erfahrungen in Form eines kleinen Tutorials niederschreiben, für all jene, die auch mit dem Thema zu kämpfen haben. Außerdem gibts im Internet praktisch keine sinnvollen Beispiele zum Thema python-dbus und das muss geändert werden.

~ DBus-Grundlagen ~

Fangen wir mit den Basics an. Zuerst einmal: was ist DBus eigentlich? DBus ist ein Programm, das Kommunikation zwischen getrennten Prozessen erlaubt (Interprocess Communication, abgekürzt IPC). Es gibt einen Server (Daemon), an dem sich Programme anmelden können, um dort gewisse Dienste anzubieten. Andere Programme können sich dann ebenfalls zu diesem Daemon verbinden, um jene Dienste in Anspruch zu nehmen. Im Folgenden will ich etwas genauer werden und die relevanten Begriffe einführen.

Will sich ein Programm am DBus anmelden, muss es dem DBus-Server zuerst mitteilen, an welchem Bus es sich anmelden will. Davon gibt es eigentlich nur zwei erwähnenswerte: den SystemBus, an dem sich Daemons und Systemdienste registrieren und den SessionBus, der für Benutzeranwendungen vorgesehen ist.

DBus Struktur-BeispielHat das Programm Verbindung zu einem Bus hergestellt, muss es sich einen Bus-Namen registrieren. Das sollte eine Zeichenkette sein, die das Programm oder den zur Verfügung gestellten Dienst identifiziert. Zum Beispiel: “org.documentroot.Calculator” oder “org.freedesktop.Notifications”.
Zugegeben, die Bezeichnung “Bus-Name” ist etwas unglücklich gewählt, da man ja nicht über den Namen des Busses redet, sondern über den Namen eines Programmes am Bus. So ist aber leider die Konvention in allen DBus-bezogenen Texten und daran will ich mich halten.

Als nächstes kann das Programm beliebig viele Objekte innerhalb seines Bus-Namens freigeben. So könnte z.B. ein Text-Editor jedes geöffnete Dokument freigeben. Ein solches Objekt wird dabei über einen sogenannten Objekt-Pfad identifiziert. Dieser sieht aus wie ein Unix-Dateipfad und dient der logischen Strukturierung bei vielen freigegebenen Objekten. Ein Beispiel: Eine Tabellenkalkulation könnte innerhalb ihres Bus-Namens folgende Objekte freigeben:
/MainApplication
/Spreadsheet_1
/Spreadsheet_1/row/1
/Spreadsheet_1/row/2
/Spreadsheet_1/cell/1/3
/Spreadsheet_1/cell/2/3
usw…

Diese Objekte können nun Methoden und Signale anbieten. Methoden können von externen Programmen aufgerufen werden, wohingegen Signale andersrum funktionieren: hier können sich fremde Prozesse einhängen, um auf irgend etwas aufmerksam gemacht zu werden. Zum Beispiel wenn ein neues Dokument geöffnet wird.

Mit dem Command-Line-Tool qdbus kann man sich etwas im DBus-System umsehen. Die Bedienung ist intuitiv:

1
2
3
4
5
6
# zeige alle Bus-Namen an (SessionBus):
qdbus
# zeige alle freigebenen Objekte von klauncher an:
qdbus org.kde.klauncher
# zeige alle freigegebenen Methoden/Signale des KLauncher-Objekts an:
qdbus org.kde.klauncher /KLauncher

Methoden können über qdbus auch aufgerufen werden, allerdings ist es manchmal nicht möglich, die erforderlichen Datentypen auf der Konsole einzugeben. Daher sparen wir uns das Aufrufen von Methoden für den Python-Teil auf.

~ Das Python-DBus Modul ~

Python hat ja für alles ein Modul und so wundert es auch nicht, dass es ein DBus-Modul gibt. Dieses Modul hat allerdings eine etwas gewöhnungsbedürftige API und verhält sich nicht immer so wie man das denken würde, daher will ich hier einige Beispiele vorstellen und einige fiese Fallen entschärfen.

Die API ist trotz ihrer Unvollständigkeit ein nützlicher Wegweiser, daher bookmarkt euch diese Adresse schonmal: http://dbus.freedesktop.org/doc/dbus-python/api/

Fangen wir an. Was wir als erstes tun wollen, ist eine Server-Anwendung zu schreiben, die ihre Dienste per DBus bereitstellt:

# -*- coding: utf-8 -*-
# file: server.py
 
from PyQt4.QtCore import *
import math
import dbus
import dbus.service
from dbus.mainloop.qt import DBusQtMainLoop
 
class Calculator(dbus.service.Object):
  def __init__(self):
    busName = dbus.service.BusName('org.documentroot.Calculator', bus = dbus.SessionBus())
    dbus.service.Object.__init__(self, busName, '/Calculator')
 
  @dbus.service.method('org.documentroot.Calculator', in_signature = 'dd', out_signature = 'd')
  def add(self, a, b): return a+b
 
  @dbus.service.method('org.documentroot.Calculator', in_signature = 'i', out_signature = 'i')
  def factorial(self, n): return 1 if n <= 1 else n*self.factorial(n-1)
 
  @dbus.service.method('org.documentroot.Calculator', in_signature = 'd', out_signature = 'd')
  def sqrt(self, n): return math.sqrt(n)
 
  @dbus.service.method('org.documentroot.Calculator', in_signature = 'd', out_signature = 'i')
  def round(self, n): return round(n)
 
DBusQtMainLoop(set_as_default = True)
app = QCoreApplication([])
calc = Calculator()
app.exec_()

Der größte Teil sollte selbsterklärend sein und meistens kann dieses Beispiel so abgeändert werden, dass es für die eigenen Zwecke passt, ein paar Erklärungen sind allerdigs nötig fürs Verständnis:

Erstens muss das Programm eine GLib- oder Qt-Event-Schleife haben (in meinem Beispiel ist es Qt). Zweitens muss beim Verbindungsaufbau zu einem Bus eine DBus-Event-Schleife gestartet werden, die die DBus-Anfragen behandeln kann. Dies geht entweder per dbus.SessionBus(mainloop = …) oder indem man eine Standard-Event-Schleife startet wie ich es getan habe. Da ich für meinen Server Qt verwende, benutze ich auch die DBus-Event-Schleife für Qt. Für GLib gibts ebenfalls eine, ein ähnliches Beispiel dafür gibts hier.

Da Python im Gegensatz zum C++-basierten DBus dynamische Typisierung besitzt, müssen die Signaturen der exportierten Methoden immer angegeben werden. Dafür übergibt man dem Decorator entsprechende Parameter. Diese enthalten in einem String kodiert die Datentypen der Parameter und des Rückgabewertes. Wie solche Strings auszusehen haben, liest man am besten hier nach.

Nun aber genug Theorie, Konsole anwerfen, Zeit zum spielen:

1
2
3
4
qdbus org.documentroot.Calculator /Calculator sqrt 5.4
qdbus org.documentroot.Calculator /Calculator factorial 10
qdbus org.documentroot.Calculator /Calculator add 4.8 12
qdbus org.documentroot.Calculator /Calculator round 6.5

Den schwierigen Teil haben wir bereits hinter uns – machen wir weiter mit der Client-Anwendung. Diese soll einfach per DBus ein paar Berechnungen an den Server delegieren:

1
2
3
4
5
6
7
8
9
10
11
# -*- coding: utf-8 -*-
# file: client.py
 
import dbus
 
bus = dbus.SessionBus()
server = bus.get_object('org.documentroot.Calculator', '/Calculator')
print '5 und 10 sind:', 
print server.add(5, 10, dbus_interface = 'org.documentroot.Calculator')
print '6 Fakultät ist:', 
print server.factorial(6, dbus_interface = 'org.documentroot.Calculator')

Das einzig unintuitive sind hier die Interface-Namen, die mit übergeben werden. Wie beim Server schon gesehen hat jede Methode ein sogenanntes Interface, welches bei Namenskonflikten eine Rolle spielt. Meiner Meinung nach unnötig, aber die API von dbus-python verlangt es nun mal.

(Hier brauchen wir beim Verbinden zum Bus keine DBus-Event-Schleife, die ist nur nötig, wenn der Prozess Objekte exportieren will.)

~ Queueing ~

Auf eine kleine Eigenheit von dbus-python muss an dieser Stelle hingewiesen werden: bei der Vergabe von Bus-Namen wird eine Warteschlange verwaltet. Versucht ein Programm also, einen bereits vergebenen Namen zu registrieren (via dbus.service.BusName), so landet die Anfrage stillschweigend in einer Warteschlange. Man erhält keinen Hinweis, die Anwendung wartet auf Freigabe des Bus-Namens. Dies kann gewollt sein, aber in manchen Fällen ist dieses Verhalten eher hinderlich.

Um das auszuschalten, kann man beim Registrieren des Bus-Namens dem Konstruktor dbus.service.BusName einen Parameter do_not_queue = True übergeben. Andere mögliche Parameter sind replace_existing (versuche vorhandenen Bus-Namen zu ersetzen) und allow_replacement (erlaube anderen Prozessen, diesen Bus-Namen an sich zu reißen).

Braucht man mal genauere Debug-Infos, was bei der Registrierung des Bus-Namens passiert, kann man sich diese folgendermaßen ausgeben lassen:

1
2
3
4
5
6
7
  answer = dbus.SessionBus().request_name('org.documentroot.Calculator')
  if answer == dbus.bus.REQUEST_NAME_REPLY_PRIMARY_OWNER: print 'got bus name'
  if answer == dbus.bus.REQUEST_NAME_REPLY_ALREADY_OWNER: print 'already had bus name'
  if answer == dbus.bus.REQUEST_NAME_REPLY_IN_QUEUE: print 'queued'
  if answer == dbus.bus.REQUEST_NAME_REPLY_EXISTS: print 'could not get bus name'
  busName = dbus.service.BusName('org.documentroot.Calculator', bus = dbus.SessionBus())
  dbus.service.Object.__init__(self, busName, '/Calculator')

~ QDBusObject für Python ~

Für die Qt-Programmierer unter uns: laut Qt-Doku gibt es eine Klasse QDBusObject, aber leider nur für C++. Für Python ist nichts vorgesehen, daher muss dbus-python her halten. Versucht man jedoch per Mehrfachvererbung eine Klasse von dbus.service.Object und QObject abzuleiten, damit man über Signale/Slots mit seinem DBus-Objekt kommunizieren kann, erhält man einen Fehler. Schreibt man eine eigene Metaklasse, gehts doch:

1
2
3
4
5
6
7
8
9
10
class QDBusMetaclass(type(QObject), type(DBusObject)):
  def __init__(cls, name, bases, dct):
    type(QObject).__init__ (cls, name, bases, dct)
    type(DBusObject).__init__ (cls, name, bases, dct)
 
class QDBusObject(DBusObject, QObject):
  __metaclass__ = QDBusMetaclass
  def __init__(self, busName, objectPath, parent = None):
    QObject.__init__(self, parent)
    DBusObject.__init__(self, busName, objectPath)

Achtung: ich hab die Klasse zwar QDBusObject genannt, allerdings unterstützt sie natürlich nicht die Methoden aus der Qt-API.

~ Self-Connections ~

Mutige Programmierer könnten ja vielleicht nun auf die Idee kommen, eine DBus-Verbindung zum eigenen Prozess aufzubauen. Und manchmal ist das auch gar nicht so sinnlos wie es sich vielleicht anhört. Doch genau dies ist mit Python nicht möglich! Das liegt daran, dass sich die DBus-Event-Schleife und die Python-Event-Schleife gegenseitig blockieren. Leider scheint es ein ziemlich tief liegendes Python-Problem zu sein, dessen Lösung wohl noch etwas Zeit brauchen wird. Mir ist jedenfalls keine bekannt. (Außer Ruby zu verwenden, dort gehts)

~ Schlusswort ~

DBus ist super – mit der Python-Library zu arbeiten ist jedoch leider etwas mühsam, da es kaum vernünftige Doku und Beispiele gibt. Nach langem Probieren findet man alles, man muss nur gelegentlich in die Innereien greifen, um an seine Infos und Fehlermeldungen zu kommen. Die API ist noch nicht völlig ausgereift, an manchen Stellen wirkt der nötige Programmcode etwas hakelig und nicht konsequent durchkonzipiert. Auch verlangt das Modul oft eine syntaktische Ausführlichkeit, die eigentlich nicht nötig wäre.
Ich hoffe, ich habe mit diesem Tutorial die Einarbeitungszeit erheblich verkürzt, einige Tipps geliefert und die ein oder andere Falle von vornherein entschärft.

Viel Spaß beim Programmieren!

Linux, Software-Entwicklung , , , , , , , , ,