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

D-Bus 完整教程 / 07 - D-Bus 属性系统

第 07 章:D-Bus 属性系统


7.1 属性概述

属性(Property)是 D-Bus 对象上的 命名值,提供结构化的状态访问接口。与方法调用不同,属性有明确的读写语义:

对比方法属性
语义执行操作/获取数据读/写状态值
读取方法调用 + 返回值Properties.Get
写入方法调用(可能有副作用)Properties.Set
变化通知需自行实现信号标准 PropertiesChanged
批量读取多次调用Properties.GetAll

几乎所有 D-Bus 服务都通过属性暴露状态信息:

服务属性示例
NetworkManagerState(网络状态)、ActiveConnections
systemd-logindBlockInhibited(锁定策略)
UPowerPercentage(电池百分比)、OnBattery
BlueZAddress(蓝牙地址)、Connected
MPRIS 播放器PlaybackStatusMetadataVolume

7.2 标准 Properties 接口

所有 D-Bus 对象都隐式实现了 org.freedesktop.DBus.Properties 接口:

interface org.freedesktop.DBus.Properties {
    methods:
        Get(s: interface_name, s: property_name) → (v: value)
        GetAll(s: interface_name) → (a{sv}: props)
        Set(s: interface_name, s: property_name, v: value)
    signals:
        PropertiesChanged(s: interface_name,
                          a{sv}: changed_properties,
                          as: invalidated_properties)
}

7.3 读取属性

7.3.1 Get — 获取单个属性

# 获取 NetworkManager 的 State 属性
busctl get-property \
  org.freedesktop.NetworkManager \
  /org/freedesktop/NetworkManager \
  org.freedesktop.NetworkManager \
  State

# 输出:u 70  (70 = NM_STATE_CONNECTED_GLOBAL)

# 获取 D-Bus 自身的 Version
busctl get-property \
  org.freedesktop.DBus \
  /org/freedesktop/DBus \
  org.freedesktop.DBus \
  Version

# 使用 gdbus
gdbus call --system \
  --dest org.freedesktop.NetworkManager \
  --object-path /org/freedesktop/NetworkManager \
  --method org.freedesktop.DBus.Properties.Get \
  "org.freedesktop.NetworkManager" "State"

# 使用 dbus-send
dbus-send --system --dest=org.freedesktop.NetworkManager \
  --type=method_call --print-reply \
  /org/freedesktop/NetworkManager \
  org.freedesktop.DBus.Properties.Get \
  string:"org.freedesktop.NetworkManager" \
  string:"State"

7.3.2 GetAll — 获取所有属性

# 获取 NetworkManager 的所有属性
busctl get-property \
  org.freedesktop.NetworkManager \
  /org/freedesktop/NetworkManager \
  org.freedesktop.NetworkManager \
  --all

# 获取 systemd-logind 的 Manager 接口所有属性
busctl get-property \
  org.freedesktop.login1 \
  /org/freedesktop/login1 \
  org.freedesktop.login1.Manager \
  --all

# 使用 gdbus
gdbus call --system \
  --dest org.freedesktop.login1 \
  --object-path /org/freedesktop/login1 \
  --method org.freedesktop.DBus.Properties.GetAll \
  "org.freedesktop.login1.Manager"

7.3.3 Python 读取属性

#!/usr/bin/env python3
"""D-Bus 属性读取示例"""

import dbus

bus = dbus.SystemBus()

# 方式 1:通过 Properties 接口读取
proxy = bus.get_object(
    'org.freedesktop.NetworkManager',
    '/org/freedesktop/NetworkManager'
)
props = dbus.Interface(proxy, 'org.freedesktop.DBus.Properties')

# Get 单个属性
state = props.Get('org.freedesktop.NetworkManager', 'State')
print(f"网络状态: {state}")

# GetAll 获取所有属性
all_props = props.GetAll('org.freedesktop.NetworkManager')
print("\n所有属性:")
for key, value in all_props.items():
    print(f"  {key}: {value}")

