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.
305 lines
14 KiB
Go
305 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Recorded answers, trimmed, from a workstation of 2026-10-05.
|
|
const busctlList = `[{"name":":1.0","pid":456,"process":"systemd-timesyn","user":"systemd-timesync","connection":":1.0","unit":"systemd-timesyncd.service","session":null,"description":null},` +
|
|
`{"name":"fi.w1.wpa_supplicant1","pid":1442,"process":"wpa_supplicant","user":"root","connection":":1.23","unit":"wpa_supplicant.service","session":null,"description":null},` +
|
|
`{"name":"org.blueman.Mechanism","pid":null,"process":null,"user":null,"connection":"(activatable)","unit":null,"session":null,"description":null},` +
|
|
`{"name":":1.11","pid":975,"process":"polkitd","user":"polkitd","connection":":1.11","unit":"polkit.service","session":null,"description":null},` +
|
|
`{"name":"org.freedesktop.DBus","pid":807,"process":"dbus-broker-lau","user":"root","connection":"org.freedesktop.DBus","unit":"dbus-broker.service","session":null,"description":null}]`
|
|
|
|
func TestNamesAreSplitIntoRunningActivatableAndConnections(t *testing.T) {
|
|
n, err := ParseNames("system", busctlList)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n.Connections != 2 || len(n.Running) != 1 || n.Running[0].Name != "fi.w1.wpa_supplicant1" ||
|
|
n.Running[0].Unit != "wpa_supplicant.service" || n.Running[0].PID != 1442 {
|
|
t.Fatalf("%+v", n)
|
|
}
|
|
if !reflect.DeepEqual(n.ActivatableNotRunning, []string{"org.blueman.Mechanism"}) {
|
|
t.Fatalf("%v", n.ActivatableNotRunning)
|
|
}
|
|
}
|
|
|
|
const introspection = `{"type":"s","data":["<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\" \"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n<node>\n <interface name=\"org.freedesktop.login1.Manager\">\n <property name=\"IdleHint\" type=\"b\" access=\"read\"></property>\n <method name=\"Inhibit\">\n <arg type=\"s\" name=\"what\" direction=\"in\"/>\n <arg type=\"h\" name=\"pipe_fd\" direction=\"out\"/>\n </method>\n <signal name=\"PrepareForSleep\">\n <arg type=\"b\" name=\"start\"/>\n </signal>\n </interface>\n <node name=\"session\"/>\n <node name=\"seat\"/>\n</node>\n"]}`
|
|
|
|
func TestAnIntrospectionIsShapedWithoutValues(t *testing.T) {
|
|
node, err := ParseIntrospection(introspection)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
o := Shape("system", "org.freedesktop.login1", "/org/freedesktop/login1", node)
|
|
if len(o.Interfaces) != 1 || !reflect.DeepEqual(o.Children, []string{"session", "seat"}) {
|
|
t.Fatalf("%+v", o)
|
|
}
|
|
i := o.Interfaces[0]
|
|
if !reflect.DeepEqual(i.Methods[0], Member{Name: "Inhibit", In: []string{"what s"}, Out: []string{"pipe_fd h"}}) ||
|
|
!reflect.DeepEqual(i.Properties[0], Property{Name: "IdleHint", Type: "b", Access: "read"}) ||
|
|
!reflect.DeepEqual(i.Signals[0], Member{Name: "PrepareForSleep", Args: []string{"start b"}}) {
|
|
t.Fatalf("%+v", i)
|
|
}
|
|
}
|
|
|
|
func TestIntrospectRefusesWhatIsNotANameOrAPathAndNeverStartsAService(t *testing.T) {
|
|
var asked []string
|
|
m := testMachine(t, nil)
|
|
m.Run = func(_ context.Context, name string, args ...string) (string, error) {
|
|
asked = append([]string{name}, args...)
|
|
return introspection, nil
|
|
}
|
|
if _, err := m.Introspect(context.Background(), "system", "--address=x", "/"); err == nil {
|
|
t.Fatal("an option was taken for a bus name")
|
|
}
|
|
if _, err := m.Introspect(context.Background(), "system", "org.freedesktop.login1", "relative"); err == nil {
|
|
t.Fatal("a relative path was taken")
|
|
}
|
|
if _, err := m.Introspect(context.Background(), "system", "org.freedesktop.login1", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(strings.Join(asked, " "), "--auto-start=no") || asked[len(asked)-3] != "/" {
|
|
t.Fatalf("%v", asked)
|
|
}
|
|
}
|
|
|
|
const monitorLines = `{"type":"method_call","endian":"l","flags":0,"version":1,"cookie":186058,"timestamp-realtime":1791193058632202,"sender":":1.797","destination":"org.freedesktop.UPower","path":"/org/freedesktop/UPower","interface":"org.freedesktop.UPower","member":"GetDisplayDevice","payload":{"type":"","data":[]}}
|
|
{"type":"signal","endian":"l","flags":1,"version":1,"cookie":7,"timestamp-realtime":1791193058632725,"sender":":1.40","path":"/org/freedesktop/Notifications","interface":"org.freedesktop.Notifications","member":"Notify","payload":{"type":"susssasa{sv}i","data":["app",0,"","the secret notification text","the clipboard's password",[],{},5000]}}
|
|
not json at all
|
|
{"type":"error","endian":"l","flags":1,"version":1,"cookie":9,"sender":":1.9","destination":":1.797","error_name":"org.freedesktop.DBus.Error.AccessDenied","payload":{"type":"s","data":["the reason, with a token"]}}`
|
|
|
|
func TestAMonitoredMessageIsItsHeaderOnly(t *testing.T) {
|
|
var asked []string
|
|
m := testMachine(t, nil)
|
|
m.Stream = func(ctx context.Context, line func(string) bool, name string, args ...string) error {
|
|
asked = append([]string{name}, args...)
|
|
if _, ok := ctx.Deadline(); !ok {
|
|
t.Error("the watch has no deadline")
|
|
}
|
|
for _, l := range strings.Split(monitorLines, "\n") {
|
|
if !line(l) {
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
w, err := m.Monitor(context.Background(), "system", 3, "type='signal'", []string{"org.freedesktop.Notifications"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, _ := json.Marshal(w)
|
|
for _, secret := range []string{"secret notification", "password", "token", "payload", "data"} {
|
|
if strings.Contains(string(raw), secret) {
|
|
t.Errorf("the answer carries %q: %s", secret, raw)
|
|
}
|
|
}
|
|
if w.Total != 3 || w.Messages[1].Member != "Notify" || w.Messages[2].ErrorName != "org.freedesktop.DBus.Error.AccessDenied" {
|
|
t.Fatalf("%+v", w)
|
|
}
|
|
cmd := strings.Join(asked, " ")
|
|
if !strings.HasPrefix(cmd, "sudo -n timeout 3s busctl --system") || !strings.Contains(cmd, "--match=type='signal'") ||
|
|
!strings.HasSuffix(cmd, "org.freedesktop.Notifications") {
|
|
t.Fatalf("%s", cmd)
|
|
}
|
|
}
|
|
|
|
func TestAMonitorIsBoundedAndTheSessionBusIsTheAccounts(t *testing.T) {
|
|
m := testMachine(t, map[string]string{"/run/user/1000/bus": ""})
|
|
var asked string
|
|
m.Stream = func(_ context.Context, _ func(string) bool, name string, args ...string) error {
|
|
asked = name + " " + strings.Join(args, " ")
|
|
return nil
|
|
}
|
|
for _, s := range []int{0, MaxMonitorSeconds + 1} {
|
|
if _, err := m.Monitor(context.Background(), "system", s, "", nil); err == nil {
|
|
t.Errorf("%d seconds were accepted", s)
|
|
}
|
|
}
|
|
if _, err := m.Monitor(context.Background(), "session", 2, "", []string{"-x"}); err == nil {
|
|
t.Error("an option was taken for a name")
|
|
}
|
|
if _, err := m.Monitor(context.Background(), "session", 2, "", nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.HasPrefix(asked, "sudo") || !strings.Contains(asked, "--address=unix:path=/run/user/1000/bus") {
|
|
t.Fatalf("%s", asked)
|
|
}
|
|
}
|
|
|
|
func TestASessionBusIsFoundOrSaidMissing(t *testing.T) {
|
|
m := testMachine(t, nil)
|
|
if _, err := m.SessionAddress(); !errors.Is(err, ErrNoSession) {
|
|
t.Fatalf("a server without a session said %v", err)
|
|
}
|
|
m = testMachine(t, map[string]string{"/run/user/1000/bus": ""})
|
|
if a, err := m.SessionAddress(); err != nil || a != "unix:path=/run/user/1000/bus" {
|
|
t.Fatalf("%q %v", a, err)
|
|
}
|
|
m.Env = func(k string) string {
|
|
if k == "DBUS_SESSION_BUS_ADDRESS" {
|
|
return "unix:path=/run/user/1000/bus,guid=abc"
|
|
}
|
|
return ""
|
|
}
|
|
if a, _ := m.SessionAddress(); a != "unix:path=/run/user/1000/bus,guid=abc" {
|
|
t.Fatalf("%q", a)
|
|
}
|
|
}
|
|
|
|
const journalLines = `{"__CURSOR":"s=1;i=1","__REALTIME_TIMESTAMP":"1791192960341045","MESSAGE":"Ready","_PID":"807"}
|
|
{"__CURSOR":"s=1;i=2","__REALTIME_TIMESTAMP":"1791192960341045","MESSAGE":"A security policy denied :1.1199 to send method call /org/freedesktop/DBus:org.freedesktop.DBus.Debug.Stats.GetStats to org.freedesktop.DBus.","DBUS_BROKER_TRANSMIT_ACTION":"send","DBUS_BROKER_MESSAGE_TYPE":"method_call","DBUS_BROKER_SENDER_UNIQUE_NAME":":1.1199","DBUS_BROKER_MESSAGE_DESTINATION":"org.freedesktop.DBus","DBUS_BROKER_MESSAGE_PATH":"/org/freedesktop/DBus","DBUS_BROKER_MESSAGE_INTERFACE":"org.freedesktop.DBus.Debug.Stats","DBUS_BROKER_MESSAGE_MEMBER":"GetStats","DBUS_BROKER_POLICY_TYPE":"internal"}
|
|
{"__CURSOR":"s=1;i=3","MESSAGE":[65,66]}`
|
|
|
|
func TestDenialsAreReadFromTheJournalByTheirFields(t *testing.T) {
|
|
d, cursor := ParseDenials(journalLines)
|
|
if cursor != "s=1;i=3" || len(d) != 1 {
|
|
t.Fatalf("%q %+v", cursor, d)
|
|
}
|
|
want := Denial{At: "2026-10-05T09:36:00Z", Action: "send", Type: "method_call", Sender: ":1.1199",
|
|
Destination: "org.freedesktop.DBus", Path: "/org/freedesktop/DBus", Interface: "org.freedesktop.DBus.Debug.Stats",
|
|
Member: "GetStats", Policy: "internal"}
|
|
if d[0] != want {
|
|
t.Fatalf("%+v", d[0])
|
|
}
|
|
}
|
|
|
|
func TestTheJournalIsReadFromTheCursorOn(t *testing.T) {
|
|
m := testMachine(t, nil)
|
|
var asked []string
|
|
m.Run = func(_ context.Context, name string, args ...string) (string, error) {
|
|
asked = append([]string{name}, args...)
|
|
return journalLines, nil
|
|
}
|
|
_, next, _ := m.Denials(context.Background(), "", time.Unix(100, 0))
|
|
if !strings.Contains(strings.Join(asked, " "), "--since @100") {
|
|
t.Fatalf("%v", asked)
|
|
}
|
|
m.Denials(context.Background(), next, time.Time{})
|
|
if !strings.Contains(strings.Join(asked, " "), "--after-cursor s=1;i=3") || !strings.Contains(strings.Join(asked, " "), "-u dbus-broker.service") {
|
|
t.Fatalf("%v", asked)
|
|
}
|
|
}
|
|
|
|
func TestAUnitIsReadFromACgroup(t *testing.T) {
|
|
for in, want := range map[string]string{
|
|
"0::/system.slice/bluetooth.service\n": "bluetooth.service",
|
|
"0::/user.slice/user-1000.slice/user@1000.service/app.slice/dunst.service": "dunst.service",
|
|
"0::/user.slice/user-1000.slice/session-2.scope": "session-2.scope",
|
|
"0::/init.scope": "init.scope",
|
|
"": "",
|
|
} {
|
|
if got := UnitFromCgroup(in); got != want {
|
|
t.Errorf("%q: %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestShowBlocksAreReadInOrder(t *testing.T) {
|
|
b := ParseShow("Id=dbus-org.freedesktop.resolve1.service\nLoadState=not-found\n\nId=systemd-hostnamed.service\nLoadState=loaded\n")
|
|
if len(b) != 2 || b[0]["LoadState"] != "not-found" || b[1]["Id"] != "systemd-hostnamed.service" {
|
|
t.Fatalf("%v", b)
|
|
}
|
|
}
|
|
|
|
func TestARunningBusOlderThanItsPackageIsSaid(t *testing.T) {
|
|
p := ParseDesc("%NAME%\ndbus-broker\n\n%VERSION%\n37-3\n\n%INSTALLDATE%\n1772097880\n")
|
|
if p.Name != "dbus-broker" || p.Version != "37-3" || p.Installed.Unix() != 1772097880 {
|
|
t.Fatalf("%+v", p)
|
|
}
|
|
if due, _ := RebootDue(time.Unix(1791123478, 0), []Package{p}); due {
|
|
t.Fatal("a bus started after its package was said to be older")
|
|
}
|
|
due, detail := RebootDue(time.Unix(1772000000, 0), []Package{p})
|
|
if !due || !strings.Contains(detail, "reboot is due") || !strings.Contains(detail, "dbus-broker 37-3") {
|
|
t.Fatalf("%v %s", due, detail)
|
|
}
|
|
}
|
|
|
|
func TestConnectionsAreCountedFromStatsOrNames(t *testing.T) {
|
|
stats := `{"type":"a{sv}","data":[{"org.bus1.DBus.Debug.Stats.PeerAccounting":{"type":"a(sa{sv}a{su})","data":[[":1.0",{},{}],[":1.1",{},{}],[":1.7",{},{}]]}}]}`
|
|
if n, ok := CountConnections(stats); !ok || n != 3 {
|
|
t.Fatalf("%d %v", n, ok)
|
|
}
|
|
if n, ok := CountConnections(`{"type":"a{sv}","data":[{"ActiveConnections":{"type":"u","data":12}}]}`); !ok || n != 12 {
|
|
t.Fatalf("%d %v", n, ok)
|
|
}
|
|
if n, ok := CountUniqueNames(`{"type":"as","data":[["org.freedesktop.DBus",":1.0","org.bluez",":1.5"]]}`); !ok || n != 2 {
|
|
t.Fatalf("%d %v", n, ok)
|
|
}
|
|
}
|
|
|
|
func TestStatsAreNeverAskedAsTheAccount(t *testing.T) {
|
|
m := testMachine(t, nil)
|
|
var calls []string
|
|
m.Run = func(_ context.Context, name string, args ...string) (string, error) {
|
|
line := name + " " + strings.Join(args, " ")
|
|
calls = append(calls, line)
|
|
if name == "sudo" {
|
|
return "", errors.New("sudo: a password is required")
|
|
}
|
|
if strings.Contains(line, "ListNames") {
|
|
return `{"type":"as","data":[[":1.0",":1.1"]]}`, nil
|
|
}
|
|
return "Id=dbus-broker.service\nActiveState=active\nMainPID=807\nActiveEnterTimestamp=@1791123478\n", nil
|
|
}
|
|
h := m.Health(context.Background(), nil)
|
|
for _, c := range calls {
|
|
if strings.Contains(c, "GetStats") && !strings.HasPrefix(c, "sudo -n ") {
|
|
t.Fatalf("Debug.Stats asked as the account, which the bus logs as a denial: %s", c)
|
|
}
|
|
}
|
|
if h["connections"] != 2 || h["unit"] != "dbus-broker.service" {
|
|
t.Fatalf("%v", h)
|
|
}
|
|
}
|
|
|
|
func TestTheCheckReadsPackagesUnitsServiceFilesAndDenials(t *testing.T) {
|
|
m := testMachine(t, map[string]string{
|
|
"/var/lib/pacman/local/dbus-1.16.2-1/desc": "%NAME%\ndbus\n\n%VERSION%\n1.16.2-1\n\n%INSTALLDATE%\n1741340000\n",
|
|
"/var/lib/pacman/local/dbus-broker-37-3/desc": "%NAME%\ndbus-broker\n\n%VERSION%\n37-3\n\n%INSTALLDATE%\n1800000000\n",
|
|
"/var/lib/pacman/local/dbus-broker-units-37-3/desc": "%NAME%\ndbus-broker-units\n\n%VERSION%\n37-3\n\n%INSTALLDATE%\n1772097880\n",
|
|
"/usr/share/dbus-1/system-services/org.bluez.service": "[D-BUS Service]\nName=org.bluez\nExec=/bin/false\nUser=root\nSystemdService=dbus-org.bluez.service\n",
|
|
"/usr/share/dbus-1/system-services/org.example.service": "[D-BUS Service]\nName=org.example\nSystemdService=example.service\n",
|
|
"/usr/share/dbus-1/system-services/org.freedesktop.systemd1.service": "[D-BUS Service]\nName=org.freedesktop.systemd1\nExec=/bin/false\n",
|
|
})
|
|
m.Run = func(_ context.Context, name string, args ...string) (string, error) {
|
|
line := strings.Join(args, " ")
|
|
switch {
|
|
case name == "journalctl":
|
|
return journalLines, nil
|
|
case strings.Contains(line, "Id,LoadState"):
|
|
return "Id=dbus-org.bluez.service\nLoadState=not-found\n\nId=example.service\nLoadState=not-found\n", nil
|
|
default:
|
|
return "Id=dbus-broker.service\nActiveState=active\nMainPID=807\nActiveEnterTimestamp=@1791123478\n", nil
|
|
}
|
|
}
|
|
got := m.Check(context.Background(), nil)
|
|
byName := map[string]Check{}
|
|
for _, c := range got["checks"].([]Check) {
|
|
byName[c.Name] = c
|
|
}
|
|
if !byName["package dbus-broker-units"].OK || !byName["system bus"].OK {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
if c := byName["running bus is the installed one"]; c.OK || !strings.Contains(c.Detail, "dbus-broker 37-3") {
|
|
t.Fatalf("an upgraded broker was not said: %+v", c)
|
|
}
|
|
if c := byName["activatable services"]; c.OK || !strings.Contains(c.Detail, "org.example → example.service") || strings.Contains(c.Detail, "bluez") {
|
|
t.Fatalf("%+v", c)
|
|
}
|
|
if c := byName["policy denials in the last hour"]; c.OK || !strings.Contains(c.Detail, "GetStats") {
|
|
t.Fatalf("%+v", c)
|
|
}
|
|
if !strings.Contains(strings.Join(got["notes"].([]string), " "), "org.bluez → dbus-org.bluez.service") {
|
|
t.Fatalf("%v", got["notes"])
|
|
}
|
|
}
|