//go:build darwin package proc import ( "os/exec" "slices" "strconv" "strings" "github.com/pranshuparmar/witr/pkg/model" ) // GetFileContext returns file descriptor and lock info for a process func GetFileContext(pid int) *model.FileContext { ctx := &model.FileContext{} // Get open file count openFiles, fileLimit := getOpenFileCount(pid) ctx.OpenFiles = openFiles ctx.FileLimit = fileLimit // Get locked files ctx.LockedFiles = getLockedFiles(pid) // Only return if we have meaningful data to show // Show if: high file usage (>50% of limit) or has locks if len(ctx.LockedFiles) > 0 { return ctx } if ctx.FileLimit > 0 && ctx.OpenFiles > 0 { usagePercent := float64(ctx.OpenFiles) / float64(ctx.FileLimit) * 100 if usagePercent > 50 { return ctx } } return nil } // getOpenFileCount returns the number of open files and the limit for a process func getOpenFileCount(pid int) (int, int) { // Use lsof to count open files // lsof -p returns all open files. lsof may exit non-zero when one // of the process's FDs is inaccessible while still emitting valid data // on stdout; salvage stdout when present. out, err := exec.Command("lsof", "-p", strconv.Itoa(pid)).Output() if err != nil && len(out) == 0 { return 0, 0 } // Count lines (subtract 1 for header) openFiles := 0 for line := range strings.Lines(string(out)) { if strings.TrimSpace(line) != "" { openFiles++ } } if openFiles > 0 { openFiles-- // Subtract header line } // Get file limit using launchctl or ulimit fileLimit := getFileLimit(pid) return openFiles, fileLimit } // getFileLimit returns the file descriptor limit for a process func getFileLimit(pid int) int { // Try to get per-process limit // On macOS, we can use launchctl limit or check /proc equivalent // Default to common macOS limits // Try launchctl limit (system-wide soft limit) out, err := exec.Command("launchctl", "limit", "maxfiles").Output() if err == nil { // Format: maxfiles 256 unlimited fields := strings.Fields(string(out)) if len(fields) >= 2 { limit, err := strconv.Atoi(fields[1]) if err == nil { return limit } } } // Default macOS limit return 256 } // getLockedFiles returns files with locks held by the process func getLockedFiles(pid int) []string { var locked []string // Use lsof to find locked files // -p for specific process // Look for lock indicators in the output // lsof may exit non-zero when one of the process's FDs is inaccessible // while still emitting valid data on stdout; salvage stdout when present. out, err := exec.Command("lsof", "-p", strconv.Itoa(pid), "-F", "fn").Output() if err != nil && len(out) == 0 { return locked } // Parse lsof -F output // f = file descriptor info // n = file name var currentFD string for line := range strings.Lines(string(out)) { if len(line) == 0 { continue } switch line[0] { case 'f': currentFD = strings.TrimSpace(line[1:]) case 'n': fileName := strings.TrimSpace(line[1:]) // Check if this FD indicates a lock // Common lock indicators: .lock files, fcntl locks shown with 'l' type if strings.HasSuffix(fileName, ".lock") || strings.HasSuffix(fileName, ".pid") || strings.Contains(fileName, "/lock") { if !slices.Contains(locked, fileName) { locked = append(locked, fileName) } } _ = currentFD // Used for future lock type detection } } // Also check for actual fcntl/flock locks using lsof -F with lock info out2, err := exec.Command("lsof", "-p", strconv.Itoa(pid)).Output() if err == nil || len(out2) > 0 { for line := range strings.Lines(string(out2)) { fields := strings.Fields(line) // Look for lock type indicators (varies by lsof version) // Typically shows "r" for read lock, "w" for write lock, "R" for read lock on entire file if len(fields) >= 5 { lockType := fields[4] if lockType == "r" || lockType == "w" || lockType == "R" || lockType == "W" { // This file has a lock if len(fields) >= 9 { fileName := fields[8] if !slices.Contains(locked, fileName) { locked = append(locked, fileName) } } } } } } return locked }