# 方式 2:使用 PyDBus 的简写(需要 pydbus 库)
# from pydbus import SystemBus
# bus = SystemBus()
# nm = bus.get("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
# print(nm.State)

7.3.4 GDBus (C) 读取属性

#include <gio/gio.h>

static void read_properties(GDBusConnection *conn) {
    GError *error = NULL;
    
    GDBusProxy *proxy = g_dbus_proxy_new_for_bus_sync(
        G_BUS_TYPE_SYSTEM,
        G_DBUS_PROXY_FLAGS_NONE,
        NULL,
        "org.freedesktop.NetworkManager",
        "/org/freedesktop/NetworkManager",
        "org.freedesktop.NetworkManager",
        NULL,
        &error
    );
    
    if (error) {
        g_printerr("创建代理失败: %s\n", error->message);
        g_error_free(error);
        return;
    }
    
    /* 读取单个属性 */
    GVariant *value = g_dbus_proxy_get_cached_property(proxy, "State");
    if (value) {
        guint32 state = g_variant_get_uint32(value);
        g_print("网络状态: %u\n", state);
        g_variant_unref(value);
    }
    
    /* 读取所有属性 */
    const gchar *const *interfaces = g_dbus_proxy_get_interface_name(proxy);
    GVariant *all_props = g_dbus_proxy_call_sync(
        proxy,
        "GetAll",
        g_variant_new("(s)", "org.freedesktop.NetworkManager"),
        G_DBUS_CALL_FLAGS_NONE,
        -1,
        NULL,
        &error
    );
    
    if (all_props) {
        GVariantIter *iter;
        const gchar *key;
        GVariant *val;
        g_variant_get(all_props, "(a{sv})", &iter);
        g_print("\n所有属性:\n");
        while (g_variant_iter_next(iter, "{&sv}", &key, &val)) {
            gchar *str = g_variant_print(val, FALSE);
            g_print("  %s: %s\n", key, str);
            g_free(str);
            g_variant_unref(val);
        }
        g_variant_iter_free(iter);
        g_variant_unref(all_props);
    }
    
    g_object_unref(proxy);
}

7.4 设置属性

7.4.1 命令行设置

# 设置属性值
busctl set-property \
  org.freedesktop.NetworkManager \
  /org/freedesktop/NetworkManager \
  org.freedesktop.NetworkManager \
  WwanEnabled \
  b false

# 设置 MPRIS 播放器音量
busctl set-property --user \
  org.mpris.MediaPlayer2.spotify \
  /org/mpris/MediaPlayer2 \
  org.mpris.MediaPlayer2.Player \
  Volume \
  d 0.5

7.4.2 Python 设置属性

#!/usr/bin/env python3
"""D-Bus 属性设置示例"""

import dbus

bus = dbus.SessionBus()
proxy = bus.get_object(
    'org.mpris.MediaPlayer2.spotify',
    '/org/mpris/MediaPlayer2'
)
props = dbus.Interface(proxy, 'org.freedesktop.DBus.Properties')

# 获取当前音量
current = props.Get('org.mpris.MediaPlayer2.Player', 'Volume')
print(f"当前音量: {current}")

# 设置新音量
new_volume = min(current + 0.1, 1.0)
props.Set(
    'org.mpris.MediaPlayer2.Player',
    'Volume',
    dbus.Double(new_volume)
)
print(f"新音量: {new_volume}")

7.4.3 只读属性的保护

当尝试设置只读属性时,D-Bus 会返回错误:

# 尝试设置只读属性
busctl set-property \
  org.freedesktop.NetworkManager \
  /org/freedesktop/NetworkManager \
  org.freedesktop.NetworkManager \
  State \
  u 0

# 输出:Call failed: org.freedesktop.DBus.Error.PropertyReadOnly

7.5 PropertiesChanged 信号

7.5.1 信号格式

