| // SPDX-License-Identifier: BSD-3-Clause |
| //go:build linux |
| |
| package process |
| |
| import ( |
| "bufio" |
| "bytes" |
| "context" |
| "encoding/json" |
| "fmt" |
| "math" |
| "os" |
| "path/filepath" |
| "sort" |
| "strconv" |
| "strings" |
| |
| "github.com/tklauser/go-sysconf" |
| "golang.org/x/sys/unix" |
| |
| "github.com/shirou/gopsutil/v4/cpu" |
| "github.com/shirou/gopsutil/v4/internal/common" |
| "github.com/shirou/gopsutil/v4/net" |
| ) |
| |
| var pageSize = uint64(os.Getpagesize()) |
| |
| const prioProcess = 0 // linux/resource.h |
| |
| var clockTicks = 100 // default value |
| |
| func init() { |
| clkTck, err := sysconf.Sysconf(sysconf.SC_CLK_TCK) |
| // ignore errors |
| if err == nil { |
| clockTicks = int(clkTck) |
| } |
| } |
| |
| // MemoryInfoExStat is different between OSes |
| type MemoryInfoExStat struct { |
| RSS uint64 `json:"rss"` // bytes |
| VMS uint64 `json:"vms"` // bytes |
| Shared uint64 `json:"shared"` // bytes |
| Text uint64 `json:"text"` // bytes |
| Lib uint64 `json:"lib"` // bytes |
| Data uint64 `json:"data"` // bytes |
| Dirty uint64 `json:"dirty"` // bytes |
| } |
| |
| func (m MemoryInfoExStat) String() string { |
| s, _ := json.Marshal(m) |
| return string(s) |
| } |
| |
| type MemoryMapsStat struct { |
| Path string `json:"path"` |
| Rss uint64 `json:"rss"` |
| Size uint64 `json:"size"` |
| Pss uint64 `json:"pss"` |
| SharedClean uint64 `json:"sharedClean"` |
| SharedDirty uint64 `json:"sharedDirty"` |
| PrivateClean uint64 `json:"privateClean"` |
| PrivateDirty uint64 `json:"privateDirty"` |
| Referenced uint64 `json:"referenced"` |
| Anonymous uint64 `json:"anonymous"` |
| Swap uint64 `json:"swap"` |
| } |
| |
| // String returns JSON value of the process. |
| func (m MemoryMapsStat) String() string { |
| s, _ := json.Marshal(m) |
| return string(s) |
| } |
| |
| func (p *Process) PpidWithContext(ctx context.Context) (int32, error) { |
| _, ppid, _, _, _, _, _, err := p.fillFromStatWithContext(ctx) |
| if err != nil { |
| return -1, err |
| } |
| return ppid, nil |
| } |
| |
| func (p *Process) NameWithContext(ctx context.Context) (string, error) { |
| if p.name == "" { |
| if err := p.fillNameWithContext(ctx); err != nil { |
| return "", err |
| } |
| } |
| return p.name, nil |
| } |
| |
| func (p *Process) TgidWithContext(ctx context.Context) (int32, error) { |
| if p.tgid == 0 { |
| if err := p.fillFromStatusWithContext(ctx); err != nil { |
| return 0, err |
| } |
| } |
| return p.tgid, nil |
| } |
| |
| func (p *Process) ExeWithContext(ctx context.Context) (string, error) { |
| return p.fillFromExeWithContext(ctx) |
| } |
| |
| func (p *Process) CmdlineWithContext(ctx context.Context) (string, error) { |
| return p.fillFromCmdlineWithContext(ctx) |
| } |
| |
| func (p *Process) CmdlineSliceWithContext(ctx context.Context) ([]string, error) { |
| return p.fillSliceFromCmdlineWithContext(ctx) |
| } |
| |
| func (p *Process) createTimeWithContext(ctx context.Context) (int64, error) { |
| _, _, _, createTime, _, _, _, err := p.fillFromStatWithContext(ctx) |
| if err != nil { |
| return 0, err |
| } |
| return createTime, nil |
| } |
| |
| func (p *Process) CwdWithContext(ctx context.Context) (string, error) { |
| return p.fillFromCwdWithContext(ctx) |
| } |
| |
| func (p *Process) StatusWithContext(ctx context.Context) ([]string, error) { |
| err := p.fillFromStatusWithContext(ctx) |
| if err != nil { |
| return []string{""}, err |
| } |
| return []string{p.status}, nil |
| } |
| |
| func (p *Process) ForegroundWithContext(ctx context.Context) (bool, error) { |
| // see https://github.com/shirou/gopsutil/issues/596#issuecomment-432707831 for implementation details |
| pid := p.Pid |
| statPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "stat") |
| contents, err := os.ReadFile(statPath) |
| if err != nil { |
| return false, err |
| } |
| fields := strings.Fields(string(contents)) |
| if len(fields) < 8 { |
| return false, fmt.Errorf("insufficient data in %s", statPath) |
| } |
| pgid := fields[4] |
| tpgid := fields[7] |
| return pgid == tpgid, nil |
| } |
| |
| func (p *Process) UidsWithContext(ctx context.Context) ([]uint32, error) { |
| err := p.fillFromStatusWithContext(ctx) |
| if err != nil { |
| return []uint32{}, err |
| } |
| return p.uids, nil |
| } |
| |
| func (p *Process) GidsWithContext(ctx context.Context) ([]uint32, error) { |
| err := p.fillFromStatusWithContext(ctx) |
| if err != nil { |
| return []uint32{}, err |
| } |
| return p.gids, nil |
| } |
| |
| func (p *Process) GroupsWithContext(ctx context.Context) ([]uint32, error) { |
| err := p.fillFromStatusWithContext(ctx) |
| if err != nil { |
| return []uint32{}, err |
| } |
| return p.groups, nil |
| } |
| |
| func (p *Process) TerminalWithContext(ctx context.Context) (string, error) { |
| t, _, _, _, _, _, _, err := p.fillFromStatWithContext(ctx) |
| if err != nil { |
| return "", err |
| } |
| termmap, err := getTerminalMap() |
| if err != nil { |
| return "", err |
| } |
| terminal := termmap[t] |
| return terminal, nil |
| } |
| |
| func (p *Process) NiceWithContext(ctx context.Context) (int32, error) { |
| _, _, _, _, _, nice, _, err := p.fillFromStatWithContext(ctx) |
| if err != nil { |
| return 0, err |
| } |
| return nice, nil |
| } |
| |
| func (*Process) IOniceWithContext(_ context.Context) (int32, error) { |
| return 0, common.ErrNotImplementedError |
| } |
| |
| func (p *Process) RlimitWithContext(ctx context.Context) ([]RlimitStat, error) { |
| return p.RlimitUsageWithContext(ctx, false) |
| } |
| |
| func (p *Process) RlimitUsageWithContext(ctx context.Context, gatherUsed bool) ([]RlimitStat, error) { |
| rlimits, err := p.fillFromLimitsWithContext(ctx) |
| if !gatherUsed || err != nil { |
| return rlimits, err |
| } |
| |
| _, _, _, _, rtprio, nice, _, err := p.fillFromStatWithContext(ctx) |
| if err != nil { |
| return nil, err |
| } |
| if err := p.fillFromStatusWithContext(ctx); err != nil { |
| return nil, err |
| } |
| |
| for i := range rlimits { |
| rs := &rlimits[i] |
| switch rs.Resource { |
| case RLIMIT_CPU: |
| times, err := p.TimesWithContext(ctx) |
| if err != nil { |
| return nil, err |
| } |
| rs.Used = uint64(times.User + times.System) |
| case RLIMIT_DATA: |
| rs.Used = uint64(p.memInfo.Data) |
| case RLIMIT_STACK: |
| rs.Used = uint64(p.memInfo.Stack) |
| case RLIMIT_RSS: |
| rs.Used = uint64(p.memInfo.RSS) |
| case RLIMIT_NOFILE: |
| n, err := p.NumFDsWithContext(ctx) |
| if err != nil { |
| return nil, err |
| } |
| rs.Used = uint64(n) |
| case RLIMIT_MEMLOCK: |
| rs.Used = uint64(p.memInfo.Locked) |
| case RLIMIT_AS: |
| rs.Used = uint64(p.memInfo.VMS) |
| case RLIMIT_LOCKS: |
| // TODO we can get the used value from /proc/$pid/locks. But linux doesn't enforce it, so not a high priority. |
| case RLIMIT_SIGPENDING: |
| rs.Used = p.sigInfo.PendingProcess |
| case RLIMIT_NICE: |
| // The rlimit for nice is a little unusual, in that 0 means the niceness cannot be decreased beyond the current value, but it can be increased. |
| // So effectively: if rs.Soft == 0 { rs.Soft = rs.Used } |
| rs.Used = uint64(nice) |
| case RLIMIT_RTPRIO: |
| rs.Used = uint64(rtprio) |
| } |
| } |
| |
| return rlimits, err |
| } |
| |
| func (p *Process) IOCountersWithContext(ctx context.Context) (*IOCountersStat, error) { |
| return p.fillFromIOWithContext(ctx) |
| } |
| |
| func (p *Process) NumCtxSwitchesWithContext(ctx context.Context) (*NumCtxSwitchesStat, error) { |
| err := p.fillFromStatusWithContext(ctx) |
| if err != nil { |
| return nil, err |
| } |
| return p.numCtxSwitches, nil |
| } |
| |
| func (p *Process) NumFDsWithContext(ctx context.Context) (int32, error) { |
| _, fnames, err := p.fillFromfdListWithContext(ctx) |
| return int32(len(fnames)), err |
| } |
| |
| func (p *Process) NumThreadsWithContext(ctx context.Context) (int32, error) { |
| err := p.fillFromStatusWithContext(ctx) |
| if err != nil { |
| return 0, err |
| } |
| return p.numThreads, nil |
| } |
| |
| func (p *Process) ThreadsWithContext(ctx context.Context) (map[int32]*cpu.TimesStat, error) { |
| ret := make(map[int32]*cpu.TimesStat) |
| taskPath := common.HostProcWithContext(ctx, strconv.Itoa(int(p.Pid)), "task") |
| |
| tids, err := readPidsFromDir(taskPath) |
| if err != nil { |
| return nil, err |
| } |
| |
| for _, tid := range tids { |
| _, _, cpuTimes, _, _, _, _, err := p.fillFromTIDStatWithContext(ctx, tid) |
| if err != nil { |
| return nil, err |
| } |
| ret[tid] = cpuTimes |
| } |
| |
| return ret, nil |
| } |
| |
| func (p *Process) TimesWithContext(ctx context.Context) (*cpu.TimesStat, error) { |
| _, _, cpuTimes, _, _, _, _, err := p.fillFromStatWithContext(ctx) |
| if err != nil { |
| return nil, err |
| } |
| return cpuTimes, nil |
| } |
| |
| func (*Process) CPUAffinityWithContext(_ context.Context) ([]int32, error) { |
| return nil, common.ErrNotImplementedError |
| } |
| |
| func (p *Process) MemoryInfoWithContext(ctx context.Context) (*MemoryInfoStat, error) { |
| meminfo, _, err := p.fillFromStatmWithContext(ctx) |
| if err != nil { |
| return nil, err |
| } |
| return meminfo, nil |
| } |
| |
| func (p *Process) MemoryInfoExWithContext(ctx context.Context) (*MemoryInfoExStat, error) { |
| _, memInfoEx, err := p.fillFromStatmWithContext(ctx) |
| if err != nil { |
| return nil, err |
| } |
| return memInfoEx, nil |
| } |
| |
| func (p *Process) PageFaultsWithContext(ctx context.Context) (*PageFaultsStat, error) { |
| _, _, _, _, _, _, pageFaults, err := p.fillFromStatWithContext(ctx) |
| if err != nil { |
| return nil, err |
| } |
| return pageFaults, nil |
| } |
| |
| func (p *Process) ChildrenWithContext(ctx context.Context) ([]*Process, error) { |
| statFiles, err := filepath.Glob(common.HostProcWithContext(ctx, "[0-9]*/stat")) |
| if err != nil { |
| return nil, err |
| } |
| ret := make([]*Process, 0, len(statFiles)) |
| for _, statFile := range statFiles { |
| statContents, err := os.ReadFile(statFile) |
| if err != nil || len(statContents) == 0 { |
| continue |
| } |
| fields := splitProcStat(statContents) |
| pid, err := strconv.ParseInt(fields[1], 10, 32) |
| if err != nil { |
| continue |
| } |
| ppid, err := strconv.ParseInt(fields[4], 10, 32) |
| if err != nil { |
| continue |
| } |
| if ppid == int64(p.Pid) { |
| np, err := NewProcessWithContext(ctx, int32(pid)) |
| if err != nil { |
| continue |
| } |
| ret = append(ret, np) |
| } |
| } |
| sort.Slice(ret, func(i, j int) bool { return ret[i].Pid < ret[j].Pid }) |
| return ret, nil |
| } |
| |
| func (p *Process) OpenFilesWithContext(ctx context.Context) ([]OpenFilesStat, error) { |
| _, ofs, err := p.fillFromfdWithContext(ctx) |
| return ofs, err |
| } |
| |
| func (p *Process) ConnectionsWithContext(ctx context.Context) ([]net.ConnectionStat, error) { |
| return net.ConnectionsPidWithContext(ctx, "all", p.Pid) |
| } |
| |
| func (p *Process) ConnectionsMaxWithContext(ctx context.Context, maxConn int) ([]net.ConnectionStat, error) { |
| return net.ConnectionsPidMaxWithContext(ctx, "all", p.Pid, maxConn) |
| } |
| |
| func (p *Process) MemoryMapsWithContext(ctx context.Context, grouped bool) (*[]MemoryMapsStat, error) { |
| pid := p.Pid |
| var ret []MemoryMapsStat |
| smapsPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "smaps") |
| if grouped { |
| ret = make([]MemoryMapsStat, 1) |
| // If smaps_rollup exists (require kernel >= 4.15), then we will use it |
| // for pre-summed memory information for a process. |
| smapsRollupPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "smaps_rollup") |
| if _, err := os.Stat(smapsRollupPath); !os.IsNotExist(err) { |
| smapsPath = smapsRollupPath |
| } |
| } |
| contents, err := os.ReadFile(smapsPath) |
| if err != nil { |
| return nil, err |
| } |
| lines := strings.Split(string(contents), "\n") |
| |
| // function of parsing a block |
| getBlock := func(firstLine []string, block []string) (MemoryMapsStat, error) { |
| m := MemoryMapsStat{} |
| if len(firstLine) >= 6 { |
| m.Path = strings.Join(firstLine[5:], " ") |
| } |
| |
| for _, line := range block { |
| if strings.Contains(line, "VmFlags") { |
| continue |
| } |
| field := strings.Split(line, ":") |
| if len(field) < 2 { |
| continue |
| } |
| v := strings.TrimSpace(strings.TrimSuffix(field[1], " kB")) |
| t, err := strconv.ParseUint(v, 10, 64) |
| if err != nil { |
| return m, err |
| } |
| |
| switch field[0] { |
| case "Size": |
| m.Size = t |
| case "Rss": |
| m.Rss = t |
| case "Pss": |
| m.Pss = t |
| case "Shared_Clean": |
| m.SharedClean = t |
| case "Shared_Dirty": |
| m.SharedDirty = t |
| case "Private_Clean": |
| m.PrivateClean = t |
| case "Private_Dirty": |
| m.PrivateDirty = t |
| case "Referenced": |
| m.Referenced = t |
| case "Anonymous": |
| m.Anonymous = t |
| case "Swap": |
| m.Swap = t |
| } |
| } |
| return m, nil |
| } |
| |
| var firstLine []string |
| blocks := make([]string, 0, 16) |
| |
| for i, line := range lines { |
| fields := strings.Fields(line) |
| if (len(fields) > 0 && !strings.HasSuffix(fields[0], ":")) || i == len(lines)-1 { |
| // new block section |
| if len(firstLine) > 0 && len(blocks) > 0 { |
| g, err := getBlock(firstLine, blocks) |
| if err != nil { |
| return &ret, err |
| } |
| if grouped { |
| ret[0].Size += g.Size |
| ret[0].Rss += g.Rss |
| ret[0].Pss += g.Pss |
| ret[0].SharedClean += g.SharedClean |
| ret[0].SharedDirty += g.SharedDirty |
| ret[0].PrivateClean += g.PrivateClean |
| ret[0].PrivateDirty += g.PrivateDirty |
| ret[0].Referenced += g.Referenced |
| ret[0].Anonymous += g.Anonymous |
| ret[0].Swap += g.Swap |
| } else { |
| ret = append(ret, g) |
| } |
| } |
| // starts new block |
| blocks = make([]string, 0, 16) |
| firstLine = fields |
| } else { |
| blocks = append(blocks, line) |
| } |
| } |
| |
| return &ret, nil |
| } |
| |
| func (p *Process) EnvironWithContext(ctx context.Context) ([]string, error) { |
| environPath := common.HostProcWithContext(ctx, strconv.Itoa(int(p.Pid)), "environ") |
| |
| environContent, err := os.ReadFile(environPath) |
| if err != nil { |
| return nil, err |
| } |
| |
| return strings.Split(string(environContent), "\000"), nil |
| } |
| |
| /** |
| ** Internal functions |
| **/ |
| |
| func limitToUint(val string) (uint64, error) { |
| if val == "unlimited" { |
| return math.MaxUint64, nil |
| } |
| res, err := strconv.ParseUint(val, 10, 64) |
| if err != nil { |
| return 0, err |
| } |
| return res, nil |
| } |
| |
| // Get num_fds from /proc/(pid)/limits |
| func (p *Process) fillFromLimitsWithContext(ctx context.Context) ([]RlimitStat, error) { |
| pid := p.Pid |
| limitsFile := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "limits") |
| d, err := os.Open(limitsFile) |
| if err != nil { |
| return nil, err |
| } |
| defer d.Close() |
| |
| var limitStats []RlimitStat |
| |
| limitsScanner := bufio.NewScanner(d) |
| for limitsScanner.Scan() { |
| var statItem RlimitStat |
| |
| str := strings.Fields(limitsScanner.Text()) |
| |
| // Remove the header line |
| if strings.Contains(str[len(str)-1], "Units") { |
| continue |
| } |
| |
| // Assert that last item is a Hard limit |
| statItem.Hard, err = limitToUint(str[len(str)-1]) |
| if err != nil { |
| // On error remove last item and try once again since it can be unit or header line |
| str = str[:len(str)-1] |
| statItem.Hard, err = limitToUint(str[len(str)-1]) |
| if err != nil { |
| return nil, err |
| } |
| } |
| // Remove last item from string |
| str = str[:len(str)-1] |
| |
| // Now last item is a Soft limit |
| statItem.Soft, err = limitToUint(str[len(str)-1]) |
| if err != nil { |
| return nil, err |
| } |
| // Remove last item from string |
| str = str[:len(str)-1] |
| |
| // The rest is a stats name |
| resourceName := strings.Join(str, " ") |
| switch resourceName { |
| case "Max cpu time": |
| statItem.Resource = RLIMIT_CPU |
| case "Max file size": |
| statItem.Resource = RLIMIT_FSIZE |
| case "Max data size": |
| statItem.Resource = RLIMIT_DATA |
| case "Max stack size": |
| statItem.Resource = RLIMIT_STACK |
| case "Max core file size": |
| statItem.Resource = RLIMIT_CORE |
| case "Max resident set": |
| statItem.Resource = RLIMIT_RSS |
| case "Max processes": |
| statItem.Resource = RLIMIT_NPROC |
| case "Max open files": |
| statItem.Resource = RLIMIT_NOFILE |
| case "Max locked memory": |
| statItem.Resource = RLIMIT_MEMLOCK |
| case "Max address space": |
| statItem.Resource = RLIMIT_AS |
| case "Max file locks": |
| statItem.Resource = RLIMIT_LOCKS |
| case "Max pending signals": |
| statItem.Resource = RLIMIT_SIGPENDING |
| case "Max msgqueue size": |
| statItem.Resource = RLIMIT_MSGQUEUE |
| case "Max nice priority": |
| statItem.Resource = RLIMIT_NICE |
| case "Max realtime priority": |
| statItem.Resource = RLIMIT_RTPRIO |
| case "Max realtime timeout": |
| statItem.Resource = RLIMIT_RTTIME |
| default: |
| continue |
| } |
| |
| limitStats = append(limitStats, statItem) |
| } |
| |
| if err := limitsScanner.Err(); err != nil { |
| return nil, err |
| } |
| |
| return limitStats, nil |
| } |
| |
| // Get list of /proc/(pid)/fd files |
| func (p *Process) fillFromfdListWithContext(ctx context.Context) (string, []string, error) { |
| pid := p.Pid |
| statPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "fd") |
| d, err := os.Open(statPath) |
| if err != nil { |
| return statPath, []string{}, err |
| } |
| defer d.Close() |
| fnames, err := d.Readdirnames(-1) |
| return statPath, fnames, err |
| } |
| |
| // Get num_fds from /proc/(pid)/fd |
| func (p *Process) fillFromfdWithContext(ctx context.Context) (int32, []OpenFilesStat, error) { |
| statPath, fnames, err := p.fillFromfdListWithContext(ctx) |
| if err != nil { |
| return 0, nil, err |
| } |
| numFDs := int32(len(fnames)) |
| |
| openfiles := make([]OpenFilesStat, 0, numFDs) |
| for _, fd := range fnames { |
| fpath := filepath.Join(statPath, fd) |
| path, err := common.Readlink(fpath) |
| if err != nil { |
| continue |
| } |
| t, err := strconv.ParseUint(fd, 10, 64) |
| if err != nil { |
| return numFDs, openfiles, err |
| } |
| o := OpenFilesStat{ |
| Path: path, |
| Fd: t, |
| } |
| openfiles = append(openfiles, o) |
| } |
| |
| return numFDs, openfiles, nil |
| } |
| |
| // Get cwd from /proc/(pid)/cwd |
| func (p *Process) fillFromCwdWithContext(ctx context.Context) (string, error) { |
| pid := p.Pid |
| cwdPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "cwd") |
| cwd, err := os.Readlink(cwdPath) |
| if err != nil { |
| return "", err |
| } |
| return string(cwd), nil |
| } |
| |
| // Get exe from /proc/(pid)/exe |
| func (p *Process) fillFromExeWithContext(ctx context.Context) (string, error) { |
| pid := p.Pid |
| exePath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "exe") |
| exe, err := os.Readlink(exePath) |
| if err != nil { |
| return "", err |
| } |
| return string(exe), nil |
| } |
| |
| // Get cmdline from /proc/(pid)/cmdline |
| func (p *Process) fillFromCmdlineWithContext(ctx context.Context) (string, error) { |
| pid := p.Pid |
| cmdPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "cmdline") |
| cmdline, err := os.ReadFile(cmdPath) |
| if err != nil { |
| return "", err |
| } |
| ret := strings.FieldsFunc(string(cmdline), func(r rune) bool { |
| return r == '\u0000' |
| }) |
| |
| return strings.Join(ret, " "), nil |
| } |
| |
| func (p *Process) fillSliceFromCmdlineWithContext(ctx context.Context) ([]string, error) { |
| pid := p.Pid |
| cmdPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "cmdline") |
| cmdline, err := os.ReadFile(cmdPath) |
| if err != nil { |
| return nil, err |
| } |
| if len(cmdline) == 0 { |
| return nil, nil |
| } |
| |
| cmdline = bytes.TrimRight(cmdline, "\x00") |
| |
| parts := bytes.Split(cmdline, []byte{0}) |
| var strParts []string |
| for _, p := range parts { |
| strParts = append(strParts, string(p)) |
| } |
| |
| return strParts, nil |
| } |
| |
| // Get IO status from /proc/(pid)/io |
| func (p *Process) fillFromIOWithContext(ctx context.Context) (*IOCountersStat, error) { |
| pid := p.Pid |
| ioPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "io") |
| ioline, err := os.ReadFile(ioPath) |
| if err != nil { |
| return nil, err |
| } |
| lines := strings.Split(string(ioline), "\n") |
| ret := &IOCountersStat{} |
| |
| for _, line := range lines { |
| field := strings.Fields(line) |
| if len(field) < 2 { |
| continue |
| } |
| t, err := strconv.ParseUint(field[1], 10, 64) |
| if err != nil { |
| return nil, err |
| } |
| param := strings.TrimSuffix(field[0], ":") |
| switch param { |
| case "syscr": |
| ret.ReadCount = t |
| case "syscw": |
| ret.WriteCount = t |
| case "read_bytes": |
| ret.DiskReadBytes = t |
| case "write_bytes": |
| ret.DiskWriteBytes = t |
| case "rchar": |
| ret.ReadBytes = t |
| case "wchar": |
| ret.WriteBytes = t |
| } |
| } |
| |
| return ret, nil |
| } |
| |
| // Get memory info from /proc/(pid)/statm |
| func (p *Process) fillFromStatmWithContext(ctx context.Context) (*MemoryInfoStat, *MemoryInfoExStat, error) { |
| pid := p.Pid |
| memPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "statm") |
| contents, err := os.ReadFile(memPath) |
| if err != nil { |
| return nil, nil, err |
| } |
| fields := strings.Split(string(contents), " ") |
| |
| vms, err := strconv.ParseUint(fields[0], 10, 64) |
| if err != nil { |
| return nil, nil, err |
| } |
| rss, err := strconv.ParseUint(fields[1], 10, 64) |
| if err != nil { |
| return nil, nil, err |
| } |
| memInfo := &MemoryInfoStat{ |
| RSS: rss * pageSize, |
| VMS: vms * pageSize, |
| } |
| |
| shared, err := strconv.ParseUint(fields[2], 10, 64) |
| if err != nil { |
| return nil, nil, err |
| } |
| text, err := strconv.ParseUint(fields[3], 10, 64) |
| if err != nil { |
| return nil, nil, err |
| } |
| lib, err := strconv.ParseUint(fields[4], 10, 64) |
| if err != nil { |
| return nil, nil, err |
| } |
| dirty, err := strconv.ParseUint(fields[5], 10, 64) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| memInfoEx := &MemoryInfoExStat{ |
| RSS: rss * pageSize, |
| VMS: vms * pageSize, |
| Shared: shared * pageSize, |
| Text: text * pageSize, |
| Lib: lib * pageSize, |
| Dirty: dirty * pageSize, |
| } |
| |
| return memInfo, memInfoEx, nil |
| } |
| |
| // Get name from /proc/(pid)/comm or /proc/(pid)/status |
| func (p *Process) fillNameWithContext(ctx context.Context) error { |
| err := p.fillFromCommWithContext(ctx) |
| if err == nil && p.name != "" && len(p.name) < 15 { |
| return nil |
| } |
| return p.fillFromStatusWithContext(ctx) |
| } |
| |
| // Get name from /proc/(pid)/comm |
| func (p *Process) fillFromCommWithContext(ctx context.Context) error { |
| pid := p.Pid |
| statPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "comm") |
| contents, err := os.ReadFile(statPath) |
| if err != nil { |
| return err |
| } |
| |
| p.name = strings.TrimSuffix(string(contents), "\n") |
| return nil |
| } |
| |
| // Get various status from /proc/(pid)/status |
| func (p *Process) fillFromStatus() error { |
| return p.fillFromStatusWithContext(context.Background()) |
| } |
| |
| func (p *Process) fillFromStatusWithContext(ctx context.Context) error { |
| pid := p.Pid |
| statPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "status") |
| contents, err := os.ReadFile(statPath) |
| if err != nil { |
| return err |
| } |
| lines := strings.Split(string(contents), "\n") |
| p.numCtxSwitches = &NumCtxSwitchesStat{} |
| p.memInfo = &MemoryInfoStat{} |
| p.sigInfo = &SignalInfoStat{} |
| for _, line := range lines { |
| tabParts := strings.SplitN(line, "\t", 2) |
| if len(tabParts) < 2 { |
| continue |
| } |
| value := tabParts[1] |
| switch strings.TrimRight(tabParts[0], ":") { |
| case "Name": |
| p.name = strings.Trim(value, " \t") |
| if len(p.name) >= 15 { |
| cmdlineSlice, err := p.CmdlineSliceWithContext(ctx) |
| if err != nil { |
| return err |
| } |
| if len(cmdlineSlice) > 0 { |
| extendedName := filepath.Base(cmdlineSlice[0]) |
| if strings.HasPrefix(extendedName, p.name) { |
| p.name = extendedName |
| } |
| } |
| } |
| // Ensure we have a copy and not reference into slice |
| p.name = string([]byte(p.name)) |
| case "State": |
| p.status = convertStatusChar(value[0:1]) |
| // Ensure we have a copy and not reference into slice |
| p.status = string([]byte(p.status)) |
| case "PPid", "Ppid": |
| pval, err := strconv.ParseInt(value, 10, 32) |
| if err != nil { |
| return err |
| } |
| p.parent = int32(pval) |
| case "Tgid": |
| pval, err := strconv.ParseInt(value, 10, 32) |
| if err != nil { |
| return err |
| } |
| p.tgid = int32(pval) |
| case "Uid": |
| p.uids = make([]uint32, 0, 4) |
| for _, i := range strings.Split(value, "\t") { |
| v, err := strconv.ParseUint(i, 10, 32) |
| if err != nil { |
| return err |
| } |
| p.uids = append(p.uids, uint32(v)) |
| } |
| case "Gid": |
| p.gids = make([]uint32, 0, 4) |
| for _, i := range strings.Split(value, "\t") { |
| v, err := strconv.ParseUint(i, 10, 32) |
| if err != nil { |
| return err |
| } |
| p.gids = append(p.gids, uint32(v)) |
| } |
| case "Groups": |
| groups := strings.Fields(value) |
| p.groups = make([]uint32, 0, len(groups)) |
| for _, i := range groups { |
| v, err := strconv.ParseUint(i, 10, 32) |
| if err != nil { |
| return err |
| } |
| p.groups = append(p.groups, uint32(v)) |
| } |
| case "Threads": |
| v, err := strconv.ParseInt(value, 10, 32) |
| if err != nil { |
| return err |
| } |
| p.numThreads = int32(v) |
| case "voluntary_ctxt_switches": |
| v, err := strconv.ParseInt(value, 10, 64) |
| if err != nil { |
| return err |
| } |
| p.numCtxSwitches.Voluntary = v |
| case "nonvoluntary_ctxt_switches": |
| v, err := strconv.ParseInt(value, 10, 64) |
| if err != nil { |
| return err |
| } |
| p.numCtxSwitches.Involuntary = v |
| case "VmRSS": |
| value = strings.TrimSpace(strings.TrimSuffix(value, " kB")) |
| v, err := strconv.ParseUint(value, 10, 64) |
| if err != nil { |
| return err |
| } |
| p.memInfo.RSS = v * 1024 |
| case "VmSize": |
| value = strings.TrimSpace(strings.TrimSuffix(value, " kB")) |
| v, err := strconv.ParseUint(value, 10, 64) |
| if err != nil { |
| return err |
| } |
| p.memInfo.VMS = v * 1024 |
| case "VmSwap": |
| value = strings.TrimSpace(strings.TrimSuffix(value, " kB")) |
| v, err := strconv.ParseUint(value, 10, 64) |
| if err != nil { |
| return err |
| } |
| p.memInfo.Swap = v * 1024 |
| case "VmHWM": |
| value = strings.TrimSpace(strings.TrimSuffix(value, " kB")) |
| v, err := strconv.ParseUint(value, 10, 64) |
| if err != nil { |
| return err |
| } |
| p.memInfo.HWM = v * 1024 |
| case "VmData": |
| value = strings.TrimSpace(strings.TrimSuffix(value, " kB")) |
| v, err := strconv.ParseUint(value, 10, 64) |
| if err != nil { |
| return err |
| } |
| p.memInfo.Data = v * 1024 |
| case "VmStk": |
| value = strings.TrimSpace(strings.TrimSuffix(value, " kB")) |
| v, err := strconv.ParseUint(value, 10, 64) |
| if err != nil { |
| return err |
| } |
| p.memInfo.Stack = v * 1024 |
| case "VmLck": |
| value = strings.TrimSpace(strings.TrimSuffix(value, " kB")) |
| v, err := strconv.ParseUint(value, 10, 64) |
| if err != nil { |
| return err |
| } |
| p.memInfo.Locked = v * 1024 |
| case "SigPnd": |
| if len(value) > 16 { |
| value = value[len(value)-16:] |
| } |
| v, err := strconv.ParseUint(value, 16, 64) |
| if err != nil { |
| return err |
| } |
| p.sigInfo.PendingThread = v |
| case "ShdPnd": |
| if len(value) > 16 { |
| value = value[len(value)-16:] |
| } |
| v, err := strconv.ParseUint(value, 16, 64) |
| if err != nil { |
| return err |
| } |
| p.sigInfo.PendingProcess = v |
| case "SigBlk": |
| if len(value) > 16 { |
| value = value[len(value)-16:] |
| } |
| v, err := strconv.ParseUint(value, 16, 64) |
| if err != nil { |
| return err |
| } |
| p.sigInfo.Blocked = v |
| case "SigIgn": |
| if len(value) > 16 { |
| value = value[len(value)-16:] |
| } |
| v, err := strconv.ParseUint(value, 16, 64) |
| if err != nil { |
| return err |
| } |
| p.sigInfo.Ignored = v |
| case "SigCgt": |
| if len(value) > 16 { |
| value = value[len(value)-16:] |
| } |
| v, err := strconv.ParseUint(value, 16, 64) |
| if err != nil { |
| return err |
| } |
| p.sigInfo.Caught = v |
| } |
| |
| } |
| return nil |
| } |
| |
| func (p *Process) fillFromTIDStat(tid int32) (uint64, int32, *cpu.TimesStat, int64, uint32, int32, *PageFaultsStat, error) { |
| return p.fillFromTIDStatWithContext(context.Background(), tid) |
| } |
| |
| func (p *Process) fillFromTIDStatWithContext(ctx context.Context, tid int32) (uint64, int32, *cpu.TimesStat, int64, uint32, int32, *PageFaultsStat, error) { |
| pid := p.Pid |
| var statPath string |
| |
| if tid == -1 { |
| statPath = common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "stat") |
| } else { |
| statPath = common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "task", strconv.Itoa(int(tid)), "stat") |
| } |
| |
| contents, err := os.ReadFile(statPath) |
| if err == nil && len(contents) == 0 { |
| err = fmt.Errorf("process %d: stat file is empty, process may have terminated", pid) |
| } |
| if err != nil { |
| return 0, 0, nil, 0, 0, 0, nil, err |
| } |
| // Indexing from one, as described in `man proc` about the file /proc/[pid]/stat |
| fields := splitProcStat(contents) |
| if len(fields) < 23 { |
| return 0, 0, nil, 0, 0, 0, nil, fmt.Errorf("malformed stat file: expected at least 23 fields, got %d", len(fields)) |
| } |
| |
| terminal, err := strconv.ParseUint(fields[7], 10, 64) |
| if err != nil { |
| return 0, 0, nil, 0, 0, 0, nil, err |
| } |
| |
| ppid, err := strconv.ParseInt(fields[4], 10, 32) |
| if err != nil { |
| return 0, 0, nil, 0, 0, 0, nil, err |
| } |
| utime, err := strconv.ParseFloat(fields[14], 64) |
| if err != nil { |
| return 0, 0, nil, 0, 0, 0, nil, err |
| } |
| |
| stime, err := strconv.ParseFloat(fields[15], 64) |
| if err != nil { |
| return 0, 0, nil, 0, 0, 0, nil, err |
| } |
| |
| // There is no such thing as iotime in stat file. As an approximation, we |
| // will use delayacct_blkio_ticks (aggregated block I/O delays, as per Linux |
| // docs). Note: I am assuming at least Linux 2.6.18 |
| var iotime float64 |
| if len(fields) > 42 { |
| iotime, err = strconv.ParseFloat(fields[42], 64) |
| if err != nil { |
| iotime = 0 // Ancient linux version, most likely |
| } |
| } else { |
| iotime = 0 // e.g. SmartOS containers |
| } |
| |
| cpuTimes := &cpu.TimesStat{ |
| CPU: "cpu", |
| User: utime / float64(clockTicks), |
| System: stime / float64(clockTicks), |
| Iowait: iotime / float64(clockTicks), |
| } |
| |
| bootTime, _ := common.BootTimeWithContext(ctx, enableBootTimeCache) |
| t, err := strconv.ParseUint(fields[22], 10, 64) |
| if err != nil { |
| return 0, 0, nil, 0, 0, 0, nil, err |
| } |
| createTime := int64((t * 1000 / uint64(clockTicks)) + uint64(bootTime*1000)) |
| |
| rtpriority, err := strconv.ParseInt(fields[18], 10, 32) |
| if err != nil { |
| return 0, 0, nil, 0, 0, 0, nil, err |
| } |
| if rtpriority < 0 { |
| rtpriority = rtpriority*-1 - 1 |
| } else { |
| rtpriority = 0 |
| } |
| |
| // p.Nice = mustParseInt32(fields[18]) |
| // use syscall instead of parse Stat file |
| snice, _ := unix.Getpriority(prioProcess, int(pid)) |
| nice := int32(snice) // FIXME: is this true? |
| |
| minFault, err := strconv.ParseUint(fields[10], 10, 64) |
| if err != nil { |
| return 0, 0, nil, 0, 0, 0, nil, err |
| } |
| cMinFault, err := strconv.ParseUint(fields[11], 10, 64) |
| if err != nil { |
| return 0, 0, nil, 0, 0, 0, nil, err |
| } |
| majFault, err := strconv.ParseUint(fields[12], 10, 64) |
| if err != nil { |
| return 0, 0, nil, 0, 0, 0, nil, err |
| } |
| cMajFault, err := strconv.ParseUint(fields[13], 10, 64) |
| if err != nil { |
| return 0, 0, nil, 0, 0, 0, nil, err |
| } |
| |
| faults := &PageFaultsStat{ |
| MinorFaults: minFault, |
| MajorFaults: majFault, |
| ChildMinorFaults: cMinFault, |
| ChildMajorFaults: cMajFault, |
| } |
| |
| return terminal, int32(ppid), cpuTimes, createTime, uint32(rtpriority), nice, faults, nil |
| } |
| |
| func (p *Process) fillFromStatWithContext(ctx context.Context) (uint64, int32, *cpu.TimesStat, int64, uint32, int32, *PageFaultsStat, error) { |
| return p.fillFromTIDStatWithContext(ctx, -1) |
| } |
| |
| func pidsWithContext(ctx context.Context) ([]int32, error) { |
| return readPidsFromDir(common.HostProcWithContext(ctx)) |
| } |
| |
| func ProcessesWithContext(ctx context.Context) ([]*Process, error) { |
| out := []*Process{} |
| |
| pids, err := PidsWithContext(ctx) |
| if err != nil { |
| return out, err |
| } |
| |
| for _, pid := range pids { |
| p, err := NewProcessWithContext(ctx, pid) |
| if err != nil { |
| continue |
| } |
| out = append(out, p) |
| } |
| |
| return out, nil |
| } |
| |
| func readPidsFromDir(path string) ([]int32, error) { |
| var ret []int32 |
| |
| d, err := os.Open(path) |
| if err != nil { |
| return nil, err |
| } |
| defer d.Close() |
| |
| fnames, err := d.Readdirnames(-1) |
| if err != nil { |
| return nil, err |
| } |
| for _, fname := range fnames { |
| if !strictIntPtrn.MatchString(fname) { |
| continue |
| } |
| pid, err := strconv.ParseInt(fname, 10, 32) |
| if err != nil { |
| // if not numeric name, just skip |
| continue |
| } |
| ret = append(ret, int32(pid)) |
| } |
| |
| return ret, nil |
| } |
| |
| func splitProcStat(content []byte) []string { |
| nameStart := bytes.IndexByte(content, '(') |
| nameEnd := bytes.LastIndexByte(content, ')') |
| restFields := strings.Fields(string(content[nameEnd+2:])) // +2 skip ') ' |
| name := content[nameStart+1 : nameEnd] |
| pid := strings.TrimSpace(string(content[:nameStart])) |
| fields := make([]string, 3, len(restFields)+3) |
| fields[1] = string(pid) |
| fields[2] = string(name) |
| fields = append(fields, restFields...) |
| return fields |
| } |