A live restart of the system bus during an upgrade hung every login on a workstation until a reboot. The module owns the bus's packages, declares the bus running with no restart or reload trigger, publishes only curated events (health, services, denials; never traffic) and serves tools to look at both buses.
134 lines
4.2 KiB
Go
134 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/godbus/dbus/v5/introspect"
|
|
)
|
|
|
|
// A bus name and an object path as the specification allows them; anything else is refused before
|
|
// it reaches busctl, so no argument is ever taken for an option.
|
|
var (
|
|
busNameRE = regexp.MustCompile(`^(:[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)+|[A-Za-z_-][A-Za-z0-9_-]*(\.[A-Za-z_-][A-Za-z0-9_-]*)+)$`)
|
|
objectPathRE = regexp.MustCompile(`^/([A-Za-z0-9_]+(/[A-Za-z0-9_]+)*)?$`)
|
|
)
|
|
|
|
// Member is one method or signal, with its arguments as "name type".
|
|
type Member struct {
|
|
Name string `json:"name"`
|
|
In []string `json:"in,omitempty"`
|
|
Out []string `json:"out,omitempty"`
|
|
Args []string `json:"args,omitempty"`
|
|
}
|
|
|
|
// Property is one property's name, type and access, never its value: a value can be anything a
|
|
// service holds, and reading it is a call of its own.
|
|
type Property struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Access string `json:"access"`
|
|
}
|
|
|
|
// Interface is one interface of an object.
|
|
type Interface struct {
|
|
Name string `json:"name"`
|
|
Methods []Member `json:"methods,omitempty"`
|
|
Properties []Property `json:"properties,omitempty"`
|
|
Signals []Member `json:"signals,omitempty"`
|
|
}
|
|
|
|
// Object is dbus_introspect's answer.
|
|
type Object struct {
|
|
Bus string `json:"bus"`
|
|
Service string `json:"service"`
|
|
Path string `json:"path"`
|
|
Interfaces []Interface `json:"interfaces"`
|
|
Children []string `json:"children,omitempty"`
|
|
}
|
|
|
|
// ParseIntrospection reads `busctl call … Introspect --json=short` ({"type":"s","data":["<xml>"]}).
|
|
func ParseIntrospection(out string) (introspect.Node, error) {
|
|
var reply struct {
|
|
Data []string `json:"data"`
|
|
}
|
|
var node introspect.Node
|
|
if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &reply); err != nil || len(reply.Data) != 1 {
|
|
return node, fmt.Errorf("the introspection answer is not a single string")
|
|
}
|
|
if err := xml.Unmarshal([]byte(reply.Data[0]), &node); err != nil {
|
|
return node, fmt.Errorf("the introspection document does not parse: %w", err)
|
|
}
|
|
return node, nil
|
|
}
|
|
|
|
// Shape turns an introspection document into the tool's answer.
|
|
func Shape(bus, service, path string, node introspect.Node) Object {
|
|
o := Object{Bus: bus, Service: service, Path: path, Interfaces: []Interface{}}
|
|
for _, i := range node.Interfaces {
|
|
iface := Interface{Name: i.Name}
|
|
for _, m := range i.Methods {
|
|
mem := Member{Name: m.Name}
|
|
for _, a := range m.Args {
|
|
s := strings.TrimSpace(a.Name + " " + a.Type)
|
|
if a.Direction == "out" {
|
|
mem.Out = append(mem.Out, s)
|
|
} else {
|
|
mem.In = append(mem.In, s)
|
|
}
|
|
}
|
|
iface.Methods = append(iface.Methods, mem)
|
|
}
|
|
for _, p := range i.Properties {
|
|
iface.Properties = append(iface.Properties, Property{Name: p.Name, Type: p.Type, Access: p.Access})
|
|
}
|
|
for _, s := range i.Signals {
|
|
mem := Member{Name: s.Name}
|
|
for _, a := range s.Args {
|
|
mem.Args = append(mem.Args, strings.TrimSpace(a.Name+" "+a.Type))
|
|
}
|
|
iface.Signals = append(iface.Signals, mem)
|
|
}
|
|
o.Interfaces = append(o.Interfaces, iface)
|
|
}
|
|
for _, c := range node.Children {
|
|
if len(o.Children) >= AnswerCap {
|
|
break
|
|
}
|
|
o.Children = append(o.Children, c.Name)
|
|
}
|
|
return o
|
|
}
|
|
|
|
// Introspect asks one object what it offers, as this account, without starting a service that is not
|
|
// running (--auto-start=no): looking must not change what runs.
|
|
func (m *Machine) Introspect(ctx context.Context, bus, service, path string) (Object, error) {
|
|
if !busNameRE.MatchString(service) {
|
|
return Object{}, fmt.Errorf("%q is not a bus name", service)
|
|
}
|
|
if path == "" {
|
|
path = "/"
|
|
}
|
|
if !objectPathRE.MatchString(path) {
|
|
return Object{}, fmt.Errorf("%q is not an object path", path)
|
|
}
|
|
args, err := m.busArgs(bus)
|
|
if err != nil {
|
|
return Object{}, err
|
|
}
|
|
out, err := m.Run(ctx, "busctl", append(args, "--json=short", "--no-pager", "--auto-start=no", "call",
|
|
service, path, "org.freedesktop.DBus.Introspectable", "Introspect")...)
|
|
if err != nil {
|
|
return Object{}, err
|
|
}
|
|
node, err := ParseIntrospection(out)
|
|
if err != nil {
|
|
return Object{}, err
|
|
}
|
|
return Shape(orWord(bus, "system"), service, path, node), nil
|
|
}
|