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,115 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user