Streamlined procfile reading code to reduce number of required syscalls. This makes it easier to catch short lived processes.

This commit is contained in:
Karim Kanso
2020-03-10 15:48:56 +00:00
parent a8b29b4527
commit d09e302cbf
3 changed files with 324 additions and 180 deletions

View File

@@ -62,6 +62,7 @@ var triggerInterval int
var colored bool var colored bool
var debug bool var debug bool
var ppid bool var ppid bool
var cmdLength int
func init() { func init() {
rootCmd.PersistentFlags().BoolVarP(&logPS, "procevents", "p", true, "print new processes to stdout") rootCmd.PersistentFlags().BoolVarP(&logPS, "procevents", "p", true, "print new processes to stdout")
@@ -72,6 +73,7 @@ func init() {
rootCmd.PersistentFlags().BoolVarP(&colored, "color", "c", true, "color the printed events") rootCmd.PersistentFlags().BoolVarP(&colored, "color", "c", true, "color the printed events")
rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "", false, "print detailed error messages") rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "", false, "print detailed error messages")
rootCmd.PersistentFlags().BoolVarP(&ppid, "ppid", "", false, "record process ppids") rootCmd.PersistentFlags().BoolVarP(&ppid, "ppid", "", false, "record process ppids")
rootCmd.PersistentFlags().IntVarP(&cmdLength, "truncate", "t", 2048, "truncate process cmds longer than this")
log.SetOutput(os.Stdout) log.SetOutput(os.Stdout)
} }
@@ -93,7 +95,7 @@ func root(cmd *cobra.Command, args []string) {
fsw := fswatcher.NewFSWatcher() fsw := fswatcher.NewFSWatcher()
defer fsw.Close() defer fsw.Close()
pss := psscanner.NewPSScanner(ppid) pss := psscanner.NewPSScanner(ppid, cmdLength)
sigCh := make(chan os.Signal) sigCh := make(chan os.Signal)
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)

View File

