强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

D-Bus 完整教程 / 05 - D-Bus 方法调用

第 05 章:D-Bus 方法调用


5.1 方法调用的基本流程

D-Bus 方法调用遵循 请求-响应 模型,类似于 HTTP 的 GET/POST:

客户端                              服务端
  │                                   │
  │── Method Call ──────────────────→│
  │   (serial, dest, path,           │
  │    interface, member, args)       │
  │                                   │
  │←── Method Return ───────────────│
  │   (reply_serial, args)            │
  │                                   │
  │  或                               │
  │←── Error ────────────────────────│
  │   (reply_serial, error_name,     │
  │    error_message)                 │

关键规则:

  • 每个 Method Call 有唯一的 serial(递增整数)
  • 对应的 Return/Error 必须携带 reply_serial 与之匹配
  • 一个连接可以同时有多个未完成的调用(多路复用)

5.2 同步调用

同步调用会阻塞等待回复,适合简单场景。

5.2.1 命令行同步调用

# 同步调用 ListNames,等待返回
busctl call \
  org.freedesktop.DBus \
  /org/freedesktop/DBus \
  org.freedesktop.DBus \
  ListNames

# 带参数的同步调用
busctl call \
  org.freedesktop.login1 \
  /org/freedesktop/login1 \
  org.freedesktop.login1.Manager \
  GetSession \
  s "auto"

5.2.2 Python 同步调用

#!/usr/bin/env python3
"""D-Bus 同步方法调用示例"""

import dbus

bus = dbus.SessionBus()

# 获取代理对象
proxy = bus.get_object(
    'org.freedesktop.DBus',
    '/org/freedesktop/DBus'
)

# 获取接口
iface = dbus.Interface(proxy, 'org.freedesktop.DBus')

# 同步调用
names = iface.ListNames()
print("总线上的名称:")
for name in names:
    print(f"  {name}")

# 使用 Properties 接口获取属性
props_iface = dbus.Interface(proxy, 'org.freedesktop.DBus.Properties')
version = props_iface.Get('org.freedesktop.DBus', 'Version')
print(f"\nD-Bus 版本: {version}")

5.2.3 GDBus (C) 同步调用

#include <gio/gio.h>
#include <stdio.h>

int main(void) {
    GError *error = NULL;
    
    GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
    if (!conn) {
        fprintf(stderr, "连接失败: %s\n", error->message);
        g_error_free(error);
        return 1;
    }
    
    /* 同步调用 ListNames */
    GVariant *result = g_dbus_connection_call_sync(
        conn,
        "org.freedesktop.DBus",           /* bus name */
        "/org/freedesktop/DBus",          /* object path */
        "org.freedesktop.DBus",           /* interface */
        "ListNames",                       /* method */
        NULL,                              /* parameters */
        G_VARIANT_TYPE("(as)"),           /* reply type */
        G_DBUS_CALL_FLAGS_NONE,
        5000,                              /* timeout ms */
        NULL,                              /* cancellable */
        &error
    );
    
    if (error) {
        fprintf(stderr, "调用失败: %s\n", error->message);
        g_error_free(error);
    } else {
        GVariantIter *iter;
        const char *name;
        g_variant_get(result, "(as)", &iter);
        printf("总线上的名称:\n");
        while (g_variant_iter_next(iter, "&s", &name)) {
            printf("  %s\n", name);
        }
        g_variant_iter_free(iter);
        g_variant_unref(result);
    }
    
    g_object_unref(conn);
    return 0;
}

编译运行:

gcc -o method-sync method-sync.c $(pkg-config --cflags --libs gio-2.0)
./method-sync

5.3 异步调用

异步调用不会阻塞,通过回调函数处理结果,适合 GUI 或高并发服务。

5.3.1 Python 异步调用

#!/usr/bin/env python3
"""D-Bus 异步方法调用示例(使用 GLib 主循环)"""

import dbus
import dbus.mainloop.glib
from gi.repository import GLib

dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SessionBus()
loop = GLib.MainLoop()

def on_reply(names):
    print("异步收到名称列表:")
    for name in names:
        print(f"  {name}")
    loop.quit()

def on_error(error):
    print(f"调用出错: {error}")
    loop.quit()

proxy = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus')
iface = dbus.Interface(proxy, 'org.freedesktop.DBus')

# 异步调用
iface.ListNames(
    reply_handler=on_reply,
    error_handler=on_error
)

print("等待异步结果...")
loop.run()

5.3.2 GDBus (C) 异步调用

#include <gio/gio.h>

static void on_list_names_ready(GObject *source, GAsyncResult *res, gpointer user_data) {
    GError *error = NULL;
    GVariant *result = g_dbus_connection_call_finish(
        G_DBUS_CONNECTION(source), res, &error
    );
    
    if (error) {
        g_printerr("调用失败: %s\n", error->message);
        g_error_free(error);
    } else {
        GVariantIter *iter;
        const char *name;
        g_variant_get(result, "(as)", &iter);
        g_print("总线上的名称:\n");
        while (g_variant_iter_next(iter, "&s", &name)) {
            g_print("  %s\n", name);
        }
        g_variant_iter_free(iter);
        g_variant_unref(result);
    }
    
    g_main_loop_quit((GMainLoop *)user_data);
}

