diff --git a/internal/inventory/btime.go b/internal/inventory/btime.go new file mode 100644 index 0000000..58d22d8 --- /dev/null +++ b/internal/inventory/btime.go @@ -0,0 +1,19 @@ +package inventory + +import ( + "strconv" + "strings" +) + +// parseBtime reads the boot time, in Unix seconds, from /proc/stat. The +// control plane compares it with the moment it asked for a reboot: a report +// is only proof the host restarted if its boot time is later. +func parseBtime(stat string) int64 { + for _, line := range strings.Split(stat, "\n") { + if rest, ok := strings.CutPrefix(line, "btime "); ok { + v, _ := strconv.ParseInt(strings.TrimSpace(rest), 10, 64) + return v + } + } + return 0 +} diff --git a/internal/inventory/btime_test.go b/internal/inventory/btime_test.go new file mode 100644 index 0000000..349db6d --- /dev/null +++ b/internal/inventory/btime_test.go @@ -0,0 +1,16 @@ +package inventory + +import "testing" + +func TestParseBtime(t *testing.T) { + stat := "cpu 1 2 3 4\ncpu0 1 2 3 4\nintr 1\nctxt 99\nbtime 1757800000\nprocesses 5\n" + if got := parseBtime(stat); got != 1757800000 { + t.Fatalf("got %d", got) + } +} + +func TestParseBtimeMissing(t *testing.T) { + if got := parseBtime("cpu 1 2 3\n"); got != 0 { + t.Fatalf("got %d, want 0", got) + } +} diff --git a/internal/inventory/collect_linux.go b/internal/inventory/collect_linux.go index d2bbeb0..4150189 100644 --- a/internal/inventory/collect_linux.go +++ b/internal/inventory/collect_linux.go @@ -12,6 +12,7 @@ import ( ) func collect(r *pb.InventoryReport, includeStatic bool) { + r.BootTimeUnix = parseBtime(readProc("/proc/stat")) r.CPU.UsagePct = cpuUsage() r.CPU.Load1 = load1() memTotal, memAvail, swapTotal, swapFree := meminfo() diff --git a/internal/inventory/collect_windows.go b/internal/inventory/collect_windows.go index 5dea7ac..34b93b9 100644 --- a/internal/inventory/collect_windows.go +++ b/internal/inventory/collect_windows.go @@ -20,6 +20,8 @@ var ( ) func collect(r *pb.InventoryReport, includeStatic bool) { + // DurationSinceBoot returns milliseconds since boot and does not wrap. + r.BootTimeUnix = time.Now().Add(-windows.DurationSinceBoot()).Unix() r.CPU.UsagePct = cpuUsage() // Windows has no load average. Left at zero; the UI already treats it as // optional because it is omitempty on the wire.