From 357cc4ce4d2888033a8163fe39861c8a88301da6 Mon Sep 17 00:00:00 2001 From: Ophestra Umiker Date: Mon, 9 Sep 2024 03:11:50 +0900 Subject: [PATCH] dbus: implement xdg-dbus-proxy wrapper Signed-off-by: Ophestra Umiker --- dbus/config.go | 59 ++++++++++++++++++++++ dbus/run.go | 134 +++++++++++++++++++++++++++++++++++++++++++++++++ dbus/setup.go | 73 +++++++++++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 dbus/config.go create mode 100644 dbus/run.go create mode 100644 dbus/setup.go diff --git a/dbus/config.go b/dbus/config.go new file mode 100644 index 0000000..9173d78 --- /dev/null +++ b/dbus/config.go @@ -0,0 +1,59 @@ +package dbus + +type Config struct { + See []string `json:"see"` + Talk []string `json:"talk"` + Own []string `json:"own"` + + Log bool `json:"log,omitempty"` + Filter bool `json:"filter"` +} + +func (c *Config) Args(address, path string) (args []string) { + argc := 2 + len(c.See) + len(c.Talk) + len(c.Own) + if c.Log { + argc++ + } + if c.Filter { + argc++ + } + + args = make([]string, 0, argc) + args = append(args, address, path) + for _, name := range c.See { + args = append(args, "--see="+name) + } + for _, name := range c.Talk { + args = append(args, "--talk="+name) + } + for _, name := range c.Own { + args = append(args, "--own="+name) + } + if c.Log { + args = append(args, "--log") + } + if c.Filter { + args = append(args, "--filter") + } + + return +} + +// NewConfig returns a reference to a Config struct with optional defaults. +// If id is an empty string own defaults are omitted. +func NewConfig(id string, defaults, mpris bool) (c *Config) { + c = &Config{Filter: true} + + if defaults { + c.Talk = []string{"org.freedesktop.DBus", "org.freedesktop.portal.*", "org.freedesktop.Notifications"} + + if id != "" { + c.Own = []string{id} + if mpris { + c.Own = append(c.Own, "org.mpris.MediaPlayer2."+id) + } + } + } + + return +} diff --git a/dbus/run.go b/dbus/run.go new file mode 100644 index 0000000..315b31d --- /dev/null +++ b/dbus/run.go @@ -0,0 +1,134 @@ +package dbus + +import ( + "errors" + "os" + "os/exec" +) + +// Start launches the D-Bus proxy and sets up the Wait method. +// ready should be buffered and should only be received from once. +func (p *Proxy) Start(ready *chan bool) error { + p.lock.Lock() + defer p.lock.Unlock() + + if p.seal == nil { + return errors.New("proxy not sealed") + } + + // acquire pipes + if pr, pw, err := os.Pipe(); err != nil { + return err + } else { + p.statP[0], p.statP[1] = pr, pw + } + if pr, pw, err := os.Pipe(); err != nil { + return err + } else { + p.argsP[0], p.argsP[1] = pr, pw + } + + p.cmd = exec.Command(p.path, + // ExtraFiles: If non-nil, entry i becomes file descriptor 3+i. + "--fd=3", + "--args=4", + ) + p.cmd.Env = []string{} + p.cmd.ExtraFiles = []*os.File{p.statP[1], p.argsP[0]} + p.cmd.Stdout = os.Stdout + p.cmd.Stderr = os.Stderr + if err := p.cmd.Start(); err != nil { + return err + } + + statsP, argsP := p.statP[0], p.argsP[1] + + if _, err := argsP.Write([]byte(*p.seal)); err != nil { + if err1 := p.cmd.Process.Kill(); err1 != nil { + panic(err1) + } + return err + } else { + if err = argsP.Close(); err != nil { + if err1 := p.cmd.Process.Kill(); err1 != nil { + panic(err1) + } + return err + } + } + + wait := make(chan error) + go func() { + // live out the lifespan of the process + wait <- p.cmd.Wait() + }() + + read := make(chan error) + go func() { + n, err := statsP.Read(make([]byte, 1)) + switch n { + case -1: + if err1 := p.cmd.Process.Kill(); err1 != nil { + panic(err1) + } + read <- err + case 0: + read <- err + case 1: + *ready <- true + read <- nil + default: + panic("unreachable") // unexpected read count + } + }() + + p.wait = &wait + p.read = &read + p.ready = ready + + return nil +} + +// Wait waits for xdg-dbus-proxy to exit or fault. +func (p *Proxy) Wait() error { + p.lock.RLock() + defer p.lock.RUnlock() + + if p.wait == nil || p.read == nil { + return errors.New("proxy not running") + } + + defer func() { + if err1 := p.statP[0].Close(); err1 != nil && !errors.Is(err1, os.ErrClosed) { + panic(err1) + } + if err1 := p.statP[1].Close(); err1 != nil && !errors.Is(err1, os.ErrClosed) { + panic(err1) + } + + if err1 := p.argsP[0].Close(); err1 != nil && !errors.Is(err1, os.ErrClosed) { + panic(err1) + } + if err1 := p.argsP[1].Close(); err1 != nil && !errors.Is(err1, os.ErrClosed) { + panic(err1) + } + + }() + + select { + case err := <-*p.wait: + *p.ready <- false + return err + case err := <-*p.read: + if err != nil { + *p.ready <- false + return err + } + return <-*p.wait + } +} + +// Close closes the status file descriptor passed to xdg-dbus-proxy, causing it to stop. +func (p *Proxy) Close() error { + return p.statP[0].Close() +} diff --git a/dbus/setup.go b/dbus/setup.go new file mode 100644 index 0000000..8fb7b1b --- /dev/null +++ b/dbus/setup.go @@ -0,0 +1,73 @@ +package dbus + +import ( + "errors" + "os" + "os/exec" + "strings" + "sync" +) + +// Proxy holds references to a xdg-dbus-proxy process, and should never be copied. +// Once sealed, configuration changes will no longer be possible and attempting to do so will result in a panic. +type Proxy struct { + cmd *exec.Cmd + + statP [2]*os.File + argsP [2]*os.File + + address [2]string + path string + + wait *chan error + read *chan error + ready *chan bool + + seal *string + lock sync.RWMutex +} + +func (p *Proxy) String() string { + if p.cmd != nil { + return p.cmd.String() + } + + if p.seal != nil { + return *p.seal + } + + return "(unsealed dbus proxy)" +} + +// Seal seals the Proxy instance. +func (p *Proxy) Seal(c *Config) error { + p.lock.Lock() + defer p.lock.Unlock() + + if p.seal != nil { + panic("dbus proxy sealed twice") + } + args := c.Args(p.address[0], p.address[1]) + + seal := strings.Builder{} + for _, arg := range args { + // reject argument strings containing null + for _, b := range arg { + if b == '\x00' { + return errors.New("argument contains null") + } + } + + // write null terminated argument + seal.WriteString(arg) + seal.WriteByte('\x00') + } + v := seal.String() + p.seal = &v + return nil +} + +// New returns a reference to a new unsealed Proxy. +func New(binPath, address, path string) *Proxy { + return &Proxy{path: binPath, address: [2]string{address, path}} +}