# D-Bus
`dbus/*` 是一个纯 Zig 实现的 D-Bus 库,同时具备客户端和服务端
两种角色:连接会话总线或系统总线、调用方法、导出对象、发送和
订阅信号。一次 `[dbus/connect]` 就是一个连接(到总线守护进程的
一个套接字)。它在所属的 VM 线程上通过事件循环进行分派,因此
导出的处理函数和订阅闭包都作为 Deft 代码内联运行,方法调用返回
可与 `[await]` 集成的 future。
没有 libdbus 依赖,也无需编写总线配置 —— 默认连接会话总线,使用
`%{ :system true }` 则连接系统总线。
## 连接与名称
D-Bus 将连接与名称区分开来:
- 每个连接都会由总线分配一个唯一名称(`:1.5`、`:1.42`……),
可通过 `[dbus/unique-name $conn]` 读取。唯一名称属于单个连接,
重连后会改变 —— 它们适合用于定位特定对端,而不是稳定的服务。
- 众所周知名称(well-known name,如 `org.freedesktop.Notifications`、
`org.deft.myapp`)是总线层面的别名,同一时刻至多由一个连接
拥有。调用方寻址的是名称而非进程,总线会把每次调用路由到
当前拥有该名称的连接。
总线上可以有任意多个连接,其中任意多个都可以调用某个众所周知
名称 —— 但只有一个能成为它的主所有者。正是这条单一所有者规则
让人意外:两个守护进程无法同时服务 `org.freedesktop.Notifications`。
先到者拥有它;后到者要么排队等待,要么被拒绝,而每次 `Notify`
调用都会继续发给在位者。
连接断开时(`dbus/disconnect`、GC 终结器或进程退出),它会失去
自己的名称,此时总线会提升下一个排队的请求者。
## 请求名称
set conn [dbus/connect] set rc [await [dbus/register-name $conn "org.deft.myapp" 7]]
`[dbus/register-name $conn "name" ?flags]` 返回一个 future,解析为
数值型的 `RequestName` 结果:
| 结果 | 含义 |
|---|---|
| `1` | 主所有者 —— 调用将路由到这里 |
| `2` | 在队列中 —— 已存在另一个所有者 |
| `3` | 已存在且未入队 —— 已存在另一个所有者且设置了 `DO_NOT_QUEUE` |
| `4` | 已经是所有者 |
可选标志是一个位掩码:
| 标志 | 值 | 含义 |
|---|---|---|
| `ALLOW_REPLACEMENT` | `1` | 带 `REPLACE_EXISTING` 的另一个请求者可以从我们这里取走该名称 |
| `REPLACE_EXISTING` | `2` | 尝试从当前所有者手中取走该名称 |
| `DO_NOT_QUEUE` | `4` | 若名称无法获取,以结果 `3` 失败而非排队 |
替换是相互的。 只有当当前所有者以 `ALLOW_REPLACEMENT` 请求
该名称时,`REPLACE_EXISTING` 才会成功。这是 D-Bus 有意设计的安全
属性(名称劫持问题在 CVE-2008-4311 中修复):未选择加入的服务
无法被外部顶替,无论新来者传入什么标志。夺走其名称的唯一方式是
让它断开连接或退出。
有用的组合:
| 标志 | 行为 |
|---|---|
| `0` | 经典服务:先到者胜,后到者静默排队 |
| `4` | 快速失败:名称被占用时返回 `3` 而非排队 |
| `6` | 若允许则接管,否则报告 `3` 且不静默排队 |
| `7` | 同 `6`,并允许未来的守护进程替换我们 |
“已拥有”与“被占用”之间的区别,对任何消费共享名称的一方都很
重要。不使用 `DO_NOT_QUEUE` 时,请求可能以结果 `2` 成功,然后
永远等待在一个永不退出的所有者后面 —— 连接看起来健康,却收不到
任何东西。
### 完整示例:通知守护进程
在 GNOME 会话中,GNOME Shell 的通知服务拥有
`org.freedesktop.Notifications`,并且未请求
`ALLOW_REPLACEMENT`。因此以标志 `6` 或 `7` 请求的 deft 守护进程
会得到结果 `3`,而不是接管 —— `notify-send` 仍然发给 GNOME。
deft 守护进程可以通过直接调用总线来报告谁持有该名称:
[await [dbus/call $conn "org.freedesktop.DBus" "/org/freedesktop/DBus" \ "org.freedesktop.DBus" "GetNameOwner" "s" "org.freedesktop.Notifications"]] [await [dbus/call $conn "org.freedesktop.DBus" "/org/freedesktop/DBus" \ "org.freedesktop.DBus" "GetConnectionUnixProcessID" "s" $owner]]
要移交该名称,请停止在位者(例如杀掉进程,或屏蔽其 D-Bus 激活),
然后重新请求;定期重试的守护进程会在名称变为空闲的那一刻接管。
## Deft 实例之间的自定义名称
众所周知名称不过是总线命名空间中的字符串,因此 deft 进程可以定义
自己的服务 —— `org.deft.<app>`、`com.example.<service>.<instance>`,
只要合适即可。这是不同的 OS 进程之间以及与外部程序互操作的路径。
服务端注册名称并导出接口:
# server.dft — run in its own deft process set conn [dbus/connect] set rc [await [dbus/register-name $conn "org.deft.notify.1" 7]] [dbus/export $conn "/org/deft/notify" "org.deft.Notify" %{ "Send" {|text| [handle-notification $text]} }] [vwait]
客户端调用它:
# client.dft set conn [dbus/connect] [await [dbus/call $conn "org.deft.notify.1" "/org/deft/notify" \ "org.deft.Notify" "Send" "s" "hello"]]
当多个实例必须共存时,给每个实例取不同的名称(`org.deft.worker.1`、
`org.deft.worker.2`……);当它们共享一个名称时,用标志 `6`/`7`
控制替换。实例可以通过向总线查询已有名称来发现彼此:
set names [await [dbus/call $conn "org.freedesktop.DBus" "/org/freedesktop/DBus" \ "org.freedesktop.DBus" "ListNames"]]
若想用扇出取代请求/响应,可发送信号并按匹配规则订阅:
# publisher [dbus/emit $conn "/org/deft/notify" "org.deft.Notify" "Posted" "s" $text] # subscriber — closure receives %{ :sender :path :interface :member :args } [dbus/subscribe $conn "type='signal',interface='org.deft.Notify',member='Posted'" {|ev| [on-posted $ev~args]}]
## 同进程内的替代方案
如果所谓“实例”是同一进程内的运行时 —— TUI 面板、由 `runtime/spawn`
派生的子运行时、集群工作进程 —— 那你几乎不会想用 D-Bus。这些
运行时已经共享平台层:
- `pub/*` 是贯穿运行时层级的共享代理,采用普通值投递 ——
见 发布/订阅。
- `@rpc` + `rpc` 提供按运行时名称寻址的一对一类型化调用 ——
见 RPC。
- `config/*` 是共享的反应式键/值注册表。
当对端是独立的 OS 进程,或者外部客户端(桌面 shell、`notify-send`、
系统服务)必须通过众所周知名称访问你时,D-Bus 才是正确的工具。
## 生命周期与内省
- 名称会一直持有到连接关闭。`dbus/disconnect`、资源的 GC 终结器
以及进程退出都会释放连接拥有的所有名称。
- `[dbus/info $conn]` 报告 `%{ :state :unique-name :pending :subs :exports }`。
- `[dbus/unique-name $conn]` 在 `HELLO` 完成后返回该连接的 `:1.x` 名称。
- 总线对象(`org.freedesktop.DBus`,路径 `/org/freedesktop/DBus`)
会应答 `ListNames`、`GetNameOwner`、`NameHasOwner` 和
`GetConnectionUnixProcessID` —— 足以在无额外依赖的情况下构建
服务发现与诊断。
- `NameOwnerChanged` 是总线信号:订阅它(为关心的名称加 `arg0`
匹配),即可在服务出现、消失或易主的那一刻获知。
## 快速参考
| 形式 | 用途 |
|---|---|
| `[dbus/connect ?opts]` | 连接会话总线(默认)或系统总线(`:system true`) |
| `[dbus/call $c dest path iface method ?sig @args]` | 方法调用;`await` 其 future |
| `[dbus/send $c dest path iface method ?sig @args]` | 发出即忘的调用 |
| `[dbus/register-name $c "name" ?flags]` | 请求众所周知名称;`await` 结果码 |
| `[dbus/export $c "path" "iface" %{ "Method" $fn }]` | 提供方法(服务端角色) |
| `[dbus/emit $c "path" "iface" "Signal" ?sig @args]` | 广播信号 |
| `[dbus/subscribe $c "match rule" $closure]` | 在匹配的信号上运行 `$closure` |
| `[dbus/disconnect $c]` | 关闭;释放拥有的名称 |