Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion agent/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
a.systemInfo.MemPct = systemStats.MemPct
a.systemInfo.DiskPct = systemStats.DiskPct
a.systemInfo.Battery = systemStats.Battery
a.systemInfo.Uptime, _ = host.Uptime()
a.systemInfo.Uptime, _ = getUptime()
a.systemInfo.BandwidthBytes = systemStats.Bandwidth[0] + systemStats.Bandwidth[1]
a.systemInfo.Threads = a.systemDetails.Threads

Expand Down
44 changes: 44 additions & 0 deletions agent/uptime_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//go:build linux

package agent

import (
"math"
"os"
"strconv"
"strings"

"github.com/shirou/gopsutil/v4/host"
)

// uptimeFilePath is a variable so tests can point it at a fixture.
var uptimeFilePath = "/proc/uptime"

// getUptime returns the system uptime in seconds.
//
// This reads /proc/uptime instead of using host.Uptime(), which calls the
// sysinfo(2) syscall. Inside an LXC container lxcfs virtualizes /proc/uptime
// but cannot intercept a syscall, so sysinfo(2) reports the host's uptime
// rather than the container's.
//
// Falls back to host.Uptime() if /proc/uptime is missing or unparseable, so
// behavior is unchanged anywhere the file isn't available.
func getUptime() (uint64, error) {
data, err := os.ReadFile(uptimeFilePath)
if err != nil {
return host.Uptime()
}
fields := strings.Fields(string(data))
if len(fields) == 0 {
return host.Uptime()
}
seconds, err := strconv.ParseFloat(fields[0], 64)
if err != nil ||
math.IsNaN(seconds) ||
math.IsInf(seconds, 0) ||
seconds < 0 ||
seconds >= 1<<64 {
return host.Uptime()
}
return uint64(seconds), nil
}
101 changes: 101 additions & 0 deletions agent/uptime_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//go:build linux

package agent

import (
"os"
"path/filepath"
"testing"
)

func TestGetUptimeFromProc(t *testing.T) {
tests := []struct {
name string
contents string
want uint64
}{
{"typical", "12345.67 98765.43\n", 12345},
{"zero", "0.00 0.00\n", 0},
{"no trailing newline", "42.99 7.00", 42},
{"single field", "600.5", 600},
{"large value", "266030.12 1000000.00\n", 266030},
}

prev := uptimeFilePath
t.Cleanup(func() { uptimeFilePath = prev })

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "uptime")
if err := os.WriteFile(path, []byte(tt.contents), 0o644); err != nil {
t.Fatal(err)
}
uptimeFilePath = path

got, err := getUptime()
if err != nil {
t.Fatalf("getUptime() returned error: %v", err)
}
if got != tt.want {
t.Errorf("getUptime() = %d, want %d", got, tt.want)
}
})
}
}

func writeUptime(contents string) func(t *testing.T) string {
return func(t *testing.T) string {
path := filepath.Join(t.TempDir(), "uptime")
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
t.Fatal(err)
}
return path
}
}

// Malformed, missing, or out-of-range input must fall back to host.Uptime()
// rather than returning a bogus value, so the agent still reports something sane.
func TestGetUptimeFallsBack(t *testing.T) {
prev := uptimeFilePath
t.Cleanup(func() { uptimeFilePath = prev })

for _, tt := range []struct {
name string
prepare func(t *testing.T) string
}{
{"missing file", func(t *testing.T) string {
return filepath.Join(t.TempDir(), "does-not-exist")
}},
{"empty file", func(t *testing.T) string {
path := filepath.Join(t.TempDir(), "uptime")
if err := os.WriteFile(path, nil, 0o644); err != nil {
t.Fatal(err)
}
return path
}},
{"unparseable", func(t *testing.T) string {
path := filepath.Join(t.TempDir(), "uptime")
if err := os.WriteFile(path, []byte("not-a-number 1.0\n"), 0o644); err != nil {
t.Fatal(err)
}
return path
}},
{"NaN", writeUptime("NaN 1.0\n")},
{"positive infinity", writeUptime("+Inf 1.0\n")},
{"negative infinity", writeUptime("-Inf 1.0\n")},
{"negative", writeUptime("-42.5 1.0\n")},
{"exceeds uint64 range", writeUptime("1e20 1.0\n")},
} {
t.Run(tt.name, func(t *testing.T) {
uptimeFilePath = tt.prepare(t)

got, err := getUptime()
if err != nil {
t.Fatalf("getUptime() returned error: %v", err)
}
if got == 0 {
t.Error("getUptime() = 0, expected fallback to host.Uptime()")
}
})
}
}
10 changes: 10 additions & 0 deletions agent/uptime_stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//go:build !linux

package agent

import "github.com/shirou/gopsutil/v4/host"

// getUptime returns the system uptime in seconds.
func getUptime() (uint64, error) {
return host.Uptime()
}