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

Vala 语言入门教程 / 10 - D-Bus 集成

第 10 章:D-Bus 集成

D-Bus 是 Linux 桌面的核心 IPC(进程间通信)机制。GNOME 的几乎所有系统服务都通过 D-Bus 暴露接口。


10.1 D-Bus 概述

10.1.1 什么是 D-Bus

D-Bus 是一个消息总线系统,允许应用程序之间进行通信:

┌─────────────────────────────────────────────────┐
│                  D-Bus 总线                      │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐      │
│  │ 系统总线  │  │ 会话总线  │  │ 启动服务  │      │
│  │(System)  │  │(Session) │  │(Activation)│     │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘      │
│       │              │              │             │
└───────┼──────────────┼──────────────┼────────────┘
        │              │              │
   ┌────┴────┐    ┌────┴────┐    ┌────┴────┐
   │Network  │    │ 文件管理 │    │ 通知守护 │
   │Manager  │    │  器     │    │  进程   │
   │(system) │    │(session)│    │(session)│
   └─────────┘    └─────────┘    └─────────┘

10.1.2 两条总线

总线用途地址
系统总线(System Bus)系统级服务(网络、蓝牙、电源)unix:path=/var/run/dbus/system_bus_socket
会话总线(Session Bus)用户级服务(通知、文件管理)随登录自动生成

10.1.3 D-Bus 通信模型

Client(客户端)          Service(服务端)
    │                         │
    │─── 方法调用 ───────────→│
    │                         │
    │←── 方法返回 ────────────│
    │                         │
    │←── 信号(订阅)─────────│
    │                         │
    │─── 属性读取 ───────────→│
    │←── 属性值 ──────────────│

10.2 查看系统 D-Bus 服务

10.2.1 使用 busctl 命令

# 列出所有总线上的服务
busctl list

# 列出会话总线上的服务
busctl --user list

# 查看某个服务的对象树
busctl tree org.freedesktop.login1

# 查看接口详情
busctl introspect org.freedesktop.login1 /org/freedesktop/login1

# 调用方法
busctl call org.freedesktop.login1 /org/freedesktop/login1 \
    org.freedesktop.login1.Manager GetSession s ""

# 监听信号
busctl monitor org.freedesktop.login1

10.2.2 使用 D-Feet(图形化工具)

# 安装 D-Feet
sudo apt install d-feet

# 启动
d-feet

D-Feet 提供图形界面浏览 D-Bus 服务、调用方法、查看属性。


10.3 Vala 中的 D-Bus 基础

10.3.1 使用 D-Bus 代理(客户端)

void main () {
    try {
        // 连接到会话总线
        var connection = GLib.Bus.get_sync (GLib.BusType.SESSION, null);

        // 创建代理
        var proxy = new GLib.DBusProxy.sync (
            connection,
            GLib.DBusProxyFlags.NONE,
            null,                                    // InterfaceInfo
            "org.freedesktop.DBus",                  // Bus name
            "/org/freedesktop/DBus",                 // Object path
            "org.freedesktop.DBus",                  // Interface
            null                                     // Cancellable
        );

        // 调用方法
        var result = proxy.call_sync (
            "ListNames",                             // Method name
            null,                                    // Parameters
            GLib.DBusCallFlags.NONE,
            -1,                                      // Timeout
            null                                     // Cancellable
        );

        // 解析结果
        if (result != null) {
            var names = result.get_child_value (0);
            uint length = names.n_children ();
            print ("总线上有 %u 个服务:\n", length);
            for (uint i = 0; i < length && i < 20; i++) {
                string? name = names.get_child_value (i).get_string ();
                print ("  %s\n", name);
            }
        }

    } catch (GLib.Error e) {
        printerr ("D-Bus 错误: %s\n", e.message);
    }
}

10.3.2 使用总线拥有者(BusName)

void main () {
    // 获取总线连接
    var connection = GLib.Bus.get_sync (GLib.BusType.SESSION, null);

    // 检查服务是否存在
    try {
        var result = connection.call_sync (
            "org.freedesktop.DBus",
            "/org/freedesktop/DBus",
            "org.freedesktop.DBus",
            "NameHasOwner",
            new GLib.Variant ("(s)", "org.freedesktop.portal.Desktop"),
            new GLib.VariantType ("(b)"),
            GLib.DBusCallFlags.NONE,
            -1,
            null
        );

        bool has_owner = result.get_child_value (0).get_boolean ();
        print ("portal 服务存在: %s\n", has_owner.to_string ());

    } catch (GLib.Error e) {
        printerr ("错误: %s\n", e.message);
    }
}

