D-Bus初探——控制你的播放器

Tiger Soldier

<tigersoldi@gmail.com>
graphic with four colored squares

大纲

  • D-Bus简介
  • 初探——控制播放器
  • 再探——实现服务

D-Bus是?

一种进程间通信(IPC)机制

为什么用D-Bus

  • 简化IPC开发
  • 开放标准
  • 广泛支持
  • 多语言绑定

D-Bus的特点

  • 基于总线连接
  • 基于消息传递
  • 类面向对象
  • 支持单点传输(方法调用)和广播(信号)
  • 按需启动
  • Introspection
  • 权限控制

能用D-Bus干些啥?

  • 与其他程序进行通信:OSD Lyrics
  • 在用户级别操作硬件:NetworkManager
  • 作为插件接口:Telepathy

大纲

  • D-Bus简介
  • 初探——控制播放器
  • 再探——实现服务

Bus

  • IPC通过Bus完成,程序连接到Bus,消息由Bus转发
  • 两条Bus
    • System Bus
    • Session Bus

System Bus

提供系统级别的服务,一般用于硬件操作与通知。

$ qdbus --system
 com.ubuntu.Upstart
 org.freedesktop.Avahi
:1.19
 org.freedesktop.PolicyKit1
:1.2
 org.freedesktop.NetworkManager
...

Session Bus

会话级别的Bus,每个登录会话各有一个。用于桌面应用。

$ qdbus 
 org.gnome.Rhythmbox
:1.31
:1.313
 org.mpris.MediaPlayer2.rhythmbox
:1.315
 org.kde.amarok
:1.318
...

Python: 连接到Bus

import dbus
bus = dbus.SessionBus()
# 使用dbus.SystemBus()连接到系统Bus

Bus Name

每个连接都有一个唯一标识,即连接的Bus Name

  • 以冒号(:)开头,后接两段以点分开的数字
  • Bus Name是唯一的,即使连接消失了也不会用在另一个连接上

Well-known name

由于Bus Name是唯一而且无法预期的,服务提供者需要申请Well-known name.

  • 类似域名,以点分隔,每段由英文字母/数字/下划线/减号组成
  • Well-known name 只是别名,一个Bus连接可以有多个Well-known name
  • 任意程序都可以申请任意合法Well-known name,冲突使用排队机制解决

例子

$ qdbus 
 org.gnome.Rhythmbox
~~~~~~~~~~~~~~~~~~~~ Well-known name
:1.31
~~~~~ Bus name
...

对象

每个服务可以导出多个对象

  • 网络管理器:每个接口作为一个对象
  • 编辑器:每个文档作为一个对象
  • 播放器:播放控制作为一个对象,每个播放列表作为一个对象

对象名称使用类似路径的方式表示

$ qdbus org.mpris.audacious 
/
/Player
/TrackList
...

Python: 获取一个对象

我们可以在Python中指定Bus Name和对象名称,得到一个远端对象在本地的proxy

import dbus
service = "org.mpris.audacious"
object_path = "/Player"

if __name__ == '__main__':
    bus = dbus.SessionBus()
    try:
        proxy = bus.get_object(service, object_path);
    except dbus.DBusException:
        print "Can not connect to service"
        exit(1)

方法调用

方法调用是在对象的基础上进行的。不同对象有不同方法。

$ qdbus org.mpris.audacious /Player Next

在Python中,直接对对象的proxy调用相应的方法即可

proxy.Next()

参数类型

D-Bus方法调用支持传入和返回参数。

  • 参数类型使用字符串描述,称为参数的签名
  • 原子类型使用单个字符表示,例如INT32的签名为"i"
  • 多个参数按顺序并排,例如"ii"表示传入两个INT32
  • 支持多个传出参数(返回值)

原子类型

类型签名备注
BYTEy
BOOLEANb有效值为0(FALSE)和1(TRUE)
INT16n
UINT16q
INT32i
UINT32u
INT64x
UINT64t
DOUBLEd
STRINGsUTF-8编码
VARIANTv万能类型,可以容纳任何类型,一般用于类型可变的组合类型
OBJECT\_PATHo对象的路径,用于传递对象
SIGNATUREg类型签名

复合类型

  • 结构体(STRUCT)
    • "(iii)"表示一个参数,内含三个INT32
    • "iii"表示3个INT32参数
  • 数组(ARRAY)
    • "aii":一个INT32数组加上另一个单独的INT32
    • "a(ii)":为两个INT32组成的结构体的数组
    • "aai":二维数组
  • 字典(DICT)
    • "a{si}": key为STRING,value为INT32的字典
    • "a{(ss)a(sai)}"

Python: 使用参数与返回值

对python函数参数和返回值的使用完全相同。参数会自动转换为签名所对应的类型,返回值会自动转换为对应的Python类型。

例如,GetMetadata方法返回值签名为"a{sv}"

metadata = proxy.GetMetadata()
for k, v in metadata.items():
    print "%s: %s" % (k, v)
# album: 初音ミクベスト ~memories~
# artist: supercell feat. 初音ミク
# title: ワールドイズマイン
# ...

