From 497e87dea758811300b7fd91ec0bdd2ebd31ef0e Mon Sep 17 00:00:00 2001 From: Karim Kanso Date: Tue, 10 Mar 2020 13:37:40 +0000 Subject: [PATCH] Added --ppid command line option that includes the parent process identifier. Also included small refactoring in PSScanner module to allow for parameter to be passed. --- cmd/root.go | 4 +- internal/pspy/pspy_test.go | 4 +- internal/psscanner/proclist.go | 70 +---- internal/psscanner/proclist_test.go | 196 ++++--------- internal/psscanner/psscanner.go | 100 ++++++- internal/psscanner/psscanner_test.go | 418 ++++++++++++++++++++++++++- 6 files changed, 576 insertions(+), 216 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 71856ba..63d26dc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -61,6 +61,7 @@ var defaultDirs = []string{} var triggerInterval int var colored bool var debug bool +var ppid bool func init() { rootCmd.PersistentFlags().BoolVarP(&logPS, "procevents", "p", true, "print new processes to stdout") @@ -70,6 +71,7 @@ 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") log.SetOutput(os.Stdout) } @@ -91,7 +93,7 @@ func root(cmd *cobra.Command, args []string) { fsw := fswatcher.NewFSWatcher() defer fsw.Close() - pss := psscanner.NewPSScanner() + pss := psscanner.NewPSScanner(ppid) sigCh := make(chan os.Signal) signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) diff --git a/internal/pspy/pspy_test.go b/internal/pspy/pspy_test.go index f66aefe..7141d6c 100644 --- a/internal/pspy/pspy_test.go +++ b/internal/pspy/pspy_test.go @@ -95,7 +95,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 +108,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") diff --git a/internal/psscanner/proclist.go b/internal/psscanner/proclist.go index 0de01fa..7211c69 100644 --- a/internal/psscanner/proclist.go +++ b/internal/psscanner/proclist.go @@ -6,26 +6,19 @@ import ( "io/ioutil" "os" "strconv" - "strings" ) var procDirReader = func() ([]os.FileInfo, error) { return ioutil.ReadDir("/proc") } -var procStatusReader = func(pid int) ([]byte, error) { - statPath := fmt.Sprintf("/proc/%d/status", pid) - return ioutil.ReadFile(statPath) +type procList map[int]struct{} + +type pidProcessor interface { + processNewPid(pid int) } -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 +28,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{}{} } } @@ -73,53 +67,3 @@ func file2Pid(f os.FileInfo) (int, error) { 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 -} diff --git a/internal/psscanner/proclist_test.go b/internal/psscanner/proclist_test.go index 6761244..0a147d2 100644 --- a/internal/psscanner/proclist_test.go +++ b/internal/psscanner/proclist_test.go @@ -1,54 +1,15 @@ 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) { @@ -126,117 +87,82 @@ func (f *mockFileInfo) Sys() interface{} { 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 - } - - 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() - } +type mockPidProcessor struct { + t *testing.T + pids []int } -func mockProcStatusReader(stat []byte, err error) (restore func()) { - oldFunc := procStatusReader - procStatusReader = func(pid int) ([]byte, error) { - return stat, err - } - return func() { - procStatusReader = oldFunc +func (m *mockPidProcessor) processNewPid(pid int) { + if testing.Verbose() { + m.t.Logf("proc %d processed", pid) } + m.pids = append(m.pids, pid) } -// refresh +var unit = struct{}{} func TestRefresh(t *testing.T) { tests := []struct { - eventCh chan PSEvent - pl procList - newPids []int - pidsAfter []int - events []string + name string + pl procList + newPids []int + 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)() - events := make([]string, 0) - done := make(chan struct{}) - go func() { - for e := range tt.eventCh { - events = append(events, e.String()) + m := &mockPidProcessor{t, []int{}} + tt.pl.refresh(m) + + if !reflect.DeepEqual(m.pids, tt.pidsProcessed) { + t.Errorf("Unexpected pids got processed: got %v but want %v", m.pids, tt.pidsProcessed) } - done <- struct{}{} - }() - tt.pl.refresh(tt.eventCh) - close(tt.eventCh) - <-done - - 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(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") + defer mockProcDirReader([]os.FileInfo{}, e)() + 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() { @@ -247,11 +173,3 @@ func mockPidList(pids []int) func() { 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 -} diff --git a/internal/psscanner/psscanner.go b/internal/psscanner/psscanner.go index 4fb9dda..7f7ba14 100644 --- a/internal/psscanner/psscanner.go +++ b/internal/psscanner/psscanner.go @@ -2,15 +2,21 @@ package psscanner import ( "fmt" + "io/ioutil" "strconv" + "strings" ) -type PSScanner struct{} +type PSScanner struct { + enablePpid bool + eventCh chan<- PSEvent +} type PSEvent struct { - UID int - PID int - CMD string + UID int + PID int + PPID int + CMD string } func (evt PSEvent) String() string { @@ -19,23 +25,101 @@ 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=%-4s PID=%-6d | %s", uid, evt.PID, evt.CMD) + } + + return fmt.Sprintf( + "UID=%-4s PID=%-6d PPID=%-6d | %s", uid, evt.PID, evt.PPID, evt.CMD) } -func NewPSScanner() *PSScanner { - return &PSScanner{} +func NewPSScanner(ppid bool) *PSScanner { + return &PSScanner{ + enablePpid: ppid, + eventCh: nil, + } } 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) { + // quickly load data into memory before processing it, with preferance for cmd + cmdLine, errCmdLine := cmdLineReader(pid) + status, errStatus := procStatusReader(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, ppid := -1, -1 + if errStatus == nil { + uid, ppid, errStatus = p.parseProcessStatus(status) + if errStatus != nil { + uid = -1 + ppid = -1 + } + } + + p.eventCh <- PSEvent{UID: uid, PID: pid, PPID: ppid, CMD: cmd} +} + +func (p *PSScanner) parseProcessStatus(status []byte) (int, int, error) { + lines := strings.Split(string(status), "\n") + if len(lines) < 9 { + return -1, -1, fmt.Errorf("no uid information") + } + + uidL := strings.Split(lines[8], "\t") + if len(uidL) < 2 { + return -1, -1, fmt.Errorf("uid line read incomplete") + } + + uid, err := strconv.Atoi(uidL[1]) + if err != nil { + return -1, -1, fmt.Errorf("converting %s to int: %v", uidL[1], err) + } + + ppid := -1 + if p.enablePpid { + ppidL := strings.Split(lines[6], "\t") + if len(ppidL) < 2 { + return -1, -1, fmt.Errorf("ppid line read incomplete") + } + + ppid, err = strconv.Atoi(ppidL[1]) + if err != nil { + return -1, -1, fmt.Errorf("converting %s to int: %v", ppidL[1], err) + } + } + + return uid, ppid, nil +} + +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) +} diff --git a/internal/psscanner/psscanner_test.go b/internal/psscanner/psscanner_test.go index 773cff8..98dc76b 100644 --- a/internal/psscanner/psscanner_test.go +++ b/internal/psscanner/psscanner_test.go @@ -1,14 +1,15 @@ package psscanner import ( + "encoding/hex" + "errors" + "reflect" "testing" "time" ) const timeout = 100 * time.Millisecond -// refresh - func TestRun(t *testing.T) { tests := []struct { pids []int @@ -26,7 +27,7 @@ func TestRun(t *testing.T) { restoreCmdLineReader := mockCmdLineReader([]byte("the-command"), nil) restoreProcStatusReader := mockProcStatusReader([]byte(""), nil) // don't mock read value since it's not worth it - pss := NewPSScanner() + pss := NewPSScanner(false) triggerCh := make(chan struct{}) eventCh, errCh := pss.Run(triggerCh) @@ -61,3 +62,414 @@ func TestRun(t *testing.T) { restoreGetPIDs() } } + +var completeStatus, _ = hex.DecodeString("4e616d653a0963726f6e0a556d6" + + "1736b3a09303032320a53746174653a09532028736c656570696e67290a5" + + "46769643a09370a4e6769643a09300a5069643a09370a505069643a09350" + + "a5472616365725069643a09300a5569643a09300930093009300a4769643" + + "a09300930093009300a464453697a653a0936340a47726f7570733a09302" + + "00a4e53746769643a09370a4e537069643a09370a4e53706769643a09310" + + "a4e537369643a09310a566d5065616b3a092020203238303132206b420a5" + + "66d53697a653a092020203237393932206b420a566d4c636b3a092020202" + + "020202030206b420a566d50696e3a092020202020202030206b420a566d4" + + "8574d3a092020202032333532206b420a566d5253533a092020202032333" + + "532206b420a527373416e6f6e3a092020202020323430206b420a5273734" + + "6696c653a092020202032313132206b420a52737353686d656d3a0920202" + + "02020202030206b420a566d446174613a092020202020333430206b420a5" + + "66d53746b3a092020202020313332206b420a566d4578653a09202020202" + + "0203434206b420a566d4c69623a092020202032383536206b420a566d505" + + "4453a092020202020203736206b420a566d504d443a09202020202020313" + + "2206b420a566d537761703a092020202020202030206b420a48756765746" + + "c6250616765733a092020202020202030206b420a546872656164733a093" + + "10a536967513a09302f34373834320a536967506e643a093030303030303" + + "03030303030303030300a536864506e643a0930303030303030303030303" + + "0303030300a536967426c6b3a09303030303030303030303030303030300" + + "a53696749676e3a09303030303030303030303030303030360a536967436" + + "7743a09303030303030303138303031303030310a436170496e683a09303" + + "030303030303061383034323566620a43617050726d3a093030303030303" + + "03061383034323566620a4361704566663a0930303030303030306138303" + + "4323566620a436170426e643a09303030303030303061383034323566620" + + "a436170416d623a09303030303030303030303030303030300a536563636" + + "f6d703a09320a437075735f616c6c6f7765643a09330a437075735f616c6" + + "c6f7765645f6c6973743a09302d310a4d656d735f616c6c6f7765643a093" + + "10a4d656d735f616c6c6f7765645f6c6973743a09300a766f6c756e74617" + + "2795f637478745f73776974636865733a0932350a6e6f6e766f6c756e746" + + "172795f637478745f73776974636865733a09310a") + +var uidLineBroken, _ = hex.DecodeString("4e616d653a0963726f6e0a556d61" + + "736b3a09303032320a53746174653a09532028736c656570696e67290a54" + + "6769643a09370a4e6769643a09300a5069643a09370a505069643a09350a" + + "5472616365725069643a09300a5569643a") + +var uidNaN, _ = hex.DecodeString("4e616d653a0963726f6e0a556d61736b3a0" + + "9303032320a53746174653a09532028736c656570696e67290a546769643" + + "a09370a4e6769643a09300a5069643a09370a505069643a09350a5472616" + + "365725069643a09300a5569643a0964") + +var ppidLineShort, _ = hex.DecodeString("4e616d653a0963726f6e0a556d61" + + "736b3a09303032320a53746174653a09532028736c656570696e67290a54" + + "6769643a09370a4e6769643a09300a5069643a09370a505069643a0a5472" + + "616365725069643a09300a5569643a09300a") + +var ppidNaN, _ = hex.DecodeString("4e616d653a0963726f6e0a556d61736b3a" + + "09303032320a53746174653a09532028736c656570696e67290a54676964" + + "3a09370a4e6769643a09300a5069643a09370a505069643a0955450a5472" + + "616365725069643a09300a5569643a09300a") + +var notEnoughLines, _ = hex.DecodeString( + "4e616d653a0963726f6e0a556d61736b3a09303032320a537461") + +func TestProcessNewPid(t *testing.T) { + tests := []struct { + name string + enablePpid bool + pid int + cmdLine []byte + cmdLineErr error + status []byte + statusErr error + expected PSEvent + }{ + { + name: "nominal-no-ppid", + enablePpid: false, + pid: 1, + cmdLine: []byte("abc\x00123"), + cmdLineErr: nil, + status: completeStatus, + statusErr: nil, + expected: PSEvent{ + UID: 0, + PID: 1, + PPID: -1, + CMD: "abc 123", + }, + }, + { + name: "nominal-ppid", + enablePpid: true, + pid: 1, + cmdLine: []byte("abc\x00123"), + cmdLineErr: nil, + status: completeStatus, + statusErr: nil, + expected: PSEvent{ + UID: 0, + PID: 1, + PPID: 5, + CMD: "abc 123", + }, + }, + { + name: "empty-cmd-ok", + enablePpid: true, + pid: 1, + cmdLine: []byte{}, + cmdLineErr: nil, + status: completeStatus, + statusErr: nil, + expected: PSEvent{ + UID: 0, + PID: 1, + PPID: 5, + CMD: "", + }, + }, + { + name: "cmd-io-error", + enablePpid: true, + pid: 2, + cmdLine: nil, + cmdLineErr: errors.New("file-system-error"), + status: completeStatus, + statusErr: nil, + expected: PSEvent{ + UID: 0, + PID: 2, + PPID: 5, + CMD: "???", + }, + }, + { + name: "status-io-error", + enablePpid: true, + pid: 2, + cmdLine: []byte("some\x00cmd\x00123"), + cmdLineErr: nil, + status: nil, + statusErr: errors.New("file-system-error"), + expected: PSEvent{ + UID: -1, + PID: 2, + PPID: -1, + CMD: "some cmd 123", + }, + }, + { + name: "status-too-short", + enablePpid: true, + pid: 3, + cmdLine: []byte("some\x00cmd\x00123"), + cmdLineErr: nil, + status: notEnoughLines, + statusErr: nil, + expected: PSEvent{ + UID: -1, + PID: 3, + PPID: -1, + CMD: "some cmd 123", + }, + }, + { + name: "status-empty", + enablePpid: true, + pid: 3, + cmdLine: []byte("some\x00cmd\x00123"), + cmdLineErr: nil, + status: []byte{}, + statusErr: nil, + expected: PSEvent{ + UID: -1, + PID: 3, + PPID: -1, + CMD: "some cmd 123", + }, + }, + { + name: "uid-line-too-short", + enablePpid: true, + pid: 3, + cmdLine: []byte("some\x00cmd\x00123"), + cmdLineErr: nil, + status: uidLineBroken, + statusErr: nil, + expected: PSEvent{ + UID: -1, + PID: 3, + PPID: -1, + CMD: "some cmd 123", + }, + }, + { + name: "uid-parse-error", + enablePpid: true, + pid: 3, + cmdLine: []byte("some\x00cmd\x00123"), + cmdLineErr: nil, + status: uidNaN, + statusErr: nil, + expected: PSEvent{ + UID: -1, + PID: 3, + PPID: -1, + CMD: "some cmd 123", + }, + }, + { + name: "ppid-line-too-short", + enablePpid: true, + pid: 3, + cmdLine: []byte("some\x00cmd\x00123"), + cmdLineErr: nil, + status: ppidLineShort, + statusErr: nil, + expected: PSEvent{ + UID: -1, + PID: 3, + PPID: -1, + CMD: "some cmd 123", + }, + }, + { + name: "ppid-parse-error", + enablePpid: true, + pid: 3, + cmdLine: []byte("some\x00cmd\x00123"), + cmdLineErr: nil, + status: ppidNaN, + statusErr: nil, + expected: PSEvent{ + UID: -1, + PID: 3, + PPID: -1, + CMD: "some cmd 123", + }, + }, + { + name: "no-ppid-line-too-short", + enablePpid: false, + pid: 3, + cmdLine: []byte("some\x00cmd\x00123"), + cmdLineErr: nil, + status: ppidLineShort, + statusErr: nil, + expected: PSEvent{ + UID: 0, + PID: 3, + PPID: -1, + CMD: "some cmd 123", + }, + }, + { + name: "no-ppid-parse-error", + enablePpid: false, + pid: 3, + cmdLine: []byte("some\x00cmd\x00123"), + cmdLineErr: nil, + status: ppidNaN, + statusErr: 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 mockCmdLineReader(tt.cmdLine, tt.cmdLineErr)() + defer mockProcStatusReader(tt.status, tt.statusErr)() + + results := make(chan PSEvent, 1) + + scanner := &PSScanner{ + enablePpid: tt.enablePpid, + eventCh: results, + } + + 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 mockCmdLineReader(cmdLine []byte, err error) (restore func()) { + oldFunc := cmdLineReader + cmdLineReader = func(pid int) ([]byte, error) { + return cmdLine, err + } + return func() { + cmdLineReader = oldFunc + } +} + +func mockProcStatusReader(stat []byte, err error) (restore func()) { + oldFunc := procStatusReader + procStatusReader = func(pid int) ([]byte, error) { + return stat, err + } + return func() { + procStatusReader = oldFunc + } +} + +func TestNewPSScanner(t *testing.T) { + for _, tt := range []struct { + name string + ppid bool + }{ + { + name: "without-ppid", + ppid: false, + }, + { + name: "with-ppid", + ppid: true, + }, + } { + t.Run(tt.name, func(t *testing.T) { + expected := &PSScanner{ + enablePpid: tt.ppid, + eventCh: nil, + } + new := NewPSScanner(tt.ppid) + + 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()) + } + }) + } +}