Skip to content
Closed
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
114 changes: 82 additions & 32 deletions host/host_aix.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package host
import (
"context"
"strings"
"time"

"github.com/shirou/gopsutil/v4/internal/common"
)
Expand All @@ -15,8 +16,19 @@ const (
user_PROCESS = 7 //nolint:revive //FIXME
)

// testInvoker is used for dependency injection in tests
var testInvoker common.Invoker

// getInvoker returns the test invoker if set, otherwise returns the default
func getInvoker() common.Invoker {
if testInvoker != nil {
return testInvoker
}
return invoke
}

func HostIDWithContext(ctx context.Context) (string, error) {
out, err := invoke.CommandWithContext(ctx, "uname", "-u")
out, err := getInvoker().CommandWithContext(ctx, "uname", "-u")
if err != nil {
return "", err
}
Expand All @@ -26,56 +38,74 @@ func HostIDWithContext(ctx context.Context) (string, error) {
}

func BootTimeWithContext(ctx context.Context) (btime uint64, err error) {
return common.BootTimeWithContext(ctx, invoke)
return common.BootTimeWithContext(ctx, getInvoker())
}

// Uses ps to get the elapsed time for PID 1 in DAYS-HOURS:MINUTES:SECONDS format.
func UptimeWithContext(ctx context.Context) (uint64, error) {
return common.UptimeWithContext(ctx, invoke)
return common.UptimeWithContext(ctx, getInvoker())
}

// This is a weak implementation due to the limitations on retrieving this data in AIX
// UsersWithContext returns a list of currently logged-in users by parsing `who` output.
// Output format: root pts/0 Feb 27 06:58 (24.236.207.124)
func UsersWithContext(ctx context.Context) ([]UserStat, error) {
var ret []UserStat
out, err := invoke.CommandWithContext(ctx, "w")
out, err := getInvoker().CommandWithContext(ctx, "who")
if err != nil {
return nil, err
}
lines := strings.Split(string(out), "\n")
if len(lines) < 3 {
return []UserStat{}, common.ErrNotImplementedError

lines := strings.Split(strings.TrimSpace(string(out)), "\n")
if len(lines) == 0 || (len(lines) == 1 && lines[0] == "") {
return []UserStat{}, nil
}

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] == "-" {
now := time.Now()
var ret []UserStat
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) < 5 {
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]
}

us := UserStat{
User: fields[0],
Terminal: fields[1],
Started: parseWhoTimestamp(fields[2], fields[3], fields[4], now),
}
if len(fields) >= 6 {
us.Host = strings.Trim(fields[5], "()")
}

// Valid User data, so append it
ret = append(ret, *us)
ret = append(ret, us)
}

return ret, nil
}

// parseWhoTimestamp converts the month, day, and time fields from `who` output
// (e.g. "Feb", "27", "06:58") into a unix timestamp. The year is inferred from
// the current time, with a correction for the year boundary: if the login month
// is December but the current month is January, the login happened last year.
func parseWhoTimestamp(month, day, hhmm string, now time.Time) int {
loginTime, err := time.Parse("Jan 2 15:04", month+" "+day+" "+hhmm)
if err != nil {
return 0
}

year := now.Year()
if loginTime.Month() == time.December && now.Month() == time.January {
year--
}

loginTime = time.Date(year, loginTime.Month(), loginTime.Day(),
loginTime.Hour(), loginTime.Minute(), 0, 0, time.Local)
return int(loginTime.Unix())
}

// Much of this function could be static. However, to be future proofed, I've made it call the OS for the information in all instances.
func PlatformInformationWithContext(ctx context.Context) (platform, family, version string, err error) {
// Set the platform (which should always, and only be, "AIX") from `uname -s`
out, err := invoke.CommandWithContext(ctx, "uname", "-s")
out, err := getInvoker().CommandWithContext(ctx, "uname", "-s")
if err != nil {
return "", "", "", err
}
Expand All @@ -85,7 +115,7 @@ func PlatformInformationWithContext(ctx context.Context) (platform, family, vers
family = strings.TrimRight(string(out), "\n")

// Set the version
out, err = invoke.CommandWithContext(ctx, "oslevel")
out, err = getInvoker().CommandWithContext(ctx, "oslevel")
if err != nil {
return "", "", "", err
}
Expand All @@ -95,7 +125,7 @@ func PlatformInformationWithContext(ctx context.Context) (platform, family, vers
}

func KernelVersionWithContext(ctx context.Context) (version string, err error) {
out, err := invoke.CommandWithContext(ctx, "oslevel", "-s")
out, err := getInvoker().CommandWithContext(ctx, "oslevel", "-s")
if err != nil {
return "", err
}
Expand All @@ -105,7 +135,7 @@ func KernelVersionWithContext(ctx context.Context) (version string, err error) {
}

func KernelArch() (arch string, err error) {
out, err := invoke.Command("bootinfo", "-y")
out, err := getInvoker().Command("bootinfo", "-y")
if err != nil {
return "", err
}
Expand All @@ -114,6 +144,26 @@ func KernelArch() (arch string, err error) {
return arch, nil
}

func VirtualizationWithContext(_ context.Context) (string, string, error) {
return "", "", common.ErrNotImplementedError
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 != "0" {
return "wpar", "guest", 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
}
}

return "", "", nil
}
Loading
Loading