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.
116 lines
3.9 KiB
Go
116 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// MaxMonitorSeconds bounds a watch of the bus: long enough to catch an exchange, short enough that
|
|
// nobody leaves a monitor running.
|
|
const MaxMonitorSeconds = 15
|
|
|
|
// MonitorLimit is the most messages busctl reads in one watch before it stops by itself.
|
|
const MonitorLimit = 20000
|
|
|
|
// Header is what dbus_monitor answers of one message: who sent what to whom. The body is never read
|
|
// into it: the struct has no field for it, so a payload is dropped as each line is decoded.
|
|
type Header struct {
|
|
Time string `json:"time,omitempty"`
|
|
Type string `json:"type"`
|
|
Sender string `json:"sender,omitempty"`
|
|
Destination string `json:"destination,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
Interface string `json:"interface,omitempty"`
|
|
Member string `json:"member,omitempty"`
|
|
ErrorName string `json:"error_name,omitempty"`
|
|
}
|
|
|
|
type monitorLine struct {
|
|
Type string `json:"type"`
|
|
Sender string `json:"sender"`
|
|
Destination string `json:"destination"`
|
|
Path string `json:"path"`
|
|
Interface string `json:"interface"`
|
|
Member string `json:"member"`
|
|
ErrorName string `json:"error_name"`
|
|
Realtime int64 `json:"timestamp-realtime"`
|
|
}
|
|
|
|
// ParseHeader reads one line of `busctl monitor --json=short` into its header alone.
|
|
func ParseHeader(line string) (Header, bool) {
|
|
var l monitorLine
|
|
if json.Unmarshal([]byte(line), &l) != nil || l.Type == "" {
|
|
return Header{}, false
|
|
}
|
|
h := Header{Type: l.Type, Sender: l.Sender, Destination: l.Destination, Path: l.Path,
|
|
Interface: l.Interface, Member: l.Member, ErrorName: l.ErrorName}
|
|
if l.Realtime > 0 {
|
|
h.Time = time.UnixMicro(l.Realtime).UTC().Format("15:04:05.000000")
|
|
}
|
|
return h, true
|
|
}
|
|
|
|
// Watch is dbus_monitor's answer.
|
|
type Watch struct {
|
|
Bus string `json:"bus"`
|
|
Seconds int `json:"seconds"`
|
|
Match string `json:"match,omitempty"`
|
|
Names []string `json:"names,omitempty"`
|
|
Total int `json:"total"`
|
|
Messages []Header `json:"messages"`
|
|
Cut bool `json:"cut,omitempty"`
|
|
Note string `json:"note"`
|
|
}
|
|
|
|
// Monitor watches one bus for a few seconds and answers the headers of what passed. The system bus is
|
|
// watched as root (sudo -n): only root may become a monitor there. The session bus is the account's
|
|
// own. The bodies, which carry secrets, notification text and the clipboard, are never kept.
|
|
func (m *Machine) Monitor(ctx context.Context, bus string, seconds int, match string, names []string) (Watch, error) {
|
|
if seconds < 1 || seconds > MaxMonitorSeconds {
|
|
return Watch{}, fmt.Errorf("seconds is 1 to %d", MaxMonitorSeconds)
|
|
}
|
|
if len(match) > 512 || strings.ContainsAny(match, "\n\x00") {
|
|
return Watch{}, fmt.Errorf("the match rule is not one line of at most 512 characters")
|
|
}
|
|
for _, n := range names {
|
|
if !busNameRE.MatchString(n) {
|
|
return Watch{}, fmt.Errorf("%q is not a bus name", n)
|
|
}
|
|
}
|
|
args, err := m.busArgs(bus)
|
|
if err != nil {
|
|
return Watch{}, err
|
|
}
|
|
cmd := append([]string{"timeout", strconv.Itoa(seconds) + "s", "busctl"}, args...)
|
|
cmd = append(cmd, "--json=short", "--no-pager", "--limit-messages="+strconv.Itoa(MonitorLimit), "monitor")
|
|
if match != "" {
|
|
cmd = append(cmd, "--match="+match)
|
|
}
|
|
cmd = append(cmd, names...)
|
|
if orWord(bus, "system") == "system" {
|
|
cmd = append([]string{"sudo", "-n"}, cmd...)
|
|
}
|
|
w := Watch{Bus: orWord(bus, "system"), Seconds: seconds, Match: match, Names: names, Messages: []Header{},
|
|
Note: "headers only; message bodies are never read into the answer"}
|
|
ctx, cancel := context.WithTimeout(ctx, time.Duration(seconds)*time.Second+5*time.Second)
|
|
defer cancel()
|
|
err = m.Stream(ctx, func(line string) bool {
|
|
h, ok := ParseHeader(line)
|
|
if !ok {
|
|
return true
|
|
}
|
|
w.Total++
|
|
if len(w.Messages) < AnswerCap {
|
|
w.Messages = append(w.Messages, h)
|
|
} else {
|
|
w.Cut = true
|
|
}
|
|
return true
|
|
}, cmd[0], cmd[1:]...)
|
|
return w, err
|
|
}
|