Skip to content

Commit

Permalink
feat: limited ProcFDUsage support on darwin
Browse files Browse the repository at this point in the history
This adds support for ProcFDUsage on darwin when the requested PID
matches that of the current process.

On MacOS, while you can get the open file descriptors for other
processes (via `proc_pidinfo`), I don't know of a clean way to get the
current rlimit data for another process.

However, for the current process, we can retrieve the rlimits via the
`getrlimit` system call.

I've used /dev/fd to count open files rather than `proc_pidinfo`
because I think the code is a bit more straight forward than the
under-documented `proc_pidinfo` call.
  • Loading branch information
stevendanna committed Nov 9, 2021
1 parent 9d6c926 commit 1fcb209
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 2 deletions.
38 changes: 38 additions & 0 deletions sigar_common_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"encoding/binary"
"fmt"
"io"
"os"
"os/user"
"runtime"
"strconv"
Expand Down Expand Up @@ -354,9 +355,46 @@ func (self *ProcExe) Get(pid int) error {

// Get returns process file usage
func (self *ProcFDUsage) Get(pid int) error {
if pid == os.Getpid() {
return self.GetSelf()
}
return ErrNotImplemented{runtime.GOOS}
}

// GetSelf populates ProcFDUsage for the current process.
func (self *ProcFDUsage) GetSelf() error {
openFDs, err := getOpenFileDescriptors()
if err != nil {
return err
}

var rlim syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlim); err != nil {
return err
}

self.Open = openFDs
self.SoftLimit = rlim.Cur
self.HardLimit = rlim.Max
return nil
}

// getOpenFileDescriptors determines the number of open files for the
// process by counting the number of files in /dev/fd. The returned
// count includes the descriptor for /dev/fd itself.
func getOpenFileDescriptors() (uint64, error) {
const fdDir = "/dev/fd"
f, err := os.Open(fdDir)
if err != nil {
return 0, err
}
defer f.Close()
// Readdirnames doesn't return . and ..
// The first argument is a limit, 0 is unlimited.
names, err := f.Readdirnames(0)
return uint64(len(names)), err
}

// kernProcargs is a wrapper around sysctl KERN_PROCARGS2
// callbacks params are optional,
// up to the caller as to which pieces of data they want
Expand Down
68 changes: 66 additions & 2 deletions sigar_darwin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ package gosigar_test
import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
"os/user"
"runtime"
"strconv"
"strings"
"syscall"
"testing"

"github.com/elastic/gosigar"
sigar "github.com/elastic/gosigar"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

var procinfo map[string]string
Expand Down Expand Up @@ -71,6 +76,65 @@ func TestDarwinProcState(t *testing.T) {
assert.Equal(t, 0, state.Ppid)
}
} else {
fmt.Println("Skipping ProcState test; run as root to test")
t.Skip("Skipping ProcState test; run as root to test")
}
}

func TestDarwinProcFDUsage(t *testing.T) {
t.Run("Get(pid) on current pid returns open file count", func(t *testing.T) {
myPid := os.Getpid()
fdUsage := &sigar.ProcFDUsage{}

require.NoError(t, fdUsage.Get(myPid))
beforeOpen := fdUsage.Open
f, err := ioutil.TempFile(t.TempDir(), "test-open-1")
require.NoError(t, err)
defer f.Close()

require.NoError(t, fdUsage.Get(myPid))
assert.Equal(t, beforeOpen+1, fdUsage.Open, "opening file increases Open count")

require.NoError(t, f.Close())
require.NoError(t, fdUsage.Get(myPid))
assert.Equal(t, beforeOpen, fdUsage.Open, "closing file decreases Open count")
})
t.Run("Get(pid) on current pid returns rlimit", func(t *testing.T) {
myPid := os.Getpid()
fdUsage := &sigar.ProcFDUsage{}
hardLim, softLim := getRlimitViaShell(t)

require.NoError(t, fdUsage.Get(myPid))
assert.Equal(t, hardLim, fdUsage.HardLimit)
assert.Equal(t, softLim, fdUsage.SoftLimit)

})
t.Run("Get(pid) on another process returns an error", func(t *testing.T) {
fdUsage := &sigar.ProcFDUsage{}
otherPid := os.Getpid() + 10
err := fdUsage.Get(otherPid)
require.Error(t, err)
assert.Equal(t, gosigar.ErrNotImplemented{runtime.GOOS}, err)
})
}

func getRlimitViaShell(t *testing.T) (uint64, uint64) {
out, err := exec.Command("/bin/sh", "-c", "ulimit -n -H").Output()
require.NoError(t, err)
hardLimit, err := parseRlimitOutput(string(out))
require.NoError(t, err)

out, err = exec.Command("/bin/sh", "-c", "ulimit -n -S").Output()
require.NoError(t, err)
softLimit, err := parseRlimitOutput(string(out))
require.NoError(t, err)

return hardLimit, softLimit
}

func parseRlimitOutput(output string) (uint64, error) {
output = strings.TrimSpace(output)
if output == "unlimited" {
return syscall.RLIM_INFINITY, nil
}
return strconv.ParseUint(output, 10, 64)
}

0 comments on commit 1fcb209

Please sign in to comment.