int main(void) {
    GMainLoop *loop = g_main_loop_new(NULL, FALSE);
    GError *error = NULL;
    
    GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
    if (!conn) {
        g_printerr("连接失败: %s\n", error->message);
        g_error_free(error);
        return 1;
    }
    
    /* 异步调用 */
    g_dbus_connection_call(
        conn,
        "org.freedesktop.DBus",
        "/org/freedesktop/DBus",
        "org.freedesktop.DBus",
        "ListNames",
        NULL,                              /* parameters */
        G_VARIANT_TYPE("(as)"),
        G_DBUS_CALL_FLAGS_NONE,
        5000,                              /* timeout ms */
        NULL,                              /* cancellable */
        on_list_names_ready,               /* callback */
        loop                               /* user_data */
    );
    
    g_print("等待异步结果...\n");
    g_main_loop_run(loop);
    
    g_object_unref(conn);
    g_main_loop_unref(loop);
    return 0;
}

5.4 超时处理

5.4.1 超时机制

配置项默认值说明
Method Call 超时25 秒客户端等待回复的最大时间
dbus-daemon 转发超时无限制守护进程不会主动截断
busctl 超时25 秒可通过 --timeout= 修改
# 设置 busctl 超时
busctl call --timeout=60s \
  org.example.SlowService \
  /org/example \
  org.example.Interface \
  LongRunningMethod

5.4.2 NoReply 方法

某些方法不需要回复(fire-and-forget),通过注解标记:

<method name="Exit">
  <annotation name="org.freedesktop.DBus.Method.NoReply" value="true"/>
</method>

客户端必须设置 NO_REPLY_EXPECTED 标志:

# 使用 gdbus 调用 NoReply 方法
gdbus call --session \
  --dest org.example.Service \
  --object-path /org/example \
  --method org.example.Interface.Exit

5.4.3 超时编程处理

#!/usr/bin/env python3
"""D-Bus 调用超时处理"""

import dbus
from dbus.exceptions import DBusException

bus = dbus.SessionBus()

try:
    proxy = bus.get_object(
        'org.freedesktop.login1',
        '/org/freedesktop/login1',
        introspect=False
    )
    iface = dbus.Interface(proxy, 'org.freedesktop.login1.Manager')
    
    # dbus-python 默认超时为 25 秒
    # 可以通过设置 timeout 参数调整
    result = iface.CanHibernate(timeout=10)  # 10 秒超时
    print(f"可以休眠: {result}")

except DBusException as e:
    if 'Timed out' in str(e):
        print("调用超时,服务可能无响应")
    elif 'ServiceUnknown' in str(e):
        print("服务不可用,可能未安装")
    else:
        print(f"D-Bus 错误: {e}")

5.5 错误处理

5.5.1 D-Bus 错误名称

D-Bus 错误使用 反向域名 格式的错误名称:

错误名称说明
org.freedesktop.DBus.Error.UnknownMethod方法不存在
org.freedesktop.DBus.Error.UnknownObject对象不存在
org.freedesktop.DBus.Error.UnknownInterface接口不存在
org.freedesktop.DBus.Error.ServiceUnknown服务不可用
org.freedesktop.DBus.Error.AccessDenied权限不足
org.freedesktop.DBus.Error.InvalidArgs参数无效
org.freedesktop.DBus.Error.NoReply超时无回复
org.freedesktop.DBus.Error.FileNotFound文件未找到

5.5.2 命令行错误处理

# 尝试调用不存在的方法
busctl call \
  org.freedesktop.DBus \
  /org/freedesktop/DBus \
  org.freedesktop.DBus \
  NonExistentMethod

# 输出:
# Call failed: Method "NonExistentMethod" with signature "" on interface "org.freedesktop.DBus" doesn't exist

# 尝试调用需要授权的方法
busctl call \
  org.freedesktop.login1 \
  /org/freedesktop/login1 \
  org.freedesktop.login1.Manager \
  PowerOff \
  b true

# 可能输出:
# Call failed: Access denied

5.5.3 GDBus 错误处理

GError *error = NULL;
GVariant *result = g_dbus_connection_call_sync(..., &error);

if (error) {
    if (g_dbus_error_is_remote_error(error)) {
        gchar *remote_error = g_dbus_error_get_remote_error(error);
        g_print("远程错误: %s\n", remote_error);
        g_free(remote_error);
    } else {
        g_print("本地错误: %s\n", error->message);
    }
    g_error_free(error);
}

5.5.4 Python 错误处理

import dbus
from dbus.exceptions import DBusException

try:
    result = iface.SomeMethod()
