Files
mesh-catalog/modules/power/cmd/power/history.go
T

103 lines
3.3 KiB
Go

package main
import (
"context"
"regexp"
"strings"
)
// Event is one thing that happened to the machine's power, as the journal recorded it.
type Event struct {
At string `json:"at"`
What string `json:"what"`
Detail string `json:"detail,omitempty"`
}
var (
sleptLine = regexp.MustCompile(`(?i)(performing sleep operation|entering sleep state|suspending system)`)
wokeLine = regexp.MustCompile(`(?i)(returned from sleep|system resumed|resumed from)`)
bootsLine = regexp.MustCompile(`^\s*(-?\d+)\s+([0-9a-f]{32})\s+(\S+\s+\S+\s+\S+(?:\s+\S+)?)\s+(?:—|-)\s+(\S+\s+\S+\s+\S+(?:\s+\S+)?)\s*$`)
)
// journal reads the system journal: as the account first, as root when the account may not.
func (m *Machine) journal(ctx context.Context, args ...string) (string, error) {
out, err := m.Run(ctx, "journalctl", args...)
if err == nil && strings.TrimSpace(out) != "" {
return out, nil
}
return m.privileged(ctx, "journalctl", args...)
}
// History is the machine's recent power events, newest last: each boot and its last entry, and every
// sleep, wake and moment run the journal recorded in the boots asked for.
func (m *Machine) History(ctx context.Context, boots int) (map[string]any, error) {
if boots < 1 {
boots = 3
}
var events []Event
list, err := m.journal(ctx, "--list-boots", "--no-pager", "-q")
if err != nil {
return nil, err
}
lines := nonEmpty(strings.Split(list, "\n"))
if len(lines) > boots {
lines = lines[len(lines)-boots:]
}
for i, l := range lines {
f := bootsLine.FindStringSubmatch(l)
if f == nil {
continue
}
events = append(events, Event{At: f[3], What: "booted", Detail: "boot " + f[1]})
if i < len(lines)-1 {
events = append(events, Event{At: f[4], What: "last entry before the next boot", Detail: "boot " + f[1]})
}
got, err := m.journal(ctx, "-b", f[1], "-q", "--no-pager", "-o", "short-iso",
"-t", "systemd-sleep", "-t", "mesh-power", "-t", "systemd-logind")
if err != nil {
continue
}
for _, e := range strings.Split(got, "\n") {
at, rest, ok := splitJournalLine(e)
if !ok {
continue
}
switch {
case sleptLine.MatchString(rest):
events = append(events, Event{At: at, What: "sleeping", Detail: rest})
case wokeLine.MatchString(rest):
events = append(events, Event{At: at, What: "woke", Detail: rest})
case strings.Contains(rest, "mesh-power"):
events = append(events, Event{At: at, What: "moment", Detail: rest})
case strings.Contains(rest, "Power key pressed") || strings.Contains(rest, "Lid closed") ||
strings.Contains(rest, "Lid opened") || strings.Contains(rest, "System is powering down") ||
strings.Contains(rest, "System is rebooting"):
events = append(events, Event{At: at, What: "logind", Detail: rest})
}
}
}
if len(events) > 200 {
events = events[len(events)-200:]
}
return map[string]any{"boots": boots, "events": orEmpty(events)}, nil
}
// splitJournalLine is a short-iso line's time and the rest (unit and message).
func splitJournalLine(line string) (string, string, bool) {
f := strings.SplitN(strings.TrimSpace(line), " ", 3)
if len(f) < 3 || !strings.Contains(f[0], "T") {
return "", "", false
}
return f[0], f[2], true
}
func nonEmpty(lines []string) []string {
var out []string
for _, l := range lines {
if strings.TrimSpace(l) != "" {
out = append(out, l)
}
}
return out
}