UP | HOME

go中使用dbus和gsettings

目录

dbus in go

使用 dbus 包

连接 dbus

dbus.SessionBus()
dbus.SystemBus()

调用方法

比如调用 dbus-daemon 提供的对象的一个方法,服务名: org.freedesktop.DBus, 路径: /org/freedesktop/DBus ,接口名: org.freedesktop.DBus , 方法名: GetNameOwner , 此方法输入参数是一个字符串,返回参数也是一个字符串。

sessionBus, err := dbus.SessionBus()
obj := sessionBus.Object("org.freedesktop.DBus", "/org/freedesktop/DBus")
var ret string
err = obj.Call("org.freedesktop.DBus.GetNameOwner", 0, "com.deepin.dde.daemon.Dock").Store(&ret)

需要注意传给 obj.Call 方法的第一个参数 method 的值是接口名 + "." + 方法名。

在处理没有返回值的方法时,可以不调用 Store 方法,而直接检查 Err 字段。

获取和设置属性

属性的获取和设置本质也是方法调用。

获取属性

获取单个属性是调用对象的 org.freedesktop.DBus.Properties 接口的 Get 方法,此方法传入参数是接口名和属性名,返回值是属性值,类型为 dbus.Variant

下面的代码是要获取 org.freedesktop.DBus 接口的 Features 属性,此属性的类型是 []string

sessionBus, err := dbus.SessionBus()
obj := sessionBus.Object("org.freedesktop.DBus", "/org/freedesktop/DBus")
var ret dbus.Variant
err = obj.Call("org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.DBus", "Features").Store(&ret)
v := ret.Value().([]string)

ret 中获取内部的值,需要调用 Value 方法,获取 interface{} 类型的内部值,再使用类型断言 .([]string)

一次获取对象在某个接口下的全部属性,可调用 Get 方法旁边的 GetAll 方法。

设置属性

设置属性是调用对象的 org.freedesktop.DBus.Properties 接口的 Set 方法,此方法的传入参数是接口名、属性名、属性值,没有返回值。

下面的代码是要设置 org.freedesktop.DBus 接口的 Features 属性。

sessionBus, err := dbus.SessionBus()
obj := sessionBus.Object("org.freedesktop.DBus", "/org/freedesktop/DBus")
propVal := dbus.MakeVariant([]string{"abc"})
err = obj.Call("org.freedesktop.DBus.Properties.Set", 0, "org.freedesktop.DBus", "Features", propVal).Err

传入参数中属性值是 dbus.Variant 类型,制造这种类型可用 dbus.MakeVariant 方法。

监控信号

调用 AddMatch 方法添加监控规则,如果不需要监控了,就调用 RemoveMatch 方法去掉监控规则。

go 的 dbus 库提供了方便的方法: AddMatchSignalRemoveMatchSignal

下面的代码是要监控 Session Bus 上的 /org/freedesktop/Bus 对象的 org.freedesktop.DBus 接口的 NameOwnerChanged 信号。

sessionBus, err := dbus.SessionBus()
err = sessionBus.BusObject().AddMatchSignal("org.freedesktop.DBus", "NameOwnerChanged",
    dbus.WithMatchObjectPath("/org/freedesktop/DBus")).Err

signalCh := make(chan  *dbus.Signal, 10)
sessionBus.Signal(signalCh)
go func() {
    for {
        select {
        case sig := <-signalCh:
            log.Printf("sig: %#v\n", sig)
            if sig.Path == "/org/freedesktop/DBus" &&
             sig.Name == "org.freedesktop.DBus.NameOwnerChanged" {
                var name string
                var oldOwner string
                var newOwner string
                err = dbus.Store(sig.Body, &name, &oldOwner, &newOwner)
                log.Printf("%s %s %s\n", name, oldOwner, newOwner)
            }
        }
    }
}()

time.Sleep(100*time.Second)
err = sessionBus.BusObject().RemoveMatchSignal("org.freedesktop.DBus", "NameOwnerChanged",
    dbus.WithMatchObjectPath("/org/freedesktop/Bus")).Err
sessionBus.RemoveSignal(signalCh)

将调用 sessionBus.Signal 方法注册 signalCh 通道,以此来通过该通道接收信号, go func () 中的代码是循环接收 signalCh 通道传来的数据并处理。如果不需要再从这个通道接收信号了,可以调用 sessionBus.RemoveSignal 取消注册 signalCh 通道,这样信号就不会再被发送到 signalCh 通道中了。

