From 50cda65469a109a95863cfd6f7a924c969c05d6d Mon Sep 17 00:00:00 2001 From: Pierre Gimalac Date: Sun, 15 Mar 2026 19:58:27 +0000 Subject: [PATCH 1/4] feat: process info collection on AIX --- process/process_aix.go | 384 ++++++++++++++++++++++++++++++++++++ process/process_aix_test.go | 290 +++++++++++++++++++++++++++ process/process_fallback.go | 2 +- process/process_posix.go | 2 +- 4 files changed, 676 insertions(+), 2 deletions(-) create mode 100644 process/process_aix.go create mode 100644 process/process_aix_test.go diff --git a/process/process_aix.go b/process/process_aix.go new file mode 100644 index 0000000000..965a0880f6 --- /dev/null +++ b/process/process_aix.go @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build aix + +package process + +import ( + "bytes" + "context" + "encoding/binary" + "os" + "strconv" + "strings" + + "github.com/shirou/gopsutil/v4/cpu" + "github.com/shirou/gopsutil/v4/internal/common" + "github.com/shirou/gopsutil/v4/net" +) + +// MemoryMapsStat is not available on AIX. +type MemoryMapsStat struct{} + +// MemoryInfoExStat is not available on AIX. +type MemoryInfoExStat struct{} + +// prTimestruc64 mirrors timestruc64_t from sys/time.h +type prTimestruc64 struct { + Sec int64 + Nsec int32 + _ uint32 +} + +// lwpSinfo mirrors AIX lwpsinfo_t from sys/procfs.h +type lwpSinfo struct { + LwpID uint64 + Addr uint64 + Wchan uint64 + Flag uint32 + Wtype uint8 + State int8 + Sname byte // process state character: 'R','S','Z','T','I', etc + Nice uint8 + Pri int32 + Policy uint32 + Clname [8]byte + Onpro int32 + Bindpro int32 + Ptid uint32 + _ uint32 + _ [7]uint64 +} + +// psinfo mirrors AIX psinfo_t from sys/procfs.h +type psinfo struct { + Flag uint32 + Flag2 uint32 + Nlwp uint32 // number of threads + _ uint32 + Uid uint64 + Euid uint64 + Gid uint64 + Egid uint64 + Pid uint64 + Ppid uint64 + Pgid uint64 + Sid uint64 + Ttydev uint64 + Addr uint64 + Size uint64 // virtual memory size in KB (pr_size) + Rssize uint64 // resident set size in KB (pr_rssize) + Start prTimestruc64 // process start time + Time prTimestruc64 // combined user+system CPU time + Cid uint16 + _ uint16 + Argc uint32 + Argv uint64 + Envp uint64 + Fname [16]byte // executable name, null-terminated (max 15 chars) + Psargs [80]byte // process args, space-separated, null-terminated (max 79 chars) + _ [8]uint64 + Lwp lwpSinfo // representative LWP +} + +func readPsinfo(ctx context.Context, pid int32) (*psinfo, error) { + f, err := os.Open(common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "psinfo")) + if err != nil { + return nil, err + } + defer f.Close() + + var psi psinfo + if err := binary.Read(f, binary.BigEndian, &psi); err != nil { + return nil, err + } + return &psi, nil +} + +func nullTerminatedBytes(b []byte) string { + if idx := bytes.IndexByte(b, 0); idx >= 0 { + return string(b[:idx]) + } + return string(b) +} + +func pidsWithContext(ctx context.Context) ([]int32, error) { + dir, err := os.Open(common.HostProcWithContext(ctx)) + if err != nil { + return nil, err + } + defer dir.Close() + + names, err := dir.Readdirnames(-1) + if err != nil { + return nil, err + } + + pids := make([]int32, 0, len(names)) + for _, name := range names { + pid, err := strconv.ParseInt(name, 10, 32) + if err != nil { + continue // skip non-numeric entries (e.g. "net", "sys") + } + pids = append(pids, int32(pid)) + } + return pids, nil +} + +func ProcessesWithContext(ctx context.Context) ([]*Process, error) { + pids, err := pidsWithContext(ctx) + if err != nil { + return nil, err + } + ret := make([]*Process, 0, len(pids)) + for _, pid := range pids { + // create Process struct directly to avoid the redundant PidExists check + ret = append(ret, &Process{Pid: pid}) + } + return ret, nil +} + +func (p *Process) PpidWithContext(ctx context.Context) (int32, error) { + psi, err := readPsinfo(ctx, p.Pid) + if err != nil { + return 0, err + } + return int32(psi.Ppid), nil +} + +func (p *Process) NameWithContext(ctx context.Context) (string, error) { + psi, err := readPsinfo(ctx, p.Pid) + if err != nil { + return "", err + } + if name := nullTerminatedBytes(psi.Fname[:]); name != "" { + return name, nil + } + // PID 0 is the swapper/idle process; its Fname is empty but ps shows "swapper". + if p.Pid == 0 { + return "swapper", nil + } + return "", nil +} + +func (p *Process) TgidWithContext(_ context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) ExeWithContext(_ context.Context) (string, error) { + return "", common.ErrNotImplementedError +} + +func (p *Process) CmdlineWithContext(ctx context.Context) (string, error) { + psi, err := readPsinfo(ctx, p.Pid) + if err != nil { + return "", err + } + // Psargs is empty for kernel threads, fall back to Fname (the executable name), + // which is what ps uses as well. + if args := nullTerminatedBytes(psi.Psargs[:]); args != "" { + return args, nil + } + if name := nullTerminatedBytes(psi.Fname[:]); name != "" { + return name, nil + } + // PID 0 is the swapper/idle process, its Fname and Psargs are both empty + // but ps shows "swapper". + if p.Pid == 0 { + return "swapper", nil + } + return "", nil +} + +func (p *Process) CmdlineSliceWithContext(ctx context.Context) ([]string, error) { + psi, err := readPsinfo(ctx, p.Pid) + if err != nil { + return nil, err + } + // Psargs is empty for kernel threads, fall back to Fname (the executable name), + // which is what ps uses as well. + args := nullTerminatedBytes(psi.Psargs[:]) + if args == "" { + args = nullTerminatedBytes(psi.Fname[:]) + } + // PID 0 is the swapper/idle process, its Fname and Psargs are both empty + // but ps shows "swapper". + if args == "" && p.Pid == 0 { + args = "swapper" + } + if args == "" { + return nil, nil + } + return strings.Fields(args), nil +} + +func (p *Process) createTimeWithContext(ctx context.Context) (int64, error) { + psi, err := readPsinfo(ctx, p.Pid) + if err != nil { + return 0, err + } + return psi.Start.Sec*1000 + int64(psi.Start.Nsec)/1000000, nil +} + +func (p *Process) CwdWithContext(_ context.Context) (string, error) { + return "", common.ErrNotImplementedError +} + +func (p *Process) StatusWithContext(ctx context.Context) ([]string, error) { + psi, err := readPsinfo(ctx, p.Pid) + if err != nil { + return nil, err + } + return []string{convertStatusChar(string([]byte{psi.Lwp.Sname}))}, nil +} + +func (p *Process) ForegroundWithContext(_ context.Context) (bool, error) { + return false, common.ErrNotImplementedError +} + +func (p *Process) UidsWithContext(ctx context.Context) ([]uint32, error) { + psi, err := readPsinfo(ctx, p.Pid) + if err != nil { + return nil, err + } + // real, effective, saved (psinfo doesn't expose saved, use real as fallback) + return []uint32{uint32(psi.Uid), uint32(psi.Euid), uint32(psi.Uid)}, nil +} + +func (p *Process) GidsWithContext(ctx context.Context) ([]uint32, error) { + psi, err := readPsinfo(ctx, p.Pid) + if err != nil { + return nil, err + } + // real, effective, saved (psinfo doesn't expose saved, use real as fallback) + return []uint32{uint32(psi.Gid), uint32(psi.Egid), uint32(psi.Gid)}, nil +} + +func (p *Process) GroupsWithContext(_ context.Context) ([]uint32, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) TerminalWithContext(_ context.Context) (string, error) { + return "", common.ErrNotImplementedError +} + +func (p *Process) NiceWithContext(ctx context.Context) (int32, error) { + psi, err := readPsinfo(ctx, p.Pid) + if err != nil { + return 0, err + } + // Returns the raw pr_nice value from lwpsinfo_t, this is not the same as what ps displays + return int32(psi.Lwp.Nice), nil +} + +func (p *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(_ context.Context, _ bool) ([]RlimitStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) NumCtxSwitchesWithContext(_ context.Context) (*NumCtxSwitchesStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) NumFDsWithContext(_ context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) NumThreadsWithContext(ctx context.Context) (int32, error) { + psi, err := readPsinfo(ctx, p.Pid) + if err != nil { + return 0, err + } + return int32(psi.Nlwp), nil +} + +func (p *Process) ThreadsWithContext(_ context.Context) (map[int32]*cpu.TimesStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) TimesWithContext(ctx context.Context) (*cpu.TimesStat, error) { + psi, err := readPsinfo(ctx, p.Pid) + if err != nil { + return nil, err + } + // psinfo only provides combined user+system time + combined := float64(psi.Time.Sec) + float64(psi.Time.Nsec)/1e9 + return &cpu.TimesStat{ + CPU: "cpu", + User: combined, + System: 0, + }, nil +} + +func (p *Process) CPUAffinityWithContext(_ context.Context) ([]int32, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) MemoryInfoWithContext(ctx context.Context) (*MemoryInfoStat, error) { + psi, err := readPsinfo(ctx, p.Pid) + if err != nil { + return nil, err + } + // pr_size and pr_rssize are in KB per documentation + return &MemoryInfoStat{ + RSS: psi.Rssize * 1024, + VMS: psi.Size * 1024, + }, nil +} + +func (p *Process) MemoryInfoExWithContext(_ context.Context) (*MemoryInfoExStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) PageFaultsWithContext(_ context.Context) (*PageFaultsStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) ChildrenWithContext(ctx context.Context) ([]*Process, error) { + pids, err := pidsWithContext(ctx) + if err != nil { + return nil, err + } + ret := make([]*Process, 0) + for _, pid := range pids { + psi, err := readPsinfo(ctx, pid) + if err != nil { + continue + } + if int32(psi.Ppid) == p.Pid { + // create Process struct directly to avoid the redundant PidExists check + ret = append(ret, &Process{Pid: pid}) + } + } + return ret, nil +} + +func (p *Process) OpenFilesWithContext(_ context.Context) ([]OpenFilesStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) ConnectionsWithContext(_ context.Context) ([]net.ConnectionStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) ConnectionsMaxWithContext(_ context.Context, _ int) ([]net.ConnectionStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) MemoryMapsWithContext(_ context.Context, _ bool) (*[]MemoryMapsStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) IOCountersWithContext(_ context.Context) (*IOCountersStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) EnvironWithContext(_ context.Context) ([]string, error) { + return nil, common.ErrNotImplementedError +} diff --git a/process/process_aix_test.go b/process/process_aix_test.go new file mode 100644 index 0000000000..61f5b904b4 --- /dev/null +++ b/process/process_aix_test.go @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build aix + +package process + +import ( + "bytes" + "context" + "encoding/binary" + "os" + "path/filepath" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// fillPsinfo writes the Fname and Psargs as bytes in the psinfo struct and returns it +func fillPsinfo(psi psinfo, fname, psargs string) psinfo { + copy(psi.Fname[:], fname) + copy(psi.Psargs[:], psargs) + return psi +} + +// writeFakePsinfo serializes psi as big-endian and writes it to +// //psinfo, creating the directory as needed. +func writeFakePsinfo(t *testing.T, dir string, psi psinfo) { + t.Helper() + pidDir := filepath.Join(dir, strconv.FormatUint(psi.Pid, 10)) + require.NoError(t, os.MkdirAll(pidDir, 0o755)) + + var buf bytes.Buffer + require.NoError(t, binary.Write(&buf, binary.BigEndian, &psi)) + require.NoError(t, os.WriteFile(filepath.Join(pidDir, "psinfo"), buf.Bytes(), 0o644)) +} + +// setupFakeProc creates a temp directory with fake psinfo files, +// sets HOST_PROC to point at it, and returns a context. +func setupFakeProc(t *testing.T, procs ...psinfo) context.Context { + t.Helper() + dir := t.TempDir() + for _, psi := range procs { + writeFakePsinfo(t, dir, psi) + } + t.Setenv("HOST_PROC", dir) + return context.Background() +} + +// mockProc is the main test process fixture +var mockProc = fillPsinfo(psinfo{ + Pid: 1234, + Ppid: 100, + Uid: 501, + Euid: 502, + Gid: 20, + Egid: 21, + Nlwp: 3, + Size: 256, + Rssize: 64, + Start: prTimestruc64{Sec: 1700000000, Nsec: 123000000}, + Time: prTimestruc64{Sec: 5, Nsec: 250000000}, + Lwp: lwpSinfo{Nice: 65, Sname: 'R'}, +}, "myproc", "/usr/bin/myproc -v --flag arg") + +// childProc is a child of mockProc (Ppid == mockProc.Pid) +var childProc = fillPsinfo(psinfo{ + Pid: 5678, + Ppid: 1234, + Uid: 501, + Euid: 501, + Gid: 20, + Egid: 20, + Nlwp: 1, + Size: 128, + Rssize: 32, + Lwp: lwpSinfo{Sname: 'S'}, +}, "child", "/usr/bin/child") + +func TestPsinfoPpid(t *testing.T) { + ctx := setupFakeProc(t, mockProc) + p := &Process{Pid: int32(mockProc.Pid)} + ppid, err := p.PpidWithContext(ctx) + require.NoError(t, err) + assert.Equal(t, int32(mockProc.Ppid), ppid) +} + +func TestPsinfoName(t *testing.T) { + ctx := setupFakeProc(t, mockProc) + p := &Process{Pid: int32(mockProc.Pid)} + name, err := p.NameWithContext(ctx) + require.NoError(t, err) + assert.Equal(t, "myproc", name) +} + +func TestPsinfoCmdline(t *testing.T) { + ctx := setupFakeProc(t, mockProc) + p := &Process{Pid: int32(mockProc.Pid)} + cmd, err := p.CmdlineWithContext(ctx) + require.NoError(t, err) + assert.Equal(t, "/usr/bin/myproc -v --flag arg", cmd) +} + +func TestPsinfoCmdlineSlice(t *testing.T) { + ctx := setupFakeProc(t, mockProc) + p := &Process{Pid: int32(mockProc.Pid)} + args, err := p.CmdlineSliceWithContext(ctx) + require.NoError(t, err) + assert.Equal(t, []string{"/usr/bin/myproc", "-v", "--flag", "arg"}, args) +} + +func TestPsinfoCmdlineSliceEmpty(t *testing.T) { + // Both Psargs and Fname empty → nil + ctx := setupFakeProc(t, psinfo{Pid: 9999, Ppid: 1, Lwp: lwpSinfo{Sname: 'S'}}) + p := &Process{Pid: 9999} + args, err := p.CmdlineSliceWithContext(ctx) + require.NoError(t, err) + assert.Nil(t, args) +} + +func TestPsinfoCmdlineFallsBackToFname(t *testing.T) { + // Psargs empty (kernel thread) → falls back to Fname, same as ps + ctx := setupFakeProc(t, fillPsinfo(psinfo{Pid: 9999, Ppid: 1, Lwp: lwpSinfo{Sname: 'S'}}, "kthread", "")) + p := &Process{Pid: 9999} + cmd, err := p.CmdlineWithContext(ctx) + require.NoError(t, err) + assert.Equal(t, "kthread", cmd) + + args, err := p.CmdlineSliceWithContext(ctx) + require.NoError(t, err) + assert.Equal(t, []string{"kthread"}, args) +} + +func TestPsinfoCreateTime(t *testing.T) { + ctx := setupFakeProc(t, mockProc) + p := &Process{Pid: int32(mockProc.Pid)} + ct, err := p.CreateTimeWithContext(ctx) + require.NoError(t, err) + // 1700000000 * 1000 + 123000000 / 1000000 = 1700000000000 + 123 = 1700000000123 + assert.Equal(t, int64(1700000000123), ct) +} + +func TestPsinfoStatus(t *testing.T) { + tests := []struct { + sname byte + expected string + }{ + {'R', Running}, + {'S', Sleep}, + {'Z', Zombie}, + {'T', Stop}, + {'I', Idle}, + {'X', UnknownState}, // unknown → UnknownState + } + for _, tt := range tests { + ctx := setupFakeProc(t, psinfo{Pid: 42, Ppid: 1, Lwp: lwpSinfo{Sname: tt.sname}}) + p := &Process{Pid: 42} + status, err := p.StatusWithContext(ctx) + require.NoError(t, err) + assert.Equal(t, []string{tt.expected}, status, "sname=%c", tt.sname) + } +} + +func TestPsinfoUids(t *testing.T) { + ctx := setupFakeProc(t, mockProc) + p := &Process{Pid: int32(mockProc.Pid)} + uids, err := p.UidsWithContext(ctx) + require.NoError(t, err) + // real=501, effective=502, saved=501 (fallback to real) + assert.Equal(t, []uint32{501, 502, 501}, uids) +} + +func TestPsinfoGids(t *testing.T) { + ctx := setupFakeProc(t, mockProc) + p := &Process{Pid: int32(mockProc.Pid)} + gids, err := p.GidsWithContext(ctx) + require.NoError(t, err) + // real=20, effective=21, saved=20 (fallback to real) + assert.Equal(t, []uint32{20, 21, 20}, gids) +} + +func TestPsinfoNice(t *testing.T) { + ctx := setupFakeProc(t, mockProc) + p := &Process{Pid: int32(mockProc.Pid)} + nice, err := p.NiceWithContext(ctx) + require.NoError(t, err) + assert.Equal(t, int32(65), nice) // raw pr_nice value +} + +func TestPsinfoNumThreads(t *testing.T) { + ctx := setupFakeProc(t, mockProc) + p := &Process{Pid: int32(mockProc.Pid)} + n, err := p.NumThreadsWithContext(ctx) + require.NoError(t, err) + assert.Equal(t, int32(3), n) +} + +func TestPsinfoTimes(t *testing.T) { + ctx := setupFakeProc(t, mockProc) + p := &Process{Pid: int32(mockProc.Pid)} + times, err := p.TimesWithContext(ctx) + require.NoError(t, err) + assert.Equal(t, "cpu", times.CPU) + assert.InDelta(t, 5.25, times.User, 1e-6) + assert.Equal(t, float64(0), times.System) +} + +func TestPsinfoMemoryInfo(t *testing.T) { + ctx := setupFakeProc(t, mockProc) + p := &Process{Pid: int32(mockProc.Pid)} + mem, err := p.MemoryInfoWithContext(ctx) + require.NoError(t, err) + // pr_rssize=64 KB, pr_size=256 KB → bytes + assert.Equal(t, uint64(64)*1024, mem.RSS) + assert.Equal(t, uint64(256)*1024, mem.VMS) +} + +func TestPsinfoPids(t *testing.T) { + ctx := setupFakeProc(t, mockProc, childProc) + pids, err := pidsWithContext(ctx) + require.NoError(t, err) + assert.ElementsMatch(t, []int32{1234, 5678}, pids) +} + +func TestPsinfoPidsSkipsNonNumeric(t *testing.T) { + dir := t.TempDir() + writeFakePsinfo(t, dir, mockProc) + require.NoError(t, os.MkdirAll(filepath.Join(dir, "net"), 0o755)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, "sys"), 0o755)) + t.Setenv("HOST_PROC", dir) + pids, err := pidsWithContext(context.Background()) + require.NoError(t, err) + assert.Equal(t, []int32{1234}, pids) +} + +func TestPsinfoProcessesWithContext(t *testing.T) { + ctx := setupFakeProc(t, mockProc, childProc) + procs, err := ProcessesWithContext(ctx) + require.NoError(t, err) + require.Len(t, procs, 2) + + pids := make(map[int32]struct{}) + for _, p := range procs { + pids[p.Pid] = struct{}{} + } + assert.Contains(t, pids, int32(mockProc.Pid)) + assert.Contains(t, pids, int32(childProc.Pid)) +} + +func TestPsinfoChildren(t *testing.T) { + ctx := setupFakeProc(t, mockProc, childProc) + parent := &Process{Pid: int32(mockProc.Pid)} + children, err := parent.ChildrenWithContext(ctx) + require.NoError(t, err) + require.Len(t, children, 1) + assert.Equal(t, int32(childProc.Pid), children[0].Pid) +} + +func TestPsinfoChildrenNone(t *testing.T) { + ctx := setupFakeProc(t, mockProc) + parent := &Process{Pid: int32(mockProc.Pid)} + children, err := parent.ChildrenWithContext(ctx) + require.NoError(t, err) + assert.Empty(t, children) +} + +func TestPsinfoMissingFile(t *testing.T) { + ctx := setupFakeProc(t) // empty proc dir + p := &Process{Pid: 9999} + _, err := p.NameWithContext(ctx) + assert.Error(t, err) +} + +func TestPsinfoSwapper(t *testing.T) { + // PID 0 (swapper) has empty Fname and Psargs; should return "swapper". + ctx := setupFakeProc(t, psinfo{Pid: 0, Lwp: lwpSinfo{Sname: 'R'}}) + p := &Process{Pid: 0} + + name, err := p.NameWithContext(ctx) + require.NoError(t, err) + assert.Equal(t, "swapper", name) + + cmd, err := p.CmdlineWithContext(ctx) + require.NoError(t, err) + assert.Equal(t, "swapper", cmd) + + args, err := p.CmdlineSliceWithContext(ctx) + require.NoError(t, err) + assert.Equal(t, []string{"swapper"}, args) +} diff --git a/process/process_fallback.go b/process/process_fallback.go index 699311a9ca..d923bd7523 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 diff --git a/process/process_posix.go b/process/process_posix.go index 9f0e93f31a..6956d5f26e 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 From 8fea2c075b971bc40fded44c79385652fd682929 Mon Sep 17 00:00:00 2001 From: Pierre Gimalac Date: Sun, 15 Mar 2026 22:35:07 +0000 Subject: [PATCH 2/4] fix: linters --- process/process_aix.go | 44 ++++++++++++++++++------------------- process/process_aix_test.go | 12 +++++----- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/process/process_aix.go b/process/process_aix.go index 965a0880f6..b99b0cdc0e 100644 --- a/process/process_aix.go +++ b/process/process_aix.go @@ -55,7 +55,7 @@ type psinfo struct { Flag2 uint32 Nlwp uint32 // number of threads _ uint32 - Uid uint64 + UID uint64 Euid uint64 Gid uint64 Egid uint64 @@ -160,11 +160,11 @@ func (p *Process) NameWithContext(ctx context.Context) (string, error) { return "", nil } -func (p *Process) TgidWithContext(_ context.Context) (int32, error) { +func (*Process) TgidWithContext(_ context.Context) (int32, error) { return 0, common.ErrNotImplementedError } -func (p *Process) ExeWithContext(_ context.Context) (string, error) { +func (*Process) ExeWithContext(_ context.Context) (string, error) { return "", common.ErrNotImplementedError } @@ -219,7 +219,7 @@ func (p *Process) createTimeWithContext(ctx context.Context) (int64, error) { return psi.Start.Sec*1000 + int64(psi.Start.Nsec)/1000000, nil } -func (p *Process) CwdWithContext(_ context.Context) (string, error) { +func (*Process) CwdWithContext(_ context.Context) (string, error) { return "", common.ErrNotImplementedError } @@ -231,7 +231,7 @@ func (p *Process) StatusWithContext(ctx context.Context) ([]string, error) { return []string{convertStatusChar(string([]byte{psi.Lwp.Sname}))}, nil } -func (p *Process) ForegroundWithContext(_ context.Context) (bool, error) { +func (*Process) ForegroundWithContext(_ context.Context) (bool, error) { return false, common.ErrNotImplementedError } @@ -241,7 +241,7 @@ func (p *Process) UidsWithContext(ctx context.Context) ([]uint32, error) { return nil, err } // real, effective, saved (psinfo doesn't expose saved, use real as fallback) - return []uint32{uint32(psi.Uid), uint32(psi.Euid), uint32(psi.Uid)}, nil + return []uint32{uint32(psi.UID), uint32(psi.Euid), uint32(psi.UID)}, nil } func (p *Process) GidsWithContext(ctx context.Context) ([]uint32, error) { @@ -253,11 +253,11 @@ func (p *Process) GidsWithContext(ctx context.Context) ([]uint32, error) { return []uint32{uint32(psi.Gid), uint32(psi.Egid), uint32(psi.Gid)}, nil } -func (p *Process) GroupsWithContext(_ context.Context) ([]uint32, error) { +func (*Process) GroupsWithContext(_ context.Context) ([]uint32, error) { return nil, common.ErrNotImplementedError } -func (p *Process) TerminalWithContext(_ context.Context) (string, error) { +func (*Process) TerminalWithContext(_ context.Context) (string, error) { return "", common.ErrNotImplementedError } @@ -270,7 +270,7 @@ func (p *Process) NiceWithContext(ctx context.Context) (int32, error) { return int32(psi.Lwp.Nice), nil } -func (p *Process) IOniceWithContext(_ context.Context) (int32, error) { +func (*Process) IOniceWithContext(_ context.Context) (int32, error) { return 0, common.ErrNotImplementedError } @@ -278,15 +278,15 @@ func (p *Process) RlimitWithContext(ctx context.Context) ([]RlimitStat, error) { return p.RlimitUsageWithContext(ctx, false) } -func (p *Process) RlimitUsageWithContext(_ context.Context, _ bool) ([]RlimitStat, error) { +func (*Process) RlimitUsageWithContext(_ context.Context, _ bool) ([]RlimitStat, error) { return nil, common.ErrNotImplementedError } -func (p *Process) NumCtxSwitchesWithContext(_ context.Context) (*NumCtxSwitchesStat, error) { +func (*Process) NumCtxSwitchesWithContext(_ context.Context) (*NumCtxSwitchesStat, error) { return nil, common.ErrNotImplementedError } -func (p *Process) NumFDsWithContext(_ context.Context) (int32, error) { +func (*Process) NumFDsWithContext(_ context.Context) (int32, error) { return 0, common.ErrNotImplementedError } @@ -298,7 +298,7 @@ func (p *Process) NumThreadsWithContext(ctx context.Context) (int32, error) { return int32(psi.Nlwp), nil } -func (p *Process) ThreadsWithContext(_ context.Context) (map[int32]*cpu.TimesStat, error) { +func (*Process) ThreadsWithContext(_ context.Context) (map[int32]*cpu.TimesStat, error) { return nil, common.ErrNotImplementedError } @@ -316,7 +316,7 @@ func (p *Process) TimesWithContext(ctx context.Context) (*cpu.TimesStat, error) }, nil } -func (p *Process) CPUAffinityWithContext(_ context.Context) ([]int32, error) { +func (*Process) CPUAffinityWithContext(_ context.Context) ([]int32, error) { return nil, common.ErrNotImplementedError } @@ -332,11 +332,11 @@ func (p *Process) MemoryInfoWithContext(ctx context.Context) (*MemoryInfoStat, e }, nil } -func (p *Process) MemoryInfoExWithContext(_ context.Context) (*MemoryInfoExStat, error) { +func (*Process) MemoryInfoExWithContext(_ context.Context) (*MemoryInfoExStat, error) { return nil, common.ErrNotImplementedError } -func (p *Process) PageFaultsWithContext(_ context.Context) (*PageFaultsStat, error) { +func (*Process) PageFaultsWithContext(_ context.Context) (*PageFaultsStat, error) { return nil, common.ErrNotImplementedError } @@ -359,26 +359,26 @@ func (p *Process) ChildrenWithContext(ctx context.Context) ([]*Process, error) { return ret, nil } -func (p *Process) OpenFilesWithContext(_ context.Context) ([]OpenFilesStat, error) { +func (*Process) OpenFilesWithContext(_ context.Context) ([]OpenFilesStat, error) { return nil, common.ErrNotImplementedError } -func (p *Process) ConnectionsWithContext(_ context.Context) ([]net.ConnectionStat, error) { +func (*Process) ConnectionsWithContext(_ context.Context) ([]net.ConnectionStat, error) { return nil, common.ErrNotImplementedError } -func (p *Process) ConnectionsMaxWithContext(_ context.Context, _ int) ([]net.ConnectionStat, error) { +func (*Process) ConnectionsMaxWithContext(_ context.Context, _ int) ([]net.ConnectionStat, error) { return nil, common.ErrNotImplementedError } -func (p *Process) MemoryMapsWithContext(_ context.Context, _ bool) (*[]MemoryMapsStat, error) { +func (*Process) MemoryMapsWithContext(_ context.Context, _ bool) (*[]MemoryMapsStat, error) { return nil, common.ErrNotImplementedError } -func (p *Process) IOCountersWithContext(_ context.Context) (*IOCountersStat, error) { +func (*Process) IOCountersWithContext(_ context.Context) (*IOCountersStat, error) { return nil, common.ErrNotImplementedError } -func (p *Process) EnvironWithContext(_ context.Context) ([]string, error) { +func (*Process) EnvironWithContext(_ context.Context) ([]string, error) { return nil, common.ErrNotImplementedError } diff --git a/process/process_aix_test.go b/process/process_aix_test.go index 61f5b904b4..cbe2252efd 100644 --- a/process/process_aix_test.go +++ b/process/process_aix_test.go @@ -32,7 +32,7 @@ func writeFakePsinfo(t *testing.T, dir string, psi psinfo) { var buf bytes.Buffer require.NoError(t, binary.Write(&buf, binary.BigEndian, &psi)) - require.NoError(t, os.WriteFile(filepath.Join(pidDir, "psinfo"), buf.Bytes(), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(pidDir, "psinfo"), buf.Bytes(), 0o600)) } // setupFakeProc creates a temp directory with fake psinfo files, @@ -40,8 +40,8 @@ func writeFakePsinfo(t *testing.T, dir string, psi psinfo) { func setupFakeProc(t *testing.T, procs ...psinfo) context.Context { t.Helper() dir := t.TempDir() - for _, psi := range procs { - writeFakePsinfo(t, dir, psi) + for i := range procs { + writeFakePsinfo(t, dir, procs[i]) } t.Setenv("HOST_PROC", dir) return context.Background() @@ -51,7 +51,7 @@ func setupFakeProc(t *testing.T, procs ...psinfo) context.Context { var mockProc = fillPsinfo(psinfo{ Pid: 1234, Ppid: 100, - Uid: 501, + UID: 501, Euid: 502, Gid: 20, Egid: 21, @@ -67,7 +67,7 @@ var mockProc = fillPsinfo(psinfo{ var childProc = fillPsinfo(psinfo{ Pid: 5678, Ppid: 1234, - Uid: 501, + UID: 501, Euid: 501, Gid: 20, Egid: 20, @@ -202,7 +202,7 @@ func TestPsinfoTimes(t *testing.T) { require.NoError(t, err) assert.Equal(t, "cpu", times.CPU) assert.InDelta(t, 5.25, times.User, 1e-6) - assert.Equal(t, float64(0), times.System) + assert.InDelta(t, float64(0), times.System, 1e-6) } func TestPsinfoMemoryInfo(t *testing.T) { From e4266110a9967c917bd4cb503fc29c77022f0f2d Mon Sep 17 00:00:00 2001 From: Pierre Gimalac Date: Fri, 3 Apr 2026 11:42:20 +0000 Subject: [PATCH 3/4] fix: review comments --- process/process_aix.go | 76 ++++++------------------------------ process/process_aix_ppc64.go | 58 +++++++++++++++++++++++++++ process/process_aix_test.go | 4 +- process/types_aix.go | 16 ++++++++ 4 files changed, 87 insertions(+), 67 deletions(-) create mode 100644 process/process_aix_ppc64.go create mode 100644 process/types_aix.go diff --git a/process/process_aix.go b/process/process_aix.go index b99b0cdc0e..3a89c98521 100644 --- a/process/process_aix.go +++ b/process/process_aix.go @@ -22,64 +22,6 @@ type MemoryMapsStat struct{} // MemoryInfoExStat is not available on AIX. type MemoryInfoExStat struct{} -// prTimestruc64 mirrors timestruc64_t from sys/time.h -type prTimestruc64 struct { - Sec int64 - Nsec int32 - _ uint32 -} - -// lwpSinfo mirrors AIX lwpsinfo_t from sys/procfs.h -type lwpSinfo struct { - LwpID uint64 - Addr uint64 - Wchan uint64 - Flag uint32 - Wtype uint8 - State int8 - Sname byte // process state character: 'R','S','Z','T','I', etc - Nice uint8 - Pri int32 - Policy uint32 - Clname [8]byte - Onpro int32 - Bindpro int32 - Ptid uint32 - _ uint32 - _ [7]uint64 -} - -// psinfo mirrors AIX psinfo_t from sys/procfs.h -type psinfo struct { - Flag uint32 - Flag2 uint32 - Nlwp uint32 // number of threads - _ uint32 - UID uint64 - Euid uint64 - Gid uint64 - Egid uint64 - Pid uint64 - Ppid uint64 - Pgid uint64 - Sid uint64 - Ttydev uint64 - Addr uint64 - Size uint64 // virtual memory size in KB (pr_size) - Rssize uint64 // resident set size in KB (pr_rssize) - Start prTimestruc64 // process start time - Time prTimestruc64 // combined user+system CPU time - Cid uint16 - _ uint16 - Argc uint32 - Argv uint64 - Envp uint64 - Fname [16]byte // executable name, null-terminated (max 15 chars) - Psargs [80]byte // process args, space-separated, null-terminated (max 79 chars) - _ [8]uint64 - Lwp lwpSinfo // representative LWP -} - func readPsinfo(ctx context.Context, pid int32) (*psinfo, error) { f, err := os.Open(common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "psinfo")) if err != nil { @@ -94,7 +36,7 @@ func readPsinfo(ctx context.Context, pid int32) (*psinfo, error) { return &psi, nil } -func nullTerminatedBytes(b []byte) string { +func nullTerminatedString(b []byte) string { if idx := bytes.IndexByte(b, 0); idx >= 0 { return string(b[:idx]) } @@ -142,6 +84,7 @@ func (p *Process) PpidWithContext(ctx context.Context) (int32, error) { if err != nil { return 0, err } + // psinfo stores PIDs as uint64; safe to truncate to int32 on AIX where PIDs fit in 32 bits. return int32(psi.Ppid), nil } @@ -150,7 +93,7 @@ func (p *Process) NameWithContext(ctx context.Context) (string, error) { if err != nil { return "", err } - if name := nullTerminatedBytes(psi.Fname[:]); name != "" { + if name := nullTerminatedString(psi.Fname[:]); name != "" { return name, nil } // PID 0 is the swapper/idle process; its Fname is empty but ps shows "swapper". @@ -175,10 +118,10 @@ func (p *Process) CmdlineWithContext(ctx context.Context) (string, error) { } // Psargs is empty for kernel threads, fall back to Fname (the executable name), // which is what ps uses as well. - if args := nullTerminatedBytes(psi.Psargs[:]); args != "" { + if args := nullTerminatedString(psi.Psargs[:]); args != "" { return args, nil } - if name := nullTerminatedBytes(psi.Fname[:]); name != "" { + if name := nullTerminatedString(psi.Fname[:]); name != "" { return name, nil } // PID 0 is the swapper/idle process, its Fname and Psargs are both empty @@ -196,9 +139,9 @@ func (p *Process) CmdlineSliceWithContext(ctx context.Context) ([]string, error) } // Psargs is empty for kernel threads, fall back to Fname (the executable name), // which is what ps uses as well. - args := nullTerminatedBytes(psi.Psargs[:]) + args := nullTerminatedString(psi.Psargs[:]) if args == "" { - args = nullTerminatedBytes(psi.Fname[:]) + args = nullTerminatedString(psi.Fname[:]) } // PID 0 is the swapper/idle process, its Fname and Psargs are both empty // but ps shows "swapper". @@ -240,8 +183,9 @@ func (p *Process) UidsWithContext(ctx context.Context) ([]uint32, error) { if err != nil { return nil, err } + // psinfo stores UIDs as uint64; safe to truncate to uint32 on AIX where UIDs fit in 32 bits. // real, effective, saved (psinfo doesn't expose saved, use real as fallback) - return []uint32{uint32(psi.UID), uint32(psi.Euid), uint32(psi.UID)}, nil + return []uint32{uint32(psi.Uid), uint32(psi.Euid), uint32(psi.Uid)}, nil } func (p *Process) GidsWithContext(ctx context.Context) ([]uint32, error) { @@ -249,6 +193,7 @@ func (p *Process) GidsWithContext(ctx context.Context) ([]uint32, error) { if err != nil { return nil, err } + // psinfo stores GIDs as uint64; safe to truncate to uint32 on AIX where GIDs fit in 32 bits. // real, effective, saved (psinfo doesn't expose saved, use real as fallback) return []uint32{uint32(psi.Gid), uint32(psi.Egid), uint32(psi.Gid)}, nil } @@ -351,6 +296,7 @@ func (p *Process) ChildrenWithContext(ctx context.Context) ([]*Process, error) { if err != nil { continue } + // psinfo stores PIDs as uint64; safe to truncate to int32 on AIX where PIDs fit in 32 bits. if int32(psi.Ppid) == p.Pid { // create Process struct directly to avoid the redundant PidExists check ret = append(ret, &Process{Pid: pid}) diff --git a/process/process_aix_ppc64.go b/process/process_aix_ppc64.go new file mode 100644 index 0000000000..1fc0d9978e --- /dev/null +++ b/process/process_aix_ppc64.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Code generated by cmd/cgo -godefs; DO NOT EDIT. +// cgo -godefs types_aix.go | sed 's/\*byte/uint64/' + +package process + +type prTimestruc64 struct { + Sec int64 + Nsec int32 + X__pad uint32 +} +type lwpSinfo struct { + Lwpid uint64 + Addr uint64 + Wchan uint64 + Flag uint32 + Wtype uint8 + State uint8 + Sname uint8 + Nice uint8 + Pri int32 + Policy uint32 + Clname [8]uint8 + Onpro int32 + Bindpro int32 + Ptid uint32 + X_pad1 uint32 + X_pad [7]uint64 +} +type psinfo struct { + Flag uint32 + Flag2 uint32 + Nlwp uint32 + X_pad1 uint32 + Uid uint64 + Euid uint64 + Gid uint64 + Egid uint64 + Pid uint64 + Ppid uint64 + Pgid uint64 + Sid uint64 + Ttydev uint64 + Addr uint64 + Size uint64 + Rssize uint64 + Start prTimestruc64 + Time prTimestruc64 + Cid uint16 + X_pad2 uint16 + Argc uint32 + Argv uint64 + Envp uint64 + Fname [16]uint8 + Psargs [80]uint8 + X_pad [8]uint64 + Lwp lwpSinfo +} diff --git a/process/process_aix_test.go b/process/process_aix_test.go index cbe2252efd..aec9a630c3 100644 --- a/process/process_aix_test.go +++ b/process/process_aix_test.go @@ -51,7 +51,7 @@ func setupFakeProc(t *testing.T, procs ...psinfo) context.Context { var mockProc = fillPsinfo(psinfo{ Pid: 1234, Ppid: 100, - UID: 501, + Uid: 501, Euid: 502, Gid: 20, Egid: 21, @@ -67,7 +67,7 @@ var mockProc = fillPsinfo(psinfo{ var childProc = fillPsinfo(psinfo{ Pid: 5678, Ppid: 1234, - UID: 501, + Uid: 501, Euid: 501, Gid: 20, Egid: 20, diff --git a/process/types_aix.go b/process/types_aix.go new file mode 100644 index 0000000000..72e1838358 --- /dev/null +++ b/process/types_aix.go @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build ignore + +// Input to cgo -godefs. See mktypes.sh in the root directory. +// go tool cgo -godefs types_aix.go | sed 's/\*byte/uint64/' > process_aix_ppc64.go + +package process + +/* +#include +*/ +import "C" + +type prTimestruc64 C.struct_pr_timestruc64 +type lwpSinfo C.struct_lwpsinfo +type psinfo C.struct_psinfo From c28bd4ec25ea0543382bbe39e1260be68e6857b5 Mon Sep 17 00:00:00 2001 From: Pierre Gimalac Date: Fri, 3 Apr 2026 11:43:41 +0000 Subject: [PATCH 4/4] fix: skip failing posix tests on AIX --- process/process_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/process/process_test.go b/process/process_test.go index b3363d3307..5c309baca7 100644 --- a/process/process_test.go +++ b/process/process_test.go @@ -126,6 +126,9 @@ func TestCmdLine(t *testing.T) { } func TestCmdLineSlice(t *testing.T) { + if runtime.GOOS == "aix" { + t.Skip("AIX: psinfo.Psargs is limited to 79 characters, so long command lines are truncated and cannot match os.Args exactly") + } p := testGetProcess() v, err := p.CmdlineSlice() @@ -196,6 +199,9 @@ func TestNumCtx(t *testing.T) { } func TestNice(t *testing.T) { + if runtime.GOOS == "aix" { + t.Skip("AIX: psinfo returns a raw kernel nice value (pr_nice) that does not map to the POSIX [-20, 19] range") + } p := testGetProcess() // https://github.com/shirou/gopsutil/issues/1532 @@ -268,6 +274,9 @@ func TestName(t *testing.T) { // #nosec G204 func TestLong_Name_With_Spaces(t *testing.T) { + if runtime.GOOS == "aix" { + t.Skip("AIX: psinfo.Fname is limited to 15 characters, so process names longer than 15 chars are truncated") + } tmpdir, err := os.MkdirTemp("", "") require.NoErrorf(t, err, "unable to create temp dir %v", err) defer os.RemoveAll(tmpdir) // clean up @@ -307,6 +316,9 @@ func TestLong_Name_With_Spaces(t *testing.T) { // #nosec G204 func TestLong_Name(t *testing.T) { + if runtime.GOOS == "aix" { + t.Skip("AIX: psinfo.Fname is limited to 15 characters, so process names longer than 15 chars are truncated") + } tmpdir, err := os.MkdirTemp("", "") require.NoErrorf(t, err, "unable to create temp dir %v", err) defer os.RemoveAll(tmpdir) // clean up @@ -579,6 +591,9 @@ func TestUsername(t *testing.T) { } func TestCPUTimes(t *testing.T) { + if runtime.GOOS == "aix" { + t.Skip("AIX: psinfo.pr_time is only updated on context switch, not continuously; short-duration CPU measurements are unreliable") + } pid := os.Getpid() process, err := NewProcess(int32(pid)) if errors.Is(err, common.ErrNotImplementedError) {