Files
wehub-resource-sync 36b3af2e3d
PR Check / Code Quality: Format (push) Failing after 1s
PR Check / Code Quality: Lint (darwin) (push) Failing after 0s
PR Check / Code Quality: Lint (freebsd) (push) Failing after 1s
PR Check / Code Quality: Lint (windows) (push) Failing after 1s
PR Check / Code Quality: Lint (linux) (push) Failing after 1s
PR Check / Security: Vulnerability Scan (push) Failing after 0s
Update Documentation / update-docs (push) Failing after 2s
PR Check / Code Quality: Vendor (push) Failing after 1s
PR Check / Code Quality: Coverage (push) Failing after 0s
PR Check / Tests: Unit (macos-latest) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04-arm) (push) Has been cancelled
PR Check / Tests: Unit (windows-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:06 +08:00

280 lines
7.2 KiB
Go

//go:build darwin
package proc
import (
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"time"
"github.com/pranshuparmar/witr/pkg/model"
)
func ReadProcess(pid int) (model.Process, error) {
if pid <= 0 {
return model.Process{}, fmt.Errorf("invalid pid %d", pid)
}
pidStr := strconv.Itoa(pid)
// Format: pid(0) ppid(1) uid(2) lstart(3-7) state(8) pcpu(9) rss(10) args(11+)
// ucomm is excluded because it can contain spaces (e.g. "Microsoft Teams"),
// which breaks strings.Fields parsing. The display name is derived from args instead.
cmd := exec.Command("ps", "-p", pidStr, "-o", "pid=,ppid=,uid=,lstart=,state=,pcpu=,rss=,args=")
cmd.Env = buildEnvForPS()
out, err := cmd.Output()
if err != nil {
return model.Process{}, fmt.Errorf("process %d not found: %w", pid, err)
}
line := strings.TrimSpace(string(out))
if line == "" {
return model.Process{}, fmt.Errorf("process %d not found", pid)
}
fields := strings.Fields(line)
if len(fields) < 11 {
return model.Process{}, fmt.Errorf("unexpected ps output format for pid %d", pid)
}
ppid, _ := strconv.Atoi(fields[1])
uid, _ := strconv.Atoi(fields[2])
lstartStr := strings.Join(fields[3:8], " ")
startedAt, _ := time.Parse("Mon Jan 2 15:04:05 2006", lstartStr)
if startedAt.IsZero() {
startedAt = time.Now().UTC()
}
state := fields[8]
cpuPct, _ := strconv.ParseFloat(fields[9], 64)
rssKB, _ := strconv.ParseFloat(fields[10], 64)
rawCmdline := ""
if len(fields) > 11 {
rawCmdline = strings.Join(fields[11:], " ")
}
cmdline := rawCmdline
env := getEnvironment(pid)
cwd, binPath := getCwdAndBinaryPath(pid)
health := "healthy"
var forked string
switch state {
case "Z":
health = "zombie"
case "T":
health = "stopped"
}
if health == "healthy" && cpuPct > 90 {
health = "high-cpu"
}
rssMB := rssKB / 1024
if health == "healthy" && rssMB > 1024 {
health = "high-mem"
}
memBytes := uint64(rssKB * 1024)
memPercent := 0.0
if total := totalMemoryBytes(); total > 0 {
memPercent = float64(memBytes) / float64(total) * 100.0
}
// Display name resolution order:
// 1. filepath.Base(binPath) — binPath comes from `lsof ftxt` as a single
// unsplit line, so spaces in .app bundle paths are preserved.
// 2. `ps -p <pid> -o comm=` on its own line — also preserves spaces.
// 3. extractExecutableName(rawCmdline) — last resort. `ps -o args=` joins
// argv with single spaces and does not quote paths, so this can
// truncate names when the executable path contains spaces (issue #201).
displayName := binaryBasename(binPath)
if displayName == "" {
if commOut, commErr := exec.Command("ps", "-p", pidStr, "-o", "comm=").Output(); commErr == nil {
displayName = binaryBasename(string(commOut))
}
}
if displayName == "" {
displayName = extractExecutableName(rawCmdline)
}
if cmdline == "" {
cmdline = displayName
}
if ppid != 1 && displayName != "launchd" {
forked = "forked"
} else {
forked = "not-forked"
}
user := readUserByUID(uid)
container := detectContainerFromCmdline(cmdline)
if displayName == "docker-proxy" && container == "" {
container = resolveDockerProxyContainer(cmdline)
}
service := detectLaunchdService(pid)
gitRepo, gitBranch := detectGitInfo(cwd)
procSockets := socketsForPID(pid)
exeDeleted := false
if binPath != "" {
_, statErr := os.Stat(binPath)
exeDeleted = os.IsNotExist(statErr)
}
return model.Process{
PID: pid,
PPID: ppid,
Command: displayName,
Cmdline: cmdline,
StartedAt: startedAt,
User: user,
CPUPercent: cpuPct,
MemoryRSS: memBytes,
MemoryPercent: memPercent,
WorkingDir: cwd,
GitRepo: gitRepo,
GitBranch: gitBranch,
Container: container,
Service: service,
Sockets: procSockets,
Health: health,
Forked: forked,
Env: env,
ExeDeleted: exeDeleted,
}, nil
}
var (
totalMemOnce sync.Once
totalMemBytes uint64
)
// totalMemoryBytes returns total physical RAM in bytes via sysctl hw.memsize,
// or 0 if it can't be read. The value is constant for the machine, so it is
// resolved once rather than spawning sysctl on every ancestry hop.
func totalMemoryBytes() uint64 {
totalMemOnce.Do(func() {
out, err := exec.Command("sysctl", "-n", "hw.memsize").Output()
if err != nil {
return
}
totalMemBytes, _ = strconv.ParseUint(strings.TrimSpace(string(out)), 10, 64)
})
return totalMemBytes
}
// getCwdAndBinaryPath returns the working directory and executable path for a process.
func getCwdAndBinaryPath(pid int) (cwd string, binPath string) {
cwd = "unknown"
// lsof may exit non-zero when one of the requested FDs (e.g., txt for a
// deleted/inaccessible binary) is unavailable, but still emit valid cwd
// data on stdout. Salvage stdout when present instead of bailing out.
out, err := exec.Command("lsof", "-a", "-p", strconv.Itoa(pid), "-d", "cwd,txt", "-F", "fn").Output()
if err != nil && len(out) == 0 {
return cwd, ""
}
// lsof -F fn output has lines like:
// p<pid>
// fcwd
// n/path/to/cwd
// ftxt
// n/path/to/binary
currentFD := ""
for line := range strings.Lines(string(out)) {
if len(line) < 2 {
continue
}
switch line[0] {
case 'f':
currentFD = strings.TrimSpace(line[1:])
case 'n':
path := strings.TrimSpace(line[1:])
switch currentFD {
case "cwd":
cwd = path
case "txt":
if binPath == "" {
binPath = path
}
}
}
}
return cwd, binPath
}
func getEnvironment(pid int) []string {
var env []string
// On macOS, getting environment of another process requires elevated privileges
// or using the proc_pidinfo syscall. For simplicity, we use ps -E when available
// Note: This might not work for all processes due to SIP restrictions
out, err := exec.Command("ps", "-p", strconv.Itoa(pid), "-E", "-o", "command=").Output()
if err != nil {
return env
}
// The -E output appends environment to the command
// This is a simplified approach; full env parsing would need libproc
output := string(out)
// Look for common environment variable patterns
for _, part := range strings.Fields(output) {
if strings.Contains(part, "=") && !strings.HasPrefix(part, "-") {
// Basic validation - should look like VAR=value
eqIdx := strings.Index(part, "=")
if eqIdx > 0 {
varName := part[:eqIdx]
// Check if it looks like an env var name (uppercase or common patterns)
if isEnvVarName(varName) {
env = append(env, part)
}
}
}
}
return env
}
func isEnvVarName(name string) bool {
if len(name) == 0 {
return false
}
// Common env var patterns
for _, c := range name {
if !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') {
return false
}
}
return true
}
func detectLaunchdService(pid int) string {
// Try to find the launchd service managing this process
// Use launchctl blame on macOS 10.10+
out, err := exec.Command("launchctl", "blame", strconv.Itoa(pid)).Output()
if err == nil {
blame := strings.TrimSpace(string(out))
if blame != "" && !strings.Contains(blame, "unknown") {
return blame
}
}
// Fallback: check if process is a known launchd service
// by looking at the parent chain or service database
return ""
}