Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions host/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,53 +64,58 @@ func Info() (*InfoStat, error) {

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

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

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

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

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

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

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

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

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

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

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

return ret, nil
Expand Down
207 changes: 141 additions & 66 deletions host/host_aix.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
package host

import (
"bytes"
"context"
"encoding/binary"
"os"
"strings"
"sync"

"github.com/shirou/gopsutil/v4/internal/common"
)
Expand All @@ -26,14 +30,39 @@ func getInvoker() common.Invoker {
return invoke
}

func HostIDWithContext(ctx context.Context) (string, error) {
out, err := getInvoker().CommandWithContext(ctx, "uname", "-u")
if err != nil {
return "", err
}
// Static host information is cached because these values (hardware ID,
// platform name, OS version, kernel version, architecture) do not change
// at runtime. Caching avoids spawning subprocesses on repeated queries.
var (
hostIDOnce sync.Once
hostIDVal string
hostIDErr error

platformOnce sync.Once
platformVal string
familyVal string
versionVal string
platformErr error

kernelVerOnce sync.Once
kernelVerVal string
kernelVerErr error

kernelArchOnce sync.Once
kernelArchVal string
kernelArchErr error
)

// The command always returns an extra newline, so we make use of Split() to get only the first line
return strings.Split(string(out), "\n")[0], nil
func HostIDWithContext(ctx context.Context) (string, error) {
hostIDOnce.Do(func() {
out, err := getInvoker().CommandWithContext(ctx, "uname", "-u")
if err != nil {
hostIDErr = err
return
}
hostIDVal = strings.Split(string(out), "\n")[0]
})
return hostIDVal, hostIDErr
}

func BootTimeWithContext(ctx context.Context) (btime uint64, err error) {
Expand All @@ -45,89 +74,135 @@ func UptimeWithContext(ctx context.Context) (uint64, error) {
return common.UptimeWithContext(ctx, getInvoker())
}

// This is a weak implementation due to the limitations on retrieving this data in AIX
func UsersWithContext(ctx context.Context) ([]UserStat, error) {
var ret []UserStat
out, err := getInvoker().CommandWithContext(ctx, "w")
// aixUtmp matches the AIX /etc/utmp binary record layout (see /usr/include/utmp.h).
// Reading utmp directly is ~180x faster than spawning `who` and avoids locale
// dependencies when parsing timestamps — ut_time is an epoch value.
type aixUtmp struct {
User [256]byte // ut_user: login name
ID [14]byte // ut_id: inittab id
Line [64]byte // ut_line: device name (pts/0, etc.)
Pid int32 // ut_pid
Type int16 // ut_type (7 = USER_PROCESS)
Time int64 // ut_time: epoch seconds (time64_t)
Exit [4]byte // ut_exit: termination/exit status
Host [256]byte // ut_host: remote host
Pad [4]byte // __dbl_word_pad
Reserved [32]byte // __reservedA[2] + __reservedV[6]
}

// UsersWithContext returns currently logged-in users by reading /etc/utmp directly.
// This avoids spawning a subprocess and eliminates locale dependencies for
// timestamp parsing — the utmp struct contains epoch seconds in ut_time.
func UsersWithContext(_ context.Context) ([]UserStat, error) {
f, err := os.Open("/etc/utmp")
if err != nil {
return nil, err
}
lines := strings.Split(string(out), "\n")
if len(lines) < 3 {
return []UserStat{}, common.ErrNotImplementedError
}
defer f.Close()

hf := strings.Fields(lines[1]) // headers
for l := 2; l < len(lines); l++ {
v := strings.Fields(lines[l]) // values
if len(v) == 0 || v[0] == "-" {
var ret []UserStat
for {
var entry aixUtmp
err := binary.Read(f, binary.BigEndian, &entry)
if err != nil {
break // EOF or read error
}

// Only include active user sessions (ut_type == USER_PROCESS)
if entry.Type != user_PROCESS {
continue
}
us := &UserStat{}
for i, header := range hf {
if i >= len(v) {
break
}
switch header {
case "User":
us.User = v[i]
case "tty":
us.Terminal = v[i]
}

user := strings.TrimRight(string(bytes.TrimRight(entry.User[:], "\x00")), " ")
if user == "" {
continue
}

// Valid User data, so append it
ret = append(ret, *us)
us := UserStat{
User: user,
Terminal: string(bytes.TrimRight(entry.Line[:], "\x00")),
Host: string(bytes.TrimRight(entry.Host[:], "\x00")),
Started: int(entry.Time),
}
ret = append(ret, us)
}

return ret, nil
}

// Much of this function could be static. However, to be future proofed, I've made it call the OS for the information in all instances.
// PlatformInformationWithContext returns the platform name, family, and OS
// version. These are immutable system identifiers cached after first query.
func PlatformInformationWithContext(ctx context.Context) (platform, family, version string, err error) {
// Set the platform (which should always, and only be, "AIX") from `uname -s`
out, err := getInvoker().CommandWithContext(ctx, "uname", "-s")
if err != nil {
return "", "", "", err
}
platform = strings.TrimRight(string(out), "\n")

// Set the family
family = strings.TrimRight(string(out), "\n")

// Set the version
out, err = getInvoker().CommandWithContext(ctx, "oslevel")
if err != nil {
return "", "", "", err
}
version = strings.TrimRight(string(out), "\n")
platformOnce.Do(func() {
out, err := getInvoker().CommandWithContext(ctx, "uname", "-s")
if err != nil {
platformErr = err
return
}
platformVal = strings.TrimRight(string(out), "\n")
familyVal = platformVal

return platform, family, version, nil
out, err = getInvoker().CommandWithContext(ctx, "oslevel")
if err != nil {
platformErr = err
return
}
versionVal = strings.TrimRight(string(out), "\n")
})
return platformVal, familyVal, versionVal, platformErr
}

// KernelVersionWithContext returns the kernel version (e.g., "7300-03-00-2446").
// This is an immutable system identifier cached after first query.
func KernelVersionWithContext(ctx context.Context) (version string, err error) {
out, err := getInvoker().CommandWithContext(ctx, "oslevel", "-s")
if err != nil {
return "", err
}
version = strings.TrimRight(string(out), "\n")

return version, nil
kernelVerOnce.Do(func() {
out, err := getInvoker().CommandWithContext(ctx, "oslevel", "-s")
if err != nil {
kernelVerErr = err
return
}
kernelVerVal = strings.TrimRight(string(out), "\n")
})
return kernelVerVal, kernelVerErr
}

// KernelArch returns the hardware architecture (e.g., "64").
// This is an immutable system identifier cached after first query.
func KernelArch() (arch string, err error) {
out, err := getInvoker().Command("getconf", "KERNEL_BITMODE")
if err != nil {
out, err = getInvoker().Command("bootinfo", "-y")
kernelArchOnce.Do(func() {
out, err := getInvoker().Command("getconf", "KERNEL_BITMODE")
if err != nil {
return "", err
out, err = getInvoker().Command("bootinfo", "-y")
if err != nil {
kernelArchErr = err
return
}
}
kernelArchVal = strings.TrimRight(string(out), "\n")
})
return kernelArchVal, kernelArchErr
}

func VirtualizationWithContext(ctx context.Context) (string, string, error) {
// Check for WPAR (Workload Partition) first — most specific virtualization layer.
// uname -W returns "0" if not in a WPAR, or the WPAR ID if inside one.
out, err := getInvoker().CommandWithContext(ctx, "uname", "-W")
if err == nil {
wparID := strings.TrimSpace(string(out))
if wparID != "" && wparID != "0" {
return "wpar", "guest", nil
}
}
arch = strings.TrimRight(string(out), "\n")

return arch, nil
}
// Check for LPAR (Logical Partition) via PowerVM.
// uname -L returns "<id> <name>", e.g. "25 soaix422". If name is "NULL", no LPAR.
out, err = getInvoker().CommandWithContext(ctx, "uname", "-L")
if err == nil {
fields := strings.Fields(strings.TrimSpace(string(out)))
if len(fields) >= 2 && fields[1] != "NULL" {
return "powervm", "guest", nil
}
}

func VirtualizationWithContext(_ context.Context) (string, string, error) {
return "", "", common.ErrNotImplementedError
return "", "", nil
}
Loading
Loading