except DBusException as e:
    # 错误名称在 e.get_dbus_name()
    error_name = e.get_dbus_name()
    error_msg = str(e)
    
    if error_name == 'org.freedesktop.DBus.Error.AccessDenied':
        print("权限不足,需要管理员权限")
    elif error_name == 'org.freedesktop.DBus.Error.ServiceUnknown':
        print("服务未安装或未启动")
    elif error_name == 'org.freedesktop.DBus.Error.InvalidArgs':
        print(f"参数错误: {error_msg}")
    else:
        print(f"未知错误 [{error_name}]: {error_msg}")

5.6 D-Bus 类型系统

D-Bus 使用严格的类型系统,所有参数必须明确类型。

5.6.1 基本类型

符号类型大小C 类型Python 类型
yBYTE1 byteguint8dbus.Byte
bBOOLEAN4 bytesgbooleandbus.Boolean
nINT162 bytesgint16dbus.Int16
qUINT162 bytesguint16dbus.UInt16
iINT324 bytesgint32dbus.Int32
uUINT324 bytesguint32dbus.UInt32
xINT648 bytesgint64dbus.Int64
tUINT648 bytesguint64dbus.UInt64
dDOUBLE8 bytesgdoubledbus.Double
sSTRING变长const gchar*str
oOBJECT_PATH变长const gchar*dbus.ObjectPath
vVARIANT变长GVariant*dbus.Variant

5.6.2 容器类型

符号类型示例签名说明
a<T>ARRAYas, ai同类型元素数组
(...)STRUCT(si), (sib)异类型元素元组
a{KV}DICTa{sv}, a{ss}键值映射
hUNIX_FD-文件描述符传递

5.6.3 类型签名示例

签名含义实际值示例
s字符串"hello"
as字符串数组["a", "b", "c"]
a{sv}键值字典(string→variant){"name": "test", "count": 42}
(si)结构体(string, int32)("hello", 42)
a(si)结构体数组[("a", 1), ("b", 2)]
a{sa{sv}}嵌套字典{"iface": {"prop": "val"}}

5.6.4 Variant 类型

VARIANTv)是 D-Bus 最灵活的类型——它可以包含任意类型的值。

# 发送 Variant 参数
dbus-send --session --dest=org.example.Service \
  --type=method_call --print-reply \
  /org/example \
  org.example.Interface.SetValue \
  variant:string:"hello"

# variant:int32:42
# variant:array:string:"a","b","c"

注意a{sv}(string→variant 字典)是 D-Bus 生态中最常用的复合类型。GNOME、systemd、Flatpak 等大量使用它来传递灵活的配置数据。


5.7 实战场景

场景 1:通过 D-Bus 控制媒体播放器

# 查看 MPRIS 播放器
busctl --user list | grep mpris

# 获取播放器名称
PLAYER="org.mpris.MediaPlayer2.spotify"

# 播放/暂停
busctl call --user \
  $PLAYER \
  /org/mpris/MediaPlayer2 \
  org.mpris.MediaPlayer2.Player \
  PlayPause

# 获取当前曲目信息
busctl get-property --user \
  $PLAYER \
  /org/mpris/MediaPlayer2 \
  org.mpris.MediaPlayer2.Player \
  Metadata

场景 2:发送桌面通知

busctl call --user \
  org.freedesktop.Notifications \
  /org/freedesktop/Notifications \
  org.freedesktop.Notifications \
  Notify \
  s "我的应用" \
  u 0 \
  s "" \
  s "通知标题" \
  s "这是通知内容" \
  as 0 \
  a{sv} 0 \
  i 5000

场景 3:查询网络状态

#!/usr/bin/env python3
"""通过 D-Bus 查询 NetworkManager 状态"""

import dbus

bus = dbus.SystemBus()
nm = bus.get_object(
    'org.freedesktop.NetworkManager',
    '/org/freedesktop/NetworkManager'
)
props = dbus.Interface(nm, 'org.freedesktop.DBus.Properties')

# 获取网络状态
state = props.Get('org.freedesktop.NetworkManager', 'State')
# 70 = 已连接全局网络
print(f"网络状态: {state}")

# 获取活动连接
active = props.Get('org.freedesktop.NetworkManager', 'ActiveConnections')
print(f"活动连接: {len(active)} 个")

# 获取设备列表
nm_iface = dbus.Interface(nm, 'org.freedesktop.NetworkManager')
devices = nm_iface.GetDevices()
print(f"网络设备: {len(devices)} 个")

5.8 常见错误排查

症状原因解决方案
ServiceUnknown服务未运行检查服务状态,配置 D-Bus 激活
AccessDenied策略拒绝检查 /etc/dbus-1/system.d/ 策略文件
InvalidArgs类型不匹配检查参数类型签名
NoReply服务无响应增加超时,检查服务是否死锁
UnknownMethod方法名错误使用 busctl introspect 确认

本章小结

概念说明
Method Call请求-响应调用,每个调用有唯一 serial
同步调用阻塞等待回复,适合简单场景
异步调用回调处理回复,适合 GUI 和高并发
超时默认 25 秒,可自定义
错误名称反向域名格式,如 org.freedesktop.DBus.Error.AccessDenied
类型系统严格类型,a{sv} 为最常用复合类型

扩展阅读