10.4 D-Bus 服务端实现

10.4.1 定义 D-Bus 接口

使用 Vala 的 [DBus] 注解可以方便地定义 D-Bus 服务:

// 定义 D-Bus 接口
[DBus (name = "com.example.Calculator")]
public class CalculatorService : Object {
    private int last_result = 0;

    // D-Bus 方法
    public int add (int a, int b) {
        last_result = a + b;
        print ("计算: %d + %d = %d\n", a, b, last_result);
        return last_result;
    }

    public int subtract (int a, int b) {
        last_result = a - b;
        return last_result;
    }

    public int multiply (int a, int b) {
        last_result = a * b;
        return last_result;
    }

    public double divide (int a, int b) throws GLib.Error {
        if (b == 0) {
            throw new GLib.IOError.FAILED ("除数不能为零");
        }
        last_result = a / b;
        return (double) a / b;
    }

    // D-Bus 属性
    public int last_result {
        get { return last_result; }
    }

    // D-Bus 信号
    public signal void calculation_done (int result);
}

void main () {
    var loop = new GLib.MainLoop ();

    // 注册 D-Bus 服务
    GLib.Bus.own_name (
        GLib.BusType.SESSION,
        "com.example.Calculator",
        GLib.BusNameOwnerFlags.NONE,
        (connection, name) => {
            // 总线名已获取
            try {
                connection.register_object (
                    "/com/example/Calculator",
                    new CalculatorService ()
                );
                print ("D-Bus 服务已注册: %s\n", name);
            } catch (GLib.Error e) {
                printerr ("注册失败: %s\n", e.message);
            }
        },
        () => {
            // 总线名获取成功
            print ("总线名已获取\n");
        },
        (connection, name) => {
            // 总线名丢失
            printerr ("总线名丢失: %s\n", name);
            loop.quit ();
        }
    );

    print ("计算器服务运行中...\n");
    print ("测试: busctl --user call com.example.Calculator /com/example/Calculator com.example.Calculator add ii 3 5\n");

    loop.run ();
}

10.4.2 异步 D-Bus 服务

[DBus (name = "com.example.FileService")]
public class FileService : Object {
    // 异步 D-Bus 方法
    public async string read_file (string path) throws GLib.Error {
        string content;
        size_t length;
        GLib.FileUtils.get_contents (path, out content, out length);
        return content;
    }

    public async void write_file (string path, string content)
        throws GLib.Error
    {
        GLib.FileUtils.set_contents (path, content);
    }

    // 同步方法
    public bool file_exists (string path) {
        return GLib.FileUtils.test (path, GLib.FileTest.EXISTS);
    }

    public signal void file_changed (string path, string event_type);
}

void main () {
    var loop = new GLib.MainLoop ();

    GLib.Bus.own_name (
        GLib.BusType.SESSION,
        "com.example.FileService",
        GLib.BusNameOwnerFlags.NONE,
        (connection, name) => {
            try {
                var service = new FileService ();
                connection.register_object (
                    "/com/example/FileService",
                    service
                );

                // 模拟文件变化信号
                GLib.Timeout.add (5000, () => {
                    service.file_changed ("/tmp/test.txt", "modified");
                    return GLib.Source.CONTINUE;
                });

                print ("文件服务已注册\n");
            } catch (GLib.Error e) {
                printerr ("注册失败: %s\n", e.message);
            }
        },
        () => {},
        (connection, name) => { loop.quit (); }
    );

    loop.run ();
}

10.5 D-Bus 客户端调用

10.5.1 使用 GDBusProxy

