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":[""]}). 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 }