Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
93b7e48
Empty commit to trigger CI
Feb 12, 2016
7d116c0
Poke CI
punya Oct 4, 2021
30908e4
Poke CI
punya Oct 6, 2021
e06c4cd
Poke CI
punya Oct 6, 2021
e28544d
Refactoring and prerequisites for AIX processes
Dylan-M Feb 21, 2025
ab3afff
Partially converted process_linx -> process_aix
Dylan-M Feb 21, 2025
244a88e
Fix missing parameter
Dylan-M Oct 28, 2025
b471a3b
Add test data and test file for AIX processes
Dylan-M Oct 28, 2025
1c79dd4
fix: correct lwp binary.Read and add splitProcStat defensive checks
Dylan-M Dec 16, 2025
87b965b
Fix AIX process metrics extraction
Dylan-M Dec 16, 2025
9f0e071
Add AIX status code mappings for cases 5-7
Dylan-M Dec 16, 2025
f257605
Implement MemoryMapsWithContext using procmap
Dylan-M Dec 16, 2025
9c1e9b6
feat(aix): Implement per-process connections and dynamic clock ticks
Dylan-M Dec 17, 2025
6a46cce
feat(nfs): Implement NFS metrics module for AIX
Dylan-M Dec 17, 2025
226b40b
fix(net): Improve per-process connection resolution on AIX
Dylan-M Dec 17, 2025
b74caa4
fix(load): Fix AIX process state counting in load.Misc()
Dylan-M Dec 17, 2025
d42e6c0
Fix AIX process metrics ps command syntax
Dylan-M Dec 17, 2025
381948a
Add AIX test coverage
Dylan-M Dec 17, 2025
29a5bca
Make host.InfoWithContext() resilient to metric failures
Dylan-M Dec 17, 2025
67b6401
Implement system metrics: syscalls, interrupts, context switches
Dylan-M Dec 17, 2025
5acb75d
Implement FD limits for AIX
Dylan-M Dec 17, 2025
8c5d48e
Add injectable invoker + cross-platform mock tests for load and host …
Dylan-M Dec 17, 2025
d44c8d8
Squashed PR/lint commits (b032c602..839531b1)
Dylan-M Dec 18, 2025
fb167ea
Implement process.cpu_utilization and process.signals_pending
Dylan-M Dec 17, 2025
ab493f8
Fix linting issues: use anonymous receiver for unimplemented function…
Dylan-M Dec 17, 2025
3cba5be
Add SignalsPendingWithContext stub to Darwin, OpenBSD, and Plan9; fix…
Dylan-M Dec 17, 2025
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
15 changes: 14 additions & 1 deletion .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
- any-glob-to-any-file:
- 'net/**'

'package:nfs':
- changed-files:
- any-glob-to-any-file:
- 'nfs/**'