void main () {
    try {
        // 创建代理
        var proxy = new GLib.DBusProxy.for_bus_sync (
            GLib.BusType.SESSION,
            GLib.DBusProxyFlags.NONE,
            null,
            "com.example.Calculator",
            "/com/example/Calculator",
            "com.example.Calculator",
            null
        );

        // 调用方法
        var result = proxy.call_sync (
            "Add",
            new GLib.Variant ("(ii)", 10, 20),
            GLib.DBusCallFlags.NONE,
            -1,
            null
        );

        int sum = result.get_child_value (0).get_int32 ();
        print ("10 + 20 = %d\n", sum);

        // 读取属性
        var props = new GLib.DBusProxy.for_bus_sync (
            GLib.BusType.SESSION,
            GLib.DBusProxyFlags.NONE,
            null,
            "com.example.Calculator",
            "/com/example/Calculator",
            "org.freedesktop.DBus.Properties",
            null
        );

        var prop_result = props.call_sync (
            "Get",
            new GLib.Variant ("(ss)",
                "com.example.Calculator",
                "LastResult"
            ),
            GLib.DBusCallFlags.NONE,
            -1,
            null
        );

        int last = (int) prop_result.get_child_value (0)
                         .get_variant ()
                         .get_int32 ();
        print ("上次结果: %d\n", last);

    } catch (GLib.Error e) {
        printerr ("调用失败: %s\n", e.message);
    }
}

10.5.2 监听 D-Bus 信号

void main () {
    var loop = new GLib.MainLoop ();

    try {
        var connection = GLib.Bus.get_sync (GLib.BusType.SESSION, null);

        // 订阅信号
        uint subscription_id = connection.signal_subscribe (
            "com.example.Calculator",        // sender
            "com.example.Calculator",        // interface
            "CalculationDone",               // signal name
            "/com/example/Calculator",       // object path
            null,                            // arg0
            GLib.DBusSignalFlags.NONE,
            (conn, sender, object_path, interface_name,
             signal_name, parameters) => {
                int result = parameters.get_child_value (0).get_int32 ();
                print ("收到信号: 计算完成,结果=%d\n", result);
            }
        );

        print ("正在监听 D-Bus 信号...\n");
        print ("按 Ctrl+C 退出\n");

        loop.run ();

    } catch (GLib.Error e) {
        printerr ("错误: %s\n", e.message);
    }
}

10.6 GObject Introspection 和 D-Bus

10.6.1 调用系统 D-Bus 服务

void main () {
    // 调用系统通知服务
    try {
        var proxy = new GLib.DBusProxy.for_bus_sync (
            GLib.BusType.SESSION,
            GLib.DBusProxyFlags.NONE,
            null,
            "org.freedesktop.Notifications",
            "/org/freedesktop/Notifications",
            "org.freedesktop.Notifications",
            null
        );

        // 发送通知
        var result = proxy.call_sync (
            "Notify",
            new GLib.Variant ("(susssasa{sv}i)",
                "ValaApp",           // app_name
                0,                   // replaces_id
                "dialog-information", // icon
                "Vala 通知",          // summary
                "这是一条来自 Vala 的通知!", // body
                new GLib.Variant ("as", new string[0]),  // actions
                new GLib.Variant ("a{sv}", new GLib.Variant[0]),  // hints
                5000                 // timeout (ms)
            ),
            GLib.DBusCallFlags.NONE,
            -1,
            null
        );

        uint32 notification_id = result.get_child_value (0).get_uint32 ();
        print ("通知已发送,ID: %u\n", notification_id);

    } catch (GLib.Error e) {
        printerr ("通知发送失败: %s\n", e.message);
    }
}

10.6.2 查询系统信息

void main () {
    // 查询系统登录信息
    try {
        var proxy = new GLib.DBusProxy.for_bus_sync (
            GLib.BusType.SYSTEM,
            GLib.DBusProxyFlags.NONE,
            null,
            "org.freedesktop.login1",
            "/org/freedesktop/login1",
            "org.freedesktop.login1.Manager",
            null
        );

        // 获取当前会话
        var result = proxy.call_sync (
            "GetSession",
            new GLib.Variant ("(s)", ""),
            GLib.DBusCallFlags.NONE,
            -1,
            null
        );

        if (result != null) {
            string session_path = result.get_child_value (0)
                                        .get_obj_path ();
            print ("当前会话路径: %s\n", session_path);
        }

    } catch (GLib.Error e) {
        printerr ("查询失败: %s\n", e.message);
    }
}

10.7 属性和接口定义

10.7.1 完整的 D-Bus 服务定义