<signal name="PropertiesChanged">
  <arg name="interface_name" type="s"/>
  <arg name="changed_properties" type="a{sv}"/>
  <arg name="invalidated_properties" type="as"/>
</signal>

参数说明:

参数类型说明
interface_names属性所属的接口名
changed_propertiesa{sv}已变更的属性名→新值
invalidated_propertiesas已失效的属性名(需重新读取)

7.5.2 监听属性变化

# 终端 1:监听所有属性变化
busctl monitor --user \
  --match="type='signal',member='PropertiesChanged'"

# 终端 2:触发属性变化(例如改变音量)

7.5.3 Python 监听属性变化

#!/usr/bin/env python3
"""监听属性变化示例"""

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_properties_changed(interface, changed, invalidated):
    """处理 PropertiesChanged 信号"""
    print(f"\n接口: {interface}")
    
    if changed:
        print("  变更的属性:")
        for key, value in changed.items():
            print(f"    {key} = {value}")
    
    if invalidated:
        print("  失效的属性(需重新读取):")
        for key in invalidated:
            print(f"    {key}")

# 订阅 MPRIS 播放器的属性变化
bus.add_signal_receiver(
    on_properties_changed,
    signal_name='PropertiesChanged',
    dbus_interface='org.freedesktop.DBus.Properties',
    bus_name='org.mpris.MediaPlayer2.spotify',
)

# 通用属性变化监听(不限制 bus_name)
bus.add_signal_receiver(
    on_properties_changed,
    signal_name='PropertiesChanged',
    dbus_interface='org.freedesktop.DBus.Properties',
)

print("监听属性变化中...")
loop.run()

7.5.4 EmitsChangedSignal 注解

属性可以在 XML 中声明其变化信号策略:

<property name="Volume" type="d" access="readwrite">
  <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
<property name="Secret" type="s" access="read">
  <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="invalidates"/>
</property>
<property name="CalculatedValue" type="i" access="read">
  <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
</property>
<property name="ComplexData" type="a{sv}" access="read">
  <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
含义
true属性变化时发送 PropertiesChanged,包含新值
invalidates属性变化时发送信号,但不包含新值(放在 invalidated 列表)
false不发送信号,客户端需要主动轮询
const属性永远不会变化(如固件版本)

7.6 属性缓存

7.6.1 代理缓存机制

GDBus Proxy 默认缓存属性值:

首次读取属性 → Properties.Get() → 缓存值
收到 PropertiesChanged → 更新缓存
后续读取 → 直接返回缓存值(无需网络调用)
/* GDBus Proxy 缓存属性 */

/* 1. 创建代理时启用属性缓存 */
GDBusProxy *proxy = g_dbus_proxy_new_for_bus_sync(
    ...,
    G_DBUS_PROXY_FLAGS_NONE,        /* 默认启用缓存 */
    ...
);

/* 2. 读取缓存的属性值(无网络调用) */
GVariant *value = g_dbus_proxy_get_cached_property(proxy, "State");

/* 3. 手动刷新缓存 */
g_dbus_proxy_call_sync(
    proxy,
    "GetAll",
    g_variant_new("(s)", "org.freedesktop.NetworkManager"),
    G_DBUS_CALL_FLAGS_NONE,
    -1, NULL, NULL
);
/* 代理会自动更新缓存 */

7.6.2 禁用缓存

/* 禁用属性缓存(每次读取都发送 Get 请求) */
GDBusProxy *proxy = g_dbus_proxy_new_for_bus_sync(
    ...,
    G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS |
    G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES,
    ...
);

7.6.3 缓存一致性问题

问题描述解决方案
过期数据服务端修改属性后客户端缓存未更新监听 PropertiesChanged
漏掉信号信号在连接前已发送连接后立即 GetAll 刷新
信号丢失网络问题导致信号丢失定期轮询 + 信号
#!/usr/bin/env python3
"""带定期刷新的属性缓存"""

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()

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