在处理信号时,可以先验证信号( *dbus.Signal 类型)的 PathName 字段,不必验证 Sender 字段。 处理信号的 Body 字段,它是 []interface{} 类型,就可以用 dbus.Store 方法了。

导出对象

将自己代码中的一个变量作为对象的一个接口导出在 DBus 上,然后供其他人调用。

下面的代码是要把类型为 Obj 结构的 obj 变量作为对象的一个接口导出,它有一个 GetString 方法,路径 /p1/p2/p3 ,接口名 p1.p2.p3

type Obj struct {
}

func (o *Obj) GetString() (string, *dbus.Error) {
    return "object", nil
}

func main() {
    sessionBus, err := dbus.SessionBus()
    obj := &Obj{}
    err = sessionBus.Export(obj, "/p1/p2/p3", "p1.p2.p3")
    log.Print("names:", sessionBus.Names())
    select {
    }
}

程序运行后会打印类似的输出:

names:[:1.4239 :1.4239]

这是此程序目前在 DBus 上的名字,冒号 : 开头的是唯一名。

导出对象接口,可以调用 sessionBus.Export 方法,要停止导出,并没有一个专门的的方法,依旧要使用 Export 方法, 但是第一个参数(之前放 obj 的位置)要换成 nil

自动导出的方法有如下要求:

  1. 公开的,方法名第一字母是大写的。
  2. 最后一个返回值是 *dbus.Error 类型。
注册服务名

下面的代码是要申请一个服务名 p1.p2.p3, 不是完整代码,可以插在前一份代码的 sessionBus.Export 方法调用之后。现有的各个项目里也一般是先导出主要对象,然后再申请服务名。

_, err = sessionBus.RequestName("p1.p2.p3", 0)

发送信号

下面的代码是要发送信号,路径 /p1/p2/p3, 接口名 p1.p2.p3, 信号名 Signal1 , 参数是字符串和数字。

sessionBus, err := dbus.SessionBus()
for {
    err = sessionBus.Emit("/p1/p2/p3", "p1.p2.p3.Signal1", "arg1", 2)
    time.Sleep(2*time.Second)
}

程序每 2 秒发送一次信号,发送信号时,不要求对象是否已经导出。

使用 go-dbus-factory 项目

此项目实现了根据 dbus 服务的自省 XML 文件和一点配置文件,自动生成访问 dbus 服务的 go 代码。

项目地址:https://gitlab.deepin.io/dde-v20/go-dbus-factory ,可以打成 deb 包,软件包名 golang-github-linuxdeepin-go-dbus-factory-dev ,每个服务放在不同的包中,包名的前缀是 github.com/linuxdeepin/go-dbus-factory/

方法调用

下面的代码要调用 com.deepin.dde.daemon.Dock 服务的 /com/deepin/dde/daemon/Dock 对象的 RequestDock 方法。

sessionBus, err := dbus.SessionBus()
dockObj := dock.NewDock(sessionBus)
ok, err := dockObj.RequestDock(0, "/usr/share/applications/deepin-editor.desktop", -1)
log.Println("ok:", ok)

需要注意传给 obj.Call 方法的第一个参数 method 的值是接口名 + "." + 方法名。

在处理没有返回值的方法时,可以不调用 Store 方法,而直接检查 Err 字段。

获取和设置属性

属性的获取和设置本质也是方法调用。

获取属性

获取单个属性是调用对象的 org.freedesktop.DBus.Properties 接口的 Get 方法,此方法传入参数是接口名和属性名,返回值是属性值,类型为 dbus.Variant

下面的代码是要获取 org.freedesktop.DBus 接口的 Features 属性,此属性的类型是 []string

sessionBus, err := dbus.SessionBus()
obj := sessionBus.Object("org.freedesktop.DBus", "/org/freedesktop/DBus")
var ret dbus.Variant
err = obj.Call("org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.DBus", "Features").Store(&ret)
v := ret.Value().([]string)
log.Println(v)

ret 中获取内部的值,需要调用 Value 方法,获取 interface{} 类型的内部值,再使用类型断言 .([]string)

一次获取对象在某个接口下的全部属性,可调用 Get 方法旁边的 GetAll 方法。

设置属性

设置属性是调用对象的 org.freedesktop.DBus.Properties 接口的 Set 方法,此方法的传入参数是接口名、属性名、属性值,没有返回值。

下面的代码是要设置 org.freedesktop.DBus 接口的 Features 属性。

