mirror of
https://github.com/DominicBreuker/pspy.git
synced 2025-12-21 11:44:51 +00:00
Merge pull request #11 from kazkansouh/ppid-no-dirs
Add ppid support and optimise PSScanner code
This commit is contained in:
@@ -61,6 +61,8 @@ var defaultDirs = []string{}
|
||||
var triggerInterval int
|
||||
var colored bool
|
||||
var debug bool
|
||||
var ppid bool
|
||||
var cmdLength int
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().BoolVarP(&logPS, "procevents", "p", true, "print new processes to stdout")
|
||||
@@ -70,6 +72,8 @@ func init() {
|
||||
rootCmd.PersistentFlags().IntVarP(&triggerInterval, "interval", "i", 100, "scan every 'interval' milliseconds for new processes")
|
||||
rootCmd.PersistentFlags().BoolVarP(&colored, "color", "c", true, "color the printed events")
|
||||
rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "", false, "print detailed error messages")
|
||||
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)
|
||||
}
|
||||
@@ -91,7 +95,7 @@ func root(cmd *cobra.Command, args []string) {
|
||||
fsw := fswatcher.NewFSWatcher()
|
||||
defer fsw.Close()
|
||||
|
||||
pss := psscanner.NewPSScanner()
|
||||
pss := psscanner.NewPSScanner(ppid, cmdLength)
|
||||
|
||||
sigCh := make(chan os.Signal)
|
||||
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
|
||||
@@ -38,9 +38,16 @@ type chans struct {
|
||||
|
||||
func Start(cfg *config.Config, b *Bindings, sigCh chan os.Signal) chan struct{} {
|
||||
b.Logger.Infof("Config: %+v", cfg)
|
||||
abort := make(chan struct{}, 1)
|
||||
abort <- struct{}{}
|
||||
|
||||
initFSW(b.FSW, cfg.RDirs, cfg.Dirs, b.Logger)
|
||||
triggerCh, fsEventCh := startFSW(b.FSW, b.Logger, cfg.DrainFor)
|
||||
if !initFSW(b.FSW, cfg.RDirs, cfg.Dirs, b.Logger, sigCh) {
|
||||
return abort
|
||||
}
|
||||
triggerCh, fsEventCh, ok := startFSW(b.FSW, b.Logger, cfg.DrainFor, sigCh)
|
||||
if !ok {
|
||||
return abort
|
||||
}
|
||||
|
||||
psEventCh := startPSS(b.PSS, b.Logger, triggerCh)
|
||||
|
||||
@@ -83,27 +90,29 @@ func printOutput(cfg *config.Config, b *Bindings, chans *chans) chan struct{} {
|
||||
return exit
|
||||
}
|
||||
|
||||
func initFSW(fsw FSWatcher, rdirs, dirs []string, logger Logger) {
|
||||
func initFSW(fsw FSWatcher, rdirs, dirs []string, logger Logger, sigCh <-chan os.Signal) bool {
|
||||
errCh, doneCh := fsw.Init(rdirs, dirs)
|
||||
for {
|
||||
select {
|
||||
case <-sigCh:
|
||||
return false
|
||||
case <-doneCh:
|
||||
return
|
||||
return true
|
||||
case err := <-errCh:
|
||||
logger.Errorf(true, "initializing fs watcher: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startFSW(fsw FSWatcher, logger Logger, drainFor time.Duration) (triggerCh chan struct{}, fsEventCh chan string) {
|
||||
func startFSW(fsw FSWatcher, logger Logger, drainFor time.Duration, sigCh <-chan os.Signal) (triggerCh chan struct{}, fsEventCh chan string, ok bool) {
|
||||
triggerCh, fsEventCh, errCh := fsw.Run()
|
||||
go logErrors(errCh, logger)
|
||||
|
||||
// ignore all file system events created on startup
|
||||
logger.Infof("Draining file system events due to startup...")
|
||||
drainEventsFor(triggerCh, fsEventCh, drainFor)
|
||||
ok = drainEventsFor(triggerCh, fsEventCh, drainFor, sigCh)
|
||||
logger.Infof("done")
|
||||
return triggerCh, fsEventCh
|
||||
return
|
||||
}
|
||||
|
||||
func startPSS(pss PSScanner, logger Logger, triggerCh chan struct{}) (psEventCh chan psscanner.PSEvent) {
|
||||
@@ -128,13 +137,15 @@ func logErrors(errCh chan error, logger Logger) {
|
||||
}
|
||||
}
|
||||
|
||||
func drainEventsFor(triggerCh chan struct{}, eventCh chan string, d time.Duration) {
|
||||
func drainEventsFor(triggerCh chan struct{}, eventCh chan string, d time.Duration, sigCh <-chan os.Signal) bool {
|
||||
for {
|
||||
select {
|
||||
case <-sigCh:
|
||||
return false
|
||||
case <-triggerCh:
|
||||
case <-eventCh:
|
||||
case <-time.After(d):
|
||||
return
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,24 +17,55 @@ func TestInitFSW(t *testing.T) {
|
||||
fsw := newMockFSWatcher()
|
||||
rdirs := make([]string, 0)
|
||||
dirs := make([]string, 0)
|
||||
sigCh := make(chan os.Signal)
|
||||
go func() {
|
||||
fsw.initErrCh <- errors.New("error1")
|
||||
fsw.initErrCh <- errors.New("error2")
|
||||
close(fsw.initDoneCh)
|
||||
}()
|
||||
|
||||
initFSW(fsw, rdirs, dirs, l)
|
||||
if !initFSW(fsw, rdirs, dirs, l, sigCh) {
|
||||
t.Error("unexpected return value")
|
||||
}
|
||||
|
||||
expectMessage(t, l.Error, "initializing fs watcher: error1")
|
||||
expectMessage(t, l.Error, "initializing fs watcher: error2")
|
||||
expectClosed(t, fsw.initDoneCh)
|
||||
}
|
||||
|
||||
func TestInitFSWInterrupt(t *testing.T) {
|
||||
l := newMockLogger()
|
||||
fsw := newMockFSWatcher()
|
||||
rdirs := make([]string, 0)
|
||||
dirs := make([]string, 0)
|
||||
sigCh := make(chan os.Signal, 0)
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
<-time.After(100 * time.Millisecond)
|
||||
sigCh <- os.Interrupt
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if initFSW(fsw, rdirs, dirs, l, sigCh) {
|
||||
t.Error("unexpected return value")
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Error("timout")
|
||||
}
|
||||
}
|
||||
|
||||
// very flaky test... refactor code!
|
||||
func TestStartFSW(t *testing.T) {
|
||||
l := newMockLogger()
|
||||
fsw := newMockFSWatcher()
|
||||
drainFor := 100 * time.Millisecond
|
||||
sigCh := make(chan os.Signal)
|
||||
|
||||
go func() {
|
||||
fsw.runTriggerCh <- struct{}{} // trigger sent while draining
|
||||
@@ -47,7 +78,10 @@ func TestStartFSW(t *testing.T) {
|
||||
}()
|
||||
|
||||
// sends no events and triggers from the drain phase
|
||||
triggerCh, fsEventCh := startFSW(fsw, l, drainFor)
|
||||
triggerCh, fsEventCh, ok := startFSW(fsw, l, drainFor, sigCh)
|
||||
if !ok {
|
||||
t.Error("unexpected return value")
|
||||
}
|
||||
expectMessage(t, l.Info, "Draining file system events due to startup...")
|
||||
expectMessage(t, l.Error, "ERROR: error sent while draining")
|
||||
expectMessage(t, l.Info, "done")
|
||||
@@ -55,6 +89,32 @@ func TestStartFSW(t *testing.T) {
|
||||
expectMessage(t, fsEventCh, "event sent after draining")
|
||||
}
|
||||
|
||||
func TestStartFSWInterrupt(t *testing.T) {
|
||||
l := newMockLogger()
|
||||
fsw := newMockFSWatcher()
|
||||
drainFor := 500 * time.Millisecond
|
||||
sigCh := make(chan os.Signal)
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
<-time.After(100 * time.Millisecond)
|
||||
sigCh <- os.Interrupt
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if _, _, ok := startFSW(fsw, l, drainFor, sigCh); ok {
|
||||
t.Error("unexpected return value")
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Error("timout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartPSS(t *testing.T) {
|
||||
pss := newMockPSScanner()
|
||||
l := newMockLogger()
|
||||
@@ -95,7 +155,7 @@ func TestStart(t *testing.T) {
|
||||
close(fsw.initDoneCh)
|
||||
<-time.After(2 * drainFor)
|
||||
fsw.runTriggerCh <- struct{}{}
|
||||
pss.runEventCh <- psscanner.PSEvent{UID: 1000, PID: 12345, CMD: "pss event"}
|
||||
pss.runEventCh <- psscanner.PSEvent{UID: 1000, PID: 12345, PPID: 54321, CMD: "pss event"}
|
||||
pss.runErrCh <- errors.New("pss error")
|
||||
fsw.runEventCh <- "fsw event"
|
||||
fsw.runErrCh <- errors.New("fsw error")
|
||||
@@ -108,7 +168,7 @@ func TestStart(t *testing.T) {
|
||||
<-time.After(2 * drainFor)
|
||||
expectMessage(t, l.Info, "done")
|
||||
expectTrigger(t, pss.runTriggerCh) // pss receives triggers from fsw
|
||||
expectMessage(t, l.Event, fmt.Sprintf("%d CMD: UID=1000 PID=12345 | pss event", logging.ColorPurple))
|
||||
expectMessage(t, l.Event, fmt.Sprintf("%d CMD: UID=1000 PID=12345 PPID=54321 | pss event", logging.ColorPurple))
|
||||
expectMessage(t, l.Error, "ERROR: pss error")
|
||||
expectMessage(t, l.Event, fmt.Sprintf("%d FS: fsw event", logging.ColorNone))
|
||||
expectMessage(t, l.Error, "ERROR: fsw error")
|
||||
|
||||
@@ -1,31 +1,19 @@
|
||||
package psscanner
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var procDirReader = func() ([]os.FileInfo, error) {
|
||||
return ioutil.ReadDir("/proc")
|
||||
type procList map[int]struct{}
|
||||
|
||||
type pidProcessor interface {
|
||||
processNewPid(pid int)
|
||||
}
|
||||
|
||||
var procStatusReader = func(pid int) ([]byte, error) {
|
||||
statPath := fmt.Sprintf("/proc/%d/status", pid)
|
||||
return ioutil.ReadFile(statPath)
|
||||
}
|
||||
|
||||
var cmdLineReader = func(pid int) ([]byte, error) {
|
||||
cmdPath := fmt.Sprintf("/proc/%d/cmdline", pid)
|
||||
return ioutil.ReadFile(cmdPath)
|
||||
}
|
||||
|
||||
type procList map[int]string
|
||||
|
||||
func (pl procList) refresh(eventCh chan PSEvent) error {
|
||||
func (pl procList) refresh(p pidProcessor) error {
|
||||
pids, err := getPIDs()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -35,7 +23,8 @@ func (pl procList) refresh(eventCh chan PSEvent) error {
|
||||
pid := pids[i]
|
||||
_, ok := pl[pid]
|
||||
if !ok {
|
||||
pl.addPid(pid, eventCh)
|
||||
p.processNewPid(pid)
|
||||
pl[pid] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,15 +32,21 @@ func (pl procList) refresh(eventCh chan PSEvent) error {
|
||||
}
|
||||
|
||||
func getPIDs() ([]int, error) {
|
||||
proc, err := procDirReader()
|
||||
f, err := dirOpen("/proc")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening proc dir: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
names, err := f.Readdirnames(-1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading proc dir: %v", err)
|
||||
}
|
||||
|
||||
pids := make([]int, 0)
|
||||
for _, f := range proc {
|
||||
pid, err := file2Pid(f)
|
||||
if err != nil {
|
||||
for _, f := range names {
|
||||
pid, err := strconv.Atoi(f)
|
||||
if err != nil || pid <= 0 {
|
||||
continue
|
||||
}
|
||||
pids = append(pids, pid)
|
||||
@@ -59,67 +54,11 @@ func getPIDs() ([]int, error) {
|
||||
return pids, nil
|
||||
}
|
||||
|
||||
var errNotAPid = errors.New("not a pid")
|
||||
|
||||
func file2Pid(f os.FileInfo) (int, error) {
|
||||
if !f.IsDir() {
|
||||
return -1, errNotAPid
|
||||
type readDirNamesCloser interface {
|
||||
Readdirnames(n int) (names []string, err error)
|
||||
io.Closer
|
||||
}
|
||||
|
||||
pid, err := strconv.Atoi(f.Name())
|
||||
if err != nil || pid <= 0 {
|
||||
return -1, errNotAPid
|
||||
}
|
||||
|
||||
return pid, nil
|
||||
}
|
||||
|
||||
func (pl procList) addPid(pid int, eventCh chan PSEvent) {
|
||||
cmd, err := getCmd(pid)
|
||||
if err != nil {
|
||||
cmd = "???" // process probably terminated
|
||||
}
|
||||
uid, err := getUID(pid)
|
||||
if err != nil {
|
||||
uid = -1
|
||||
}
|
||||
eventCh <- PSEvent{UID: uid, PID: pid, CMD: cmd}
|
||||
pl[pid] = cmd
|
||||
}
|
||||
|
||||
func getCmd(pid int) (string, error) {
|
||||
cmd, err := cmdLineReader(pid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for i := 0; i < len(cmd); i++ {
|
||||
if cmd[i] == 0 {
|
||||
cmd[i] = 32
|
||||
}
|
||||
}
|
||||
return string(cmd), nil
|
||||
}
|
||||
|
||||
func getUID(pid int) (int, error) {
|
||||
stat, err := procStatusReader(pid)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(stat), "\n")
|
||||
if len(lines) < 9 {
|
||||
return -1, fmt.Errorf("no uid information")
|
||||
}
|
||||
|
||||
uidL := strings.Split(lines[8], "\t")
|
||||
if len(uidL) < 2 {
|
||||
return -1, fmt.Errorf("uid line read incomplete")
|
||||
}
|
||||
|
||||
uid, err := strconv.Atoi(uidL[1])
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("converting %s to int: %v", uidL[1], err)
|
||||
}
|
||||
|
||||
return uid, nil
|
||||
var dirOpen func(string) (readDirNamesCloser, error) = func(s string) (readDirNamesCloser, error) {
|
||||
return os.Open(s)
|
||||
}
|
||||
|
||||
@@ -1,72 +1,77 @@
|
||||
package psscanner
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetCmd
|
||||
|
||||
func TestGetCmd(t *testing.T) {
|
||||
tests := []struct {
|
||||
pid int
|
||||
cmdLine []byte
|
||||
cmdErr error
|
||||
cmd string
|
||||
err string
|
||||
}{
|
||||
{pid: 1, cmdLine: []byte("abc"), cmdErr: nil, cmd: "abc", err: ""},
|
||||
{pid: 1, cmdLine: []byte(""), cmdErr: nil, cmd: "", err: ""}, // can work with empty result
|
||||
{pid: 1, cmdLine: []byte("abc\x00123"), cmdErr: nil, cmd: "abc 123", err: ""}, // turns null bytes into spaces
|
||||
{pid: 1, cmdLine: []byte("abc"), cmdErr: errors.New("file-system-error"), cmd: "", err: "file-system-error"}, // returns error from file reader
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
restore := mockCmdLineReader(tt.cmdLine, tt.cmdErr)
|
||||
cmd, err := getCmd(tt.pid)
|
||||
if cmd != tt.cmd {
|
||||
t.Errorf("Wrong cmd line returned: got %s but want %s", cmd, tt.cmd)
|
||||
}
|
||||
if (err != nil || tt.err != "") && fmt.Sprintf("%v", err) != tt.err {
|
||||
t.Errorf("Wrong error returned: got %v but want %s", err, tt.err)
|
||||
}
|
||||
restore()
|
||||
}
|
||||
}
|
||||
|
||||
func mockCmdLineReader(cmdLine []byte, err error) (restore func()) {
|
||||
oldFunc := cmdLineReader
|
||||
cmdLineReader = func(pid int) ([]byte, error) {
|
||||
return cmdLine, err
|
||||
}
|
||||
return func() {
|
||||
cmdLineReader = oldFunc
|
||||
}
|
||||
}
|
||||
|
||||
// GetPIDs
|
||||
|
||||
func TestGetPIDs(t *testing.T) {
|
||||
tests := []struct {
|
||||
proc []os.FileInfo
|
||||
procErr error
|
||||
name string
|
||||
proc []string
|
||||
procErrOpen error
|
||||
procErrRead error
|
||||
pids []int
|
||||
err string
|
||||
}{
|
||||
{proc: []os.FileInfo{newMockDir("42"), newMockDir("somedir")}, procErr: nil, pids: []int{42}, err: ""}, // reads numbers and ignores everything else
|
||||
{proc: []os.FileInfo{newMockDir("42"), newMockFile("13")}, procErr: nil, pids: []int{42}, err: ""}, // reads directories and ignores files
|
||||
{proc: []os.FileInfo{newMockDir("0"), newMockDir("-1")}, procErr: nil, pids: []int{}, err: ""}, // ignores 0 and negative numbers
|
||||
{proc: []os.FileInfo{}, procErr: nil, pids: []int{}, err: ""}, // can handle empty procfs
|
||||
{proc: []os.FileInfo{}, procErr: errors.New("file-system-error"), pids: nil, err: "opening proc dir: file-system-error"}, // returns errors
|
||||
{
|
||||
name: "numbers-only",
|
||||
proc: []string{"42", "somedir"},
|
||||
procErrOpen: nil,
|
||||
procErrRead: nil,
|
||||
pids: []int{42},
|
||||
err: "",
|
||||
},
|
||||
{
|
||||
name: "multiple-entries",
|
||||
proc: []string{"42", "13"},
|
||||
procErrOpen: nil,
|
||||
procErrRead: nil,
|
||||
pids: []int{42, 13},
|
||||
err: "",
|
||||
},
|
||||
{
|
||||
name: "ignores-lte-0",
|
||||
proc: []string{"0", "-1"},
|
||||
procErrOpen: nil,
|
||||
procErrRead: nil,
|
||||
pids: []int{},
|
||||
err: "",
|
||||
},
|
||||
{
|
||||
name: "empty-procfs",
|
||||
proc: []string{},
|
||||
procErrOpen: nil,
|
||||
procErrRead: nil,
|
||||
pids: []int{},
|
||||
err: "",
|
||||
},
|
||||
{
|
||||
name: "handle-open-error",
|
||||
proc: []string{},
|
||||
procErrOpen: errors.New("file-system-error"),
|
||||
procErrRead: nil,
|
||||
pids: nil,
|
||||
err: "opening proc dir: file-system-error",
|
||||
},
|
||||
{
|
||||
name: "handle-read-error",
|
||||
proc: []string{},
|
||||
procErrOpen: nil,
|
||||
procErrRead: errors.New("file-system-error"),
|
||||
pids: nil,
|
||||
err: "reading proc dir: file-system-error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
restore := mockProcDirReader(tt.proc, tt.procErr)
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer mockDir("/proc", tt.proc, tt.procErrRead, tt.procErrOpen, t)()
|
||||
pids, err := getPIDs()
|
||||
if !reflect.DeepEqual(pids, tt.pids) {
|
||||
t.Errorf("Wrong pids returned: got %v but want %v", pids, tt.pids)
|
||||
@@ -74,184 +79,155 @@ func TestGetPIDs(t *testing.T) {
|
||||
if (err != nil || tt.err != "") && fmt.Sprintf("%v", err) != tt.err {
|
||||
t.Errorf("Wrong error returned: got %v but want %s", err, tt.err)
|
||||
}
|
||||
restore()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mockProcDirReader(proc []os.FileInfo, err error) (restore func()) {
|
||||
oldFunc := procDirReader
|
||||
procDirReader = func() ([]os.FileInfo, error) {
|
||||
return proc, err
|
||||
}
|
||||
return func() {
|
||||
procDirReader = oldFunc
|
||||
}
|
||||
type MockDir struct {
|
||||
names []string
|
||||
err error
|
||||
}
|
||||
|
||||
func newMockDir(name string) *mockFileInfo {
|
||||
return &mockFileInfo{
|
||||
name: name,
|
||||
isDir: true,
|
||||
}
|
||||
}
|
||||
|
||||
func newMockFile(name string) *mockFileInfo {
|
||||
return &mockFileInfo{
|
||||
name: name,
|
||||
isDir: false,
|
||||
}
|
||||
}
|
||||
|
||||
type mockFileInfo struct {
|
||||
name string
|
||||
isDir bool
|
||||
}
|
||||
|
||||
func (f *mockFileInfo) Name() string {
|
||||
return f.name
|
||||
}
|
||||
func (f *mockFileInfo) Size() int64 {
|
||||
return 0
|
||||
}
|
||||
func (f *mockFileInfo) Mode() os.FileMode {
|
||||
return 0
|
||||
}
|
||||
func (f *mockFileInfo) ModTime() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
func (f *mockFileInfo) IsDir() bool {
|
||||
return f.isDir
|
||||
}
|
||||
func (f *mockFileInfo) Sys() interface{} {
|
||||
func (f *MockDir) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUID
|
||||
|
||||
func TestGetUID(t *testing.T) {
|
||||
completeStatus, _ := hex.DecodeString("4e616d653a0963726f6e0a556d61736b3a09303032320a53746174653a09532028736c656570696e67290a546769643a09370a4e6769643a09300a5069643a09370a505069643a09350a5472616365725069643a09300a5569643a09300930093009300a4769643a09300930093009300a464453697a653a0936340a47726f7570733a0930200a4e53746769643a09370a4e537069643a09370a4e53706769643a09310a4e537369643a09310a566d5065616b3a092020203238303132206b420a566d53697a653a092020203237393932206b420a566d4c636b3a092020202020202030206b420a566d50696e3a092020202020202030206b420a566d48574d3a092020202032333532206b420a566d5253533a092020202032333532206b420a527373416e6f6e3a092020202020323430206b420a52737346696c653a092020202032313132206b420a52737353686d656d3a092020202020202030206b420a566d446174613a092020202020333430206b420a566d53746b3a092020202020313332206b420a566d4578653a092020202020203434206b420a566d4c69623a092020202032383536206b420a566d5054453a092020202020203736206b420a566d504d443a092020202020203132206b420a566d537761703a092020202020202030206b420a48756765746c6250616765733a092020202020202030206b420a546872656164733a09310a536967513a09302f34373834320a536967506e643a09303030303030303030303030303030300a536864506e643a09303030303030303030303030303030300a536967426c6b3a09303030303030303030303030303030300a53696749676e3a09303030303030303030303030303030360a5369674367743a09303030303030303138303031303030310a436170496e683a09303030303030303061383034323566620a43617050726d3a09303030303030303061383034323566620a4361704566663a09303030303030303061383034323566620a436170426e643a09303030303030303061383034323566620a436170416d623a09303030303030303030303030303030300a536563636f6d703a09320a437075735f616c6c6f7765643a09330a437075735f616c6c6f7765645f6c6973743a09302d310a4d656d735f616c6c6f7765643a09310a4d656d735f616c6c6f7765645f6c6973743a09300a766f6c756e746172795f637478745f73776974636865733a0932350a6e6f6e766f6c756e746172795f637478745f73776974636865733a09310a")
|
||||
uidLineBroken, _ := hex.DecodeString("4e616d653a0963726f6e0a556d61736b3a09303032320a53746174653a09532028736c656570696e67290a546769643a09370a4e6769643a09300a5069643a09370a505069643a09350a5472616365725069643a09300a5569643a")
|
||||
notEnoughLines, _ := hex.DecodeString("4e616d653a0963726f6e0a556d61736b3a09303032320a537461")
|
||||
tests := []struct {
|
||||
pid int
|
||||
stat []byte
|
||||
statErr error
|
||||
uid int
|
||||
err string
|
||||
}{
|
||||
{pid: 7, stat: completeStatus, statErr: nil, uid: 0, err: ""}, // can read normal stat
|
||||
{pid: 7, stat: uidLineBroken, statErr: nil, uid: -1, err: "uid line read incomplete"}, // errors on incomplete Uid line
|
||||
{pid: 7, stat: notEnoughLines, statErr: nil, uid: -1, err: "no uid information"}, // errors for insufficient lines
|
||||
{pid: 7, stat: []byte(""), statErr: nil, uid: -1, err: "no uid information"}, // errors for insufficient lines
|
||||
{pid: 7, stat: []byte(""), statErr: errors.New("file-system-error"), uid: -1, err: "file-system-error"}, // returns file system errors
|
||||
func min(a, b int) int {
|
||||
if a > b {
|
||||
return b
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
restore := mockProcStatusReader(tt.stat, tt.statErr)
|
||||
uid, err := getUID(tt.pid)
|
||||
if uid != tt.uid {
|
||||
fmt.Printf("STAT: %s", tt.stat)
|
||||
t.Errorf("Wrong uid returned: got %d but want %d", uid, tt.uid)
|
||||
}
|
||||
if (err != nil || tt.err != "") && fmt.Sprintf("%v", err) != tt.err {
|
||||
t.Errorf("Wrong error returned: got %v but want %s", err, tt.err)
|
||||
}
|
||||
restore()
|
||||
func (f *MockDir) Readdirnames(n int) (names []string, err error) {
|
||||
if n < 0 {
|
||||
return f.names, f.err
|
||||
}
|
||||
return f.names[:min(n, len(f.names))], f.err
|
||||
}
|
||||
|
||||
func mockProcStatusReader(stat []byte, err error) (restore func()) {
|
||||
oldFunc := procStatusReader
|
||||
procStatusReader = func(pid int) ([]byte, error) {
|
||||
return stat, err
|
||||
// Hook/chain a mocked file into the "open" variable
|
||||
func mockDir(name string, names []string, errRead error, errOpen error, t *testing.T) func() {
|
||||
oldopen := dirOpen
|
||||
dirOpen = func(n string) (readDirNamesCloser, error) {
|
||||
if name == n {
|
||||
if testing.Verbose() {
|
||||
t.Logf("opening mocked dir: %s", n)
|
||||
}
|
||||
return &MockDir{
|
||||
names: names,
|
||||
err: errRead,
|
||||
}, errOpen
|
||||
}
|
||||
return oldopen(n)
|
||||
}
|
||||
return func() {
|
||||
procStatusReader = oldFunc
|
||||
dirOpen = oldopen
|
||||
}
|
||||
}
|
||||
|
||||
// refresh
|
||||
type mockPidProcessor struct {
|
||||
t *testing.T
|
||||
pids []int
|
||||
}
|
||||
|
||||
func (m *mockPidProcessor) processNewPid(pid int) {
|
||||
if testing.Verbose() {
|
||||
m.t.Logf("proc %d processed", pid)
|
||||
}
|
||||
m.pids = append(m.pids, pid)
|
||||
}
|
||||
|
||||
var unit = struct{}{}
|
||||
|
||||
func TestRefresh(t *testing.T) {
|
||||
tests := []struct {
|
||||
eventCh chan PSEvent
|
||||
name string
|
||||
pl procList
|
||||
newPids []int
|
||||
pidsAfter []int
|
||||
events []string
|
||||
plAfter procList
|
||||
pidsProcessed []int
|
||||
}{
|
||||
{eventCh: make(chan PSEvent), pl: procList{}, newPids: []int{1, 2, 3}, pidsAfter: []int{3, 2, 1}, events: []string{
|
||||
"UID=??? PID=3 | the-command",
|
||||
"UID=??? PID=2 | the-command",
|
||||
"UID=??? PID=1 | the-command",
|
||||
}},
|
||||
{eventCh: make(chan PSEvent), pl: procList{1: "pid-found-before"}, newPids: []int{1, 2, 3}, pidsAfter: []int{1, 3, 2}, events: []string{
|
||||
"UID=??? PID=3 | the-command",
|
||||
"UID=??? PID=2 | the-command",
|
||||
}}, // no events emitted for PIDs already known
|
||||
{
|
||||
name: "nominal",
|
||||
pl: procList{},
|
||||
newPids: []int{1, 2, 3},
|
||||
plAfter: procList{1: unit, 2: unit, 3: unit},
|
||||
pidsProcessed: []int{3, 2, 1},
|
||||
},
|
||||
{
|
||||
name: "merge",
|
||||
pl: procList{1: unit},
|
||||
newPids: []int{1, 2, 3},
|
||||
plAfter: procList{1: unit, 2: unit, 3: unit},
|
||||
pidsProcessed: []int{3, 2},
|
||||
},
|
||||
{
|
||||
name: "nothing-new",
|
||||
pl: procList{1: unit, 2: unit, 3: unit},
|
||||
newPids: []int{1, 2, 3},
|
||||
plAfter: procList{1: unit, 2: unit, 3: unit},
|
||||
pidsProcessed: []int{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
restoreGetPIDs := mockPidList(tt.newPids)
|
||||
restoreCmdLineReader := mockCmdLineReader([]byte("the-command"), nil)
|
||||
restoreProcStatusReader := mockProcStatusReader([]byte(""), nil) // don't mock read value since it's not worth it
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer mockPidList(tt.newPids, t)()
|
||||
|
||||
events := make([]string, 0)
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
for e := range tt.eventCh {
|
||||
events = append(events, e.String())
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
tt.pl.refresh(tt.eventCh)
|
||||
close(tt.eventCh)
|
||||
<-done
|
||||
m := &mockPidProcessor{t, []int{}}
|
||||
tt.pl.refresh(m)
|
||||
|
||||
restoreProcStatusReader()
|
||||
restoreCmdLineReader()
|
||||
restoreGetPIDs()
|
||||
|
||||
pidsAfter := getPids(&tt.pl)
|
||||
|
||||
for _, pid := range tt.pidsAfter {
|
||||
if !contains(pidsAfter, pid) {
|
||||
t.Errorf("PID %d should be in list %v but was not!", pid, pidsAfter)
|
||||
if !reflect.DeepEqual(m.pids, tt.pidsProcessed) {
|
||||
t.Errorf("Unexpected pids got processed: got %v but want %v", m.pids, tt.pidsProcessed)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.pl, tt.plAfter) {
|
||||
t.Errorf("Unexpected pids stored in procList: got %v but want %v", tt.pl, tt.plAfter)
|
||||
}
|
||||
for _, pid := range pidsAfter {
|
||||
if !contains(tt.pidsAfter, pid) {
|
||||
t.Errorf("PID %d should be in list %v but was not!", pid, pidsAfter)
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(events, tt.events) {
|
||||
t.Errorf("Wrong events returned: got %v but want %v", events, tt.events)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func contains(list []int, v int) bool {
|
||||
for _, i := range list {
|
||||
if i == v {
|
||||
return true
|
||||
// separate test for failing, only one case where getPids fails
|
||||
func TestRefreshFail(t *testing.T) {
|
||||
e := errors.New("file-system-error")
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
errRead error
|
||||
errOpen error
|
||||
}{
|
||||
{
|
||||
name: "open-dir-fail",
|
||||
errRead: nil,
|
||||
errOpen: e,
|
||||
},
|
||||
{
|
||||
name: "read-dir-fail",
|
||||
errRead: e,
|
||||
errOpen: nil,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer mockDir("/proc", []string{}, tt.errRead, tt.errOpen, t)()
|
||||
m := &mockPidProcessor{t, []int{}}
|
||||
pl := procList{1: unit}
|
||||
err := pl.refresh(m)
|
||||
if err == nil {
|
||||
t.Errorf("Expected an error")
|
||||
} else {
|
||||
if strings.Index(err.Error(), e.Error()) == -1 {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func mockPidList(pids []int) func() {
|
||||
dirs := make([]os.FileInfo, 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mockPidList(pids []int, t *testing.T) func() {
|
||||
dirs := make([]string, 0)
|
||||
for _, pid := range pids {
|
||||
dirs = append(dirs, newMockDir(fmt.Sprintf("%d", pid)))
|
||||
dirs = append(dirs, fmt.Sprintf("%d", pid))
|
||||
}
|
||||
restore := mockProcDirReader(dirs, nil)
|
||||
return restore
|
||||
}
|
||||
|
||||
func getPids(pl *procList) []int {
|
||||
pids := make([]int, 0)
|
||||
for pid := range *pl {
|
||||
pids = append(pids, pid)
|
||||
}
|
||||
return pids
|
||||
return mockDir("/proc", dirs, nil, nil, t)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
package psscanner
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type PSScanner struct{}
|
||||
type PSScanner struct {
|
||||
enablePpid bool
|
||||
eventCh chan<- PSEvent
|
||||
maxCmdLength int
|
||||
}
|
||||
|
||||
type PSEvent struct {
|
||||
UID int
|
||||
PID int
|
||||
PPID int
|
||||
CMD string
|
||||
}
|
||||
|
||||
@@ -19,23 +29,100 @@ func (evt PSEvent) String() string {
|
||||
uid = "???"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("UID=%-4s PID=%-6d | %s", uid, evt.PID, evt.CMD)
|
||||
if evt.PPID == -1 {
|
||||
return fmt.Sprintf("UID=%-5s PID=%-6d | %s", uid, evt.PID, evt.CMD)
|
||||
}
|
||||
|
||||
func NewPSScanner() *PSScanner {
|
||||
return &PSScanner{}
|
||||
return fmt.Sprintf(
|
||||
"UID=%-5s PID=%-6d PPID=%-6d | %s", uid, evt.PID, evt.PPID, evt.CMD)
|
||||
}
|
||||
|
||||
var (
|
||||
// identify ppid in stat file
|
||||
ppidRegex, _ = regexp.Compile("\\d+ \\(.*\\) [[:alpha:]] (\\d+)")
|
||||
// hook for testing, directly use Lstat syscall as os.Lstat hides data in Sys member
|
||||
lstat = syscall.Lstat
|
||||
// hook for testing
|
||||
open = func(s string) (io.ReadCloser, error) {
|
||||
return os.Open(s)
|
||||
}
|
||||
)
|
||||
|
||||
func NewPSScanner(ppid bool, cmdLength int) *PSScanner {
|
||||
return &PSScanner{
|
||||
enablePpid: ppid,
|
||||
eventCh: nil,
|
||||
maxCmdLength: cmdLength,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PSScanner) Run(triggerCh chan struct{}) (chan PSEvent, chan error) {
|
||||
eventCh := make(chan PSEvent, 100)
|
||||
p.eventCh = eventCh
|
||||
errCh := make(chan error)
|
||||
pl := make(procList)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
<-triggerCh
|
||||
pl.refresh(eventCh)
|
||||
pl.refresh(p)
|
||||
}
|
||||
}()
|
||||
return eventCh, errCh
|
||||
}
|
||||
|
||||
func (p *PSScanner) processNewPid(pid int) {
|
||||
statInfo := syscall.Stat_t{}
|
||||
errStat := lstat(fmt.Sprintf("/proc/%d", pid), &statInfo)
|
||||
cmdLine, errCmdLine := readFile(fmt.Sprintf("/proc/%d/cmdline", pid), p.maxCmdLength)
|
||||
ppid, _ := p.getPpid(pid)
|
||||
|
||||
cmd := "???" // process probably terminated
|
||||
if errCmdLine == nil {
|
||||
for i := 0; i < len(cmdLine); i++ {
|
||||
if cmdLine[i] == 0 {
|
||||
cmdLine[i] = 32
|
||||
}
|
||||
}
|
||||
cmd = string(cmdLine)
|
||||
}
|
||||
|
||||
uid := -1
|
||||
if errStat == nil {
|
||||
uid = int(statInfo.Uid)
|
||||
}
|
||||
|
||||
p.eventCh <- PSEvent{UID: uid, PID: pid, PPID: ppid, CMD: cmd}
|
||||
}
|
||||
|
||||
func (p *PSScanner) getPpid(pid int) (int, error) {
|
||||
if !p.enablePpid {
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
stat, err := readFile(fmt.Sprintf("/proc/%d/stat", pid), 512)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
if m := ppidRegex.FindStringSubmatch(string(stat)); m != nil {
|
||||
return strconv.Atoi(m[1])
|
||||
}
|
||||
return -1, errors.New("corrupt stat file")
|
||||
}
|
||||
|
||||
// no nonsense file reading
|
||||
func readFile(filename string, maxlen int) ([]byte, error) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,32 +1,44 @@
|
||||
package psscanner
|
||||
|
||||
import (
|
||||
//"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const timeout = 100 * time.Millisecond
|
||||
|
||||
// refresh
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pids []int
|
||||
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=2 | the-command",
|
||||
"UID=??? PID=1 | the-command",
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
restoreGetPIDs := mockPidList(tt.pids)
|
||||
restoreCmdLineReader := mockCmdLineReader([]byte("the-command"), nil)
|
||||
restoreProcStatusReader := mockProcStatusReader([]byte(""), nil) // don't mock read value since it's not worth it
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer mockPidList(tt.pids, t)()
|
||||
for _, pid := range tt.pids {
|
||||
defer mockPidCmdLine(pid, []byte("the-command"), nil, nil, t)()
|
||||
defer mockPidUid(pid, 0, errors.New("file not found"), t)()
|
||||
}
|
||||
|
||||
pss := NewPSScanner()
|
||||
pss := NewPSScanner(false, 2048)
|
||||
triggerCh := make(chan struct{})
|
||||
eventCh, errCh := pss.Run(triggerCh)
|
||||
|
||||
@@ -55,9 +67,605 @@ func TestRun(t *testing.T) {
|
||||
t.Errorf("Received unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
restoreProcStatusReader()
|
||||
restoreCmdLineReader()
|
||||
restoreGetPIDs()
|
||||
var (
|
||||
completeStat = []byte("1314 (some proc with) odd chars)) in name) R 5560 1314 5560 34821 1314 4194304 82 0 0 0 0 0 0 0 20 0 1 0 15047943 7790592 196 18446744073709551615 94260770430976 94260770462160 140725974097504 0 0 0 0 0 0 0 0 0 17 1 0 0 0 0 0 94260772559472 94260772561088 94260783992832 140725974106274 140725974106294 140725974106294 140725974110191 0\n")
|
||||
partialStat = []byte("1314 (ps) ")
|
||||
invalidPpid = []byte("1314 (ps) R XYZ 1314 5560 34821 1314 4194304 82 0 0 0 0 0 0 0 20 0 1 0 15047943 7790592 196 18446744073709551615 94260770430976 94260770462160 140725974097504 0 0 0 0 0 0 0 0 0 17 1 0 0 0 0 0 94260772559472 94260772561088 94260783992832 140725974106274 140725974106294 140725974106294 140725974110191 0\n")
|
||||
)
|
||||
|
||||
func TestProcessNewPid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
enablePpid bool
|
||||
truncate int
|
||||
pid int
|
||||
cmdLine []byte
|
||||
cmdLineErrRead error
|
||||
cmdLineErrOpen error
|
||||
stat []byte
|
||||
statErrRead error
|
||||
statErrOpen error
|
||||
lstatUid uint32
|
||||
lstatErr error
|
||||
expected PSEvent
|
||||
}{
|
||||
{
|
||||
name: "nominal-no-ppid",
|
||||
enablePpid: false,
|
||||
truncate: 100,
|
||||
pid: 1,
|
||||
cmdLine: []byte("abc\x00123"),
|
||||
cmdLineErrRead: nil,
|
||||
cmdLineErrOpen: nil,
|
||||
stat: completeStat,
|
||||
statErrRead: nil,
|
||||
statErrOpen: nil,
|
||||
lstatUid: 0,
|
||||
lstatErr: nil,
|
||||
expected: PSEvent{
|
||||
UID: 0,
|
||||
PID: 1,
|
||||
PPID: -1,
|
||||
CMD: "abc 123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nominal-ppid",
|
||||
enablePpid: true,
|
||||
truncate: 100,
|
||||
pid: 1,
|
||||
cmdLine: []byte("abc\x00123"),
|
||||
cmdLineErrRead: nil,
|
||||
cmdLineErrOpen: nil,
|
||||
stat: completeStat,
|
||||
statErrRead: nil,
|
||||
statErrOpen: nil,
|
||||
lstatUid: 999,
|
||||
lstatErr: nil,
|
||||
expected: PSEvent{
|
||||
UID: 999,
|
||||
PID: 1,
|
||||
PPID: 5560,
|
||||
CMD: "abc 123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty-cmd-ok",
|
||||
enablePpid: true,
|
||||
truncate: 100,
|
||||
pid: 1,
|
||||
cmdLine: []byte{},
|
||||
cmdLineErrRead: nil,
|
||||
cmdLineErrOpen: nil,
|
||||
stat: completeStat,
|
||||
statErrRead: nil,
|
||||
statErrOpen: nil,
|
||||
lstatUid: 0,
|
||||
lstatErr: nil,
|
||||
expected: PSEvent{
|
||||
UID: 0,
|
||||
PID: 1,
|
||||
PPID: 5560,
|
||||
CMD: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cmd-truncate",
|
||||
enablePpid: false,
|
||||
truncate: 10,
|
||||
pid: 1,
|
||||
cmdLine: []byte("abc\x00123\x00alpha"),
|
||||
cmdLineErrRead: nil,
|
||||
cmdLineErrOpen: nil,
|
||||
stat: completeStat,
|
||||
statErrRead: nil,
|
||||
statErrOpen: nil,
|
||||
lstatUid: 0,
|
||||
lstatErr: nil,
|
||||
expected: PSEvent{
|
||||
UID: 0,
|
||||
PID: 1,
|
||||
PPID: -1,
|
||||
CMD: "abc 123 al",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cmd-io-error",
|
||||
enablePpid: true,
|
||||
truncate: 100,
|
||||
pid: 2,
|
||||
cmdLine: nil,
|
||||
cmdLineErrRead: errors.New("file-system-error"),
|
||||
cmdLineErrOpen: nil,
|
||||
stat: completeStat,
|
||||
statErrRead: nil,
|
||||
statErrOpen: nil,
|
||||
lstatUid: 0,
|
||||
lstatErr: nil,
|
||||
expected: PSEvent{
|
||||
UID: 0,
|
||||
PID: 2,
|
||||
PPID: 5560,
|
||||
CMD: "???",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cmd-io-error2",
|
||||
enablePpid: true,
|
||||
truncate: 100,
|
||||
pid: 2,
|
||||
cmdLine: nil,
|
||||
cmdLineErrRead: nil,
|
||||
cmdLineErrOpen: errors.New("file-system-error"),
|
||||
stat: completeStat,
|
||||
statErrRead: nil,
|
||||
statErrOpen: nil,
|
||||
lstatUid: 0,
|
||||
lstatErr: nil,
|
||||
expected: PSEvent{
|
||||
UID: 0,
|
||||
PID: 2,
|
||||
PPID: 5560,
|
||||
CMD: "???",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stat-io-error",
|
||||
enablePpid: true,
|
||||
truncate: 100,
|
||||
pid: 2,
|
||||
cmdLine: []byte("some\x00cmd\x00123"),
|
||||
cmdLineErrRead: nil,
|
||||
cmdLineErrOpen: nil,
|
||||
stat: nil,
|
||||
statErrRead: errors.New("file-system-error"),
|
||||
statErrOpen: nil,
|
||||
lstatUid: 321,
|
||||
lstatErr: nil,
|
||||
expected: PSEvent{
|
||||
UID: 321,
|
||||
PID: 2,
|
||||
PPID: -1,
|
||||
CMD: "some cmd 123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stat-io-error2",
|
||||
enablePpid: true,
|
||||
truncate: 100,
|
||||
pid: 2,
|
||||
cmdLine: []byte("some\x00cmd\x00123"),
|
||||
cmdLineErrRead: nil,
|
||||
cmdLineErrOpen: nil,
|
||||
stat: nil,
|
||||
statErrRead: nil,
|
||||
statErrOpen: errors.New("file-system-error"),
|
||||
lstatUid: 4454,
|
||||
lstatErr: nil,
|
||||
expected: PSEvent{
|
||||
UID: 4454,
|
||||
PID: 2,
|
||||
PPID: -1,
|
||||
CMD: "some cmd 123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "lstat-fail",
|
||||
enablePpid: false,
|
||||
truncate: 100,
|
||||
pid: 3,
|
||||
cmdLine: []byte("some\x00cmd\x00123"),
|
||||
cmdLineErrRead: nil,
|
||||
cmdLineErrOpen: nil,
|
||||
stat: completeStat,
|
||||
statErrRead: nil,
|
||||
statErrOpen: nil,
|
||||
lstatUid: 0,
|
||||
lstatErr: errors.New("file not found"),
|
||||
expected: PSEvent{
|
||||
UID: -1,
|
||||
PID: 3,
|
||||
PPID: -1,
|
||||
CMD: "some cmd 123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "lstat-with-ppid",
|
||||
enablePpid: true,
|
||||
truncate: 100,
|
||||
pid: 3,
|
||||
cmdLine: []byte("some\x00cmd\x00123"),
|
||||
cmdLineErrRead: nil,
|
||||
cmdLineErrOpen: nil,
|
||||
stat: completeStat,
|
||||
statErrRead: nil,
|
||||
statErrOpen: nil,
|
||||
lstatUid: 0,
|
||||
lstatErr: errors.New("file not found"),
|
||||
expected: PSEvent{
|
||||
UID: -1,
|
||||
PID: 3,
|
||||
PPID: 5560,
|
||||
CMD: "some cmd 123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stat-too-short",
|
||||
enablePpid: true,
|
||||
truncate: 100,
|
||||
pid: 3,
|
||||
cmdLine: []byte("some\x00cmd\x00123"),
|
||||
cmdLineErrRead: nil,
|
||||
cmdLineErrOpen: nil,
|
||||
stat: partialStat,
|
||||
statErrRead: nil,
|
||||
statErrOpen: nil,
|
||||
lstatUid: 66,
|
||||
lstatErr: nil,
|
||||
expected: PSEvent{
|
||||
UID: 66,
|
||||
PID: 3,
|
||||
PPID: -1,
|
||||
CMD: "some cmd 123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stat-bad-ppid",
|
||||
enablePpid: true,
|
||||
truncate: 100,
|
||||
pid: 3,
|
||||
cmdLine: []byte("some\x00cmd\x00123"),
|
||||
cmdLineErrRead: nil,
|
||||
cmdLineErrOpen: nil,
|
||||
stat: invalidPpid,
|
||||
statErrRead: nil,
|
||||
statErrOpen: nil,
|
||||
lstatUid: 66,
|
||||
lstatErr: nil,
|
||||
expected: PSEvent{
|
||||
UID: 66,
|
||||
PID: 3,
|
||||
PPID: -1,
|
||||
CMD: "some cmd 123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stat-empty",
|
||||
enablePpid: true,
|
||||
truncate: 100,
|
||||
pid: 3,
|
||||
cmdLine: []byte("some\x00cmd\x00123"),
|
||||
cmdLineErrRead: nil,
|
||||
cmdLineErrOpen: nil,
|
||||
stat: []byte{},
|
||||
statErrRead: nil,
|
||||
statErrOpen: nil,
|
||||
lstatUid: 88,
|
||||
lstatErr: nil,
|
||||
expected: PSEvent{
|
||||
UID: 88,
|
||||
PID: 3,
|
||||
PPID: -1,
|
||||
CMD: "some cmd 123",
|
||||
},
|
||||
},
|
||||
/*{
|
||||
name: "uid-line-too-short",
|
||||
enablePpid: true,
|
||||
truncate: 100,
|
||||
pid: 3,
|
||||
cmdLine: []byte("some\x00cmd\x00123"),
|
||||
cmdLineErrRead: nil,
|
||||
cmdLineErrOpen: nil,
|
||||
stat: uidLineBroken,
|
||||
statErrRead: nil,
|
||||
statErrOpen: nil,
|
||||
lstatUid: 0,
|
||||
lstatErr: nil,
|
||||
expected: PSEvent{
|
||||
UID: -1,
|
||||
PID: 3,
|
||||
PPID: -1,
|
||||
CMD: "some cmd 123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "uid-parse-error",
|
||||
enablePpid: true,
|
||||
truncate: 100,
|
||||
pid: 3,
|
||||
cmdLine: []byte("some\x00cmd\x00123"),
|
||||
cmdLineErrRead: nil,
|
||||
cmdLineErrOpen: nil,
|
||||
stat: uidNaN,
|
||||
statErrRead: nil,
|
||||
statErrOpen: nil,
|
||||
lstatUid: 0,
|
||||
lstatErr: nil,
|
||||
expected: PSEvent{
|
||||
UID: -1,
|
||||
PID: 3,
|
||||
PPID: -1,
|
||||
CMD: "some cmd 123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ppid-line-too-short",
|
||||
enablePpid: true,
|
||||
truncate: 100,
|
||||
pid: 3,
|
||||
cmdLine: []byte("some\x00cmd\x00123"),
|
||||
cmdLineErrRead: nil,
|
||||
cmdLineErrOpen: nil,
|
||||
stat: ppidLineShort,
|
||||
statErrRead: nil,
|
||||
statErrOpen: nil,
|
||||
lstatUid: 0,
|
||||
lstatErr: nil,
|
||||
expected: PSEvent{
|
||||
UID: -1,
|
||||
PID: 3,
|
||||
PPID: -1,
|
||||
CMD: "some cmd 123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ppid-parse-error",
|
||||
enablePpid: true,
|
||||
truncate: 100,
|
||||
pid: 3,
|
||||
cmdLine: []byte("some\x00cmd\x00123"),
|
||||
cmdLineErrRead: nil,
|
||||
cmdLineErrOpen: nil,
|
||||
stat: ppidNaN,
|
||||
statErrRead: nil,
|
||||
statErrOpen: nil,
|
||||
lstatUid: 0,
|
||||
lstatErr: nil,
|
||||
expected: PSEvent{
|
||||
UID: -1,
|
||||
PID: 3,
|
||||
PPID: -1,
|
||||
CMD: "some cmd 123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no-ppid-line-too-short",
|
||||
enablePpid: false,
|
||||
truncate: 100,
|
||||
pid: 3,
|
||||
cmdLine: []byte("some\x00cmd\x00123"),
|
||||
cmdLineErrRead: nil,
|
||||
cmdLineErrOpen: nil,
|
||||
stat: ppidLineShort,
|
||||
statErrRead: nil,
|
||||
statErrOpen: nil,
|
||||
lstatUid: 0,
|
||||
lstatErr: nil,
|
||||
expected: PSEvent{
|
||||
UID: 0,
|
||||
PID: 3,
|
||||
PPID: -1,
|
||||
CMD: "some cmd 123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no-ppid-parse-error",
|
||||
enablePpid: false,
|
||||
truncate: 100,
|
||||
pid: 3,
|
||||
cmdLine: []byte("some\x00cmd\x00123"),
|
||||
cmdLineErrRead: nil,
|
||||
cmdLineErrOpen: nil,
|
||||
stat: ppidNaN,
|
||||
statErrRead: nil,
|
||||
statErrOpen: nil,
|
||||
lstatUid: 0,
|
||||
lstatErr: nil,
|
||||
expected: PSEvent{
|
||||
UID: 0,
|
||||
PID: 3,
|
||||
PPID: -1,
|
||||
CMD: "some cmd 123",
|
||||
},
|
||||
},*/
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer mockPidCmdLine(tt.pid, tt.cmdLine, tt.cmdLineErrRead, tt.cmdLineErrOpen, t)()
|
||||
defer mockPidStat(tt.pid, tt.stat, tt.statErrRead, tt.statErrOpen, t)()
|
||||
defer mockPidUid(tt.pid, tt.lstatUid, tt.lstatErr, t)()
|
||||
|
||||
results := make(chan PSEvent, 1)
|
||||
|
||||
scanner := &PSScanner{
|
||||
enablePpid: tt.enablePpid,
|
||||
eventCh: results,
|
||||
maxCmdLength: tt.truncate,
|
||||
}
|
||||
|
||||
go func() {
|
||||
scanner.processNewPid(tt.pid)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
t.Error("Timeout waiting for event")
|
||||
case event := <-results:
|
||||
close(results)
|
||||
if testing.Verbose() {
|
||||
t.Logf("received event: %#v", event)
|
||||
}
|
||||
if !reflect.DeepEqual(event, tt.expected) {
|
||||
t.Errorf("Event received but format is has unexpected values: got %#v but want %#v", event, tt.expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mockPidStat(pid int, stat []byte, errRead error, errOpen error, t *testing.T) func() {
|
||||
return mockFile(fmt.Sprintf("/proc/%d/stat", pid), stat, errRead, errOpen, t)
|
||||
}
|
||||
|
||||
func mockPidCmdLine(pid int, cmdline []byte, errRead error, errOpen error, t *testing.T) func() {
|
||||
return mockFile(fmt.Sprintf("/proc/%d/cmdline", pid), cmdline, errRead, errOpen, t)
|
||||
}
|
||||
|
||||
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() {
|
||||
open = oldopen
|
||||
}
|
||||
}
|
||||
|
||||
func mockPidUid(pid int, uid uint32, err error, t *testing.T) func() {
|
||||
return mockLStat(fmt.Sprintf("/proc/%d", pid), uid, err, t)
|
||||
}
|
||||
|
||||
func mockLStat(name string, uid uint32, err error, t *testing.T) func() {
|
||||
oldlstat := lstat
|
||||
lstat = func(path string, stat *syscall.Stat_t) error {
|
||||
if path == name {
|
||||
if testing.Verbose() {
|
||||
t.Logf("mocking lstat for %s", name)
|
||||
}
|
||||
stat.Uid = uid
|
||||
return err
|
||||
}
|
||||
return oldlstat(path, stat)
|
||||
}
|
||||
return func() {
|
||||
lstat = oldlstat
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPSScanner(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
ppid bool
|
||||
cmdlen int
|
||||
}{
|
||||
{
|
||||
name: "without-ppid",
|
||||
ppid: false,
|
||||
cmdlen: 30,
|
||||
},
|
||||
{
|
||||
name: "with-ppid",
|
||||
ppid: true,
|
||||
cmdlen: 5000,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
expected := &PSScanner{
|
||||
enablePpid: tt.ppid,
|
||||
eventCh: nil,
|
||||
maxCmdLength: tt.cmdlen,
|
||||
}
|
||||
new := NewPSScanner(tt.ppid, tt.cmdlen)
|
||||
|
||||
if !reflect.DeepEqual(new, expected) {
|
||||
t.Errorf("Unexpected scanner initialisation state: got %#v but want %#v", new, expected)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSEvent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
uid int
|
||||
pid int
|
||||
ppid int
|
||||
cmd string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "nominal-with-ppid",
|
||||
uid: 999,
|
||||
pid: 123,
|
||||
ppid: 321,
|
||||
cmd: "some cmd",
|
||||
expected: "UID=999 PID=123 PPID=321 | some cmd",
|
||||
},
|
||||
{
|
||||
name: "nominal-without-ppid",
|
||||
uid: 999,
|
||||
pid: 123,
|
||||
ppid: -1,
|
||||
cmd: "some cmd",
|
||||
expected: "UID=999 PID=123 | some cmd",
|
||||
},
|
||||
{
|
||||
name: "nocmd-without-ppid",
|
||||
uid: 999,
|
||||
pid: 123,
|
||||
ppid: -1,
|
||||
cmd: "",
|
||||
expected: "UID=999 PID=123 | ",
|
||||
},
|
||||
{
|
||||
name: "nocmd-with-ppid",
|
||||
uid: 999,
|
||||
pid: 123,
|
||||
ppid: 321,
|
||||
cmd: "",
|
||||
expected: "UID=999 PID=123 PPID=321 | ",
|
||||
},
|
||||
{
|
||||
name: "nouid",
|
||||
uid: -1,
|
||||
pid: 123,
|
||||
ppid: 321,
|
||||
cmd: "some cmd",
|
||||
expected: "UID=??? PID=123 PPID=321 | some cmd",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ps := PSEvent{
|
||||
UID: tt.uid,
|
||||
PID: tt.pid,
|
||||
PPID: tt.ppid,
|
||||
CMD: tt.cmd,
|
||||
}
|
||||
if ps.String() != tt.expected {
|
||||
t.Errorf("Expecting \"%s\", got \"%s\"", tt.expected, ps.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user