From 9e1bc6d33782fb81908ba32daa3dbf375d2e2b03 Mon Sep 17 00:00:00 2001 From: Dylan Myers Date: Tue, 16 Dec 2025 11:12:59 -0500 Subject: [PATCH 1/3] Replace AIX uptime function with ps etimes-based implementation Previously, the AIX host module parsed the output of the `uptime` command to determine system uptime. This approach had limitations in terms of reliability and parsing complexity due to the variable output format of the uptime command. This change replaces the uptime-based approach with a more reliable method: querying PID 1's elapsed time using `ps -o etimes -p 1`. This directly accesses the process table rather than relying on human-readable output. The new implementation handles three possible etimes output formats: - DAYS-HOURS:MINUTES:SECONDS (e.g., "124-01:40:39") - HOURS:MINUTES:SECONDS (e.g., "15:03:02") - MINUTES:SECONDS (e.g., "01:02") - from just-rebooted systems where hours are omitted when zero Changes: - Updated UptimeWithContext() to execute `ps -o etimes -p 1` - Added validation to ensure at least 2 rows of output (header + data) - Modified parseUptime() to handle the new etimes format with flexible time component parsing - Added graceful error handling that returns 0 for malformed input - Updated function comments to document the new approach and formats Tests: - Replaced all test cases to use the new etimes format - Added test coverage for all three time format variations - Updated invalid input test cases to reflect new parsing logic --- host/host_aix.go | 106 +++++++++++++++++++++++------------------- host/host_aix_test.go | 28 +++++++---- 2 files changed, 77 insertions(+), 57 deletions(-) diff --git a/host/host_aix.go b/host/host_aix.go index 2e8fb5539c..1a65764799 100644 --- a/host/host_aix.go +++ b/host/host_aix.go @@ -45,79 +45,89 @@ func BootTimeWithContext(ctx context.Context) (btime uint64, err error) { return timeSince(ut), nil } -// Parses result from uptime into minutes -// Some examples of uptime output that this command handles: -// 11:54AM up 13 mins, 1 user, load average: 2.78, 2.62, 1.79 -// 12:41PM up 1 hr, 1 user, load average: 2.47, 2.85, 2.83 -// 07:43PM up 5 hrs, 1 user, load average: 3.27, 2.91, 2.72 -// 11:18:23 up 83 days, 18:29, 4 users, load average: 0.16, 0.03, 0.01 -// 08:47PM up 2 days, 20 hrs, 1 user, load average: 2.47, 2.17, 2.17 -// 01:16AM up 4 days, 29 mins, 1 user, load average: 2.29, 2.31, 2.21 +// 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) func UptimeWithContext(ctx context.Context) (uint64, error) { - out, err := invoke.CommandWithContext(ctx, "uptime") + out, err := invoke.CommandWithContext(ctx, "ps", "-o", "etimes", "-p", "1") if err != nil { return 0, err } - return parseUptime(string(out)), nil + 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 } -func parseUptime(uptime string) uint64 { - ut := strings.Fields(uptime) - var days, hours, mins uint64 - var err error +// 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 + } - switch ut[3] { - case "day,", "days,": - days, err = strconv.ParseUint(ut[2], 10, 64) + var err error + days, err = strconv.ParseUint(parts[0], 10, 64) if err != nil { return 0 } - // day provided along with a single hour or hours - // ie: up 2 days, 20 hrs, - if ut[5] == "hr," || ut[5] == "hrs," { - hours, err = strconv.ParseUint(ut[4], 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, ":") + if len(timeParts) == 3 { + // HH:MM:SS format + var err error + hours, err = strconv.ParseUint(timeParts[0], 10, 64) + if err != nil { + return 0 } - // mins provided along with a single min or mins - // ie: up 4 days, 29 mins, - if ut[5] == "min," || ut[5] == "mins," { - mins, err = strconv.ParseUint(ut[4], 10, 64) - if err != nil { - return 0 - } + mins, err = strconv.ParseUint(timeParts[1], 10, 64) + if err != nil { + return 0 } - // alternatively day provided with hh:mm - // ie: up 83 days, 18:29 - if strings.Contains(ut[4], ":") { - hm := strings.Split(ut[4], ":") - hours, err = strconv.ParseUint(hm[0], 10, 64) - if err != nil { - return 0 - } - mins, err = strconv.ParseUint(strings.Trim(hm[1], ","), 10, 64) - if err != nil { - return 0 - } + secs, err = strconv.ParseUint(timeParts[2], 10, 64) + if err != nil { + return 0 } - case "hr,", "hrs,": - hours, err = strconv.ParseUint(ut[2], 10, 64) + } else if len(timeParts) == 2 { + // MM:SS format (just-rebooted systems) + var err error + mins, err = strconv.ParseUint(timeParts[0], 10, 64) if err != nil { return 0 } - case "min,", "mins,": - mins, err = strconv.ParseUint(ut[2], 10, 64) + + secs, err = strconv.ParseUint(timeParts[1], 10, 64) if err != nil { return 0 } + } else { + return 0 } - return (days * 24 * 60) + (hours * 60) + mins + // Convert to total minutes + totalMinutes := (days * 24 * 60) + (hours * 60) + mins + (secs / 60) + return totalMinutes } // This is a weak implementation due to the limitations on retrieving this data in AIX diff --git a/host/host_aix_test.go b/host/host_aix_test.go index 934766f7b2..8d5fdd8cf5 100644 --- a/host/host_aix_test.go +++ b/host/host_aix_test.go @@ -14,12 +14,19 @@ func TestParseUptimeValidInput(t *testing.T) { input string expected uint64 }{ - {"11:54AM up 13 mins, 1 user, load average: 2.78, 2.62, 1.79", 13}, - {"12:41PM up 1 hr, 1 user, load average: 2.47, 2.85, 2.83", 60}, - {"07:43PM up 5 hrs, 1 user, load average: 3.27, 2.91, 2.72", 300}, - {"11:18:23 up 83 days, 18:29, 4 users, load average: 0.16, 0.03, 0.01", 120629}, - {"08:47PM up 2 days, 20 hrs, 1 user, load average: 2.47, 2.17, 2.17", 4080}, - {"01:16AM up 4 days, 29 mins, 1 user, load average: 2.29, 2.31, 2.21", 5789}, + // 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) @@ -29,9 +36,12 @@ func TestParseUptimeValidInput(t *testing.T) { func TestParseUptimeInvalidInput(t *testing.T) { testCases := []string{ - "", // blank - "2x", // invalid string - "150", // integer + "", // blank + "invalid", // invalid string + "01:02", // incomplete time format (missing seconds) + "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 { From df0746d6f8b799e2bfd216c5a89c0ce4bc0ecf5e Mon Sep 17 00:00:00 2001 From: Dylan Myers Date: Tue, 16 Dec 2025 11:22:46 -0500 Subject: [PATCH 2/3] Fix linter issues in AIX uptime implementation - Convert if-else chain to switch statement in parseUptime() to satisfy gocritic - Format files with goimports/gofmt to resolve gci formatting issues These changes address all remaining linter warnings reported by golangci-lint. --- host/host_aix.go | 8 +++++--- host/host_aix_test.go | 14 +++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/host/host_aix.go b/host/host_aix.go index 1a65764799..0f300be194 100644 --- a/host/host_aix.go +++ b/host/host_aix.go @@ -49,6 +49,7 @@ func BootTimeWithContext(ctx context.Context) (btime uint64, err error) { // 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 { @@ -92,7 +93,8 @@ func parseUptime(etimes string) uint64 { // Parse time portions (either HH:MM:SS or MM:SS) timeParts := strings.Split(etimes, ":") - if len(timeParts) == 3 { + switch len(timeParts) { + case 3: // HH:MM:SS format var err error hours, err = strconv.ParseUint(timeParts[0], 10, 64) @@ -109,7 +111,7 @@ func parseUptime(etimes string) uint64 { if err != nil { return 0 } - } else if len(timeParts) == 2 { + case 2: // MM:SS format (just-rebooted systems) var err error mins, err = strconv.ParseUint(timeParts[0], 10, 64) @@ -121,7 +123,7 @@ func parseUptime(etimes string) uint64 { if err != nil { return 0 } - } else { + default: return 0 } diff --git a/host/host_aix_test.go b/host/host_aix_test.go index 8d5fdd8cf5..87b5680d3d 100644 --- a/host/host_aix_test.go +++ b/host/host_aix_test.go @@ -15,17 +15,17 @@ func TestParseUptimeValidInput(t *testing.T) { 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 + {"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 + {"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 { @@ -36,7 +36,7 @@ func TestParseUptimeValidInput(t *testing.T) { func TestParseUptimeInvalidInput(t *testing.T) { testCases := []string{ - "", // blank + "", // blank "invalid", // invalid string "01:02", // incomplete time format (missing seconds) "1-2:3", // incomplete time format after dash From 0ae21a0cbd37d0cb58a266ec316e6abfb27621e5 Mon Sep 17 00:00:00 2001 From: Dylan Myers Date: Mon, 22 Dec 2025 08:35:12 -0500 Subject: [PATCH 3/3] Remove incorrect test for invalid formats --- host/host_aix_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/host/host_aix_test.go b/host/host_aix_test.go index 87b5680d3d..485fe9fccc 100644 --- a/host/host_aix_test.go +++ b/host/host_aix_test.go @@ -38,7 +38,6 @@ func TestParseUptimeInvalidInput(t *testing.T) { testCases := []string{ "", // blank "invalid", // invalid string - "01:02", // incomplete time format (missing seconds) "1-2:3", // incomplete time format after dash "abc-01:02:03", // non-numeric days "1-ab:02:03", // non-numeric hours