'package:process':
- changed-files:
- any-glob-to-any-file:
Expand Down Expand Up @@ -109,4 +114,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'
23 changes: 14 additions & 9 deletions host/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,53 +65,58 @@ func Info() (*InfoStat, error) {

func InfoWithContext(ctx context.Context) (*InfoStat, error) {
var err error
var errs []error
ret := &InfoStat{
OS: runtime.GOOS,
}

ret.Hostname, err = os.Hostname()
if err != nil && !errors.Is(err, common.ErrNotImplementedError) {
return nil, fmt.Errorf("getting hostname: %w", err)
errs = append(errs, fmt.Errorf("getting hostname: %w", err))
}

ret.Platform, ret.PlatformFamily, ret.PlatformVersion, err = PlatformInformationWithContext(ctx)
if err != nil && !errors.Is(err, common.ErrNotImplementedError) {
return nil, fmt.Errorf("getting platform information: %w", err)
errs = append(errs, fmt.Errorf("getting platform information: %w", err))
}

ret.KernelVersion, err = KernelVersionWithContext(ctx)
if err != nil && !errors.Is(err, common.ErrNotImplementedError) {
return nil, fmt.Errorf("getting kernel version: %w", err)
errs = append(errs, fmt.Errorf("getting kernel version: %w", err))
}

ret.KernelArch, err = KernelArch()
if err != nil && !errors.Is(err, common.ErrNotImplementedError) {
return nil, fmt.Errorf("getting kernel architecture: %w", err)
errs = append(errs, fmt.Errorf("getting kernel architecture: %w", err))
}

ret.VirtualizationSystem, ret.VirtualizationRole, err = VirtualizationWithContext(ctx)
if err != nil && !errors.Is(err, common.ErrNotImplementedError) {
return nil, fmt.Errorf("getting virtualization information: %w", err)
errs = append(errs, fmt.Errorf("getting virtualization information: %w", err))
}

ret.BootTime, err = BootTimeWithContext(ctx)
if err != nil && !errors.Is(err, common.ErrNotImplementedError) {
return nil, fmt.Errorf("getting boot time: %w", err)
errs = append(errs, fmt.Errorf("getting boot time: %w", err))
}

ret.Uptime, err = UptimeWithContext(ctx)
if err != nil && !errors.Is(err, common.ErrNotImplementedError) {
return nil, fmt.Errorf("getting uptime: %w", err)
errs = append(errs, fmt.Errorf("getting uptime: %w", err))
}

ret.Procs, err = numProcs(ctx)
if err != nil && !errors.Is(err, common.ErrNotImplementedError) {
return nil, fmt.Errorf("getting number of procs: %w", err)
errs = append(errs, fmt.Errorf("getting number of procs: %w", err))
}

ret.HostID, err = HostIDWithContext(ctx)
if err != nil && !errors.Is(err, common.ErrNotImplementedError) {
return nil, fmt.Errorf("getting host ID: %w", err)
errs = append(errs, fmt.Errorf("getting host ID: %w", err))
}

if len(errs) > 0 {
return ret, errors.Join(errs...)
}

return ret, nil
Expand Down
138 changes: 60 additions & 78 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 @@ -17,8 +16,19 @@ const (
user_PROCESS = 7 //nolint:revive //FIXME
)

// testInvoker is used for dependency injection in tests
var testInvoker common.Invoker

// getInvoker returns the test invoker if set, otherwise returns the default
func getInvoker() common.Invoker {
if testInvoker != nil {
return testInvoker
}
return invoke
}

func HostIDWithContext(ctx context.Context) (string, error) {
out, err := invoke.CommandWithContext(ctx, "uname", "-u")
out, err := getInvoker().CommandWithContext(ctx, "uname", "-u")
if err != nil {
return "", err
}
Expand All @@ -32,17 +42,7 @@ 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, getInvoker())
}

// Parses result from uptime into minutes
Expand All @@ -54,76 +54,17 @@ func BootTimeWithContext(ctx context.Context) (btime uint64, err error) {
// 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
func UptimeWithContext(ctx context.Context) (uint64, error) {
out, err := invoke.CommandWithContext(ctx, "uptime")
if err != nil {
return 0, err
}

return parseUptime(string(out)), nil
return common.UptimeWithContext(ctx, invoke)
}

func parseUptime(uptime string) uint64 {
ut := strings.Fields(uptime)
var days, hours, mins uint64
var err error

switch ut[3] {
case "day,", "days,":
days, err = strconv.ParseUint(ut[2], 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
}
}

// 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
}
}

// 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
}
}
case "hr,", "hrs,":
hours, err = strconv.ParseUint(ut[2], 10, 64)
if err != nil {
return 0
}
case "min,", "mins,":
mins, err = strconv.ParseUint(ut[2], 10, 64)
if err != nil {
return 0
}
}

return (days * 24 * 60) + (hours * 60) + mins
return common.ParseUptime(uptime)
}

