feat(platform): darwin process inspector via sysctl and proc_pidinfo
- commit
- a83ab491a88beb86a7b43629f34b3374a27245a8
- author
- brice · · 2026-07-16 18:06 UTC
- parent
- bb8d2ba
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | + | } | |||