dbus: hold node-message-bus, and never restart the bus live (hq ADR 0215)
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.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user