cached_state = {}

def refresh_all():
    """定期刷新所有缓存属性"""
    global cached_state
    try:
        cached_state = props.GetAll('org.freedesktop.NetworkManager')
        print(f"缓存刷新: {len(cached_state)} 个属性")
    except dbus.DBusException as e:
        print(f"刷新失败: {e}")
    return True  # 继续定时器

def on_prop_changed(interface, changed, invalidated):
    """实时更新缓存"""
    if interface == 'org.freedesktop.NetworkManager':
        for key, value in changed.items():
            cached_state[key] = value
            print(f"缓存更新: {key} = {value}")
        for key in invalidated:
            cached_state.pop(key, None)
            print(f"缓存失效: {key}")

# 初始加载
refresh_all()

# 监听变化
bus.add_signal_receiver(
    on_prop_changed,
    signal_name='PropertiesChanged',
    dbus_interface='org.freedesktop.DBus.Properties',
    bus_name='org.freedesktop.NetworkManager',
)

# 每 60 秒刷新一次(兜底)
GLib.timeout_add_seconds(60, refresh_all)

print("属性缓存监控中...")
loop.run()

7.7 实战场景

场景 1:电池状态监控

#!/usr/bin/env python3
"""电池状态监控"""

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

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

def on_upower_changed(interface, changed, invalidated):
    if 'Percentage' in changed:
        pct = changed['Percentage']
        print(f"🔋 电量: {pct}%")
    if 'OnBattery' in changed:
        on_battery = changed['OnBattery']
        print(f"⚡ {'电池供电' if on_battery else '充电中'}")
    if 'TimeToEmpty' in changed:
        tte = changed['TimeToEmpty']
        if tte > 0:
            print(f"⏱️ 剩余时间: {tte // 60} 分钟")

# 监听所有 UPower 设备的属性变化
bus.add_signal_receiver(
    on_upower_changed,
    signal_name='PropertiesChanged',
    dbus_interface='org.freedesktop.DBus.Properties',
    sender_keyword='sender',
    path_keyword='path',
)

# 获取当前电池状态
try:
    bat = bus.get_object('org.freedesktop.UPower', '/org/freedesktop/UPower/devices/battery_BAT0')
    props = dbus.Interface(bat, 'org.freedesktop.DBus.Properties')
    pct = props.Get('org.freedesktop.UPower.Device', 'Percentage')
    print(f"当前电量: {pct}%")
except:
    print("未找到电池设备")

print("监控电池状态中...")
loop.run()

场景 2:获取 NetworkManager 连接信息

#!/bin/bash
# 获取当前网络连接信息

echo "=== 网络状态 ==="
busctl get-property org.freedesktop.NetworkManager \
  /org/freedesktop/NetworkManager \
  org.freedesktop.NetworkManager State

echo ""
echo "=== 主机名 ==="
busctl get-property org.freedesktop.NetworkManager \
  /org/freedesktop/NetworkManager \
  org.freedesktop.NetworkManager Hostname

echo ""
echo "=== 活动连接 ==="
busctl get-property org.freedesktop.NetworkManager \
  /org/freedesktop/NetworkManager \
  org.freedesktop.NetworkManager ActiveConnections

echo ""
echo "=== 所有设备 ==="
busctl call org.freedesktop.NetworkManager \
  /org/freedesktop/NetworkManager \
  org.freedesktop.NetworkManager GetDevices

echo ""
echo "=== 所有属性 ==="
busctl get-property org.freedesktop.NetworkManager \
  /org/freedesktop/NetworkManager \
  org.freedesktop.NetworkManager --all | head -30

本章小结

概念说明
Properties.Get读取单个属性
Properties.GetAll读取所有属性
Properties.Set设置属性值
PropertiesChanged属性变化信号
EmitsChangedSignal属性变化通知策略注解
属性缓存代理层缓存,避免重复网络调用

扩展阅读