diff --git a/process/process_aix.go b/process/process_aix.go new file mode 100644 index 0000000000..3a89c98521 --- /dev/null +++ b/process/process_aix.go @@ -0,0 +1,330 @@ +// 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{} + +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 nullTerminatedString(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 + } + // psinfo stores PIDs as uint64; safe to truncate to int32 on AIX where PIDs fit in 32 bits. + 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 := nullTerminatedString(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 (*Process) TgidWithContext(_ context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (*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 := nullTerminatedString(psi.Psargs[:]); args != "" { + return args, nil + } + if name := nullTerminatedString(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 := nullTerminatedString(psi.Psargs[:]) + if args == "" { + args = nullTerminatedString(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 (*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 (*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 + } + // 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 +} + +func (p *Process) GidsWithContext(ctx context.Context) ([]uint32, error) { + psi, err := readPsinfo(ctx, p.Pid) + 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 +} + +func (*Process) GroupsWithContext(_ context.Context) ([]uint32, error) { + return nil, common.ErrNotImplementedError +} + +func (*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 (*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 (*Process) RlimitUsageWithContext(_ context.Context, _ bool) ([]RlimitStat, error) { + return nil, common.ErrNotImplementedError +} + +func (*Process) NumCtxSwitchesWithContext(_ context.Context) (*NumCtxSwitchesStat, error) { + return nil, common.ErrNotImplementedError +} + +func (*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 (*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 (*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 (*Process) MemoryInfoExWithContext(_ context.Context) (*MemoryInfoExStat, error) { + return nil, common.ErrNotImplementedError +} + +func (*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 + } + // 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}) + } + } + return ret, nil +} + +func (*Process) OpenFilesWithContext(_ context.Context) ([]OpenFilesStat, error) { + return nil, common.ErrNotImplementedError +} + +func (*Process) ConnectionsWithContext(_ context.Context) ([]net.ConnectionStat, error) { + return nil, common.ErrNotImplementedError +} + +func (*Process) ConnectionsMaxWithContext(_ context.Context, _ int) ([]net.ConnectionStat, error) { + return nil, common.ErrNotImplementedError +} + +func (*Process) MemoryMapsWithContext(_ context.Context, _ bool) (*[]MemoryMapsStat, error) { + return nil, common.ErrNotImplementedError +} + +func (*Process) IOCountersWithContext(_ context.Context) (*IOCountersStat, error) { + return nil, common.ErrNotImplementedError +} + +func (*Process) EnvironWithContext(_ context.Context) ([]string, error) { + return nil, common.ErrNotImplementedError +} 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 new file mode 100644 index 0000000000..aec9a630c3 --- /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 +//