From b9b3dbe67a5cd51eb1d60708851f9ae04e289c7b Mon Sep 17 00:00:00 2001 From: Pierre Fersing Date: Thu, 13 Jan 2022 10:49:17 +0100 Subject: [PATCH 1/3] Avoid ps command and use KProc on MacOS --- go.mod | 2 +- go.sum | 4 +-- process/process_darwin.go | 80 ++++++++++------------------------------------- 3 files changed, 19 insertions(+), 67 deletions(-) diff --git a/go.mod b/go.mod index f19cec5..1bb369e 100644 --- a/go.mod +++ b/go.mod @@ -9,5 +9,5 @@ require ( github.com/stretchr/testify v1.7.0 github.com/tklauser/go-sysconf v0.3.9 github.com/yusufpapurcu/wmi v1.2.2 - golang.org/x/sys v0.0.0-20211013075003-97ac67df715c + golang.org/x/sys v0.0.0-20220111092808-5a964db01320 ) diff --git a/go.sum b/go.sum index 50b5a10..ac1a981 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,8 @@ github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211013075003-97ac67df715c h1:taxlMj0D/1sOAuv/CbSD+MMDof2vbyPTqz5FNYKpXt8= -golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY= +golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/process/process_darwin.go b/process/process_darwin.go index c1dc918..3f02058 100644 --- a/process/process_darwin.go +++ b/process/process_darwin.go @@ -10,7 +10,6 @@ import ( "path/filepath" "strconv" "strings" - "time" "github.com/shirou/gopsutil/v3/cpu" "github.com/shirou/gopsutil/v3/internal/common" @@ -46,34 +45,25 @@ type _Ctype_struct___0 struct { func pidsWithContext(ctx context.Context) ([]int32, error) { var ret []int32 - pids, err := callPsWithContext(ctx, "pid", 0, false, false) + kprocs, err := unix.SysctlKinfoProcSlice("kern.proc.all") if err != nil { return ret, err } - for _, pid := range pids { - v, err := strconv.Atoi(pid[0]) - if err != nil { - return ret, err - } - ret = append(ret, int32(v)) + for _, proc := range kprocs { + ret = append(ret, int32(proc.Proc.P_pid)) } return ret, nil } func (p *Process) PpidWithContext(ctx context.Context) (int32, error) { - r, err := callPsWithContext(ctx, "ppid", p.Pid, false, false) - if err != nil { - return 0, err - } - - v, err := strconv.Atoi(r[0][0]) + k, err := p.getKProc() if err != nil { return 0, err } - return int32(v), err + return k.Eproc.Ppid, nil } func (p *Process) NameWithContext(ctx context.Context) (string, error) { @@ -81,7 +71,8 @@ func (p *Process) NameWithContext(ctx context.Context) (string, error) { if err != nil { return "", err } - name := common.IntToString(k.Proc.P_comm[:]) + + name := common.ByteToString(k.Proc.P_comm[:]) if len(name) >= 15 { cmdName, err := p.cmdNameWithContext(ctx) @@ -111,51 +102,21 @@ func (p *Process) cmdNameWithContext(ctx context.Context) ([]string, error) { } func (p *Process) createTimeWithContext(ctx context.Context) (int64, error) { - r, err := callPsWithContext(ctx, "etime", p.Pid, false, false) + k, err := p.getKProc() if err != nil { return 0, err } - elapsedSegments := strings.Split(strings.Replace(r[0][0], "-", ":", 1), ":") - var elapsedDurations []time.Duration - for i := len(elapsedSegments) - 1; i >= 0; i-- { - p, err := strconv.ParseInt(elapsedSegments[i], 10, 0) - if err != nil { - return 0, err - } - elapsedDurations = append(elapsedDurations, time.Duration(p)) - } - - elapsed := time.Duration(elapsedDurations[0]) * time.Second - if len(elapsedDurations) > 1 { - elapsed += time.Duration(elapsedDurations[1]) * time.Minute - } - if len(elapsedDurations) > 2 { - elapsed += time.Duration(elapsedDurations[2]) * time.Hour - } - if len(elapsedDurations) > 3 { - elapsed += time.Duration(elapsedDurations[3]) * time.Hour * 24 - } - - start := time.Now().Add(-elapsed) - return start.Unix() * 1000, nil + return k.Proc.P_starttime.Sec*1000 + int64(k.Proc.P_starttime.Usec)/1000, nil } func (p *Process) ParentWithContext(ctx context.Context) (*Process, error) { - out, err := common.CallLsofWithContext(ctx, invoke, p.Pid, "-FR") + ppid, err := p.PpidWithContext(ctx) if err != nil { return nil, err } - for _, line := range out { - if len(line) >= 1 && line[0] == 'R' { - v, err := strconv.Atoi(line[1:]) - if err != nil { - return nil, err - } - return NewProcessWithContext(ctx, int32(v)) - } - } - return nil, fmt.Errorf("could not find parent line") + + return NewProcessWithContext(ctx, ppid) } func (p *Process) StatusWithContext(ctx context.Context) ([]string, error) { @@ -188,7 +149,7 @@ func (p *Process) UidsWithContext(ctx context.Context) ([]int32, error) { } // See: http://unix.superglobalmegacorp.com/Net2/newsrc/sys/ucred.h.html - userEffectiveUID := int32(k.Eproc.Ucred.UID) + userEffectiveUID := int32(k.Eproc.Ucred.Uid) return []int32{userEffectiveUID}, nil } @@ -200,7 +161,7 @@ func (p *Process) GidsWithContext(ctx context.Context) ([]int32, error) { } gids := make([]int32, 0, 3) - gids = append(gids, int32(k.Eproc.Pcred.P_rgid), int32(k.Eproc.Ucred.Ngroups), int32(k.Eproc.Pcred.P_svgid)) + gids = append(gids, int32(k.Eproc.Pcred.P_rgid), int32(k.Eproc.Pcred.P_rgid), int32(k.Eproc.Pcred.P_svgid)) return gids, nil } @@ -399,17 +360,8 @@ func ProcessesWithContext(ctx context.Context) ([]*Process, error) { // Returns a proc as defined here: // http://unix.superglobalmegacorp.com/Net2/newsrc/sys/kinfo_proc.h.html -func (p *Process) getKProc() (*KinfoProc, error) { - buf, err := unix.SysctlRaw("kern.proc.pid", int(p.Pid)) - if err != nil { - return nil, err - } - k, err := parseKinfoProc(buf) - if err != nil { - return nil, err - } - - return &k, nil +func (p *Process) getKProc() (*unix.KinfoProc, error) { + return unix.SysctlKinfoProc("kern.proc.pid", int(p.Pid)) } // call ps command. From 60eae48e6aa75012c62f2926a43d64bf40d88a8d Mon Sep 17 00:00:00 2001 From: Pierre Fersing Date: Thu, 13 Jan 2022 13:27:05 +0100 Subject: [PATCH 2/3] Drop test for Darwin Pids() --- process/process_test.go | 20 -------------------- process/testdata/darwin/ps-ax-opid_fail | 10 ---------- 2 files changed, 30 deletions(-) delete mode 100644 process/testdata/darwin/ps-ax-opid_fail diff --git a/process/process_test.go b/process/process_test.go index 2bb72b8..786eb51 100644 --- a/process/process_test.go +++ b/process/process_test.go @@ -46,26 +46,6 @@ func Test_Pids(t *testing.T) { } } -func Test_Pids_Fail(t *testing.T) { - if runtime.GOOS != "darwin" { - t.Skip("darwin only") - } - - mu.Lock() - defer mu.Unlock() - - invoke = common.FakeInvoke{Suffix: "fail"} - ret, err := Pids() - skipIfNotImplementedErr(t, err) - invoke = common.Invoke{} - if err != nil { - t.Errorf("error %v", err) - } - if len(ret) != 9 { - t.Errorf("wrong getted pid nums: %v/%d", ret, len(ret)) - } -} - func Test_Pid_exists(t *testing.T) { checkPid := os.Getpid() diff --git a/process/testdata/darwin/ps-ax-opid_fail b/process/testdata/darwin/ps-ax-opid_fail deleted file mode 100644 index fce59ef..0000000 --- a/process/testdata/darwin/ps-ax-opid_fail +++ /dev/null @@ -1,10 +0,0 @@ - PID - 245 - 247 - 248 - 249 - 254 - 262 - 264 - 265 - 267 From 43e50e1d7d027c76e89ade0efb93e8f7e8a0ca84 Mon Sep 17 00:00:00 2001 From: Pierre Fersing Date: Thu, 13 Jan 2022 17:55:01 +0100 Subject: [PATCH 3/3] Reduce call to ps for process package on darwin --- process/process_darwin.go | 72 +------------------------------- process/process_darwin_cgo.go | 91 ++++++++++++++++++++++++++++++++++++++--- process/process_darwin_nocgo.go | 71 ++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 76 deletions(-) diff --git a/process/process_darwin.go b/process/process_darwin.go index 3f02058..9d3bf19 100644 --- a/process/process_darwin.go +++ b/process/process_darwin.go @@ -11,7 +11,6 @@ import ( "strconv" "strings" - "github.com/shirou/gopsutil/v3/cpu" "github.com/shirou/gopsutil/v3/internal/common" "github.com/shirou/gopsutil/v3/net" "github.com/tklauser/go-sysconf" @@ -80,11 +79,11 @@ func (p *Process) NameWithContext(ctx context.Context) (string, error) { return "", err } if len(cmdName) > 0 { - extendedName := filepath.Base(cmdName[0]) + extendedName := filepath.Base(cmdName) if strings.HasPrefix(extendedName, p.name) { name = extendedName } else { - name = cmdName[0] + name = cmdName } } } @@ -92,15 +91,6 @@ func (p *Process) NameWithContext(ctx context.Context) (string, error) { return name, nil } -// cmdNameWithContext returns the command name (including spaces) without any arguments -func (p *Process) cmdNameWithContext(ctx context.Context) ([]string, error) { - r, err := callPsWithContext(ctx, "command", p.Pid, false, true) - if err != nil { - return nil, err - } - return r[0], err -} - func (p *Process) createTimeWithContext(ctx context.Context) (int64, error) { k, err := p.getKProc() if err != nil { @@ -211,14 +201,6 @@ func (p *Process) IOCountersWithContext(ctx context.Context) (*IOCountersStat, e return nil, common.ErrNotImplementedError } -func (p *Process) NumThreadsWithContext(ctx context.Context) (int32, error) { - r, err := callPsWithContext(ctx, "utime,stime", p.Pid, true, false) - if err != nil { - return 0, err - } - return int32(len(r)), nil -} - func convertCPUTimes(s string) (ret float64, err error) { var t int var _tmp string @@ -265,56 +247,6 @@ func convertCPUTimes(s string) (ret float64, err error) { return float64(t) / float64(clockTicks), nil } -func (p *Process) TimesWithContext(ctx context.Context) (*cpu.TimesStat, error) { - r, err := callPsWithContext(ctx, "utime,stime", p.Pid, false, false) - if err != nil { - return nil, err - } - - utime, err := convertCPUTimes(r[0][0]) - if err != nil { - return nil, err - } - stime, err := convertCPUTimes(r[0][1]) - if err != nil { - return nil, err - } - - ret := &cpu.TimesStat{ - CPU: "cpu", - User: utime, - System: stime, - } - return ret, nil -} - -func (p *Process) MemoryInfoWithContext(ctx context.Context) (*MemoryInfoStat, error) { - r, err := callPsWithContext(ctx, "rss,vsize,pagein", p.Pid, false, false) - if err != nil { - return nil, err - } - rss, err := strconv.Atoi(r[0][0]) - if err != nil { - return nil, err - } - vms, err := strconv.Atoi(r[0][1]) - if err != nil { - return nil, err - } - pagein, err := strconv.Atoi(r[0][2]) - if err != nil { - return nil, err - } - - ret := &MemoryInfoStat{ - RSS: uint64(rss) * 1024, - VMS: uint64(vms) * 1024, - Swap: uint64(pagein), - } - - return ret, nil -} - func (p *Process) ChildrenWithContext(ctx context.Context) ([]*Process, error) { pids, err := common.CallPgrepWithContext(ctx, invoke, p.Pid) if err != nil { diff --git a/process/process_darwin_cgo.go b/process/process_darwin_cgo.go index 972b74b..2ac413f 100644 --- a/process/process_darwin_cgo.go +++ b/process/process_darwin_cgo.go @@ -9,6 +9,7 @@ package process // #include // #include // #include +// #include import "C" import ( @@ -18,12 +19,18 @@ import ( "strings" "syscall" "unsafe" + + "github.com/shirou/gopsutil/v3/cpu" ) -var argMax int +var ( + argMax int + timescaleToNanoSeconds float64 +) func init() { argMax = getArgMax() + timescaleToNanoSeconds = getTimeScaleToNanoSeconds() } func getArgMax() int { @@ -39,6 +46,14 @@ func getArgMax() int { return 0 } +func getTimeScaleToNanoSeconds() float64 { + var timeBaseInfo C.struct_mach_timebase_info + + C.mach_timebase_info(&timeBaseInfo) + + return float64(timeBaseInfo.numer) / float64(timeBaseInfo.denom) +} + func (p *Process) ExeWithContext(ctx context.Context) (string, error) { var c C.char // need a var for unsafe.Sizeof need a var const bufsize = C.PROC_PIDPATHINFO_MAXSIZE * unsafe.Sizeof(c) @@ -82,7 +97,7 @@ func (p *Process) CwdWithContext(ctx context.Context) (string, error) { return C.GoString(&vpi.pvi_cdir.vip_path[0]), err } -func procArgs(pid int32) (*[]byte, int, error) { +func procArgs(pid int32) ([]byte, int, error) { var ( mib = [...]C.int{C.CTL_KERN, C.KERN_PROCARGS2, C.int(pid)} size C.size_t = C.ulong(argMax) @@ -91,23 +106,27 @@ func procArgs(pid int32) (*[]byte, int, error) { ) procargs := (*C.char)(C.malloc(C.ulong(argMax))) defer C.free(unsafe.Pointer(procargs)) - retval := C.sysctl(&mib[0], 3, unsafe.Pointer(procargs), &size, C.NULL, 0) + retval, err := C.sysctl(&mib[0], 3, unsafe.Pointer(procargs), &size, C.NULL, 0) if retval == 0 { C.memcpy(unsafe.Pointer(&nargs), unsafe.Pointer(procargs), C.sizeof_int) result = C.GoBytes(unsafe.Pointer(procargs), C.int(size)) // fmt.Printf("size: %d %d\n%s\n", size, nargs, hex.Dump(result)) - return &result, int(nargs), nil + return result, int(nargs), nil } - return nil, 0, fmt.Errorf("error: %d", retval) + return nil, 0, err } func (p *Process) CmdlineSliceWithContext(ctx context.Context) ([]string, error) { + return p.cmdlineSliceWithContext(ctx, true) +} + +func (p *Process) cmdlineSliceWithContext(ctx context.Context, fallback bool) ([]string, error) { pargs, nargs, err := procArgs(p.Pid) if err != nil { return nil, err } // The first bytes hold the nargs int, skip it. - args := bytes.Split((*pargs)[C.sizeof_int:], []byte{0}) + args := bytes.Split((pargs)[C.sizeof_int:], []byte{0}) var argStr string // The first element is the actual binary/command path. // command := args[0] @@ -131,6 +150,20 @@ func (p *Process) CmdlineSliceWithContext(ctx context.Context) ([]string, error) return argSlice, err } +// cmdNameWithContext returns the command name (including spaces) without any arguments +func (p *Process) cmdNameWithContext(ctx context.Context) (string, error) { + r, err := p.cmdlineSliceWithContext(ctx, false) + if err != nil { + return "", err + } + + if len(r) == 0 { + return "", nil + } + + return r[0], err +} + func (p *Process) CmdlineWithContext(ctx context.Context) (string, error) { r, err := p.CmdlineSliceWithContext(ctx) if err != nil { @@ -138,3 +171,49 @@ func (p *Process) CmdlineWithContext(ctx context.Context) (string, error) { } return strings.Join(r, " "), err } + +func (p *Process) NumThreadsWithContext(ctx context.Context) (int32, error) { + const tiSize = C.sizeof_struct_proc_taskinfo + ti := (*C.struct_proc_taskinfo)(C.malloc(tiSize)) + + _, err := C.proc_pidinfo(C.int(p.Pid), C.PROC_PIDTASKINFO, 0, unsafe.Pointer(ti), tiSize) + if err != nil { + return 0, err + } + + return int32(ti.pti_threadnum), nil +} + +func (p *Process) TimesWithContext(ctx context.Context) (*cpu.TimesStat, error) { + const tiSize = C.sizeof_struct_proc_taskinfo + ti := (*C.struct_proc_taskinfo)(C.malloc(tiSize)) + + _, err := C.proc_pidinfo(C.int(p.Pid), C.PROC_PIDTASKINFO, 0, unsafe.Pointer(ti), tiSize) + if err != nil { + return nil, err + } + + ret := &cpu.TimesStat{ + CPU: "cpu", + User: float64(ti.pti_total_user) * timescaleToNanoSeconds / 1e9, + System: float64(ti.pti_total_system) * timescaleToNanoSeconds / 1e9, + } + return ret, nil +} + +func (p *Process) MemoryInfoWithContext(ctx context.Context) (*MemoryInfoStat, error) { + const tiSize = C.sizeof_struct_proc_taskinfo + ti := (*C.struct_proc_taskinfo)(C.malloc(tiSize)) + + _, err := C.proc_pidinfo(C.int(p.Pid), C.PROC_PIDTASKINFO, 0, unsafe.Pointer(ti), tiSize) + if err != nil { + return nil, err + } + + ret := &MemoryInfoStat{ + RSS: uint64(ti.pti_resident_size), + VMS: uint64(ti.pti_virtual_size), + Swap: uint64(ti.pti_pageins), + } + return ret, nil +} diff --git a/process/process_darwin_nocgo.go b/process/process_darwin_nocgo.go index 1426c97..3d696b7 100644 --- a/process/process_darwin_nocgo.go +++ b/process/process_darwin_nocgo.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" + "github.com/shirou/gopsutil/v3/cpu" "github.com/shirou/gopsutil/v3/internal/common" ) @@ -47,6 +48,18 @@ func (p *Process) CmdlineWithContext(ctx context.Context) (string, error) { return strings.Join(r[0], " "), err } +func (p *Process) cmdNameWithContext(ctx context.Context) (string, error) { + r, err := callPsWithContext(ctx, "command", p.Pid, false, true) + if err != nil { + return "", err + } + if len(r) > 0 && len(r[0]) > 0 { + return r[0][0], err + } + + return "", err +} + // CmdlineSliceWithContext returns the command line arguments of the process as a slice with each // element being an argument. Because of current deficiencies in the way that the command // line arguments are found, single arguments that have spaces in the will actually be @@ -59,3 +72,61 @@ func (p *Process) CmdlineSliceWithContext(ctx context.Context) ([]string, error) } return r[0], err } + +func (p *Process) NumThreadsWithContext(ctx context.Context) (int32, error) { + r, err := callPsWithContext(ctx, "utime,stime", p.Pid, true, false) + if err != nil { + return 0, err + } + return int32(len(r)), nil +} + +func (p *Process) TimesWithContext(ctx context.Context) (*cpu.TimesStat, error) { + r, err := callPsWithContext(ctx, "utime,stime", p.Pid, false, false) + if err != nil { + return nil, err + } + + utime, err := convertCPUTimes(r[0][0]) + if err != nil { + return nil, err + } + stime, err := convertCPUTimes(r[0][1]) + if err != nil { + return nil, err + } + + ret := &cpu.TimesStat{ + CPU: "cpu", + User: utime, + System: stime, + } + return ret, nil +} + +func (p *Process) MemoryInfoWithContext(ctx context.Context) (*MemoryInfoStat, error) { + r, err := callPsWithContext(ctx, "rss,vsize,pagein", p.Pid, false, false) + if err != nil { + return nil, err + } + rss, err := strconv.Atoi(r[0][0]) + if err != nil { + return nil, err + } + vms, err := strconv.Atoi(r[0][1]) + if err != nil { + return nil, err + } + pagein, err := strconv.Atoi(r[0][2]) + if err != nil { + return nil, err + } + + ret := &MemoryInfoStat{ + RSS: uint64(rss) * 1024, + VMS: uint64(vms) * 1024, + Swap: uint64(pagein), + } + + return ret, nil +}