sessionBus, err := dbus.SessionBus()
obj := sessionBus.Object("org.freedesktop.DBus", "/org/freedesktop/DBus")
propVal := dbus.MakeVariant([]string{"abc"})
err = obj.Call("org.freedesktop.DBus.Properties.Set", 0, "org.freedesktop.DBus", "Features", propVal).Err
监控信号

下面的代码是要监控之前说过的 Dock 对象的 EntryAdded 信号。

sessionBus, err := dbus.SessionBus()
dockObj := dock.NewDock(sessionBus)
sigLoop := dbusutil.NewSignalLoop(sessionBus, 10)
sigLoop.Start()
dockObj.InitSignalExt(sigLoop, true)
sigHid, err := dockObj.ConnectEntryAdded(func(entryPath dbus.ObjectPath, index int32) {
    log.Println(entryPath, index)
})

time.Sleep(100*time.Second)
sigLoop.Stop()
dockObj.RemoveHandler(sigHid)

程序运行后,从启动器打开一个任务栏上之前没有的应用,然后程序就能收到 EntryAdded 信号并打印参数值出来。

这部分代码用到了新的包 pkg.deepin.io/lib/dbusutil 包,使用它的 SignalLoop 功能来处理信号。调用 dbusutil.NewSignalLoop 创建一个信号循环,然后调用 Start 方法开始,当不需要时调用 Stop 结束。 dockObj 默认是不支持监听信号的,直接调用 dockObj.ConnectXXX 方法会崩溃,必须先调用 InitSignalExt 方法,传入 SignalLoop。

使用 dbusutil 包

之前介绍了使用 dbus1 包,现在介绍使用更多的 dbusutil 包,它是对 dbus1 包的功能的一次包装,更加偏向于导出服务。

导出对象

type Obj struct {
    methods *struct {
        GetString func() `out:"result"`
    }
}

func (o *Obj) GetString() (string, *dbus.Error) {
    return "object", nil
}

func (o *Obj) GetInterfaceName() string {
    return "p1.p2.p3"
}

func main() {
    service, err := dbusutil.NewSessionService()
    obj := &Obj{
    }
    err = service.Export("/p1/p2/p3", obj)
    err = service.RequestName("p1.p2.p3")
    service.Wait()
}

main 方法中,使用 dbusutil.NewSessionService 创建 *dbusutil.Service 类型的变量 service ,然后调用 service.Export 方法导出 obj 变量。要导出 obj 变量,就得让它的类型 *Obj 实现 GetInterfaceName 方法,此方法要返回接口名。

定义属性

定义一个属性的简单方式是,给导出的结构增加公开字段,比如想要导出 Obj 结构,导出的接口 p1.p2.p3 中会有一个 Name 属性,类型为字符串,就要有如下定义:

type Obj struct {
    Name string
}

这个 Name 属性是只读的,如果要让它可以写、设置,就要给 Name 字段加上结构字段 tag : prop:"access:rw" , 如下:

type Obj struct {
    Name string `prop:"access:rw"`
}

发送信号

下面的代码是要发送信号,路径 /p1/p2/p3, 接口名 p1.p2.p3, 信号名 Signal1 , 参数是字符串和数字。

type Obj struct {
    signals *struct {
        Added struct {
            arg0 string
            arg1 int32
        }
    }
}

func (o *Obj) GetInterfaceName() string {
    return "p1.p2.p3"
}

func main() {
    service, err := dbusutil.NewSessionService()
    obj := &Obj{
    }
    err = service.Export("/p1/p2/p3", obj)
    err = service.RequestName("p1.p2.p3")
    for {
        err = service.Emit(obj, "Added", "val0", 11)
        time.Sleep(2*time.Second)
    }
}

要点是在定义 Obj 结构时,加上 signals 字段,并在 signals 字段类型定义结构中加上 Added 字段 ,然后调用 service.Emit 方法发送信号。

发送属性改变信号

下面的代码是要发送属性改变信号,路径 /p1/p2/p3, 接口名 p1.p2.p3, 属性名 Name ,类型字符串。

type Obj struct {
    Name string
}

func (o *Obj) GetInterfaceName() string {
    return "p1.p2.p3"
}

func main() {
    service, err := dbusutil.NewSessionService()
    obj := &Obj{
    }
    err = service.Export("/p1/p2/p3", obj)
    err = service.RequestName("p1.p2.p3")
    for {
        err = service.EmitPropertyChanged(obj, "Name", "name1")
        time.Sleep(2*time.Second)
    }
}

