From 4b83504853fed310660295e470e7e19257f02c6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Chigot?= Date: Tue, 28 Jan 2020 09:31:45 +0100 Subject: [PATCH 1/2] add AIX support Not all the features are available yet. Currently implemented: - Cpu - CpuList - FileSystemList - FileSystemUsage - LoadAverage - Mem - ProcArgs - ProcEnv - ProcExe - ProcList - ProcMem - ProcState - ProcTime - Rusage - Swap - Uptime --- CHANGELOG.md | 2 + README.md | 42 +-- concrete_sigar_test.go | 2 +- sigar_aix.go | 562 +++++++++++++++++++++++++++++++++++++++++ sigar_stub.go | 2 +- sigar_unix.go | 2 +- sigar_util.go | 9 + 7 files changed, 597 insertions(+), 24 deletions(-) create mode 100644 sigar_aix.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ae848ffb1..81dcfd37c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Add AIX support. #133 + ### Fixed ### Changed diff --git a/README.md b/README.md index ca1854bf2..07b479d44 100644 --- a/README.md +++ b/README.md @@ -19,27 +19,27 @@ in pure go/cgo, rather than cgo bindings for libsigar. The features vary by operating system. -| Feature | Linux | Darwin | Windows | OpenBSD | FreeBSD | -|-----------------|:-----:|:------:|:-------:|:-------:|:-------:| -| Cpu | X | X | X | X | X | -| CpuList | X | X | | X | X | -| FDUsage | X | | | | X | -| FileSystemList | X | X | X | X | X | -| FileSystemUsage | X | X | X | X | X | -| HugeTLBPages | X | | | | | -| LoadAverage | X | X | | X | X | -| Mem | X | X | X | X | X | -| ProcArgs | X | X | X | | X | -| ProcEnv | X | X | | | X | -| ProcExe | X | X | | | X | -| ProcFDUsage | X | | | | X | -| ProcList | X | X | X | | X | -| ProcMem | X | X | X | | X | -| ProcState | X | X | X | | X | -| ProcTime | X | X | X | | X | -| Rusage | X | | X | | | -| Swap | X | X | | X | X | -| Uptime | X | X | | X | X | +| Feature | Linux | Darwin | Windows | OpenBSD | FreeBSD | AIX | +|-----------------|:-----:|:------:|:-------:|:-------:|:-------:|:-------:| +| Cpu | X | X | X | X | X | X | +| CpuList | X | X | | X | X | X | +| FDUsage | X | | | | X | | +| FileSystemList | X | X | X | X | X | X | +| FileSystemUsage | X | X | X | X | X | X | +| HugeTLBPages | X | | | | | | +| LoadAverage | X | X | | X | X | X | +| Mem | X | X | X | X | X | X | +| ProcArgs | X | X | X | | X | X | +| ProcEnv | X | X | | | X | X | +| ProcExe | X | X | | | X | X | +| ProcFDUsage | X | | | | X | | +| ProcList | X | X | X | | X | X | +| ProcMem | X | X | X | | X | X | +| ProcState | X | X | X | | X | X | +| ProcTime | X | X | X | | X | X | +| Rusage | X | | X | | | X | +| Swap | X | X | | X | X | X | +| Uptime | X | X | | X | X | X | ## OS Specific Notes diff --git a/concrete_sigar_test.go b/concrete_sigar_test.go index 93fb3547d..99e3a36fe 100644 --- a/concrete_sigar_test.go +++ b/concrete_sigar_test.go @@ -79,7 +79,7 @@ func TestConcreteFileSystemUsage(t *testing.T) { func TestConcreteGetFDUsage(t *testing.T) { concreteSigar := &sigar.ConcreteSigar{} fdUsage, err := concreteSigar.GetFDUsage() - skipNotImplemented(t, err, "windows", "darwin") + skipNotImplemented(t, err, "windows", "darwin", "aix") if assert.NoError(t, err) { assert.True(t, fdUsage.Open > 0) assert.True(t, fdUsage.Open <= fdUsage.Max) diff --git a/sigar_aix.go b/sigar_aix.go new file mode 100644 index 000000000..b2896cfaf --- /dev/null +++ b/sigar_aix.go @@ -0,0 +1,562 @@ +// +build aix + +package gosigar + +/* +#cgo LDFLAGS: -L/usr/lib -lperfstat + +#include +#include +#include +#include +#include +#include +#include +#include + +*/ +import "C" + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "os" + "os/user" + "runtime" + "strconv" + "syscall" + "time" + "unsafe" +) + +var system struct { + ticks uint64 + btime uint64 + pagesize uint64 +} + +func init() { + // sysconf(_SC_CLK_TCK) returns the number of ticks by second. + system.ticks = uint64(C.sysconf(C._SC_CLK_TCK)) + system.pagesize = uint64(os.Getpagesize()) +} + +// utmp can't be used by "encoding/binary" if generated by cgo, +// some pads will be explicitly missing. +type utmp struct { + User [256]uint8 + Id [14]uint8 + Line [64]uint8 + XPad1 int16 + Pid int32 + Type int16 + XPad2 int16 + Time int64 + Termination int16 + Exit int16 + Host [256]uint8 + Xdbl_word_pad int32 + XreservedA [2]int32 + XreservedV [6]int32 +} + +func bootTime() (uint64, error) { + if system.btime != 0 { + return system.btime, nil + } + + // Get boot time from /etc/utmp + file, err := os.Open("/etc/utmp") + if err != nil { + return 0, fmt.Errorf("error while opening /etc/utmp: %s", err) + } + + defer file.Close() + + for { + var utmp utmp + if err := binary.Read(file, binary.BigEndian, &utmp); err != nil { + break + } + + if utmp.Type == C.BOOT_TIME { + system.btime = uint64(utmp.Time) + break + } + } + return system.btime, nil +} + +func tick2msec(val uint64) uint64 { + return val * 1000 / system.ticks +} + +// Return the list of file systems +func (self *FileSystemList) Get() error { + var size C.int + _, err := C.mntctl(C.MCTL_QUERY, C.sizeof_int, (*C.char)(unsafe.Pointer(&size))) + if err != nil { + return fmt.Errorf("error while retrieving file system number: %s", err) + } + + buf := make([]byte, size) + num, err := C.mntctl(C.MCTL_QUERY, C.ulong(size), (*C.char)(&buf[0])) + if err != nil { + return fmt.Errorf("error while retrieving file system list: %s", err) + } + + // Vmount structure has a fixed size area for common data (type, + // offsets, etc) and another area with variable length data (devname, + // options, etc). These data can be accessed based on the offsets + // stored in an array inside the fixed part. They can be retrieve + // using index given by C define. + vmt2data := func(buf []byte, ent *C.struct_vmount, idx int, baseOff int) []byte { + off := int(ent.vmt_data[idx].vmt_off) + size := int(ent.vmt_data[idx].vmt_size) + return buf[baseOff+off : baseOff+off+size] + } + + entOff := 0 + + fslist := make([]FileSystem, num) + for i := 0; i < int(num); i++ { + ent := (*C.struct_vmount)(unsafe.Pointer(&buf[entOff])) + fs := &fslist[i] + + // Correspondances taken for /etc/vfs + switch ent.vmt_gfstype { + case C.MNT_AIX: + fs.SysTypeName = "jfs2" + case C.MNT_NAMEFS: + fs.SysTypeName = "namefs" + case C.MNT_NFS: + fs.SysTypeName = "nfs" + case C.MNT_JFS: + fs.SysTypeName = "jfs" + case C.MNT_CDROM: + fs.SysTypeName = "cdrom" + case C.MNT_PROCFS: + fs.SysTypeName = "proc" + case C.MNT_SFS: + fs.SysTypeName = "sfs" + case C.MNT_CACHEFS: + fs.SysTypeName = "cachefs" + case C.MNT_NFS3: + fs.SysTypeName = "nfs3" + case C.MNT_AUTOFS: + fs.SysTypeName = "autofs" + case C.MNT_POOLFS: + fs.SysTypeName = "poolfs" + case C.MNT_UDF: + fs.SysTypeName = "udfs" + case C.MNT_NFS4: + fs.SysTypeName = "nfs4" + case C.MNT_CIFS: + fs.SysTypeName = "cifs" + case C.MNT_PMEMFS: + fs.SysTypeName = "pmemfs" + case C.MNT_AHAFS: + fs.SysTypeName = "ahafs" + case C.MNT_STNFS: + fs.SysTypeName = "stnfs" + default: + if ent.vmt_flags&C.MNT_REMOTE != 0 { + fs.SysTypeName = "network" + } else { + fs.SysTypeName = "none" + } + } + + fs.DirName = convertBytesToString(vmt2data(buf, ent, C.VMT_STUB, entOff)) + fs.Options = convertBytesToString(vmt2data(buf, ent, C.VMT_ARGS, entOff)) + devname := convertBytesToString(vmt2data(buf, ent, C.VMT_OBJECT, entOff)) + if ent.vmt_flags&C.MNT_REMOTE != 0 { + hostname := convertBytesToString(vmt2data(buf, ent, C.VMT_OBJECT, entOff)) + fs.DevName = hostname + ":" + devname + } else { + fs.DevName = devname + } + + entOff += int(ent.vmt_length) + } + + self.List = fslist + + return nil +} + +// Return the CPU load average +func (self *LoadAverage) Get() error { + cpudata := C.perfstat_cpu_total_t{} + + if _, err := C.perfstat_cpu_total(nil, &cpudata, C.sizeof_perfstat_cpu_total_t, 1); err != nil { + return fmt.Errorf("perfstat_cpu_total: %s", err) + } + + // from libperfstat.h: + // "To calculate the load average, divide the numbers by (1<." + fixedToFloat64 := func(x uint64) float64 { + return float64(x) / (1 << C.SBITS) + } + self.One = fixedToFloat64(uint64(cpudata.loadavg[0])) + self.Five = fixedToFloat64(uint64(cpudata.loadavg[1])) + self.Fifteen = fixedToFloat64(uint64(cpudata.loadavg[2])) + + return nil +} + +// Return the system uptime +func (self *Uptime) Get() error { + btime, err := bootTime() + if err != nil { + return err + } + uptime := time.Now().Sub(time.Unix(int64(btime), 0)) + self.Length = uptime.Seconds() + return nil +} + +// Return the current system memory +func (self *Mem) Get() error { + meminfo := C.perfstat_memory_total_t{} + _, err := C.perfstat_memory_total(nil, &meminfo, C.sizeof_perfstat_memory_total_t, 1) + if err != nil { + return fmt.Errorf("perfstat_memory_total: %s", err) + } + + self.Total = uint64(meminfo.real_total) * system.pagesize + self.Free = uint64(meminfo.real_free) * system.pagesize + + kern := uint64(meminfo.numperm) * system.pagesize // number of pages in file cache + + self.Used = self.Total - self.Free + self.ActualFree = self.Free + kern + self.ActualUsed = self.Used - kern + + return nil +} + +// Return the current system swap memory +func (self *Swap) Get() error { + ps := C.perfstat_pagingspace_t{} + id := C.perfstat_id_t{} + + id.name[0] = 0 + + for { + // errno can be set during perfstat_pagingspace's call even + // if it succeeds. Thus, only check it when the result is -1. + if r, err := C.perfstat_pagingspace(&id, &ps, C.sizeof_perfstat_pagingspace_t, 1); r == -1 && err != nil { + return fmt.Errorf("perfstat_memory_total: %s", err) + } + + if ps.active != 1 { + continue + } + + // convert MB sizes to bytes + self.Total += uint64(ps.mb_size) * 1024 * 1024 + self.Used += uint64(ps.mb_used) * 1024 * 1024 + + if id.name[0] == 0 { + break + } + } + + self.Free = self.Total - self.Used + + return nil +} + +// Return information about a CPU +func (self *Cpu) Get() error { + cpudata := C.perfstat_cpu_total_t{} + + if _, err := C.perfstat_cpu_total(nil, &cpudata, C.sizeof_perfstat_cpu_total_t, 1); err != nil { + return fmt.Errorf("perfstat_cpu_total: %s", err) + } + + self.User = tick2msec(uint64(cpudata.user)) + self.Sys = tick2msec(uint64(cpudata.sys)) + self.Idle = tick2msec(uint64(cpudata.idle)) + self.Wait = tick2msec(uint64(cpudata.wait)) + + return nil +} + +// Return the list of CPU used by the system +func (self *CpuList) Get() error { + cpudata := C.perfstat_cpu_t{} + id := C.perfstat_id_t{} + id.name[0] = 0 + + // Retrieve the number of cpu using perfstat_cpu + capacity, err := C.perfstat_cpu(nil, nil, C.sizeof_perfstat_cpu_t, 0) + if err != nil { + return fmt.Errorf("error while retrieving CPU number: %s", err) + } + list := make([]Cpu, 0, capacity) + + for { + if _, err := C.perfstat_cpu(&id, &cpudata, C.sizeof_perfstat_cpu_t, 1); err != nil { + return fmt.Errorf("perfstat_cpu: %s", err) + } + + cpu := Cpu{} + cpu.User = tick2msec(uint64(cpudata.user)) + cpu.Sys = tick2msec(uint64(cpudata.sys)) + cpu.Idle = tick2msec(uint64(cpudata.idle)) + cpu.Wait = tick2msec(uint64(cpudata.wait)) + + list = append(list, cpu) + + if id.name[0] == 0 { + break + } + } + + self.List = list + + return nil + +} + +// Return the list of all active processes +func (self *ProcList) Get() error { + info := C.struct_procsinfo64{} + pid := C.pid_t(0) + + var list []int + + for { + // getprocs first argument is a void* + num, err := C.getprocs(unsafe.Pointer(&info), C.sizeof_struct_procsinfo64, nil, 0, &pid, 1) + if err != nil { + return err + } + + list = append(list, int(info.pi_pid)) + + if num == 0 { + break + } + } + + self.List = list + + return nil +} + +// Return information about a process +func (self *ProcState) Get(pid int) error { + info := C.struct_procsinfo64{} + cpid := C.pid_t(pid) + + num, err := C.getprocs(unsafe.Pointer(&info), C.sizeof_struct_procsinfo64, nil, 0, &cpid, 1) + if err != nil { + return err + } + if num != 1 { + return syscall.ESRCH + } + + self.Name = C.GoString(&info.pi_comm[0]) + self.Ppid = int(info.pi_ppid) + self.Pgid = int(info.pi_pgrp) + self.Nice = int(info.pi_nice) + self.Tty = int(info.pi_ttyd) + self.Priority = int(info.pi_pri) + + switch info.pi_state { + case C.SACTIVE: + self.State = RunStateRun + case C.SIDL: + self.State = RunStateIdle + case C.SSTOP: + self.State = RunStateStop + case C.SZOMB: + self.State = RunStateZombie + case C.SSWAP: + self.State = RunStateSleep + default: + self.State = RunStateUnknown + } + + // Get process username. Fallback to UID if username is not available. + uid := strconv.Itoa(int(info.pi_uid)) + user, err := user.LookupId(uid) + if err == nil && user.Username != "" { + self.Username = user.Username + } else { + self.Username = uid + } + + thrinfo := C.struct_thrdsinfo64{} + tid := C.tid_t(0) + + if _, err := C.getthrds(cpid, unsafe.Pointer(&thrinfo), C.sizeof_struct_thrdsinfo64, &tid, 1); err != nil { + self.Processor = int(thrinfo.ti_affinity) + } + + return nil +} + +// Return the current memory usage of a process +func (self *ProcMem) Get(pid int) error { + info := C.struct_procsinfo64{} + cpid := C.pid_t(pid) + + num, err := C.getprocs(unsafe.Pointer(&info), C.sizeof_struct_procsinfo64, nil, 0, &cpid, 1) + if err != nil { + return err + } + if num != 1 { + return syscall.ESRCH + } + + self.Size = uint64(info.pi_size) * system.pagesize + self.Share = uint64(info.pi_sdsize) * system.pagesize + self.Resident = uint64(info.pi_drss+info.pi_trss) * system.pagesize + + self.MinorFaults = uint64(info.pi_minflt) + self.MajorFaults = uint64(info.pi_majflt) + self.PageFaults = self.MinorFaults + self.MajorFaults + + return nil +} + +// Return a process uptime +func (self *ProcTime) Get(pid int) error { + info := C.struct_procsinfo64{} + cpid := C.pid_t(pid) + + num, err := C.getprocs(unsafe.Pointer(&info), C.sizeof_struct_procsinfo64, nil, 0, &cpid, 1) + if err != nil { + return err + } + if num != 1 { + return syscall.ESRCH + } + + self.StartTime = uint64(info.pi_start) * 1000 + self.User = uint64(info.pi_utime) * 1000 + self.Sys = uint64(info.pi_stime) * 1000 + self.Total = self.User + self.Sys + + return nil +} + +// Return arguments of a process +func (self *ProcArgs) Get(pid int) error { + /* If buffer is not large enough, args are truncated */ + buf := make([]byte, 8192) + info := C.struct_procsinfo64{} + info.pi_pid = C.pid_t(pid) + + if _, err := C.getargs(unsafe.Pointer(&info), C.sizeof_struct_procsinfo64, (*C.char)(&buf[0]), 8192); err != nil { + return err + } + + bbuf := bytes.NewBuffer(buf) + + var args []string + + for { + arg, err := bbuf.ReadBytes(0) + if err == io.EOF || arg[0] == 0 { + break + } + if err != nil { + return err + } + + args = append(args, string(chop(arg))) + } + + self.List = args + return nil +} + +// Return the environment of a process +func (self *ProcEnv) Get(pid int) error { + if self.Vars == nil { + self.Vars = map[string]string{} + } + + /* If buffer is not large enough, args are truncated */ + buf := make([]byte, 8192) + info := C.struct_procsinfo64{} + info.pi_pid = C.pid_t(pid) + + if _, err := C.getevars(unsafe.Pointer(&info), C.sizeof_struct_procsinfo64, (*C.char)(&buf[0]), 8192); err != nil { + return err + } + + bbuf := bytes.NewBuffer(buf) + + delim := []byte{61} // "=" + + for { + line, err := bbuf.ReadBytes(0) + if err == io.EOF || line[0] == 0 { + break + } + if err != nil { + return err + } + + pair := bytes.SplitN(chop(line), delim, 2) + if len(pair) != 2 { + return fmt.Errorf("Error reading process environment for PID: %d", pid) + } + self.Vars[string(pair[0])] = string(pair[1]) + } + + return nil +} + +// Return the path of the process executable +func (self *ProcExe) Get(pid int) error { + /* If buffer is not large enough, args are truncated */ + buf := make([]byte, 8192) + info := C.struct_procsinfo64{} + info.pi_pid = C.pid_t(pid) + + if _, err := C.getargs(unsafe.Pointer(&info), C.sizeof_struct_procsinfo64, (*C.char)(&buf[0]), 8192); err != nil { + return err + } + + bbuf := bytes.NewBuffer(buf) + + // retrieve the first argument + cmd, err := bbuf.ReadBytes(0) + if err != nil { + return err + } + self.Name = string(chop(cmd)) + + cwd, err := os.Readlink("/proc/" + strconv.Itoa(pid) + "/cwd") + if err != nil { + if !os.IsNotExist(err) { + return err + } + } + self.Cwd = cwd + + return nil +} + +func (self *ProcFDUsage) Get(pid int) error { + return ErrNotImplemented{runtime.GOOS} +} + +func (self *FDUsage) Get() error { + return ErrNotImplemented{runtime.GOOS} +} + +func (self *HugeTLBPages) Get() error { + return ErrNotImplemented{runtime.GOOS} +} diff --git a/sigar_stub.go b/sigar_stub.go index de9565aec..4156439df 100644 --- a/sigar_stub.go +++ b/sigar_stub.go @@ -1,4 +1,4 @@ -// +build !darwin,!freebsd,!linux,!openbsd,!windows +// +build !aix,!darwin,!freebsd,!linux,!openbsd,!windows package gosigar diff --git a/sigar_unix.go b/sigar_unix.go index 3f3a9f7ff..e423419f9 100644 --- a/sigar_unix.go +++ b/sigar_unix.go @@ -1,6 +1,6 @@ // Copyright (c) 2012 VMware, Inc. -// +build darwin freebsd linux +// +build aix darwin freebsd linux package gosigar diff --git a/sigar_util.go b/sigar_util.go index bf93b02b2..c976b3d8d 100644 --- a/sigar_util.go +++ b/sigar_util.go @@ -3,6 +3,7 @@ package gosigar import ( + "bytes" "unsafe" ) @@ -20,3 +21,11 @@ func bytePtrToString(ptr *int8) string { func chop(buf []byte) []byte { return buf[0 : len(buf)-1] } + +func convertBytesToString(arr []byte) string { + n := bytes.IndexByte(arr, 0) + if n == -1 { + return string(arr[:]) + } + return string(arr[:n]) +} From 56383e0262480f9a701254ac4f64f7c2442be8d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Chigot?= Date: Tue, 28 Jan 2020 09:37:54 +0100 Subject: [PATCH 2/2] test: trim environment variables given by ProcEnv Environment variable might have spaces at the end. Therefore, trim both entries before calling the assert function. Examples: ``` expected: "${debian_chroot:+($debian_chroot)}\\u@\\h:\\w\\$" actual: "${debian_chroot:+($debian_chroot)}\\u@\\h:\\w\\$ " ``` --- sigar_interface_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sigar_interface_test.go b/sigar_interface_test.go index cc55281f7..545738752 100644 --- a/sigar_interface_test.go +++ b/sigar_interface_test.go @@ -137,7 +137,7 @@ func TestProcEnv(t *testing.T) { assert.True(t, len(env.Vars) > 0, "env is empty") for k, v := range env.Vars { - assert.Equal(t, strings.TrimSpace(os.Getenv(k)), v) + assert.Equal(t, strings.TrimSpace(os.Getenv(k)), strings.TrimSpace(v)) } } }