diff --git a/host/host_aix_nocgo.go b/host/host_aix_nocgo.go index c1fd4b3036..c5ff06be81 100644 --- a/host/host_aix_nocgo.go +++ b/host/host_aix_nocgo.go @@ -6,9 +6,13 @@ package host import ( "context" - "github.com/shirou/gopsutil/v4/internal/common" + "github.com/shirou/gopsutil/v4/process" ) -func numProcs(_ context.Context) (uint64, error) { - return 0, common.ErrNotImplementedError +func numProcs(ctx context.Context) (uint64, error) { + procs, err := process.PidsWithContext(ctx) + if err != nil { + return 0, err + } + return uint64(len(procs)), nil } 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..50c66c2284 --- /dev/null +++ b/process/process_aix.go @@ -0,0 +1,1838 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build aix + +package process + +import ( + "bytes" + "context" + "encoding/binary" + "encoding/json" + "errors" + "math" + "os" + "os/exec" + "os/user" + "path/filepath" + "sort" + "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 uint16 // corral id + Pad2 uint16 // 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) { + // AIX has no tpgid (terminal process group ID) in any procfs structure. + // The psinfo_t and pstatus_t structs contain pr_pgid, pr_sid, and + // pr_ttydev but no pr_tpgid field (/usr/include/sys/procfs.h verified + // on AIX 7.3). The ps command does not support -o tpgid, -o pgrp, or + // -o sid specifiers (only -o pgid works). + // + // The TIOCGPGRP ioctl (0x40047309, defined in golang.org/x/sys/unix + // zerrors_aix_ppc64.go) could theoretically be used: read pr_ttydev + // from psinfo -> resolve device path -> open tty -> ioctl to get + // foreground pgrp -> compare with pr_pgid. However this approach has + // significant issues: + // - Opening another process's tty requires elevated privileges + // - AIX has no devname() to convert dev_t to a device path + // - Go's IoctlGetInt is broken on ppc64 big-endian (Go issue #60429); + // must use IoctlGetUint32 instead + // - Historical TIOCGPGRP bugs on AIX 6.1/7.1 (APARs IV64838/IV67163, + // fixed Nov 2014) + // - TOCTOU race between reading psinfo and querying the terminal + // + // No other library implements tpgid on AIX (Python psutil does not). + // Windows, Solaris, and Plan9 also return ErrNotImplementedError in + // gopsutil. + 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) { + // AIX has no per-process I/O priority mechanism. There is no ionice + // command, no ioprio_get/ioprio_set syscalls (not in + // /usr/include/sys/syscall.h), and no ioprio kernel headers. The + // renice command only affects CPU scheduling priority (nice value). + // + // AIX's Workload Manager (WLM) provides class-level DKIO (disk I/O) + // resource management via wlmstat/wlmassign, but this operates on + // process classes, not individual processes, and IBM documentation + // notes DKIO metrics are "usually not significant." The ioo and + // schedo kernel tuneables contain no per-process I/O priority + // parameters. JFS2 and CIO/DIO are filesystem/mount-level features + // with no per-process scheduling. The getprocs64() procentry64 struct + // contains pi_pri and pi_nice but no I/O priority field. + // + // Every other gopsutil platform (including Linux) also returns + // ErrNotImplementedError for IOniceWithContext. Python psutil also + // does not implement ionice on AIX. + 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, _ bool) ([]RlimitStat, error) { + return p.fillFromLimitsWithContext(ctx) +} + +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 per-process context switch counts. The ps command + // field specifiers do not include nvcsw or vcsw. The procfs binary + // structures (pstatus_t, psinfo_t) contain no context switch fields. + // The vmstat command provides system-wide context switch rates but not + // per-process. The only path to per-process data is the cgo perfstat + // library (perfstat_process_t.num_ctx_switches), which is unavailable + // in nocgo builds. + 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 uses bindprocessor(1) for CPU binding, not Linux-style affinity + // bitmasks. "bindprocessor -q " can report which CPU a process is + // bound to, but the semantics differ from Linux sched_getaffinity: AIX + // binding is a single-CPU assignment, not a bitmask of allowed CPUs. + // Mapping this to []int32 would be misleading since an unbound AIX + // process is eligible for all CPUs but bindprocessor reports nothing. + // The cgo perfstat path could provide this via perfstat_process_t, but + // that is unavailable in nocgo builds. + 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 (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 { + ppid, err := readPpidFromStatus(ctx, pid) + if err != nil { + continue + } + if ppid == p.Pid { + child, err := NewProcessWithContext(ctx, pid) + if err != nil { + continue + } + ret = append(ret, child) + } + } + sort.Slice(ret, func(i, j int) bool { return ret[i].Pid < ret[j].Pid }) + return ret, nil +} + +// readPpidFromStatus reads only the PPID from /proc//status without +// parsing the entire struct, avoiding the overhead of fillFromStatWithContext. +// Falls back to /proc//psinfo for zombie/kernel processes where status +// doesn't exist. +func readPpidFromStatus(ctx context.Context, pid int32) (int32, error) { + statPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "status") + f, err := os.Open(statPath) + if err != nil { + if !os.IsNotExist(err) { + return 0, err + } + // status file doesn't exist (zombie/kernel thread) — fall back to psinfo + return readPpidFromPSInfo(ctx, pid) + } + defer f.Close() + + var stat AIXStat + if err := binary.Read(f, binary.BigEndian, &stat); err != nil { + return 0, err + } + return int32(stat.Ppid), nil +} + +// readPpidFromPSInfo reads PPID from /proc//psinfo as a fallback +// for processes where /proc//status doesn't exist. +func readPpidFromPSInfo(ctx context.Context, pid int32) (int32, error) { + infoPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "psinfo") + f, err := os.Open(infoPath) + if err != nil { + return 0, err + } + defer f.Close() + + var psinfo AIXPSInfo + if err := binary.Read(f, binary.BigEndian, &psinfo); err != nil { + return 0, err + } + return int32(psinfo.Ppid), nil +} + +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 (p *Process) EnvironWithContext(ctx context.Context) ([]string, error) { + // Get the command line (without environment) to use as a separator. + //nolint:gosec // Process ID from internal tracking, not untrusted input + argsCmd := exec.CommandContext(ctx, "ps", "-o", "args=", "-p", strconv.Itoa(int(p.Pid))) + argsOut, err := argsCmd.Output() + if err != nil { + return nil, err + } + cmdline := strings.TrimSpace(string(argsOut)) + + // Get command + environment using BSD-style ps with 'e' flag. + //nolint:gosec // Process ID from internal tracking, not untrusted input + ewwCmd := exec.CommandContext(ctx, "ps", "eww", strconv.Itoa(int(p.Pid))) + ewwOut, err := ewwCmd.Output() + if err != nil { + return nil, err + } + + lines := strings.SplitN(string(ewwOut), "\n", 3) + if len(lines) < 2 { + return nil, errors.New("unexpected ps eww output") + } + // The data line is everything after the header. + dataLine := strings.TrimSpace(lines[1]) + + // Find where the known command line appears and take everything after it. + idx := strings.Index(dataLine, cmdline) + if idx < 0 { + return []string{}, nil + } + envStr := strings.TrimSpace(dataLine[idx+len(cmdline):]) + if envStr == "" { + return []string{}, nil + } + + // Parse space-separated KEY=VALUE tokens. Environment variable names + // match [A-Za-z_][A-Za-z0-9_]*. Tokens that don't start a new variable + // are appended to the previous value (handles values containing spaces). + tokens := strings.Fields(envStr) + var result []string + for _, tok := range tokens { + eqIdx := strings.IndexByte(tok, '=') + if eqIdx > 0 && isEnvVarName(tok[:eqIdx]) { + result = append(result, tok) + } else if len(result) > 0 { + result[len(result)-1] += " " + tok + } + } + return result, nil +} + +// isEnvVarName returns true if s is a valid POSIX environment variable name. +func isEnvVarName(s string) bool { + if s == "" { + return false + } + for i, c := range s { + if c == '_' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') { + continue + } + if i > 0 && c >= '0' && c <= '9' { + continue + } + return false + } + return true +} + +/** +** 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 +} + +// fillFromLimitsWithContext returns resource limits for the process owner +// by querying AIX's /etc/security/limits via the lsuser command. +// +// AIX does not expose per-process limits through /proc. Instead, limits are +// configured per-user in /etc/security/limits. The lsuser command resolves +// the inheritance chain (user stanza -> default stanza) and returns the +// effective values. Size-based limits (fsize, core, data, stack, rss) are +// stored in 512-byte blocks and converted to bytes here. A value of -1 +// means unlimited. +// +// Hard limits that are not explicitly set use these defaults: +// - fsize_hard = fsize, cpu_hard = cpu +// - core_hard = -1, data_hard = -1, rss_hard = -1, nofiles_hard = -1 +// - stack_hard = 8388608 (blocks = 4 GB) +func (p *Process) fillFromLimitsWithContext(ctx context.Context) ([]RlimitStat, error) { + uids, err := p.UidsWithContext(ctx) + if err != nil { + return nil, err + } + if len(uids) == 0 { + return nil, errors.New("no UID available for process") + } + u, err := user.LookupId(strconv.Itoa(int(uids[0]))) + if err != nil { + return nil, err + } + + attrs := "fsize fsize_hard core core_hard cpu cpu_hard data data_hard stack stack_hard rss rss_hard nofiles nofiles_hard" + cmd := exec.CommandContext(ctx, "lsuser", "-a") + cmd.Args = append(cmd.Args, strings.Fields(attrs)...) + cmd.Args = append(cmd.Args, u.Username) + output, err := cmd.Output() + if err != nil { + return nil, err + } + + // Parse "username key=val key=val ..." output. + line := strings.TrimSpace(string(output)) + vals := make(map[string]int64) + for _, field := range strings.Fields(line) { + kv := strings.SplitN(field, "=", 2) + if len(kv) != 2 { + continue + } + v, err := strconv.ParseInt(kv[1], 10, 64) + if err != nil { + continue + } + vals[kv[0]] = v + } + + const blockSize = 512 + + // toBytes converts an AIX limit value: -1 means unlimited, size-based + // limits are in 512-byte blocks that need conversion to bytes. + toBytes := func(v int64, isBytes bool) uint64 { + if v == -1 { + return math.MaxUint64 + } + if isBytes { + return uint64(v) * blockSize + } + return uint64(v) + } + + // hardDefault returns the hard limit, applying AIX defaults when the + // _hard attribute was not explicitly set. + hardDefault := func(soft int64, hardKey string, defaultHard int64) int64 { + if v, ok := vals[hardKey]; ok { + return v + } + if defaultHard == 0 { + // Default is "same as soft" + return soft + } + return defaultHard + } + + type limitDef struct { + resource int32 + softKey string + hardKey string + defaultHard int64 // 0 means "same as soft", -1 means unlimited + isBytes bool // true = 512-byte blocks, false = direct value + } + + defs := []limitDef{ + {RLIMIT_FSIZE, "fsize", "fsize_hard", 0, true}, + {RLIMIT_CORE, "core", "core_hard", -1, true}, + {RLIMIT_CPU, "cpu", "cpu_hard", 0, false}, + {RLIMIT_DATA, "data", "data_hard", -1, true}, + {RLIMIT_STACK, "stack", "stack_hard", 8388608, true}, + {RLIMIT_RSS, "rss", "rss_hard", -1, true}, + {RLIMIT_NOFILE, "nofiles", "nofiles_hard", -1, false}, + } + + var stats []RlimitStat + for _, d := range defs { + soft, ok := vals[d.softKey] + if !ok { + continue + } + hard := hardDefault(soft, d.hardKey, d.defaultHard) + softVal := toBytes(soft, d.isBytes) + hardVal := toBytes(hard, d.isBytes) + // AIX /etc/security/limits can have soft=-1 (unlimited) with a + // finite hard limit. The kernel clamps soft to hard at process + // creation, so reflect that here. + if softVal > hardVal { + softVal = hardVal + } + stats = append(stats, RlimitStat{ + Resource: d.resource, + Soft: softVal, + Hard: hardVal, + }) + } + + return stats, nil +} + +// 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 { + if os.IsNotExist(err) { + // fd directory doesn't exist for zombie/kernel processes + return statPath, []string{}, 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 { + if os.IsNotExist(err) { + // cwd symlink doesn't exist for zombie/kernel processes + return "", 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) +} + +func (*Process) fillFromIOWithContext(_ context.Context) (*IOCountersStat, error) { + // AIX /proc does not expose per-process I/O byte counters. The procfs + // binary structures (pstatus_t, psinfo_t) contain no read/write byte + // fields. The ps format specifier "tdiskio" provides only a single + // aggregate I/O count (see IOCountersWithContext above), not separate + // read/write byte totals. The cgo perfstat library + // (perfstat_process_t) could provide this data but is unavailable in + // nocgo builds. + 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 + + p.numCtxSwitches = &NumCtxSwitchesStat{} + p.memInfo = &MemoryInfoStat{} + p.sigInfo = &SignalInfoStat{} + + // Try reading /proc//status for full process state + statusPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "status") + statusFile, err := os.Open(statusPath) + if err != nil && !os.IsNotExist(err) { + return err + } + if err == nil { + defer statusFile.Close() + + var aixStat AIXStat + err = binary.Read(statusFile, binary.BigEndian, &aixStat) + if err != nil { + return err + } + + // Extract process state + p.status = convertStatusChar(string([]byte{aixStat.Stat})) + // Recognize AIX-specific status codes if the converted value is empty + if p.status == "" { + 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)) + } + } else { + // status file doesn't exist (zombie/kernel thread) — mark as zombie + p.status = Zombie + } + + // Read psinfo for UID/GID, thread count, and PPID fallback + 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 { + p.uids = []uint32{uint32(aixPSinfo.UID), uint32(aixPSinfo.Euid), uint32(aixPSinfo.Euid), uint32(aixPSinfo.Euid)} + p.gids = []uint32{uint32(aixPSinfo.Gid), uint32(aixPSinfo.Egid), uint32(aixPSinfo.Egid), uint32(aixPSinfo.Egid)} + p.numThreads = int32(aixPSinfo.Nlwp) + // If status was missing, fill parent PID from psinfo + if p.parent == 0 { + p.parent = int32(aixPSinfo.Ppid) + } + if p.tgid == 0 { + p.tgid = int32(aixPSinfo.Pid) + } + } + } + + 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 { + tidStr := strconv.Itoa(int(tid)) + lwpStatPath = common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "lwp", tidStr, "lwpstatus") + lwpInfoPath = common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "lwp", tidStr, "lwpinfo") + } + + // psinfo is the minimum required file — always present for visible processes + infoFile, err := os.Open(infoPath) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + defer infoFile.Close() + + var aixPSinfo AIXPSInfo + err = binary.Read(infoFile, binary.BigEndian, &aixPSinfo) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + + // Try to open /proc//status — may not exist for zombie/kernel processes + var aixStat AIXStat + hasStatus := false + statFile, err := os.Open(statPath) + if err != nil && !os.IsNotExist(err) { + return 0, 0, nil, 0, 0, 0, nil, err + } + if err == nil { + defer statFile.Close() + err = binary.Read(statFile, binary.BigEndian, &aixStat) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + hasStatus = true + } + + // Try lwp files if requested and status exists + if tid > -1 && hasStatus { + var openErr error + lwpStatFile, openErr = os.Open(lwpStatPath) + if openErr != nil { + tid = -1 + } else { + defer lwpStatFile.Close() + lwpInfoFile, openErr = os.Open(lwpInfoPath) + if openErr != nil { + lwpStatFile.Close() + tid = -1 + } else { + defer lwpInfoFile.Close() + } + } + } else if tid > -1 { + // No status file means no lwp directory either + tid = -1 + } + + var aixlwpStat LwpStatus + var aixlspPSinfo LwpsInfo + 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 + } + } + + // Use status for CPU times if available, otherwise zero + var ppid uint64 + cpuTimes := &cpu.TimesStat{CPU: "cpu"} + if hasStatus { + ppid = uint64(aixStat.Ppid) + utime := float64(aixStat.Utime.TvSec) + stime := float64(aixStat.Stime.TvSec) + cpuTimes.User = utime / float64(clockTicks) + cpuTimes.System = stime / float64(clockTicks) + } else { + // Fall back to psinfo for PPID + ppid = aixPSinfo.Ppid + } + + 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 + } + // AIX /proc can list the same PID multiple times, so deduplicate. + seen := make(map[int32]struct{}, len(fnames)) + for _, fname := range fnames { + pid, err := strconv.ParseInt(fname, 10, 32) + if err != nil { + // if not numeric name, just skip + continue + } + if _, ok := seen[int32(pid)]; ok { + continue + } + seen[int32(pid)] = struct{}{} + 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") +} diff --git a/process/process_aix_test.go b/process/process_aix_test.go new file mode 100644 index 0000000000..65a9e6d992 --- /dev/null +++ b/process/process_aix_test.go @@ -0,0 +1,458 @@ +// 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_AIX(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)) + _, _, cpuTimes, _, _, _, _, err := p.fillFromTIDStat(-1) + if err != nil { + t.Error(err) + } + // Verify CPU times are populated + require.NotNil(t, cpuTimes) + assert.GreaterOrEqual(t, cpuTimes.User, float64(0)) + assert.GreaterOrEqual(t, cpuTimes.System, float64(0)) + } +} + +func TestProcessMemoryMaps(t *testing.T) { + // Read the procmap output test data file + procmapData, err := os.ReadFile("testdata/aix/procmap_output") + require.NoError(t, err) + + // Create a process and test the parseMemoryMaps function directly + p := &Process{Pid: 1} + maps := p.parseMemoryMaps(string(procmapData)) + + // Verify we got some memory maps back + require.NotNil(t, maps) + require.NotEmpty(t, *maps, "expected to get at least one memory map") + + // Expected AIX memory maps from procmap output for PID 1 + // These are the actual ranges from a typical AIX init process + // Based on actual procmap output: + // - KERTXT: no mapped object → "[kertxt]" + // - MAINTEXT: "init" as mapped object + // - MAINDATA: "init" as mapped object + // - HEAP: no mapped object → "[heap]" + // - STACK: no mapped object → "[stack]" + // - Libraries: actual library paths + expectedMaps := []struct { + pathContains string + minSize uint64 + }{ + {"[kertxt]", 262144 * 1024}, // KERTXT segment - no mapped object + {"[heap]", 768 * 1024}, // HEAP segment - no mapped object + {"[stack]", 260272 * 1024}, // STACK segment - no mapped object + {"libc.a[shr.o]", 586 * 1024}, // libc shared library + } + + // Verify all expected memory map types are present + for _, expected := range expectedMaps { + found := false + for _, m := range *maps { + if strings.Contains(m.Path, expected.pathContains) { + require.GreaterOrEqual(t, m.Size, expected.minSize, + "memory map %s has unexpected size", m.Path) + found = true + break + } + } + require.True(t, found, "expected to find memory map containing %s", expected.pathContains) + } + + // Verify that the memory maps have valid field values (non-nil) + for _, m := range *maps { + // Fields should be populated (at least Path must be non-empty) + require.NotEmpty(t, m.Path, "memory map path should not be empty") + } +} + +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 35c34adb3d..8230c5d7bb 100644 --- a/process/process_darwin.go +++ b/process/process_darwin.go @@ -521,3 +521,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 764523dcf7..bc8c2b6cab 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 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 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 b3363d3307..37b45ac199 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 a1e28be698..99fc8482b6 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 diff --git a/process/testdata/aix/procmap_output b/process/testdata/aix/procmap_output new file mode 100644 index 0000000000..a88f9a5e3c --- /dev/null +++ b/process/testdata/aix/procmap_output @@ -0,0 +1,23 @@ +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 +20036288 200f6300 768K rw- sm HEAP 890192 +200f7000 2ff23000 260272K rw- sm STACK 890192 +d0100000 d0192a75 586K r-x m SLIBTEXT a000 /usr/lib/libc.a[shr.o] +d0193000 d019b33c 32K r-x m SLIBTEXT a000 /usr/lib/libpthreads.a[shr_xpg5.o] +d019c480 d06152ce 4579K r-x m SLIBTEXT a000 /usr/lib/libc.a[_shr.o] +d0616100 d06169c1 2K r-x m SLIBTEXT a000 /usr/lib/libcrypt.a[shr.o] +d0617000 d06523c4 236K r-x m SLIBTEXT a000 /usr/lib/libpthreads.a[_shr_xpg5.o] +d0653000 d06561f4 12K r-x m SLIBTEXT a000 /usr/lib/libpthreads.a[shr_comm.o] +d0e88100 d0e8c3db 16K r-x m SLIBTEXT a000 /usr/lib/libefs.a[shr.o] +f0100250 f0182600 520K rw- sm PLIBDATA 808181 /usr/lib/libc.a[shr.o] +f01832a0 f01fb388 480K rw- sm PLIBDATA 808181 /usr/lib/libc.a[_shr.o] +f01fc5c8 f01fc704 0K rw- sm PLIBDATA 808181 /usr/lib/libcrypt.a[shr.o] +f01fd000 f02407b8 269K rw- sm PLIBDATA 808181 /usr/lib/libpthreads.a[shr_comm.o] +f0241270 f02424b8 4K rw- sm PLIBDATA 808181 /usr/lib/libpthreads.a[shr_xpg5.o] +f0243000 f0246a10 14K rw- sm PLIBDATA 808181 /usr/lib/libpthreads.a[_shr_xpg5.o] +f043fe10 f04406f4 2K rw- sm PLIBDATA 808181 /usr/lib/libefs.a[shr.o] + Total 530211K