// 定义接口 XML(可选,Vala 也可以自动推断)
/*
<node>
  <interface name="com.example.ConfigService">
    <method name="GetConfig">
      <arg name="key" direction="in" type="s"/>
      <arg name="value" direction="out" type="s"/>
    </method>
    <method name="SetConfig">
      <arg name="key" direction="in" type="s"/>
      <arg name="value" direction="in" type="s"/>
    </method>
    <property name="Version" type="s" access="read"/>
    <signal name="ConfigChanged">
      <arg name="key" type="s"/>
      <arg name="value" type="s"/>
    </signal>
  </interface>
</node>
*/

[DBus (name = "com.example.ConfigService")]
public class ConfigService : Object {
    private GLib.HashTable<string, string> config;

    construct {
        config = new GLib.HashTable<string, string> (str_hash, str_equal);
        config["app.name"] = "MyApp";
        config["app.version"] = "1.0.0";
        config["app.debug"] = "true";
    }

    // D-Bus 方法
    public string get_config (string key) throws GLib.Error {
        if (!config.contains (key)) {
            throw new GLib.IOError.NOT_FOUND (
                "配置键不存在: %s".printf (key)
            );
        }
        return config[key];
    }

    public void set_config (string key, string value) {
        config[key] = value;
        config_changed (key, value);
        print ("配置更新: %s = %s\n", key, value);
    }

    // D-Bus 属性
    public string version {
        owned get { return "1.0.0"; }
    }

    // D-Bus 信号
    public signal void config_changed (string key, string value);
}

void main () {
    var loop = new GLib.MainLoop ();

    GLib.Bus.own_name (
        GLib.BusType.SESSION,
        "com.example.ConfigService",
        GLib.BusNameOwnerFlags.NONE,
        (connection, name) => {
            try {
                connection.register_object (
                    "/com/example/ConfigService",
                    new ConfigService ()
                );
                print ("配置服务已启动\n");
            } catch (GLib.Error e) {
                printerr ("注册失败: %s\n", e.message);
            }
        },
        () => {},
        (connection, name) => { loop.quit (); }
    );

    loop.run ();
}

10.8 系统总线 vs 会话总线

特性系统总线会话总线
访问权限需要 root 或 PolicyKit用户级
地址/var/run/dbus/system_bus_socket自动生成
用途系统服务用户应用
安全高(受限访问)中(用户会话内)
示例服务NetworkManager, systemdNotifications, Portal

10.8.1 使用 PolicyKit 进行权限检查

[DBus (name = "com.example.SystemService")]
public class SystemService : Object {
    public void shutdown () throws GLib.Error {
        // 在实际应用中,这里应该检查 PolicyKit 权限
        print ("系统关机请求\n");

        // 使用 systemd 关机
        try {
            var proxy = new GLib.DBusProxy.for_bus_sync (
                GLib.BusType.SYSTEM,
                GLib.DBusProxyFlags.NONE,
                null,
                "org.freedesktop.login1",
                "/org/freedesktop/login1",
                "org.freedesktop.login1.Manager",
                null
            );

            proxy.call_sync (
                "PowerOff",
                new GLib.Variant ("(b)", false),
                GLib.DBusCallFlags.NONE,
                -1,
                null
            );
        } catch (GLib.Error e) {
            throw new GLib.IOError.FAILED (
                "关机失败: %s".printf (e.message)
            );
        }
    }

    public string status {
        owned get { return "running"; }
    }
}

void main () {
    var loop = new GLib.MainLoop ();

    GLib.Bus.own_name (
        GLib.BusType.SYSTEM,
        "com.example.SystemService",
        GLib.BusNameOwnerFlags.NONE,
        (connection, name) => {
            try {
                connection.register_object (
                    "/com/example/SystemService",
                    new SystemService ()
                );
                print ("系统服务已注册\n");
            } catch (GLib.Error e) {
                printerr ("注册失败: %s\n", e.message);
            }
        },
        () => {},
        (connection, name) => { loop.quit (); }
    );

    loop.run ();
}

10.9 业务场景:桌面通知服务

[DBus (name = "org.freedesktop.Notifications")]
public class NotificationService : Object {
    private uint32 next_id = 1;
    private GLib.HashTable<uint32, NotificationInfo> notifications;

    construct {
        notifications = new GLib.HashTable<uint32, NotificationInfo> (
            direct_hash, direct_equal
        );
    }

