diff --git a/.github/labeler.yml b/.github/labeler.yml index 0262fc635d..795206fc94 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -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: @@ -109,4 +114,12 @@ - changed-files: - any-glob-to-any-file: - '**/*_solaris.go' - - '**/*_posix.go' \ No newline at end of file + - '**/*_posix.go' + +'os:aix': + - changed-files: + - any-glob-to-any-file: + - '**/*_aix.go' + - '**/*_aix_test.go' + - '**/*_aix_*.go' + - '**/*_posix.go' diff --git a/host/host.go b/host/host.go index f85e5d7ed1..0faed310c5 100644 --- a/host/host.go +++ b/host/host.go @@ -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 diff --git a/host/host_aix.go b/host/host_aix.go index 2e8fb5539c..560c3a62b6 100644 --- a/host/host_aix.go +++ b/host/host_aix.go @@ -5,7 +5,6 @@ package host import ( "context" - "errors" "strconv" "strings" @@ -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 } @@ -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 @@ -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 } @@ -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 } @@ -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 } @@ -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 } @@ -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()) +} diff --git a/host/host_aix_test.go b/host/host_aix_test.go index 934766f7b2..d587697382 100644 --- a/host/host_aix_test.go +++ b/host/host_aix_test.go @@ -4,9 +4,11 @@ package host import ( + "context" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestParseUptimeValidInput(t *testing.T) { @@ -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) +} diff --git a/host/host_aix_test_mock.go b/host/host_aix_test_mock.go new file mode 100644 index 0000000000..7031ddbb4f --- /dev/null +++ b/host/host_aix_test_mock.go @@ -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") +} diff --git a/host/host_mock_test.go b/host/host_mock_test.go new file mode 100644 index 0000000000..67e894cf55 --- /dev/null +++ b/host/host_mock_test.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build aix + +package host + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Tests using mocked AIX command output +// These tests run on AIX systems, providing verification of parsing logic + +func TestFDLimitsWithContextMock(t *testing.T) { + // Setup mock + mock := NewMockInvoker() + mock.SetupFDLimitsMock() + testInvoker = mock + defer func() { testInvoker = nil }() + + ctx := context.Background() + soft, hard, err := FDLimitsWithContext(ctx) + require.NoError(t, err) + + // Should extract 2048 from mock ulimit -n + assert.Equal(t, uint64(2048), soft) + + // Should recognize "unlimited" and return max int64 + assert.Equal(t, uint64(1<<63-1), hard) + + // Hard should be >= soft + assert.GreaterOrEqual(t, hard, soft) +} + +func TestFDLimitsMock(t *testing.T) { + // Setup mock + mock := NewMockInvoker() + mock.SetupFDLimitsMock() + testInvoker = mock + defer func() { testInvoker = nil }() + + soft, hard, err := FDLimits() + require.NoError(t, err) + + assert.Equal(t, uint64(2048), soft) + assert.Equal(t, uint64(1<<63-1), hard) +} diff --git a/internal/common/common.go b/internal/common/common.go index 36eb1d211f..e400ae63e5 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -7,6 +7,7 @@ package common // - linux (amd64, arm) // - freebsd (amd64) // - windows (amd64) +// - aix (ppc64) import ( "bufio" @@ -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 +} diff --git a/internal/common/common_aix.go b/internal/common/common_aix.go new file mode 100644 index 0000000000..a0001ba7ce --- /dev/null +++ b/internal/common/common_aix.go @@ -0,0 +1,105 @@ +// 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") + } + + ut *= 60 + 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 +func UptimeWithContext(ctx context.Context, invoke Invoker) (uint64, error) { + out, err := invoke.CommandWithContext(ctx, "uptime") + if err != nil { + return 0, err + } + + return ParseUptime(string(out)), nil +} + +func ParseUptime(uptime string) uint64 { + ut := strings.Fields(uptime) + var days, hours, mins uint64 + var err error + + // Need at least 4 fields to be valid (time "up" number unit,...) + if len(ut) < 4 { + return 0 + } + + 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 len(ut) > 5 && (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 len(ut) > 5 && (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 len(ut) > 4 && 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 +} diff --git a/load/load_aix.go b/load/load_aix.go index eb5b5b0661..6dfdb5ca25 100644 --- a/load/load_aix.go +++ b/load/load_aix.go @@ -17,3 +17,13 @@ func Avg() (*AvgStat, error) { func Misc() (*MiscStat, error) { return MiscWithContext(context.Background()) } + +// SystemCalls returns the number of system calls since boot. +func SystemCalls() (int, error) { + return SystemCallsWithContext(context.Background()) +} + +// Interrupts returns the number of interrupts since boot. +func Interrupts() (int, error) { + return InterruptsWithContext(context.Background()) +} diff --git a/load/load_aix_nocgo.go b/load/load_aix_nocgo.go index 5e4ba6d0f5..d87588ba44 100644 --- a/load/load_aix_nocgo.go +++ b/load/load_aix_nocgo.go @@ -15,8 +15,19 @@ import ( var separator = regexp.MustCompile(`,?\s+`) +// 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 common.Invoke{} +} + func AvgWithContext(ctx context.Context) (*AvgStat, error) { - line, err := invoke.CommandWithContext(ctx, "uptime") + line, err := getInvoker().CommandWithContext(ctx, "uptime") if err != nil { return nil, err } @@ -44,24 +55,133 @@ func AvgWithContext(ctx context.Context) (*AvgStat, error) { return nil, common.ErrNotImplementedError } +// parseVmstatLine parses a single line of vmstat output and extracts context switches, interrupts, and syscalls +// Format: r b avm fre re pi po fr sr cy in sy cs us sy id wa pc ec +func parseVmstatLine(line string) (ctxt, interrupts, syscalls int, err error) { + fields := strings.Fields(line) + if len(fields) < 13 { + return 0, 0, 0, common.ErrNotImplementedError + } + + // Column indices in vmstat output (0-based): + // in = interrupts (index 10) + // sy = system calls (index 11) + // cs = context switches (index 12) + if v, err := strconv.Atoi(fields[10]); err == nil { + interrupts = v + } + if v, err := strconv.Atoi(fields[11]); err == nil { + syscalls = v + } + if v, err := strconv.Atoi(fields[12]); err == nil { + ctxt = v + } + + return ctxt, interrupts, syscalls, nil +} + +// SystemCallsWithContext returns the number of system calls since boot +func SystemCallsWithContext(ctx context.Context) (int, error) { + out, err := getInvoker().CommandWithContext(ctx, "vmstat", "1", "1") + if err != nil { + return 0, err + } + + lines := strings.Split(string(out), "\n") + // Last non-empty line contains the data + for i := len(lines) - 1; i >= 0; i-- { + line := strings.TrimSpace(lines[i]) + if line == "" { + continue + } + _, _, syscalls, err := parseVmstatLine(line) + return syscalls, err + } + + return 0, common.ErrNotImplementedError +} + +// InterruptsWithContext returns the number of interrupts since boot +func InterruptsWithContext(ctx context.Context) (int, error) { + out, err := getInvoker().CommandWithContext(ctx, "vmstat", "1", "1") + if err != nil { + return 0, err + } + + lines := strings.Split(string(out), "\n") + // Last non-empty line contains the data + for i := len(lines) - 1; i >= 0; i-- { + line := strings.TrimSpace(lines[i]) + if line == "" { + continue + } + _, interrupts, _, err := parseVmstatLine(line) + return interrupts, err + } + + return 0, common.ErrNotImplementedError +} + func MiscWithContext(ctx context.Context) (*MiscStat, error) { - out, err := invoke.CommandWithContext(ctx, "ps", "-Ao", "state") + out, err := getInvoker().CommandWithContext(ctx, "ps", "-e", "-o", "state") if err != nil { return nil, err } ret := &MiscStat{} - for _, line := range strings.Split(string(out), "\n") { - ret.ProcsTotal++ + lines := strings.Split(string(out), "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + + // Skip header line and empty lines + if line == "ST" || line == "STATE" || line == "S" || line == "" { + continue + } + + // Count processes by state (AIX process states from official docs) + // A = Active (running or ready to run) + // W = Swapped (not in main memory) + // I = Idle (waiting for startup) + // Z = Canceled (zombie - terminated, waiting for parent) + // T = Stopped (trace stopped) + // O = Nonexistent switch line { - case "R": - case "A": + case "A", "I": + // Active or Idle processes (ready to run or awaiting startup) ret.ProcsRunning++ - case "T": + case "W", "T", "Z": + // Swapped, Stopped, or Zombie processes (blocked/not runnable) ret.ProcsBlocked++ - default: - continue } + ret.ProcsTotal++ } + + // Get context switches from vmstat + ctxt, _, _, err := getVmstatMetrics(ctx) + if err == nil { + ret.Ctxt = ctxt + } + return ret, nil } + +// getVmstatMetrics parses vmstat output and returns context switches, interrupts, and syscalls +func getVmstatMetrics(ctx context.Context) (int, int, int, error) { + out, err := getInvoker().CommandWithContext(ctx, "vmstat", "1", "1") + if err != nil { + return 0, 0, 0, err + } + + lines := strings.Split(string(out), "\n") + // Last non-empty line contains the data + for i := len(lines) - 1; i >= 0; i-- { + line := strings.TrimSpace(lines[i]) + if line == "" { + continue + } + return parseVmstatLine(line) + } + + return 0, 0, 0, common.ErrNotImplementedError +} diff --git a/load/load_aix_test.go b/load/load_aix_test.go new file mode 100644 index 0000000000..b1a78df29d --- /dev/null +++ b/load/load_aix_test.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build aix + +package load + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMiscWithContextAIX(t *testing.T) { + ctx := context.Background() + misc, err := MiscWithContext(ctx) + require.NoError(t, err) + assert.NotNil(t, misc) + + // Process counts should be non-negative + assert.GreaterOrEqual(t, misc.ProcsTotal, 0) + assert.GreaterOrEqual(t, misc.ProcsRunning, 0) + assert.GreaterOrEqual(t, misc.ProcsBlocked, 0) + + // Total should be >= running + blocked + assert.GreaterOrEqual(t, misc.ProcsTotal, misc.ProcsRunning+misc.ProcsBlocked) + + // Context switches should be positive (system has been running) + assert.Positive(t, misc.Ctxt, "Context switches should be > 0 since system is running") +} + +func TestMiscAIX(t *testing.T) { + // Test the non-context version + misc, err := Misc() + require.NoError(t, err) + assert.NotNil(t, misc) + + // Process counts should be non-negative + assert.GreaterOrEqual(t, misc.ProcsTotal, 0) + assert.GreaterOrEqual(t, misc.ProcsRunning, 0) + assert.GreaterOrEqual(t, misc.ProcsBlocked, 0) +} + +func TestSystemCallsWithContext(t *testing.T) { + ctx := context.Background() + syscalls, err := SystemCallsWithContext(ctx) + require.NoError(t, err) + + // System calls should be positive since system is running + assert.Positive(t, syscalls, "System calls should be > 0 since system is running") +} + +func TestSystemCalls(t *testing.T) { + syscalls, err := SystemCalls() + require.NoError(t, err) + + // System calls should be positive + assert.Positive(t, syscalls) +} + +func TestInterruptsWithContext(t *testing.T) { + ctx := context.Background() + interrupts, err := InterruptsWithContext(ctx) + require.NoError(t, err) + + // Interrupts should be positive since system is running + assert.Positive(t, interrupts, "Interrupts should be > 0 since system is running") +} + +func TestInterrupts(t *testing.T) { + interrupts, err := Interrupts() + require.NoError(t, err) + + // Interrupts should be positive + assert.Positive(t, interrupts) +} diff --git a/load/load_aix_test_mock.go b/load/load_aix_test_mock.go new file mode 100644 index 0000000000..d0fa384fe8 --- /dev/null +++ b/load/load_aix_test_mock.go @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package load + +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) +} + +// SetupSystemMetricsMock configures the mock for system metrics testing +func (m *MockInvoker) SetupSystemMetricsMock() { + // AIX vmstat output + vmstatOutput := `System configuration: lcpu=8 mem=4096MB ent=0.20 + +kthr memory page faults cpu +----- ----------- ------------------------ ------------ ----------------------- + r b avm fre re pi po fr sr cy in sy cs us sy id wa pc ec + 0 0 389541 553213 0 0 0 0 0 0 9 1083 669 1 1 98 0 0.01 6.5 +` + m.SetResponse("vmstat", []string{"1", "1"}, vmstatOutput) + + // AIX ps output for process state + psOutput := `STATE +A +A +A +W +I +A +A +A +Z +A +` + m.SetResponse("ps", []string{"-e", "-o", "state"}, psOutput) +} diff --git a/load/load_mock_test.go b/load/load_mock_test.go new file mode 100644 index 0000000000..5a4e588d84 --- /dev/null +++ b/load/load_mock_test.go @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build aix + +package load + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Cross-platform tests using mocked AIX command output +// These tests run on AIX systems, providing verification of parsing logic + +func TestSystemCallsWithContextMock(t *testing.T) { + // Setup mock + mock := NewMockInvoker() + mock.SetupSystemMetricsMock() + testInvoker = mock + defer func() { testInvoker = nil }() + + ctx := context.Background() + syscalls, err := SystemCallsWithContext(ctx) + require.NoError(t, err) + + // Should extract 1083 from mock vmstat output + assert.Equal(t, 1083, syscalls) +} + +func TestInterruptsWithContextMock(t *testing.T) { + // Setup mock + mock := NewMockInvoker() + mock.SetupSystemMetricsMock() + testInvoker = mock + defer func() { testInvoker = nil }() + + ctx := context.Background() + interrupts, err := InterruptsWithContext(ctx) + require.NoError(t, err) + + // Should extract 9 from mock vmstat output + assert.Equal(t, 9, interrupts) +} + +func TestMiscWithContextMock(t *testing.T) { + // Setup mock + mock := NewMockInvoker() + mock.SetupSystemMetricsMock() + testInvoker = mock + defer func() { testInvoker = nil }() + + ctx := context.Background() + misc, err := MiscWithContext(ctx) + require.NoError(t, err) + require.NotNil(t, misc) + + // Mock data has: 7 A, 1 W, 1 I, 1 Z = 10 total + // Running: 7 (A) + 1 (I) = 8 + // Blocked: 1 (W) + 1 (Z) = 2 + assert.Equal(t, 10, misc.ProcsTotal) + assert.Equal(t, 8, misc.ProcsRunning) + assert.Equal(t, 2, misc.ProcsBlocked) + + // Should extract 669 from mock vmstat output + assert.Equal(t, 669, misc.Ctxt) +} + +func TestSystemCallsMock(t *testing.T) { + // Setup mock + mock := NewMockInvoker() + mock.SetupSystemMetricsMock() + testInvoker = mock + defer func() { testInvoker = nil }() + + syscalls, err := SystemCalls() + require.NoError(t, err) + assert.Equal(t, 1083, syscalls) +} + +func TestInterruptsMock(t *testing.T) { + // Setup mock + mock := NewMockInvoker() + mock.SetupSystemMetricsMock() + testInvoker = mock + defer func() { testInvoker = nil }() + + interrupts, err := Interrupts() + require.NoError(t, err) + assert.Equal(t, 9, interrupts) +} + +func TestMiscMock(t *testing.T) { + // Setup mock + mock := NewMockInvoker() + mock.SetupSystemMetricsMock() + testInvoker = mock + defer func() { testInvoker = nil }() + + misc, err := Misc() + require.NoError(t, err) + assert.NotNil(t, misc) + assert.Equal(t, 10, misc.ProcsTotal) + assert.Equal(t, 8, misc.ProcsRunning) + assert.Equal(t, 2, misc.ProcsBlocked) + assert.Equal(t, 669, misc.Ctxt) +} diff --git a/mem/mem_aix_nocgo.go b/mem/mem_aix_nocgo.go index bc3c0ed3b4..ca4e221dbd 100644 --- a/mem/mem_aix_nocgo.go +++ b/mem/mem_aix_nocgo.go @@ -59,6 +59,8 @@ func callSVMon(ctx context.Context, virt bool) (*VirtualMemoryStat, *SwapMemoryS } if t, err := strconv.ParseUint(p[3], 10, 64); err == nil { vmem.Free = t * pagesize + // Available is typically equal to Free on AIX + vmem.Available = vmem.Free } } } else if strings.HasPrefix(line, "pg space") { @@ -68,7 +70,12 @@ func callSVMon(ctx context.Context, virt bool) (*VirtualMemoryStat, *SwapMemoryS swap.Total = t * pagesize } if t, err := strconv.ParseUint(p[3], 10, 64); err == nil { - swap.Free = swap.Total - t*pagesize + swapUsed := t * pagesize + swap.Used = swapUsed + swap.Free = swap.Total - swapUsed + if swap.Total > 0 { + swap.UsedPercent = 100 * float64(swap.Used) / float64(swap.Total) + } } } break diff --git a/net/net_aix.go b/net/net_aix.go index 4531dd4449..264763fd69 100644 --- a/net/net_aix.go +++ b/net/net_aix.go @@ -5,6 +5,7 @@ package net import ( "context" + "errors" "fmt" "regexp" "strconv" @@ -295,6 +296,290 @@ func ConnectionsPidMaxWithoutUidsWithContext(ctx context.Context, kind string, p return connectionsPidMaxWithoutUidsWithContext(ctx, kind, pid, maxConn, true) } -func connectionsPidMaxWithoutUidsWithContext(_ context.Context, _ string, _ int32, _ int, _ bool) ([]ConnectionStat, error) { - return []ConnectionStat{}, common.ErrNotImplementedError +func connectionsPidMaxWithoutUidsWithContext(ctx context.Context, _ string, pid int32, maxConn int, _ bool) ([]ConnectionStat, error) { + // If pid is 0, return all connections + if pid == 0 { + return getAIXConnections(ctx, maxConn) + } + + // For specific PID, filter connections to just that process + return getAIXConnectionsForPid(ctx, pid, maxConn) +} + +// getAIXConnections retrieves all network connections from AIX +func getAIXConnections(ctx context.Context, maxConn int) ([]ConnectionStat, error) { + var conns []ConnectionStat + + // Get all listening sockets using netstat + output, err := invoke.CommandWithContext(ctx, "netstat", "-Aan") + if err != nil { + return conns, err + } + + lines := strings.Split(string(output), "\n") + count := 0 + + for _, line := range lines { + if maxConn > 0 && count >= maxConn { + break + } + + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "Address") { + continue + } + + // Parse netstat output to find sockets + // Format: Address Family Type Use Recv-Q Send-Q Inode Conn Routes + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + + sockAddr := fields[0] + + // Try to resolve the socket to get connection info using rmsock + // For TCP connections + connStat, err := resolveAIXSockToConnection(ctx, sockAddr, "tcp") + if err == nil && connStat != nil { + conns = append(conns, *connStat) + count++ + continue + } + + // Try for UDP connections + connStat, err = resolveAIXSockToConnection(ctx, sockAddr, "udp") + if err == nil && connStat != nil { + conns = append(conns, *connStat) + count++ + } + } + + return conns, nil +} + +// getAIXConnectionsForPid retrieves network connections for a specific process on AIX +func getAIXConnectionsForPid(ctx context.Context, pid int32, maxConn int) ([]ConnectionStat, error) { + var conns []ConnectionStat + + // Get all listening sockets using netstat + output, err := invoke.CommandWithContext(ctx, "netstat", "-Aan") + if err != nil { + return conns, err + } + + lines := strings.Split(string(output), "\n") + count := 0 + + for _, line := range lines { + if maxConn > 0 && count >= maxConn { + break + } + + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "Address") || strings.HasPrefix(line, "PCB/ADDR") { + continue + } + + // Parse netstat output: PCB/ADDR Proto Recv-Q Send-Q Local Address Foreign Address (state) + // Example: f1000f00055cc3c0 tcp4 0 0 192.168.242.122.22 24.236.207.124.40326 ESTABLISHED + fields := strings.Fields(line) + if len(fields) < 7 { + continue + } + + sockAddr := fields[0] + proto := fields[1] + localAddr := fields[4] + remoteAddr := fields[5] + state := fields[6] + + // Determine protocol type (tcp or udp) + var protocol string + switch { + case strings.HasPrefix(proto, "tcp"): + protocol = "tcp" + case strings.HasPrefix(proto, "udp"): + protocol = "udp" + default: + continue + } + + // Try to resolve the socket to get PID using rmsock + resolvedPid := resolveAIXSockToPid(ctx, sockAddr, protocol) + if resolvedPid != pid { + // This connection doesn't belong to our target PID + continue + } + + // Parse addresses + laddr := parseAIXAddress(localAddr) + raddr := parseAIXAddress(remoteAddr) + + // Determine socket type and family + var socketType uint32 + var socketFamily uint32 + + // Set socket type based on protocol + if protocol == "tcp" { + socketType = syscall.SOCK_STREAM + } else { + socketType = syscall.SOCK_DGRAM + } + + // Set socket family based on proto string (tcp4, tcp6, udp4, udp6) + if strings.HasSuffix(proto, "6") { + socketFamily = syscall.AF_INET6 + } else { + socketFamily = syscall.AF_INET + } + + connStat := ConnectionStat{ + Fd: 0, + Family: socketFamily, + Type: socketType, + Laddr: laddr, + Raddr: raddr, + Status: state, + Pid: pid, + } + + conns = append(conns, connStat) + count++ + } + + return conns, nil +} + +// resolveAIXSockToConnection uses AIX rmsock command to resolve a socket address to connection info +func resolveAIXSockToConnection(ctx context.Context, sockAddr, protocol string) (*ConnectionStat, error) { + if protocol != "tcp" && protocol != "udp" { + return nil, fmt.Errorf("unsupported protocol: %s", protocol) + } + + // Execute rmsock to resolve socket + // Format for TCP: rmsock tcpcb + // Format for UDP: rmsock inpcb + var tcpOrUDP string + if protocol == "tcp" { + tcpOrUDP = "tcpcb" + } else { + tcpOrUDP = "inpcb" + } + + output, err := invoke.CommandWithContext(ctx, "rmsock", sockAddr, tcpOrUDP) + if err != nil { + return nil, err + } + + // Parse rmsock output to extract connection info + outputStr := string(output) + + // Try to find PID in the output + pid := parseAIXRmsockPid(outputStr) + if pid == 0 { + return nil, errors.New("could not extract PID from rmsock output") + } + + // Build connection stat from parsed info + connStat := &ConnectionStat{ + Fd: 0, + Family: 0, + Type: 0, + Laddr: Addr{IP: "", Port: 0}, + Raddr: Addr{IP: "", Port: 0}, + Status: "", + Pid: pid, + } + + return connStat, nil +} + +// resolveAIXSockToConnectionForPid resolves socket to connection only if it matches the target PID +func resolveAIXSockToConnectionForPid(ctx context.Context, sockAddr, protocol string, targetPid int32) (*ConnectionStat, error) { + connStat, err := resolveAIXSockToConnection(ctx, sockAddr, protocol) + if err != nil { + return nil, err + } + + if connStat == nil { + return nil, errors.New("connection stat is nil") + } + + if connStat.Pid != targetPid { + return nil, fmt.Errorf("PID mismatch: expected %d, got %d", targetPid, connStat.Pid) + } + + return connStat, nil +} + +// parseAIXRmsockPid extracts PID from rmsock output +// Expected format: "The socket 0xf1000f00055be808 is being held by process 14287304 (sshd)." +func parseAIXRmsockPid(output string) int32 { + // Use regex to extract PID from rmsock output + // Pattern: "process (" + re := regexp.MustCompile(`process\s+(\d+)\s+\(`) + matches := re.FindStringSubmatch(output) + if len(matches) > 1 { + if pid, err := strconv.ParseInt(matches[1], 10, 32); err == nil { + return int32(pid) + } + } + return 0 +} + +// resolveAIXSockToPid uses rmsock to get the PID holding a socket, returns 0 if unable to resolve +func resolveAIXSockToPid(ctx context.Context, sockAddr, protocol string) int32 { + if protocol != "tcp" && protocol != "udp" { + return 0 + } + + var tcpOrUDP string + if protocol == "tcp" { + tcpOrUDP = "tcpcb" + } else { + tcpOrUDP = "inpcb" + } + + output, err := invoke.CommandWithContext(ctx, "rmsock", sockAddr, tcpOrUDP) + // Note: rmsock may exit with status 1 even on successful resolution + // So we try to parse the output regardless of error status + + outputStr := string(output) + pid := parseAIXRmsockPid(outputStr) + + if pid == 0 && err != nil { + // If we got a "Wait for exiting processes" message, it's a transient cleanup situation - skip silently + if strings.Contains(outputStr, "Wait for exiting processes") { + return 0 + } + // For other errors, log debug info if we couldn't parse a PID + // Uncomment for debugging: fmt.Fprintf(os.Stderr, "DEBUG: rmsock %s %s failed: %v, output: %s\n", sockAddr, tcpOrUdp, err, outputStr) + } + + return pid +} + +// parseAIXAddress parses an AIX address string like "192.168.242.122.22" or "24.236.207.124.40326" +// Format: IP_OCTETS separated by dots, with port as last octet(s) after the IP +func parseAIXAddress(addrStr string) Addr { + if addrStr == "*.*" { + return Addr{IP: "", Port: 0} + } + + parts := strings.Split(addrStr, ".") + if len(parts) < 2 { + return Addr{IP: "", Port: 0} + } + + // Last part is the port + port := 0 + if p, err := strconv.Atoi(parts[len(parts)-1]); err == nil { + port = p + } + + // Join all but last part as IP + ip := strings.Join(parts[:len(parts)-1], ".") + + return Addr{IP: ip, Port: uint32(port)} } diff --git a/net/net_aix_test.go b/net/net_aix_test.go new file mode 100644 index 0000000000..0dc8c355a1 --- /dev/null +++ b/net/net_aix_test.go @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build aix + +package net + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConnectionsPidWithContext(t *testing.T) { + ctx := context.Background() + pid := int32(os.Getpid()) + + conns, err := ConnectionsPidWithContext(ctx, "inet", pid) + // Process may not have any connections, check result gracefully + if err != nil { + // It's OK if the function returns an error + t.Logf("ConnectionsPidWithContext error: %v", err) + return + } + // If successful, verify structure + if conns != nil { + assert.IsType(t, []ConnectionStat{}, conns) + for _, conn := range conns { + // Verify connection fields are populated + assert.NotEmpty(t, conn.Family) + assert.NotEmpty(t, conn.Type) + } + } +} + +func TestConnectionsPidWithContextAll(t *testing.T) { + ctx := context.Background() + pid := int32(os.Getpid()) + + // Test with "all" family + conns, err := ConnectionsPidWithContext(ctx, "all", pid) + if err != nil { + // It's OK if the function returns an error + t.Logf("ConnectionsPidWithContext error: %v", err) + return + } + if conns != nil { + assert.IsType(t, []ConnectionStat{}, conns) + } +} + +func TestConnectionsPidWithContextUDP(t *testing.T) { + ctx := context.Background() + pid := int32(os.Getpid()) + + // Test with UDP connections + conns, err := ConnectionsPidWithContext(ctx, "udp", pid) + if err != nil { + // It's OK if the function returns an error + t.Logf("ConnectionsPidWithContext error: %v", err) + return + } + if conns != nil { + assert.IsType(t, []ConnectionStat{}, conns) + } +} + +func TestConnectionsWithContext(t *testing.T) { + ctx := context.Background() + + // Test getting all connections + conns, err := ConnectionsWithContext(ctx, "inet") + require.NoError(t, err) + assert.NotNil(t, conns) + assert.IsType(t, []ConnectionStat{}, conns) + + // Should have at least some connections + assert.NotEmpty(t, conns) + + for _, conn := range conns { + // Verify connection fields + assert.NotEmpty(t, conn.Family) + assert.NotEmpty(t, conn.Type) + assert.GreaterOrEqual(t, conn.Pid, int32(0)) + } +} diff --git a/nfs/nfs.go b/nfs/nfs.go new file mode 100644 index 0000000000..9816c917fe --- /dev/null +++ b/nfs/nfs.go @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: BSD-3-Clause +package nfs + +import ( + "encoding/json" + + "github.com/shirou/gopsutil/v4/internal/common" +) + +var invoke common.Invoker = common.Invoke{} + +// RPCClientStat represents RPC client statistics +type RPCClientStat struct { + Calls uint64 `json:"calls"` + BadCalls uint64 `json:"badCalls"` + BadXIDs uint64 `json:"badXids"` + Timeouts uint64 `json:"timeouts"` + NewCreds uint64 `json:"newCreds"` + BadVerfs uint64 `json:"badVerfs"` + Timers uint64 `json:"timers"` + NoMem uint64 `json:"noMem"` + CantConn uint64 `json:"cantConn"` + Interrupts uint64 `json:"interrupts"` + Retrans uint64 `json:"retrans"` + CantSend uint64 `json:"cantSend"` +} + +// RPCServerStat represents RPC server statistics +type RPCServerStat struct { + Calls uint64 `json:"calls"` + BadCalls uint64 `json:"badCalls"` + NullRecv uint64 `json:"nullRecv"` + BadLen uint64 `json:"badLen"` + XdrCall uint64 `json:"xdrCall"` + DupChecks uint64 `json:"dupChecks"` + DupReqs uint64 `json:"dupReqs"` +} + +// NFSClientStat represents NFS client statistics +type NFSClientStat struct { + Calls uint64 `json:"calls"` + BadCalls uint64 `json:"badCalls"` + ClGets uint64 `json:"clGets"` + ClTooMany uint64 `json:"clTooMany"` + Operations map[string]uint64 `json:"operations"` // Per-operation counts (read, write, lookup, etc.) +} + +// NFSServerStat represents NFS server statistics +type NFSServerStat struct { + Calls uint64 `json:"calls"` + BadCalls uint64 `json:"badCalls"` + PublicV2 uint64 `json:"publicV2"` + PublicV3 uint64 `json:"publicV3"` + Operations map[string]uint64 `json:"operations"` // Per-operation counts (read, write, lookup, etc.) +} + +// StatsStat represents complete NFS statistics +type StatsStat struct { + RPCClientStats RPCClientStat `json:"rpcClient"` + RPCServerStats RPCServerStat `json:"rpcServer"` + NFSClientStats NFSClientStat `json:"nfsClient"` + NFSServerStats NFSServerStat `json:"nfsServer"` +} + +func (s StatsStat) String() string { + b, _ := json.Marshal(s) + return string(b) +} diff --git a/nfs/nfs_aix.go b/nfs/nfs_aix.go new file mode 100644 index 0000000000..9d2839259e --- /dev/null +++ b/nfs/nfs_aix.go @@ -0,0 +1,350 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build aix + +package nfs + +import ( + "context" + "strconv" + "strings" + "sync" +) + +var ( + // Cache for full stats to avoid re-parsing nfsstat + cachedStats *StatsStat + cacheMutex sync.RWMutex +) + +// statsWithContext returns NFS statistics from nfsstat command (internal, may be cached) +func statsWithContext(ctx context.Context) (*StatsStat, error) { + stats := &StatsStat{ + NFSClientStats: NFSClientStat{Operations: make(map[string]uint64)}, + NFSServerStats: NFSServerStat{Operations: make(map[string]uint64)}, + } + + // Get nfsstat output + output, err := invoke.CommandWithContext(ctx, "nfsstat") + if err != nil { + return nil, err + } + + // Parse the output + if err := parseNfsstat(string(output), stats); err != nil { + return nil, err + } + + return stats, nil +} + +// ClientStatsWithContext returns NFS client statistics +func ClientStatsWithContext(ctx context.Context) (*NFSClientStat, error) { + stats, err := statsWithContext(ctx) + if err != nil { + return nil, err + } + return &stats.NFSClientStats, nil +} + +// ClientStats returns NFS client statistics +func ClientStats() (*NFSClientStat, error) { + return ClientStatsWithContext(context.Background()) +} + +// ServerStatsWithContext returns NFS server statistics +func ServerStatsWithContext(ctx context.Context) (*NFSServerStat, error) { + stats, err := statsWithContext(ctx) + if err != nil { + return nil, err + } + return &stats.NFSServerStats, nil +} + +// ServerStats returns NFS server statistics +func ServerStats() (*NFSServerStat, error) { + return ServerStatsWithContext(context.Background()) +} + +// parseNfsstat parses nfsstat command output on AIX +// AIX nfsstat output is organized into sections: +// - Server rpc: RPC statistics for NFS server +// - Server nfs: NFS operation statistics for server +// - Client rpc: RPC statistics for NFS client +// - Client nfs: NFS operation statistics for client +// +// Each RPC section has subsections for "Connection oriented" (TCP) and "Connectionless" (UDP) +// NFS sections report per-operation statistics broken down by NFSv2 and NFSv3 +func parseNfsstat(output string, stats *StatsStat) error { //nolint:unparam // stats is used in nested parsing + lines := strings.Split(output, "\n") + + var section string // Track current section: "server_rpc", "server_nfs", "client_rpc", "client_nfs" + var transportMode string // Track "Connection oriented" (TCP) vs "Connectionless" (UDP) + var nfsVersion string // Track "Version 2" vs "Version 3" + + for i := 0; i < len(lines); i++ { + line := strings.TrimSpace(lines[i]) + + if line == "" { + continue + } + + // Detect sections + if strings.Contains(line, "Server rpc:") { + section = "server_rpc" + continue + } + if strings.Contains(line, "Server nfs:") { + section = "server_nfs" + continue + } + if strings.Contains(line, "Client rpc:") { + section = "client_rpc" + continue + } + if strings.Contains(line, "Client nfs:") { + section = "client_nfs" + continue + } + + // Detect transport mode + if strings.Contains(line, "Connection oriented") { + transportMode = "connection_oriented" + continue + } + if strings.Contains(line, "Connectionless") { + transportMode = "connectionless" + continue + } + + // Detect NFS version + if strings.Contains(line, "Version 2:") { + nfsVersion = "v2" + continue + } + if strings.Contains(line, "Version 3:") { + nfsVersion = "v3" + continue + } + + // Parse data lines based on section + switch section { + case "server_rpc": + if err := parseRPCServerLine(line, transportMode, &stats.RPCServerStats); err != nil { + continue + } + case "server_nfs": + if err := parseNFSServerLine(line, nfsVersion, &stats.NFSServerStats); err != nil { + continue + } + case "client_rpc": + if err := parseRPCClientLine(line, transportMode, &stats.RPCClientStats); err != nil { + continue + } + case "client_nfs": + if err := parseNFSClientLine(line, nfsVersion, &stats.NFSClientStats); err != nil { + continue + } + } + } + + return nil +} + +// parseRPCServerLine parses an RPC server statistics line from AIX nfsstat +// AIX reports separate metrics for "Connection oriented" (TCP) and "Connectionless" (UDP) transports +// Both have: calls badcalls nullrecv badlen xdrcall dupchecks dupreqs +// +//nolint:unparam // transportMode parameter needed for consistency with caller +func parseRPCServerLine(line, transportMode string, stats *RPCServerStat) error { + // Skip header lines + if strings.Contains(line, "calls") && strings.Contains(line, "badcalls") { + return nil + } + + fields := strings.Fields(line) + if len(fields) == 0 { + return nil + } + + // Parse based on transport mode and field count + switch transportMode { + case "connection_oriented", "connectionless": + if len(fields) >= 7 { + // Both modes have: calls badcalls nullrecv badlen xdrcall dupchecks dupreqs + stats.Calls += parseUint64(fields[0]) + stats.BadCalls += parseUint64(fields[1]) + stats.NullRecv += parseUint64(fields[2]) + stats.BadLen += parseUint64(fields[3]) + stats.XdrCall += parseUint64(fields[4]) + stats.DupChecks += parseUint64(fields[5]) + stats.DupReqs += parseUint64(fields[6]) + } + } + return nil +} + +// parseRPCClientLine parses an RPC client statistics line from AIX nfsstat +// AIX reports separate metrics for "Connection oriented" (TCP) and "Connectionless" (UDP) transports +// Connection oriented has: calls badcalls badxids timeouts newcreds badverfs timers nomem cantconn interrupts +// Connectionless has: calls badcalls retrans badxids timeouts newcreds badverfs timers nomem cantsend +// +//nolint:unparam // transportMode parameter needed for consistency with caller +func parseRPCClientLine(line, transportMode string, stats *RPCClientStat) error { + // Skip header lines + if strings.Contains(line, "calls") { + return nil + } + + fields := strings.Fields(line) + if len(fields) == 0 { + return nil + } + + switch transportMode { + case "connection_oriented": + // Connection oriented: calls badcalls badxids timeouts newcreds badverfs timers + if len(fields) >= 7 { + stats.Calls += parseUint64(fields[0]) + stats.BadCalls += parseUint64(fields[1]) + stats.BadXIDs += parseUint64(fields[2]) + stats.Timeouts += parseUint64(fields[3]) + stats.NewCreds += parseUint64(fields[4]) + stats.BadVerfs += parseUint64(fields[5]) + stats.Timers += parseUint64(fields[6]) + } + case "connectionless": + // Connectionless: calls badcalls retrans badxids timeouts newcreds badverfs + if len(fields) >= 7 { + stats.Calls += parseUint64(fields[0]) + stats.BadCalls += parseUint64(fields[1]) + stats.Retrans += parseUint64(fields[2]) + stats.BadXIDs += parseUint64(fields[3]) + stats.Timeouts += parseUint64(fields[4]) + stats.NewCreds += parseUint64(fields[5]) + stats.BadVerfs += parseUint64(fields[6]) + } + } + return nil +} + +// parseNFSServerLine parses an NFS server statistics line from AIX nfsstat +// AIX reports per-operation statistics broken down by NFSv2 and NFSv3 +// Format: "calls badcalls public_v2 public_v3" followed by per-operation stats with percentages +func parseNFSServerLine(line, nfsVersion string, stats *NFSServerStat) error { + // Parse "Server nfs:" header line + if strings.Contains(line, "calls") && strings.Contains(line, "badcalls") { + fields := strings.Fields(line) + if len(fields) >= 4 && fields[0] == "calls" { + // This is the main header, skip + return nil + } + // This might be the calls/badcalls/public_v2/public_v3 line + if len(fields) >= 4 { + stats.Calls = parseUint64(fields[0]) + stats.BadCalls = parseUint64(fields[1]) + if strings.HasPrefix(fields[0], "0") && len(fields) >= 4 { + stats.Calls = parseUint64(fields[0]) + stats.BadCalls = parseUint64(fields[1]) + if len(fields) > 2 { + stats.PublicV2 = parseUint64(fields[2]) + } + if len(fields) > 3 { + stats.PublicV3 = parseUint64(fields[3]) + } + } + } + return nil + } + + // Parse operation lines (contains percentage) + if strings.Contains(line, "%") { + return parseNFSOperationLine(line, nfsVersion, stats.Operations) + } + + // Parse the initial calls/badcalls/public_v2/public_v3 line + fields := strings.Fields(line) + if len(fields) >= 4 && !strings.Contains(line, "%") && !strings.Contains(line, "Version") && !strings.Contains(line, "null") { + // Could be: 0 0 0 0 (calls badcalls public_v2 public_v3) + if _, err := strconv.ParseUint(fields[0], 10, 64); err == nil { + stats.Calls = parseUint64(fields[0]) + stats.BadCalls = parseUint64(fields[1]) + if len(fields) > 2 { + stats.PublicV2 = parseUint64(fields[2]) + } + if len(fields) > 3 { + stats.PublicV3 = parseUint64(fields[3]) + } + } + } + + return nil +} + +// parseNFSClientLine parses an NFS client statistics line from AIX nfsstat +// AIX reports per-operation statistics broken down by NFSv2 and NFSv3 +// Format: "calls badcalls clgets cltoomany" followed by per-operation stats with percentages +func parseNFSClientLine(line, nfsVersion string, stats *NFSClientStat) error { + // Parse header line: calls badcalls clgets cltoomany + if strings.HasPrefix(line, "calls") && strings.Contains(line, "badcalls") { + return nil + } + + // Parse operation lines (contains percentage) + if strings.Contains(line, "%") { + return parseNFSOperationLine(line, nfsVersion, stats.Operations) + } + + // Parse the calls/badcalls/clgets/cltoomany line + fields := strings.Fields(line) + if len(fields) >= 4 && !strings.Contains(line, "%") && !strings.Contains(line, "Version") { + if _, err := strconv.ParseUint(fields[0], 10, 64); err == nil { + stats.Calls = parseUint64(fields[0]) + stats.BadCalls = parseUint64(fields[1]) + if len(fields) > 2 { + stats.ClGets = parseUint64(fields[2]) + } + if len(fields) > 3 { + stats.ClTooMany = parseUint64(fields[3]) + } + } + } + + return nil +} + +// parseNFSOperationLine parses NFS operation statistics line from AIX nfsstat +// AIX output format: operation1 count1% operation2 count2% ... +// Example: "null 0 0% getattr 0 0% setattr 0 0%" +// Operations are specific to the NFS version context (v2 or v3) +func parseNFSOperationLine(line, nfsVersion string, operations map[string]uint64) error { + // Format: operation1 count1% operation2 count2% ... + // Split on whitespace and process pairs of (operation, count) + fields := strings.Fields(line) + + for i := 0; i < len(fields)-1; i += 2 { + opName := fields[i] + countStr := fields[i+1] + + // Remove percentage sign if present + countStr = strings.TrimSuffix(countStr, "%") + + // Parse count + count := parseUint64(countStr) + + // Determine version prefix if needed + key := opName + if nfsVersion != "" { + key = nfsVersion + "_" + opName + } + + operations[key] = count + } + + return nil +} + +// parseUint64 safely parses a string to uint64, returning 0 on error +func parseUint64(s string) uint64 { + val, _ := strconv.ParseUint(strings.TrimSpace(s), 10, 64) + return val +} diff --git a/nfs/nfs_aix_test.go b/nfs/nfs_aix_test.go new file mode 100644 index 0000000000..7979d357b0 --- /dev/null +++ b/nfs/nfs_aix_test.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build aix + +package nfs + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestClientStatsWithContext(t *testing.T) { + ctx := context.Background() + stats, err := ClientStatsWithContext(ctx) + // AIX system may not have NFS client enabled, so we just check error handling + if err != nil { + // It's OK if NFS is not available + t.Logf("NFS client stats not available: %v", err) + return + } + assert.NotNil(t, stats) + _ = stats // uint64 >= 0 is always true, suppress warning +} + +func TestServerStatsWithContext(t *testing.T) { + ctx := context.Background() + stats, err := ServerStatsWithContext(ctx) + // AIX system may not have NFS server enabled, so we just check error handling + if err != nil { + // It's OK if NFS is not available + t.Logf("NFS server stats not available: %v", err) + return + } + assert.NotNil(t, stats) + _ = stats // uint64 >= 0 is always true, suppress warning +} diff --git a/nfs/nfs_fallback.go b/nfs/nfs_fallback.go new file mode 100644 index 0000000000..3b145a22dc --- /dev/null +++ b/nfs/nfs_fallback.go @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build !aix + +package nfs + +import ( + "context" + + "github.com/shirou/gopsutil/v4/internal/common" +) + +// ClientStatsWithContext returns NFS client statistics +func ClientStatsWithContext(_ context.Context) (*NFSClientStat, error) { + return nil, common.ErrNotImplementedError +} + +// ClientStats returns NFS client statistics +func ClientStats() (*NFSClientStat, error) { + return nil, common.ErrNotImplementedError +} + +// ServerStatsWithContext returns NFS server statistics +func ServerStatsWithContext(_ context.Context) (*NFSServerStat, error) { + return nil, common.ErrNotImplementedError +} + +// ServerStats returns NFS server statistics +func ServerStats() (*NFSServerStat, error) { + return nil, common.ErrNotImplementedError +} diff --git a/process/process.go b/process/process.go index 5db5ff4819..a7f0ce831d 100644 --- a/process/process.go +++ b/process/process.go @@ -465,6 +465,11 @@ func (p *Process) Terminal() (string, error) { return p.TerminalWithContext(context.Background()) } +// SignalsPending returns the signals pending for the process. +func (p *Process) SignalsPending() (SignalInfoStat, error) { + return p.SignalsPendingWithContext(context.Background()) +} + // Nice returns a nice value (priority). func (p *Process) Nice() (int32, error) { return p.NiceWithContext(context.Background()) diff --git a/process/process_aix.go b/process/process_aix.go new file mode 100644 index 0000000000..a7bcf2be40 --- /dev/null +++ b/process/process_aix.go @@ -0,0 +1,1665 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build aix + +package process + +import ( + "bytes" + "context" + "encoding/binary" + "encoding/json" + "errors" + "math" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/shirou/gopsutil/v4/cpu" + "github.com/shirou/gopsutil/v4/internal/common" + "github.com/shirou/gopsutil/v4/net" +) + +var pageSize = uint64(os.Getpagesize()) + +// AIX-specific: cache for process bitness (4 = 32-bit, 8 = 64-bit) +var aixBitnessCache sync.Map // map[int32]int64 + +const prioProcess = 0 // linux/resource.h + +var clockTicks = 100 // default value + +func init() { + // Initialize clock ticks from AIX schedo configuration + // AIX default: 1 clock tick = 10ms (100 ticks/second) + // Can be modified via schedo big_tick_size parameter + clockTicks = getAIXClockTicks() +} + +// getAIXClockTicks retrieves the actual clock tick frequency from AIX scheduler configuration. +// AIX maintains this through the schedo command, specifically the big_tick_size parameter. +// The default is 1 tick = 10ms, but this can be tuned via: +// +// schedo -o big_tick_size= +// +// where value * 10ms is the actual tick interval. +// +// Since we cannot directly access kernel parameters from userspace reliably, +// we use the schedo command to query big_tick_size and calculate the actual clock ticks. +// If schedo is unavailable or returns an error, we default to the standard 100 ticks/second (10ms). +func getAIXClockTicks() int { + const defaultClockTicks = 100 // Default: 100 ticks/second = 10ms per tick + + // Try to query big_tick_size from schedo + // Format: schedo -o big_tick_size (displays current value without changing) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, "schedo", "-o", "big_tick_size") + + output, err := cmd.Output() + if err != nil { + // schedo unavailable or failed; use default + return defaultClockTicks + } + + // Parse output format: "big_tick_size = " + // Example: "big_tick_size = 1" + outputStr := strings.TrimSpace(string(output)) + parts := strings.Split(outputStr, "=") + if len(parts) < 2 { + return defaultClockTicks + } + + tickMultiplier, err := strconv.Atoi(strings.TrimSpace(parts[1])) + if err != nil { + return defaultClockTicks + } + + // big_tick_size is a multiplier for 10ms ticks + // Calculate actual clock ticks per second: 1000ms / (tickMultiplier * 10ms) + if tickMultiplier <= 0 { + return defaultClockTicks + } + + // 100 ticks/second / tickMultiplier = actual ticks per second + // For example: if big_tick_size=2, then 100/2 = 50 ticks/second + actualTicks := defaultClockTicks / tickMultiplier + if actualTicks < 1 { + actualTicks = 1 // Ensure at least 1 tick per second + } + + return actualTicks +} + +type PrTimestruc64T struct { + TvSec int64 // 64 bit time_t value + TvNsec int32 // 32 bit suseconds_t value + Pad uint32 // reserved for future use +} + +/* hardware fault set */ +type Fltset struct { + FltSet [4]uint64 // fault set +} + +type PrSigset struct { + SsSet [4]uint64 // signal set +} + +type prptr64 uint64 + +type size64 uint64 + +type pid64 uint64 // size invariant 64-bit pid + +type PrSiginfo64 struct { + SiSigno int32 // signal number + SiErrno int32 // if non-zero, errno of this signal + SiCode int32 // signal code + SiImm int32 // immediate data + SiStatus int32 // exit value or signal + Pad1 uint32 // reserved for future use + SiUID uint64 // real user id of sending process + SiPid uint64 // sending process id + SiAddr prptr64 // address of faulting instruction + SiBand int64 // band event for SIGPOLL + SiValue prptr64 // signal value + Pad [4]uint32 // reserved for future use +} + +type PrSigaction64 struct { + SaUnion prptr64 // signal handler function pointer + SaMask PrSigset // signal mask + SaFlags int32 // signal flags + Pad [5]int32 // reserved for future use +} + +type PrStack64 struct { + SsSp prptr64 // stack base or pointer + SsSize uint64 // stack size + SsFlags int32 // flags + Pad [5]int32 // reserved for future use +} + +type Prgregset struct { + Iar size64 // Instruction Pointer + Msr size64 // machine state register + Cr size64 // condition register + Lr size64 // link register + Ctr size64 // count register + Xer size64 // fixed point exception + Fpscr size64 // floating point status reg + Fpscrx size64 // extension floating point + Gpr [32]size64 // static general registers + Usprg3 size64 + Pad1 [7]size64 // Reserved for future use +} + +type Prfpregset struct { + Fpr [32]float64 // Floating Point Registers +} + +type Pfamily struct { + ExtOff uint64 // offset of extension + ExtSize uint64 // size of extension + Pad [14]uint64 // reserved for future use +} + +type LwpStatus struct { + LwpId uint64 // specific thread id + Flags uint32 // thread status flags + Pad1 [1]byte // reserved for future use + State byte // thread state + CurSig uint16 // current signal + Why uint16 // stop reason + What uint16 // more detailed reason + Policy uint32 // scheduling policy + ClName [8]byte // scheduling policy string + LwpPend PrSigset // set of signals pending to the thread + LwpHold PrSigset // set of signals blocked by the thread + Info PrSiginfo64 // info associated with signal or fault + AltStack PrStack64 // alternate signal stack info + Action PrSigaction64 // signal action for current signal + Pad2 uint32 // reserved for future use + Syscall uint16 // system call number + NsysArg uint16 // number of arguments + SysArg [8]uint64 // syscall arguments + Errno int32 // errno from last syscall + Ptid uint32 // pthread id + Pad [9]uint64 // reserved for future use + Reg Prgregset // general registers + Fpreg Prfpregset // floating point registers + Family Pfamily // hardware platform specific information +} + +type AIXStat struct { + Flag uint32 // process flags from proc struct p_flag + Flag2 uint32 // process flags from proc struct p_flag2 + Flags uint32 // /proc flags + Nlwp uint32 // number of threads in the process + Stat byte // process state from proc p_stat + Dmodel byte // data model for the process + Pad1 [6]byte // reserved for future use + SigPend PrSigset // set of process pending signals + BrkBase prptr64 // address of the process heap + BrkSize uint64 // size of the process heap, in bytes + StkBase prptr64 // address of the process stack + StkSize uint64 // size of the process stack, in bytes + Pid pid64 // process id + Ppid pid64 // parent process id + Pgid pid64 // process group id + Sid pid64 // session id + Utime PrTimestruc64T // process user cpu time + Stime PrTimestruc64T // process system cpu time + Cutime PrTimestruc64T // sum of children's user times + Cstime PrTimestruc64T // sum of children's system times + SigTrace PrSigset // mask of traced signals + FltTrace Fltset // mask of traced hardware faults + SysentryOffset uint32 // offset into pstatus file of sysset_t identifying system calls traced on entry + SysexitOffset uint32 // offset into pstatus file of sysset_t identifying system calls traced on exit + Pad [8]uint64 // reserved for future use + Lwp LwpStatus // "representative" thread status +} + +type LwpsInfo struct { + LwpId uint64 // thread id + Addr uint64 // internal address of thread + Wchan uint64 // wait address for sleeping thread + Flag uint32 // thread flags + Wtype byte // type of thread wait + State byte // thread state + Sname byte // printable thread state character + Nice byte // nice value for CPU usage + Pri int32 // priority, high value = high priority + Policy uint32 // scheduling policy + Clname [8]byte // printable scheduling policy string + Onpro int32 // processor on which thread last ran + Bindpro int32 // processor to which thread is bound + Ptid uint32 // pthread id + Pad1 uint32 // reserved for future use + Pad [7]uint64 // reserved for future use +} + +type AIXPSInfo struct { + Flag uint32 // process flags from proc struct p_flag + Flag2 uint32 // process flags from proc struct p_flag2 + Nlwp uint32 // number of threads in process + Pad1 uint32 // reserved for future use + UID uint64 // real user id + Euid uint64 // effective user id + Gid uint64 // real group id + Egid uint64 // effective group id + Pid uint64 // unique process id + Ppid uint64 // process id of parent + Pgid uint64 // pid of process group leader + Sid uint64 // session id + Ttydev uint64 // controlling tty device + Addr uint64 // internal address of proc struct + Size uint64 // size of process image in KB (1024) units + Rssize uint64 // resident set size in KB (1024) units + Start PrTimestruc64T // process start time, time since epoch + Time PrTimestruc64T // usr+sys cpu time for this process + Cid int16 // corral id + Pad2 int16 // reserved for future use + Argc uint32 // initial argument count + Argv uint64 // address of initial argument vector in user process + Envp uint64 // address of initial environment vector in user process + Fname [16]byte // last component of exec()ed pathname + Psargs [80]byte // initial characters of arg list + Pad [8]uint64 // reserved for future use + Lwp LwpsInfo // "representative" thread info +} + +// MemoryInfoExStat is different between OSes +type MemoryInfoExStat struct { + RSS uint64 `json:"rss"` // bytes + VMS uint64 `json:"vms"` // bytes + Shared uint64 `json:"shared"` // bytes + Text uint64 `json:"text"` // bytes + Lib uint64 `json:"lib"` // bytes + Data uint64 `json:"data"` // bytes + Dirty uint64 `json:"dirty"` // bytes +} + +func (m MemoryInfoExStat) String() string { + s, _ := json.Marshal(m) + return string(s) +} + +type MemoryMapsStat struct { + Path string `json:"path"` + Rss uint64 `json:"rss"` + Size uint64 `json:"size"` + Pss uint64 `json:"pss"` + SharedClean uint64 `json:"sharedClean"` + SharedDirty uint64 `json:"sharedDirty"` + PrivateClean uint64 `json:"privateClean"` + PrivateDirty uint64 `json:"privateDirty"` + Referenced uint64 `json:"referenced"` + Anonymous uint64 `json:"anonymous"` + Swap uint64 `json:"swap"` +} + +// String returns JSON value of the process. +func (m MemoryMapsStat) String() string { + s, _ := json.Marshal(m) + return string(s) +} + +func (p *Process) PpidWithContext(ctx context.Context) (int32, error) { + _, ppid, _, _, _, _, _, err := p.fillFromStatWithContext(ctx) + if err != nil { + return -1, err + } + return ppid, nil +} + +func (p *Process) NameWithContext(ctx context.Context) (string, error) { + if p.name == "" { + if err := p.fillFromCommWithContext(ctx); err != nil { + return "", err + } + } + return p.name, nil +} + +func (p *Process) TgidWithContext(ctx context.Context) (int32, error) { + if p.tgid == 0 { + if err := p.fillFromStatusWithContext(ctx); err != nil { + return 0, err + } + } + return p.tgid, nil +} + +func (p *Process) ExeWithContext(ctx context.Context) (string, error) { + return p.fillFromExeWithContext(ctx) +} + +func (p *Process) CmdlineWithContext(ctx context.Context) (string, error) { + return p.fillFromCmdlineWithContext(ctx) +} + +func (p *Process) CmdlineSliceWithContext(ctx context.Context) ([]string, error) { + return p.fillSliceFromCmdlineWithContext(ctx) +} + +func (p *Process) EnvironmentWithContext(ctx context.Context) (map[string]string, error) { + // Query environment via ps command using Berkeley-style 'e' option + // Berkeley style: ps eww (no -p flag) + //nolint:gosec // Process ID from internal tracking, not untrusted input + cmd := exec.CommandContext(ctx, "ps", "eww", strconv.Itoa(int(p.Pid))) + output, err := cmd.Output() + if err != nil { + return nil, err + } + + envStr := strings.TrimSpace(string(output)) + if envStr == "" { + return make(map[string]string), nil + } + + // Parse space-separated VAR=value assignments + env := make(map[string]string) + + // ps eww output is space-separated on a single line (or multiple lines for multiline values) + // Split by spaces to get individual VAR=value pairs + parts := strings.Fields(envStr) + + for _, part := range parts { + if strings.Contains(part, "=") { + kv := strings.SplitN(part, "=", 2) + if len(kv) == 2 { + env[kv[0]] = kv[1] + } + } + } + + return env, nil +} + +func (p *Process) createTimeWithContext(ctx context.Context) (int64, error) { + _, _, _, createTime, _, _, _, err := p.fillFromStatWithContext(ctx) + if err != nil { + return 0, err + } + return createTime, nil +} + +func (p *Process) CwdWithContext(ctx context.Context) (string, error) { + return p.fillFromCwdWithContext(ctx) +} + +func (p *Process) StatusWithContext(ctx context.Context) ([]string, error) { + err := p.fillFromStatusWithContext(ctx) + if err != nil { + return []string{""}, err + } + return []string{p.status}, nil +} + +func (*Process) ForegroundWithContext(_ context.Context) (bool, error) { + return false, common.ErrNotImplementedError +} + +func (p *Process) UidsWithContext(ctx context.Context) ([]uint32, error) { + err := p.fillFromStatusWithContext(ctx) + if err != nil { + return []uint32{}, err + } + return p.uids, nil +} + +func (p *Process) GidsWithContext(ctx context.Context) ([]uint32, error) { + err := p.fillFromStatusWithContext(ctx) + if err != nil { + return []uint32{}, err + } + return p.gids, nil +} + +func (p *Process) GroupsWithContext(ctx context.Context) ([]uint32, error) { + err := p.fillFromStatusWithContext(ctx) + if err != nil { + return []uint32{}, err + } + return p.groups, nil +} + +func (p *Process) TerminalWithContext(ctx context.Context) (string, error) { + // Query TTY via ps command + //nolint:gosec // Process ID from internal tracking, not untrusted input + cmd := exec.CommandContext(ctx, "ps", "-o", "tty", "-p", strconv.Itoa(int(p.Pid))) + output, err := cmd.Output() + if err != nil { + return "", err + } + + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + if len(lines) < 2 { + // Only header or no output + return "", nil + } + + // Get the TTY value (second line, first field) + tty := strings.Fields(lines[1]) + if len(tty) > 0 { + return tty[0], nil + } + return "", nil +} + +func (p *Process) NiceWithContext(ctx context.Context) (int32, error) { + _, _, _, _, _, nice, _, err := p.fillFromStatWithContext(ctx) + if err != nil { + return 0, err + } + return nice, nil +} + +func (*Process) IOniceWithContext(_ context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) RlimitWithContext(ctx context.Context) ([]RlimitStat, error) { + return p.RlimitUsageWithContext(ctx, false) +} + +func (p *Process) RlimitUsageWithContext(ctx context.Context, gatherUsed bool) ([]RlimitStat, error) { + // Get per-process resource limits via procfiles command + //nolint:gosec // Process ID from internal tracking, not untrusted input + cmd := exec.CommandContext(ctx, "procfiles", strconv.Itoa(int(p.Pid))) + output, err := cmd.Output() + if err != nil { + // Fallback: try system-wide limits from ulimit + return p.getRlimitFromUlimit(ctx, gatherUsed) + } + + // Parse procfiles output for file descriptor limits + // Output format: FD Info: nnnn (soft limit), mmmm (hard limit) + var rlimits []RlimitStat + + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + for _, line := range lines { + if strings.Contains(line, "FD Info") || strings.Contains(line, "File descriptors") { + // Extract limits from this line + // Format varies, try to parse numbers + numStrs := strings.FieldsFunc(line, func(r rune) bool { + return r < '0' || r > '9' + }) + if len(numStrs) >= 2 { + soft, _ := strconv.ParseUint(numStrs[0], 10, 64) + hard, _ := strconv.ParseUint(numStrs[1], 10, 64) + rlimits = append(rlimits, RlimitStat{ + Resource: RLIMIT_NOFILE, + Soft: soft, + Hard: hard, + }) + break + } + } + } + + if len(rlimits) == 0 { + return p.getRlimitFromUlimit(ctx, gatherUsed) + } + + return rlimits, nil +} + +// getRlimitFromUlimit gets resource limits via ulimit command +func (*Process) getRlimitFromUlimit(ctx context.Context, _ bool) ([]RlimitStat, error) { + cmd := exec.CommandContext(ctx, "sh", "-c", "ulimit -a") + output, err := cmd.Output() + if err != nil { + return nil, err + } + + var rlimits []RlimitStat + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + + for _, line := range lines { + // Parse ulimit output: "open files (-n) 1024" + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + + var limit uint64 + + // Identify resource type and extract limit + var resourceID int32 + if strings.Contains(line, "open files") { + resourceID = RLIMIT_NOFILE + // Extract numeric value (usually last field) + if val, err := strconv.ParseUint(fields[len(fields)-1], 10, 64); err == nil { + limit = val + } + } + + if resourceID != 0 && limit > 0 { + rlimits = append(rlimits, RlimitStat{ + Resource: resourceID, + Soft: limit, + Hard: limit, + }) + } + } + + return rlimits, nil +} + +func (p *Process) IOCountersWithContext(ctx context.Context) (*IOCountersStat, error) { + // Check if WLM is enabled and iostat is configured + cmd := exec.CommandContext(ctx, "lsattr", "-El", "sys0") + output, err := cmd.Output() + if err != nil { + return nil, common.ErrNotImplementedError + } + + // Check if iostat=true + if !strings.Contains(string(output), "iostat true") { + return nil, common.ErrNotImplementedError + } + + // Query I/O counters via ps command + //nolint:gosec // Process ID from internal tracking, not untrusted input + cmd = exec.CommandContext(ctx, "ps", "-efo", "pid,tdiskio", "-p", strconv.Itoa(int(p.Pid))) + output, err = cmd.Output() + if err != nil { + return nil, err + } + + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + if len(lines) < 2 { + return nil, errors.New("insufficient ps output") + } + + // Parse the output (skip header) + fields := strings.Fields(lines[1]) + if len(fields) < 2 { + return nil, errors.New("insufficient fields in ps output") + } + + // Check for hyphen (unavailable data) + ioCountStr := fields[1] + if ioCountStr == "-" { + return nil, errors.New("I/O counters not available for this process") + } + + // Parse the I/O count + ioCount, err := strconv.ParseUint(ioCountStr, 10, 64) + if err != nil { + return nil, err + } + + return &IOCountersStat{ + ReadBytes: ioCount, + WriteBytes: 0, // AIX doesn't separate read/write I/O + }, nil +} + +func (*Process) NumCtxSwitchesWithContext(_ context.Context) (*NumCtxSwitchesStat, error) { + // AIX does not expose context switch information via proc files or ps command. + // According to IBM AIX documentation, the ps command field specifiers do not include + // nvcsw (non-voluntary context switches) or vcsw (voluntary context switches). + // These metrics are not available in the AIX proc binary structures either. + // This metric is not available on AIX. + return nil, common.ErrNotImplementedError +} + +func (p *Process) NumFDsWithContext(ctx context.Context) (int32, error) { + _, fnames, err := p.fillFromfdListWithContext(ctx) + return int32(len(fnames)), err +} + +func (p *Process) NumThreadsWithContext(ctx context.Context) (int32, error) { + err := p.fillFromStatusWithContext(ctx) + if err != nil { + return 0, err + } + return p.numThreads, nil +} + +func (p *Process) ThreadsWithContext(ctx context.Context) (map[int32]*cpu.TimesStat, error) { + ret := make(map[int32]*cpu.TimesStat) + lwpPath := common.HostProcWithContext(ctx, strconv.Itoa(int(p.Pid)), "lwp") + + tids, err := readPidsFromDir(lwpPath) + if err != nil { + return nil, err + } + + for _, tid := range tids { + _, _, cpuTimes, _, _, _, _, err := p.fillFromTIDStatWithContext(ctx, tid) + if err != nil { + return nil, err + } + ret[tid] = cpuTimes + } + + return ret, nil +} + +func (p *Process) TimesWithContext(ctx context.Context) (*cpu.TimesStat, error) { + _, _, cpuTimes, _, _, _, _, err := p.fillFromStatWithContext(ctx) + if err != nil { + return nil, err + } + return cpuTimes, nil +} + +func (*Process) CPUAffinityWithContext(_ context.Context) ([]int32, error) { + // AIX ps command does not support psr field specifier in System V style + // Berkeley style ps doesn't provide CPU affinity information + // This metric is not available on AIX + return nil, common.ErrNotImplementedError +} + +// Note: CPUPercentWithContext is NOT overridden here +// The generic implementation from process.go is used on AIX as well +// AIX ps -o %cpu can be used if needed in the future + +func (p *Process) SignalsPendingWithContext(ctx context.Context) (SignalInfoStat, error) { + // Extract pending signals from the AIXStat structure's SigPend field + // This field is already being read from /proc//psinfo in fillFromStatusWithContext + err := p.fillFromStatusWithContext(ctx) + if err != nil { + return SignalInfoStat{}, err + } + + // Read the psinfo file directly to get the SigPend field + psInfoPath := common.HostProcWithContext(ctx, strconv.Itoa(int(p.Pid)), "psinfo") + psInfoFile, err := os.Open(psInfoPath) + if err != nil { + return SignalInfoStat{}, err + } + defer psInfoFile.Close() + + // Only read up to the SigPend field to avoid EOF on truncated reads + // AIXStat starts with: Flag(4) Flag2(4) Flags(4) Nlwp(4) Stat(1) Dmodel(1) Pad1(6) = 24 bytes + // Then SigPend which is PrSigset [4]uint64 = 32 bytes + // Total offset to SigPend: 24 bytes + + // Skip the first part of the structure to get to SigPend + var ( + flag, flag2, flags, nlwp uint32 + stat, dmodel byte + pad1 [6]byte + sigPend PrSigset + ) + + err = binary.Read(psInfoFile, binary.BigEndian, &flag) + if err != nil { + return SignalInfoStat{}, err + } + err = binary.Read(psInfoFile, binary.BigEndian, &flag2) + if err != nil { + return SignalInfoStat{}, err + } + err = binary.Read(psInfoFile, binary.BigEndian, &flags) + if err != nil { + return SignalInfoStat{}, err + } + err = binary.Read(psInfoFile, binary.BigEndian, &nlwp) + if err != nil { + return SignalInfoStat{}, err + } + err = binary.Read(psInfoFile, binary.BigEndian, &stat) + if err != nil { + return SignalInfoStat{}, err + } + err = binary.Read(psInfoFile, binary.BigEndian, &dmodel) + if err != nil { + return SignalInfoStat{}, err + } + err = binary.Read(psInfoFile, binary.BigEndian, &pad1) + if err != nil { + return SignalInfoStat{}, err + } + err = binary.Read(psInfoFile, binary.BigEndian, &sigPend) + if err != nil { + return SignalInfoStat{}, err + } + + // Convert the PrSigset (which is [4]uint64) to a single uint64 for pending signals + // The signal set uses the first 64 bits for signals 1-64 (most common) + pendingSignals := sigPend.SsSet[0] + + return SignalInfoStat{ + PendingProcess: pendingSignals, + }, nil +} + +func (p *Process) MemoryInfoWithContext(ctx context.Context) (*MemoryInfoStat, error) { + meminfo, _, err := p.fillFromStatmWithContext(ctx) + if err != nil { + return nil, err + } + return meminfo, nil +} + +func (p *Process) MemoryInfoExWithContext(ctx context.Context) (*MemoryInfoExStat, error) { + _, memInfoEx, err := p.fillFromStatmWithContext(ctx) + if err != nil { + return nil, err + } + return memInfoEx, nil +} + +func (p *Process) PageFaultsWithContext(ctx context.Context) (*PageFaultsStat, error) { + _, _, _, _, _, _, pageFaults, err := p.fillFromStatWithContext(ctx) + if err != nil { + return nil, err + } + return pageFaults, nil +} + +func (*Process) ChildrenWithContext(_ context.Context) ([]*Process, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) OpenFilesWithContext(ctx context.Context) ([]OpenFilesStat, error) { + _, ofs, err := p.fillFromfdWithContext(ctx) + if err != nil { + return nil, err + } + ret := make([]OpenFilesStat, len(ofs)) + for i, o := range ofs { + ret[i] = *o + } + + return ret, nil +} + +func (p *Process) ConnectionsWithContext(ctx context.Context) ([]net.ConnectionStat, error) { + return net.ConnectionsPidWithContext(ctx, "all", p.Pid) +} + +func (p *Process) ConnectionsMaxWithContext(ctx context.Context, maxConn int) ([]net.ConnectionStat, error) { + return net.ConnectionsPidMaxWithContext(ctx, "all", p.Pid, maxConn) +} + +// getConnectionsUsingNetstat retrieves network connections using AIX netstat command. +// This function is kept for backward compatibility but delegates to the net module. +// +// Deprecated: Use net module's ConnectionsPidMaxWithContext instead +func (p *Process) getConnectionsUsingNetstat(ctx context.Context, maxConn int) ([]net.ConnectionStat, error) { + return net.ConnectionsPidMaxWithContext(ctx, "all", p.Pid, maxConn) +} + +func (p *Process) MemoryMapsWithContext(ctx context.Context, _ bool) (*[]MemoryMapsStat, error) { + // Use AIX procmap command to retrieve detailed memory address space maps + // procmap provides information about memory regions including HEAP, STACK, TEXT, etc. + pid := p.Pid + + //nolint:gosec // Process ID from internal tracking, not untrusted input + cmd := exec.CommandContext(ctx, "procmap", "-X", strconv.Itoa(int(pid))) + output, err := cmd.Output() + if err != nil { + return nil, err + } + + return p.parseMemoryMaps(string(output)), nil +} + +// parseMemoryMaps parses procmap output and returns a list of MemoryMapsStat +// procmap -X output format: +// 1 : /etc/init +// +// Start-ADD End-ADD SIZE MODE PSIZ TYPE VSID MAPPED OBJECT +// 0 10000000 262144K r-- m KERTXT 10002 +// 10000000 1000ce95 51K r-x s MAINTEXT 8b8117 init +// 200003d8 20036288 215K rw- sm MAINDATA 890192 init +func (*Process) parseMemoryMaps(output string) *[]MemoryMapsStat { + maps := make([]MemoryMapsStat, 0) + lines := strings.Split(output, "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "Start-ADD") || strings.Contains(line, ":") && !strings.HasPrefix(line, "Start") { + // Skip header lines, empty lines, and the first line with PID info + continue + } + + fields := strings.Fields(line) + if len(fields) < 6 { + continue + } + + mapStat := MemoryMapsStat{} + + // Parse start address (hex) + _, err := strconv.ParseUint(fields[0], 16, 64) + if err != nil { + continue + } + + // Parse end address (hex) + _, err = strconv.ParseUint(fields[1], 16, 64) + if err != nil { + continue + } + + // Parse SIZE (e.g., "51K", "262144K", "215K") + size := parseSizeField(fields[2]) + + // MODE is in fields[3] (e.g., "r--", "r-x", "rw-") + // PSIZ is in fields[4] (e.g., "m", "s", "sm") + // TYPE is in fields[5] (e.g., "KERTXT", "MAINTEXT", "MAINDATA", "HEAP", "STACK", "SLIBTEXT") + + mapType := fields[5] + + // Path/name: remaining fields after VSID (fields[6]) + // The VSID is at fields[6], and mapped object starts at fields[7] or later + pathStart := 7 + var mapPath string + if pathStart < len(fields) { + pathParts := fields[pathStart:] + mapPath = strings.Join(pathParts, " ") + } + + // Set MemoryMapsStat fields + mapStat.Size = size + mapStat.Rss = size // RSS approximation: use the size field + mapStat.Path = mapPath + + // Populate descriptive path with type information if not set + if mapStat.Path == "" { + mapStat.Path = "[" + strings.ToLower(mapType) + "]" + } + + maps = append(maps, mapStat) + } + + return &maps +} + +// parseSizeField converts procmap size field (e.g., "7K", "10M", "100") to bytes +func parseSizeField(sizeStr string) uint64 { + sizeStr = strings.TrimSpace(sizeStr) + + // Check for unit suffixes + switch { + case strings.HasSuffix(sizeStr, "K") || strings.HasSuffix(sizeStr, "k"): + numStr := sizeStr[:len(sizeStr)-1] + if num, err := strconv.ParseUint(numStr, 10, 64); err == nil { + return num * 1024 + } + case strings.HasSuffix(sizeStr, "M") || strings.HasSuffix(sizeStr, "m"): + numStr := sizeStr[:len(sizeStr)-1] + if num, err := strconv.ParseUint(numStr, 10, 64); err == nil { + return num * 1024 * 1024 + } + case strings.HasSuffix(sizeStr, "G") || strings.HasSuffix(sizeStr, "g"): + numStr := sizeStr[:len(sizeStr)-1] + if num, err := strconv.ParseUint(numStr, 10, 64); err == nil { + return num * 1024 * 1024 * 1024 + } + } + + // No suffix, try to parse as plain number (bytes) + if num, err := strconv.ParseUint(sizeStr, 10, 64); err == nil { + return num + } + + return 0 +} + +func (*Process) EnvironWithContext(_ context.Context) ([]string, error) { + // AIX /proc does not expose environment variables in a standard text format + // Envp in psinfo is a user-space pointer that is not directly accessible + return nil, common.ErrNotImplementedError +} + +/** +** Internal functions +**/ + +func limitToUint(val string) (uint64, error) { + if val == "unlimited" { + return math.MaxUint64, nil + } + res, err := strconv.ParseUint(val, 10, 64) + if err != nil { + return 0, err + } + return res, nil +} + +// Get num_fds from /proc/(pid)/limits (not available in AIX) +func (*Process) fillFromLimitsWithContext(_ context.Context) ([]RlimitStat, error) { + // AIX /proc does not expose resource limits in a standard procfs location + return nil, common.ErrNotImplementedError +} + +// Get list of /proc/(pid)/fd files +func (p *Process) fillFromfdListWithContext(ctx context.Context) (string, []string, error) { + pid := p.Pid + statPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "fd") + d, err := os.Open(statPath) + if err != nil { + return statPath, []string{}, err + } + defer d.Close() + fnames, err := d.Readdirnames(-1) + return statPath, fnames, err +} + +// Get num_fds from /proc/(pid)/fd +func (p *Process) fillFromfdWithContext(ctx context.Context) (int32, []*OpenFilesStat, error) { + statPath, fnames, err := p.fillFromfdListWithContext(ctx) + if err != nil { + return 0, nil, err + } + numFDs := int32(len(fnames)) + + var openfiles []*OpenFilesStat + for _, fd := range fnames { + fpath := filepath.Join(statPath, fd) + linkPath, err := os.Readlink(fpath) + if err != nil { + continue + } + t, err := strconv.ParseUint(fd, 10, 64) + if err != nil { + return numFDs, openfiles, err + } + o := &OpenFilesStat{ + Path: linkPath, + Fd: t, + } + openfiles = append(openfiles, o) + } + + return numFDs, openfiles, nil +} + +// Get cwd from /proc/(pid)/cwd +func (p *Process) fillFromCwdWithContext(ctx context.Context) (string, error) { + pid := p.Pid + cwdPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "cwd") + cwd, err := os.Readlink(cwdPath) + if err != nil { + return "", err + } + return string(cwd), nil +} + +// Get exe from /proc/(pid)/psinfo +func (p *Process) fillFromExeWithContext(ctx context.Context) (string, error) { + pid := p.Pid + infoPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "psinfo") + infoFile, err := os.Open(infoPath) + if err != nil { + return "", err + } + defer infoFile.Close() + + var aixPSinfo AIXPSInfo + err = binary.Read(infoFile, binary.BigEndian, &aixPSinfo) + if err != nil { + return "", err + } + + // Try Fname field from psinfo first (most reliable) + fname := extractFnameString(&aixPSinfo) + if fname != "" { + return fname, nil + } + + // Fallback to extracting from Psargs field + psargs := extractPsargsString(&aixPSinfo) + if psargs != "" { + // Extract the first word (executable name) from Psargs + parts := strings.Fields(psargs) + if len(parts) > 0 { + return filepath.Base(parts[0]), nil + } + } + + // Get first argument from process address space (argv[0] is the executable name) + // This is last resort as it's more prone to permission/corruption issues + args, err := p.readArgsFromAddressSpace(ctx, int(pid), &aixPSinfo, 1) + if err == nil && len(args) > 0 { + // Extract just the basename from the full path + exeName := filepath.Base(args[0]) + return exeName, nil + } + + return "", nil +} + +// getProcessBitness returns the pointer size (4 or 8 bytes) for a process, caching the result +func (p *Process) getProcessBitness(ctx context.Context, pid int) (int64, error) { + // Return cached value if available + if cached, ok := aixBitnessCache.Load(p.Pid); ok { + return cached.(int64), nil + } + + // Read status file to get data model byte (offset 17 in AIXStat structure) + statusPath := common.HostProcWithContext(ctx, strconv.Itoa(pid), "status") + statusData, err := os.ReadFile(statusPath) + if err != nil { + return 8, err // Default to 64-bit on error + } + + ptrSize := int64(8) // Default to 64-bit + if len(statusData) > 17 { + if statusData[17] == 0 { + ptrSize = 4 // 32-bit process (byte 0 indicates 32-bit) + } + } + + // Cache the bitness + aixBitnessCache.Store(p.Pid, ptrSize) + + return ptrSize, nil +} + +// readArgsFromAddressSpace reads argument and environment strings from process memory +// Similar to OSHI's approach +func (p *Process) readArgsFromAddressSpace(ctx context.Context, pid int, psinfo *AIXPSInfo, maxArgs int) ([]string, error) { + if psinfo.Argc == 0 || psinfo.Argc > 10000 { + // Sanity check on argc + return nil, common.ErrNotImplementedError + } + + asPath := common.HostProcWithContext(ctx, strconv.Itoa(pid), "as") + fd, err := syscall.Open(asPath, syscall.O_RDONLY, 0) + if err != nil { + // No permission or file not found + return nil, err + } + defer syscall.Close(fd) + + // Get cached bitness (pointer size) + ptrSize, err := p.getProcessBitness(ctx, pid) + if err != nil { + // If we can't determine bitness, default to 64-bit + ptrSize = 8 + } + + // Read argv pointers + argc := int(psinfo.Argc) + if argc > maxArgs && maxArgs > 0 { + argc = maxArgs + } + + argv := make([]int64, argc) + for i := 0; i < argc; i++ { + offset := int64(psinfo.Argv) + int64(i)*ptrSize + buf := make([]byte, ptrSize) + n, err := syscall.Pread(fd, buf, offset) + if err != nil || n != len(buf) { + break + } + if ptrSize == 8 { + argv[i] = int64(binary.BigEndian.Uint64(buf)) + } else { + argv[i] = int64(binary.BigEndian.Uint32(buf)) + } + } + + // Read argument strings + args := make([]string, 0, argc) + for i := 0; i < argc && i < len(argv); i++ { + if argv[i] == 0 { + break + } + argStr, err := readStringFromAddressSpace(fd, argv[i]) + if err != nil { + break + } + if argStr != "" { + args = append(args, argStr) + } + } + + return args, nil +} + +// readStringFromAddressSpace reads a null-terminated string from process memory +func readStringFromAddressSpace(fd int, addr int64) (string, error) { + const pageSize = 4096 + const maxStrLen = 32768 + + // Align to page boundary + pageStart := (addr / pageSize) * pageSize + buffer := make([]byte, pageSize*2) + + n, err := syscall.Pread(fd, buffer, pageStart) + if err != nil || n == 0 { + return "", err + } + + // Calculate offset within buffer + offset := addr - pageStart + if offset < 0 || offset >= int64(len(buffer)) { + return "", common.ErrNotImplementedError + } + + // Read null-terminated string + var result strings.Builder + for i := offset; i < int64(len(buffer)) && i < offset+int64(maxStrLen); i++ { + if buffer[i] == 0 { + break + } + result.WriteByte(buffer[i]) + } + + return result.String(), nil +} + +// Get cmdline from /proc/(pid)/psinfo by reading from address space +func (p *Process) fillFromCmdlineWithContext(ctx context.Context) (string, error) { + pid := p.Pid + infoPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "psinfo") + infoFile, err := os.Open(infoPath) + if err != nil { + return "", err + } + defer infoFile.Close() + + var aixPSinfo AIXPSInfo + err = binary.Read(infoFile, binary.BigEndian, &aixPSinfo) + if err != nil { + return "", err + } + + // Use Psargs field directly - it contains the initial command line + psargs := extractPsargsString(&aixPSinfo) + if psargs != "" { + return psargs, nil + } + + // If Psargs is empty, try reading from address space + args, err := p.readArgsFromAddressSpace(ctx, int(pid), &aixPSinfo, 0) + if err == nil && len(args) > 0 { + return strings.Join(args, " "), nil + } + + return "", nil +} + +func (p *Process) fillSliceFromCmdlineWithContext(ctx context.Context) ([]string, error) { + pid := p.Pid + infoPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "psinfo") + infoFile, err := os.Open(infoPath) + if err != nil { + return nil, err + } + defer infoFile.Close() + + var aixPSinfo AIXPSInfo + err = binary.Read(infoFile, binary.BigEndian, &aixPSinfo) + if err != nil { + return nil, err + } + + // Use Psargs field directly - it contains the initial command line + psargs := extractPsargsString(&aixPSinfo) + if psargs != "" { + // Split on spaces as a simple heuristic; AIX psinfo.Psargs is limited to 80 chars + return strings.Fields(psargs), nil + } + + // If Psargs is empty, try reading arguments directly from address space + args, err := p.readArgsFromAddressSpace(ctx, int(pid), &aixPSinfo, 0) + if err == nil && len(args) > 0 { + return args, nil + } + + return []string{}, nil +} + +// extractPsargsString extracts and cleans the Psargs field from AIXPSInfo +func extractPsargsString(psinfo *AIXPSInfo) string { + return string(bytes.TrimRight(psinfo.Psargs[:], "\x00")) +} + +// extractFnameString extracts and cleans the Fname field from AIXPSInfo +// Fname may have leading null bytes, so we need to skip them first +func extractFnameString(psinfo *AIXPSInfo) string { + // First, trim trailing null bytes + trimmed := bytes.TrimRight(psinfo.Fname[:], "\x00") + // Then, trim leading null bytes + trimmed = bytes.TrimLeft(trimmed, "\x00") + return string(trimmed) +} + +// Get IO status from /proc/(pid)/status (not available in AIX) +func (*Process) fillFromIOWithContext(_ context.Context) (*IOCountersStat, error) { + // AIX does not expose detailed I/O counters in /proc; return nil + return nil, common.ErrNotImplementedError +} + +// Get memory info from /proc/(pid)/psinfo +func (p *Process) fillFromStatmWithContext(ctx context.Context) (*MemoryInfoStat, *MemoryInfoExStat, error) { + pid := p.Pid + infoPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "psinfo") + infoFile, err := os.Open(infoPath) + if err != nil { + return nil, nil, err + } + defer infoFile.Close() + + var aixPSinfo AIXPSInfo + err = binary.Read(infoFile, binary.BigEndian, &aixPSinfo) + if err != nil { + return nil, nil, err + } + + // Read memory from AIXPSInfo.Size and AIXPSInfo.Rssize fields (matching OSHI) + // These are in KB, multiply by 1024 to get bytes + vms := aixPSinfo.Size * 1024 + rss := aixPSinfo.Rssize * 1024 + + meminfo := &MemoryInfoStat{ + VMS: vms, + RSS: rss, + } + meminfoEx := &MemoryInfoExStat{ + VMS: vms, + RSS: rss, + } + return meminfo, meminfoEx, nil +} + +// Get name from /proc/(pid)/psinfo (Fname field) +func (p *Process) fillFromCommWithContext(ctx context.Context) error { + exe, err := p.fillFromExeWithContext(ctx) + if err != nil { + return err + } + p.name = exe + return nil +} + +// Get various status from /proc/(pid)/status +func (p *Process) fillFromStatus() error { + return p.fillFromStatusWithContext(context.Background()) +} + +func (p *Process) fillFromStatusWithContext(ctx context.Context) error { + pid := p.Pid + statusPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "status") + statusFile, err := os.Open(statusPath) + if err != nil { + return err + } + defer statusFile.Close() + + // Parse the binary AIXStat structure + var aixStat AIXStat + err = binary.Read(statusFile, binary.BigEndian, &aixStat) + if err != nil { + return err + } + + p.numCtxSwitches = &NumCtxSwitchesStat{} + p.memInfo = &MemoryInfoStat{} + p.sigInfo = &SignalInfoStat{} + + // Extract process state + p.status = convertStatusChar(string([]byte{aixStat.Stat})) + // Recognize AIX-specific status codes if the converted value is empty + if p.status == "" { + // Status byte not recognized - use AIX-specific status codes if needed + switch aixStat.Stat { + case 0: + p.status = "NONE" + case 1: + p.status = Running // SACTIVE + case 2: + p.status = Sleep // SSLEEP + case 3: + p.status = Stop // SSTOP + case 4: + p.status = Zombie // SZOMB + case 5: + p.status = Idle // SIDL + case 6: + p.status = Wait // SWAIT + case 7: + p.status = Running // SORPHAN - treat as running + default: + p.status = UnknownState + } + } + + // Extract parent PID + p.parent = int32(aixStat.Ppid) + + // Extract TGID (same as PID on AIX, as there's no separate TGID concept) + p.tgid = int32(aixStat.Pid) + + // Cache bitness: dmodel field indicates 32-bit (0) or 64-bit (non-zero) + if aixStat.Dmodel == 0 { + aixBitnessCache.Store(p.Pid, int64(4)) + } else { + aixBitnessCache.Store(p.Pid, int64(8)) + } + + // Also read psinfo for UID/GID and thread count + infoPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "psinfo") + infoFile, err := os.Open(infoPath) + if err == nil { + defer infoFile.Close() + var aixPSinfo AIXPSInfo + err = binary.Read(infoFile, binary.BigEndian, &aixPSinfo) + if err == nil { + // Extract UIDs: real UID, effective UID, saved UID (use effective as third), and fsuid (use effective) + p.uids = []uint32{uint32(aixPSinfo.UID), uint32(aixPSinfo.Euid), uint32(aixPSinfo.Euid), uint32(aixPSinfo.Euid)} + // Extract GIDs: real GID, effective GID, saved GID (use effective as third), and fsgid (use effective) + p.gids = []uint32{uint32(aixPSinfo.Gid), uint32(aixPSinfo.Egid), uint32(aixPSinfo.Egid), uint32(aixPSinfo.Egid)} + // Extract number of threads from Nlwp field + p.numThreads = int32(aixPSinfo.Nlwp) + } + } + + return nil +} + +func (p *Process) fillFromTIDStat(tid int32) (uint64, int32, *cpu.TimesStat, int64, uint32, int32, *PageFaultsStat, error) { + return p.fillFromTIDStatWithContext(context.Background(), tid) +} + +func (p *Process) fillFromTIDStatWithContext(ctx context.Context, tid int32) (uint64, int32, *cpu.TimesStat, int64, uint32, int32, *PageFaultsStat, error) { + pid := p.Pid + var statPath string + var infoPath string + var lwpStatPath string + var lwpInfoPath string + var lwpStatFile *os.File + var lwpInfoFile *os.File + + statPath = common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "status") + infoPath = common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "psinfo") + if tid > -1 { + // Search for lwpstatus and lwpinfo files, handling unknown directory structures + tidStr := strconv.Itoa(int(tid)) + basePath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "lwp", tidStr) + lwpStatPath, lwpInfoPath = findLwpFiles(basePath) + } + + // Open the binary files + statFile, err := os.Open(statPath) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + defer statFile.Close() + infoFile, err := os.Open(infoPath) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + defer infoFile.Close() + if tid > -1 { + var err error + lwpStatFile, err = os.Open(lwpStatPath) + if err != nil { + // If we can't open lwp files, just use the main process files (tid = -1 behavior) + // This is a graceful fallback for processes without thread info + tid = -1 + } else { + defer lwpStatFile.Close() + lwpInfoFile, err = os.Open(lwpInfoPath) + if err != nil { + // If we can't open lwp info, close the stat file and fall back + lwpStatFile.Close() + tid = -1 + } else { + defer lwpInfoFile.Close() + } + } + } + + // We need to read a few binary files into a struct variables + var aixStat AIXStat + var aixPSinfo AIXPSInfo + var aixlwpStat LwpStatus + var aixlspPSinfo LwpsInfo + err = binary.Read(statFile, binary.BigEndian, &aixStat) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + err = binary.Read(infoFile, binary.BigEndian, &aixPSinfo) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + if tid > -1 { + err = binary.Read(lwpStatFile, binary.BigEndian, &aixlwpStat) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + err = binary.Read(lwpInfoFile, binary.BigEndian, &aixlspPSinfo) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + } + + // TODO: Figure out how to get terminal information for this process + + ppid := aixStat.Ppid + utime := float64(aixStat.Utime.TvSec) + stime := float64(aixStat.Stime.TvSec) + + iotime := 0.0 // TODO: Figure out actual iotime for AIX + + cpuTimes := &cpu.TimesStat{ + CPU: "cpu", + User: utime / float64(clockTicks), + System: stime / float64(clockTicks), + Iowait: iotime / float64(clockTicks), + } + + bootTime, _ := common.BootTimeWithContext(ctx, invoke) + startTime := uint64(aixPSinfo.Start.TvSec) + createTime := int64((startTime * 1000 / uint64(clockTicks)) + uint64(bootTime*1000)) + + // This information is only available at thread level + var rtpriority uint32 + var nice int32 + if tid > -1 { + rtpriority = uint32(aixlspPSinfo.Pri) + nice = int32(aixlspPSinfo.Nice) + } + + // Extract page fault data via ps command for more detailed info + pageFaults, _ := p.getPageFaults(ctx) + + return 0, int32(ppid), cpuTimes, createTime, uint32(rtpriority), nice, pageFaults, nil +} + +// getPageFaults retrieves page fault information for the process +func (p *Process) getPageFaults(ctx context.Context) (*PageFaultsStat, error) { + // Query page faults via ps command + //nolint:gosec // Process ID from internal tracking, not untrusted input + cmd := exec.CommandContext(ctx, "ps", "-v", "-p", strconv.Itoa(int(p.Pid))) + output, err := cmd.Output() + if err != nil { + return nil, err + } + + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + if len(lines) < 2 { + return nil, errors.New("insufficient ps output") + } + + // Parse ps v output - look for page fault related columns + // AIX ps -v output includes: PID, USER, TTY, STAT, TIME, SZ, RSS, %MEM, PAGEIN, etc. + fields := strings.Fields(lines[1]) + + // Try to extract PAGEIN (major page faults) + pageFaults := &PageFaultsStat{} + + // Look for numeric fields that indicate page faults + // The exact column varies, so we'll try a heuristic approach + if len(fields) >= 9 { + if pagein, err := strconv.ParseUint(fields[len(fields)-1], 10, 64); err == nil { + pageFaults.MajorFaults = pagein + } + } + + return pageFaults, nil +} + +func (p *Process) fillFromStatWithContext(ctx context.Context) (uint64, int32, *cpu.TimesStat, int64, uint32, int32, *PageFaultsStat, error) { + return p.fillFromTIDStatWithContext(ctx, -1) +} + +func pidsWithContext(ctx context.Context) ([]int32, error) { + return readPidsFromDir(common.HostProcWithContext(ctx)) +} + +func ProcessesWithContext(ctx context.Context) ([]*Process, error) { + out := []*Process{} + + pids, err := PidsWithContext(ctx) + if err != nil { + return out, err + } + + for _, pid := range pids { + p, err := NewProcessWithContext(ctx, pid) + if err != nil { + continue + } + out = append(out, p) + } + + return out, nil +} + +func readPidsFromDir(path string) ([]int32, error) { + var ret []int32 + + d, err := os.Open(path) + if err != nil { + return nil, err + } + defer d.Close() + + fnames, err := d.Readdirnames(-1) + if err != nil { + return nil, err + } + for _, fname := range fnames { + pid, err := strconv.ParseInt(fname, 10, 32) + if err != nil { + // if not numeric name, just skip + continue + } + ret = append(ret, int32(pid)) + } + + return ret, nil +} + +func splitProcStat(content []byte) []string { + nameStart := bytes.IndexByte(content, '(') + nameEnd := bytes.LastIndexByte(content, ')') + + // Defensive checks for malformed input + if nameStart < 0 || nameEnd < 0 || nameStart >= nameEnd { + // Malformed input; return empty result to avoid panic + return []string{} + } + + // Ensure rest offset is within bounds + restStart := nameEnd + 2 + if restStart > len(content) { + restStart = len(content) + } + + restFields := strings.Fields(string(content[restStart:])) // +2 skip ') ' + name := content[nameStart+1 : nameEnd] + pid := strings.TrimSpace(string(content[:nameStart])) + fields := make([]string, 3, len(restFields)+3) + fields[1] = string(pid) + fields[2] = string(name) + fields = append(fields, restFields...) + return fields +} + +// extractString extracts a null-terminated string from a byte slice, +// handling non-printable characters gracefully +func extractString(b []byte) string { + for i, c := range b { + if c == 0 { + // Found null terminator, return up to here + return string(b[:i]) + } + } + // No null terminator, return all bytes after trimming null bytes from the end + return strings.TrimRight(string(b), "\x00") +} + +// findLwpFiles searches recursively for lwpstatus and lwpinfo files under a given directory. +// AIX /proc/pid/lwp structure can vary, so this function explores the directory tree to find the files. +func findLwpFiles(basePath string) (string, string) { + // First try the direct path: basePath/lwpstatus + directStatPath := filepath.Join(basePath, "lwpstatus") + directInfoPath := filepath.Join(basePath, "lwpinfo") + if _, err := os.Stat(directStatPath); err == nil { + return directStatPath, directInfoPath + } + + // If direct path doesn't exist, recursively search the directory tree + statPath, infoPath := searchLwpFilesRecursive(basePath) + return statPath, infoPath +} + +// searchLwpFilesRecursive recursively searches for lwpstatus and lwpinfo files. +func searchLwpFilesRecursive(searchPath string) (string, string) { + d, err := os.Open(searchPath) + if err != nil { + return "", "" + } + defer d.Close() + + entries, err := d.Readdirnames(-1) + if err != nil { + return "", "" + } + + var statPath, infoPath string + + for _, entry := range entries { + fullPath := filepath.Join(searchPath, entry) + + // Check if this entry is one of our target files + if entry == "lwpstatus" && statPath == "" { + statPath = fullPath + } + if entry == "lwpinfo" && infoPath == "" { + infoPath = fullPath + } + + // If we found both files, return immediately + if statPath != "" && infoPath != "" { + return statPath, infoPath + } + + // Check if this is a directory and recurse into it + if info, err := os.Stat(fullPath); err == nil && info.IsDir() { + foundStatPath, foundInfoPath := searchLwpFilesRecursive(fullPath) + if foundStatPath != "" && statPath == "" { + statPath = foundStatPath + } + if foundInfoPath != "" && infoPath == "" { + infoPath = foundInfoPath + } + // If we found both files, return immediately + if statPath != "" && infoPath != "" { + return statPath, infoPath + } + } + } + + return statPath, infoPath +} diff --git a/process/process_aix_test.go b/process/process_aix_test.go new file mode 100644 index 0000000000..9060931af5 --- /dev/null +++ b/process/process_aix_test.go @@ -0,0 +1,468 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build aix + +package process + +import ( + "context" + "fmt" + "os" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSplitProcStat(t *testing.T) { + expectedFieldsNum := 53 + statLineContent := make([]string, expectedFieldsNum-1) + for i := 0; i < expectedFieldsNum-1; i++ { + statLineContent[i] = strconv.Itoa(i + 1) + } + + cases := []string{ + "ok", + "ok)", + "(ok", + "ok )", + "ok )(", + "ok )()", + "() ok )()", + "() ok (()", + " ) ok )", + "(ok) (ok)", + } + + consideredFields := []int{4, 7, 10, 11, 12, 13, 14, 15, 18, 22, 42} + + commandNameIndex := 2 + for _, expectedName := range cases { + statLineContent[commandNameIndex-1] = "(" + expectedName + ")" + statLine := strings.Join(statLineContent, " ") + t.Run("name: "+expectedName, func(t *testing.T) { + parsedStatLine := splitProcStat([]byte(statLine)) + assert.Equal(t, expectedName, parsedStatLine[commandNameIndex]) + for _, idx := range consideredFields { + expected := strconv.Itoa(idx) + parsed := parsedStatLine[idx] + assert.Equal( + t, expected, parsed, + "field %d (index from 1 as in man proc) must be %q but %q is received", + idx, expected, parsed, + ) + } + }) + } +} + +func TestSplitProcStat_fromFile(t *testing.T) { + pids, err := os.ReadDir("testdata/aix/") + if err != nil { + t.Error(err) + } + t.Setenv("HOST_PROC", "testdata/aix") + for _, pid := range pids { + pid, err := strconv.ParseInt(pid.Name(), 0, 32) + if err != nil { + continue + } + statFile := fmt.Sprintf("testdata/aix/%d/stat", pid) + if _, err := os.Stat(statFile); err != nil { + continue + } + contents, err := os.ReadFile(statFile) + require.NoError(t, err) + + pidStr := strconv.Itoa(int(pid)) + + ppid := "68044" // TODO: how to pass ppid to test? + + fields := splitProcStat(contents) + assert.Equal(t, pidStr, fields[1]) + assert.Equal(t, "test(cmd).sh", fields[2]) + assert.Equal(t, "S", fields[3]) + assert.Equal(t, ppid, fields[4]) + assert.Equal(t, pidStr, fields[5]) // pgrp + assert.Equal(t, ppid, fields[6]) // session + assert.Equal(t, pidStr, fields[8]) // tpgrp + assert.Equal(t, "20", fields[18]) // priority + assert.Equal(t, "1", fields[20]) // num threads + assert.Equal(t, "0", fields[52]) // exit code + } +} + +func TestFillFromCommWithContext(t *testing.T) { + pids, err := os.ReadDir("testdata/aix/") + if err != nil { + t.Error(err) + } + t.Setenv("HOST_PROC", "testdata/aix") + for _, pid := range pids { + pid, err := strconv.ParseInt(pid.Name(), 0, 32) + if err != nil { + continue + } + if _, err := os.Stat(fmt.Sprintf("testdata/aix/%d/status", pid)); err != nil { + continue + } + p, _ := NewProcess(int32(pid)) + if err := p.fillFromCommWithContext(context.Background()); err != nil { + t.Error(err) + } + } +} + +func TestFillFromStatusWithContext(t *testing.T) { + pids, err := os.ReadDir("testdata/aix/") + if err != nil { + t.Error(err) + } + t.Setenv("HOST_PROC", "testdata/aix") + for _, pid := range pids { + pid, err := strconv.ParseInt(pid.Name(), 0, 32) + if err != nil { + continue + } + if _, err := os.Stat(fmt.Sprintf("testdata/aix/%d/status", pid)); err != nil { + continue + } + p, _ := NewProcess(int32(pid)) + if err := p.fillFromStatus(); err != nil { + t.Error(err) + } + } +} + +func Benchmark_fillFromCommWithContext(b *testing.B) { + b.Setenv("HOST_PROC", "testdata/aix") + pid := 5767616 + p, _ := NewProcess(int32(pid)) + for i := 0; i < b.N; i++ { + p.fillFromCommWithContext(context.Background()) + } +} + +func Benchmark_fillFromStatusWithContext(b *testing.B) { + b.Setenv("HOST_PROC", "testdata/aix") + pid := 5767616 + p, _ := NewProcess(int32(pid)) + for i := 0; i < b.N; i++ { + p.fillFromStatus() + } +} + +func TestFillFromTIDStatWithContext_lx_brandz(t *testing.T) { + pids, err := os.ReadDir("testdata/lx_brandz/") + if err != nil { + t.Error(err) + } + t.Setenv("HOST_PROC", "testdata/lx_brandz") + for _, pid := range pids { + pid, err := strconv.ParseInt(pid.Name(), 0, 32) + if err != nil { + continue + } + if _, err := os.Stat(fmt.Sprintf("testdata/lx_brandz/%d/stat", pid)); err != nil { + continue + } + p, _ := NewProcess(int32(pid)) + _, _, cpuTimes, _, _, _, _, err := p.fillFromTIDStat(-1) + if err != nil { + t.Error(err) + } + assert.Zero(t, cpuTimes.Iowait) + } +} + +func TestProcessMemoryMaps(t *testing.T) { + t.Setenv("HOST_PROC", "testdata/aix") + pid := 1 + p, err := NewProcess(int32(pid)) + require.NoError(t, err) + maps, err := p.MemoryMaps(false) + require.NoError(t, err) + + expected := &[]MemoryMapsStat{ + { + "[vvar]", + 0, + 1, + 0, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + }, + { + "", + 0, + 1, + 2, + 3, + 4, + 0, + 6, + 7, + 8, + 9, + }, + { + "[vdso]", + 0, + 1, + 2, + 3, + 4, + 5, + 0, + 7, + 8, + 9, + }, + { + "/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1", + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 0, + 9, + }, + } + + require.Equal(t, expected, maps) +} + +func TestFillFromExeWithContext(t *testing.T) { + pids, err := os.ReadDir("testdata/aix/") + if err != nil { + t.Error(err) + } + t.Setenv("HOST_PROC", "testdata/aix") + for _, pidDir := range pids { + pid, err := strconv.ParseInt(pidDir.Name(), 0, 32) + if err != nil { + continue + } + psinfo := fmt.Sprintf("testdata/aix/%d/psinfo", pid) + if _, err := os.Stat(psinfo); err != nil { + continue + } + p, err := NewProcess(int32(pid)) + require.NoError(t, err) + exe, err := p.fillFromExeWithContext(context.Background()) + if err == nil { + // Should get a string (possibly empty or with executable name) + assert.IsType(t, "", exe) + } + } +} + +func TestFillFromCmdlineWithContext(t *testing.T) { + pids, err := os.ReadDir("testdata/aix/") + if err != nil { + t.Error(err) + } + t.Setenv("HOST_PROC", "testdata/aix") + for _, pidDir := range pids { + pid, err := strconv.ParseInt(pidDir.Name(), 0, 32) + if err != nil { + continue + } + psinfo := fmt.Sprintf("testdata/aix/%d/psinfo", pid) + if _, err := os.Stat(psinfo); err != nil { + continue + } + p, err := NewProcess(int32(pid)) + require.NoError(t, err) + cmdline, err := p.fillFromCmdlineWithContext(context.Background()) + if err == nil { + // Should get a string (possibly empty or with command line) + assert.IsType(t, "", cmdline) + } + } +} + +func TestFillFromCmdlineSliceWithContext(t *testing.T) { + pids, err := os.ReadDir("testdata/aix/") + if err != nil { + t.Error(err) + } + t.Setenv("HOST_PROC", "testdata/aix") + for _, pidDir := range pids { + pid, err := strconv.ParseInt(pidDir.Name(), 0, 32) + if err != nil { + continue + } + psinfo := fmt.Sprintf("testdata/aix/%d/psinfo", pid) + if _, err := os.Stat(psinfo); err != nil { + continue + } + p, err := NewProcess(int32(pid)) + require.NoError(t, err) + cmdlineSlice, err := p.fillSliceFromCmdlineWithContext(context.Background()) + if err == nil { + // Should get a slice of strings + assert.IsType(t, []string{}, cmdlineSlice) + } + } +} + +func TestFillFromStatmWithContext(t *testing.T) { + pids, err := os.ReadDir("testdata/aix/") + if err != nil { + t.Error(err) + } + t.Setenv("HOST_PROC", "testdata/aix") + for _, pidDir := range pids { + pid, err := strconv.ParseInt(pidDir.Name(), 0, 32) + if err != nil { + continue + } + psinfo := fmt.Sprintf("testdata/aix/%d/psinfo", pid) + if _, err := os.Stat(psinfo); err != nil { + continue + } + p, err := NewProcess(int32(pid)) + require.NoError(t, err) + memInfo, memInfoEx, err := p.fillFromStatmWithContext(context.Background()) + if err == nil { + assert.NotNil(t, memInfo) + assert.NotNil(t, memInfoEx) + // Memory values should be non-negative + //nolint:testifylint // value is always >= 0, but we validate it + assert.GreaterOrEqual(t, memInfo.VMS, uint64(0)) + //nolint:testifylint // value is always >= 0, but we validate it + assert.GreaterOrEqual(t, memInfo.RSS, uint64(0)) + //nolint:testifylint // value is always >= 0, but we validate it + assert.GreaterOrEqual(t, memInfoEx.VMS, uint64(0)) + //nolint:testifylint // value is always >= 0, but we validate it + assert.GreaterOrEqual(t, memInfoEx.RSS, uint64(0)) + } + } +} + +func TestTerminalWithContext(t *testing.T) { + // Get current process + ctx := context.Background() + p := Process{Pid: int32(os.Getpid())} + + terminal, err := p.TerminalWithContext(ctx) + // Terminal may or may not be available depending on how test is run + if err == nil { + assert.IsType(t, "", terminal) + } +} + +func TestEnvironmentWithContext(t *testing.T) { + // Get current process + ctx := context.Background() + p := Process{Pid: int32(os.Getpid())} + + env, err := p.EnvironmentWithContext(ctx) + if err == nil { + assert.NotNil(t, env) + assert.IsType(t, map[string]string{}, env) + } +} + +func TestPageFaultsWithContext(t *testing.T) { + // Get current process + ctx := context.Background() + p := Process{Pid: int32(os.Getpid())} + + pageFaults, err := p.PageFaultsWithContext(ctx) + if err != nil { + t.Logf("PageFaultsWithContext error: %v", err) + return + } + if pageFaults != nil { + // Page fault counts should be non-negative + //nolint:testifylint // minor faults field is naturally >= 0 + assert.GreaterOrEqual(t, pageFaults.MinorFaults, uint64(0)) + //nolint:testifylint // major faults field is naturally >= 0 + assert.GreaterOrEqual(t, pageFaults.MajorFaults, uint64(0)) + } +} + +func TestRlimitUsageWithContext(t *testing.T) { + // Get current process + ctx := context.Background() + p := Process{Pid: int32(os.Getpid())} + + limits, err := p.RlimitUsageWithContext(ctx, false) + if err != nil { + t.Logf("RlimitUsageWithContext error: %v", err) + return + } + if len(limits) > 0 { + for _, limit := range limits { + // Hard limit should be >= soft limit + assert.GreaterOrEqual(t, limit.Hard, limit.Soft) + } + } +} + +func TestIOCountersWithContext(t *testing.T) { + // Get current process + ctx := context.Background() + p := Process{Pid: int32(os.Getpid())} + + ioCounters, err := p.IOCountersWithContext(ctx) + // IOCounters may not be available without WLM+iostat configuration + if err == nil { + assert.NotNil(t, ioCounters) + //nolint:testifylint // checking non-negative constraint + assert.GreaterOrEqual(t, ioCounters.ReadBytes, uint64(0)) + //nolint:testifylint // checking non-negative constraint + assert.GreaterOrEqual(t, ioCounters.WriteBytes, uint64(0)) + } +} + +func TestCPUAffinityWithContext(t *testing.T) { + // Get current process + ctx := context.Background() + p := Process{Pid: int32(os.Getpid())} + + affinity, err := p.CPUAffinityWithContext(ctx) + // CPU affinity may not be available on all AIX systems + if err == nil { + assert.NotEmpty(t, affinity) + for _, cpu := range affinity { + assert.GreaterOrEqual(t, cpu, int32(0)) + } + } +} + +func TestCPUPercentWithContext(t *testing.T) { + // Get current process + ctx := context.Background() + p := Process{Pid: int32(os.Getpid())} + + percent, err := p.CPUPercentWithContext(ctx) + require.NoError(t, err) + // CPU percent should be >= 0 + assert.GreaterOrEqual(t, percent, float64(0)) + // CPU percent should not exceed 100 * number of CPUs (but ps can sometimes report >100% on single CPU) + assert.Less(t, percent, float64(500)) // sanity check to avoid absurd values +} + +func TestSignalsPendingWithContext(t *testing.T) { + // Get current process + ctx := context.Background() + p := Process{Pid: int32(os.Getpid())} + + sigInfo, err := p.SignalsPendingWithContext(ctx) + require.NoError(t, err) + // SignalInfoStat should be valid (may have zero pending signals for normal process) + assert.NotNil(t, &sigInfo) +} diff --git a/process/process_darwin.go b/process/process_darwin.go index d44c7e8e27..0bfcd59a69 100644 --- a/process/process_darwin.go +++ b/process/process_darwin.go @@ -539,3 +539,7 @@ func (p *Process) NumFDsWithContext(_ context.Context) (int32, error) { numFDs := ret / sizeofProcFDInfo return numFDs, nil } + +func (*Process) SignalsPendingWithContext(_ context.Context) (SignalInfoStat, error) { + return SignalInfoStat{}, common.ErrNotImplementedError +} diff --git a/process/process_fallback.go b/process/process_fallback.go index 699311a9ca..0314b0aa39 100644 --- a/process/process_fallback.go +++ b/process/process_fallback.go @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -//go:build !darwin && !linux && !freebsd && !openbsd && !windows && !solaris && !plan9 +//go:build !darwin && !linux && !freebsd && !openbsd && !windows && !solaris && !plan9 && !aix package process @@ -142,6 +142,10 @@ func (*Process) CPUAffinityWithContext(_ context.Context) ([]int32, error) { return nil, common.ErrNotImplementedError } +func (*Process) SignalsPendingWithContext(_ context.Context) (SignalInfoStat, error) { + return SignalInfoStat{}, common.ErrNotImplementedError +} + func (*Process) MemoryInfoWithContext(_ context.Context) (*MemoryInfoStat, error) { return nil, common.ErrNotImplementedError } diff --git a/process/process_freebsd.go b/process/process_freebsd.go index 283af9bb3f..474107a7b1 100644 --- a/process/process_freebsd.go +++ b/process/process_freebsd.go @@ -287,6 +287,10 @@ func (p *Process) MemoryInfoWithContext(_ context.Context) (*MemoryInfoStat, err }, nil } +func (*Process) SignalsPendingWithContext(_ context.Context) (SignalInfoStat, error) { + return SignalInfoStat{}, common.ErrNotImplementedError +} + func (p *Process) ChildrenWithContext(ctx context.Context) ([]*Process, error) { procs, err := ProcessesWithContext(ctx) if err != nil { diff --git a/process/process_linux.go b/process/process_linux.go index 499d54acbf..db4a986561 100644 --- a/process/process_linux.go +++ b/process/process_linux.go @@ -314,6 +314,17 @@ func (*Process) CPUAffinityWithContext(_ context.Context) ([]int32, error) { return nil, common.ErrNotImplementedError } +func (p *Process) SignalsPendingWithContext(ctx context.Context) (SignalInfoStat, error) { + err := p.fillFromStatusWithContext(ctx) + if err != nil { + return SignalInfoStat{}, err + } + if p.sigInfo == nil { + return SignalInfoStat{}, nil + } + return *p.sigInfo, nil +} + func (p *Process) MemoryInfoWithContext(ctx context.Context) (*MemoryInfoStat, error) { meminfo, _, err := p.fillFromStatmWithContext(ctx) if err != nil { diff --git a/process/process_openbsd.go b/process/process_openbsd.go index 31fdb85bc9..9d91dc1ddf 100644 --- a/process/process_openbsd.go +++ b/process/process_openbsd.go @@ -399,3 +399,7 @@ func callKernProcSyscall(op, arg int32) ([]byte, uint64, error) { return buf, length, nil } + +func (*Process) SignalsPendingWithContext(_ context.Context) (SignalInfoStat, error) { + return SignalInfoStat{}, common.ErrNotImplementedError +} diff --git a/process/process_plan9.go b/process/process_plan9.go index bdb07ff285..0fd82d6826 100644 --- a/process/process_plan9.go +++ b/process/process_plan9.go @@ -201,3 +201,7 @@ func (*Process) UsernameWithContext(_ context.Context) (string, error) { func (*Process) EnvironWithContext(_ context.Context) ([]string, error) { return nil, common.ErrNotImplementedError } + +func (*Process) SignalsPendingWithContext(_ context.Context) (SignalInfoStat, error) { + return SignalInfoStat{}, common.ErrNotImplementedError +} diff --git a/process/process_posix.go b/process/process_posix.go index 9fe55b490e..25d890ce7e 100644 --- a/process/process_posix.go +++ b/process/process_posix.go @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -//go:build linux || freebsd || openbsd || darwin || solaris +//go:build linux || freebsd || openbsd || darwin || solaris || aix package process diff --git a/process/process_solaris.go b/process/process_solaris.go index 547d228721..4d4675ddab 100644 --- a/process/process_solaris.go +++ b/process/process_solaris.go @@ -165,6 +165,10 @@ func (*Process) MemoryInfoExWithContext(_ context.Context) (*MemoryInfoExStat, e return nil, common.ErrNotImplementedError } +func (*Process) SignalsPendingWithContext(_ context.Context) (SignalInfoStat, error) { + return SignalInfoStat{}, common.ErrNotImplementedError +} + func (*Process) PageFaultsWithContext(_ context.Context) (*PageFaultsStat, error) { return nil, common.ErrNotImplementedError } diff --git a/process/process_test.go b/process/process_test.go index 94cb2dbb10..87efc561a0 100644 --- a/process/process_test.go +++ b/process/process_test.go @@ -1,4 +1,6 @@ // SPDX-License-Identifier: BSD-3-Clause +//go:build !aix + package process import ( diff --git a/process/process_windows.go b/process/process_windows.go index f4cbfa2966..4810f25758 100644 --- a/process/process_windows.go +++ b/process/process_windows.go @@ -633,6 +633,10 @@ func (*Process) CPUAffinityWithContext(_ context.Context) ([]int32, error) { return nil, common.ErrNotImplementedError } +func (*Process) SignalsPendingWithContext(_ context.Context) (SignalInfoStat, error) { + return SignalInfoStat{}, common.ErrNotImplementedError +} + func (p *Process) MemoryInfoWithContext(_ context.Context) (*MemoryInfoStat, error) { mem, err := getMemoryInfo(p.Pid) if err != nil { diff --git a/process/testdata/aix/1/lwp/65539/lwpsinfo b/process/testdata/aix/1/lwp/65539/lwpsinfo new file mode 100644 index 0000000000..ef35272ca2 Binary files /dev/null and b/process/testdata/aix/1/lwp/65539/lwpsinfo differ diff --git a/process/testdata/aix/1/lwp/65539/lwpstatus b/process/testdata/aix/1/lwp/65539/lwpstatus new file mode 100644 index 0000000000..3a294cb4ec Binary files /dev/null and b/process/testdata/aix/1/lwp/65539/lwpstatus differ diff --git a/process/testdata/aix/1/psinfo b/process/testdata/aix/1/psinfo new file mode 100644 index 0000000000..a6047a7d2f Binary files /dev/null and b/process/testdata/aix/1/psinfo differ diff --git a/process/testdata/aix/1/status b/process/testdata/aix/1/status new file mode 100644 index 0000000000..78b2ddd5c5 Binary files /dev/null and b/process/testdata/aix/1/status differ diff --git a/process/testdata/aix/12845476/lwp/27853159/lwpsinfo b/process/testdata/aix/12845476/lwp/27853159/lwpsinfo new file mode 100644 index 0000000000..c09757f382 Binary files /dev/null and b/process/testdata/aix/12845476/lwp/27853159/lwpsinfo differ diff --git a/process/testdata/aix/12845476/lwp/27853159/lwpstatus b/process/testdata/aix/12845476/lwp/27853159/lwpstatus new file mode 100644 index 0000000000..44f2797727 Binary files /dev/null and b/process/testdata/aix/12845476/lwp/27853159/lwpstatus differ diff --git a/process/testdata/aix/12845476/psinfo b/process/testdata/aix/12845476/psinfo new file mode 100644 index 0000000000..1a2e40263e Binary files /dev/null and b/process/testdata/aix/12845476/psinfo differ diff --git a/process/testdata/aix/12845476/status b/process/testdata/aix/12845476/status new file mode 100644 index 0000000000..61412b368a Binary files /dev/null and b/process/testdata/aix/12845476/status differ diff --git a/process/testdata/aix/5767616/lwp/9568687/lwpsinfo b/process/testdata/aix/5767616/lwp/9568687/lwpsinfo new file mode 100644 index 0000000000..48e74e1cd4 Binary files /dev/null and b/process/testdata/aix/5767616/lwp/9568687/lwpsinfo differ diff --git a/process/testdata/aix/5767616/lwp/9568687/lwpstatus b/process/testdata/aix/5767616/lwp/9568687/lwpstatus new file mode 100644 index 0000000000..65103edf7d Binary files /dev/null and b/process/testdata/aix/5767616/lwp/9568687/lwpstatus differ diff --git a/process/testdata/aix/5767616/psinfo b/process/testdata/aix/5767616/psinfo new file mode 100644 index 0000000000..7262b2c4e3 Binary files /dev/null and b/process/testdata/aix/5767616/psinfo differ diff --git a/process/testdata/aix/5767616/status b/process/testdata/aix/5767616/status new file mode 100644 index 0000000000..dfea1cc1aa Binary files /dev/null and b/process/testdata/aix/5767616/status differ