103 lines
3.3 KiB
Go
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
|
|
}
|