@@ -2,7 +2,8 @@ package psscanner
import ( import (
"fmt" "fmt"
"io/ioutil" "io"
"os"
"strconv" "strconv"
"strings" "strings"
) )
@@ -10,6 +11,7 @@ import (
type PSScanner struct { type PSScanner struct {
enablePpid bool enablePpid bool
eventCh chan<- PSEvent eventCh chan<- PSEvent
maxCmdLength int
} }
type PSEvent struct { type PSEvent struct {
@@ -33,10 +35,11 @@ func (evt PSEvent) String() string {
"UID=%-5s PID=%-6d PPID=%-6d | %s", uid, evt.PID, evt.PPID, evt.CMD) "UID=%-5s PID=%-6d PPID=%-6d | %s", uid, evt.PID, evt.PPID, evt.CMD)
} }
func NewPSScanner(ppid bool) *PSScanner { func NewPSScanner(ppid bool, cmdLength int) *PSScanner {
return &PSScanner{ return &PSScanner{
enablePpid: ppid, enablePpid: ppid,
eventCh: nil, eventCh: nil,
maxCmdLength: cmdLength,
} }
} }
@@ -57,8 +60,8 @@ func (p *PSScanner) Run(triggerCh chan struct{}) (chan PSEvent, chan error) {
func (p *PSScanner) processNewPid(pid int) { func (p *PSScanner) processNewPid(pid int) {
// quickly load data into memory before processing it, with preferance for cmd // quickly load data into memory before processing it, with preferance for cmd
cmdLine, errCmdLine := cmdLineReader(pid) cmdLine, errCmdLine := readFile(fmt.Sprintf("/proc/%d/cmdline", pid), p.maxCmdLength)
status, errStatus := procStatusReader(pid) status, errStatus := readFile(fmt.Sprintf("/proc/%d/status", pid), 512)
cmd := "???" // process probably terminated cmd := "???" // process probably terminated
if errCmdLine == nil { if errCmdLine == nil {
@@ -114,12 +117,22 @@ func (p *PSScanner) parseProcessStatus(status []byte) (int, int, error) {
return uid, ppid, nil return uid, ppid, nil
} }
var procStatusReader = func(pid int) ([]byte, error) { var open func(string) (io.ReadCloser, error) = func(s string) (io.ReadCloser, error) {
statPath := fmt.Sprintf("/proc/%d/status", pid) return os.Open(s)
return ioutil.ReadFile(statPath)
} }
var cmdLineReader = func(pid int) ([]byte, error) { // no nonsense file reading
cmdPath := fmt.Sprintf("/proc/%d/cmdline", pid) func readFile(filename string, maxlen int) ([]byte, error) {
return ioutil.ReadFile(cmdPath) file, err := open(filename)
if err != nil {
return nil, err
}
defer file.Close()
buffer := make([]byte, maxlen)
n, err := file.Read(buffer)
if err != io.EOF && err != nil {
return nil, err
}
return buffer[:n], nil
} }

View File

@@ -3,6 +3,8 @@ package psscanner
import ( import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt"
"io"
"reflect" "reflect"
"testing" "testing"
"time" "time"
@@ -12,22 +14,30 @@ const timeout = 100 * time.Millisecond
func TestRun(t *testing.T) { func TestRun(t *testing.T) {
tests := []struct { tests := []struct {
name string
pids []int pids []int
events []string events []string
}{ }{
{pids: []int{1, 2, 3}, events: []string{ {
name: "nominal",
pids: []int{1, 2, 3},
events: []string{
"UID=??? PID=3 | the-command", "UID=??? PID=3 | the-command",
"UID=??? PID=2 | the-command", "UID=??? PID=2 | the-command",
"UID=??? PID=1 | the-command", "UID=??? PID=1 | the-command",
}}, },
},
} }
for _, tt := range tests { for _, tt := range tests {
restoreGetPIDs := mockPidList(tt.pids) t.Run(tt.name, func(t *testing.T) {
restoreCmdLineReader := mockCmdLineReader([]byte("the-command"), nil) defer mockPidList(tt.pids)()
restoreProcStatusReader := mockProcStatusReader([]byte(""), nil) // don't mock read value since it's not worth it for _, pid := range tt.pids {
defer mockPidCmdLine(pid, []byte("the-command"), nil, nil, t)()
defer mockPidStatus(pid, []byte{}, nil, nil, t)() // don't mock read value since it's not worth it
}
pss := NewPSScanner(false) pss := NewPSScanner(false, 2048)
triggerCh := make(chan struct{}) triggerCh := make(chan struct{})
eventCh, errCh := pss.Run(triggerCh) eventCh, errCh := pss.Run(triggerCh)
@@ -56,10 +66,7 @@ func TestRun(t *testing.T) {
t.Errorf("Received unexpected error: %v", err) t.Errorf("Received unexpected error: %v", err)
} }
} }
})
restoreProcStatusReader()
restoreCmdLineReader()
restoreGetPIDs()
} }
} }
@@ -123,21 +130,27 @@ func TestProcessNewPid(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
enablePpid bool enablePpid bool
truncate int
pid int pid int
cmdLine []byte cmdLine []byte
cmdLineErr error cmdLineErrRead error
cmdLineErrOpen error
status []byte status []byte
statusErr error statusErrRead error
statusErrOpen error
expected PSEvent expected PSEvent
}{ }{
{ {
name: "nominal-no-ppid", name: "nominal-no-ppid",
enablePpid: false, enablePpid: false,
truncate: 100,
pid: 1, pid: 1,
cmdLine: []byte("abc\x00123"), cmdLine: []byte("abc\x00123"),
cmdLineErr: nil, cmdLineErrRead: nil,
cmdLineErrOpen: nil,
status: completeStatus, status: completeStatus,
statusErr: nil, statusErrRead: nil,
statusErrOpen: nil,
expected: PSEvent{ expected: PSEvent{
UID: 0, UID: 0,
PID: 1, PID: 1,
@@ -148,11 +161,14 @@ func TestProcessNewPid(t *testing.T) {
{ {
name: "nominal-ppid", name: "nominal-ppid",
enablePpid: true, enablePpid: true,
truncate: 100,
pid: 1, pid: 1,
cmdLine: []byte("abc\x00123"), cmdLine: []byte("abc\x00123"),
cmdLineErr: nil, cmdLineErrRead: nil,
cmdLineErrOpen: nil,
status: completeStatus, status: completeStatus,
statusErr: nil, statusErrRead: nil,
statusErrOpen: nil,
expected: PSEvent{ expected: PSEvent{
UID: 0, UID: 0,
PID: 1, PID: 1,
@@ -163,11 +179,14 @@ func TestProcessNewPid(t *testing.T) {
{ {
name: "empty-cmd-ok", name: "empty-cmd-ok",
enablePpid: true, enablePpid: true,
truncate: 100,
pid: 1, pid: 1,
cmdLine: []byte{}, cmdLine: []byte{},
cmdLineErr: nil, cmdLineErrRead: nil,
cmdLineErrOpen: nil,
status: completeStatus, status: completeStatus,
statusErr: nil, statusErrRead: nil,
statusErrOpen: nil,
expected: PSEvent{ expected: PSEvent{
UID: 0, UID: 0,
PID: 1, PID: 1,
@@ -175,14 +194,53 @@ func TestProcessNewPid(t *testing.T) {
CMD: "", CMD: "",
}, },
}, },
{
name: "cmd-truncate",
enablePpid: false,
truncate: 10,
pid: 1,
cmdLine: []byte("abc\x00123\x00alpha"),
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
status: completeStatus,
statusErrRead: nil,
statusErrOpen: nil,
expected: PSEvent{
UID: 0,
PID: 1,
PPID: -1,
CMD: "abc 123 al",
},
},
{ {
name: "cmd-io-error", name: "cmd-io-error",
enablePpid: true, enablePpid: true,
truncate: 100,
pid: 2, pid: 2,
cmdLine: nil, cmdLine: nil,
cmdLineErr: errors.New("file-system-error"), cmdLineErrRead: errors.New("file-system-error"),
cmdLineErrOpen: nil,
status: completeStatus, status: completeStatus,
statusErr: nil, statusErrRead: nil,
statusErrOpen: nil,
expected: PSEvent{
UID: 0,
PID: 2,
PPID: 5,
CMD: "???",
},
},
{
name: "cmd-io-error2",
enablePpid: true,
truncate: 100,
pid: 2,
cmdLine: nil,
cmdLineErrRead: nil,
cmdLineErrOpen: errors.New("file-system-error"),
status: completeStatus,
statusErrRead: nil,
statusErrOpen: nil,
expected: PSEvent{ expected: PSEvent{
UID: 0, UID: 0,
PID: 2, PID: 2,
@@ -193,11 +251,32 @@ func TestProcessNewPid(t *testing.T) {
{ {
name: "status-io-error", name: "status-io-error",
enablePpid: true, enablePpid: true,
truncate: 100,
pid: 2, pid: 2,
cmdLine: []byte("some\x00cmd\x00123"), cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErr: nil, cmdLineErrRead: nil,
cmdLineErrOpen: nil,
status: nil, status: nil,
statusErr: errors.New("file-system-error"), statusErrRead: errors.New("file-system-error"),
statusErrOpen: nil,
expected: PSEvent{
UID: -1,
PID: 2,
PPID: -1,
CMD: "some cmd 123",
},
},
{
name: "status-io-error2",
enablePpid: true,
truncate: 100,
pid: 2,
cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
status: nil,
statusErrRead: nil,
statusErrOpen: errors.New("file-system-error"),
expected: PSEvent{ expected: PSEvent{
UID: -1, UID: -1,
PID: 2, PID: 2,
@@ -208,11 +287,14 @@ func TestProcessNewPid(t *testing.T) {
{ {
name: "status-too-short", name: "status-too-short",
enablePpid: true, enablePpid: true,
truncate: 100,
pid: 3, pid: 3,
cmdLine: []byte("some\x00cmd\x00123"), cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErr: nil, cmdLineErrRead: nil,
cmdLineErrOpen: nil,
status: notEnoughLines, status: notEnoughLines,
statusErr: nil, statusErrRead: nil,
statusErrOpen: nil,
expected: PSEvent{ expected: PSEvent{
UID: -1, UID: -1,
PID: 3, PID: 3,
@@ -223,11 +305,14 @@ func TestProcessNewPid(t *testing.T) {
{ {
name: "status-empty", name: "status-empty",
enablePpid: true, enablePpid: true,
truncate: 100,
pid: 3, pid: 3,
cmdLine: []byte("some\x00cmd\x00123"), cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErr: nil, cmdLineErrRead: nil,
cmdLineErrOpen: nil,
status: []byte{}, status: []byte{},
statusErr: nil, statusErrRead: nil,
statusErrOpen: nil,
expected: PSEvent{ expected: PSEvent{
UID: -1, UID: -1,
PID: 3, PID: 3,
@@ -238,11 +323,14 @@ func TestProcessNewPid(t *testing.T) {
{ {
name: "uid-line-too-short", name: "uid-line-too-short",
enablePpid: true, enablePpid: true,
truncate: 100,
pid: 3, pid: 3,
cmdLine: []byte("some\x00cmd\x00123"), cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErr: nil, cmdLineErrRead: nil,
cmdLineErrOpen: nil,
status: uidLineBroken, status: uidLineBroken,
statusErr: nil, statusErrRead: nil,
statusErrOpen: nil,
expected: PSEvent{ expected: PSEvent{
UID: -1, UID: -1,
PID: 3, PID: 3,
@@ -253,11 +341,14 @@ func TestProcessNewPid(t *testing.T) {
{ {
name: "uid-parse-error", name: "uid-parse-error",
enablePpid: true, enablePpid: true,
truncate: 100,
pid: 3, pid: 3,
cmdLine: []byte("some\x00cmd\x00123"), cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErr: nil, cmdLineErrRead: nil,
cmdLineErrOpen: nil,
status: uidNaN, status: uidNaN,
statusErr: nil, statusErrRead: nil,
statusErrOpen: nil,
expected: PSEvent{ expected: PSEvent{
UID: -1, UID: -1,
PID: 3, PID: 3,
@@ -268,11 +359,14 @@ func TestProcessNewPid(t *testing.T) {
{ {
name: "ppid-line-too-short", name: "ppid-line-too-short",
enablePpid: true, enablePpid: true,
truncate: 100,
pid: 3, pid: 3,
cmdLine: []byte("some\x00cmd\x00123"), cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErr: nil, cmdLineErrRead: nil,
cmdLineErrOpen: nil,
status: ppidLineShort, status: ppidLineShort,
statusErr: nil, statusErrRead: nil,
statusErrOpen: nil,
expected: PSEvent{ expected: PSEvent{
UID: -1, UID: -1,
PID: 3, PID: 3,
@@ -283,11 +377,14 @@ func TestProcessNewPid(t *testing.T) {
{ {
name: "ppid-parse-error", name: "ppid-parse-error",
enablePpid: true, enablePpid: true,
truncate: 100,
pid: 3, pid: 3,
cmdLine: []byte("some\x00cmd\x00123"), cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErr: nil, cmdLineErrRead: nil,
cmdLineErrOpen: nil,
status: ppidNaN, status: ppidNaN,
statusErr: nil, statusErrRead: nil,
statusErrOpen: nil,
expected: PSEvent{ expected: PSEvent{
UID: -1, UID: -1,
PID: 3, PID: 3,
@@ -298,11 +395,14 @@ func TestProcessNewPid(t *testing.T) {
{ {
name: "no-ppid-line-too-short", name: "no-ppid-line-too-short",
enablePpid: false, enablePpid: false,
truncate: 100,
pid: 3, pid: 3,
cmdLine: []byte("some\x00cmd\x00123"), cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErr: nil, cmdLineErrRead: nil,
cmdLineErrOpen: nil,
status: ppidLineShort, status: ppidLineShort,
statusErr: nil, statusErrRead: nil,
statusErrOpen: nil,
expected: PSEvent{ expected: PSEvent{
UID: 0, UID: 0,
PID: 3, PID: 3,
@@ -313,11 +413,14 @@ func TestProcessNewPid(t *testing.T) {
{ {
name: "no-ppid-parse-error", name: "no-ppid-parse-error",
enablePpid: false, enablePpid: false,
truncate: 100,
pid: 3, pid: 3,
cmdLine: []byte("some\x00cmd\x00123"), cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErr: nil, cmdLineErrRead: nil,
cmdLineErrOpen: nil,
status: ppidNaN, status: ppidNaN,
statusErr: nil, statusErrRead: nil,
statusErrOpen: nil,
expected: PSEvent{ expected: PSEvent{
UID: 0, UID: 0,
PID: 3, PID: 3,
@@ -329,14 +432,15 @@ func TestProcessNewPid(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
defer mockCmdLineReader(tt.cmdLine, tt.cmdLineErr)() defer mockPidCmdLine(tt.pid, tt.cmdLine, tt.cmdLineErrRead, tt.cmdLineErrOpen, t)()
defer mockProcStatusReader(tt.status, tt.statusErr)() defer mockPidStatus(tt.pid, tt.status, tt.statusErrRead, tt.statusErrOpen, t)()
results := make(chan PSEvent, 1) results := make(chan PSEvent, 1)
scanner := &PSScanner{ scanner := &PSScanner{
enablePpid: tt.enablePpid, enablePpid: tt.enablePpid,
eventCh: results, eventCh: results,
maxCmdLength: tt.truncate,
} }
go func() { go func() {
@@ -359,23 +463,44 @@ func TestProcessNewPid(t *testing.T) {
} }
} }
func mockCmdLineReader(cmdLine []byte, err error) (restore func()) { func mockPidStatus(pid int, stat []byte, errRead error, errOpen error, t *testing.T) func() {
oldFunc := cmdLineReader return mockFile(fmt.Sprintf("/proc/%d/status", pid), stat, errRead, errOpen, t)
cmdLineReader = func(pid int) ([]byte, error) {
return cmdLine, err
}
return func() {
cmdLineReader = oldFunc
}
} }
func mockProcStatusReader(stat []byte, err error) (restore func()) { func mockPidCmdLine(pid int, cmdline []byte, errRead error, errOpen error, t *testing.T) func() {
oldFunc := procStatusReader return mockFile(fmt.Sprintf("/proc/%d/cmdline", pid), cmdline, errRead, errOpen, t)
procStatusReader = func(pid int) ([]byte, error) { }
return stat, err
type MockFile struct {
content []byte
err error
}
func (f *MockFile) Close() error {
return nil
}
func (f *MockFile) Read(p []byte) (int, error) {
return copy(p, f.content), f.err
}
// Hook/chain a mocked file into the "open" variable
func mockFile(name string, content []byte, errRead error, errOpen error, t *testing.T) func() {
oldopen := open
open = func(n string) (io.ReadCloser, error) {
if name == n {
if testing.Verbose() {
t.Logf("opening mocked file: %s", n)
}
return &MockFile{
content: content,
err: errRead,
}, errOpen
}
return oldopen(n)
} }
return func() { return func() {
procStatusReader = oldFunc open = oldopen
} }
} }
@@ -383,22 +508,26 @@ func TestNewPSScanner(t *testing.T) {
for _, tt := range []struct { for _, tt := range []struct {
name string name string
ppid bool ppid bool
cmdlen int
}{ }{
{ {
name: "without-ppid", name: "without-ppid",
ppid: false, ppid: false,
cmdlen: 30,
}, },
{ {
name: "with-ppid", name: "with-ppid",
ppid: true, ppid: true,
cmdlen: 5000,
}, },
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
expected := &PSScanner{ expected := &PSScanner{
enablePpid: tt.ppid, enablePpid: tt.ppid,
eventCh: nil, eventCh: nil,
maxCmdLength: tt.cmdlen,
} }
new := NewPSScanner(tt.ppid) new := NewPSScanner(tt.ppid, tt.cmdlen)
if !reflect.DeepEqual(new, expected) { if !reflect.DeepEqual(new, expected) {
t.Errorf("Unexpected scanner initialisation state: got %#v but want %#v", new, expected) t.Errorf("Unexpected scanner initialisation state: got %#v but want %#v", new, expected)