// This is a weak implementation due to the limitations on retrieving this data in AIX
func UsersWithContext(ctx context.Context) ([]UserStat, error) {
var ret []UserStat
out, err := invoke.CommandWithContext(ctx, "w")
out, err := getInvoker().CommandWithContext(ctx, "w")
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -162,7 +103,7 @@ func UsersWithContext(ctx context.Context) ([]UserStat, error) {
// Much of this function could be static. However, to be future proofed, I've made it call the OS for the information in all instances.
func PlatformInformationWithContext(ctx context.Context) (platform, family, version string, err error) {
// Set the platform (which should always, and only be, "AIX") from `uname -s`
out, err := invoke.CommandWithContext(ctx, "uname", "-s")
out, err := getInvoker().CommandWithContext(ctx, "uname", "-s")
if err != nil {
return "", "", "", err
}
Expand All @@ -172,7 +113,7 @@ func PlatformInformationWithContext(ctx context.Context) (platform, family, vers
family = strings.TrimRight(string(out), "\n")

// Set the version
out, err = invoke.CommandWithContext(ctx, "oslevel")
out, err = getInvoker().CommandWithContext(ctx, "oslevel")
if err != nil {
return "", "", "", err
}
Expand All @@ -182,7 +123,7 @@ func PlatformInformationWithContext(ctx context.Context) (platform, family, vers
}

func KernelVersionWithContext(ctx context.Context) (version string, err error) {
out, err := invoke.CommandWithContext(ctx, "oslevel", "-s")
out, err := getInvoker().CommandWithContext(ctx, "oslevel", "-s")
if err != nil {
return "", err
}
Expand All @@ -204,3 +145,44 @@ func KernelArch() (arch string, err error) {
func VirtualizationWithContext(_ context.Context) (string, string, error) {
return "", "", common.ErrNotImplementedError
}

// FDLimitsWithContext returns the system-wide file descriptor limits on AIX
// Returns (soft limit, hard limit, error)
// Note: hard limit may be reported as "unlimited" on AIX, in which case returns math.MaxUint64
func FDLimitsWithContext(ctx context.Context) (uint64, uint64, error) {
// Get soft limit via ulimit -n
out, err := getInvoker().CommandWithContext(ctx, "bash", "-c", "ulimit -n")
if err != nil {
return 0, 0, err
}
softStr := strings.TrimSpace(string(out))
soft, err := strconv.ParseUint(softStr, 10, 64)
if err != nil {
return 0, 0, err
}

// Get hard limit via ulimit -Hn
out, err = getInvoker().CommandWithContext(ctx, "bash", "-c", "ulimit -Hn")
if err != nil {
return 0, 0, err
}
hardStr := strings.TrimSpace(string(out))

// Handle "unlimited" case - common on AIX
var hard uint64
if hardStr == "unlimited" {
hard = 1<<63 - 1 // Use max int64 as "unlimited"
} else {
hard, err = strconv.ParseUint(hardStr, 10, 64)
if err != nil {
return 0, 0, err
}
}

return soft, hard, nil
}

// FDLimits returns the system-wide file descriptor limits
func FDLimits() (uint64, uint64, error) {
return FDLimitsWithContext(context.Background())
}
30 changes: 29 additions & 1 deletion host/host_aix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
package host

import (
"context"
"testing"

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

func TestParseUptimeValidInput(t *testing.T) {
Expand Down Expand Up @@ -36,6 +38,32 @@ func TestParseUptimeInvalidInput(t *testing.T) {

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

func TestFDLimitsWithContext(t *testing.T) {
ctx := context.Background()
soft, hard, err := FDLimitsWithContext(ctx)
require.NoError(t, err)

// Both limits should be positive
assert.Positive(t, soft, "Soft limit should be > 0")
assert.Positive(t, hard, "Hard limit should be > 0")

// Hard limit should be >= soft limit
assert.GreaterOrEqual(t, hard, soft, "Hard limit should be >= soft limit")

// Reasonable ranges for AIX (typically 1024-32767, or unlimited which is max int64)
assert.GreaterOrEqual(t, soft, uint64(256), "Soft limit should be >= 256")
}

func TestFDLimits(t *testing.T) {
soft, hard, err := FDLimits()
require.NoError(t, err)

// Both limits should be positive
assert.Positive(t, soft)
assert.Positive(t, hard)
assert.GreaterOrEqual(t, hard, soft)
}
51 changes: 51 additions & 0 deletions host/host_aix_test_mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: BSD-3-Clause

package host

import (
"context"
"fmt"
"strings"

"github.com/shirou/gopsutil/v4/internal/common"
)

// MockInvoker allows mocking command output for testing
type MockInvoker struct {
commands map[string][]byte
}

// NewMockInvoker creates a new mock invoker with predefined responses
func NewMockInvoker() *MockInvoker {
return &MockInvoker{
commands: make(map[string][]byte),
}
}

// SetResponse sets the response for a command
func (m *MockInvoker) SetResponse(name string, args []string, output string) {
key := name + " " + strings.Join(args, " ")
m.commands[key] = []byte(output)
}

// Command implements the Invoker interface
func (m *MockInvoker) Command(name string, arg ...string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), common.Timeout)
defer cancel()
return m.CommandWithContext(ctx, name, arg...)
}

// CommandWithContext implements the Invoker interface
func (m *MockInvoker) CommandWithContext(_ context.Context, name string, arg ...string) ([]byte, error) {
key := name + " " + strings.Join(arg, " ")
if output, ok := m.commands[key]; ok {
return output, nil
}
return nil, fmt.Errorf("command not mocked: %s", key)
}

// SetupFDLimitsMock configures the mock for FD limits testing
func (m *MockInvoker) SetupFDLimitsMock() {
m.SetResponse("bash", []string{"-c", "ulimit -n"}, "2048\n")
m.SetResponse("bash", []string{"-c", "ulimit -Hn"}, "unlimited\n")
}
Loading
Loading