350 lines
8.1 KiB
Go
350 lines
8.1 KiB
Go
// Copyright (c) 2023 Joshua Rich <joshua.rich@gmail.com>
|
|
//
|
|
// This software is released under the MIT License.
|
|
// https://opensource.org/licenses/MIT
|
|
|
|
package dbushelpers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os/user"
|
|
"sync"
|
|
|
|
"github.com/godbus/dbus/v5"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
const (
|
|
SessionBus dbusType = iota // session
|
|
SystemBus // system
|
|
PropChangedSignal = "org.freedesktop.DBus.Properties.PropertiesChanged"
|
|
)
|
|
|
|
type dbusType int
|
|
|
|
type Bus struct {
|
|
conn *dbus.Conn
|
|
busType dbusType
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
// NewBus sets up DBus connections and channels for receiving signals. It creates both a system and session bus connection.
|
|
func NewBus(ctx context.Context, t dbusType) *Bus {
|
|
var conn *dbus.Conn
|
|
var err error
|
|
dbusCtx, cancelFunc := context.WithCancel(context.Background())
|
|
switch t {
|
|
case SessionBus:
|
|
conn, err = dbus.ConnectSessionBus(dbus.WithContext(dbusCtx))
|
|
case SystemBus:
|
|
conn, err = dbus.ConnectSystemBus(dbus.WithContext(dbusCtx))
|
|
}
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Could not connect to bus.")
|
|
cancelFunc()
|
|
return nil
|
|
}
|
|
b := &Bus{
|
|
conn: conn,
|
|
busType: t,
|
|
}
|
|
go func() {
|
|
defer conn.Close()
|
|
defer cancelFunc()
|
|
<-ctx.Done()
|
|
b.wg.Wait()
|
|
}()
|
|
return b
|
|
}
|
|
|
|
// busRequest contains properties for building different types of DBus requests.
|
|
type busRequest struct {
|
|
bus *Bus
|
|
eventHandler func(*dbus.Signal)
|
|
path dbus.ObjectPath
|
|
event string
|
|
dest string
|
|
match []dbus.MatchOption
|
|
}
|
|
|
|
func NewBusRequest(ctx context.Context, busType dbusType) *busRequest {
|
|
if bus, ok := getBus(ctx, busType); !ok {
|
|
log.Warn().Msg("No D-Bus connection present in context.")
|
|
return &busRequest{}
|
|
} else {
|
|
return &busRequest{
|
|
bus: bus,
|
|
}
|
|
}
|
|
}
|
|
|
|
func NewBusRequest2(ctx context.Context, busType dbusType) *busRequest {
|
|
if b := NewBus(ctx, busType); b != nil {
|
|
return &busRequest{
|
|
bus: b,
|
|
}
|
|
} else {
|
|
log.Warn().Msg("No D-Bus connection present in context.")
|
|
return &busRequest{}
|
|
}
|
|
}
|
|
|
|
// Path defines the DBus path on which a request will operate.
|
|
func (r *busRequest) Path(p dbus.ObjectPath) *busRequest {
|
|
r.path = p
|
|
return r
|
|
}
|
|
|
|
// Match defines DBus routing match rules on which a request will operate.
|
|
func (r *busRequest) Match(m []dbus.MatchOption) *busRequest {
|
|
r.match = m
|
|
return r
|
|
}
|
|
|
|
// Event defines an event on which a DBus request should match.
|
|
func (r *busRequest) Event(e string) *busRequest {
|
|
r.event = e
|
|
return r
|
|
}
|
|
|
|
// Handler defines a function that will handle a matched DBus signal.
|
|
func (r *busRequest) Handler(h func(*dbus.Signal)) *busRequest {
|
|
r.eventHandler = h
|
|
return r
|
|
}
|
|
|
|
// Destination defines the location/interface on a given DBus path for a request
|
|
// to operate.
|
|
func (r *busRequest) Destination(d string) *busRequest {
|
|
r.dest = d
|
|
return r
|
|
}
|
|
|
|
// GetProp fetches the specified property from DBus with the options specified
|
|
// in the builder.
|
|
func (r *busRequest) GetProp(prop string) (dbus.Variant, error) {
|
|
if r.bus == nil {
|
|
return dbus.MakeVariant(""), errors.New("no bus connection")
|
|
}
|
|
obj := r.bus.conn.Object(r.dest, r.path)
|
|
res, err := obj.GetProperty(prop)
|
|
if err != nil {
|
|
log.Warn().Err(err).
|
|
Msgf("Unable to retrieve property %s (%s)", prop, r.dest)
|
|
return dbus.MakeVariant(""), err
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// SetProp sets the specific property to the specified value.
|
|
func (r *busRequest) SetProp(prop string, value dbus.Variant) error {
|
|
if r.bus != nil {
|
|
obj := r.bus.conn.Object(r.dest, r.path)
|
|
return obj.SetProperty(prop, value)
|
|
}
|
|
return errors.New("no bus connection")
|
|
}
|
|
|
|
// GetData fetches DBus data from the given method in the builder.
|
|
func (r *busRequest) GetData(method string, args ...any) *dbusData {
|
|
if r.bus == nil {
|
|
log.Error().Msg("No bus connection.")
|
|
return nil
|
|
}
|
|
d := new(dbusData)
|
|
obj := r.bus.conn.Object(r.dest, r.path)
|
|
var err error
|
|
if args != nil {
|
|
err = obj.Call(method, 0, args...).Store(&d.data)
|
|
} else {
|
|
err = obj.Call(method, 0).Store(&d.data)
|
|
}
|
|
if err != nil {
|
|
log.Warn().Err(err).
|
|
Msgf("Unable to execute %s on %s (args: %s)", method, r.dest, args)
|
|
}
|
|
return d
|
|
}
|
|
|
|
// Call executes the given method in the builder and returns the error state.
|
|
func (r *busRequest) Call(method string, args ...any) error {
|
|
if r.bus == nil {
|
|
return errors.New("no bus connection")
|
|
}
|
|
obj := r.bus.conn.Object(r.dest, r.path)
|
|
if args != nil {
|
|
return obj.Call(method, 0, args...).Err
|
|
}
|
|
return obj.Call(method, 0).Err
|
|
}
|
|
|
|
func (r *busRequest) AddWatch(ctx context.Context) error {
|
|
if r.bus == nil {
|
|
return errors.New("no bus connection")
|
|
}
|
|
if err := r.bus.conn.AddMatchSignalContext(ctx, r.match...); err != nil {
|
|
return err
|
|
}
|
|
signalCh := make(chan *dbus.Signal)
|
|
r.bus.conn.Signal(signalCh)
|
|
r.bus.wg.Add(1)
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
r.bus.conn.RemoveSignal(signalCh)
|
|
close(signalCh)
|
|
return
|
|
case signal := <-signalCh:
|
|
r.eventHandler(signal)
|
|
}
|
|
}
|
|
}()
|
|
go func() {
|
|
wg.Wait()
|
|
r.bus.wg.Done()
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func (r *busRequest) RemoveWatch(ctx context.Context) error {
|
|
if r.bus == nil {
|
|
return errors.New("no bus connection")
|
|
}
|
|
if err := r.bus.conn.RemoveMatchSignalContext(ctx, r.match...); err != nil {
|
|
return err
|
|
}
|
|
log.Trace().
|
|
Str("path", string(r.path)).
|
|
Str("dest", r.dest).
|
|
Str("event", r.event).
|
|
Msgf("Removed D-Bus signal.")
|
|
return nil
|
|
}
|
|
|
|
type dbusData struct {
|
|
data any
|
|
}
|
|
|
|
// AsVariantMap formats DBus data as a map[string]dbus.Variant.
|
|
func (d *dbusData) AsVariantMap() map[string]dbus.Variant {
|
|
if d.data == nil {
|
|
return nil
|
|
}
|
|
wanted := make(map[string]dbus.Variant)
|
|
data, ok := d.data.(map[string]any)
|
|
if !ok {
|
|
log.Warn().Msgf("Could not represent D-Bus data as %T.", wanted)
|
|
return wanted
|
|
}
|
|
for k, v := range data {
|
|
wanted[k] = dbus.MakeVariant(v)
|
|
}
|
|
return wanted
|
|
}
|
|
|
|
// AsStringMap formats DBus data as a map[string]string.
|
|
func (d *dbusData) AsStringMap() map[string]string {
|
|
if d.data == nil {
|
|
return nil
|
|
}
|
|
data, ok := d.data.(map[string]string)
|
|
if !ok {
|
|
log.Warn().Msgf("Could not represent D-Bus data as %T.", data)
|
|
return make(map[string]string)
|
|
}
|
|
return data
|
|
}
|
|
|
|
// AsObjectPathList formats DBus data as a []dbus.ObjectPath.
|
|
func (d *dbusData) AsObjectPathList() []dbus.ObjectPath {
|
|
if d.data == nil {
|
|
return nil
|
|
}
|
|
var data []dbus.ObjectPath
|
|
var ok bool
|
|
data, ok = d.data.([]dbus.ObjectPath)
|
|
if !ok {
|
|
log.Warn().Msgf("Could not represent D-Bus data as %T.", data)
|
|
}
|
|
return data
|
|
}
|
|
|
|
// AsStringList formats DBus data as a []string.
|
|
func (d *dbusData) AsStringList() []string {
|
|
if d.data == nil {
|
|
return nil
|
|
}
|
|
var data []string
|
|
var ok bool
|
|
data, ok = d.data.([]string)
|
|
if !ok {
|
|
log.Warn().Msgf("Could not represent D-Bus data as %T.", data)
|
|
}
|
|
return data
|
|
}
|
|
|
|
// AsObjectPath formats DBus data as a dbus.ObjectPath.
|
|
func (d *dbusData) AsObjectPath() dbus.ObjectPath {
|
|
if d.data == nil {
|
|
return dbus.ObjectPath("")
|
|
}
|
|
var data dbus.ObjectPath
|
|
var ok bool
|
|
data, ok = d.data.(dbus.ObjectPath)
|
|
if !ok {
|
|
log.Warn().Msgf("Could not represent D-Bus data as %T.", data)
|
|
}
|
|
return data
|
|
}
|
|
|
|
// AsRawInterface formats DBus data as a plain interface{}.
|
|
func (d *dbusData) AsRawInterface() any {
|
|
if d.data != nil {
|
|
return d.data
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GetSessionPath(ctx context.Context) dbus.ObjectPath {
|
|
u, err := user.Current()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
sessions := NewBusRequest(ctx, SystemBus).
|
|
Path("/org/freedesktop/login1").
|
|
Destination("org.freedesktop.login1").
|
|
GetData("org.freedesktop.login1.Manager.ListSessions").
|
|
AsRawInterface()
|
|
allSessions, ok := sessions.([][]any)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
for _, s := range allSessions {
|
|
if thisUser, ok := s[2].(string); ok && thisUser == u.Username {
|
|
if p, ok := s[4].(string); ok {
|
|
return dbus.ObjectPath(p)
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// VariantToValue converts a dbus.Variant interface{} value into the specified
|
|
// Go native type. If the value is nil, then the return value will be the
|
|
// default value of the specified type.
|
|
func VariantToValue[S any](variant dbus.Variant) S {
|
|
var value S
|
|
err := variant.Store(&value)
|
|
if err != nil {
|
|
log.Warn().Err(err).
|
|
Msgf("Unable to convert dbus variant %v to type %T.", variant, value)
|
|
return value
|
|
}
|
|
return value
|
|
}
|