~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
192 lines · 5.4 KB Raw History
//go:build linux

package hostsvc

import (
	"context"
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"sync"

	sd "github.com/coreos/go-systemd/v22/dbus"
	"github.com/godbus/dbus/v5"
)

const (
	systemdDest = "org.freedesktop.systemd1"
	systemdPath = dbus.ObjectPath("/org/freedesktop/systemd1")
	managerIfc  = "org.freedesktop.systemd1.Manager"
)

type bus interface {
	StartTransientUnit(ctx context.Context, name string, props []sd.Property, ch chan<- string) error
	StopUnit(ctx context.Context, name string, ch chan<- string) error
	ResetFailedUnit(ctx context.Context, name string) error
	UnitProperties(ctx context.Context, name string) (map[string]any, error)
	ServiceProperties(ctx context.Context, name string) (map[string]any, error)
	ListUnitsByPatterns(ctx context.Context, patterns []string) ([]sd.UnitStatus, error)
	UnitByPIDFD(ctx context.Context, fd int) (string, string, error)
	Version(ctx context.Context) (string, error)
	ManagerStarted(ctx context.Context) (uint64, error)
	Subscribe(ctx context.Context, ch chan<- *dbus.Signal) error
	Close()
}

type dialError struct{ err error }

func (e dialError) Error() string { return "hostsvc: dialing the user manager: " + e.err.Error() }

func (e dialError) Unwrap() error { return e.err }

func PrivateSocketPath() string {
	dir := os.Getenv("XDG_RUNTIME_DIR")
	if dir == "" {
		dir = filepath.Join("/run/user", strconv.Itoa(os.Getuid()))
	}
	return filepath.Join(dir, "systemd", "private")
}

func dialPrivate(path string) func(ctx context.Context) (bus, error) {
	return func(ctx context.Context) (bus, error) {
		var mu sync.Mutex
		var conns []*dbus.Conn
		dialOne := func() (*dbus.Conn, error) {
			c, err := dbus.Dial("unix:path=" + path)
			if err != nil {
				return nil, err
			}
			mu.Lock()
			conns = append(conns, c)
			mu.Unlock()
			if err := ctx.Err(); err != nil {
				c.Close()
				return nil, err
			}
			if err := c.Auth([]dbus.Auth{dbus.AuthExternal(strconv.Itoa(os.Getuid()))}); err != nil {
				c.Close()
				return nil, err
			}
			return c, nil
		}
		watchdog := context.AfterFunc(ctx, func() {
			mu.Lock()
			defer mu.Unlock()
			for _, c := range conns {
				c.Close()
			}
		})
		conn, err := sd.NewConnection(dialOne)
		if !watchdog() {
			if conn != nil {
				conn.Close()
			}
			return nil, dialError{fmt.Errorf("%s: %w", path, ctx.Err())}
		}
		if err != nil {
			return nil, dialError{fmt.Errorf("%s: %w", path, err)}
		}
		return &sdBus{conn: conn, raw: conns[0], sig: conns[1]}, nil
	}
}

type sdBus struct {
	conn *sd.Conn
	raw  *dbus.Conn
	sig  *dbus.Conn
}

func (b *sdBus) StartTransientUnit(ctx context.Context, name string, props []sd.Property, ch chan<- string) error {
	_, err := b.conn.StartTransientUnitContext(ctx, name, "fail", props, ch)
	return err
}

func (b *sdBus) StartUnit(ctx context.Context, name string, ch chan<- string) error {
	_, err := b.conn.StartUnitContext(ctx, name, "replace", ch)
	return err
}

func (b *sdBus) EnableUnitFiles(ctx context.Context, names []string) error {
	_, _, err := b.conn.EnableUnitFilesContext(ctx, names, false, false)
	return err
}

func (b *sdBus) DisableUnitFiles(ctx context.Context, names []string) error {
	_, err := b.conn.DisableUnitFilesContext(ctx, names, false)
	return err
}

func (b *sdBus) StopUnit(ctx context.Context, name string, ch chan<- string) error {
	_, err := b.conn.StopUnitContext(ctx, name, "replace", ch)
	return err
}

func (b *sdBus) ResetFailedUnit(ctx context.Context, name string) error {
	return b.conn.ResetFailedUnitContext(ctx, name)
}

func (b *sdBus) UnitProperties(ctx context.Context, name string) (map[string]any, error) {
	return b.conn.GetUnitPropertiesContext(ctx, name)
}

func (b *sdBus) ServiceProperties(ctx context.Context, name string) (map[string]any, error) {
	return b.conn.GetUnitTypePropertiesContext(ctx, name, "Service")
}

func (b *sdBus) ListUnitsByPatterns(ctx context.Context, patterns []string) ([]sd.UnitStatus, error) {
	return b.conn.ListUnitsByPatternsContext(ctx, nil, patterns)
}

func (b *sdBus) UnitByPIDFD(ctx context.Context, fd int) (string, string, error) {
	var path dbus.ObjectPath
	var name string
	var inv []byte
	err := b.raw.Object(systemdDest, systemdPath).
		CallWithContext(ctx, managerIfc+".GetUnitByPIDFD", 0, dbus.UnixFD(fd)).
		Store(&path, &name, &inv)
	if err != nil {
		return "", "", err
	}
	return name, hex.EncodeToString(inv), nil
}

func (b *sdBus) Version(ctx context.Context) (string, error) {
	var v dbus.Variant
	err := b.raw.Object(systemdDest, systemdPath).
		CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, managerIfc, "Version").
		Store(&v)
	if err != nil {
		return "", err
	}
	s, ok := v.Value().(string)
	if !ok {
		return "", fmt.Errorf("hostsvc: the manager's Version is a %T, not a string", v.Value())
	}
	return s, nil
}

func (b *sdBus) ManagerStarted(ctx context.Context) (uint64, error) {
	var v dbus.Variant
	err := b.raw.Object(systemdDest, systemdPath).
		CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, managerIfc, "UserspaceTimestampMonotonic").
		Store(&v)
	if err != nil {
		return 0, err
	}
	n, ok := v.Value().(uint64)
	if !ok {
		return 0, fmt.Errorf("hostsvc: the manager's UserspaceTimestampMonotonic is a %T, not a uint64", v.Value())
	}
	return n, nil
}

func (b *sdBus) Subscribe(ctx context.Context, ch chan<- *dbus.Signal) error {
	b.sig.Signal(ch)
	return b.sig.Object(systemdDest, systemdPath).CallWithContext(ctx, managerIfc+".Subscribe", 0).Err
}

func (b *sdBus) Close() {
	b.conn.Close()
}