169 lines
3.7 KiB
Go
169 lines
3.7 KiB
Go
package budget
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
type task struct {
|
|
Parent uint32
|
|
Group string
|
|
Profile string
|
|
Slice string
|
|
}
|
|
|
|
type procSource interface {
|
|
Snapshot(config) (map[uint32]task, error)
|
|
Group(uint32) (string, error)
|
|
}
|
|
|
|
type procFS struct {
|
|
root string
|
|
uid uint32
|
|
}
|
|
|
|
var pythonName = regexp.MustCompile(`^python[23]?(?:\.\d+)?$`)
|
|
|
|
func gone(err error) bool {
|
|
return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ESRCH)
|
|
}
|
|
|
|
func (c config) sourceGroup(group string) (bool, bool) {
|
|
unit := filepath.Base(group)
|
|
terminal := false
|
|
for _, prefix := range c.Terminals {
|
|
if strings.HasPrefix(unit, prefix) {
|
|
terminal = true
|
|
break
|
|
}
|
|
}
|
|
job := (strings.HasPrefix(unit, "budget-c") || strings.HasPrefix(unit, "budget-b")) && strings.HasSuffix(unit, ".scope")
|
|
return terminal, job
|
|
}
|
|
|
|
func (c config) classify(exe, group string) (string, string) {
|
|
terminal, job := c.sourceGroup(group)
|
|
if !terminal && !job {
|
|
return "", ""
|
|
}
|
|
name := c.Commands[exe]
|
|
slice := ""
|
|
if shared, ok := c.Shared[exe]; ok {
|
|
name, slice = shared, c.sharedSlice()
|
|
}
|
|
if pythonName.MatchString(exe) {
|
|
name = c.Python
|
|
}
|
|
if job && name != "search" && slice == "" {
|
|
name = ""
|
|
}
|
|
return name, slice
|
|
}
|
|
|
|
func readGroup(path string) (string, error) {
|
|
data, err := os.ReadFile(path)
|
|
return strings.TrimPrefix(strings.TrimSpace(string(data)), "0::"), err
|
|
}
|
|
|
|
func (p procFS) Group(pid uint32) (string, error) {
|
|
return readGroup(filepath.Join(p.root, strconv.FormatUint(uint64(pid), 10), "cgroup"))
|
|
}
|
|
|
|
func parseParent(stat string) (uint32, error) {
|
|
i := strings.LastIndex(stat, ") ")
|
|
if i < 0 {
|
|
return 0, fmt.Errorf("invalid proc stat")
|
|
}
|
|
fields := strings.Fields(stat[i+2:])
|
|
if len(fields) < 2 {
|
|
return 0, fmt.Errorf("missing parent in proc stat")
|
|
}
|
|
value, err := strconv.ParseUint(fields[1], 10, 32)
|
|
return uint32(value), err
|
|
}
|
|
|
|
func (p procFS) readTask(path string, c config) (task, bool, error) {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return task{}, false, err
|
|
}
|
|
if info.Sys().(*syscall.Stat_t).Uid != p.uid {
|
|
return task{}, false, nil
|
|
}
|
|
group, err := readGroup(filepath.Join(path, "cgroup"))
|
|
if err != nil {
|
|
return task{}, false, err
|
|
}
|
|
terminal, job := c.sourceGroup(group)
|
|
if !terminal && !job {
|
|
return task{}, false, nil
|
|
}
|
|
exe, err := os.Readlink(filepath.Join(path, "exe"))
|
|
if err != nil {
|
|
return task{}, false, err
|
|
}
|
|
stat, err := os.ReadFile(filepath.Join(path, "stat"))
|
|
if err != nil {
|
|
return task{}, false, err
|
|
}
|
|
parent, err := parseParent(string(stat))
|
|
if err != nil {
|
|
return task{}, false, err
|
|
}
|
|
name, slice := c.classify(filepath.Base(exe), group)
|
|
return task{Parent: parent, Group: group, Profile: name, Slice: slice}, true, nil
|
|
}
|
|
|
|
func (p procFS) Snapshot(c config) (map[uint32]task, error) {
|
|
entries, err := os.ReadDir(p.root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tasks := map[uint32]task{}
|
|
for _, entry := range entries {
|
|
pid, err := strconv.ParseUint(entry.Name(), 10, 32)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
value, include, err := p.readTask(filepath.Join(p.root, entry.Name()), c)
|
|
if gone(err) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if include {
|
|
tasks[uint32(pid)] = value
|
|
}
|
|
}
|
|
return tasks, nil
|
|
}
|
|
|
|
func family(tasks map[uint32]task, pid uint32) []uint32 {
|
|
group := tasks[pid].Group
|
|
seen := map[uint32]bool{pid: true}
|
|
pending := []uint32{pid}
|
|
for len(pending) > 0 {
|
|
parent := pending[0]
|
|
pending = pending[1:]
|
|
for child, value := range tasks {
|
|
if value.Parent == parent && value.Group == group && !seen[child] {
|
|
seen[child] = true
|
|
pending = append(pending, child)
|
|
}
|
|
}
|
|
}
|
|
result := make([]uint32, 0, len(seen))
|
|
for child := range seen {
|
|
result = append(result, child)
|
|
}
|
|
slices.Sort(result)
|
|
return result
|
|
}
|