07 - D-Bus 属性系统
第 07 章:D-Bus 属性系统
7.1 属性概述
属性(Property)是 D-Bus 对象上的 命名值,提供结构化的状态访问接口。与方法调用不同,属性有明确的读写语义:
| 对比 | 方法 | 属性 |
|---|---|---|
| 语义 | 执行操作/获取数据 | 读/写状态值 |
| 读取 | 方法调用 + 返回值 | Properties.Get |
| 写入 | 方法调用(可能有副作用) | Properties.Set |
| 变化通知 | 需自行实现信号 | 标准 PropertiesChanged |
| 批量读取 | 多次调用 | Properties.GetAll |
几乎所有 D-Bus 服务都通过属性暴露状态信息:
| 服务 | 属性示例 |
|---|---|
| NetworkManager | State(网络状态)、ActiveConnections |
| systemd-logind | BlockInhibited(锁定策略) |
| UPower | Percentage(电池百分比)、OnBattery |
| BlueZ | Address(蓝牙地址)、Connected |
| MPRIS 播放器 | PlaybackStatus、Metadata、Volume |
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_name | s | 属性所属的接口名 |
changed_properties | a{sv} | 已变更的属性名→新值 |
invalidated_properties | as | 已失效的属性名(需重新读取) |
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 | 属性变化通知策略注解 |
| 属性缓存 | 代理层缓存,避免重复网络调用 |