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.
291 lines
9.2 KiB
Go
291 lines
9.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Packages are the bus implementation's packages, as the manifest declares them: dbus-broker (the
|
|
// bus), its units (the dbus.service alias), and the reference package, which ships the bus's
|
|
// configuration, the socket that starts it at boot, and libdbus.
|
|
var Packages = []string{"dbus", "dbus-broker", "dbus-broker-units"}
|
|
|
|
// RunningPackages are the packages whose files the running bus loaded when it started: a newer one
|
|
// installed since takes effect only at the next boot (novox/hq ADR 0215 §2).
|
|
var RunningPackages = []string{"dbus-broker", "dbus"}
|
|
|
|
// SystemUnit is the bus's real unit; dbus.service is its alias.
|
|
const SystemUnit = "dbus-broker.service"
|
|
|
|
// ServiceDirs are where activatable system services are described.
|
|
var ServiceDirs = []string{"/usr/share/dbus-1/system-services", "/usr/local/share/dbus-1/system-services",
|
|
"/usr/lib/dbus-1/system-services"}
|
|
|
|
// Package is one installed package as the package manager's local database records it.
|
|
type Package struct {
|
|
Name string
|
|
Version string
|
|
Installed time.Time
|
|
}
|
|
|
|
// ParseDesc reads one package's desc file in the local database.
|
|
func ParseDesc(desc string) Package {
|
|
var p Package
|
|
lines := strings.Split(desc, "\n")
|
|
for i := 0; i+1 < len(lines); i++ {
|
|
v := strings.TrimSpace(lines[i+1])
|
|
switch strings.TrimSpace(lines[i]) {
|
|
case "%NAME%":
|
|
p.Name = v
|
|
case "%VERSION%":
|
|
p.Version = v
|
|
case "%INSTALLDATE%":
|
|
if n, err := strconv.ParseInt(v, 10, 64); err == nil {
|
|
p.Installed = time.Unix(n, 0)
|
|
}
|
|
}
|
|
}
|
|
return p
|
|
}
|
|
|
|
// InstalledPackage reads a package from the local database, without running the package manager.
|
|
func (m *Machine) InstalledPackage(name string) (Package, bool) {
|
|
for _, d := range m.glob("/var/lib/pacman/local/" + name + "-*/desc") {
|
|
if p := ParseDesc(m.read(d) + "\n"); p.Name == name {
|
|
return p, true
|
|
}
|
|
}
|
|
return Package{}, false
|
|
}
|
|
|
|
// ParseShow reads `systemctl show` blocks: one map per unit, in the order asked.
|
|
func ParseShow(out string) []map[string]string {
|
|
var blocks []map[string]string
|
|
cur := map[string]string{}
|
|
for _, line := range strings.Split(out, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
if len(cur) > 0 {
|
|
blocks = append(blocks, cur)
|
|
cur = map[string]string{}
|
|
}
|
|
continue
|
|
}
|
|
if k, v, ok := strings.Cut(line, "="); ok {
|
|
cur[k] = v
|
|
}
|
|
}
|
|
if len(cur) > 0 {
|
|
blocks = append(blocks, cur)
|
|
}
|
|
return blocks
|
|
}
|
|
|
|
// unixStamp reads systemd's "@<seconds>" timestamp.
|
|
func unixStamp(s string) (time.Time, bool) {
|
|
n, err := strconv.ParseInt(strings.TrimPrefix(s, "@"), 10, 64)
|
|
if err != nil || n == 0 {
|
|
return time.Time{}, false
|
|
}
|
|
return time.Unix(n, 0), true
|
|
}
|
|
|
|
// BusUnit is the system bus's unit as the service manager has it.
|
|
type BusUnit struct {
|
|
Unit string `json:"unit"`
|
|
Active string `json:"active"`
|
|
PID uint32 `json:"pid,omitempty"`
|
|
Since time.Time `json:"-"`
|
|
Started string `json:"started,omitempty"`
|
|
}
|
|
|
|
// SystemBusUnit asks the service manager about the system bus through its alias, so the answer is
|
|
// the implementation's unit whichever it is.
|
|
func (m *Machine) SystemBusUnit(ctx context.Context) (BusUnit, error) {
|
|
out, err := m.Run(ctx, "systemctl", "show", "dbus.service", "-p", "Id,ActiveState,MainPID,ActiveEnterTimestamp",
|
|
"--timestamp=unix")
|
|
if err != nil {
|
|
return BusUnit{}, err
|
|
}
|
|
b := ParseShow(out)
|
|
if len(b) == 0 {
|
|
return BusUnit{}, fmt.Errorf("systemctl show answered nothing for dbus.service")
|
|
}
|
|
u := BusUnit{Unit: b[0]["Id"], Active: b[0]["ActiveState"], PID: parsePID(b[0]["MainPID"])}
|
|
if t, ok := unixStamp(b[0]["ActiveEnterTimestamp"]); ok {
|
|
u.Since, u.Started = t, t.UTC().Format(time.RFC3339)
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
// ServiceFile is one activatable system service's description.
|
|
type ServiceFile struct {
|
|
File string
|
|
Name string
|
|
Unit string
|
|
}
|
|
|
|
// ParseServiceFile reads the Name and SystemdService of a D-Bus service file.
|
|
func ParseServiceFile(file, content string) ServiceFile {
|
|
s := ServiceFile{File: file}
|
|
for _, line := range strings.Split(content, "\n") {
|
|
k, v, ok := strings.Cut(strings.TrimSpace(line), "=")
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch strings.TrimSpace(k) {
|
|
case "Name":
|
|
s.Name = strings.TrimSpace(v)
|
|
case "SystemdService":
|
|
s.Unit = strings.TrimSpace(v)
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Check is one thing the module expects of the machine.
|
|
type Check struct {
|
|
Name string `json:"name"`
|
|
OK bool `json:"ok"`
|
|
Detail string `json:"detail"`
|
|
}
|
|
|
|
// RebootDue says, per package the running bus loaded, whether a newer one was installed after the bus
|
|
// started: the bus is never restarted live, so that package waits for a boot.
|
|
func RebootDue(started time.Time, pkgs []Package) (bool, string) {
|
|
var newer []string
|
|
for _, p := range pkgs {
|
|
if !p.Installed.IsZero() && p.Installed.After(started) {
|
|
newer = append(newer, fmt.Sprintf("%s %s installed %s", p.Name, p.Version, p.Installed.UTC().Format(time.RFC3339)))
|
|
}
|
|
}
|
|
if len(newer) == 0 {
|
|
return false, "the running bus started " + started.UTC().Format(time.RFC3339) + ", after every package it loaded was installed"
|
|
}
|
|
return true, "the running bus started " + started.UTC().Format(time.RFC3339) + " and is older than " +
|
|
strings.Join(newer, ", ") + ": a reboot is due (the bus is never restarted live, novox/hq ADR 0215)"
|
|
}
|
|
|
|
// Check says what this module expects and whether the machine meets it.
|
|
func (m *Machine) Check(ctx context.Context, w *Watcher) map[string]any {
|
|
var checks []Check
|
|
var notes []string
|
|
add := func(name string, ok bool, format string, args ...any) {
|
|
checks = append(checks, Check{Name: name, OK: ok, Detail: fmt.Sprintf(format, args...)})
|
|
}
|
|
var running []Package
|
|
for _, name := range Packages {
|
|
p, ok := m.InstalledPackage(name)
|
|
add("package "+name, ok, "installed: %v %s", ok, p.Version)
|
|
for _, r := range RunningPackages {
|
|
if ok && r == name {
|
|
running = append(running, p)
|
|
}
|
|
}
|
|
}
|
|
unit, err := m.SystemBusUnit(ctx)
|
|
if err != nil {
|
|
add("system bus", false, "%v", err)
|
|
} else {
|
|
add("system bus", unit.Unit == SystemUnit && unit.Active == "active",
|
|
"dbus.service is %s, %s, pid %d, since %s (want %s active)", orWord(unit.Unit, "unknown"),
|
|
orWord(unit.Active, "unknown"), unit.PID, orWord(unit.Started, "unknown"), SystemUnit)
|
|
if !unit.Since.IsZero() {
|
|
due, detail := RebootDue(unit.Since, running)
|
|
add("running bus is the installed one", !due, "%s", detail)
|
|
}
|
|
}
|
|
|
|
var files []ServiceFile
|
|
for _, dir := range ServiceDirs {
|
|
for _, f := range m.glob(dir + "/*.service") {
|
|
if s := ParseServiceFile(f, m.read(f)); s.Unit != "" {
|
|
files = append(files, s)
|
|
}
|
|
}
|
|
}
|
|
sort.Slice(files, func(i, j int) bool { return files[i].Name < files[j].Name })
|
|
if len(files) > 0 {
|
|
args := []string{"show", "-p", "Id,LoadState"}
|
|
for _, f := range files {
|
|
args = append(args, f.Unit)
|
|
}
|
|
out, err := m.Run(ctx, "systemctl", args...)
|
|
blocks := ParseShow(out)
|
|
switch {
|
|
case err != nil:
|
|
add("activatable services", false, "systemctl show: %v", err)
|
|
case len(blocks) != len(files):
|
|
add("activatable services", false, "systemctl show answered %d units for %d service files", len(blocks), len(files))
|
|
default:
|
|
var broken, disabled []string
|
|
for i, f := range files {
|
|
if blocks[i]["LoadState"] != "not-found" {
|
|
continue
|
|
}
|
|
// systemd's own bus services are reached through a dbus-org.* alias that exists only
|
|
// while the service is enabled: a disabled one is a choice, not a fault.
|
|
if strings.HasPrefix(f.Unit, "dbus-org.") {
|
|
disabled = append(disabled, f.Name+" → "+f.Unit)
|
|
} else {
|
|
broken = append(broken, f.Name+" → "+f.Unit+" ("+f.File+")")
|
|
}
|
|
}
|
|
add("activatable services", len(broken) == 0, "%d service files; whose unit does not exist: %s",
|
|
len(files), orWord(strings.Join(broken, ", "), "none"))
|
|
if len(disabled) > 0 {
|
|
notes = append(notes, "activation fails for "+strings.Join(disabled, ", ")+
|
|
": the service is not enabled, so its dbus-org alias does not exist")
|
|
}
|
|
}
|
|
}
|
|
|
|
denials, _, err := m.Denials(ctx, "", m.Now().Add(-time.Hour))
|
|
if err != nil {
|
|
add("policy denials in the last hour", false, "reading the journal: %v", err)
|
|
} else {
|
|
seen := map[string]bool{}
|
|
var ex []string
|
|
for _, d := range denials {
|
|
k := strings.TrimSpace(d.Type + " " + d.Interface + "." + d.Member + " to " + d.Destination)
|
|
if !seen[k] && len(ex) < DenialExamples {
|
|
seen[k] = true
|
|
ex = append(ex, k)
|
|
}
|
|
}
|
|
add("policy denials in the last hour", len(denials) == 0, "%d: %s", len(denials), orWord(strings.Join(ex, "; "), "none"))
|
|
}
|
|
|
|
if a, err := m.SessionAddress(); err == nil {
|
|
notes = append(notes, "this account's session bus: "+a)
|
|
} else {
|
|
notes = append(notes, err.Error())
|
|
}
|
|
|
|
if w != nil {
|
|
s := w.Snapshot()
|
|
add("watcher", s.Connected && !s.Stalled, "connected %v, stalled %v%s, last ping %.1f ms, %d well-known names",
|
|
s.Connected, s.Stalled, orNote(s.StallReason), s.LastPingMS, s.Services)
|
|
add("events reach the mesh's bus", s.Pending == 0 && s.Problem == "", "%d event(s) waiting, %d dropped%s",
|
|
s.Pending, s.Dropped, orNote(s.Problem))
|
|
}
|
|
failing := 0
|
|
for _, c := range checks {
|
|
if !c.OK {
|
|
failing++
|
|
}
|
|
}
|
|
return map[string]any{"checks": checks, "failing": failing, "notes": notes}
|
|
}
|
|
|
|
func orNote(s string) string {
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
return " (" + s + ")"
|
|
}
|