aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/system/dbus.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/system/dbus.go')
-rw-r--r--internal/system/dbus.go207
1 files changed, 207 insertions, 0 deletions
diff --git a/internal/system/dbus.go b/internal/system/dbus.go
new file mode 100644
index 00000000..b9021b81
--- /dev/null
+++ b/internal/system/dbus.go
@@ -0,0 +1,207 @@
+package system
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "log"
+ "reflect"
+ "strings"
+ "sync"
+ "syscall"
+
+ "hakurei.app/container"
+ "hakurei.app/hst"
+ "hakurei.app/internal/system/dbus"
+)
+
+// ErrDBusConfig is returned when a required [hst.BusConfig] argument is nil.
+var ErrDBusConfig = errors.New("dbus config not supplied")
+
+// MustProxyDBus calls ProxyDBus and panics if an error is returned.
+func (sys *I) MustProxyDBus(
+ session, system *hst.BusConfig,
+ sessionBus, systemBus dbus.ProxyPair,
+) *I {
+ if err := sys.ProxyDBus(session, system, sessionBus, systemBus); err != nil {
+ panic(err.Error())
+ } else {
+ return sys
+ }
+}
+
+// ProxyDBus finalises configuration ahead of time and starts xdg-dbus-proxy via [dbus] and terminates it on revert.
+// This [Op] is always [Process] scoped.
+func (sys *I) ProxyDBus(
+ session, system *hst.BusConfig,
+ sessionBus, systemBus dbus.ProxyPair,
+) error {
+ d := new(dbusProxyOp)
+
+ // session bus is required as otherwise this is effectively a very expensive noop
+ if session == nil {
+ return newOpErrorMessage("dbus", ErrDBusConfig,
+ "attempted to create message bus proxy args without session bus config", false)
+ }
+
+ // system bus is optional
+ d.system = system != nil
+
+ d.out = &linePrefixWriter{println: log.Println, prefix: "(dbus) ", buf: new(strings.Builder)}
+ if final, err := sys.dbusFinalise(sessionBus, systemBus, session, system); err != nil {
+ if errors.Is(err, syscall.EINVAL) {
+ return newOpErrorMessage("dbus", err,
+ "message bus proxy configuration contains NUL byte", false)
+ }
+ return newOpErrorMessage("dbus", err,
+ fmt.Sprintf("cannot finalise message bus proxy: %v", err), false)
+ } else {
+ if sys.msg.IsVerbose() {
+ sys.msg.Verbose("session bus proxy:", dbus.Args(session, sessionBus))
+ if system != nil {
+ sys.msg.Verbose("system bus proxy:", dbus.Args(system, systemBus))
+ }
+
+ // this calls the argsWt String method
+ sys.msg.Verbose("message bus proxy final args:", final.WriterTo)
+ }
+
+ d.final = final
+ }
+
+ sys.ops = append(sys.ops, d)
+ return nil
+}
+
+// dbusProxyOp implements [I.ProxyDBus].
+type dbusProxyOp struct {
+ proxy *dbus.Proxy // populated during apply
+
+ final *dbus.Final
+ out *linePrefixWriter
+ // whether system bus proxy is enabled
+ system bool
+}
+
+func (d *dbusProxyOp) Type() hst.Enablement { return Process }
+
+func (d *dbusProxyOp) apply(sys *I) error {
+ sys.msg.Verbosef("session bus proxy on %q for upstream %q", d.final.Session[1], d.final.Session[0])
+ if d.system {
+ sys.msg.Verbosef("system bus proxy on %q for upstream %q", d.final.System[1], d.final.System[0])
+ }
+
+ d.proxy = dbus.New(sys.ctx, sys.msg, d.final, d.out)
+ if err := sys.dbusProxyStart(d.proxy); err != nil {
+ d.out.Dump()
+ return newOpErrorMessage("dbus", err,
+ fmt.Sprintf("cannot start message bus proxy: %v", err), false)
+ }
+ sys.msg.Verbose("starting message bus proxy", d.proxy)
+ return nil
+}
+
+func (d *dbusProxyOp) revert(sys *I, _ *Criteria) error {
+ // criteria ignored here since dbus is always process-scoped
+ sys.msg.Verbose("terminating message bus proxy")
+ sys.dbusProxyClose(d.proxy)
+
+ exitMessage := "message bus proxy exit"
+ defer func() { sys.msg.Verbose(exitMessage) }()
+
+ if d.out != nil {
+ d.out.Dump()
+ }
+
+ err := sys.dbusProxyWait(d.proxy)
+ if errors.Is(err, context.Canceled) {
+ exitMessage = "message bus proxy canceled upstream"
+ err = nil
+ }
+ return newOpErrorMessage("dbus", err,
+ fmt.Sprintf("message bus proxy error: %v", err), true)
+}
+
+func (d *dbusProxyOp) Is(o Op) bool {
+ target, ok := o.(*dbusProxyOp)
+ return ok && d != nil && target != nil &&
+ d.system == target.system &&
+ d.final != nil && target.final != nil &&
+ d.final.Session == target.final.Session &&
+ d.final.System == target.final.System &&
+ dbus.EqualAddrEntries(d.final.SessionUpstream, target.final.SessionUpstream) &&
+ dbus.EqualAddrEntries(d.final.SystemUpstream, target.final.SystemUpstream) &&
+ reflect.DeepEqual(d.final.WriterTo, target.final.WriterTo)
+}
+
+func (d *dbusProxyOp) Path() string { return container.Nonexistent }
+func (d *dbusProxyOp) String() string { return d.proxy.String() }
+
+const (
+ // lpwSizeThreshold is the threshold of bytes written to linePrefixWriter which,
+ // if reached or exceeded, causes linePrefixWriter to drop all future writes.
+ lpwSizeThreshold = 1 << 24
+)
+
+// linePrefixWriter calls println with a prefix for every line written.
+type linePrefixWriter struct {
+ prefix string
+ println func(v ...any)
+
+ n int
+ msg []string
+ buf *strings.Builder
+
+ mu sync.RWMutex
+}
+
+func (s *linePrefixWriter) Write(p []byte) (n int, err error) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ return s.write(p, 0)
+}
+
+func (s *linePrefixWriter) write(p []byte, a int) (int, error) {
+ if s.n >= lpwSizeThreshold {
+ if len(p) == 0 {
+ return a, nil
+ }
+ return a, syscall.ENOMEM
+ }
+
+ if i := bytes.IndexByte(p, '\n'); i == -1 {
+ n, _ := s.buf.Write(p)
+ s.n += n
+ return a + n, nil
+ } else {
+ n, _ := s.buf.Write(p[:i])
+ s.n += n + 1
+
+ v := s.buf.String()
+ if strings.HasPrefix(v, "init: ") {
+ s.n -= len(v) + 1
+ // pass through container init messages
+ s.println(s.prefix + v)
+ } else {
+ s.msg = append(s.msg, v)
+ }
+
+ s.buf.Reset()
+ return s.write(p[i+1:], a+n+1)
+ }
+}
+
+func (s *linePrefixWriter) Dump() {
+ s.mu.RLock()
+ for _, m := range s.msg {
+ s.println(s.prefix + m)
+ }
+ if s.buf != nil && s.buf.Len() != 0 {
+ s.println("*" + s.prefix + s.buf.String())
+ }
+ if s.n >= lpwSizeThreshold {
+ s.println("+" + s.prefix + "write threshold reached, output may be incomplete")
+ }
+ s.mu.RUnlock()
+}