summaryrefslogtreecommitdiff
path: root/internal/tui/pidpicker/proclist.go
blob: 63306e976a8ee865a8eb111652b40f2a7eb44813 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
package pidpicker

import (
	"bytes"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
)

// ProcessInfo is the metadata shown in the PID picker list.
type ProcessInfo struct {
	Pid     int
	Comm    string
	Cmdline string
}

// ScanProcesses returns process metadata from /proc.
func ScanProcesses() ([]ProcessInfo, error) {
	return scanProcessesFrom("/proc")
}

func scanProcessesFrom(procRoot string) ([]ProcessInfo, error) {
	entries, err := os.ReadDir(procRoot)
	if err != nil {
		return nil, fmt.Errorf("read proc root %q: %w", procRoot, err)
	}

	processes := make([]ProcessInfo, 0, len(entries))
	for _, entry := range entries {
		process, ok := readProcessInfo(procRoot, entry)
		if !ok {
			continue
		}
		processes = append(processes, process)
	}

	sort.Slice(processes, func(i, j int) bool {
		return processes[i].Pid < processes[j].Pid
	})
	return processes, nil
}

func readProcessInfo(procRoot string, entry fs.DirEntry) (ProcessInfo, bool) {
	if !entry.IsDir() {
		return ProcessInfo{}, false
	}

	pid, err := strconv.Atoi(entry.Name())
	if err != nil {
		return ProcessInfo{}, false
	}

	statPath := filepath.Join(procRoot, entry.Name(), "stat")
	statData, err := os.ReadFile(statPath)
	if err != nil {
		return ProcessInfo{}, false
	}

	comm, err := parseCommFromStat(string(statData))
	if err != nil {
		return ProcessInfo{}, false
	}

	cmdlinePath := filepath.Join(procRoot, entry.Name(), "cmdline")
	cmdlineData, err := os.ReadFile(cmdlinePath)
	if err != nil {
		cmdlineData = nil
	}

	return ProcessInfo{
		Pid:     pid,
		Comm:    comm,
		Cmdline: normalizeCmdline(cmdlineData),
	}, true
}

func parseCommFromStat(statLine string) (string, error) {
	open := strings.IndexByte(statLine, '(')
	close := strings.LastIndexByte(statLine, ')')
	if open < 0 || close < 0 || close <= open+1 {
		return "", fmt.Errorf("invalid stat line")
	}

	comm := statLine[open+1 : close]
	if strings.TrimSpace(comm) == "" {
		return "", fmt.Errorf("empty comm in stat line")
	}
	return comm, nil
}

func normalizeCmdline(raw []byte) string {
	if len(raw) == 0 {
		return ""
	}

	trimmed := bytes.TrimRight(raw, "\x00")
	if len(trimmed) == 0 {
		return ""
	}

	parts := bytes.Split(trimmed, []byte{0})
	out := make([]string, 0, len(parts))
	for _, part := range parts {
		if len(part) == 0 {
			continue
		}
		out = append(out, string(part))
	}
	return strings.Join(out, " ")
}