diff --git a/README.md b/README.md index ba7b4e76f2..56916777a5 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,7 @@ Some code is ported from Ohai. Many thanks. |terminal |x |x |x | | | |io\_counters |x |x |x | |x | |nice |x |x |x |x |x | -|num\_fds |x | | | |x | +|num\_fds |x | | |x |x | |num\_ctx\_switches |x | | | | | |num\_threads |x |x |x |x |x | |cpu\_times |x | | | |x | diff --git a/internal/common/common_darwin.go b/internal/common/common_darwin.go index 8b756a11d4..d9c7f2d9d7 100644 --- a/internal/common/common_darwin.go +++ b/internal/common/common_darwin.go @@ -256,6 +256,7 @@ const ( const ( MAXPATHLEN = 1024 + PROC_PIDLISTFDS = 1 PROC_PIDPATHINFO_MAXSIZE = 4 * MAXPATHLEN PROC_PIDTASKINFO = 4 PROC_PIDVNODEPATHINFO = 9 diff --git a/process/process_bsd.go b/process/process_bsd.go index e591e2d157..d094d389d4 100644 --- a/process/process_bsd.go +++ b/process/process_bsd.go @@ -36,10 +36,6 @@ func (*Process) NumCtxSwitchesWithContext(_ context.Context) (*NumCtxSwitchesSta return nil, common.ErrNotImplementedError } -func (*Process) NumFDsWithContext(_ context.Context) (int32, error) { - return 0, common.ErrNotImplementedError -} - func (*Process) CPUAffinityWithContext(_ context.Context) ([]int32, error) { return nil, common.ErrNotImplementedError } diff --git a/process/process_darwin.go b/process/process_darwin.go index d0bba15088..d44c7e8e27 100644 --- a/process/process_darwin.go +++ b/process/process_darwin.go @@ -488,3 +488,54 @@ func (p *Process) MemoryInfoWithContext(_ context.Context) (*MemoryInfoStat, err } return ret, nil } + +// procFDInfo represents a file descriptor entry from sys/proc_info.h +type procFDInfo struct { + ProcFd int32 + ProcFdtype uint32 +} + +// NumFDsWithContext returns the number of file descriptors used by the process. +// It uses proc_pidinfo with PROC_PIDLISTFDS to query the kernel for the count +// of open file descriptors. The method makes a single syscall and calculates +// the count from the buffer size returned by the kernel. +func (p *Process) NumFDsWithContext(_ context.Context) (int32, error) { + funcs, err := loadProcFuncs() + if err != nil { + return 0, err + } + defer funcs.Close() + + // First call: get required buffer size + bufferSize := funcs.procPidInfo( + p.Pid, + common.PROC_PIDLISTFDS, + 0, + 0, // NULL buffer + 0, // 0 size + ) + if bufferSize <= 0 { + return 0, fmt.Errorf("unknown error: proc_pidinfo returned %d", bufferSize) + } + + // Allocate buffer of the required size + const sizeofProcFDInfo = int32(unsafe.Sizeof(procFDInfo{})) + numEntries := bufferSize / sizeofProcFDInfo + buf := make([]procFDInfo, numEntries) + + // Second call: get actual data + ret := funcs.procPidInfo( + p.Pid, + common.PROC_PIDLISTFDS, + 0, + uintptr(unsafe.Pointer(&buf[0])), // Real buffer + bufferSize, // Size from first call + ) + if ret <= 0 { + return 0, fmt.Errorf("unknown error: proc_pidinfo returned %d", ret) + } + + // Calculate actual number of FDs returned + numFDs := ret / sizeofProcFDInfo + return numFDs, nil +} diff --git a/process/process_darwin_test.go b/process/process_darwin_test.go new file mode 100644 index 0000000000..e6636c66bd --- /dev/null +++ b/process/process_darwin_test.go @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build darwin + +package process + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNumFDs(t *testing.T) { + pid := os.Getpid() + p, err := NewProcess(int32(pid)) + require.NoError(t, err) + + ctx := context.Background() + + before, err := p.NumFDsWithContext(ctx) + require.NoError(t, err) + + // Open files to increase FD count + f1, err := os.Open("/dev/null") + require.NoError(t, err) + defer f1.Close() + + f2, err := os.Open("/dev/null") + require.NoError(t, err) + defer f2.Close() + + after, err := p.NumFDsWithContext(ctx) + require.NoError(t, err) + + assert.GreaterOrEqual(t, after, before+2) +} + +func TestNumFDs_NonExistent(t *testing.T) { + p := &Process{Pid: 99999} + _, err := p.NumFDsWithContext(context.Background()) + assert.Error(t, err) +} + +func BenchmarkNumFDs(b *testing.B) { + pid := int32(os.Getpid()) + p, err := NewProcess(pid) + if err != nil { + b.Skip(err) + } + + ctx := context.Background() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := p.NumFDsWithContext(ctx) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/process/process_freebsd.go b/process/process_freebsd.go index ae173ff1de..283af9bb3f 100644 --- a/process/process_freebsd.go +++ b/process/process_freebsd.go @@ -344,6 +344,10 @@ func ProcessesWithContext(ctx context.Context) ([]*Process, error) { return results, nil } +func (*Process) NumFDsWithContext(_ context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + func (p *Process) getKProc() (*KinfoProc, error) { mib := []int32{CTLKern, KernProc, KernProcPID, p.Pid} diff --git a/process/process_openbsd.go b/process/process_openbsd.go index 11bc5c18d2..31fdb85bc9 100644 --- a/process/process_openbsd.go +++ b/process/process_openbsd.go @@ -342,6 +342,10 @@ func ProcessesWithContext(ctx context.Context) ([]*Process, error) { return results, nil } +func (*Process) NumFDsWithContext(_ context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + func (p *Process) getKProc() (*KinfoProc, error) { buf, length, err := callKernProcSyscall(KernProcPID, p.Pid) if err != nil {