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
157 lines
4.1 KiB
Go
157 lines
4.1 KiB
Go
//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 <pid> 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 <pid> 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
|
|
}
|