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
10 changes: 9 additions & 1 deletion .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,12 @@
- changed-files:
- any-glob-to-any-file:
- '**/*_solaris.go'
- '**/*_posix.go'
- '**/*_posix.go'

'os:aix':
- changed-files:
- any-glob-to-any-file:
- '**/*_aix.go'
- '**/*_aix_test.go'
- '**/*_aix_*.go'
- '**/*_posix.go'
97 changes: 2 additions & 95 deletions host/host_aix.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package host

import (
"context"
"errors"
"strconv"
"strings"

Expand All @@ -32,104 +31,12 @@ func numProcs(_ context.Context) (uint64, error) {
}

func BootTimeWithContext(ctx context.Context) (btime uint64, err error) {
ut, err := UptimeWithContext(ctx)
if err != nil {
return 0, err
}

if ut <= 0 {
return 0, errors.New("uptime was not set, so cannot calculate boot time from it")
}

ut *= 60
return timeSince(ut), nil
return common.BootTimeWithContext(ctx, invoke)
}

// Uses ps to get the elapsed time for PID 1 in DAYS-HOURS:MINUTES:SECONDS format.
// Examples of ps -o etimes -p 1 output:
// 124-01:40:39 (with days)
// 15:03:02 (without days, hours only)
// 01:02 (just-rebooted systems, minutes and seconds)
func UptimeWithContext(ctx context.Context) (uint64, error) {
out, err := invoke.CommandWithContext(ctx, "ps", "-o", "etimes", "-p", "1")
if err != nil {
return 0, err
}

lines := strings.Split(strings.TrimSpace(string(out)), "\n")
if len(lines) < 2 {
return 0, errors.New("ps output has fewer than 2 rows")
}

// Extract the etimes value from the second row, trimming whitespace
etimes := strings.TrimSpace(lines[1])
return parseUptime(etimes), nil
}

// Parses etimes output from ps command into total minutes.
// Handles formats like:
// - "124-01:40:39" (DAYS-HOURS:MINUTES:SECONDS)
// - "15:03:02" (HOURS:MINUTES:SECONDS)
// - "01:02" (MINUTES:SECONDS, from just-rebooted systems)
func parseUptime(etimes string) uint64 {
var days, hours, mins, secs uint64

// Check if days component is present (contains a dash)
if strings.Contains(etimes, "-") {
parts := strings.Split(etimes, "-")
if len(parts) != 2 {
return 0
}

var err error
days, err = strconv.ParseUint(parts[0], 10, 64)
if err != nil {
return 0
}

// Parse the HH:MM:SS portion
etimes = parts[1]
}

// Parse time portions (either HH:MM:SS or MM:SS)
timeParts := strings.Split(etimes, ":")
switch len(timeParts) {
case 3:
// HH:MM:SS format
var err error
hours, err = strconv.ParseUint(timeParts[0], 10, 64)
if err != nil {
return 0
}

mins, err = strconv.ParseUint(timeParts[1], 10, 64)
if err != nil {
return 0
}

secs, err = strconv.ParseUint(timeParts[2], 10, 64)
if err != nil {
return 0
}
case 2:
// MM:SS format (just-rebooted systems)
var err error
mins, err = strconv.ParseUint(timeParts[0], 10, 64)
if err != nil {
return 0
}

secs, err = strconv.ParseUint(timeParts[1], 10, 64)
if err != nil {
return 0
}
default:
return 0
}

// Convert to total minutes
totalMinutes := (days * 24 * 60) + (hours * 60) + mins + (secs / 60)
return totalMinutes
return common.UptimeWithContext(ctx, invoke)
}

// This is a weak implementation due to the limitations on retrieving this data in AIX
Expand Down
50 changes: 14 additions & 36 deletions host/host_aix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,25 @@
package host

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseUptimeValidInput(t *testing.T) {
testCases := []struct {
input string
expected uint64
}{
// Format: MINUTES:SECONDS (just-rebooted systems, hours dropped when 0)
{"00:13", 0}, // 13 seconds rounds down to 0 minutes
{"01:00", 1}, // 1 minute
{"01:02", 1}, // 1 minute, 2 seconds
// Format: HOURS:MINUTES:SECONDS (no days, hours > 0)
{"01:00:00", 60}, // 1 hour
{"05:00:00", 300}, // 5 hours
{"15:03:02", 903}, // 15 hours, 3 minutes, 2 seconds
// Format: DAYS-HOURS:MINUTES:SECONDS (with days)
{"2-20:00:00", 4080}, // 2 days, 20 hours
{"4-00:29:00", 5789}, // 4 days, 29 minutes
{"83-18:29:00", 120629}, // 83 days, 18 hours, 29 minutes
{"124-01:40:39", 178660}, // 124 days, 1 hour, 40 minutes, 39 seconds
}
for _, tc := range testCases {
got := parseUptime(tc.input)
assert.Equalf(t, tc.expected, got, "parseUptime(%q) = %v, want %v", tc.input, got, tc.expected)
}
func TestBootTimeWithContext(t *testing.T) {
// This is a wrapper function that delegates to common.BootTimeWithContext
// Actual implementation testing is done in common_aix_test.go
bootTime, err := BootTimeWithContext(context.TODO())
require.NoError(t, err)
assert.Positive(t, bootTime)
}

func TestParseUptimeInvalidInput(t *testing.T) {
testCases := []string{
"", // blank
"invalid", // invalid string
"1-2:3", // incomplete time format after dash
"abc-01:02:03", // non-numeric days
"1-ab:02:03", // non-numeric hours
}

for _, tc := range testCases {
got := parseUptime(tc)
assert.LessOrEqualf(t, got, 0, "parseUptime(%q) expected zero to be returned, received %v", tc, got)
}
func TestUptimeWithContext(t *testing.T) {
// This is a wrapper function that delegates to common.UptimeWithContext
// Actual implementation testing is done in common_aix_test.go
uptime, err := UptimeWithContext(context.TODO())
require.NoError(t, err)
assert.Positive(t, uptime)
}
5 changes: 5 additions & 0 deletions internal/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package common
// - linux (amd64, arm)
// - freebsd (amd64)
// - windows (amd64)
// - aix (ppc64)

import (
"bufio"
Expand Down Expand Up @@ -464,3 +465,7 @@ func Round(val float64, n int) float64 {
// Multiply the value by pow10, round it, then divide it by pow10
return math.Round(val*pow10) / pow10
}

func timeSince(ts uint64) uint64 {
return uint64(time.Now().Unix()) - ts
}
131 changes: 131 additions & 0 deletions internal/common/common_aix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build aix

package common

import (
"context"
"errors"
"strconv"
"strings"
)

func BootTimeWithContext(ctx context.Context, invoke Invoker) (btime uint64, err error) {
ut, err := UptimeWithContext(ctx, invoke)
if err != nil {
return 0, err
}

if ut <= 0 {
return 0, errors.New("uptime was not set, so cannot calculate boot time from it")
}

return timeSince(ut), nil
}

// Uses ps to get the elapsed time for PID 1 in DAYS-HOURS:MINUTES:SECONDS format.
// Examples of ps -o etimes -p 1 output:
// 124-01:40:39 (with days)
// 15:03:02 (without days, hours only)
// 01:02 (just-rebooted systems, minutes and seconds)
func UptimeWithContext(ctx context.Context, invoke Invoker) (uint64, error) {
out, err := invoke.CommandWithContext(ctx, "ps", "-o", "etimes", "-p", "1")
if err != nil {
return 0, err
}

lines := strings.Split(strings.TrimSpace(string(out)), "\n")
if len(lines) < 2 {
return 0, errors.New("ps output has fewer than 2 rows")
}

// Extract the etimes value from the second row, trimming whitespace
etimes := strings.TrimSpace(lines[1])
return ParseUptime(etimes), nil
}

// Parses etimes output from ps command into total seconds.
// Handles formats like:
// - "124-01:40:39" (DAYS-HOURS:MINUTES:SECONDS)
// - "15:03:02" (HOURS:MINUTES:SECONDS)
// - "01:02" (MINUTES:SECONDS, from just-rebooted systems)
func ParseUptime(etimes string) uint64 {
var days, hours, mins, secs uint64

// Check if days component is present (contains a dash)
if strings.Contains(etimes, "-") {
parts := strings.Split(etimes, "-")
if len(parts) != 2 {
return 0
}

var err error
days, err = strconv.ParseUint(parts[0], 10, 64)
if err != nil {
return 0
}

// Parse the HH:MM:SS portion (after days, must have 3 parts)
etimes = parts[1]
timeParts := strings.Split(etimes, ":")
if len(timeParts) != 3 {
return 0
}

var err2 error
hours, err2 = strconv.ParseUint(timeParts[0], 10, 64)
if err2 != nil {
return 0
}

mins, err2 = strconv.ParseUint(timeParts[1], 10, 64)
if err2 != nil {
return 0
}

secs, err2 = strconv.ParseUint(timeParts[2], 10, 64)
if err2 != nil {
return 0
}
} else {
// Parse time portions (either HH:MM:SS or MM:SS) when no days present
timeParts := strings.Split(etimes, ":")
switch len(timeParts) {
case 3:
// HH:MM:SS format
var err error
hours, err = strconv.ParseUint(timeParts[0], 10, 64)
if err != nil {
return 0
}

mins, err = strconv.ParseUint(timeParts[1], 10, 64)
if err != nil {
return 0
}

secs, err = strconv.ParseUint(timeParts[2], 10, 64)
if err != nil {
return 0
}
case 2:
// MM:SS format (just-rebooted systems)
var err error
mins, err = strconv.ParseUint(timeParts[0], 10, 64)
if err != nil {
return 0
}

secs, err = strconv.ParseUint(timeParts[1], 10, 64)
if err != nil {
return 0
}
default:
return 0
}
}

// Convert to total seconds
totalSeconds := (days * 24 * 60 * 60) + (hours * 60 * 60) + (mins * 60) + secs
return totalSeconds
}
50 changes: 50 additions & 0 deletions internal/common/common_aix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build aix

package common

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestParseUptimeValidInput(t *testing.T) {
testCases := []struct {
input string
expected uint64
}{
// Format: MINUTES:SECONDS (just-rebooted systems, hours dropped when 0)
{"00:13", 13}, // 13 seconds
{"01:00", 60}, // 1 minute
{"01:02", 62}, // 1 minute, 2 seconds
// Format: HOURS:MINUTES:SECONDS (no days, hours > 0)
{"01:00:00", 3600}, // 1 hour
{"05:00:00", 18000}, // 5 hours
{"15:03:02", 54182}, // 15 hours, 3 minutes, 2 seconds
// Format: DAYS-HOURS:MINUTES:SECONDS (with days)
{"2-20:00:00", 244800}, // 2 days, 20 hours
{"4-00:29:00", 347340}, // 4 days, 29 minutes
{"83-18:29:00", 7237740}, // 83 days, 18 hours, 29 minutes
{"124-01:40:39", 10719639}, // 124 days, 1 hour, 40 minutes, 39 seconds
}
for _, tc := range testCases {
got := ParseUptime(tc.input)
assert.Equalf(t, tc.expected, got, "ParseUptime(%q) = %v, want %v", tc.input, got, tc.expected)
}
}

func TestParseUptimeInvalidInput(t *testing.T) {
testCases := []string{
"", // blank
"invalid", // invalid string
"1-2:3", // incomplete time format after dash
"abc-01:02:03", // non-numeric days
"1-ab:02:03", // non-numeric hours
}

for _, tc := range testCases {
got := ParseUptime(tc)
assert.Equalf(t, uint64(0), got, "ParseUptime(%q) expected zero to be returned, received %v", tc, got)
}
}