metric/system/process/process_darwin.go (183 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//go:build darwin && cgo
package process
/*
#include <stdlib.h>
#include <sys/sysctl.h>
#include <sys/mount.h>
#include <mach/mach_init.h>
#include <mach/mach_host.h>
#include <mach/host_info.h>
#include <libproc.h>
#include <mach/processor_info.h>
#include <mach/vm_map.h>
*/
import "C"
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"os/user"
"strconv"
"syscall"
"time"
"unsafe"
"github.com/elastic/elastic-agent-libs/mapstr"
"github.com/elastic/elastic-agent-libs/opt"
"github.com/elastic/elastic-agent-system-metrics/metric/system/resolve"
)
// GetSelfPid is the darwin implementation; see the linux version in
// process_linux_common.go for more context.
func GetSelfPid(hostfs resolve.Resolver) (int, error) {
return os.Getpid(), nil
}
// FetchPids returns a map and array of pids
func (procStats *Stats) FetchPids() (ProcsMap, []ProcState, error) {
n := C.proc_listpids(C.PROC_ALL_PIDS, 0, nil, 0)
if n <= 0 {
return nil, nil, syscall.EINVAL
}
buf := make([]byte, n)
n = C.proc_listpids(C.PROC_ALL_PIDS, 0, unsafe.Pointer(&buf[0]), n)
if n <= 0 {
return nil, nil, syscall.ENOMEM
}
var pid int32
num := int(n) / binary.Size(pid)
bbuf := bytes.NewBuffer(buf)
procMap := make(ProcsMap, num)
plist := make([]ProcState, 0, num)
var wrappedErr error
var err error
for i := 0; i < num; i++ {
if err := binary.Read(bbuf, binary.LittleEndian, &pid); err != nil {
procStats.logger.Debugf("Errror reading from PROC_ALL_PIDS buffer: %s", err)
continue
}
if pid == 0 {
continue
}
procMap, plist, err = procStats.pidIter(int(pid), procMap, plist)
wrappedErr = errors.Join(wrappedErr, err)
}
return procMap, plist, toNonFatal(wrappedErr)
}
// GetInfoForPid returns basic info for the process
func GetInfoForPid(_ resolve.Resolver, pid int) (ProcState, error) {
info := C.struct_proc_taskallinfo{}
size := C.int(unsafe.Sizeof(info))
ptr := unsafe.Pointer(&info)
// For docs, see the link below. Check the `proc_taskallinfo` struct, which
// is a composition of `proc_bsdinfo` and `proc_taskinfo`.
// https://opensource.apple.com/source/xnu/xnu-1504.3.12/bsd/sys/proc_info.h.auto.html
n, err := C.proc_pidinfo(C.int(pid), C.PROC_PIDTASKALLINFO, 0, ptr, size)
if n != size {
return ProcState{}, fmt.Errorf("could not read process info for pid %d: proc_pidinfo returned %d, err: %w", pid, int(n), err)
}
status := ProcState{}
status.Name = C.GoString(&info.pbsd.pbi_comm[0])
switch info.pbsd.pbi_status {
case C.SIDL:
status.State = Idle
case C.SRUN:
status.State = Running
case C.SSLEEP:
status.State = Sleeping
case C.SSTOP:
status.State = Stopped
case C.SZOMB:
status.State = Zombie
default:
status.State = Unknown
}
status.Ppid = opt.IntWith(int(info.pbsd.pbi_ppid))
status.Pid = opt.IntWith(pid)
status.Pgid = opt.IntWith(int(info.pbsd.pbi_pgid))
status.NumThreads = opt.IntWith(int(info.ptinfo.pti_threadnum))
// Get process username. Fallback to UID if username is not available.
uid := strconv.Itoa(int(info.pbsd.pbi_uid))
user, err := user.LookupId(uid)
if err == nil && user.Username != "" {
status.Username = user.Username
} else {
status.Username = uid
}
// grab memory info + process time while we have it from struct_proc_taskallinfo
status.Memory.Size = opt.UintWith(uint64(info.ptinfo.pti_virtual_size))
status.Memory.Rss.Bytes = opt.UintWith(uint64(info.ptinfo.pti_resident_size))
status.CPU.User.Ticks = opt.UintWith(uint64(info.ptinfo.pti_total_user) / uint64(time.Millisecond))
status.CPU.System.Ticks = opt.UintWith(uint64(info.ptinfo.pti_total_system) / uint64(time.Millisecond))
status.CPU.Total.Ticks = opt.UintWith(opt.SumOptUint(status.CPU.User.Ticks, status.CPU.System.Ticks))
status.CPU.StartTime = unixTimeMsToTime((uint64(info.pbsd.pbi_start_tvsec) * 1000) + (uint64(info.pbsd.pbi_start_tvusec) / 1000))
return status, nil
}
// FillPidMetrics is the darwin implementation
func FillPidMetrics(_ resolve.Resolver, pid int, state ProcState, filter func(string) bool) (ProcState, error) {
args, exe, env, err := getProcArgs(pid, filter)
if err != nil {
return state, fmt.Errorf("error fetching string data from process: %w", err)
}
state.Args = args
state.Exe = exe
if state.Env == nil {
state.Env = env
}
return state, nil
}
func getProcArgs(pid int, filter func(string) bool) ([]string, string, mapstr.M, error) {
mib := []C.int{C.CTL_KERN, C.KERN_PROCARGS2, C.int(pid)}
argmax := uintptr(C.ARG_MAX)
buf := make([]byte, argmax)
err := sysctl(mib, &buf[0], &argmax, nil, 0)
if err != nil {
return nil, "", nil, fmt.Errorf("error in sysctl: %w", err)
}
bbuf := bytes.NewBuffer(buf)
bbuf.Truncate(int(argmax))
var argc int32 // raw buffer
_ = binary.Read(bbuf, binary.LittleEndian, &argc) // read length
path, err := bbuf.ReadBytes(0)
if err != nil {
return nil, "", nil, fmt.Errorf("error reading the executable name: %w", err)
}
exeName := stripNullByte(path)
// skip trailing nul bytes
for {
c, err := bbuf.ReadByte()
if err != nil {
return nil, "", nil, fmt.Errorf("error skipping nul values in KERN_PROCARGS2 buffer: %w", err)
}
if c != 0 {
_ = bbuf.UnreadByte()
break
}
}
// read CLI args
argv := make([]string, 0, argc)
for i := 0; i < int(argc); i++ {
arg, err := bbuf.ReadBytes(0)
if err == io.EOF {
break
}
if err != nil {
return nil, exeName, nil, fmt.Errorf("error reading args from KERN_PROCARGS2: %w", err)
}
argv = append(argv, stripNullByte(arg))
}
delim := []byte{61} // "=" for key value pairs
envVars := mapstr.M{}
var envErr error
for {
line, err := bbuf.ReadBytes(0)
if err == io.EOF || line[0] == 0 {
break
}
if err != nil {
return argv, exeName, nil, fmt.Errorf("error reading args from KERN_PROCARGS2 buffer: %w", err)
}
pair := bytes.SplitN(stripNullByteRaw(line), delim, 2)
if len(pair) != 2 {
// invalid k-v pair encountered, return non-fatal error so that we can continue
err := fmt.Errorf("error reading process information from KERN_PROCARGS2: encountered invalid env pair for pid %d", pid)
envErr = errors.Join(envErr, NonFatalErr{Err: err})
continue
}
eKey := string(pair[0])
if filter == nil || filter(eKey) {
envVars[string(pair[0])] = string(pair[1])
}
}
return argv, exeName, envVars, envErr
}
func sysctl(mib []C.int, old *byte, oldlen *uintptr,
new *byte, newlen uintptr) (err error) {
p0 := unsafe.Pointer(&mib[0])
_, _, e1 := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(p0),
uintptr(len(mib)),
uintptr(unsafe.Pointer(old)), uintptr(unsafe.Pointer(oldlen)),
uintptr(unsafe.Pointer(new)), newlen)
if e1 != 0 {
err = e1
}
return err
}
func FillMetricsRequiringMoreAccess(_ int, state ProcState) (ProcState, error) {
return state, nil
}