Skip to content

Commit

Permalink
Use combined method for darwin process lookups on darwin
Browse files Browse the repository at this point in the history
This is so we can get parent pid and processes that have been deleted while
running (on darwin).
  • Loading branch information
gabriel committed May 23, 2016
1 parent 080aff6 commit 44bcd9b
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 45 deletions.
3 changes: 2 additions & 1 deletion process.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ type Process interface {
// executable.
Executable() string

// Path is full path to the executable.
// Path is full path to the executable. The path may be unavailable if the
// exectuable was deleted from the system while it was still running.
Path() (string, error)
}

Expand Down
67 changes: 50 additions & 17 deletions process_darwin.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,67 @@
#include <strings.h>
#include <libproc.h>
#include <unistd.h>
#include <sys/sysctl.h>

// This is declared in process_darwin.go
extern void goDarwinAppendProc(pid_t, pid_t, char *);
extern void goDarwinSetPath(pid_t, char *);

// Loads the process table and calls the exported Go function to insert
// the data back into the Go space.
// darwinProcesses loads the process table and calls the exported Go function to
// insert the data back into the Go space.
//
// This function is implemented in C because while it would technically
// be possible to do this all in Go, I didn't want to go spelunking through
// header files to get all the structures properly. It is much easier to just
// call it in C and be done with it.
int darwinProcesses() {
int err = 0;
int i = 0;
static const int name[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0 };
size_t length = 0;
struct kinfo_proc *result = NULL;
size_t resultCount = 0;

void darwinProcesses() {
// Get the length first
err = sysctl((int*)name, (sizeof(name) / sizeof(*name)) - 1,
NULL, &length, NULL, 0);
if (err != 0) {
goto ERREXIT;
}

// Allocate the appropriate sized buffer to read the process list
result = malloc(length);

// Call sysctl again with our buffer to fill it with the process list
err = sysctl((int*)name, (sizeof(name) / sizeof(*name)) - 1,
result, &length,
NULL, 0);
if (err != 0) {
goto ERREXIT;
}

resultCount = length / sizeof(struct kinfo_proc);
for (i = 0; i < resultCount; i++) {
struct kinfo_proc *single = &result[i];
goDarwinAppendProc(
single->kp_proc.p_pid,
single->kp_eproc.e_ppid,
single->kp_proc.p_comm);
}

ERREXIT:
if (result != NULL) {
free(result);
}

uid_t euid = geteuid();
if (err != 0) {
return errno;
}
return 0;
}

// darwinProcessPaths looks up paths for process pids
void darwinProcessPaths() {
int pid_buf_size = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0);
int pid_count = pid_buf_size / sizeof(pid_t);

Expand All @@ -31,23 +76,11 @@ void darwinProcesses() {
proc_listpids(PROC_ALL_PIDS, 0, pids, pid_buf_size);
char path_buffer[PROC_PIDPATHINFO_MAXSIZE];

int ppid = 0;

for (int i=0; i < pid_count; i++) {
if (pids[i] == 0) break;

if (euid == 0) {
// You need root permission to get proc_bsdinfo from some processes.
// When you call following function with normal user permission you will
// receive 'operation not permitted' error and it will be terminated.
struct proc_bsdinfo bsdinfo;
proc_pidinfo(pids[i], PROC_PIDTBSDINFO, 0, &bsdinfo, sizeof(struct proc_bsdinfo));
ppid = bsdinfo.pbi_ppid;
}

bzero(path_buffer, PROC_PIDPATHINFO_MAXSIZE);
if (proc_pidpath(pids[i], path_buffer, sizeof(path_buffer)) > 0) {
goDarwinAppendProc(pids[i], ppid, path_buffer);
goDarwinSetPath(pids[i], path_buffer);
}
}
free(pids);
Expand Down
29 changes: 23 additions & 6 deletions process_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,27 @@ package ps
#include <stdio.h>
#include <errno.h>
#include <libproc.h>
extern void darwinProcesses();
extern int darwinProcesses();
extern void darwinProcessPaths();
*/
import "C"

import (
"fmt"
"path/filepath"
"sync"
)

// This lock is what verifies that C calling back into Go is only
// modifying data once at a time.
var darwinLock sync.Mutex
var darwinProcs []Process
var darwinProcsByPID map[int]*DarwinProcess

// DarwinProcess is process definition for OS X
type DarwinProcess struct {
pid int
ppid int
name string
path string
}

Expand All @@ -40,7 +42,7 @@ func (p *DarwinProcess) PPid() int {

// Executable returns process executable name
func (p *DarwinProcess) Executable() string {
return filepath.Base(p.path)
return p.name
}

// Path returns path to process executable
Expand All @@ -53,9 +55,17 @@ func goDarwinAppendProc(pid C.pid_t, ppid C.pid_t, comm *C.char) {
proc := &DarwinProcess{
pid: int(pid),
ppid: int(ppid),
path: C.GoString(comm),
name: C.GoString(comm),
}
darwinProcs = append(darwinProcs, proc)
darwinProcsByPID[proc.pid] = proc
}

//export goDarwinSetPath
func goDarwinSetPath(pid C.pid_t, comm *C.char) {
if proc, ok := darwinProcsByPID[int(pid)]; ok && proc != nil {
proc.path = C.GoString(comm)
}
}

func findProcess(pid int) (Process, error) {
Expand All @@ -81,11 +91,18 @@ func processes() ([]Process, error) {
darwinLock.Lock()
defer darwinLock.Unlock()
darwinProcs = make([]Process, 0, 50)
darwinProcsByPID = make(map[int]*DarwinProcess)

// To ignore deadcode warning for goDarwinAppendProc
// To ignore deadcode warnings for exported functions
_ = goDarwinAppendProc
_ = goDarwinSetPath

_, err := C.darwinProcesses()
if err != nil {
return nil, err
}

C.darwinProcesses()
C.darwinProcessPaths()

return darwinProcs, nil
}
27 changes: 13 additions & 14 deletions process_darwin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestFindProcessDarwin(t *testing.T) {
testFindProcess(t, "go-ps.test")
proc := testFindProcess(t, "go-ps.test")
assert.True(t, proc.PPid() > 0)
}

func TestProcessesDarwin(t *testing.T) {
Expand All @@ -27,17 +29,14 @@ func TestProcessesDarwinError(t *testing.T) {
assert.EqualError(t, err, "Error listing processes: oops")
}

func TestProcessExecRun(t *testing.T) {
testExecRun(t)
func TestProcessExecRemoved(t *testing.T) {
procPath, cmd, proc := testExecRun(t)
defer cleanup(cmd, procPath)
t.Logf("Ran with PID: %d", cmd.Process.Pid)
// Remove it while it is running
_ = os.Remove(procPath)
matchPath := func(p Process) bool { return p.Pid() == proc.Pid() }
procs, err := findProcessesWithFn(processes, matchPath, 1)
require.NoError(t, err)
t.Logf("Proc: %#v", procs[0])
}

// TODO: Figure out how to get processes that have been removed
// func TestProcessExecRemoved(t *testing.T) {
// procPath, proc := testExecRun(t)
// // Remove it while it is running
// os.Remove(procPath)
// matchPath := func(p Process) bool { return p.Pid() == proc.Pid() }
// procs, err := findProcessesWithFn(processes, matchPath, 1)
// require.NoError(t, err)
// t.Logf("Proc: %#v", procs[0])
// }
10 changes: 7 additions & 3 deletions process_nix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import (
"github.com/stretchr/testify/require"
)

func TestProcessExecRun(t *testing.T) {
procPath, cmd, _ := testExecRun(t)
defer cleanup(cmd, procPath)
}

func testProcessPath(t *testing.T, procPath string) Process {
matchPath := func(p Process) bool {
return matchPath(t, p, procPath)
Expand Down Expand Up @@ -45,14 +50,13 @@ func testExecPath(t *testing.T) string {
return procPath
}

func testExecRun(t *testing.T) (string, Process) {
func testExecRun(t *testing.T) (string, *exec.Cmd, Process) {
procPath := testExecPath(t)
cmd := exec.Command(procPath, "10")
defer cleanup(cmd, procPath)
err := cmd.Start()
require.NoError(t, err)
proc := testProcessPath(t, procPath)
return procPath, proc
return procPath, cmd, proc
}

func copyFile(sourcePath string, destinationPath string, mode os.FileMode) error {
Expand Down
4 changes: 0 additions & 4 deletions process_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,3 @@ import "testing"
func TestUnixProcess(t *testing.T) {
var _ Process = new(UnixProcess)
}

func TestProcessExecRun(t *testing.T) {
testExecRun(t)
}

0 comments on commit 44bcd9b

Please sign in to comment.