
Linux | Go! D-Bus!
上一篇提到D-Bus是一种统一桌面程序通信的手段,可以用来调用其他程序的方法,也可以给其他程序提供服务。
前两天刚刚好有微信群的朋友问到说怎么用,也写好了一个例子,但是比较粗略,今天稍微解释和改进一下这个例子。
目标
写一个服务,提供计数器的功能。
设计
我们的计数器应该可以给不同的程序提供不同的计数器,也可以给几个程序共享一个计数器。
所以需要提供两个接口,一个创建共享的计数器,一个创建私有的计数器。两个接口都会创建一个对象,但是只有共享计数器的这个会把对象发布到路径上。
计数器本身只能增加或者减少一定的值,就需要一个可以管计数器的类,我们就把计数器叫做Counter,把管计数器的叫Controller吧。
既然我博客域名是blog.jeffthecoder.xyz,就用xyz.jeffthecoder.Counter.Controller和xyz.jeffthecoder.Counter作为类名吧。
根据我们上一篇说的几个要素来分析一下我们需要什么:
- 是哪根bus上的服务
- 提供服务的连接名是啥
- 提供服务的对象的路径是啥
- 需要对象上的哪个接口
- 需要的是函数、信号还是属性
- 名字叫啥
大致来说,我们就需要:
- 随意选一个bus上,system和session都可以
- 连接名不重要,但是为了统一,还是请求一个xyz.jeffthecoder.Counter
- 在/xyz/jeffthecoder/Counter上注册一个xyz.jeffthecoder.Counter.Controller接口的对象:
- xyz.jeffthecoder.Counter.Controller
- 方法
- 新建私有计数器
- 在DBus上导出一个路径是/xyz/jeffthecoder/Counter/{id}的xyz.jeffthecoder.Counter对象
- 新建共享计数器
- 在DBus上导出一个路径是/xyz/jeffthecoder/Counter/{id}的xyz.jeffthecoder.Counter对象
- 更新到Controller的children里面
- 新建私有计数器
- 方法
- xyz.jeffthecoder.Counter.Controller
- /xyz/jeffthecoder/Counter/{id}
- xyz.jeffthecoder.Counter
- 方法
- 加int64的delta
- 属性
- Value int64
- 方法
- xyz.jeffthecoder.Counter
- 在/xyz/jeffthecoder/Counter上注册一个xyz.jeffthecoder.Counter.Controller接口的对象:
- 连接名不重要,但是为了统一,还是请求一个xyz.jeffthecoder.Counter
实现
连接 dbus
我们起手先简单的连接一下dbus:
package main
import (
"flag"
"log"
"os"
"os/signal"
"syscall"
"github.com/godbus/dbus/v5"
)
var (
fSystemBus = flag.Bool("system", false, "connect to system bus")
fBusName = flag.String("name", "xyz.jeffthecoder.Counter", "dbus name")
)
func init() {
flag.Parse()
}
func main() {
var (
bus *dbus.Conn
err error
)
if *fSystemBus {
bus, err = dbus.SystemBus()
} else {
bus, err = dbus.SessionBus()
}
if err != nil {
log.Fatal(err)
}
if reply, err := bus.RequestName(*fBusName, dbus.NameFlagAllowReplacement|dbus.NameFlagAllowReplacement); err != nil {
log.Fatal("failure during requesting name: ", err)
} else if reply != dbus.RequestNameReplyPrimaryOwner {
log.Fatal("unexpected result when requesting name: ", reply)
}
defer bus.ReleaseName(*fBusName)
busSigChan := make(chan *dbus.Signal)
bus.Signal(busSigChan)
procSigChan := make(chan os.Signal)
signal.Notify(procSigChan, syscall.SIGINT)
MainLoop:
for {
select {
case sig := <-busSigChan:
log.Println("signal from dbus: ", sig)
case sig := <-procSigChan:
log.Println("signal from system: ", sig)
switch sig {
case syscall.SIGINT:
break MainLoop
}
}
}
}
代码比较好懂,流水帐式的稍微解释一下(其他的包有问题建议自己查一下文档)
一开始根据选项。连接SystemBus或者登录用户的SessionBus:
if *fSystemBus {
bus, err = dbus.SystemBus()
} else {
bus, err = dbus.SessionBus()
}
然后给自己的这根连接请求一个名字,这个名字我默认就用xyz.jeffthecoder.Counter,万一有程序占着这个名字了(比如说你运行了几遍)我们也退出程序:
if reply, err := bus.RequestName(*fBusName, dbus.NameFlagAllowReplacement|dbus.NameFlagAllowReplacement); err != nil {
log.Fatal("failure during requesting name: ", err)
} else if reply != dbus.RequestNameReplyPrimaryOwner {
log.Fatal("unexpected result when requesting name: ", reply)
}
defer bus.ReleaseName(*fBusName)
建两个channel,存放系统的Signal和DBus的Signal,系统Signal就比如说键盘ctrl c就属于系统信号的中断信号。然后用一个循环来不停的处理这些信号(还记得事件循环吗?):
busSigChan := make(chan *dbus.Signal)
bus.Signal(busSigChan)
procSigChan := make(chan os.Signal)
signal.Notify(procSigChan, syscall.SIGINT)
MainLoop:
for {
select {
case sig := <-busSigChan:
log.Println("signal from dbus: ", sig)
case sig := <-procSigChan:
log.Println("signal from system: ", sig)
switch sig {
case syscall.SIGINT:
break MainLoop
}
}
}
写一个空Controller
type Controller struct {
bus *dbus.Conn
}
func NewController(busConn *dbus.Conn) *Controller {
return &Controller{
bus: busConn,
}
}
func (controller *Controller) Counter(initValue int64) (string, *dbus.Error) {
id := uuid.New().String()
path := fmt.Sprintf("/xyz/jeffthecoder/Counter/%v", id)
return path, nil
}
这里肯定都没问题。那个path会用来做具体计数器的对象路径。
把Controller导出到DBus里面
我们需要改一下main函数
// ...
// if reply, err := bus.RequestName(*fBusName, dbus.NameFlagAllowReplacement|dbus.NameFlagAllowReplacement); err != nil {
// log.Fatal("failure during requesting name: ", err)
// } else if reply != dbus.RequestNameReplyPrimaryOwner {
// log.Fatal("unexpected result when requesting name: ", reply)
// }
// defer bus.ReleaseName(*fBusName)
controller := NewController(bus)
if err := bus.Export(controller, "/xyz/jeffthecoder/Counter", "xyz.jeffthecoder.Counter.Manager"); err != nil {
log.Fatal("failed to export object: ", err)
}
// busSigChan := make(chan *dbus.Signal)
// if err := bus.AddMatchSignal(
// dbus.WithMatchInterface("xyz.jeffthecoder.Counter"),
// dbus.WithMatchMember("Changed"),
// ); err != nil {
// log.Fatal("failed to match signal")
// }
// bus.Signal(busSigChan)
// ...
bus.Export函数就会在我们这个连接上导出controller这个对象。
可是我们并不能看到这个对象,D-Feet甚至给我们报了个错
为什么呢?因为大部分的工具都需要用org.freedesktop.DBus.Introspectable这个接口才能看到具体的类型定义,而我们并没有导出这个接口。但是这个时候已经可以正常调用我们的对象了。
有一些对象,是不希望别人可以直接看到的,但是又希望别人可以调用,怎么办呢?解决办法就是像这样不导出Introspectable。比较常见的例子就是NetworkManager常用的SecretAgent了。
那如果我们想在D-Feet或者其他自动解析Introspectable的工具中看到这个对象,要怎么办呢?好办啊,在同一个路径上再导出一个Introspectable接口就好了。借reflect的福,这个接口内容也是godbus可以自动生成的:
// ...
// if reply, err := bus.RequestName(*fBusName, dbus.NameFlagAllowReplacement|dbus.NameFlagAllowReplacement); err != nil {
// log.Fatal("failure during requesting name: ", err)
// } else if reply != dbus.RequestNameReplyPrimaryOwner {
// log.Fatal("unexpected result when requesting name: ", reply)
// }
// defer bus.ReleaseName(*fBusName)
// controller := NewController(bus)
// if err := bus.Export(controller, "/xyz/jeffthecoder/Counter", "xyz.jeffthecoder.Counter.Manager"); err != nil {
// log.Fatal("failed to export object: ", err)
// }
if err := bus.Export(introspect.NewIntrospectable(&introspect.Node{
Interfaces: []introspect.Interface{
{
Name: "xyz.jeffthecoder.Counter.Manager",
Methods: introspect.Methods(controller),
},
},
}), "/xyz/jeffthecoder/Counter",
"org.freedesktop.DBus.Introspectable"); err != nil {
}
// busSigChan := make(chan *dbus.Signal)
// if err := bus.AddMatchSignal(
// dbus.WithMatchInterface("xyz.jeffthecoder.Counter"),
// dbus.WithMatchMember("Changed"),
// ); err != nil {
// log.Fatal("failed to match signal")
// }
// bus.Signal(busSigChan)
// ...
再用D-Feet看的时候,就可以正常用这个对象了:
写一个计数器
import (
"github.com/godbus/dbus/v5"
"github.com/godbus/dbus/v5/prop"
"sync"
)
type Counter struct {
path dbus.ObjectPath
controller *Controller
value int64
locker sync.Locker
}
func (counter *Counter) Add(delta int64) (int64, *dbus.Error) {
counter.locker.Lock()
defer counter.locker.Unlock()
counter.value += delta
if _, err := prop.Export(counter.controller.bus, counter.path, map[string]map[string]*prop.Prop{
"xyz.jeffthecoder.Counter": {
"value": &prop.Prop{
Value: counter.value,
Writable: false,
},
},
}); err != nil {
return 0, &dbus.Error{}
}
return counter.value, nil
}
这里也不难,新建了一个类型。
因为godbus的两个调用可能会并发,存在线程安全问题,所以我们用到了sync包的锁。本来也可以用atomic包的原子操作,但是考虑到增加delta和更新prop也应该同时,算一个不可分割的整体,想了想还是用锁了。
然后我们通过dbus/prop包,在之前这个 bus连接上 的 counter的对象路径 上,为xyz.jeffthecoder.Counter这个接口导出了一个属性value,大小为新值,不可写入。
最后返回了新值,这个Counter的功能就算完整了。
在Controller中导出Counter
在Controller中导出Counter意味着Controller要记录所有Counter的路径和信息,我们通过map和locker来完成:
// type Controller struct {
// bus *dbus.Conn
counters map[string]*Counter
locker sync.Locker
// }
// func NewController(busConn *dbus.Conn) *Controller {
// return &Controller{
// bus: busConn,
counters: make(map[string]*Counter),
locker: &sync.Mutex{},
// }
// }
// func (controller *Controller) Counter(initValue int64) (string, *dbus.Error) {
id := strings.ReplaceAll(uuid.New().String(), "-", "")
// path := fmt.Sprintf("/xyz/jeffthecoder/Counter/%v", id)
counter := &Counter{path: dbus.ObjectPath(path), controller: controller, value: initValue, locker: &sync.Mutex{}}
if err := controller.bus.Export(
counter,
dbus.ObjectPath(path),
"xyz.jeffthecoder.Counter",
); err != nil {
return "", &dbus.Error{}
}
if err := controller.bus.Export(introspect.NewIntrospectable(&introspect.Node{
Name: path,
Interfaces: []introspect.Interface{
{
Name: "xyz.jeffthecoder.Counter",
Methods: introspect.Methods(counter),
Properties: []introspect.Property{
{
Name: "value",
Type: "x",
Access: "read",
},
},
},
},
}), dbus.ObjectPath(path),
"org.freedesktop.DBus.Introspectable"); err != nil {
return "", &dbus.Error{}
}
if _, err := prop.Export(controller.bus, dbus.ObjectPath(path), map[string]map[string]*prop.Prop{
"xyz.jeffthecoder.Counter": {
"value": &prop.Prop{
Value: initValue,
Writable: false,
},
},
}); err != nil {
return "", &dbus.Error{}
}
// return path, nil
// }
类似于前面导出Controller,但是稍微有一点点不一样的地方。
我发现路径上有减号"-“的时候,导出函数时会卡死,原因还没有特别明白。
导出counter没有任何差别:
counter := &Counter{path: dbus.ObjectPath(path), controller: controller, value: initValue, locker: &sync.Mutex{}}
if err := controller.bus.Export(
counter,
dbus.ObjectPath(path),
"xyz.jeffthecoder.Counter",
); err != nil {
return "", &dbus.Error{}
}
但是导出counter的Introspectable接口的时候,多出来了value属性:
if err := controller.bus.Export(introspect.NewIntrospectable(&introspect.Node{
Name: path,
Interfaces: []introspect.Interface{
{
Name: "xyz.jeffthecoder.Counter",
Methods: introspect.Methods(counter),
Properties: []introspect.Property{
{
Name: "value",
Type: "x",
Access: "read",
},
},
},
},
}), dbus.ObjectPath(path),
"org.freedesktop.DBus.Introspectable"); err != nil {
return "", &dbus.Error{}
}
顺便我们更新了Counter对象上value属性的值:
if _, err := prop.Export(controller.bus, dbus.ObjectPath(path), map[string]map[string]*prop.Prop{
"xyz.jeffthecoder.Counter": {
"value": &prop.Prop{
Value: initValue,
Writable: false,
},
},
}); err != nil {
return "", &dbus.Error{}
}
然后就可以正常用我们的Counter了:
但是就像图里面我尝试的东西一样,我在dbus里面新建了counter,但是即使刷新以后,dbus也不显示这个counter,为什么呢?
探索了一番以后我发现,我还需要更新Controller的Introspectable接口,提供Children的列表:
// if _, err := prop.Export(controller.bus, dbus.ObjectPath(path), map[string]map[string]*prop.Prop{
// "xyz.jeffthecoder.Counter": {
// "value": &prop.Prop{
// Value: initValue,
// Writable: false,
// },
// },
// }); err != nil {
// return "", &dbus.Error{}
// }
controller.locker.Lock()
controller.counters[id] = counter
controller.locker.Unlock()
node := introspect.Node{
Name: "/xyz/jeffthecoder/Counter",
Interfaces: []introspect.Interface{
{
Name: "xyz.jeffthecoder.Counter.Manager",
Methods: introspect.Methods(controller),
},
},
}
for k := range controller.counters {
node.Children = append(node.Children, introspect.Node{
Name: fmt.Sprint(k),
})
}
if err := controller.bus.Export(introspect.NewIntrospectable(&node), "/xyz/jeffthecoder/Counter",
"org.freedesktop.DBus.Introspectable"); err != nil {
return "", &dbus.Error{}
}
// return path, nil
我们在counters里面存了各个counter的id,并且重新发布了controller的introspectable接口,于是……
销毁Counter
有的同学说,那Counter岂不是越攒越多了,那我们加个Destroy接口吧
func (controller *Controller) Destroy(id string) *dbus.Error {
controller.locker.Lock()
defer controller.locker.Unlock()
if _, ok := controller.counters[id]; !ok {
return nil
}
delete(controller.counters, id)
path := fmt.Sprintf("/xyz/jeffthecoder/Counter/%v", id)
// unexport
if err := controller.bus.Export(
nil,
dbus.ObjectPath(path),
"xyz.jeffthecoder.Counter",
); err != nil {
return &dbus.Error{}
}
if err := controller.bus.Export(
nil,
dbus.ObjectPath(path),
"org.freedesktop.DBus.Introspectable"); err != nil {
return &dbus.Error{}
}
// re-export controller's introspectable interface
node := introspect.Node{
Name: "/xyz/jeffthecoder/Counter",
Interfaces: []introspect.Interface{
{
Name: "xyz.jeffthecoder.Counter.Manager",
Methods: introspect.Methods(controller),
},
},
}
for k := range controller.counters {
node.Children = append(node.Children, introspect.Node{
Name: fmt.Sprint(k),
})
}
if err := controller.bus.Export(introspect.NewIntrospectable(&node), "/xyz/jeffthecoder/Counter",
"org.freedesktop.DBus.Introspectable"); err != nil {
return &dbus.Error{}
}
return nil
}
私有计数器
有的同学说,我就想建一个我这个连接才能使用的计数器,连接断开以后删除计数器。
也简单,我们导出对象,但是不更新Controller就好了。但是说不定其他人也能调用呢?我翻看了一下godbus的代码,发现函数可以这样定义:
func (controller *Controller) PrivateCounter(sender dbus.Sender, initValue int64) (string, *dbus.Error) {
// ...
}
godbus在函数参数中发现dbus.Sender类型参数后,会在这个参数中填上发送者的名字。
依靠这种能力,可以非常简单的给Counter加上控制。新加入的片段类似于这样:
func (controller *Controller) PrivateCounter(sender dbus.Sender, initValue int64) (string, *dbus.Error) {
id := strings.ReplaceAll(uuid.New().String(), "-", "")
path := fmt.Sprintf("/xyz/jeffthecoder/Counter/%v/%v", string(sender), id)
counter := &PrivateCounter{
Counter: Counter{path: dbus.ObjectPath(path), controller: controller, value: initValue, locker: &sync.Mutex{}},
Sender: sender,
}
if err := controller.bus.Export(
counter,
dbus.ObjectPath(path),
"xyz.jeffthecoder.Counter",
); err != nil {
return "", &dbus.Error{}
}
if err := controller.bus.Export(introspect.NewIntrospectable(&introspect.Node{
Name: path,
Interfaces: []introspect.Interface{
{
Name: "xyz.jeffthecoder.Counter",
Methods: introspect.Methods(counter),
Properties: []introspect.Property{
{
Name: "value",
Type: "x",
Access: "read",
},
},
},
},
}), dbus.ObjectPath(path),
"org.freedesktop.DBus.Introspectable"); err != nil {
return "", &dbus.Error{}
}
if _, err := prop.Export(controller.bus, dbus.ObjectPath(path), map[string]map[string]*prop.Prop{
"xyz.jeffthecoder.Counter": {
"value": &prop.Prop{
Value: initValue,
Writable: false,
},
},
}); err != nil {
return "", &dbus.Error{}
}
return path, nil
}
type PrivateCounter struct {
Counter
Sender dbus.Sender
}
func (privateCounter *PrivateCounter) Add(sender dbus.Sender, delta int64) (int64, *dbus.Error) {
if sender == privateCounter.Sender {
return privateCounter.Counter.Add(delta)
}
return 0, &dbus.ErrMsgNoObject
}
自动销毁私有计数器
和刚才的Destroy问题类似,godbus中注册的object会越来越多,作为第三方又很难去销毁一个私有的counter。怎么办呢?我们可以监控sender消失的事件。
我们通过ps aux | grep ipython
拿到刚才python进程的pid,是34270,d-feet里面对应的连接名叫:1.89。
通过dbus-monitor
,运行的同时我们退出这个进程,可以看到中间有一个事件是
signal time=1606555271.716596 sender=org.freedesktop.DBus -> destination=:1.89 serial=5 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameLost
string ":1.89"
signal time=1606555271.716610 sender=org.freedesktop.DBus -> destination=(null destination) serial=522 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameOwnerChanged
string ":1.89"
string ":1.89"
string ""
其中NameLost看起来是最理想的,但是destination是ipython这个进程自己,说明counter这一侧捕获不到这个signal。那我们只能选择NameOwnerChanged这个广播出去(destination=null)的signal了
也就是说我们监控sender的NameList信号,就可以知道,对方下线了。这个时候,我们在main函数里面的那个MainLoop就有用了,为了把有关的代码移动的更集中一些,我们把这部分代码移动到Controller内:
func (controller *Controller) Run() {
busSigChan := make(chan *dbus.Signal)
controller.bus.Signal(busSigChan)
procSigChan := make(chan os.Signal)
signal.Notify(procSigChan, syscall.SIGINT)
MainLoop:
for {
select {
case sig := <-busSigChan:
log.Println("signal from dbus: ", sig)
case sig := <-procSigChan:
log.Println("signal from system: ", sig)
switch sig {
case syscall.SIGINT:
break MainLoop
}
}
}
}
func main() {
// ...
// if err := bus.Export(introspect.NewIntrospectable(&introspect.Node{
// Interfaces: []introspect.Interface{
// {
// Name: "xyz.jeffthecoder.Counter.Manager",
// Methods: introspect.Methods(controller),
// },
// },
// }), "/xyz/jeffthecoder/Counter",
// "org.freedesktop.DBus.Introspectable"); err != nil {
// }
controller.Run()
}
然后我们在PrivateCounter里面,记录一下哪个sender建了哪些counter,加上监听的选项
// type Controller struct {
// bus *dbus.Conn
// counters map[string]*Counter
// locker sync.Locker
privateCounter map[string][]string
privateLocker sync.Locker
// }
// func NewController(busConn *dbus.Conn) *Controller {
// return &Controller{
// bus: busConn,
// counters: make(map[string]*Counter),
// locker: &sync.Mutex{},
privateCounter: make(map[string][]string),
privateLocker: &sync.Mutex{},
// }
// }
// func (controller *Controller) PrivateCounter(sender dbus.Sender, initValue int64) (string, *dbus.Error) {
// ...
// if _, err := prop.Export(controller.bus, dbus.ObjectPath(path), map[string]map[string]*prop.Prop{
// "xyz.jeffthecoder.Counter": {
// "value": &prop.Prop{
// Value: initValue,
// Writable: false,
// },
// },
// }); err != nil {
// return "", &dbus.Error{}
// }
controller.privateLocker.Lock()
if arr, ok := controller.privateCounter[string(sender)]; ok {
controller.privateCounter[string(sender)] = append(arr, id)
} else {
controller.privateCounter[string(sender)] = []string{id}
}
controller.privateLocker.Unlock()
if err := controller.bus.AddMatchSignal(
dbus.WithMatchObjectPath("/org/freedesktop/DBus"),
dbus.WithMatchInterface("org.freedesktop.DBus"),
dbus.WithMatchSender("org.freedesktop.DBus"),
dbus.WithMatchMember("NameLost"),
dbus.WithMatchOption("arg1", string(sender)),
); err != nil {
log.Printf("warning: failed to add match signal for %v, there will be memory leak in the future: %v", sender, err)
}
// return path, nil
// }
在Run里面,收集到signal后,unexport一下就好了
// func (controller *Controller) Run() {
// busSigChan := make(chan *dbus.Signal)
// controller.bus.Signal(busSigChan)
// procSigChan := make(chan os.Signal)
// signal.Notify(procSigChan, syscall.SIGINT)
// MainLoop:
// for {
// select {
// case sig := <-busSigChan:
// log.Println("signal from dbus: ", sig)
if sig.Name == "org.freedesktop.DBus.NameOwnerChanged" && len(sig.Body) >= 1 {
if sender, ok := sig.Body[0].(string); ok {
controller.privateLocker.Lock()
if counters, ok := controller.privateCounter[sender]; ok {
for _, counter := range counters {
path := fmt.Sprintf("/xyz/jeffthecoder/Counter/%v/%v", string(sender), counter)
_ = controller.bus.Export(nil, dbus.ObjectPath(path), "xyz.jeffthecoder.Counter")
_ = controller.bus.Export(nil, dbus.ObjectPath(path), "org.freedesktop.DBus.Introspectable")
}
delete(controller.privateCounter, sender)
}
controller.privateLocker.Unlock()
_ = controller.bus.RemoveMatchSignal(
dbus.WithMatchObjectPath("/org/freedesktop/DBus"),
dbus.WithMatchInterface("org.freedesktop.DBus"),
dbus.WithMatchSender("org.freedesktop.DBus"),
dbus.WithMatchMember("NameLost"),
dbus.WithMatchOption("arg1", sender),
)
}
}
// case sig := <-procSigChan:
// log.Println("signal from system: ", sig)
// switch sig {
// case syscall.SIGINT:
// break MainLoop
// }
// }
// }
// }
完美。
代码在Gist上:Controller.go