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 }