要点是先在 Obj 结构中定义一个属性 Name ,然后调用 service.EmitPropertyChanged 方法发送属性改变信号。

gsettings in go

gsettings 是 gio 提供的功能,go 语言要用就得用 cgo 包装一下,这部分代码是由 go-gir-generator 工具自动生成的,软件包 golang-gir-gio-2.0-dev 提供了源码文件,安装之后放在 /usr/share/gocode/src/pkg.deepin.io/gir/gio-2.0 文件夹下。

项目地址: https://gitlab.deepin.io/github-linuxdeepin-mirror/go-gir-generator

简单使用

下面的代码是要获取和设置 gsettings ca.desrt.dconf-editor.Demo 的几个设置项。

gs := gio.NewSettings("ca.desrt.dconf-editor.Demo")
str := gs.GetString("string")

gs.SetString("string", str)

int0 := gs.GetInt("integer-32-signed")

gs.SetInt("integer-32-signed", int0)
gs.Unref()

NewSettings 方法,创建 Settings 对象。 GetString 方法获取字符串类型的设置项的值,=SetString= 方法设置字符串类型的设置项的值。 GetInt 方法获取整数 int32 类型的设置项的值,=SetInt= 方法设置整数 int32 类型的设置项的值。 Unref 方法,在不再需要 gs 对象时,减少引用计数的值。

监听改变信号(原生)

下面的代码是要监听 gsettings ca.desrt.dconf-editor.Demoboolean 设置项的值改变事件。

gs := gio.NewSettings("ca.desrt.dconf-editor.Demo")
gs.Connect("changed::boolean", func(gs *gio.Settings, key string) {
    boolean := gs.GetBoolean(key)
    log.Println(boolean)
})
glib.StartLoop()

调用 Connect 方法注册事件监听处理函数,第一个参数为事件名,第二个参数为事件处理函数。

glib.StartLoop() 开始一个 Main Loop 事件循环。

监听改变信号(dbus 实现)

由于原生的信号监听方式之前在一种特殊架构上偶尔出现发生了改变事件但不触发处理函数的问题,于是写了另外一种信号监听方式,来规避这种问题。

下面的代码使用了另外一个包 pkg.deepin.io/lib/gsettings 也实现了监听 boolean 设置项的值改变事件。

gs := gio.NewSettings("ca.desrt.dconf-editor.Demo")
gsettings.ConnectChanged("ca.desrt.dconf-editor.Demo", "boolean",
 func(key string) {
        boolean := gs.GetBoolean(key)
        log.Println(boolean)
})
err := gsettings.StartMonitor()
select {
}

ConnectChanged 方法注册改变事件的处理函数,第一个参数是 schema id , 第二个参数是设置项名,第三个参数是事件处理函数。

StartMonitor 方法开始监听循环。

dbus 服务中作为属性

下面的代码是要导出 DBus 服务,服务名 p1.p2.p3, 对象路径 /p1/p2/p3, 接口名 p1.p2.p3 ,把 gsettings ca.desrt.dconf-editor.Demoboolean 设置项当作接口的属性。

type Obj struct {
    Boolean gsprop.Bool `prop:"access:rw"`
}

func (o *Obj) GetInterfaceName() string {
    return "p1.p2.p3"
}

func main() {
    gs := gio.NewSettings("ca.desrt.dconf-editor.Demo")

    service, err := dbusutil.NewSessionService()

    obj := &Obj{}
    obj.Boolean.Bind(gs, "boolean")
    log.Println("get result:", obj.Boolean.Get())

    err = service.Export("/p1/p2/p3", obj)
    err = service.RequestName("p1.p2.p3")
    err = gsettings.StartMonitor()
    service.Wait()
}

这算是 gsettings 与 dbusutil 包知识相结合的内容。

要点是 Obj 结构定义了一个公开字段 Name ,类型为 gsprop.Bool ,然后调用 Bind 方法绑定了 gsboolean 设置项和 objName 属性的关联,使得对 Name 属性的读写即是对 gsboolean 设置项的读写, 如果 gsboolean 设置项的值改变会自动发出 Name 属性改变信号。

gsprop 包中还有其他的类型,来支持各种数据类型,比如 String=,=Int=,=Uint 等。

同样要记得调用 StartMonitor 方法开始监听循环。

作者: Petrus.Z

Created: 2021-09-01 Wed 00:39