参数类型转换

  • 各种UINT、INT,以及BYTE都等价于 int 类型
  • BOOLEAN可以是任意对象(使用 bool() 转换)
  • BYTE还可以是一个字符(使用 ord() 转换)
  • ARRAY对应为 list (dbus.Array)
  • STRUCT对应为 tuple (dbus.Struct)
  • DICT对应 dict (dbus.Dictionary)

接口

每个方法属于一个接口,一个对象可以实现多个接口,接口主要有以下用途

  • 标明支持的标准,实现相应规范
  • 为同名方法提供命名空间

接口与Well-known names一样,使用类似域名的命名方法

$ qdbus org.mpris.audacious /Player 
method QString org.freedesktop.DBus.Introspectable.Introspect()
               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~
                          接口                        方法名

Python: 指定接口

指定接口主要是为了防止不同接口接口具有同名方法。

在Python中指定接口有两种方法:

  • 调用时指定 dbus_interface 参数
    interface = "org.freedesktop.MediaPlayer"
    metadata = proxy.GetMetadata(dbus_interface=interface)
    
  • 使用dbus.Interface对proxy进行包装
    interface = "org.freedesktop.MediaPlayer"
    iface = dbus.Interface(proxy, dbus_interface=interface)
    metadata = iface.GetMetadata()
    

控制播放器: MPRIS

Media Player Remote Interfaceing Specification

  • 服务以"org.mpris."作为前缀命名
  • 导出"/"、"/Player"、"TrackList"三个对象
  • 使用"org.freedesktop.MediaPlayer"接口
  • 一系列标准函数:Play、Pause、Stop、Next、Prev……

异步调用

D-Bus的方法调用本身是异步的。要实现异步调用,需要两个条件

  • 使用消息循环
  • 为方法调用加入异步回调函数

消息循环

dbus-python支持基于glib的消息循环。可以为所有连接指定全局消息循环,也可以为每个连接单独指定消息循环。

首先需要导入消息循环:

from dbus.mainloop.glib import DBusGMainLoop
import glib
loop = glib.MainLoop()

指定全局消息循环:

mainloop = DBusGMainLoop(set_as_default=True)
bus = dbus.SessionBus()

单独指定消息循环:

mainloop = DBusGMainLoop()
session = dbus.SessionBus(mainloop=mainloop)

异步操作

  • 在方法调用时提供 reply_handlererror_handler 两个回调函数
  • 返回值在 reply_handler 里提供
    def reply_cb(metadata):
        print "Got Metadata"
        for k, v in metadata.items():
            print "%s: %s" % (k, v)
        loop.quit()
    
    def error_cb(e):
        print "Async call failed"
        loop.quit()
    
    iface.GetMetadata(reply_handler=reply_cb,
                      error_handler=error_cb)
    

信号

  • 由服务提供者发送
  • 信号属于接口
  • 信号可以带有参数
  • 使用信号需要建立消息循环
    def track_change_cb(track, sender=None):
        print "Track changed: %s" % track['title']
        print "Sender is: %s" % sender
    iface.connect_to_signal("TrackChange",
                            track_change_cb,
                            sender_keyword="sender")
    

大纲

  • D-Bus简介
  • 初探——控制播放器
  • 再探——实现服务

获取well-known name

import dbus.service
BUS_NAME = 'org.mpris.demo'
bus = dbus.SessionBus()
bus_name = dbus.service.BusName(bus_name, bus)

创建D-Bus对象

  • 继承dbus.service.Object对象
  • 初始化函数中 bus_nameconn 至少指定一项
  • bus_name 为获取的well-known name对象
  • conndbus.SessionBus() 或者 dbus.SystemBus() 所得到的连接对象
    class MprisPlayer(dbus.service.Object):
        def __init__(self, bus_name):
            dbus.service.Object.__init__(self,
                                         bus_name=bus_name,
                                         object_path='/Player')
    #...
    player_obj = MprisPlayer(self._bus_name)
    

创建方法

使用dbus.service.method修饰类的方法即可

class MprisPlayer(dbus.service.Object):
#...
    @dbus.service.method(dbus_interface=MPRIS_IFACE,
                         in_signature='',
                         out_signature='a{sv}')
    def GetMetadata(self):
        return ['title': 'demo']
#...

创建信号

  • 使用dbus.service.signal修饰类的方法
  • 消息的参数就是方法的传入参数
  • 调用方法即触发信号
    class MprisPlayer(dbus.service.Object):
    #...
        @dbus.service.signal(dbus_interface=MPRIS_IFACE,
                             signature='a{sv}')
        def TrackChange(self, track):
            pass
    #...
    player_obj.TrackChange(['title': 'demo'])
    

按需启动

  • 只有在需要的时候,才启动服务
  • 客户端无需考虑服务是否已经启动

    创建以.service为后缀的文件,置于/usr/share/dbus-1/services目录下(系统服务为system-services目录),内容如下:

    [D-BUS Service]
    Name=well.known.name1;well.known.name2
    Exec=/path/to/program
    

FAQ