    public uint32 notify (
        string app_name,
        uint32 replaces_id,
        string app_icon,
        string summary,
        string body,
        string[] actions,
        GLib.HashTable<string, GLib.Variant> hints,
        int32 expire_timeout
    ) throws GLib.Error {
        uint32 id = replaces_id > 0 ? replaces_id : next_id++;
        if (replaces_id == 0) next_id++;

        var info = new NotificationInfo ();
        info.app_name = app_name;
        info.summary = summary;
        info.body = body;
        info.timestamp = new GLib.DateTime.now_local ().to_string ();

        notifications[id] = info;

        print ("┌─────────────────────────────────┐\n");
        print ("│ 📢 %s\n", summary);
        print ("│ %s\n", body);
        print ("│ 来自: %s | ID: %u\n", app_name, id);
        print ("└─────────────────────────────────┘\n");

        notification_received (id, app_name, summary, body);

        // 自动过期
        if (expire_timeout > 0) {
            GLib.Timeout.add_once ((uint) expire_timeout, () => {
                close_notification (id);
            });
        }

        return id;
    }

    public void close_notification (uint32 id) throws GLib.Error {
        if (notifications.contains (id)) {
            notifications.remove (id);
            notification_closed (id, 1);  // 1 = dismissed
            print ("通知已关闭: %u\n", id);
        }
    }

    public string[] get_capabilities () {
        return {
            "body",
            "body-markup",
            "body-hyperlinks",
            "persistence"
        };
    }

    public void get_server_information (
        out string name,
        out string vendor,
        out string version,
        out string spec_version
    ) {
        name = "ValaNotificationService";
        vendor = "Example";
        version = "1.0.0";
        spec_version = "1.2";
    }

    public signal void notification_received (
        uint32 id, string app_name,
        string summary, string body
    );
    public signal void notification_closed (uint32 id, uint32 reason);
}

public class NotificationInfo : Object {
    public string app_name;
    public string summary;
    public string body;
    public string timestamp;
}

void main () {
    var loop = new GLib.MainLoop ();

    GLib.Bus.own_name (
        GLib.BusType.SESSION,
        "org.freedesktop.Notifications",
        GLib.BusNameOwnerFlags.REPLACE,
        (connection, name) => {
            try {
                var service = new NotificationService ();
                connection.register_object (
                    "/org/freedesktop/Notifications",
                    service
                );
                print ("通知服务已启动\n");
                print ("测试: busctl --user call org.freedesktop.Notifications /org/freedesktop/Notifications org.freedesktop.Notifications Notify susssasa{sv}i \"Test\" 0 \"\" \"测试标题\" \"测试内容\" 0 0 5000\n");
            } catch (GLib.Error e) {
                printerr ("注册失败: %s\n", e.message);
            }
        },
        () => {},
        (connection, name) => { loop.quit (); }
    );

    loop.run ();
}

10.10 注意事项

⚠️ D-Bus 常见陷阱

  1. 总线名冲突:确保你的服务名唯一,使用反向域名格式
  2. 线程安全:D-Bus 回调可能在不同线程
  3. 超时处理:设置合理的调用超时
  4. 错误传播:D-Bus 错误会包装在 GLib.Error
  5. 类型匹配:参数类型必须与接口定义完全匹配
  6. 资源清理:使用 GLib.Bus.unown_name() 释放总线名

10.11 扩展阅读

资源链接
D-Bus 规范https://dbus.freedesktop.org/doc/dbus-specification.html
GDBus 文档https://docs.gtk.org/gio/DBus.html
D-Bus 最佳实践https://dbus.freedesktop.org/doc/dbus-api-design.html
busctl 手册man busctl
D-Feethttps://wiki.gnome.org/Apps/DFeet
GNOME D-Bus 教程https://developer.gnome.org/documentation/tutorials/d-bus.html

10.12 总结

要点说明
D-BusLinux IPC 标准,两条总线(系统/会话)
服务端[DBus] 注解 + Bus.own_name
客户端DBusProxy 或直接 connection.call_sync
信号connection.signal_subscribe
属性通过 org.freedesktop.DBus.Properties
安全系统总线需要 PolicyKit

下一章我们将学习 Docker 构建与部署。→ 第 11 章:Docker 构建与部署