detached a83ab49

feat(platform): darwin process inspector via sysctl and proc_pidinfo

commit
a83ab491a88beb86a7b43629f34b3374a27245a8
author
brice · · 2026-07-16 18:06 UTC
parent
bb8d2ba
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +420 -0
A internal/platform/process_darwin_arm64.go +374 -0
@@ -0,0 +1,374 @@
1+//go:build darwin && arm64
2+
3+package platform
4+
5+import (
6+ "bytes"
7+ "fmt"
8+ "runtime"
9+ "sync"
10+ "syscall"
11+ "time"
12+ "unsafe"
13+)
14+
15+const (
16+ ctlKern = 1
17+ kernProc = 14
18+ kernProcAll = 0
19+ kernProcPID = 1
20+ kernProcArgs2 = 49
21+ procInfoCallPIDInfo = 2
22+ procPIDVnodePathInfo = 9
23+ darwinKinfoProcSize = 0x288
24+ darwinVnodePathInfoLen = 2352
25+)
26+
27+// ProcessInspector implements ports.ProcessInspector using Darwin sysctl and
28+// proc_pidinfo interfaces.
29+type ProcessInspector struct {
30+ mu sync.Mutex
31+ cachedRecords []processRecord
32+ cachedRecordsUntil time.Time
33+}
34+
35+// NewProcessInspector returns a Darwin process inspector.
36+func NewProcessInspector() *ProcessInspector { return &ProcessInspector{} }
37+
38+func (p *ProcessInspector) Cwd(pid int) (string, error) { return ProcessCwd(pid) }
39+func (p *ProcessInspector) Comm(pid int) (string, error) { return ProcessComm(pid) }
40+func (p *ProcessInspector) Argv(pid int) ([]string, error) { return ProcessArgv(pid) }
41+func (p *ProcessInspector) GroupArgv(pgid int, shellPid int) ([]string, error) {
42+ recs, err := p.processRecords()
43+ if err != nil {
44+ return nil, err
45+ }
46+ pid, ok := selectProcessGroupPID(recs, pgid, shellPid)
47+ if !ok {
48+ return nil, nil
49+ }
50+ return p.Argv(pid)
51+}
52+
53+func (p *ProcessInspector) processRecords() ([]processRecord, error) {
54+ p.mu.Lock()
55+ defer p.mu.Unlock()
56+ now := time.Now()
57+ if now.Before(p.cachedRecordsUntil) {
58+ return p.cachedRecords, nil
59+ }
60+ recs, err := readDarwinProcessRecords()
61+ if err != nil {
62+ return nil, err
63+ }
64+ p.cachedRecords = recs
65+ p.cachedRecordsUntil = now.Add(processRecordCacheTTL)
66+ return recs, nil
67+}
68+
69+// ProcessCwd returns the current working directory from proc_pidinfo.
70+func ProcessCwd(pid int) (string, error) {
71+ if pid <= 0 {
72+ return "", fmt.Errorf("process cwd: invalid pid %d", pid)
73+ }
74+ var info procVnodePathInfo
75+ n, _, errno := syscall.Syscall6(
76+ syscall.SYS_PROC_INFO,
77+ procInfoCallPIDInfo,
78+ uintptr(pid),
79+ procPIDVnodePathInfo,
80+ 0,
81+ uintptr(unsafe.Pointer(&info)),
82+ unsafe.Sizeof(info),
83+ )
84+ runtime.KeepAlive(&info)
85+ if errno != 0 {
86+ return "", fmt.Errorf("process cwd: proc_pidinfo pid %d: %w", pid, errno)
87+ }
88+ if n != unsafe.Sizeof(info) {
89+ return "", fmt.Errorf("process cwd: proc_pidinfo pid %d returned %d bytes; want %d", pid, n, unsafe.Sizeof(info))
90+ }
91+ path := info.Cdir.Path[:]
92+ if end := bytes.IndexByte(path, 0); end >= 0 {
93+ path = path[:end]
94+ }
95+ if len(path) == 0 {
96+ return "", fmt.Errorf("process cwd: empty cwd for pid %d", pid)
97+ }
98+ return string(path), nil
99+}
100+
101+// ProcessComm returns the p_comm field from KERN_PROC_PID data.
102+func ProcessComm(pid int) (string, error) {
103+ if pid <= 0 {
104+ return "", fmt.Errorf("process comm: invalid pid %d", pid)
105+ }
106+ data, err := rawSysctl([]int32{ctlKern, kernProc, kernProcPID, int32(pid)})
107+ if err != nil {
108+ return "", fmt.Errorf("process comm: sysctl pid %d: %w", pid, err)
109+ }
110+ if len(data) != darwinKinfoProcSize {
111+ return "", fmt.Errorf("process comm: sysctl pid %d returned %d bytes; want %d", pid, len(data), darwinKinfoProcSize)
112+ }
113+ proc := *(*kinfoProc)(unsafe.Pointer(&data[0]))
114+ runtime.KeepAlive(data)
115+ comm := proc.Proc.Comm[:]
116+ if end := bytes.IndexByte(comm, 0); end >= 0 {
117+ comm = comm[:end]
118+ }
119+ if len(comm) == 0 {
120+ return "", fmt.Errorf("process comm: empty comm for pid %d", pid)
121+ }
122+ return string(comm), nil
123+}
124+
125+// ProcessArgv returns argv from KERN_PROCARGS2.
126+func ProcessArgv(pid int) ([]string, error) {
127+ if pid <= 0 {
128+ return nil, fmt.Errorf("process argv: invalid pid %d", pid)
129+ }
130+ data, err := rawSysctl([]int32{ctlKern, kernProcArgs2, int32(pid)})
131+ if err != nil {
132+ return nil, fmt.Errorf("process argv: sysctl pid %d: %w", pid, err)
133+ }
134+ argv, err := parseKernProcArgs2(data)
135+ if err != nil {
136+ return nil, fmt.Errorf("process argv: pid %d: %w", pid, err)
137+ }
138+ return argv, nil
139+}
140+
141+// ProcessGroupArgv returns argv for a restorable foreground process in pgid.
142+// A bare pane shell (the only process in its foreground group) returns nil argv.
143+func ProcessGroupArgv(pgid int, shellPid int) ([]string, error) {
144+ return NewProcessInspector().GroupArgv(pgid, shellPid)
145+}
146+
147+func readDarwinProcessRecords() ([]processRecord, error) {
148+ data, err := rawSysctl([]int32{ctlKern, kernProc, kernProcAll})
149+ if err != nil {
150+ return nil, err
151+ }
152+ if len(data)%darwinKinfoProcSize != 0 {
153+ return nil, fmt.Errorf("KERN_PROC_ALL returned %d bytes, not a multiple of %d", len(data), darwinKinfoProcSize)
154+ }
155+ recs := make([]processRecord, 0, len(data)/darwinKinfoProcSize)
156+ for len(data) > 0 {
157+ proc := (*kinfoProc)(unsafe.Pointer(&data[0]))
158+ recs = append(recs, processRecord{pid: int(proc.Proc.Pid), pgrp: int(proc.Eproc.Pgid)})
159+ data = data[darwinKinfoProcSize:]
160+ }
161+ runtime.KeepAlive(data)
162+ return recs, nil
163+}
164+
165+// rawSysctl reads a binary sysctl MIB, retrying if a growing result races the
166+// size query. It is the stdlib syscall equivalent of sysctl(3).
167+func rawSysctl(mib []int32) ([]byte, error) {
168+ for {
169+ var size uintptr
170+ if err := sysctl(mib, nil, &size); err != nil {
171+ return nil, err
172+ }
173+ if size == 0 {
174+ return nil, nil
175+ }
176+ buf := make([]byte, size)
177+ n := size
178+ if err := sysctl(mib, &buf[0], &n); err != nil {
179+ if err == syscall.ENOMEM {
180+ continue
181+ }
182+ return nil, err
183+ }
184+ return buf[:n], nil
185+ }
186+}
187+
188+func sysctl(mib []int32, old *byte, oldlen *uintptr) error {
189+ _, _, errno := syscall.Syscall6(
190+ syscall.SYS___SYSCTL,
191+ uintptr(unsafe.Pointer(&mib[0])),
192+ uintptr(len(mib)),
193+ uintptr(unsafe.Pointer(old)),
194+ uintptr(unsafe.Pointer(oldlen)),
195+ 0,
196+ 0,
197+ )
198+ runtime.KeepAlive(mib)
199+ if errno != 0 {
200+ return errno
201+ }
202+ return nil
203+}
204+
205+// The following Darwin/arm64 ABI layouts are derived from Apple XNU
206+// bsd/sys/proc.h and bsd/sys/proc_info.h, via golang.org/x/sys/unix
207+// ztypes_darwin_arm64.go (BSD-3-Clause). Pointer fields are uintptr so this
208+// package has no dependency on x/sys and never lets kernel pointers escape.
209+type kinfoProc struct {
210+ Proc externProc
211+ Eproc eproc
212+}
213+
214+type eproc struct {
215+ Paddr uintptr
216+ Sess uintptr
217+ Pcred pcred
218+ Ucred ucred
219+ Vm vmspace
220+ Ppid int32
221+ Pgid int32
222+ Jobc int16
223+ Tdev int32
224+ Tpgid int32
225+ Tsess uintptr
226+ Wmesg [8]byte
227+ Xsize int32
228+ Xrssize int16
229+ Xccount int16
230+ Xswrss int16
231+ Flag int32
232+ Login [12]byte
233+ Spare [4]int32
234+ _ [4]byte
235+}
236+
237+type externProc struct {
238+ Un [16]byte
239+ Vmspace uintptr
240+ Sigacts uintptr
241+ _ [3]byte
242+ Flag int32
243+ Stat int8
244+ Pid int32
245+ Oppid int32
246+ Dupfd int32
247+ _ [4]byte
248+ UserStack uintptr
249+ ExitThread uintptr
250+ Debugger int32
251+ Sigwait int32
252+ Estcpu uint32
253+ Cpticks int32
254+ Pctcpu uint32
255+ _ [4]byte
256+ Wchan uintptr
257+ Wmesg uintptr
258+ Swtime uint32
259+ Slptime uint32
260+ Realtimer itimerval
261+ Rtime timeval
262+ Uticks uint64
263+ Sticks uint64
264+ Iticks uint64
265+ Traceflag int32
266+ _ [4]byte
267+ Tracep uintptr
268+ Siglist int32
269+ _ [4]byte
270+ Textvp uintptr
271+ Holdcnt int32
272+ Sigmask uint32
273+ Sigignore uint32
274+ Sigcatch uint32
275+ Priority uint8
276+ Usrpri uint8
277+ Nice int8
278+ Comm [17]byte
279+ _ [4]byte
280+ Pgrp uintptr
281+ Addr uintptr
282+ Xstat uint16
283+ Acflag uint16
284+ _ [4]byte
285+ Ru uintptr
286+}
287+
288+type itimerval struct {
289+ Interval timeval
290+ Value timeval
291+}
292+
293+type timeval struct {
294+ Sec int64
295+ Usec int32
296+ _ [4]byte
297+}
298+
299+type pcred struct {
300+ Lock [72]int8
301+ Ucred uintptr
302+ Ruid uint32
303+ Svuid uint32
304+ Rgid uint32
305+ Svgid uint32
306+ Refcnt int32
307+ _ [4]byte
308+}
309+
310+type ucred struct {
311+ Ref int32
312+ UID uint32
313+ Ngroups int16
314+ Groups [16]uint32
315+}
316+
317+type vmspace struct {
318+ Dummy int32
319+ Dummy2 uintptr
320+ Dummy3 [5]int32
321+ Dummy4 [3]uintptr
322+}
323+
324+type vinfoStat struct {
325+ Dev uint32
326+ Mode uint16
327+ Nlink uint16
328+ Ino uint64
329+ UID uint32
330+ GID uint32
331+ Atime int64
332+ Atimensec int64
333+ Mtime int64
334+ Mtimensec int64
335+ Ctime int64
336+ Ctimensec int64
337+ Birthtime int64
338+ Birthtimensec int64
339+ Size int64
340+ Blocks int64
341+ Blksize int32
342+ Flags uint32
343+ Gen uint32
344+ Rdev uint32
345+ Qspare [2]int64
346+}
347+
348+type fsid struct{ Val [2]int32 }
349+
350+type vnodeInfo struct {
351+ Stat vinfoStat
352+ Type int32
353+ Pad int32
354+ Fsid fsid
355+}
356+
357+type vnodeInfoPath struct {
358+ Vi vnodeInfo
359+ Path [1024]byte
360+}
361+
362+type procVnodePathInfo struct {
363+ Cdir vnodeInfoPath
364+ Rdir vnodeInfoPath
365+}
366+
367+func init() {
368+ if unsafe.Sizeof(kinfoProc{}) != darwinKinfoProcSize {
369+ panic(fmt.Sprintf("darwin kinfo_proc ABI size = %d; want %d", unsafe.Sizeof(kinfoProc{}), darwinKinfoProcSize))
370+ }
371+ if unsafe.Sizeof(procVnodePathInfo{}) != darwinVnodePathInfoLen {
372+ panic(fmt.Sprintf("darwin proc_vnodepathinfo ABI size = %d; want %d", unsafe.Sizeof(procVnodePathInfo{}), darwinVnodePathInfoLen))
373+ }
374+}
@@ -0,0 +1,374 @@
1+//go:build darwin && arm64
2+
3+package platform
4+
5+import (
6+ "bytes"
7+ "fmt"
8+ "runtime"
9+ "sync"
10+ "syscall"
11+ "time"
12+ "unsafe"
13+)
14+
15+const (
16+ ctlKern = 1
17+ kernProc = 14
18+ kernProcAll = 0
19+ kernProcPID = 1
20+ kernProcArgs2 = 49
21+ procInfoCallPIDInfo = 2
22+ procPIDVnodePathInfo = 9
23+ darwinKinfoProcSize = 0x288
24+ darwinVnodePathInfoLen = 2352
25+)
26+
27+// ProcessInspector implements ports.ProcessInspector using Darwin sysctl and
28+// proc_pidinfo interfaces.
29+type ProcessInspector struct {
30+ mu sync.Mutex
31+ cachedRecords []processRecord
32+ cachedRecordsUntil time.Time
33+}
34+
35+// NewProcessInspector returns a Darwin process inspector.
36+func NewProcessInspector() *ProcessInspector { return &ProcessInspector{} }
37+
38+func (p *ProcessInspector) Cwd(pid int) (string, error) { return ProcessCwd(pid) }
39+func (p *ProcessInspector) Comm(pid int) (string, error) { return ProcessComm(pid) }
40+func (p *ProcessInspector) Argv(pid int) ([]string, error) { return ProcessArgv(pid) }
41+func (p *ProcessInspector) GroupArgv(pgid int, shellPid int) ([]string, error) {
42+ recs, err := p.processRecords()
43+ if err != nil {
44+ return nil, err
45+ }
46+ pid, ok := selectProcessGroupPID(recs, pgid, shellPid)
47+ if !ok {
48+ return nil, nil
49+ }
50+ return p.Argv(pid)
51+}
52+
53+func (p *ProcessInspector) processRecords() ([]processRecord, error) {
54+ p.mu.Lock()
55+ defer p.mu.Unlock()
56+ now := time.Now()
57+ if now.Before(p.cachedRecordsUntil) {
58+ return p.cachedRecords, nil
59+ }
60+ recs, err := readDarwinProcessRecords()
61+ if err != nil {
62+ return nil, err
63+ }
64+ p.cachedRecords = recs
65+ p.cachedRecordsUntil = now.Add(processRecordCacheTTL)
66+ return recs, nil
67+}
68+
69+// ProcessCwd returns the current working directory from proc_pidinfo.
70+func ProcessCwd(pid int) (string, error) {
71+ if pid <= 0 {
72+ return "", fmt.Errorf("process cwd: invalid pid %d", pid)
73+ }
74+ var info procVnodePathInfo
75+ n, _, errno := syscall.Syscall6(
76+ syscall.SYS_PROC_INFO,
77+ procInfoCallPIDInfo,
78+ uintptr(pid),
79+ procPIDVnodePathInfo,
80+ 0,
81+ uintptr(unsafe.Pointer(&info)),
82+ unsafe.Sizeof(info),
83+ )
84+ runtime.KeepAlive(&info)
85+ if errno != 0 {
86+ return "", fmt.Errorf("process cwd: proc_pidinfo pid %d: %w", pid, errno)
87+ }
88+ if n != unsafe.Sizeof(info) {
89+ return "", fmt.Errorf("process cwd: proc_pidinfo pid %d returned %d bytes; want %d", pid, n, unsafe.Sizeof(info))
90+ }
91+ path := info.Cdir.Path[:]
92+ if end := bytes.IndexByte(path, 0); end >= 0 {
93+ path = path[:end]
94+ }
95+ if len(path) == 0 {
96+ return "", fmt.Errorf("process cwd: empty cwd for pid %d", pid)
97+ }
98+ return string(path), nil
99+}
100+
101+// ProcessComm returns the p_comm field from KERN_PROC_PID data.
102+func ProcessComm(pid int) (string, error) {
103+ if pid <= 0 {
104+ return "", fmt.Errorf("process comm: invalid pid %d", pid)
105+ }
106+ data, err := rawSysctl([]int32{ctlKern, kernProc, kernProcPID, int32(pid)})
107+ if err != nil {
108+ return "", fmt.Errorf("process comm: sysctl pid %d: %w", pid, err)
109+ }
110+ if len(data) != darwinKinfoProcSize {
111+ return "", fmt.Errorf("process comm: sysctl pid %d returned %d bytes; want %d", pid, len(data), darwinKinfoProcSize)
112+ }
113+ proc := *(*kinfoProc)(unsafe.Pointer(&data[0]))
114+ runtime.KeepAlive(data)
115+ comm := proc.Proc.Comm[:]
116+ if end := bytes.IndexByte(comm, 0); end >= 0 {
117+ comm = comm[:end]
118+ }
119+ if len(comm) == 0 {
120+ return "", fmt.Errorf("process comm: empty comm for pid %d", pid)
121+ }
122+ return string(comm), nil
123+}
124+
125+// ProcessArgv returns argv from KERN_PROCARGS2.
126+func ProcessArgv(pid int) ([]string, error) {
127+ if pid <= 0 {
128+ return nil, fmt.Errorf("process argv: invalid pid %d", pid)
129+ }
130+ data, err := rawSysctl([]int32{ctlKern, kernProcArgs2, int32(pid)})
131+ if err != nil {
132+ return nil, fmt.Errorf("process argv: sysctl pid %d: %w", pid, err)
133+ }
134+ argv, err := parseKernProcArgs2(data)
135+ if err != nil {
136+ return nil, fmt.Errorf("process argv: pid %d: %w", pid, err)
137+ }
138+ return argv, nil
139+}
140+
141+// ProcessGroupArgv returns argv for a restorable foreground process in pgid.
142+// A bare pane shell (the only process in its foreground group) returns nil argv.
143+func ProcessGroupArgv(pgid int, shellPid int) ([]string, error) {
144+ return NewProcessInspector().GroupArgv(pgid, shellPid)
145+}
146+
147+func readDarwinProcessRecords() ([]processRecord, error) {
148+ data, err := rawSysctl([]int32{ctlKern, kernProc, kernProcAll})
149+ if err != nil {
150+ return nil, err
151+ }
152+ if len(data)%darwinKinfoProcSize != 0 {
153+ return nil, fmt.Errorf("KERN_PROC_ALL returned %d bytes, not a multiple of %d", len(data), darwinKinfoProcSize)
154+ }
155+ recs := make([]processRecord, 0, len(data)/darwinKinfoProcSize)
156+ for len(data) > 0 {
157+ proc := (*kinfoProc)(unsafe.Pointer(&data[0]))
158+ recs = append(recs, processRecord{pid: int(proc.Proc.Pid), pgrp: int(proc.Eproc.Pgid)})
159+ data = data[darwinKinfoProcSize:]
160+ }
161+ runtime.KeepAlive(data)
162+ return recs, nil
163+}
164+
165+// rawSysctl reads a binary sysctl MIB, retrying if a growing result races the
166+// size query. It is the stdlib syscall equivalent of sysctl(3).
167+func rawSysctl(mib []int32) ([]byte, error) {
168+ for {
169+ var size uintptr
170+ if err := sysctl(mib, nil, &size); err != nil {
171+ return nil, err
172+ }
173+ if size == 0 {
174+ return nil, nil
175+ }
176+ buf := make([]byte, size)
177+ n := size
178+ if err := sysctl(mib, &buf[0], &n); err != nil {
179+ if err == syscall.ENOMEM {
180+ continue
181+ }
182+ return nil, err
183+ }
184+ return buf[:n], nil
185+ }
186+}
187+
188+func sysctl(mib []int32, old *byte, oldlen *uintptr) error {
189+ _, _, errno := syscall.Syscall6(
190+ syscall.SYS___SYSCTL,
191+ uintptr(unsafe.Pointer(&mib[0])),
192+ uintptr(len(mib)),
193+ uintptr(unsafe.Pointer(old)),
194+ uintptr(unsafe.Pointer(oldlen)),
195+ 0,
196+ 0,
197+ )
198+ runtime.KeepAlive(mib)
199+ if errno != 0 {
200+ return errno
201+ }
202+ return nil
203+}
204+
205+// The following Darwin/arm64 ABI layouts are derived from Apple XNU
206+// bsd/sys/proc.h and bsd/sys/proc_info.h, via golang.org/x/sys/unix
207+// ztypes_darwin_arm64.go (BSD-3-Clause). Pointer fields are uintptr so this
208+// package has no dependency on x/sys and never lets kernel pointers escape.
209+type kinfoProc struct {
210+ Proc externProc
211+ Eproc eproc
212+}
213+
214+type eproc struct {
215+ Paddr uintptr
216+ Sess uintptr
217+ Pcred pcred
218+ Ucred ucred
219+ Vm vmspace
220+ Ppid int32
221+ Pgid int32
222+ Jobc int16
223+ Tdev int32
224+ Tpgid int32
225+ Tsess uintptr
226+ Wmesg [8]byte
227+ Xsize int32
228+ Xrssize int16
229+ Xccount int16
230+ Xswrss int16
231+ Flag int32
232+ Login [12]byte
233+ Spare [4]int32
234+ _ [4]byte
235+}
236+
237+type externProc struct {
238+ Un [16]byte
239+ Vmspace uintptr
240+ Sigacts uintptr
241+ _ [3]byte
242+ Flag int32
243+ Stat int8
244+ Pid int32
245+ Oppid int32
246+ Dupfd int32
247+ _ [4]byte
248+ UserStack uintptr
249+ ExitThread uintptr
250+ Debugger int32
251+ Sigwait int32
252+ Estcpu uint32
253+ Cpticks int32
254+ Pctcpu uint32
255+ _ [4]byte
256+ Wchan uintptr
257+ Wmesg uintptr
258+ Swtime uint32
259+ Slptime uint32
260+ Realtimer itimerval
261+ Rtime timeval
262+ Uticks uint64
263+ Sticks uint64
264+ Iticks uint64
265+ Traceflag int32
266+ _ [4]byte
267+ Tracep uintptr
268+ Siglist int32
269+ _ [4]byte
270+ Textvp uintptr
271+ Holdcnt int32
272+ Sigmask uint32
273+ Sigignore uint32
274+ Sigcatch uint32
275+ Priority uint8
276+ Usrpri uint8
277+ Nice int8
278+ Comm [17]byte
279+ _ [4]byte
280+ Pgrp uintptr
281+ Addr uintptr
282+ Xstat uint16
283+ Acflag uint16
284+ _ [4]byte
285+ Ru uintptr
286+}
287+
288+type itimerval struct {
289+ Interval timeval
290+ Value timeval
291+}
292+
293+type timeval struct {
294+ Sec int64
295+ Usec int32
296+ _ [4]byte
297+}
298+
299+type pcred struct {
300+ Lock [72]int8
301+ Ucred uintptr
302+ Ruid uint32
303+ Svuid uint32
304+ Rgid uint32
305+ Svgid uint32
306+ Refcnt int32
307+ _ [4]byte
308+}
309+
310+type ucred struct {
311+ Ref int32
312+ UID uint32
313+ Ngroups int16
314+ Groups [16]uint32
315+}
316+
317+type vmspace struct {
318+ Dummy int32
319+ Dummy2 uintptr
320+ Dummy3 [5]int32
321+ Dummy4 [3]uintptr
322+}
323+
324+type vinfoStat struct {
325+ Dev uint32
326+ Mode uint16
327+ Nlink uint16
328+ Ino uint64
329+ UID uint32
330+ GID uint32
331+ Atime int64
332+ Atimensec int64
333+ Mtime int64
334+ Mtimensec int64
335+ Ctime int64
336+ Ctimensec int64
337+ Birthtime int64
338+ Birthtimensec int64
339+ Size int64
340+ Blocks int64
341+ Blksize int32
342+ Flags uint32
343+ Gen uint32
344+ Rdev uint32
345+ Qspare [2]int64
346+}
347+
348+type fsid struct{ Val [2]int32 }
349+
350+type vnodeInfo struct {
351+ Stat vinfoStat
352+ Type int32
353+ Pad int32
354+ Fsid fsid
355+}
356+
357+type vnodeInfoPath struct {
358+ Vi vnodeInfo
359+ Path [1024]byte
360+}
361+
362+type procVnodePathInfo struct {
363+ Cdir vnodeInfoPath
364+ Rdir vnodeInfoPath
365+}
366+
367+func init() {
368+ if unsafe.Sizeof(kinfoProc{}) != darwinKinfoProcSize {
369+ panic(fmt.Sprintf("darwin kinfo_proc ABI size = %d; want %d", unsafe.Sizeof(kinfoProc{}), darwinKinfoProcSize))
370+ }
371+ if unsafe.Sizeof(procVnodePathInfo{}) != darwinVnodePathInfoLen {
372+ panic(fmt.Sprintf("darwin proc_vnodepathinfo ABI size = %d; want %d", unsafe.Sizeof(procVnodePathInfo{}), darwinVnodePathInfoLen))
373+ }
374+}
A internal/platform/process_darwin_arm64_test.go +46 -0
@@ -0,0 +1,46 @@
1+//go:build darwin && arm64
2+
3+package platform
4+
5+import (
6+ "os"
7+ "syscall"
8+ "testing"
9+)
10+
11+func TestDarwinProcessInspectorCurrentProcess(t *testing.T) {
12+ inspector := NewProcessInspector()
13+ pid := os.Getpid()
14+
15+ cwd, err := inspector.Cwd(pid)
16+ if err != nil {
17+ t.Fatalf("Cwd(%d): %v", pid, err)
18+ }
19+ if cwd == "" {
20+ t.Fatal("Cwd returned an empty path")
21+ }
22+
23+ comm, err := inspector.Comm(pid)
24+ if err != nil {
25+ t.Fatalf("Comm(%d): %v", pid, err)
26+ }
27+ if comm == "" {
28+ t.Fatal("Comm returned an empty command")
29+ }
30+
31+ argv, err := inspector.Argv(pid)
32+ if err != nil {
33+ t.Fatalf("Argv(%d): %v", pid, err)
34+ }
35+ if len(argv) == 0 {
36+ t.Fatal("Argv returned no arguments")
37+ }
38+
39+ groupArgv, err := inspector.GroupArgv(syscall.Getpgrp(), -1)
40+ if err != nil {
41+ t.Fatalf("GroupArgv(current group): %v", err)
42+ }
43+ if len(groupArgv) == 0 {
44+ t.Fatal("GroupArgv returned no arguments for the current process group")
45+ }
46+}
@@ -0,0 +1,46 @@
1+//go:build darwin && arm64
2+
3+package platform
4+
5+import (
6+ "os"
7+ "syscall"
8+ "testing"
9+)
10+
11+func TestDarwinProcessInspectorCurrentProcess(t *testing.T) {
12+ inspector := NewProcessInspector()
13+ pid := os.Getpid()
14+
15+ cwd, err := inspector.Cwd(pid)
16+ if err != nil {
17+ t.Fatalf("Cwd(%d): %v", pid, err)
18+ }
19+ if cwd == "" {
20+ t.Fatal("Cwd returned an empty path")
21+ }
22+
23+ comm, err := inspector.Comm(pid)
24+ if err != nil {
25+ t.Fatalf("Comm(%d): %v", pid, err)
26+ }
27+ if comm == "" {
28+ t.Fatal("Comm returned an empty command")
29+ }
30+
31+ argv, err := inspector.Argv(pid)
32+ if err != nil {
33+ t.Fatalf("Argv(%d): %v", pid, err)
34+ }
35+ if len(argv) == 0 {
36+ t.Fatal("Argv returned no arguments")
37+ }
38+
39+ groupArgv, err := inspector.GroupArgv(syscall.Getpgrp(), -1)
40+ if err != nil {
41+ t.Fatalf("GroupArgv(current group): %v", err)
42+ }
43+ if len(groupArgv) == 0 {
44+ t.Fatal("GroupArgv returned no arguments for the current process group")
45+ }
46+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close