1package platform
2
3import (
4 "bytes"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strconv"
9 "strings"
10 "sync"
11 "time"
12)
13
14// ProcessInspector implements ports.ProcessInspector using Linux /proc.
15type ProcessInspector struct {
16 root string
17
18 mu sync.Mutex
19 cachedRecords []processRecord
20 cachedRecordsUntil time.Time
21}
22
23// NewProcessInspector returns a Linux /proc-backed process inspector.
24func NewProcessInspector() *ProcessInspector { return newProcessInspector("/proc") }
25
26func newProcessInspector(root string) *ProcessInspector { return &ProcessInspector{root: root} }
27
28func (p *ProcessInspector) Cwd(pid int) (string, error) {
29 return processCwd(p.root, pid)
30}
31func (p *ProcessInspector) Comm(pid int) (string, error) {
32 return processComm(p.root, pid)
33}
34func (p *ProcessInspector) Argv(pid int) ([]string, error) {
35 return processArgv(filepath.Join(p.root, strconv.Itoa(pid), "cmdline"), pid)
36}
37func (p *ProcessInspector) GroupArgv(pgid int, shellPid int) ([]string, error) {
38 recs, err := p.processRecords()
39 if err != nil {
40 return nil, err
41 }
42 pid, ok := selectProcessGroupPID(recs, pgid, shellPid)
43 if !ok {
44 return nil, nil
45 }
46 return p.Argv(pid)
47}
48
49func (p *ProcessInspector) processRecords() ([]processRecord, error) {
50 p.mu.Lock()
51 defer p.mu.Unlock()
52 now := time.Now()
53 if now.Before(p.cachedRecordsUntil) {
54 return p.cachedRecords, nil
55 }
56 recs, err := readProcRecords(p.root)
57 if err != nil {
58 return nil, err
59 }
60 p.cachedRecords = recs
61 p.cachedRecordsUntil = now.Add(processRecordCacheTTL)
62 return recs, nil
63}
64
65// ProcessArgv returns argv from /proc/<pid>/cmdline.
66func ProcessArgv(pid int) ([]string, error) {
67 return processArgv(filepath.Join("/proc", strconv.Itoa(pid), "cmdline"), pid)
68}
69
70func processArgv(path string, pid int) ([]string, error) {
71 data, err := os.ReadFile(path)
72 if err != nil {
73 return nil, err
74 }
75 data = bytes.TrimRight(data, "\x00")
76 if len(data) == 0 {
77 return nil, fmt.Errorf("process argv: empty argv for pid %d", pid)
78 }
79 parts := bytes.Split(data, []byte{0})
80 argv := make([]string, len(parts))
81 for i, part := range parts {
82 argv[i] = string(part)
83 }
84 return argv, nil
85}
86
87// ProcessGroupArgv returns argv for a restorable foreground process in pgid.
88// A bare pane shell (the only process in its foreground group) returns nil argv.
89func ProcessGroupArgv(pgid int, shellPid int) ([]string, error) {
90 recs, err := readProcRecords("/proc")
91 if err != nil {
92 return nil, err
93 }
94 pid, ok := selectProcessGroupPID(recs, pgid, shellPid)
95 if !ok {
96 return nil, nil
97 }
98 return ProcessArgv(pid)
99}
100
101func readProcRecords(root string) ([]processRecord, error) {
102 ents, err := os.ReadDir(root)
103 if err != nil {
104 return nil, err
105 }
106 var recs []processRecord
107 for _, ent := range ents {
108 pid, err := strconv.Atoi(ent.Name())
109 if err != nil {
110 continue
111 }
112 data, err := os.ReadFile(filepath.Join(root, ent.Name(), "stat"))
113 if err != nil {
114 continue
115 }
116 pgrp, err := parseStatPgrp(string(data))
117 if err != nil {
118 continue
119 }
120 recs = append(recs, processRecord{pid: pid, pgrp: pgrp})
121 }
122 return recs, nil
123}
124
125func parseStatPgrp(stat string) (int, error) {
126 end := strings.LastIndex(stat, ")")
127 if end < 0 || end+2 >= len(stat) {
128 return 0, fmt.Errorf("invalid stat")
129 }
130 fields := strings.Fields(stat[end+2:])
131 if len(fields) < 3 {
132 return 0, fmt.Errorf("invalid stat")
133 }
134 return strconv.Atoi(fields[2])
135}